Платформа 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 и поверхность атаки
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) | Нет |
| webshell | HTTP-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, замкнутых на ту же архитектурную проблему:| CVE | CVSS | Severity | Вектор | Версии | EPSS |
|---|---|---|---|---|---|
| CVE-2026-25893 | 10.0 (4.0) | Critical | Heartbeat refresh API | < 1.2.10 | 0.0068 |
| CVE-2026-25938 | 9.5 (4.0) | Critical | Auth bypass (Node-RED) | 1.2.8 – 1.2.10 | 0.0098 |
| CVE-2026-25939 | 9.3 (4.0) | Critical | Scheduler manipulation | 1.2.8 – 1.2.10 | 0.1205 (Top 5%) |
| CVE-2026-25752 | 9.3 (4.0) | Critical | Device tag bypass (WebSocket) | 0.0048 | |
| CVE-2026-13207 | 8.7 (4.0) | High | Dot-segment normalization | <= 1.3.1 | 0.0035 |
| CVE-2026-25951 | 8.6 (4.0) | High | Post-auth (PR:H) nested traversal ....// | < 1.2.11 | 0.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
📚 Часть контента скрыта. Этот материал доступен участникам сообщества с рангом 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:- Initial Access - Exploit Public-Facing Application (T1190): POST-запрос к
/api/uploadбез аутентификации. Предусловие: FUXA доступна из сети атакующего по порту 1881. - Execution - Unix Shell (T1059.004): записанный скрипт выполняется через cron или при перезагрузке FUXA (settings.js как Node.js-модуль).
- Persistence - Web Shell (T1505.003): settings.js с HTTP-listener работает в process space FUXA, переживает рестарты сервиса.
- Command and Control - Ingress Tool Transfer (T1105): через webshell атакующий загружает инструменты для пивота в OT-сегмент.
/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:
- Обновление до FUXA 1.3.3+ - закрывает все описанные CVE кроме GHSA-p69w-mmfv-xrfj (проверять статус advisory).
- Reverse proxy (nginx, HAProxy) перед FUXA с включённой нормализацией путей (
merge_slashes onдля nginx) и deny-правилами для/api/upload,/api/settingsс внешних IP. - Сетевая сегментация: доступ к порту 1881 только с IP инженерных станций. В OT-контексте - отдельный VLAN для HMI с ACL на маршрутизаторе.
- Запуск Docker-контейнера с
--userи без--privileged- ограничивает импакт file write уязвимостей. - Мониторинг: 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-образов
Шаги
- Запустить уязвимую версию:
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). - Включить Secure Mode через Editor в веб-интерфейсе
http://localhost:1881. - Проверить information disclosure:
curl http://localhost:1881/api/settings- JSON с конфигурацией подтверждает CVE-2026-25751. - Проверить 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/. - Для GHSA-p69w-mmfv-xrfj: образ
frangoteam/fuxa:1.3.0(проверить наличие тега черезdocker manifest inspect; при отсутствии - собрать из git-тега v1.3.0) с включённым Node-RED плагином через интерфейс.
Сорок уязвимостей за время существования проекта - это не серия разовых ошибок, это диагноз архитектуре. 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 без единого слоя защиты.
Последнее редактирование модератором: