HackTheBox – Dusty Alleys (Challenge Web/Medium)

En este post estaremos resolviendo el challenge web llamado «Dusty Alleys» de Hack The Box, calificado como Medium. Se trata de un challenge ya retirado. No obstante, creemos que el nivel de este challenge es más sencillo, salvo por un par de detalles quizá menos conocidos.

Comenzamos como siempre visualizando el acceso inicial a la web a través de su URL, aunque en este caso no nos aporta gran cosa:

Dado que a priori en caja negra es difícil de observar vulnerabilidades, pasamos a caja blanca, analizando la configuración y ficheros de código fuente. Lo primero que llama la atención es la configuración del servicio web:

server {
        listen 80 default_server;
        server_name alley.$SECRET_ALLEY;

    location / {
        root /var/www/html/;  
        index index.html;              
    }

        location /alley {
                        proxy_pass http://localhost:1337;
                        proxy_set_header Host $host;
                        proxy_set_header X-Real-IP $remote_addr;
                        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
                        proxy_set_header X-Forwarded-Proto $scheme;
        }

        location /think  { 
                        proxy_pass http://localhost:1337;
                        proxy_set_header Host $host;
                        proxy_set_header X-Real-IP $remote_addr;
                        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
                        proxy_set_header X-Forwarded-Proto $scheme;

                        }
}

server {
        listen 80;
                server_name guardian.$SECRET_ALLEY;

        location /guardian {
                        proxy_pass http://localhost:1337;
                        proxy_set_header Host $host;
                        proxy_set_header X-Real-IP $remote_addr;
                        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
                        proxy_set_header X-Forwarded-Proto $scheme;
        }
}

Remarcamos en negrita los puntos más relevantes y es que parece existir virtual hosting en el sistema bajo análisis junto a dos servicios:

  • alley.$SECRET_ALLEY
  • guardian.$SECRET_ALLEY

Así que, como primer objetivo, tenemos que descubrir dicho valor. Este es el detalle que comentábamos quizá un poco diferente al resto de retos. Normalmente, el virtual hosting es algo habitual y que no debe llamarnos la atención. Sin embargo, si queremos intentar descubrir el host virtual detrás de una IP podemos hacerlo, siempre y cuando tengamos un endpoint que nos refleje las cabeceras que estamos enviando. En este caso, la ruta «/think» del fichero «guardian.js» se utiliza, precisamente, para esto mismo:

router.get("/think", async (req, res) => {
  return res.json(req.headers);
});

Así que, si utilizamos el protocolo HTTP 1.0 y enviamos un mensaje sin cabecera Host, quizá el servidor le añade una por defecto. Lanzamos una petición al endpoint «/think» sin cabecera Host con la esperanza de que el servidor la añada con el valor por defecto y observamos la respuesta:

Tal como podemos ver, el valor de la variable $SECRET_ALLEY es inequívocamente «firstalleyontheleft.com», así que ya sabemos los dos virtual hosts bajo análisis:

  • alley.firstalleyontheleft.com
  • guardian.firstalleyontheleft.com

Nos fijamos ahora en la ruta «/guardian» dentro del mismo fichero «guardian.js» y resaltamos en su código fuente la parte más relevante.

router.get("/guardian", async (req, res) => {
  const quote = req.query.quote;

  if (!quote) return res.render("guardian");

  try {
    const location = new URL(quote);
    const direction = location.hostname;
    if (!direction.endsWith("localhost") && direction !== "localhost")
      return res.send("guardian", {
        error: "You are forbidden from talking with me.",
      });
  } catch (error) {
    return res.render("guardian", { error: "My brain circuits are mad." });
  }

  try {
    let result = await node_fetch(quote, {
      method: "GET",
      headers: { Key: process.env.FLAG || "HTB{REDACTED}" },
    }).then((res) => res.text());

    res.set("Content-Type", "text/plain");

    res.send(result);
  } catch (e) {
    console.error(e);
    return res.render("guardian", {
      error: "The words are lost in my circuits",
    });
  }
});

Del script anterior, las partes fundamentales son las siguientes:

  • Se requiere un parámetro denominado «quote» a la hora de llamar a la ruta «/guardian»
  • El valor del parámetro «quote» se utiliza para formar una URL
  • Se revisa que la URL solo contenga referencias a localhost, intentando acotar así ante ataques de Server-Side Request Forgery (SSRF)
  • Si la URL es interna, se realiza una petición añadiendo la cabecera «Key» en cuyo valor se incluye el flag. Esto hace al código vulnerable a Server-Side Request Forgery (SSRF)
  • El resultado de la petición anterior se refleja en la salida

Así las cosas, y dado que el endpoint «/think» nos devuelve un reflejo de las cabeceras exactamente, podemos exfiltrar el valor del flag incluso aunque solo se puede explotar el Server-Side Request Forgery (SSRF) con endpoints locales. Para ello, simplemente construimos la siguiente URL como valor del parámetro «quote»:

http://localhost:1337/think

Fíjate que usamos «http://localhost:1337» como destino, dado que es donde está escuchando realmente la aplicación web según el fichero de configuración del servidor web mostrado al inicio. Dado este payload, lanzamos la petición y rápidamente obtenemos el flag, al reflejarse el valor de la cabecera «Key» añadida por el servidor en la respuesta:

En definitiva, hemos explotado un sencillo Server-Side Request Forgery para acceder a recursos internos y exfiltrar el valor de la cabecera añadida por el propio servidor a la hora de realizar dicha petición forzada.

~km0xu95

Comments are closed.