js原型链污染浅析

📅 发布时间:2026/8/19 14:50:33
js原型链污染浅析
Nodejsnode.js就是运行在服务端的javascriptNodejs启动http服务var http require(http); http.createServer(function (request,response){ //发送HTTP头部 //HTTP状态码:200 //内容类型:text/plain response.writeHead(200,{Content-Type:text/plain}); //发送响应数据:Hello Wrold response.end(Hello World\n); }).listen(8888); //终端打印如下信息 console.log(Server running at http://127.0.0.1:8888);require导入http模块createServer方法创建了一个http服务。使用node js代码运行该代码文件。然后访问127.0.0.1:8888接可以看到对应的服务。Nodejs的缺点大小写变换toUpperCase(): 将小写字母的字符转换成大写如果是其他字符则原字符不变。toLowerCase(): 将大写字母的字符转换成小写如果是其他字符则原字符不变。其中有一些类编码本质是ascii混淆比如说a的ascii码是97但是由其他的特殊字符会被解析成ascii码是97的字符最终解析为a但是实际上他们的二进制编码不一样。但是js的宽松处理造成了绕过。弱类型比较数字与字符串比较时会优先将纯数字型字符串转为数字之后再进行比较而字符串之间比较时会将字符串的第一个字符转为ASCII之后再比较因此像第五行这种情况就会出现非数字型字符串与任何数字比较都是false空数组之间比较永远是false数组之间比较只比较数组间的第一个值对第一个值转ASCII然后比较数组与非数值型字符串比较数组永远小于非数值型字符串数组与数值型字符串比较取第一个然后还是转ASCII然后比较。补充一个console.log([]![]) //输出true任何对象 在逻辑判断里都是 true因为[]是一个对象对象的值为true而!true就是flase所以原始式子就是[]flase。此时 [] 会转换成而flase转换成0最终式子为0不知道原理就直接控制台NaN 无效数字无效 无效标准规定NaN 不等于任何东西包括自己所以NaN NaN → false其实最简单的方法就是直接控制台里面输入consolo.log(比较式子)然后看最后的输出。md5绕过假如有这样一个过滤a b a.lengthb.length a!b md5(aflag) md5(bflag)我们可以使用定义对象来绕过例如a{x:1} b{x:2}它会被解析为{object,Object}也可以使用a [] b [,]数组绕过编码绕过16进制编码console.log(a\x61);//trueUnicode编码console.log(a\u0061);//truebase64编码console.log(Buffer.from(dGVzdA,base64).toString())//输出结果是test。危险函数exec() //执行系统指令 require(child_process).exec(calc); //弹出本地计算器 eval() //执行代码 console.log(eval(document.cookie));//执行document.cookie console.log(document.cookie);//输出document.cookie writeFileSync() require(fs).writeFileSync(input.txt,sss); //导入fs 要写入input.txt,内容是sss的字符串 //同步执行 代码会卡住直到文件写完才继续执行下面的代码 //无回调出错会直接抛出异常 writeFile() require(fs).writeFile(input.txt,test,(err){ //回调函数在完成后再执行比如这里可以直接写console.log(111) //如果这个地方后面你还有需要执行的代码写在异步后面那么就算这一步无法执行也不影响后面的代码执行。 }) //文件读取也是一样的 //readFile() require(fs).readFile(/etc/passwd,utf-8,(err,data) { if(err)throw err; console.log(data); }) //readFileSync() require(fs).readFile(/etc/passwd,utf-8)js创建对象的方式//普通方式 var a {name:pan,age:19}; ​ //构造函数方式 function a(){ this.name pan; this.age 19; } a.prototype.name wang; //赋值 b new a(); console.log(web.name);//调用 ​ //object方式 var a new Object(); a.name pan; a.age 19;在js中每一个对象都有一个__proto__属性我们联想一下SSTI我们是如何找到的父类最后到基类的__base__和这里的__proto__很像。我们来看一下MDN的介绍为了污染对象攻击者需要一种方法来向原型对象添加任意属性。这可能是XSS的结果攻击者获得了页面 JavaScript 执行环境的直接访问权限。然而具有这种访问级别的攻击者可以更直接地造成损害因此原型污染通常被讨论为一种仅数据攻击即攻击者构建由应用程序代码处理的有效负载从而导致污染。一个关键的攻击向量是__proto__属性它允许访问任意对象的原型对象。你还可以通过yourObject.constructor.prototype访问原型。对于构造函数而言有一个prototype与之对应,也就是说在构造函数创建对象时prototype__proto__属性.尽管__proto__是非标准的属性仅作为浏览器/运行时实现的扩展存在但它在实际开发和安全研究中被广泛使用。它等价于Object.getPrototypeOf()方法返回的结果关于继承关系我们可以看到有就用自己的没有就继承。当访问对象的某个属性时引擎会按照以下顺序查找自身属性own property→ 2.原型链上的属性→ 3. 直至null终止。那么接下来我们来看一个经典案例案例:function merge(target, source) { for (const key in source) { if (source[key] typeof source[key] object) { if (!target[key]) target[key] {}; merge(target[key], source[key]); } else { target[key] source[key]; } } return target; }这里着重说一下这个for...in;小tips:这里如果学过其他语言,看到这个source[key]可能下意识就会想到数组啊,字典啥的.我们只需要记住一切皆对象,当你有点昏的时候你就把它看作对象就行了.也就是说source[key]是对象。for (const key in obj) { // ... }它会遍历对象所有可枚举的属性包括对象自身的可枚举属性从原型链上继承来的可枚举属性注意一个点在于:因为 constructor、toString、hasOwnProperty 这些属性虽然存在于 Object.prototype 上但它们的 enumerable 特性是 false不可枚举。所以{}进行for...in遍历输出得不到什么东西。function merge(dst, src) { for (const k in src) { console.log(当前 key: ${k}); console.log( src[${k}] , src[k]); console.log( dst[${k}] , dst[k]); if (src[k] typeof src[k] object) { if (!dst[k]) { console.log( → dst[${k}] 不存在创建空对象); dst[k] {}; } else { console.log( → dst[${k}] 已存在直接使用它); } merge(dst[k], src[k]); } else { console.log( → 直接赋值 dst[${k}] ${src[k]}); dst[k] src[k]; } console.log(---); } } console.log( 开始污染 ); const payload { constructor: { prototype: { isAdmin: true } } }; merge({}, payload); console.log(\n 污染结果 ); console.log({}.isAdmin , {}.isAdmin); console.log(Object.prototype.isAdmin , Object.prototype.isAdmin);可以在浏览器控制台跑这一段会很直观看清楚整个过程。当然这只是最经典的merge函数案例,通过一直取payload的键值对中对应的值向上污染到最里层。const payload { constructor: { prototype: { isAdmin: true } }当然还需要具体函数具体分析.这里的constructor和prototype只是帮助我们向上递归到原型.污染后第一反应看代码里有没有被读取的原型属性污染只能写 Object.prototype能不能打全靠下游代码读了什么。审计顺序找出所有spawn/exec/fork/vm/puppeteer/new Function调用点看它们读取的options.xxx是否有默认值兜底有默认值就吃不到污染锁定无默认值、直接读 options 属性的调用代码审计点看函数_.merge _.defaultsDeep _.set $.extend()等看库名lodash jQuery minimist merge 等等,还要注意版本信息。JSON.parse后再Object.assign/ 展开运算符{...userInput}req.query/req.body直接用于config合并模板引擎/配置加载器如js-yaml旧版的!!js/functionsort比较函数 /reduce累积对象new Function/eval构造时把对象塞进原型最后附上几个CVE大家搜来可以存一下,实在干不了直接查库当脚本小子吧CVE-2018-3721lodash_.merge原型链污染CVE-2019-10744lodashdefaultsDeep深层污染链到 RCECVE-2019-11358jQuery$.extend(true,...)污染CVE-2020-7598minimist命令行参数污染GHSL-2020-035CVE-2020-8203lodashzipObjectDeep污染CVE-2020-15256object-pathset系列污染CVE-2021-21353webpack 系列配置合并污染GhostScript/CVE-2018-16509沙箱逃逸链路常与原型链 gadget 串联