AST Injection, Prototype Pollution to RCE
This article describes how to trigger RCE
in two well-known template engines,
using a new technique called AST Injection.
AST Injection
What is AST?
AST in NodeJS
In NodeJS, AST is used in JS really often, as template engines and typescript etc.
For the template engine, the structure is as shown above.
If prototype pollution vulnerability exists in the JS application,
Any AST can be inserted in the function by making it insert during the Parser
or Compiler
process.
Here, you can insert AST without proper filtering of input (which has not been properly filtered) that has not been verified by lexer or parser.
Then, we can give unexpected input to the compiler.
Below is how to actually use AST Injection to execute arbitrary commands in handlebars
and pug
Handlebars
Until today, handlebars
has been downloaded a total of 998,602,213
times.
Handlebars are the most commonly used template engine except for ejs.
For newer versions, it is known to be safe because no command can be executed, even if any template can be inserted.
How to Detect
1 | const Handlebars = require('handlebars'); |
Before you start, here’s how to use templates in handlebars.
The Handlebar.compile function converts a string into a template function and passes object factors for reference.
1 | const Handlebars = require('handlebars'); |
In here, we can make influence to the compilation process using prototype pollution.
You can insert any string into Object.prototype.pendingContent
to determine the possibility of an attack.
This allows you to be sure that servers are using handlebars engine when a prototype pollution exists in a black-box environment.
1 | <!-- /node_modules/handlebars/dist/cjs/handlebars/compiler/javascript-compiler.js --> |
This is done by the appendContent
function of javascript-compiler.js
appendContent
is this.If pendingContent
is present, append to the content and returns.
pushSource
makes the pendingContent
to undefined
, preventing the string from being inserted multiple times.
Exploit
Handlebars work as shown in the graph above.
After lexer and parser generater AST, It passes to compiler.js
We can run the template function compiler generated with some arguments.
and It returns the string like “Hello posix” (when msg is posix)
1 | <!-- /node_modules/handlebars/dist/cjs/handlebars/compiler/parser.js --> |
The parser in handlebars forces the value of a node whose type is NumberLiteral to always be a number through the Number constructor.
However, you can insert a non-numeric string here using the prototype pollution.
1 | <!-- /node_modules/handlebars/dist/cjs/handlebars/compiler/base.js --> |
First, look at the compile function, and it supports two ways of input, AST Object and template string.
when input.type is a Program
, although the input value is actually string.
Parser considers it’s already AST parsed by parser.js and send it to the compiler without any processing.
1 | <!-- /node_modules/handlebars/dist/cjs/handlebars/compiler/compiler.js --> |
The compiler given the AST Object (actually a string) send it to the accept
method.
and accept
calls this[node.type]
of Compiler.
Then take body attribute of AST and use it for constructing function.
1 | const Handlebars = require('handlebars'); |
As a result, an attack can be configured like this.
If you have gone through parser, specify a string that cannot be assigned to the value of NumberLiteral.
But Injected AST processed, we can insert any code into the function.
Example
1 | const express = require('express'); |
Example of configuring a vulnerable server using a flat module it has prototype pollution vulnerability.
The flat is a popular module with 4.61 million downloads per week
the patch for the prototype pollution vulnerablity reported has not yet been made.
1 | import requests |
We can Insert the any command into the value to obtain the shell.
Pug
Until today, this pug
has been downloaded a total of 65,827,719
times.
pug
is a module that was previously developed under the name jade and renamed.
According to statistics, it is the 4th most popular template engine in nodejs.
How to Detect
1 | const pug = require('pug'); |
A common way to use a template in a pug is as above.
The pug.compile function converts a string into a template function and passes the object for reference.
1 | const pug = require('pug'); |
It is a method to check the use of pug template engine in a black-box environment using prototype pollution.
When you insert AST into the Object.prototype.block
, the compiler adds it to the buffer by referring to the val.
1 | switch (ast.type) { |
When ast.type is While
, it calls walkASK with ast.block
(refers prototype if the value does not exist)
If the template refer any value from argument, the While node is always exists, so the reliblity is considered quite high.
In fact, if developer don’t have to refer to any values from argument in the template
they wouldn’t use any template engines in the first place.
Exploit
Pug
work as shown in the graph above.
Unlike handlebars
, each process is separated into a separate module.
AST generated by pug-parser
is passed to the pug-code-gen
and made into a function.
and finally, it will be executed.
1 | <!-- /node_modules/pug-code-gen/index.js --> |
In the compiler of the pug, there is a variable that stores the line number named pug_debug_line
for debugging.
If the node.line value exists, it is added to the buffer, otherwise it is passed.
For AST generated with pug-parser, the node.line
value is always specified as an integer.
But, we can insert a string into the node.line
through AST Injection and cause arbitrary code execution.
1 | const pug = require('pug'); |
Example of a generated function.
You can see that the Object.prototype.line
value is inserted in the right-hand side of the pug_debug_line
definition.
1 | const pug = require('pug'); |
As a result, an attack can be configured like this.
By specifying a string in the node.line
value, which is always defined as number through parser.
So, any command could be inserted into the function.
Example
1 | const express = require('express'); |
As in the example of handlebars, flat used to configure the server.
The template engine has been changed to pug
1 | import requests |
We can insert any code into the block.line
, and get a shell.
Conclusion
I described how to make arbitrary command execution,
through AST Injection on the JS template engines.
In fact, these parts are very hard to fix completely
So I expect this to remain like EJS.