
POC exploit for CVE-2026-25895 FUXA Unauthenticated Path Traversal -> Arbitrary File Write -> RCE
Unauthenticated, pre-auth arbitrary file write against FUXA, a Node.js-based
SCADA/HMI platform. Chains to remote code execution via several distinct
post-write primitives. Works even when secureEnabled = true (authentication
on) because the vulnerable endpoint has no middleware attached.
Also available on ExploitDB: https://www.exploit-db.com/exploits/52568
| Field | Value |
|---|---|
| CVE ID | CVE-2026-25895 |
| Affected | FUXA <= 1.2.9 |
| Patched | FUXA 1.2.10 |
| Vendor | frangoteam / FUXA |
| Attack vector | Network (HTTP/HTTPS) |
| Authentication | None required |
| Impact | Arbitrary file write, remote code execution |
| CVSS v3.1 | 9.8 (Critical) — AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H |
| Researcher | Anthony Cihan (Hann1bl3L3ct3r) |
FUXA's POST /api/upload endpoint (server/api/projects/index.js:193) is
registered without middleware, bypassing both the secureFnc JWT / API-key
check and the admin permission gate applied to every other project-management
endpoint. Inside the handler, the JSON body field destination is concatenated
into a filesystem path with only a leading underscore and no normalization or
containment check:
let destinationDir = path.resolve(runtime.settings.appDir, `_${destination}`);
filePath = path.join(destinationDir, fullPath || fileName);
fs.writeFileSync(filePath, basedata, encoding);
A destination value of the form a/../../../../../etc (where a absorbs
the leading underscore prefix) makes Node's path.resolve climb out of
appDir to any location the FUXA process can write. Because fs.writeFileSync
is preceded by a conditional fs.mkdirSync(dir, { recursive: true }), the
attacker can also create parent directories as needed.
The result is an unauthenticated arbitrary file write primitive reachable on
the default HTTP port (1881), exploitable pre-auth regardless of whether
the FUXA admin has enabled login.
An unauthenticated remote attacker can:
settings.js to achieve code execution on the next FUXA restart/etc/cron.d/<file>) for code execution within 60 seconds
when FUXA runs as root (the default in the vendor's Docker image)/root/.ssh/authorized_keys or any user's
~/.ssh/authorized_keysThis is a pre-auth critical severity finding on an ICS/SCADA platform used to operate industrial processes.
| Version | Status |
|---|---|
<= 1.2.9 | Vulnerable |
1.2.10+ | Patched |
Confirmed exploitable against a clean install of FUXA 1.2.9 on Ubuntu Server.
Single-file Python 3 script, one third-party dependency (requests).
pip install requests
python3 fuxapwn.py --help
Identify the running OS user, Node-RED exposure, and any other accounts on the host without writing anything unusual:
python3 fuxapwn.py -u http://target:1881 --mode recon \
--probe-root --probe-home
Write a neutral /tmp/healthcheck marker (no CVE-specific IOCs in the
filename or content):
python3 fuxapwn.py -u http://target:1881 --mode canary
If FUXA runs as root, drop a cron file that fires within 60 seconds with
no FUXA restart required:
python3 fuxapwn.py -u http://target:1881 --mode cron \
--cron-cmd 'id > /tmp/fx.txt 2>&1'
settings.js replacementInstall an HTTP webshell listener inside the FUXA Node process (activates on
next cold start, since require() caches modules), then drop into an
interactive REPL once FUXA restarts:
# 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
| Mode | Purpose |
|---|---|
recon | Unauthenticated info leak via GET /api/settings; infers running OS user from absolute paths; reports Node-RED status; optional --probe-root and --probe-home active probes |
canary | Proof of the file-write primitive with a neutral default path |
settings-rce | Replaces settings.js with a payload that runs a configurable command on the next FUXA cold start |
ssh-key | Writes a public key to a target user's authorized_keys |
drop | Arbitrary file drop to any absolute path |
cron | Drops /etc/cron.d/<name> for RCE within 60 seconds without waiting for a FUXA restart (requires FUXA running as root) |
webshell | Installs an HTTP webshell listener inside the FUXA Node process via settings.js replacement (activates on next cold start) |
webshell-exec | Client for an already-installed webshell; single command or --interact REPL |
Full per-mode flag reference: python3 fuxapwn.py --help.
GET /api/settings (server/api/index.js:103) is registered without
middleware and returns the live runtime configuration, lightly redacted
(the server deletes secretCode and smtp.password before sending). The
remaining fields leak absolute paths (appDir, workDir, userSettingsFile,
logsDir, uploadFileDir) that typically identify the service user, plus
nodeRedEnabled, which is a direct tell for a secondary unauthenticated
RCE path (see below).
When FUXA is launched via npm start under a local user account and install
paths don't encode the user (e.g. the install lives under /opt, /tmp, or
a generic /app), and /root/ is not writable, the POC falls back to
iterating /home/<candidate>/ with zero-byte writes.
The FUXA upload handler conditionally calls
fs.mkdirSync(parent, { recursive: true }) before fs.writeFileSync, which
creates an EACCES ambiguity: a non-existent /home/<user>/ fails with
EACCES on the mkdir attempt (the process cannot create directories under
root-owned /home/), while an existing /home/<other>/ with mode 0700
fails EACCES on the write itself. Same errno, different meaning. The POC
disambiguates by parsing the syscall token out of the server-forwarded
err.message (libuv format "<CODE>: <reason>, <syscall> '<path>'") and
only reports EACCES-on-open (or any non-mkdir syscall) as "other user
exists." This eliminates the false-positive lateral-movement list that a
naive errno-only probe would produce.