Overview
Machine author: NoobHacker9999. IP: 10.10.10.x. Technique categories: SSRF, SSRF filter
bypass, redirect-based SSRF, data exfiltration, Python pdb privesc.
| |
Three concepts to take away:
- An SSRF does not need to support
ftp://directly; if the server follows redirects, a301/302from your host can move it to any scheme or host it would not accept directly. - A blacklist filter on “localhost” breaks on redirect; validation happens on the input URL, not the target after the redirect (a TOCTOU in SSRF).
pdb.post_mortem()in anexceptblock is a remote root shell when the script runs viasudo. Any uncontrolled exception yields an interactive debugger with the process’s privileges.
Reconnaissance
| |
| Port | State | Service | Note |
|---|---|---|---|
| 21/tcp | filtered | ftp | Firewalled, localhost only (the key to SSRF) |
| 22/tcp | open | OpenSSH 8.2p1 | Entry after obtaining the key |
| 80/tcp | open | Apache httpd 2.4.41 | Redirect to http://forge.htb (virtual hosting) |
filtered on 21 plus a redirect to a vhost means the machine almost certainly has
internal services reachable only from localhost. Think in terms of SSRF before you even
see the upload form.
| |
Directory and vhost enumeration
| |
| |
For gobuster vhost, always filter the base response status or size (here -v 302), or
you drown in false positives. Newer versions have --exclude-length / -b.
Initial Access
The upload feature
/upload has two options: “Upload local file” and “Upload from URL”. A local PHP upload
(“Hello world!”) shows that:
- the filename is randomized and the extension is stripped, so no code execution via upload
- the file content is preserved, so this is an exfiltration channel: whatever the server
fetches from a URL, it stores and serves back at
/uploads/<random>
“Upload from URL” plus content preservation is an SSRF read primitive.
Discovering the SSRF and mapping the filters
| Input | Server response | Conclusion |
|---|---|---|
ftp://127.0.0.1 | Invalid protocol! Supported protocols: http, https | Scheme whitelist: http/https only |
http://127.0.0.1, http://[::1], localhost, http://[0:0:0:0:0:0:0:0], http://forge.htb | URL contains a blacklisted address! | Blacklist on the URL string (localhost, IP forms) |
http://10.10.14.x (our host) | passes | The server makes outbound requests to any external host |
| |
python-requests follows redirects by default (allow_redirects=True), which opens the
bypass.
SSRF via redirect
The validation checks only the input URL (http://10.10.14.x, legal). The server follows
Location:, and the redirect target is not re-validated. Stand up a server that returns a
301 with the internal target.
| |
In the form, supply http://10.10.14.x; the server gets the 301, follows to
http://forge.htb/, and stores the HTML at /uploads/<random>.
| |
When changing Location:, clear the response file each time (> response or
rm response), or you concatenate headers and the response is malformed.
Reaching admin.forge.htb and exfiltrating data
Point Location: at the internal vhost:
| |
/announcements contents:
- FTP credentials:
user:heightofsecurity123! - The
/uploadendpoint on admin supportsftp,ftps,http,https /uploadaccepts a GET parameter?u=<url>for the image source
admin.forge.htb/upload accepts ftp://, which forge.htb/upload rejected. Chain the
two SSRF layers: an external redirect to the internal endpoint that knows FTP.
Lateral Movement
Build a redirect targeting admin /upload?u=<ftp url> with embedded credentials:
| |
| |
The FTP password did not work for SSH; the key was required. Do not assume credentials are shared between services; check both vectors.
Privilege Escalation
pdb.post_mortem in a sudo script
| |
/opt/remote-manage.py (skeleton):
| |
- The script opens a local socket on a random port and asks for the hardcoded password
secretadminpassword. - The menu runs
int(clientsock.recv(...))on uncontrolled input. A non-numeric character raisesValueError. - The exception reaches
except, andpdb.post_mortem()starts an interactive Python debugger with the process’s privileges (root, becausesudo). - In
pdbyou can run arbitrary Python to spawn a shell.
| |
root.txt is in /root/.
Detection and Mitigation
SSRF:
- Validate the target host after DNS resolution and after every redirect, not just the input string.
- Disable redirect following for fetchers, or re-validate
Location(allow_redirects=Falseplus a controlled loop). - Use an allow-list of target hosts/ranges instead of a blacklist. Block RFC1918, loopback, link-local (169.254/16), metadata (169.254.169.254).
- Enforce a scheme whitelist in the HTTP client, not just on user input.
- Network isolation: run the fetcher in a separate segment with no access to internal services.
Privilege escalation:
- Never leave
pdb/breakpoint()in production code; setPYTHONBREAKPOINT=0. - Do not run interactive or debuggable scripts via
sudo NOPASSWD. - Validate and sanitize input before type conversion; handle exceptions without dropping into a debugger.
Detection signals:
- Outbound HTTP from the application server to attacker-pool hosts.
- Requests to “localhost only” vhosts originating from the loopback with unusual timing.
python3run viasudospawning/bin/bash(audit with auditd /execve).
Lessons Learned
- Any “provide a URL” feature (upload from URL, webhook, PDF/HTML renderer, image fetcher, avatar import, link preview) is a potential SSRF.
- Check whether the primitive is a read (content returns to you) or blind (side effect only).
- Map the filters separately: scheme whitelist vs host blacklist. Attack the weaker link.
- Redirect bypass works whenever the HTTP client follows redirects; check the
User-Agentin the callback to see which client and schemes are supported. sudoplus a Python script: look forpdb,eval/execon input,pickle.loads,os.system/subprocesswith concatenation, injectablePYTHONPATH/PYTHONSTARTUP.pdb.post_mortem()/breakpoint()inexceptis a ready-made backdoor; force any exception on controlled input.
Command Reference
| |