Пост-мортем взлома Zimbra: майнер, sudo nginx и root за сутки

В автосервисе на Тверской, 22 рабочих места, почтовый сервер стоял с 2019-го, и его боялись трогать: работает — не лезь. В январе 2026 он перестал работать. Zimbra падала сразу после старта, грузила процессор в потолок, SQL умирал молча. Клиент написал: «кажется, нас взломали». Он был прав, но не в том, о чём думал. Ниже — как я разбирал этот инцидент по шагам, где ошибся сам и почему «убить майнер» и «вычистить взлом» — две разные работы.

Что сказал клиент и что показала первая минута

Заявка звучала как техническая авария. «Zimbra при перезапуске сразу падает, грузит процессор, падает SQL». Классический набор симптомов, за которым девять раз из десяти стоит переполненный диск или разъехавшийся LDAP. Я так и подумал.

Сервер — Zimbra 9.0.0 Network Edition на CentOS 7.9. Сама связка уже говорит о многом: ОС снята с поддержки 30 июня 2024 года, обновлений нет ни у неё, ни у почты. Патч-уровень оказался P39 — то есть отставание больше чем на год.

Первое, что я сделал, — посмотрел на нагрузку. Не в рабочее время, вечером. И увидел ровное плато загрузки CPU под 100% там, где ночью в автосервисе на 22 человека не должно быть ничего.

top -b -n1 | head -20
# %Cpu(s): 98.7 us,  1.1 sy ...
# один процесс javab держит 780% CPU (8 ядер), запущен от zimbra

Вот тут версия про «не хватает памяти» рассыпалась. Память была свободна. CPU ел чужой процесс с именем javab — почти как java, чтобы не бросался в глаза в списке. Это майнер. А симптомы, на которые жаловался клиент, — не три поломки, а следствие одного заражения.

Почему молча умирал MySQL — ложная версия про InnoDB

Отдельно меня сбило поведение базы. mysqld под Zimbra падал без единой строки в mysql_error.log. Только сухое Number of processes running now: 0. OOM-killer молчал, в dmesg — чисто.

Я потратил минут сорок на версию про повреждение InnoDB после аварийного выключения. Гонял recovery, поднимал базу вручную — она поднималась, работала crash-recovery нормально, а через минуту снова умирала. Ошибок нет, а процесс исчезает. Так InnoDB себя не ведёт.

Правильная мысль пришла, когда я перестал чинить базу и стал смотреть, кто её убивает. Нашёл в /dev/shm скрытый файл .rguard — «сторож» майнера. Это чистильщик конкурентов: в вечном цикле он рассылает kill -9 по маскам процессов, которые считает соперниками за CPU. mysqld попадал под раздачу.

ls -la /dev/shm
# -rwxr-xr-x 1 zimbra zimbra 5.5M javab
# -rwxr-xr-x 1 zimbra zimbra  18K .rguard   (сторож, kill -9 по маскам)
# -rwxr-xr-x 1 zimbra zimbra  42K .khp      (загрузчик)

Урок на будущее: когда база умирает молча — ищи внешнего убийцу, а не чини InnoDB. Тихая смерть без логов — это чужой kill, а не внутренний сбой движка.

Как поднялись до root: sudo nginx и запись в sudoers.d

Майнер крутился от пользователя zimbra. Но следы вели выше — к правам root. Мне нужно было понять вектор повышения привилегий, иначе чистка бессмысленна.

Виновником оказалось штатное на вид правило sudo. У Zimbra есть служебная строка, разрешающая пользователю zimbra запускать nginx без пароля:

grep -r nginx /etc/sudoers /etc/sudoers.d
# %zimbra ALL=NOPASSWD:/opt/zimbra/common/sbin/nginx

Правило без ограничения аргументов. А это значит, что zimbra может запустить nginx со своим конфигом. Атакующий подсунул конфиг, где access_log пишется прямо в /etc/sudoers.d/, а формат лога назначен так, что в файл попадает HTTP-заголовок запроса. Дальше — один curl с заголовком вида zimbra ALL=(ALL) NOPASSWD:ALL, и nginx сам дописывает эту строку в sudoers. Всё, у процесса-владельца веб-сервера появился беспарольный root.

Красиво и тихо. Ни одного эксплойта ядра, ни одного бинарника-дроппера на этом этапе — легитимная служба записала атакующему права своими руками. Именно поэтому антивирус на такое не реагирует: файлов-вредоносов в момент эскалации нет.

Проверьте у себя за 2 минуты

Прежде чем читать дальше, посмотрите на свой сервер. Три команды, меньше двух минут, никаких изменений — только чтение. Запускать от root на хосте Zimbra.

# 1. кто ест CPU в нерабочие часы
top -b -n1 | head -15

# 2. что лежит в разделах, где вредонос любит прятаться
ls -la /dev/shm /tmp /var/tmp

# 3. свежие юниты и sudo-правила, появившиеся после установки
systemctl list-unit-files --state=enabled | tail -30
grep -rn nginx /etc/sudoers /etc/sudoers.d 2>/dev/null
Что увиделиКак трактовать
Ровная загрузка CPU 80–100% ночью, процесс не из списка служб ZimbraПочти наверняка майнер. Дальше — не убивать сгоряча, читайте про сторожа
Исполняемые файлы в /dev/shm или /tmp со скрытым именем (точка в начале)Персистентный payload. В штатной Zimbra их там нет
sudo-правило на nginx без фиксированных аргументовОткрытый путь к root. Ужесточать до конкретных команд
Юнит с общим именем (logger, journallogging, syslog-helper), которого нет в свежей CentOSМаскировка. Проверять ExecStart руками

Если совпал хотя бы первый пункт — сервер, скорее всего, работает не только на вас. Дальше показываю, почему просто «убить процесс» не помогает.

Первая чистка, которая не удержалась — моя ошибка

Дальше я сделал ровно то, что просится: снял автозапуск, убил сторожа, убил майнер, удалил файлы из /dev/shm, поднял базу. Проверил — CPU в норме, Zimbra запустилась, все сервисы Running. Отчитался, что чисто.

Заражение вернулось за две минуты. Тот же javab, тот же сторож, тот же 100% CPU. Я недоглядел.

Ошибка была в том, что я снял два слоя из трёх. Убил сторожа и майнер (runtime), но не тронул корневой мастер. А им оказался systemd-юнит с невинным именем:

cat /etc/systemd/system/journallogging.service
# [Unit] Description=System Logger
# [Service] User=root  Restart=always  RestartSec=10
# ExecStart=/lib/udev/.logsvc

Юнит назван «System Logger», лежит среди системных, бинарь спрятан в /lib/udev/ под видом udev-хелпера, работает от root и перезапускается каждые 10 секунд. Пока он жив, он передропливает cron и payload быстрее, чем я успеваю их удалять. Это и есть разница между «почистить майнер» и «вычистить взлом»: майнер — симптом, мастер-юнит — болезнь.

Так я на этом кейсе усвоил порядок, который теперь не нарушаю: сначала снять все слои персистентности атомарно, и только потом трогать процессы. Иначе — бесконечный цикл, в котором ты проигрываешь по времени.

Атакующий был онлайн — и почему это меняло всё

Пока я разбирал персистентность, всплыла деталь, из-за которой любые правки теряли смысл. На сервере были живые SSH-сессии, которых там быть не должно.

ss -tnp | grep :22
last -a | head
grep 'Accepted password for root' /var/log/secure | tail

В sshd_config стояло PasswordAuthentication yes и PermitRootLogin yes, а в логах — успешные входы root по паролю. Плюс нашлась UID-0 учётка adminner с пустым паролем в /etc/shadow, добавленная в AllowUsers, и посторонний ed25519-ключ в authorized_keys пользователя zimbra. То есть у атакующего было несколько независимых входов, и он мог сидеть внутри в реальном времени.

Вывод простой и жёсткий: пока хост доступен по SSH из интернета с валидными кредами, чистить его — толочь воду. Он передропит всё, что ты снял, за минуты. Сначала изоляция: закрыть SSH из мира на уровне firewall, оставить доступ только со своих управляющих IP, оборвать активные сессии. И лишь потом — снятие персистентности. Рядом стоял второй хост клиента, с которого шли входы, — предполагаемый плацдарм. Доступа к нему у нас не было, и это отдельно тревожно: даже идеально вычищенная Zimbra ничего не стоит, если соседняя машина заражена и держит ключи.

Что я проверял на соседних машинах и в самих ящиках

Изоляция закрыла дверь на этом хосте. Но в той же сети стояли другие машины клиента, и пока я не знал, чистые они или нет, любой мой отчёт был бы враньём. Ключ, которым атакующий заходил на Zimbra, мог лежать где угодно ещё, и тогда вся зачистка — просто отсрочка на неделю.

По соседям, до которых мы дотянулись, я шёл по одному короткому списку. Публичная часть постороннего ed25519-ключа — самый дешёвый межхостовый индикатор: это одна строка, её достаточно поискать по всем домашним каталогам. Дальше учётки с UID 0, юниты systemd с датой создания позже установки системы, исполняемые файлы в /dev/shm, /tmp и /var/tmp, установленные исходящие соединения на нестандартные порты.

grep -rn 'AAAAC3NzaC1lZDI1NTE5' /root/.ssh /home/*/.ssh /opt/*/.ssh 2>/dev/null
find /etc/systemd/system -name '*.service' -newermt '2025-06-01'
awk -F: '$3==0{print $1}' /etc/passwd
ss -tnp state established | grep -v ':22 '

Минут по сорок на машину, только чтение, ничего не меняем. Файловый сервер и шлюз прошли чисто. До того самого второго хоста мы так и не дотянулись — и это единственная дыра, которая в этой картине осталась.

Вторую половину проверки пропускают почти всегда, а она про саму почту. У атакующего был root на почтовом сервере — читать переписку он мог и без веб-шелла, прямо из /opt/zimbra/store. Зато постоянный вынос почты наружу оставляет следы в конфигурации, и вот они проверяются по списку: переадресация на ящиках, правило redirect в фильтрах Sieve, лишние администраторы, посторонние делегированные права на чужие папки.

zmprov -l gaa | while read a; do
  zmprov ga "$a" zimbraPrefMailForwardingAddress zimbraMailSieveScript | grep -q . && echo "$a"
done
zmprov -l gaa | xargs -n1 -I{} zmprov ga {} zimbraIsAdminAccount | grep TRUE

Тут повезло. Переадресации не было ни на одном ящике, redirect в Sieve не нашёлся, администраторов оказалось трое — директор, бухгалтер и прежний приходящий админ, все узнаваемые. Это не доказывает, что почту не читали. Но это значит, что постоянного канала утечки атакующий не оставил, и директору я сказал именно так, а не расплывчатое «всякое могло быть».

Такая переадресация живёт дольше всех остальных следов. Убей я майнер и закрой заявку — правило «копию входящих директора на посторонний адрес» продолжало бы работать месяцами после того, как все успокоились. Ровное плато CPU ночью видит любой график. Копию письма не видит никто.

Чем закончилось: изоляция, атомарная зачистка и вывод хоста

Порядок, который сработал, был такой. Сначала firewalld: SSH убран из публичной зоны, заведён ipset из наших управляющих адресов, всем остальным 22-й порт закрыт. Почтовые порты остались открыты миру — почта должна была работать.

Потом — один атомарный проход по всем трём слоям: stop/disable/mask мастер-юнита, удаление /lib/udev/.logsvc, снятие cron-строки zimbra, убийство сторожа и майнера строго по PID из /proc/*/exe (не pkill по маске — иначе рискуешь убить собственную сессию), очистка /dev/shm. Затем 75 секунд HOLD-проверки: не вернулось. Мастер разорвал цикл — цепочка встала.

ЭтапВремяЧто дал
Диагностика и вектор~1 часПонят механизм, а не симптом
Первая (неполная) чистка~30 минВернулось за 2 мин — урок
Изоляция + атомарная зачистка~2 часаДержится, HOLD 75 c чисто
Миграция на чистый контуротдельный проектСтарый хост выведен

Но зачистка — это не финал. Хост был скомпрометирован до root, и доверия к нему больше нет. Единственный честный итог — вывести его из эксплуатации. Мы подняли чистый сервер (в этом кейсе — Carbonio Community Edition, тот же стек Postfix/OpenLDAP/Jetty), перенесли ящики через imapsync, сменили MX и все креды до единой, а старую машину погасили. Ротация паролей — не «на всякий случай», а обязательный пункт: любой пароль, который эта Zimbra когда-либо видела, считается утёкшим.

Что из этого следует вам

Если у вас Zimbra на CentOS 7 и её «боятся трогать» — вы в точности та мишень, по которой это и работает. Непропатченный сервер на EOL-системе компрометируют не адресно, а массово: сканер нашёл, эксплойт отработал, майнер приехал. Первым сигналом будет не «странный трафик», а тихо съеденный ночью процессор.

Считайте цену. В этом кейсе сервер несколько дней работал на чужой карман и параллельно ронял себе SQL — почта в автосервисе стояла урывками, приёмка не могла отправлять заказ-наряды. Разбор, изоляция и перенос — это десятки часов работы, которые можно было не тратить, если бы диагностику провели до аварии, а не после. А главное — мы так и не знаем наверняка, что атакующий успел прочитать в почте за месяцы своего присутствия.

Проверить, не в таком ли вы положении, стоит копейки. Пришлите мне вывод трёх команд из блока «проверьте у себя за 2 минуты» — top, содержимое /dev/shm и список включённых юнитов с sudo-правилами. За день отвечу, чисто у вас или нет, и если нет — что именно там сидит и в каком порядке это снимать, чтобы не получить второй заход, как получил я в первый раз.

Частые вопросы

Антивирус на сервере ничего не нашёл. Значит, всё чисто?

Нет. В этом кейсе повышение до root шло без единого файла-вредоноса: легитимный nginx по подсунутому конфигу сам записал строку в sudoers.d. Антивирусу нечего было ловить — вредоносного бинаря на этапе эскалации не существует, есть только злоупотребление штатной службой. Майнер и сторож появляются позже и часто лежат в /dev/shm, которую многие сканеры вообще не смотрят. Отсутствие детекта у антивируса не доказывает, что сервер чист, — доказывает только то, что этот антивирус не про такие атаки.

Я убил майнер и удалил его файлы, нагрузка упала. Инцидент закрыт?

Скорее всего нет, и я сам на этом обжёгся. Убийство процесса и удаление файлов снимает runtime-слой, но не трогает механизм перезапуска. В нашем случае корнем был systemd-юнит от root с невинным именем «System Logger» и бинарём в /lib/udev/ — он передропливал всё обратно каждые 10 секунд, заражение вернулось за две минуты. Пока живы мастер-юнит и сетевой доступ атакующего, чистить процессы бессмысленно: сначала изоляция и снятие ВСЕХ слоёв персистентности атомарно, потом уже процессы.

Почему нельзя просто перезагрузить сервер, чтобы всё сбросилось?

Потому что ребут — ровно то, на что рассчитан вредонос. Мастер-юнит прописан в автозапуск (Restart=always, симлинк в multi-user.target.wants), сторож поднимается из cron, ключ атакующего лежит в authorized_keys. После перезагрузки всё это стартует заново, а вы теряете живую картину: активные соединения, содержимое /dev/shm, список работающих процессов. Перезагрузка не лечит компрометацию уровня root — она только затирает улики и даёт ложное ощущение, что «стало тихо».

Насколько реально вычистить взломанный сервер и оставить его в работе?

Технически цепочку снять можно, и мы её сняли — HOLD-проверка 75 секунд показала, что не возвращается. Но оставлять такой хост в проде я не советую и клиенту не советовал. Компрометация была до root: атакующий мог оставить закладки, которые вы не нашли, мог знать все пароли, которые сервер видел. Честный путь — поднять чистую машину, перенести ящики через imapsync, сменить MX и абсолютно все креды, а скомпрометированный хост погасить. Доверие к машине, где чужой был root, восстановить нельзя.

У нас Zimbra 9 на CentOS 7. Мы в группе риска?

Да, это ровно та связка, которую бьют массово. CentOS 7 снят с поддержки 30 июня 2024 — обновлений безопасности для ОС нет. Zimbra 9 без свежих патчей ловит эксплойты, которые публикуются и начинают применяться в течение суток после огласки. Отставание на год по патч-уровню, как было здесь (P39), — это открытая дверь. Не обязательно ждать аварии: пришлите вывод команд из статьи, и я скажу, взломан сервер уже или пока только уязвим.

Нужна помощь с проектом?

Специалисты АйТи Фреш помогут с архитектурой, DevOps, безопасностью и разработкой — 15+ лет опыта

📞 Связаться с нами
#Zimbra#безопасность#инцидент#форензика#CentOS 7
Комментарии 0

Оставить комментарий

загрузка...

Подпишитесь на рассылку ITfresh

Раз в неделю — практические гайды для руководителя IT и сисадмина: безопасность, 1С, миграции, резервные копии, лайфхаки из реальных проектов.

Реквизиты оператора персональных данных

ООО «АЙТИ-ФРЕШ», ИНН 7719418495, КПП 771901001. Юридический адрес: 105523, г. Москва, Щёлковское шоссе, д. 92, корп. 7. Контакт: info@itfresh.ru, +7 903 729-62-41. Оператор обрабатывает e-mail подписчика в целях рассылки информационных и рекламных материалов до момента отзыва согласия.