Overview
Machine author: Tr1s0n. IP: 10.10.10.x.
Chain: forum token, escalation to admin, XXE (file / source read), Redis (session
replacement), access to developers., LFI + PHP filter chain (RCE as www-data),
PHP-FPM / FastCGI (pivot to victor), prototype pollution in a Node.js API (RCE as root).
Reconnaissance
| |
Open: 22 (SSH), 80 (HTTP). Redis (6379) is local only / password protected.
| |
The vhost developers.collect.htb is protected by Basic Auth.
Initial Access
Web foothold, admin + XXE
- A forum post (MyBB) has an attached Burp history export. It contains a token and the
request
POST /set/role/admin. - Register and log in on
collect.htb, capture thePHPSESSID. - Send
POST /set/role/adminwith the leaked token; the account becomes an admin, with access to/admin. - The admin panel has API registration that makes a server-side request with
controllable XML (the
manage_apiparameter), an XXE.
Blind (out-of-band) XXE to exfiltrate files: host an evil.dtd on your server and read,
for example:
/var/www/developers/.htpasswd-> hash -> hashcat -> the Basic Auth password fordevelopers.- application source, including
bootstrap.php
Basic Auth for developers: developers_group : r0cket.
Redis, access to the developers portal
bootstrap.php reveals the Redis session configuration and password:
| |
Sessions are stored in Redis, so they can be replaced to bypass the developers login:
| |
The portal requires auth = True (and the admin role) in the session.
LFI + PHP filter chain, RCE as www-data
Vulnerable code in index.php:
| |
Traps:
- The vulnerable parameter is
page(notfile, notaction). The wrong key meansempty($_GET['page'])and a redirect to/?page=home(a clean home page misleadingly suggests “almost working”). includeappends.php, so in the chain useresource=php://temp(after appending,php://temp.php, still valid) rather than a real file, so nothing is appended after the payload.- URL length limit (~3000 chars): a full reverse shell in the chain can be too long. Use a short webshell and pass commands as a parameter.
- Send the session cookie (with
auth) plus Basic Auth.
Chain generation (synacktiv):
| |
Python skeleton:
| |
Trigger: &0=<command>. Result: a reverse shell as www-data.
Lateral Movement
PHP-FPM / FastCGI, pivot to victor
PHP-FPM (FastCGI) listens locally. Use it to execute code as victor:
| |
A reverse shell as victor. ~victor contains a copy of the API source,
~/pollution_api (Node.js / Express).
Privilege Escalation
Prototype pollution in the Node.js API
pollution_api runs as root (/root/pollution_api). You must (a) become an admin in the
database, and (b) trigger prototype pollution.
Credentials found in the API source:
- JWT secret:
JWT_COLLECT_124_SECRET_KEY - MySQL:
webapp_user : Str0ngP4ssw0rdB*12@1, databasepollution_api
admin.js checks the JWT and the database:
| |
A forged role:admin token is not enough; a user+admin row must exist in the database.
The admin token embedded in the source is expired (exp = 2022).
| |
The vulnerable handler does _.merge(obj, req.body) (lodash CVE-2018-3721), then
exec(...). The key __proto__ poisons Object.prototype.shell. child_process.exec
without an explicit options.shell walks the prototype and runs our file instead of
/bin/sh.
| |
On the attacker: rlwrap -cAr nc -lvnp 1234.
Payload (one line; token from the step above, expires after an hour):
| |
The response {"Status":"Ok"} gives a root shell on 1234. If the first attempt returns
nothing, repeat the request (the prototype poisoning persists in the process; the next
exec picks up the poisoned value).
| |
Detection and Mitigation
- Disable external entities in XML parsers.
- Do not store Redis auth in a world-readable config; bind Redis to localhost with a
strong
requirepass. - LFI: whitelist includes, use
basename, disable stream wrappers. - Do not run the API as root; use
_.mergealternatives that reject__proto__/constructorkeys, and always passoptions.shellexplicitly. - Enforce authorization against the database, not just the JWT.
Lessons Learned
- Filter chain LFI: pick the right parameter, use
resource=php://temp, mind the URL length limit, useallow_redirects=Falsefor diagnostics. - Prototype pollution is not string injection; it changes the prototype (here, the
execshell interpreter). The first request sets it, the second executes. - Auth is not just a token; middleware cross-checked the database, so an
UPDATE ... role='admin'plus re-login was needed, not just a forged JWT. - Reverse-shell debugging: the port in the script must match the listener; an empty handler response (0 bytes, no error) means the code ran but there is no receiver.
- Stabilise the shell:
python3 -c 'import pty;pty.spawn("/bin/bash")'or multi-line commands concatenate.
Command Reference
| Item | Value |
|---|---|
| Basic Auth (developers) | developers_group : r0cket |
| Redis | COLLECTR3D1SPASS |
| JWT secret | JWT_COLLECT_124_SECRET_KEY |
| MySQL | webapp_user : Str0ngP4ssw0rdB*12@1 (db pollution_api) |
| |