Express+lodash+ejs: 从原型链污染到RCE

由 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 调试

res 1

可以看到进入了 express 的 response 中,获取 options,然后触发 app 渲染引擎进行渲染

app 1

这里又是一系列参数和设置,可以跳过

app 2

这里是寻找 app 的渲染引擎。可以看到已经被配置为了 ejs,传递参数和模版文件,开始尝试渲染

app 3

view 1

调用渲染引擎。到这里,我们已经调试到了 ejs 库中

ejs 1

可以看到很复杂一坨,都是配置渲染设置,不管他,继续调试到 tryHandleCache

ejs 2

一堆 Promise 的适配,再看这坨屎一样的 js 我要死了,继续调 handleCache

ejs 3

是缓存设置,没啥用,终于调到了编译函数,开始渲染页面了

ejs 4

又是设置,跳过,看到实例化一个模版类,然后调用模版的 compile 成员函数,继续跟进

ejs 5

看到这里,立刻发现这里有一个代码注入的漏洞

仔细解释一下:

可以看到, 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

发表评论

发表评论

沙发空缺中,还不快抢~