$ cat writeup.md…
$ cat writeup.md…
HackTheBox
Task: a Laravel 10 screenshot/source-view service exposes SSRF in `/api/get-html` because localhost filtering only checks literal IPv4 hosts. Solution: use `localtest.me` plus `gopher://` to push a malicious Redis queue job and reach command injection in `rmFile`, then copy `/flag` into the web root.
$ cat /etc/rate-limit
Rate limit reached (20 reads/hour per IP). Showing preview only — full content returns at the next hour roll-over.
New screenshot service just dropped! They talk alot but can they hack it?
This challenge provides a Laravel 10 / PHP 8.1 service with two user-facing features: fetching page source and generating screenshots. The intended path is not the screenshot feature, but the HTML fetcher behind POST /api/get-html.
The application tried to prevent SSRF by rejecting literal localhost IPv4 hosts and by requiring a domain with DNS records. That check was incomplete: it never resolved the hostname and verified the resulting IP. Using localtest.me as a localhost alias let me reach local Redis via gopher://, enqueue a malicious Laravel job, and trigger command injection in App\Jobs\rmFile.
The relevant routes were:
GET /POST /api/get-htmlPOST /api/getssInitial testing showed that POST /api/get-html successfully fetched normal remote content such as http://example.com, so this endpoint was a good SSRF candidate.
The screenshot path was a dead end. It relied on external screenshotmachine.com, so it was not the intended local attack surface.
I also briefly considered Laravel Ignition/debug-style issues, but that was unnecessary here.
/api/get-htmlThe vulnerable logic was in validateUrl():
private function validateUrl($url) { $parsedUrl = parse_url($url); if (!isset($parsedUrl['host'])) return false; if ($this->isValidIPv4($parsedUrl['host']) && $this->isLocalIP($parsedUrl['host'])) { return false; } if (!$this->isValidDomain($parsedUrl['host'])) { return false; } return true; }
This only blocked a host if it was a literal IPv4 string and local. It did not resolve a hostname and then check whether the resolved address was loopback or private.
That made localtest.me perfect:
127.0.0.1.So a URL like gopher://localtest.me:6379/... passed validation and still hit localhost.
...
$ grep --similar