Статья FUXA SCADA уязвимость RCE: обход Secure Mode через path confusion

Разрезанный промышленный шаровой кран на чёрном антистатическом коврике с обнажённым внутренним механизмом. На латунном фланце выгравирована надпись CVE-2025-69985.


Платформа FUXA, стоящая за этими идентификаторами, - open-source SCADA/HMI на Node.js, которую типично поднимают одной командой docker run -d -p 1881:1881 frangoteam/fuxa:latest и забывают. Secure Mode - единственный встроенный механизм аутентификации FUXA для защиты API-эндпоинтов - оказался настолько дырявым, что каждый патч рождал новый вектор обхода. Ниже - разбор трёх классов pre-auth RCE: от Referer spoofing через path traversal до path confusion с query string, который работает даже при включённой аутентификации Node-RED.

Бизнес-логика атаки: зачем контролировать HMI в OT-сети​

FUXA - не корпоративная SCADA уровня Siemens WinCC или Ignition. Это легковесный веб-интерфейс визуализации и управления, который любят в малых промышленных проектах, IoT-стендах и учебных лабораториях. Архитектурно FUXA работает как gateway между оператором и контроллерами: принимает MQTT-сообщения от PLC, обменивается данными по Modbus RTU/TCP и OPC UA, хранит исторические значения тегов в SQLite или InfluxDB.

Контроль над FUXA-сервером даёт атакующему три вектора воздействия:

Манипуляция тегами PLC. FUXA позволяет записывать значения в Holding Registers по Modbus (Function Code 06 - Write Single Register, FC 16 - Write Multiple Registers) и управлять Coils (FC 05, FC 15) прямо из веб-интерфейса. Через MQTT картина та же: запись значения 1 в топик /devices/buzzer/controls/enabled/on включает физическое устройство на контроллере. После компрометации HMI атакующий получает маппинг тегов к регистрам - он знает, какой регистр отвечает за температуру, скорость или состояние клапана.

Утечка конфигурации OT-сегмента. CVE-2026-25751 (CVSS 9.1, Critical, CWE-306 + CWE-312) позволяет без аутентификации забрать учётные данные InfluxDB и параметры OPC UA-подключений через GET /api/settings.

Пивот в промышленную сеть. На практике FUXA часто развёрнута на том же хосте или в той же VLAN, что и PLC. Компрометация через порт 1881 - начальная точка для lateral movement в OT-сегмент.

Разница с IT-контекстом - принципиальная. RCE на веб-сервере в IT - утечка данных и криптолокер. RCE на SCADA-сервере в OT - манипуляция физическими процессами. TRITON (2017, атака на системы безопасности нефтехимического предприятия) и Industroyer (2016, отключение электроподстанций на Украине) показали, что атакующие целенаправленно ищут именно такие точки входа.

Архитектура Secure Mode FUXA и поверхность атаки​

366px-Scada_fuxa_editor_button.webp

FUXA построена на Express.js и по умолчанию слушает порт 1881. Проект активно развивается - на момент публикации последний релиз 1.3.3 доступен в npm-реестре (пакет @frangoteam/fuxa, по данным Snyk), патчи для описанных ниже CVE выпущены в версиях 1.2.10, 1.2.11 и 1.3.2.

Аутентификация реализована через флаг runtime.settings.secureEnabled, который активирует JWT-проверку в middleware secureFnc (файл server/api/jwt-helper.js). При включённом Secure Mode secureFnc должен проверять каждый запрос к защищённым эндпоинтам.

Проблема - в том, как secureFnc подключается к маршрутам. FUXA использует смешанный подход: часть эндпоинтов регистрируется с middleware, часть - без. По данным публичных PoC (EDB-52544, EDB-52568) и advisory, вот что доступно без аутентификации:

ЭндпоинтНазначениеAuth middleware
GET /api/settingsПолная конфигурация сервераОтсутствует
POST /api/uploadЗагрузка файлов на серверОтсутствует
GET /api/versionВерсия APIОтсутствует

Это не баг конфигурации - это архитектурный просчёт. Каждый раз, когда разработчик добавляет новый маршрут через prjApp.post(), он должен явно подключить secureFnc. Забыл - получи новый pre-auth вектор.

Дефекты фундамента: hardcoded secret и heartbeat bypass​

Помимо неконсистентного применения middleware, фундамент имеет ещё две системные слабости:

Hardcoded JWT secret (CVE-2026-25894, CVSS 9.5 Critical по CVSS 4.0, CWE-321 + CWE-1188): если администратор не задал кастомный секрет, используется захардкоженный fallback. Атакующий генерирует валидный JWT-токен с правами администратора, не зная пароля. Исправлено в 1.2.10.

Один GET-запрос - полные права. Исправлено в 1.2.10.

На практике Secure Mode FUXA до версии 1.2.10 - декларация, а не защита. Но и после 1.2.10 ситуация принципиально лучше не стала.

Три класса pre-auth RCE в FUXA SCADA​

CVE-2025-69985: обход JWT через заголовок Referer​

[Применимо: внешний и внутренний пентест, FUXA <= 1.2.8]

CVSS 3.1: 9.8 Critical (AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H)
CWE: CWE-288 (Authentication Bypass Using an Alternate Path or Channel)
EPSS: 0.0563, перцентиль 0.92 (Top 10%) - высокая вероятность эксплуатации
Пакет: @frangoteam/fuxa, last_affected 1.2.8 (по данным OSV.dev)

Middleware jwt-helper.js проверяет HTTP-заголовок Referer: если он содержит адрес самого сервера, запрос считается "внутренним" и пропускается без JWT-проверки. Атакующий добавляет Referer: http://target:1881/fuxa к POST-запросу на /api/runscript - и middleware его пропускает. Вот так просто.

Endpoint /api/runscript принимает произвольный JavaScript-код и выполняет его через child_process.execSync в контексте Node.js-процесса FUXA. Публичный PoC (EDB-52544, автор Joshua van der Poll) демонстрирует полный захват с выводом stdout - достаточно передать заголовок Referer и JSON-тело с полем script.code.

Когда не работает: FUXA >= 1.2.9. В 1.2.9 Referer-проверку удалили. Но параллельно обнаружились новые векторы.

CVE-2026-25895: pre-auth path traversal через /api/upload​

[Применимо: внешний и внутренний пентест, FUXA <= 1.2.9]

CVSS 4.0: 9.5 Critical (AV:N/AC:L/AT/PR:N/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H)
CWE: CWE-22 (Path Traversal) + CWE-306 (Missing Authentication for Critical Function)
EPSS: 0.0267, перцентиль 0.84 - выше медианы
Пакет: fuxa-server, fixed 1.2.10 (по данным OSV.dev)

Эндпоинт POST /api/upload зарегистрирован полностью без middleware (EDB-52568, автор Anthony Cihan). Внутри обработчика JSON-поле destination конкатенируется с appDir через path.resolve():
JavaScript:
let destinationDir = path.resolve(
  runtime.settings.appDir, `_${destination}`
);
// destination = "a/../../../../etc/cron.d"
// path.resolve() выходит за пределы appDir
filePath = path.join(destinationDir, fileName);
fs.writeFileSync(filePath, basedata, encoding);
К полю destination добавляется только символ _ как префикс - никакой нормализации и проверки containment. Payload вида a/../../../../<ЦЕЛЕВАЯ_ДИРЕКТОРИЯ> заставляет path.resolve() выйти за пределы appDir и записать файл куда угодно на файловой системе.

При file.type != 'svg' данные декодируются из Base64, при file.type == 'svg' - записываются как есть (raw text). Поля fullPath и fileName фильтруют последовательности .., поэтому path traversal контролирует только директорию назначения, но не вложенность имени файла.

PoC-эксплойт FUXAPWN (репозиторий Hann1bl3L3ct3r/FUXAPWN) реализует пять режимов post-exploitation:

РежимМеханизм RCEЗадержкаТребует root
settings-rceПерезапись settings.js с webshell-модулемПри рестарте FUXAНет
cronЗапись задания в /etc/cron.d/До 60 секДа
ssh-keyЗапись authorized_keysМгновенно (при SSH)Нет
webshellHTTP-listener внутри процесса FUXAПри рестартеНет
dropПроизвольная запись файлаN/AЗависит от пути

GHSA-p69w-mmfv-xrfj: path confusion через query string​

[Применимо: внешний и внутренний пентест, FUXA 1.3.0 с включённым Node-RED]

Severity: High (по данным advisory GHSA-p69w-mmfv-xrfj; CVE-ID не присвоен в NVD на момент написания)
CWE: CWE-290 (Authentication Bypass by Spoofing) - по классификации аналогичных CVE в FUXA

Эта FUXA SCADA уязвимость RCE интересна тем, что она обходит Secure Mode даже при одновременно включённых secureEnabled и nodeRedAuthMode. Появилась в коде уже после патчей для CVE-2025-69985. Примечание: CVE-2026-25938 (CWE-290 + CWE-306, CVSS 9.5, версии 1.2.8–1.2.10) описывает схожий вектор - auth bypass при включённом Node-RED. GHSA-p69w-mmfv-xrfj может быть связанной или повторной уязвимостью того же класса в более поздних версиях; точное соотношение между ними требует уточнения по данным maintainer.

По данным advisory, auth middleware определяет необходимость проверки через substring-match на полный URL запроса:
JavaScript:
// Уязвимый паттерн (GHSA-p69w-mmfv-xrfj, CVE-ID не присвоен)
const url = req.originalUrl || req.url || req.path;
if (url.includes('/socket.io')) return next();
// Исправление: использовать req.path вместо req.originalUrl
const pathname = req.path;
if (pathname.startsWith('/socket.io/')) return next();
req.originalUrl в Express содержит полный URL включая query string. Запрос GET /nodered/admin?x=/socket.io проходит проверку url.includes('/socket.io') - условие истинно, middleware вызывает next() без JWT-проверки. Атакующий получает доступ к административным эндпоинтам Node-RED. Если там развёрнуты ноды с возможностью выполнения команд (exec-нода, function-нода с child_process), это RCE.

Два момента в исправлении: (1) req.path вместо req.originalUrl - Express pre-парсит path, удаляя query string; (2) startsWith вместо includes - иначе URL вида /admin/socket.io тоже прошёл бы проверку.

Когда не работает: без включённого плагина Node-RED. Без него эндпоинты /nodered/* не существуют и bypass бесполезен. По данным advisory, уязвимость актуальна для конфигураций, где Node-RED используется для автоматизации логики управления - типичный сценарий для промышленных IoT-проектов.

Сопутствующие CVE: паттерн системных дефектов в FUXA​

Полная картина обхода аутентификации включает ещё ряд CVE, замкнутых на ту же архитектурную проблему:

CVECVSSSeverityВекторВерсииEPSS
CVE-2026-2589310.0 (4.0)CriticalHeartbeat refresh API< 1.2.100.0068
CVE-2026-259389.5 (4.0)CriticalAuth bypass (Node-RED)1.2.8 – 1.2.100.0098
CVE-2026-259399.3 (4.0)CriticalScheduler manipulation1.2.8 – 1.2.100.1205 (Top 5%)
CVE-2026-257529.3 (4.0)CriticalDevice tag bypass (WebSocket)0.0048
CVE-2026-132078.7 (4.0)HighDot-segment normalization<= 1.3.10.0035
CVE-2026-259518.6 (4.0)HighPost-auth (PR:H) nested traversal ....//< 1.2.110.0122

CVE-2026-25939 (CWE-862, Missing Authorization) стоит выделить отдельно: EPSS score 0.1205, percentile ~95.6 (Top 5% по вероятности эксплуатации) - самый высокий показатель среди всех FUXA CVE. Атакующий без аутентификации создаёт произвольные планировщики, которые выполняют действия в контексте FUXA. В OT-среде это позволяет запланировать периодическую перезапись тегов PLC - по сути, persistence-механизм, встроенный в легитимные функции SCADA.

CVE-2026-13207 (CWE-290, CVSS 8.7, High) показывает, что проблема не ограничена старыми версиями. Уязвимость затрагивает FUXA <= 1.3.1 - уже после серии патчей 1.2.10 и 1.2.11. API-роутер не нормализует dot-segment последовательности перед применением middleware: запрос GET /api/./users или GET /api/project/../users возвращает данные пользователей и ролей без аутентификации. CISA выпустила advisory ICSA-26-181-02 по этой уязвимости, оценив Technical Impact как partial и Automatable как yes. Исправлено в версии 1.3.2.

Эксплуатация CVE-2026-25895: от file write к root shell​

1783107905011.webp

📚 Часть контента скрыта. Этот материал доступен участникам сообщества с рангом One Level или выше
Получить доступ просто — достаточно зарегистрироваться и проявить активность на форуме


Listener защищён рандомным токеном (24 hex-символа) через заголовок X-Auth-Token и секретным path. Запросы без токена или на другой path получают 404 - для сканера эндпоинт невидим. Обработка ошибок (server.on('error')) перехватывает EADDRINUSE, чтобы неудачная привязка порта не уронила FUXA. Для срабатывания нужен рестарт FUXA - в Docker-окружении это orchestrator health check, docker restart, или kill процесса с автоматическим перезапуском.

Cron-based RCE при запуске от root​

Если FUXA работает от root - а это стандартная ситуация для docker run без --user - запись в /etc/cron.d/ даёт RCE с задержкой до 60 секунд. Демон cron автоматически перечитывает файлы в этой директории, новое задание выполняется без рестарта сервиса.

Где cron-метод не работает: при запуске от непривилегированного пользователя /etc/cron.d/ недоступен на запись. Пользовательские crontab в /var/spool/cron/crontabs/<user> требуют mode 0600 и правильного владельца - fs.writeFileSync() создаёт файл с mode 0644, что принимает cronie (RHEL), но отклоняет Vixie cron (Debian/Ubuntu). В минимальных Alpine-контейнерах cron может отсутствовать вообще.

Место в kill chain и MITRE ATT&CK​

Полная цепочка через CVE-2026-25895 маппится на MITRE ATT&CK:
  1. Initial Access - Exploit Public-Facing Application (T1190): POST-запрос к /api/upload без аутентификации. Предусловие: FUXA доступна из сети атакующего по порту 1881.
  2. Execution - Unix Shell (T1059.004): записанный скрипт выполняется через cron или при перезагрузке FUXA (settings.js как Node.js-модуль).
  3. Persistence - Web Shell (T1505.003): settings.js с HTTP-listener работает в process space FUXA, переживает рестарты сервиса.
  4. Command and Control - Ingress Tool Transfer (T1105): через webshell атакующий загружает инструменты для пивота в OT-сегмент.
Для GHSA-p69w-mmfv-xrfj цепочка аналогична, но Initial Access идёт через Node-RED admin endpoints вместо /api/upload, а Execution - через Node-RED flow с exec-нодой.

Чем OT-контекст меняет импакт ICS RCE уязвимости​

Стандартные IT-средства защиты (EDR, SIEM, host-based IDS) в OT-сегменте либо отсутствуют, либо работают в пассивном режиме мониторинга. Промышленные IDS (Claroty, Dragos, PT ISIM) обнаружат аномальные Modbus-команды, но компрометация через HTTP-порт 1881 - легитимный трафик для HMI. Корреляция HTTP-логов FUXA с промышленным трафиком в большинстве OT-инфраструктур не настроена. И это ключевое слепое пятно.

Каскадный эффект. В отличие от IT-сервера, который можно изолировать и переразвернуть, OT-процесс нельзя остановить без физических последствий. Атакующий с доступом к планировщикам FUXA (CVE-2026-25939) создаёт задание, которое периодически модифицирует теги PLC через Modbus FC 06 / FC 16 - обнаружение потребует корреляции на уровне SIEM, собирающего и HTTP-логи FUXA, и зеркалированный Modbus-трафик.

Отсутствие аутентификации на уровне протокола. Modbus TCP не предусматривает аутентификации - любой хост в VLAN может слать команды на PLC. Компрометация FUXA-сервера, у которого уже есть сетевой доступ к PLC, снимает необходимость отдельного сканирования OT-сегмента. Атакующий использует существующие подключения FUXA как готовый канал управления.

Предусловия, ограничения и контрмеры​

ТехникаНе работает если
CVE-2025-69985 (Referer spoofing)FUXA >= 1.2.9
CVE-2026-25895 (path traversal /api/upload)FUXA >= 1.2.10; reverse proxy с нормализацией путей
GHSA-p69w-mmfv-xrfj (path confusion query string)Node-RED не включён; патч по GHSA-p69w-mmfv-xrfj
CVE-2026-13207 (dot-segment normalization)FUXA >= 1.3.2
Cron-based RCE (post-exploitation)FUXA запущена не от root; cron отсутствует в контейнере
Settings.js overwrite (post-exploitation)Нет возможности вызвать рестарт FUXA-процесса

Контрмеры, работающие независимо от конкретной CVE:
  1. Обновление до FUXA 1.3.3+ - закрывает все описанные CVE кроме GHSA-p69w-mmfv-xrfj (проверять статус advisory).
  2. Reverse proxy (nginx, HAProxy) перед FUXA с включённой нормализацией путей (merge_slashes on для nginx) и deny-правилами для /api/upload, /api/settings с внешних IP.
  3. Сетевая сегментация: доступ к порту 1881 только с IP инженерных станций. В OT-контексте - отдельный VLAN для HMI с ACL на маршрутизаторе.
  4. Запуск Docker-контейнера с --user и без --privileged - ограничивает импакт file write уязвимостей.
  5. Мониторинг: Sigma-правило на POST-запросы к /api/upload и /api/runscript без предшествующей аутентификации на /api/signin.

Минилаб для воспроизведения​

Требования к окружению​

  • ОС: Ubuntu 22.04+ или Debian 12 (хост для Docker)
  • RAM: минимум 2 ГБ, рекомендуется 4 ГБ (FUXA + опциональный OpenPLC)
  • Docker: 20.10+
  • Инструменты: curl, Python 3.10+ с requests, Burp Suite Community для инспекции HTTP
  • Сеть: изолированная сеть или loopback; интернет для загрузки Docker-образов

Шаги​

  1. Запустить уязвимую версию: docker run -d -p 1881:1881 frangoteam/fuxa:1.2.9 (предварительно проверить наличие тега: docker manifest inspect frangoteam/fuxa:1.2.9; если тег отсутствует - собрать из git-тега v1.2.9 репозитория frangoteam/FUXA).
  2. Включить Secure Mode через Editor в веб-интерфейсе http://localhost:1881.
  3. Проверить information disclosure: curl http://localhost:1881/api/settings - JSON с конфигурацией подтверждает CVE-2026-25751.
  4. Проверить path traversal: POST на /api/upload с destination: "a/../../../../../../tmp", file.name: "testfile", file.data: "dGVzdA==" (Base64 "test"), file.type: "bin". Проверить наличие файла в /tmp/ внутри контейнера: docker exec <id> ls /tmp/.
  5. Для GHSA-p69w-mmfv-xrfj: образ frangoteam/fuxa:1.3.0 (проверить наличие тега через docker manifest inspect; при отсутствии - собрать из git-тега v1.3.0) с включённым Node-RED плагином через интерфейс.
Опционально: подключить эмулятор PLC через OpenPLC Runtime по Modbus TCP (порт 502) для полной визуализации цепочки "exploit -> изменение регистра PLC". На HackerLab.pro (https://hackerlab.pro) можно потренировать аналогичные path traversal и middleware bypass цепочки в Web-категории - формально это задачи CTF, но механика ломающегося middleware там ровно та же, что и в FUXA.

Сорок уязвимостей за время существования проекта - это не серия разовых ошибок, это диагноз архитектуре. Express-middleware как единственный барьер аутентификации в SCADA-системе - решение, которое закономерно приводит к тому, что каждый новый маршрут или query-параметр открывает очередной bypass. Патч 1.2.9 закрыл Referer spoofing - и тут же обнаружился path traversal в upload. Патч 1.2.11 закрыл nested traversal - через несколько месяцев нашли dot-segment bypass в 1.3.1. Каждый фикс - заплатка, а не переработка фундамента.

На мой взгляд, корень в подходе "authentication as middleware": в зрелых SCADA-платформах аутентификация встроена на уровне session gateway, а не навешивается на каждый маршрут отдельно. FUXA полагается на то, что разработчик не забудет добавить secureFnc к каждому новому app.post(). В проекте с десятками эндпоинтов это не вопрос "если забудут", а "когда".

Для OT-среды вывод простой: open-source HMI на Node.js допустим для лаборатории и прототипирования, но не для продакшена с реальными PLC - без reverse proxy, сегментации и мониторинга промышленных протоколов. Кто считает иначе - пусть посмотрит на CVE-2026-25939 с EPSS 0.12 и прикинет, сколько FUXA-инстансов прямо сейчас торчат в Shodan без единого слоя защиты.
 
Последнее редактирование модератором:
Мы в соцсетях:

Взломай свой первый сервер и прокачай скилл — Начни игру на HackerLab

🚀 Первый раз на Codeby?
Гайд для новичков: что делать в первые 15 минут, ключевые разделы, правила
Начать здесь →
🧭 Навигатор · ИБ 2026
Не знаешь, какой трек твой?
5 направлений ИБ, реальные зарплаты и точка входа для каждого — в одном треде.
JuniorSenior+
100K → 600K+ ₽ /мес
Открыть навигатор →
🔴 Свежие CVE, 0-day и инциденты
То, о чём ChatGPT ещё не знает — обсуждаем в реальном времени
Threat Intel →
💼 Вакансии и заказы в ИБ
Pentest, SOC, DevSecOps, bug bounty — работа и проекты от проверенных компаний
Карьера в ИБ →

HackerLab