文章总结: 本文介绍服务端原型污染无回显场景下的三种无损探测技巧:状态码覆盖、JSON空格覆盖、字符集覆盖。通过注入特定属性对比服务器行为变化验证漏洞,方法安全可控,适用于Express等Node框架漏洞挖掘,为安全测试提供实用思路。 综合评分: 86 文章分类: 漏洞分析,渗透测试,WEB安全,代码审计,安全工具
服务端原型污染不回显怎么办?这三个技巧能帮你拿下赏金
原创
升斗安全XiuXiu 升斗安全XiuXiu
升斗安全
2026年5月20日 08:18 广东
在小说阅读器读本章
去阅读
【文章说明】
- 目的:本文内容仅为网络安全技术研究与教育目的而创作。
- 红线:严禁将本文知识用于任何未授权的非法活动。使用者必须遵守《网络安全法》等相关法律。
- 责任:任何对本文技术的滥用所引发的后果自负,与本公众号及作者无关。
- 免责:内容仅供参考,作者不对其准确性、完整性作任何担保。
阅读即代表您同意以上条款。
在挖服务端原型污染漏洞时,经常会碰到一种让人头疼的情况:明明感觉已经污染成功了,服务器响应里却啥也看不出来。毕竟我们没法像浏览器控制台那样直接打印对象来确认,怎么判断到底污染了没?
思路其实很简单,既然看不到回显,那就让它“动起来”。尝试注入一些可能与服务器配置相关的属性,然后对比注入前后服务器的行为变化。如果某些配置明显被改变了,那基本就能断定:你发现了一个服务端原型污染漏洞。
这次我们聊三种无损探测手法,即便污染成功也不会搞崩服务器,但行为变化足够明显,用来验证漏洞非常稳。
一、状态码覆盖
Express 这类 Node 框架允许开发者自定义 HTTP 响应状态码。有时你会碰到这种情况:
HTTP/1.1 200 OK...{ "error": { "success": false, "status": 401, "message": "You do not have permission to access this resource." }}
明明 HTTP 状态行是 200 OK,响应体里的 JSON 却藏着一个 401,告诉你“没权限”。这种“表里不一”的返回在实战中相当常见。
Node 的 http-errors 模块在生成这类错误响应时,核心逻辑大致是这样的:
function createError () { if (type === 'object' && arg instanceof Error) { err = arg status = err.status || err.statusCode || status } else if (type === 'number' && i === 0) { if (typeof status !== 'number' || (!statuses.message[status] && (status < 400 || status >= 600))) { status = 500 }
注意第一个高亮行:如果传入的参数是 Error 对象,函数会去读 err.status 或 err.statusCode 来决定最终状态码。开发者要是没在错误对象里显式设置这两个属性,机会就来了!它就会顺着原型链往上找。
利用步骤:
- 找到一个能触发错误响应的地方,记住默认返回的状态码。
- 用一个冷门的状态码(422、451 这种)去污染原型的 status 属性。
- 再次触发同一个错误,看状态码有没有变成你设的那个值。
有个硬性要求:状态码必须在 400-599 这个区间内。你看第二个高亮行的逻辑——超出范围的话,Node 会直接兜底返回 500,到时候根本分不清是污染生效了还是被强制覆盖了。
二、JSON 空格覆盖
Express 框架有个 json spaces 选项,用来控制返回 JSON 时每个缩进占几个空格。很多开发者对默认缩进挺满意,压根不去显式设置这个属性,让它一直处于 undefined 状态,这就完美落入了原型污染的射程。
只要你能访问到任意返回 JSON 数据的接口,就可以试试:先用自定义的 json spaces 值污染原型,再重新请求一次,观察 JSON 缩进有没有跟着变宽。想验证得更踏实,也可以反过来把缩进去掉再观察。
这招有两个明显优势:
一是不依赖某个特定字段是否在响应里回显;
二是极其安全,把属性值改回原来的缩进量就恢复如初,跟开关一样可控。
Express 在 4.17.4 版本已经修了这个问题,但没升级的站点一抓一大把,该试还是得试。
另外提醒一下:用 Burp 测这个的时候,务必切到 Raw 标签页去看响应。默认的美化视图会自动把缩进统一处理,等于你在白测。
三、字符集覆盖
Express 服务器通常会挂载一些“中间件”来预处理请求。比如 body-parser 模块,它的活儿就是解析请求体,给你生成 req.body 对象。它内部把请求体丢给 read() 函数处理时,会传入一个 options 对象,里面有个 encoding 选项来决定用哪种字符编码:
var charset = getCharset(req) or 'utf-8'function getCharset (req) { try { return (contentType.parse(req).parameters.charset || '').toLowerCase() } catch (e) { return undefined }}read(req, res, next, parse, debug, { encoding: charset, inflate: inflate, limit: limit, verify: verify})
仔细看 getCharset() 这个函数。开发者显然考虑到了 Content-Type 头里可能不写 charset,所以做了个 fallback 处理——解析不到就返回空字符串 ”。问题恰恰出在这里:既然允许 fallback 到空字符串,那就意味着最终的行为可以通过原型污染来控制。
如果你能找到响应中会原样回显的字段,就能用它来当“探测工具”。下面用 UTF-7 编码配合 JSON 源来演示:
第一步:找一个响应里会原样返回的属性,塞入一段 UTF-7 编码的字符串。比如 foo 用 UTF-7 编码后是 +AGYAbwBv-:
{ "sessionId":"0123456789", "username":"wiener", "role":"+AGYAbwBv-"}
发送请求,正常情况服务器不会用 UTF-7 去解析,响应里你看到的应该还是那段“乱码”。
第二步:尝试用指定了 UTF-7 字符集的 content-type 属性去污染原型:
{ "sessionId":"0123456789", "username":"wiener", "role":"default", "__proto__":{ "content-type": "application/json; charset=utf-7" }}
第三步:重复第一步的请求。如果原型污染成功,服务器就会用 UTF-7 来解析,那段“乱码”会被正确解码:
{ "sessionId":"0123456789", "username":"wiener", "role":"foo"}
看到 foo 就说明污染生效了。
这背后还有个底层机制在“助攻”。Node 的 _http_incoming 模块在处理请求头时,为了避免重复头部覆盖已有属性,_addHeaderLine() 函数会先检查目标对象上是不是已经存在同名字段:
IncomingMessage.prototype._addHeaderLine = _addHeaderLine;function _addHeaderLine(field, value, dest) { // ... } else if (dest[field] === undefined) { // 丢弃重复项 dest[field] = value; }}
关键点来了:这个存在性检查 dest[field] === undefined 会顺着原型链往上找。也就是说,如果你已经用 content-type 污染了原型,那请求里真正带的 Content-Type 头反而因为“目标对象上已有该属性”被直接丢弃,你的污染值会取而代之生效。这算是一个意料之外的“便利条件”了。
以上三种方法总结下来就是:不动声色地改变服务器行为,通过对比差异来确认漏洞存在。不需要破坏性操作,风险低、干扰小,特别适合在没有显式回显的场景下验证服务端原型污染。
读到这儿觉得有收获?点个“在看”分享给一起挖洞的兄弟。你手里有没有更骚的探测姿势?欢迎到后台留言聊聊,实战案例越多越好,一起把技术玩透。
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:升斗安全 升斗安全XiuXiu 升斗安全XiuXiu《服务端原型污染不回显怎么办?这三个技巧能帮你拿下赏金》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。










评论