服务端原型污染不回显怎么办?这三个技巧能帮你拿下赏金

admin 2026-05-22 03:31:48 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 本文介绍服务端原型污染无回显场景下的三种无损探测技巧:状态码覆盖、JSON空格覆盖、字符集覆盖。通过注入特定属性对比服务器行为变化验证漏洞,方法安全可控,适用于Express等Node框架漏洞挖掘,为安全测试提供实用思路。 综合评分: 86 文章分类: 漏洞分析,渗透测试,WEB安全,代码审计,安全工具


cover_image

服务端原型污染不回显怎么办?这三个技巧能帮你拿下赏金

原创

升斗安全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&nbsp;createError&nbsp;() {&nbsp; &nbsp;&nbsp;if&nbsp;(type&nbsp;===&nbsp;'object'&nbsp;&& arg&nbsp;instanceof&nbsp;Error) {&nbsp; &nbsp; &nbsp; &nbsp; err = arg&nbsp; &nbsp; &nbsp; &nbsp; status = err.status&nbsp;|| err.statusCode&nbsp;|| status&nbsp; &nbsp; }&nbsp;else&nbsp;if&nbsp;(type&nbsp;===&nbsp;'number'&nbsp;&& i ===&nbsp;0) {&nbsp; &nbsp;&nbsp;if&nbsp;(typeof&nbsp;status !==&nbsp;'number'&nbsp;||&nbsp; &nbsp; (!statuses.message[status] && (status <&nbsp;400&nbsp;|| status >=&nbsp;600))) {&nbsp; &nbsp; &nbsp; &nbsp; status =&nbsp;500&nbsp; &nbsp; }

注意第一个高亮行:如果传入的参数是 Error 对象,函数会去读 err.status 或 err.statusCode 来决定最终状态码。开发者要是没在错误对象里显式设置这两个属性,机会就来了!它就会顺着原型链往上找。

利用步骤:

  1. 找到一个能触发错误响应的地方,记住默认返回的状态码。
  2. 用一个冷门的状态码(422、451 这种)去污染原型的 status 属性。
  3. 再次触发同一个错误,看状态码有没有变成你设的那个值。

有个硬性要求:状态码必须在 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&nbsp;charset =&nbsp;getCharset(req)&nbsp;or&nbsp;'utf-8'function&nbsp;getCharset&nbsp;(req)&nbsp;{&nbsp; &nbsp;&nbsp;try&nbsp;{&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;(contentType.parse(req).parameters.charset ||&nbsp;'').toLowerCase()&nbsp; &nbsp; }&nbsp;catch&nbsp;(e) {&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;undefined&nbsp; &nbsp; }}read(req, res, next, parse, debug, {&nbsp;&nbsp;&nbsp; encoding: charset,&nbsp;&nbsp;&nbsp; inflate: inflate,&nbsp;&nbsp;&nbsp; limit: limit,&nbsp;&nbsp;&nbsp; verify: verify})

仔细看 getCharset() 这个函数。开发者显然考虑到了 Content-Type 头里可能不写 charset,所以做了个 fallback 处理——解析不到就返回空字符串 ”。问题恰恰出在这里:既然允许 fallback 到空字符串,那就意味着最终的行为可以通过原型污染来控制。

如果你能找到响应中会原样回显的字段,就能用它来当“探测工具”。下面用 UTF-7 编码配合 JSON 源来演示:

第一步:找一个响应里会原样返回的属性,塞入一段 UTF-7 编码的字符串。比如 foo 用 UTF-7 编码后是 +AGYAbwBv-:

{&nbsp; &nbsp;&nbsp;"sessionId":"0123456789",&nbsp; &nbsp;&nbsp;"username":"wiener",&nbsp; &nbsp;&nbsp;"role":"+AGYAbwBv-"}

发送请求,正常情况服务器不会用 UTF-7 去解析,响应里你看到的应该还是那段“乱码”。

第二步:尝试用指定了 UTF-7 字符集的 content-type 属性去污染原型:

{&nbsp; &nbsp; "sessionId":"0123456789",&nbsp; &nbsp;&nbsp;"username":"wiener",&nbsp; &nbsp;&nbsp;"role":"default",&nbsp; &nbsp;&nbsp;"__proto__":{&nbsp; &nbsp; &nbsp; &nbsp; "content-type":&nbsp;"application/json; charset=utf-7"&nbsp; &nbsp; }}

第三步:重复第一步的请求。如果原型污染成功,服务器就会用 UTF-7 来解析,那段“乱码”会被正确解码:

{&nbsp; &nbsp;&nbsp;"sessionId":"0123456789",&nbsp; &nbsp;&nbsp;"username":"wiener",&nbsp; &nbsp;&nbsp;"role":"foo"}

看到 foo 就说明污染生效了。

这背后还有个底层机制在“助攻”。Node 的 _http_incoming 模块在处理请求头时,为了避免重复头部覆盖已有属性,_addHeaderLine() 函数会先检查目标对象上是不是已经存在同名字段:

IncomingMessage.prototype._addHeaderLine&nbsp;= _addHeaderLine;function&nbsp;_addHeaderLine(field, value, dest) {&nbsp; &nbsp;&nbsp;// ...&nbsp; &nbsp; }&nbsp;else&nbsp;if&nbsp;(dest[field] ===&nbsp;undefined) {&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// 丢弃重复项&nbsp; &nbsp; &nbsp; &nbsp; dest[field] = value;&nbsp; &nbsp; }}

关键点来了:这个存在性检查 dest[field] === undefined 会顺着原型链往上找。也就是说,如果你已经用 content-type 污染了原型,那请求里真正带的 Content-Type 头反而因为“目标对象上已有该属性”被直接丢弃,你的污染值会取而代之生效。这算是一个意料之外的“便利条件”了。

以上三种方法总结下来就是:不动声色地改变服务器行为,通过对比差异来确认漏洞存在。不需要破坏性操作,风险低、干扰小,特别适合在没有显式回显的场景下验证服务端原型污染。

读到这儿觉得有收获?点个“在看”分享给一起挖洞的兄弟。你手里有没有更骚的探测姿势?欢迎到后台留言聊聊,实战案例越多越好,一起把技术玩透。


免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我

本文转载自:升斗安全 升斗安全XiuXiu 升斗安全XiuXiu《服务端原型污染不回显怎么办?这三个技巧能帮你拿下赏金》

评论:0   参与:  0