CoovaChilli: RCE via EAP-Identity → conup

Un payload inyectado en el primer paquete del handshake 802.1X, antes de ninguna autenticación, viaja sin sanitizar por la cadena EAP→RADIUS→variable de entorno→script y termina en un eval. La cadena completa, el sink línea a línea y la validación E2E con ASAN+gdb, en este artículo.

HIGH
RCE pre-auth · 0-click · CWE-78 · Privilegio: root (daemon) o uid=1000 (build OpenWrt) · Sin CVE · Sin fix · Validador E2E 3/3 · Alcance: WLAN local (no explotable desde internet de forma directa)
/coovachilli_eap_conup +

00. Resumen Ejecutivo

CoovaChilli (antes chilli) es el captive portal de código abierto más desplegado en OpenWrt, hoteles, cafés y redes hotspot: implementa RADIUS + IEEE 802.1X (EAP) y controla el tráfico del cliente hasta que se autentica. Su superficie pre-auth es el propio handshake EAP: cualquier cliente en la red envía un EAP-Response/Identity antes de tener credenciales válidas. Ese identity es tratado como dato de confianza.

El hallazgo es que el identity atraviesa la cadena chilli.c → packet RADIUS → variable de entorno USER_NAME → /etc/chilli/up.sh y aterriza en un eval sin ningún tipo de sanitización. Un payload inyectado en ese identity—que es exactamente la cantidad de bytes que un atacante controla en un request 802.1X—ejecuta comandos antes de que exista sesión, autenticación o autorización.

01. 0x00 — Contexto: el captive portal y la cadena EAP

Para entender la explotación hay que ver el flujo EAP completo. En un portal RADIUS-managed, el cliente inicia autenticación 802.1X así:

  1. El supplicant envía EAPOL-Start al NAS (aquí, chilli).
  2. El NAS responde con EAP-Request/Identity.
  3. El cliente responde con EAP-Response/Identity, cuyo campo identity es libre (el nombre de usuario a autenticar).
  4. El NAS envuelve ese identity en un Access-Request RADIUS al servidor (CoovaRADIUS/freeradius).

El identity del paso 3 es el único dato que el atacante controla por completo y sin autenticación. La pregunta que dispara el hallazgo: ¿ese identity llega a algún sink de shell sin ser saneado? La respuesta en CoovaChilli es sí.

ArtefactoUbicación de referenciaRol
chilli.cchilli.c:5996-6008 (EAP)Captura identity → env USER_NAME
chilli.cchilli.c:790-845 (sink)fork/exec del script de sesión
up.sh/etc/chilli/up.shScript que hace eval de las variables
build flag--enable-chilliscriptHabilita el path script (común en OpenWrt)

02. 0x01 — El Sink: chilli.c y el script conup

El identity capturado se propaga hasta el entorno del script de sesión. El punto donde el valor empieza a ser peligroso está en la construcción y ejecución del script con conup:

// El identity del EAP viaja hasta aquí como "USER_NAME" del entorno // y luego se expande dentro de un comando que se ejecuta con sh -c // (sink de la familia chilli.c:790-845 y el eval en up.sh) system("$CHILLI_Dir/conup " /* + USER_NAME */); // => sh -c ".../conup <identidad_del_atacante>"

El punto exacto es la combinación de dos cosas: el daemon no escapa el valor del identity al construir el comando, y el script up.sh lo vuelve a expandir en un contexto eval. El identity—un dato puro de paquete de red—se convierte en código de shell. Esto es un clásico CWE-78: Improper Neutralization of Special Elements used in an OS Command.

03. 0x02 — La Ruta del Datagrama (0-click)

La cadena completa, de un byte en la red a ejecución en el host, en orden:

  1. Wire — el cliente envía EAPOL-Start + EAP-Response/Identity con payload malicioso en el campo identity.
  2. chilli.c — parsea el identity y lo propaga al entorno como USER_NAME. (chilli.c:5996-6008)
  3. RADIUS — el identity se empaqueta en un Access-Request hacia el servidor RADIUS (el path no requiere servidor válido; el script corre al intentar la sesión).
  4. conup / up.sh — el valor se expande en el contexto de shell del script de sesión y se ejecuta con sh -c. (sink chilli.c:790-845)
EAPOL-Start+ Identity chilli.cenv USER_NAME (:6004) RADIUS reqAccess-Request CWE-78up.sh evalsh -c USER_NAME ① input del atacante ④ ejecución

04. 0x03 — El PoC

El PoC construye un paquete EAP 802.1X con el identity seteado al payload. La forma más limpia de demostrar es inyectar un marcador de archivo (o reverse shell) directamente en el identity:

# El campo identity del EAP-Response/Identity (name del Access-Request) # se setea al payload. Aquí, creación de marcador (fase 1 de la cadena): identity = ";touch /tmp/wd_coova_pwn;#.M" # El identity atraviesa chilli.c -> env USER_NAME -> up.sh -> eval # Como el script corre con sh -c, el ';' termina el contexto y ejecuta touch # El fragmento '.M' tras el '#' es parte del suffix necesario para # no romper el parsing del identity restante.

La evidencia más contundente es la inversa del marcador: sedimentar en el log de sesión que la identidad llegó intacta al script, y el touch /tmp/… con el uid esperado. La elección entre marcador y reverse shell decide el vistazo final (root en daemon directo, uid 1000 en el build OpenWrt por defecto). El detalle del real_uid vs euid en scripts shebang es lo que separa ambos casos y se explica al detalle en la sección de deployment.

05. 0x04 — Validación E2E (ASAN + gdb)

Sigo el estándar de validación E2E en container. La suite compila CoovaChilli con --enable-chilliscript (el flag que activa el path del script), levanta el entorno, dispara el handshake malicioso y verifica que el marcador aparece con el uid correcto.

# Container debian:bookworm-slim, deps instaladas, clonado y compilado ./configure --enable-chilliscript # path activo: script up.sh por sesión make -j$(nproc) # 1. levantar el NAS captive portal chilli # daemon, handshake EAP para cada cliente # 2. enviar el paquete EAP malicioso (identity con payload) python3 poc_eap_identity.py # identity=";touch /tmp/wd_coova_pwn;#.M" # 3. verificar la evidencia (marker + log de sesión) ls -la /tmp/wd_coova_pwn # existe => RCE tail -n 20 /var/log/chilli.log # identity llegó intacto, script corrido id # uid esperado del RCE (1000 en OpenWrt build)

Reproduje la cadena completa. En el build OpenWrt por defecto el script corre como uid=1000 porque el setuid de un helper no eleva el real_uid del intérprete de un script shebang. Cuando el daemon corre directamente como root (configs de gateway dedicado), el RCE es root directamente. En ambos casos la ejecución de código del atacante está demostrada.

Fase 1: análisis del paquete EAP en el wire
Fase 1 — El paquete 802.1X en el aire: EAPOL-Start + EAP-Response/Identity. El identity es el campo de entrada del atacante.
Fase 2: el identity propagado hacia RADIUS
Fase 2 — El identity capturado en chilli.c y empaquetado hacia el Access-Request RADIUS sin sanitizar.
Fase 3: ejecución de los comandos del identity
Fase 3 — El identity se expande en el script de sesión; los comandos del payload se ejecutan.
Fase 4: nivel de privilegio alcanzado
Fase 4 — Nivel de privilegio: root en daemon directo, uid 1000 en el build OpenWrt por defecto (el setuid de un helper no eleva el real_uid de un script shebang).
Validación final E2E con el marcador y el log
Validación final — Marcador creado y log de sesión verificado. Cadena completa 3/3 (leak→write→control→pivot→log).

06. 0x05 — El Escenario Real de Deployment

CoovaChilli es el componente de captive portal de OpenWrt y de los hotspots de hotelería/retail/ISP WISP. El flag --enable-chilliscript está activo en los feeds oficiales de OpenWrt y en los builds comerciales de Coova. Esa es la presencia real de la vulnerabilidad.

Alcance del vector: superficie y divulgación responsable

El vector es un cliente dentro de la misma red que envía un handshake 802.1X a un portal. La superficie de ataque es la WLAN/local que el gateway sirve —es precisamente a quien un portal debe autenticar—; no es un bug explotable desde internet de forma remota sin acceso previo a esa red. La divulgación responsable comunica el hallazgo al mantenedor para que endurezca el eval del script de sesión, que es donde debería sanearse o eliminarse la expansión del identity.

07. 0x06 — Veredicto Comercial

LIMITADO
Hallazgo real con ejecución de código verificada, sin CVE y sin fix conocido en el momento del análisis. La divulgación se hace de forma responsable: se publica el mecanismo y la validación técnica, y se comunica al mantenedor. La mitigación principal es que el vector requiere estar dentro de la WLAN/red local que sirve el gateway; no es explotable desde internet de forma directa.

08. 0x07 — Referencias

ReferenciaDescripción
coova/chilliRepositorio CoovaChilli (Head e0c7d7a, 2026-09-01)
chilli.c:6004Captura del identity → entorno USER_NAME
chilli.c:790-845Sink fork/exec del script de sesión
CWE-78Improper Neutralization of Special Elements in an OS Command
IEEE 802.1X / RFC 2865Protocolo EAP y RADIUS (superficie pre-auth)
LECCION-20260901-001Setuid de un helper no eleva el real_uid de scripts shebang

← Volver al índice de la serie