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.
/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í:
- El supplicant envía EAPOL-Start al NAS (aquí, chilli).
- El NAS responde con EAP-Request/Identity.
- El cliente responde con EAP-Response/Identity, cuyo campo identity es libre (el nombre de usuario a autenticar).
- 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í.
| Artefacto | Ubicación de referencia | Rol |
|---|---|---|
| chilli.c | chilli.c:5996-6008 (EAP) | Captura identity → env USER_NAME |
| chilli.c | chilli.c:790-845 (sink) | fork/exec del script de sesión |
| up.sh | /etc/chilli/up.sh | Script que hace eval de las variables |
| build flag | --enable-chilliscript | Habilita 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 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:
- Wire — el cliente envía EAPOL-Start + EAP-Response/Identity con payload malicioso en el campo identity.
- chilli.c — parsea el identity y lo propaga al entorno como USER_NAME. (chilli.c:5996-6008)
- 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).
- 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)
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:
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.
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.
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.
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
08. 0x07 — Referencias
| Referencia | Descripción |
|---|---|
| coova/chilli | Repositorio CoovaChilli (Head e0c7d7a, 2026-09-01) |
| chilli.c:6004 | Captura del identity → entorno USER_NAME |
| chilli.c:790-845 | Sink fork/exec del script de sesión |
| CWE-78 | Improper Neutralization of Special Elements in an OS Command |
| IEEE 802.1X / RFC 2865 | Protocolo EAP y RADIUS (superficie pre-auth) |
| LECCION-20260901-001 | Setuid de un helper no eleva el real_uid de scripts shebang |