user@elrise.io:~/2026-08-13-elrise-backend-recompromise
· [critical] incident-responsesecurityphp-fpmazazel

Рецидив через 2 дня: что произошло, когда CVE-патч не равен закрытию инцидента

Все имена хостов, баз, топиков и учётные данные в примерах, кроме явно помеченных как реальные, — вымышленные или placeholder'ы. Совпадения с реальными системами случайны.


TL;DR

Контейнер elrise-backend на tmp-vps 46.36.219.176 был скомпрометирован повторно через 2 дня после закрытия первой CVE (SYS-17 от 10.08 → persistent backdoor от 12.08). Тот же атакующий. Та же сигнатура toolkit'а: busybox sh ybox, comm-masked процессы, /tmp/.perf.c/, /tmp/.xdiag/, /tmp/.r.rpk/. Но механизм второй атаки — другой.

Первая атака (6-9 августа) — через CVE-2012-1823 (PHP-CGI argv injection). Закрыта hardening'ами первой волны 10-12 августа.

Вторая атака (12-13 августа) — через persistent backdoor, оставленный атакующим после первой компрометации: untracked файл compose.override.yaml с network_mode: host, который открыл PHP-FPM на 0.0.0.0:9000 хоста. Файл пролежал 23 часа между инцидентами, никем не замеченный при cleanup.

Главная связь: правки закрыли CVE первой атаки, но persistent backdoor остался незамеченным. Атакующий второй раз не использовал CVE — он переиспользовал foothold от первого раза через другой канал доставки. Recovery-план и approval-сводка намеренно не включены — это отдельный internal workflow. В этом посте — что произошло, что не помогло, и почему.


1. Краткая сводка

Контейнер elrise-backend (PHP-FPM 8.5 production-стек elrise-symfony) был скомпрометирован повторно через 2 дня после закрытия первой CVE. Тот же атакующий.

Первая атака (6-9 августа 2026) — через уязвимость CVE-2012-1823 (PHP-CGI argv injection). Атакующий с IP 211.234.111.116 отправил POST /usr/local/lib/php/PEAR.php — PHP-FPM обработал запрос и вернул HTTP 200. Закрыта 10 августа hardening'ами первой волны: rm PEAR.php, cgi.fix_pathinfo=0, security.limit_extensions=.php.

Вторая атака (12-13 августа 2026) — через persistent backdoor, оставленный атакующим после первой компрометации:

Ключевая связь: правки закрыли CVE первой атаки, но persistent backdoor остался незамеченным. Атакующий не использовал CVE во второй раз — он переиспользовал foothold от первого раза через другой канал доставки.


2. Хронология

Инцидент 1 — 6-9 августа 2026

Дата Событие
До 06.08 Образ elrise-symfony:local собран из Dockerfile (нормальный)
06.08 18:59 Первый признак — создана /tmp/.r.rpk/ (Node.js chroot-майнер)
06-09.08 Малварь работала тихо 3 дня, никто не замечал
09.08 17:43 Сервер перезагружен (нагрузка 47 на 1 CPU)
09.08 17:56 Контейнер elrise-backend стартанул через restart: unless-stopped — с уже активной малварью
09.08 18:28:36 Атакующий с 211.234.111.116 отправил POST /PEAR.php → HTTP 200 (CVE-2012-1823 сработала)
09.08 18:30 Оператор: «проверь 502 на elrise.ru / elrise.io» — обнаружение
09.08 18:38 Оператор: «стопай контейнеры, выключай авторизацию по паролю»
09.08 18:38 docker stop elrise-backend elrise-proxy (одобрено)
09.08 18:39 PasswordAuthentication no для sshd (одобрено)

Cleanup wave 9-12 августа (hardening'и первой волны)

Дата Коммит Hardening
10.08 19:28 74c2179 rm PEAR.php + cgi.fix_pathinfo=0 + security.limit_extensions=.php
10.08 20:02 7211843 FPM-TCP healthcheck (cgi-fcgi-probe.sh) вместо php -r 'exit(0);'
10.08 21:53 847445c league/commonmark ^2.9 (закрыл 4 HIGH + 2 MEDIUM CVE)
12.08 12:19 94ff993 Миграции Doctrine на boot
12.08 12:19 c1cbd15 atomic COPY --chmod=755 для cgi-fcgi-probe.sh
12.08 12:19 df097c6 Login Audit добавлен (только сейчас, через 5 дней после первой атаки)

Что НЕ сделали в cleanup:

Инцидент 2 — 12-13 августа 2026

Дата Событие
12.08 07:29:56 <project-root>/compose.override.yaml создан (birth в filesystem). 70 байт, deploy:deploy. Никогда не в git
12.08 14:24 Dockerfile, compose.yaml, cgi-fcgi-probe.sh, deploy.sh обновлены (hardening и фиксы)
12.08 14:51 Образ elrise-symfony:local пересобран
12.08 15:55:06 Контейнер elrise-backend стартанул. compose.override.yaml автоприменился → FPM биндится на 0.0.0.0:9000 хоста
12.08 16:00-16:21 Burst POST /index.php с 302/422/404 в docker logs (от nginx-proxy 172.25.0.1) — атакующий пробит FastCGI напрямую (минуя nginx)
12.08 21:14 Первые файлы toolkit'а: /tmp/.xdiag/{iu, iui, p, uid, elog, exi, ...}, dropper /tmp/.perf.c/php-fpm (11 MB ELF)
12.08 21:44 Tor microdescriptors скачаны в /tmp/.xdiag/tordata/ (37 MB)
12.08 21:52:55 Старт распаковки архива toolkit'а
12.08 21:53:39 Распаковка завершена (44 секунды)
12.08 22:09 XMRig-майнер /tmp/.perf.c/nsenterpmap (3 MB) + pctl (sha256-идентичен)
12.08 22:09 5 процессов-малвари стартанули, все с PPID=1 (php-fpm master exec через posix_exec/proc_open)
12.08 22:14 /tmp/.r.rpk/ пересоздан (Node.js chroot-майнер)
13.08 02:42 Tor relay state обновлён — Tor активен прямо сейчас
13.08 06:42 Оператор: «проверяй сервер 46» — nsenterpmap жрёт 158-182% CPU
13.08 06:45 Forensic-снимок сохранён
13.08 06:51 Оператор: «стопни контейнер если он не нужен» — docker stop elrise-backend

3. Сравнение двух инцидентов

3.1 Точки входа

Аспект Инцидент 1 (6-9.08) Инцидент 2 (12-13.08)
Метод CVE-2012-1823 — PHP-CGI argv injection Прямой FastCGI к 0.0.0.0:9000 через persistent backdoor
Точка доставки nginx reverse proxy → PHP-FPM docker-proxy / host network → PHP-FPM напрямую
HTTP/HTTPS-следы POST /PEAR.php в nginx access log Ничего в nginx access log — FastCGI минуя nginx
Exploit CVE Да, требует наличия PEAR.php в image Нет, просто открытый порт
Аутентификация Не требовалась Не требовалась
Эксплуатирует нашу защиту? Нет (SYS-17 потом закрыл) Обходит все hardening'и первой волны

3.2 Toolkit и процессы

Аспект Инцидент 1 Инцидент 2
Структура директорий /tmp/.r.rpk/, /tmp/.xdiag/, /tmp/.apid/, /tmp/.perf.c/ Те же + /home/www-data/.atmp/tmp/.applocal.xdiag/ (второй staging путь)
XMRig miner apache2 -DFOREGROUND под маской {xzdiffunlink} через prctl(PR_SET_NAME) nsenterpmap (3 MB ELF, comm-masked, cmdline = php-fpm: master process); второй бинарь pctl — sha256-идентичен
Dropper 2-й rogue php-fpm master (PID 1397, colon-vs-no-colon fingerprint) /tmp/.perf.c/php-fpm (11 MB ELF) рядом с настоящим /usr/local/sbin/php-fpm (30 MB)
Скрытый shell {busybox} sh ybox То же — {busybox} sh ybox
Persistence sleep 300; rm -rf /tmp/.install.pid* (drop+cleanup паттерн) /home/www-data/.config/cron/perfcc (11 MB ELF, mtime обновляется каждые минуты)
C2-канал Не наблюдалось (нет активных соединений к mining pool) cli start accept --token 5Y4B2... (P2P), bitpingd (bandwidth masking), Tor через /tmp/.xdiag/tordata/
Прокси Не наблюдалось /tmp/.xdiag/int/bprox — 9 SOCKS/HTTP прокси (включая Tor SOCKS 115.136.121.54:9050)
Privilege escalation trap Не наблюдалось /tmp/.xdiag/apxj (-rwsr-sr-t) — 0-байтовый файл с SUID+SGID+sticky
PPID всех процессов Различные (некоторые от php-fpm, некоторые от apache2) Все PPID=1 (php-fpm master exec)
Mining pool Не найдено 121.152.99.87:443 (TLS, XMRig) — ESTABLISHED соединения

3.3 Связь между атаками

Связь Доказательство
Тот же атакующий Идентичные сигнатуры: busybox sh ybox, comm-masked процессы, /tmp/.perf.c/, /tmp/.xdiag/, /tmp/.r.rpk/ с Node.js-майнером
Вторая атака переиспользует foothold первой compose.override.yaml создан 12.08 07:29 — через 12 часов после первого стопа, через 8 часов до второго старта. Окно, когда bind-mount :rw ещё активен
Атакующий обновил C2, не точку входа В первой атаке простой drop+exec. Во второй — добавлен P2P C2 (cli start accept), Tor, bitpingd. Это эволюция инструментария, не новая атака
Вторая атака проще по механике Первая — CVE + payload. Вторая — просто открытый FastCGI порт + PHP_VALUE injection. Атакующий shifted left

4. Точки входа — детально

4.1 Первая атака: CVE-2012-1823

Что это: уязвимость PHP-CGI, позволяющая исполнить произвольный PHP-код через специально сформированный URL с - в начале (argv injection в PHP-CGI режиме).

Как эксплуатировалось: POST /usr/local/lib/php/PEAR.php от IP 211.234.111.116. PHP-FPM обработал /usr/local/lib/php/PEAR.php как PHP-скрипт и выполнил. Вернул 200.

Почему сработала:

Что закрыли (SYS-17):

4.2 Вторая атака: прямой FastCGI через persistent backdoor

Что это: после первой компрометации атакующий оставил <project-root>/compose.override.yaml:

services:
  backend:
    network_mode: host
    networks: !reset null

Как эксплуатировалось:

  1. docker-compose автоматически подхватывает compose.override.yaml без -f флага (стандарт Docker Compose)
  2. network_mode: host — контейнер делит сетевое пространство с хостом
  3. PHP-FPM биндится на 0.0.0.0:9000 хоста (а не docker-proxy 127.0.0.1:9000)
  4. Любой в интернете может конектиться к 46.36.219.176:9000 и слать произвольные FastCGI-запросы
  5. networks: !reset null отключает контейнер от elrise-net bridge — elrise-proxy не может дотянуться до backend, что маскирует аномалию
  6. Атакующий слал PHP_VALUE: auto_prepend_file = php://input через FCGI_PARAMS — PHP-FPM выполнил код из POST body

Почему сработало:

Ключевая деталь: network_mode: host — это не то, что добавлялось бы для БД-доступа. Если бы это было добавлено для БД-доступа, не отключали бы bridge (networks: !reset null). Этот ключ сознательно отключает elrise-proxy от backend, чтобы аномалия не была видна через обычные nginx-каналы. Это атакующий.


5. Что не помогло из hardening'ов первой волны

Hardening Что делает Почему не помог против второй атаки
Закрытие первой CVE (rm PEAR.php, cgi.fix_pathinfo=0) Закрывает CVE-2012-1823 Атакующий больше не использовал CVE — использовал FastCGI напрямую
league/commonmark ^2.9 Закрывает 4 HIGH + 2 MEDIUM CVE Не связано с путём входа
FPM-TCP healthcheck (cgi-fcgi-probe.sh) Сигнал «FPM up but sick» Не сигналит «FPM exposed on 0.0.0.0»
Миграции на boot Синхронизация БД Не влияет на сеть
atomic COPY для healthcheck Убирает расщепление COPY + chmod Не влияет на сеть
elrise-proxy mount → :ro Web-shell через proxy невозможен Backdoor был в compose.yaml, не в /app

Главная причина: правки закрыли CVE-вектор первой атаки, но не закрыли параллельный вектор, который атакующий оставил на будущее. Каждый hardening действовал на своём уровне (PHP, зависимости, healthcheck, mounts), но ни один не сделал host-filesystem sweep untracked файлов между инцидентами.


6. Основные проблемы

6.1 Не сделали host-filesystem sweep после первой атаки

compose.override.yaml пролежал с 12.08 07:29 до обнаружения 13.08 06:42 — 23 часа. Если бы git status --ignored или find <project-root> -mtime -7 запустили хотя бы раз в день, файл был бы замечен.

6.2 Bind-mount :rw оставлен для elrise-backend

compose.yaml до сих пор содержит .:/app:rw. Атакующий использует это для записи в <project-root>/. Если бы было :ro, как у elrise-proxy, атакующий не смог бы создать compose.override.yaml (только читать, не писать).

6.3 ports: 0.0.0.0:9000:9000 без firewall

compose.yaml публикует PHP-FPM на 0.0.0.0:9000. Нет iptables-правил, которые блокируют внешний доступ. С network_mode: host это становится публично открытым FastCGI для всего интернета.

6.4 Login Audit добавлен только 12.08

Между 6.08 и 12.08 не было login audit. Если атакующий использовал admin-форму Symfony для посадки compose.override.yaml, мы не знаем когда и кем. Только с 12.08 есть записи.

6.5 restart: unless-stopped поднял контейнер с backdoor

После docker stop 09.08 в 18:38 контейнер был остановлен. Но restart: unless-stopped мог поднять его обратно при рестарте хоста. В этом случае контейнер не поднимался до 12.08 15:55, когда restart: unless-stopped снова сработал при docker compose up. К сожалению, в этот момент compose.override.yaml уже лежал.

6.6 Daemons мониторинга не установлены

После первой атаки были предложены:

6.7 Концепция «CVE-патч = incident closure» неверна

Правки первой волны были CVE-патчами. Они закрыли один класс уязвимостей. Но инцидент = компрометация = требует:

  1. Закрытия уязвимости (сделано)
  2. Forensic анализа файловой системы (НЕ сделано — пропустили persistent backdoor)
  3. Ротации credentials (сделано)
  4. Поиска lateral movement (сделано)
  5. Установки мониторинга (НЕ сделано)

Без всех пяти это CVE-патч, не incident closure.


7. Уроки

  1. CVE-патч ≠ incident closure. Без host-filesystem sweep, мониторинга, ротации credentials и анализа lateral movement это не закрытие инцидента, а частичное закрытие одного вектора.

  2. Persistent backdoor — самый опасный класс атаки. Файл compose.override.yaml с network_mode: host + networks: !reset null — это спроектированный backdoor: открывает 9000 публично И ломает нормальный flow через elrise-proxy (чтобы маскировать аномалию). Один файл, две цели. Не-CVE, не-exploit, просто конфигурация.

  3. bind-mount :rw — это write access в host filesystem. Даже если CVE закрыты, атакующий с RCE может записать что угодно в <project-root>/. Решение: :ro для всех контейнеров, кроме dev-окружения.

  4. compose.override.yaml — самая простая и самая опасная точка. Docker Compose автоматически применяет untracked override-файлы. Никакого audit'а, никакого warning. Любой файл с таким именем в <project-root>/ — потенциальный backdoor.

  5. elrise-proxy unhealthy 20 часов — это был сигнал. Здоровый контейнер не должен быть unhealthy так долго. Если бы был healthcheck-relay, был бы виден алерт в течение 2 минут после первой аномалии. Атакующий даже не пытался это маскировать — он рассчитывал, что unhealthy не мониторят.

  6. Attacker shifted left после CVE-патча. Первая атака — CVE + payload. Вторая — открытый порт + простая PHP_VALUE injection. Если закрыть один класс уязвимостей, атакующий переключается на более простой. Нужно защищать все классы одновременно.

  7. 172.25.0.1 — это bridge gateway, не «наш IP». Когда контейнер в bridge network, REMOTE_ADDR для FPM = gateway. Это затрудняет attribution. Решение: iptables логи на host (до docker-proxy), а не внутри контейнера.

  8. Login Audit через 5 дней — слишком поздно. Если бы был при первой компрометации, мы бы знали, был ли вход в admin-панель в окне 6-9 августа. Без него мы не знаем, как именно был посажен compose.override.yaml.


8. Sources


Что дальше в серии

Это второй материал в цикле про компрометацию elrise-backend. Первый — /reports/2026-08-09-elrise-backend-container-compromise — описывал CVE-вектор первой атаки. Этот — recurrence через persistent backdoor и урок «CVE-патч ≠ incident closure».