由 X-NUCA 2019 Web 的 hard_js 启发
RCE 的前提是要有原型链污染,原型链污染原理等具体不再赘述
先来个最简单的应用
const express = require('express');
const bodyParser = require('body-parser');
const lodash = require('lodash');
const ejs = require('ejs');
const app = express();
app
.use(bodyParser.urlencoded({extended: true}))
.use(bodyParser.json());
app.set('views', './');
app.set('view engine', 'ejs');
app.get("/", (req, res) => {
res.render('index');
});
app.post("/", (req, res) => {
let data = {};
let input = JSON.parse(req.body.content);
lodash.defaultsDeep(data, input);
res.json({message: "OK"});
});
let server = app.listen(8086, '0.0.0.0', function() {
console.log('Listening on port %d', server.address().port);
});
断点打在 18 行,Force Step Into 调试
可以看到进入了 express 的 response
中,获取 options
,然后触发 app 渲染引擎进行渲染
这里又是一系列参数和设置,可以跳过
这里是寻找 app 的渲染引擎。可以看到已经被配置为了 ejs,传递参数和模版文件,开始尝试渲染
调用渲染引擎。到这里,我们已经调试到了 ejs 库中
可以看到很复杂一坨,都是配置渲染设置,不管他,继续调试到 tryHandleCache
一堆 Promise 的适配,再看这坨屎一样的 js 我要死了,继续调 handleCache
是缓存设置,没啥用,终于调到了编译函数,开始渲染页面了
又是设置,跳过,看到实例化一个模版类,然后调用模版的 compile
成员函数,继续跟进
看到这里,立刻发现这里有一个代码注入的漏洞
仔细解释一下:
可以看到, opts
对象 outputFunctionName
成员在 express 配置的时候并没有给他赋值,默认也是未定义,即 undefined
,这样在 574 行时,if 判否,跳过
但是在我们有原型链污染的前提之下,我们可以控制基类的成员。这样我们给 Object
类创建一个成员 outputFunctionName
,这样可以进入 if 语句,并将我们控制的成员 outputFunctionName
赋值为一串恶意代码,从而造成代码注入。在后面模版渲染的时候,注入的代码被执行,也就是这里存在一个代码注入的 RCE
至于恶意代码构造就非常简单了。在不考虑后果的情况下,我们可以直接构造如下代码
a; return global.process.mainModule.constructor._load('child_process').execSync('whoami'); //
放到代码里面看就是
prepended += ' var ' + opts.outputFunctionName + ' = __append;' + '\n';
// After injection
prepended += ' var a; return global.process.mainModule.constructor._load("child_process").execSync("whoami"); // 后面的代码都被注释了'
可想而知,在污染了原型链之后,渲染直接变成了执行代码,并提前 return,从而 getshell
PS: 其实如果能打到原型链污染这种洞,一旦污染原型链,也就意味着整个 Runtime 都完蛋了,代码不在各种奇奇怪怪的依赖库里面出奇奇怪怪的问题真就谢天谢地了。在出 ezts 这个题的时候,就出现了各种鬼畜的问题,比如拼接进 ORM 的 SQL 语句里面了,注入到 HTTP Header 里面了这些,当时真的是对依赖库各种 patch 来保证正常拿 shell。也就是说,这个代码注入能不能利用,会不会在奇怪的依赖库里面死掉,完 全 看 脸 xD
师傅,这里ejs的漏洞是哪个版本的呢?
首先这不是一个漏洞(至少官方承认不是,见 Issue #451
目前看来,这个问题官方不会去修补,我也没有具体追溯过最早版本,但是看起来这种代码拼接的传统在 ejs 里面一直都存在
总之,近期全版本应该都有这种问题