Все имена хостов, баз, топиков и учётные данные в примерах, кроме явно помеченных как реальные, — вымышленные или 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, оставленный атакующим после первой компрометации:
- Untracked файл
<project-root>/compose.override.yamlсnetwork_mode: hostоткрыл PHP-FPM на0.0.0.0:9000хоста (вместо docker-proxy на 127.0.0.1:9000) - Файл никогда не был в git-репозитории
- Создан 12.08 в 07:29 UTC — через 12 часов после первого стопа контейнера, через 8 часов до второго старта
- Атакующий использовал прямой FastCGI к
0.0.0.0:9000в обход nginx, обходя все hardening'и первой волны
Ключевая связь: правки закрыли 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:
- ❌ Untracked файлы в
<project-root>/не swept —compose.override.yamlостался незамеченным - ❌
bind-mount :rwдляelrise-backendоставлен - ❌
ports: 0.0.0.0:9000:9000оставлен без firewall - ❌ daemons мониторинга (
php-fpm-guard,stuck-process-reaper,healthcheck-relay) предложены, но не установлены
Инцидент 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.
Почему сработала:
cgi.fix_pathinfo=1(стандарт в PHP-FPM alpine) делал FPM выполнять любой URL, который «выглядит как» PHP-файл через PATH_INFOsecurity.limit_extensionsНЕ был включён вphp-fpm.d/www.confupstream-конфиге- В image был
PEAR.phpот composer-deps stage
Что закрыли (SYS-17):
rm /usr/local/lib/php/PEAR.php pearcmd.php peclcmd.phpв Dockerfilecgi.fix_pathinfo=0вzz-hardened.inisecurity.limit_extensions=.phpвzz-fpm-hardening.conf
4.2 Вторая атака: прямой FastCGI через persistent backdoor
Что это: после первой компрометации атакующий оставил <project-root>/compose.override.yaml:
services:
backend:
network_mode: host
networks: !reset null
Как эксплуатировалось:
docker-composeавтоматически подхватываетcompose.override.yamlбез-fфлага (стандарт Docker Compose)network_mode: host— контейнер делит сетевое пространство с хостом- PHP-FPM биндится на
0.0.0.0:9000хоста (а не docker-proxy127.0.0.1:9000) - Любой в интернете может конектиться к
46.36.219.176:9000и слать произвольные FastCGI-запросы networks: !reset nullотключает контейнер отelrise-netbridge —elrise-proxyне может дотянуться до backend, что маскирует аномалию- Атакующий слал
PHP_VALUE: auto_prepend_file = php://inputчерез FCGI_PARAMS — PHP-FPM выполнил код из POST body
Почему сработало:
docker-composeне проверяет наличие untracked override-файлов- iptables FORWARD policy = ACCEPT, нет правил блокировки 0.0.0.0:9000
- Закрытие первой CVE не закрыло FastCGI exposure
restart: unless-stoppedподнял контейнер обратно с уже активным override
Ключевая деталь: 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 мониторинга не установлены
После первой атаки были предложены:
php-fpm-guard— детектирует CPU spike на php-fpmstuck-process-reaper— детектирует orphan/comm-masked процессыhealthcheck-relay— alerting на unhealthy контейнеры- demon-tmp-dotdir-watcher — детектирует dot-директории в
/tmp/(вроде.r.rpk/,.xdiag/,.perf.c/) djn Ни один не установлен. Если быphp-fpm-guardработал, CPU-spike 158% наnsenterpmapбыл бы замечен в течение 30 секунд. Если быhealthcheck-relayработал,unhealthyнаelrise-proxy(20 часов!) алертил бы немедленно.
6.7 Концепция «CVE-патч = incident closure» неверна
Правки первой волны были CVE-патчами. Они закрыли один класс уязвимостей. Но инцидент = компрометация = требует:
- Закрытия уязвимости (сделано)
- Forensic анализа файловой системы (НЕ сделано — пропустили persistent backdoor)
- Ротации credentials (сделано)
- Поиска lateral movement (сделано)
- Установки мониторинга (НЕ сделано)
Без всех пяти это CVE-патч, не incident closure.
7. Уроки
-
CVE-патч ≠ incident closure. Без host-filesystem sweep, мониторинга, ротации credentials и анализа lateral movement это не закрытие инцидента, а частичное закрытие одного вектора.
-
Persistent backdoor — самый опасный класс атаки. Файл
compose.override.yamlсnetwork_mode: host+networks: !reset null— это спроектированный backdoor: открывает 9000 публично И ломает нормальный flow черезelrise-proxy(чтобы маскировать аномалию). Один файл, две цели. Не-CVE, не-exploit, просто конфигурация. -
bind-mount
:rw— это write access в host filesystem. Даже если CVE закрыты, атакующий с RCE может записать что угодно в<project-root>/. Решение::roдля всех контейнеров, кроме dev-окружения. -
compose.override.yaml— самая простая и самая опасная точка. Docker Compose автоматически применяет untracked override-файлы. Никакого audit'а, никакого warning. Любой файл с таким именем в<project-root>/— потенциальный backdoor. -
elrise-proxyunhealthy 20 часов — это был сигнал. Здоровый контейнер не должен быть unhealthy так долго. Если бы былhealthcheck-relay, был бы виден алерт в течение 2 минут после первой аномалии. Атакующий даже не пытался это маскировать — он рассчитывал, что unhealthy не мониторят. -
Attacker shifted left после CVE-патча. Первая атака — CVE + payload. Вторая — открытый порт + простая
PHP_VALUEinjection. Если закрыть один класс уязвимостей, атакующий переключается на более простой. Нужно защищать все классы одновременно. -
172.25.0.1— это bridge gateway, не «наш IP». Когда контейнер в bridge network, REMOTE_ADDR для FPM = gateway. Это затрудняет attribution. Решение: iptables логи на host (до docker-proxy), а не внутри контейнера. -
Login Audit через 5 дней — слишком поздно. Если бы был при первой компрометации, мы бы знали, был ли вход в admin-панель в окне 6-9 августа. Без него мы не знаем, как именно был посажен
compose.override.yaml.
8. Sources
- Первый инцидент в этой серии: 2026-08-09-elrise-backend-container-compromise
- Публикации elrise.io по первому инциденту:
azazel/incident-502-detection— нарративная версия с полным placeholder-redactazazel/azazel-cve-2012-1823-php-fpm— детальный разбор первой CVEazazel/azazel-detection-daemons-monitoring— четыре daemon'а, которые должны были поймать оба инцидентаazazel/azazel-image-rebuild-dockerfile— что нужно поменять в Dockerfile
- Production IP:
46.36.219.176 - Attacker IP первой волны:
211.234.111.116 - Mining pool IP второй волны:
121.152.99.87:443 - Tor SOCKS:
115.136.121.54:9050
Что дальше в серии
Это второй материал в цикле про компрометацию elrise-backend. Первый — /reports/2026-08-09-elrise-backend-container-compromise — описывал CVE-вектор первой атаки. Этот — recurrence через persistent backdoor и урок «CVE-патч ≠ incident closure».