模版引擎是什么
JS web开发中常用的模版引擎如 ejs
、pug
、handlebars
功能:
动态渲染HTML代码,创建可重复使用的页面结构
ejs
模版使用
1 | // 安装EJS模块:npm install ejs |
handlebars
模版使用
1 | // 安装Handlebars模块:npm install handlebars |
pug
模版使用
1 | // 安装Pug模块:npm install pug |
总结:
可以看到模版引擎其实都有各自的一些特定语法规则,比如 pug 中可以通过 #{name}
来引用外部环境的变量, ejs 则是 \<%= name %>
。通过这种方式简化html代码的编写,同时实现模版重用
模版引擎的工作原理
本质上,引擎是通过针对你使用模版语言编写的模版进行解析,从而生成新的JS代码。大题过程可以概括如下:
1 | 词法解析 -> 语法解析 -> 代码生成 |
但是在语法树处理的过程中,在处理节点的时候,存在大量的赋值、循环操作,而在大部分模版引擎中,都是这么写的:
1 | attrs[name] = attrs[value] |
- 赋值操作未判断对应的属性是否为对象自身的属性,导致访问到原型链的
Object.prototype
的属性 - 判断某个属性是否存在,同样未判断是否为对象自身属性是否存在,若存在原型链污染,则可以进入if判断
- JS的 for…in 循环会遍历对象的所有可枚举属性,包括原型链上的属性。例如:
1 | let obj = { a: 1, b: 2 }; |
因此若存在原型链污染,则可以随意修改AST树,进而影响生成的代码,最终达到RCE(远程代码执行)的目的
需要注意的是:
AST树的生成本质上是影响生成的字符串,因此也可以导致XSS漏洞
代码执行的那一步才会导致RCE,这时候需要第一步通过原型链污染注入代码,进而影响生成的代码
pug template AST injection
1 | const pug = require('pug'); |
当执行到 fn({msg: 'It works'});
这一步的时候,本质上是进入了一段函数
打印出这段函数的代码,可以看到通过原型链污染我们实现了向生成代码中插入一段字符串
1 | (function anonymous(pug |
原理分析(以pug为例)
语法树结构
pug 解析 h1= msg
,生成的语法树结构:
1 | { |
语法树执行顺序
以刚刚生成的语法树结构举例,解析顺序为:
- Block
- Tag
- Block
- Code
- ……
注意第4步解析 node.Type
为 Code
类型,会执行如下代码:
1 | case 'Code': |
- 判断
ast.block
属性是否存在,此处的ast
即当前ast语法树的节点 - 如果存在,继续递归解析 block
结合原型链污染
如果某处存在原型链污染漏洞,使得
1 | Object.prototype.block = {"type":"Text","val":`<script>alert(origin)</script>`}; |
那么 ast.block
就会访问到 ast.__proto__.block
,即Object.prototype.block
的属性
此时代码输出结果,导致了XSS
1 | const pug = require('pug'); |
RCE
我们知道pug本质上是将一段代码,如 h1 =msg
编译为一段js代码,背后其实就是生成语法树+ new Function
因此如果能通过AST Injection插入节点,并使之成为代码,即可达到远程代码执行的目的。
刚好pug中就有如下代码:
1 | // /node_modules/pug-code-gen/index.js |
那么我们通过 AST Injection + Prototype Pollution 即可实现RCE
1 | const pug = require('pug'); |