
Недавно моё утро началось с такого вот прекрасного e-mail от Hetzner:
Уважаемый, м-р Джей Сандерс,
У нас есть свидетельства того, что с вашего сервера была произведена атака. Пожалуйста, примите необходимые меры для решения проблемы и избежания подобного в будущем.
Также просим вас прислать краткое пояснение с описанием того, как такое могло произойти, и что вы собираетесь в этом отношении предпринять. Если последующие шаги не будут успешно выполнены, ваш сервер может быть заблокирован в любой момент после 2025-12-17 12:46:15 +0100.
К письму прилагалось подтверждение того, что с моего сервера было выполнено сканирование некоего IP-диапазона в Таиланде. Отлично. Никакого вам «Здравствуйте», а лишь заявление о злоупотреблении и угроза отключить всю инфраструктуру через 4 часа.
Немного предыстории. Я использую сервер Hetzner, которым управляю с помощью Coolify. Это мой небольшой уголок в интернете, на котором работают все мои системы:
-
этот блог (имеется в виду блог Unfinished Side Projects, — прим. пер.),
-
аналитика,
-
сайт моего отца (он электрик).
8:30 AM: ох, чёрт…
Первым делом я с помощью SSH проверил среднюю нагрузку.
$ w
08:25:17 up 55 days, 17:23, 5 users, load average: 15.35, 15.44, 15.60
У меня запущено несколько бэкенд-сервисов на Go и кое-что из фронтенда на SvelteKit. В пике ежедневное число пользователей достигает 20, то есть здесь явно что-то было не так.
Тогда я выполнил ps aux, чтобы понять, кто занимает CPU:
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
1001 714822 819 3.6 2464788 2423424 ? Sl Dec16 9385:36 /tmp/.XIN-unix/javae
1001 35035 760 0.0 0 0 ? Z Dec14 31638:25 [javae] <defunct>
1001 3687838 586 0.0 0 0 ? Z Dec07 82103:58 [runnv] <defunct>
1001 4011270 125 0.0 0 0 ? Z Dec11 10151:54 [xmrig] <defunct>
1001 35652 62.3 0.0 0 0 ? Z Dec12 4405:17 [xmrig] <defunct>
Потребление CPU 819% процессом javae, запущенным по адресу /tmp/.XIN-unix/. И ещё несколько загруженных процессов xmrig, представляющих буквально ПО для майнинга крипты — в частности, Monero.
Похоже, я уже с 7 декабря майню для кого-то криптовалюту, то есть целых десять дней. Невероятно.
Расследование
Моей первой мыслью было «я в полной жопе». На моём сервере уже неделю работает крипто-майнер, а значит, он основательно заражён. Придётся всё сносить и перенастраивать.
К счастью, я додумался сперва провести некоторые детективные изыскания. Мне хотелось хотя бы понять, как меня вообще взломали, чтобы на будущее иметь в виду. К этому делу я в качестве помощника привлёк Claude (нет, это не моя типичная тактика).
С самого начала я заметил кое-что интересное. Все эти процессы работали под пользователем 1001. Не root, не системный пользователь, а UID 1001.
Посмотрим-ка, что фактически выполняется:
$ docker ps
CONTAINER ID IMAGE CREATED STATUS PORTS NAMES
c604f579efd5 dsw80g4w8g0kgog8oskc0sks:63e3be6167b43de47663445dd72f92f97887b843 2 days ago Up 2 days (healthy) [DELETED] dsw80g4w8g0kgog8oskc0sks-075301203997
00aec82c2650 o4wk8gsckwgkcgcgkcw8gcsc:40497e7208602d31d7b5e58af4f2e86611b9850c 2 days ago Up 2 days [DELETED] o4wk8gsckwgkcgcgkcw8gcsc-072326337252
a42f72cb1bc5 ghcr.io/umami-software/umami:postgresql-latest 9 days ago Up 9 days (healthy) [DELETED] umami-bkc4kkss848cc4kw4gkw8s44
7c365a792902 postgres:16-alpine 9 days ago Up 9 days (healthy) [DELETED] postgresql-bkc4kkss848cc4kw4gkw8s44
af077d142471 ghcr.io/coollabsio/coolify:4.0.0-beta.452 10 days ago Up 10 days (healthy) [DELETED] coolify
fdc3cc9b926b ghcr.io/coollabsio/coolify-realtime:1.0.10 10 days ago Up 10 days (healthy) [DELETED] coolify-realtime
d3dc2af3ff4d postgres:15-alpine 10 days ago Up 10 days (healthy) [DELETED] coolify-db
dc77adba40bb redis:7-alpine 10 days ago Up 10 days (healthy) [DELETED] coolify-redis
4962dd18bed7 ghcr.io/coollabsio/sentinel:0.0.18 3 weeks ago Up 7 hours (healthy) [DELETED] coolify-sentinel
5ec997e35140 nginx:stable-alpine 6 weeks ago Up 6 weeks (healthy) [DELETED] kcwsosksw084swoog04g0w0k-proxy
5da5e2f2052b prom/prometheus:latest 6 weeks ago Up 6 weeks [DELETED] yg400wo4wok8k0cgo8844gcg-155648790718
32815a5e2e52 twakedrive/tdrive-frontend 7 weeks ago Up 7 weeks [DELETED] frontend-ssowscwgccgk8k0k8oos8w40-120609116307
5d6bc828fe7f twakedrive/tdrive-node 7 weeks ago Up 7 weeks [DELETED] tdrive_node-ssowscwgccgk8k0k8oos8w40-120609108796
3e727b84415d mongo 7 weeks ago Up 7 weeks [DELETED] mongo-ssowscwgccgk8k0k8oos8w40-120609102533
3506728b808b a4c00g0ggkk4cww4scsw8scw:682dfd679845535f873d3c5b4599295f4d855ba5 7 weeks ago Up 7 weeks [DELETED] a4c00g0ggkk4cww4scsw8scw-113711308615
736d9f03d152 rccwscgosk48gs0844sogsgw:51d68c7e7665371569aacc5f044c82ec1f06fa4c 7 weeks ago Up 7 weeks [DELETED] rccwscgosk48gs0844sogsgw-111702410410
8f79e6f4c981 grafana/grafana-oss 7 weeks ago Up 7 weeks (healthy) [DELETED] grafana-ik8wokwgowow8gksok8k40sc
09d013497f9f 24a90047f2d2 7 weeks ago Up 7 weeks (healthy) [DELETED] postgresql-ik8wokwgowow8gksok8k40sc
bf8b6a969b19 gcr.io/cadvisor/cadvisor:latest 7 weeks ago Up 7 weeks (healthy) [DELETED] k0gkw4koc8swo4wkg44w408g-211926055160
30e4d6edf675 prom/node-exporter:latest 7 weeks ago Up 7 weeks [DELETED] yc4c4ckg80ogggc4ck8gwgww-211604215046
b227504e8787 rabbitmq:3-management 7 weeks ago Up 7 weeks (healthy) [DELETED] rabbitmq-xscowck8kgc0wssokoggcskc
b260ad24c434 d741b3768746 7 weeks ago Up 7 weeks (healthy) [DELETED] kcwsosksw084swoog04g0w0k
6d038254e9ef grafana/loki:latest 7 weeks ago Up 7 weeks [DELETED] b88cwo8ckwo0gw840oo444kk-193205274080
fe2aad5d9704 traefik:v3.1 7 weeks ago Up 7 weeks (healthy) [DELETED] coolify-proxy
Примечание: я удалил порты из списка, так как они могут раскрывать внутренние детали.
Существенный момент в том, что я использовал Umami — инструмент аналитики с акцентом на конфиденциальность, который я заново развернул девять дней назад для отслеживания трафика в своём блоге. Перезапустить я его решил, потому что он начал барахлить, и я не мог понять причину. И здесь мне показалось подозрительным это совпадение событий по времени.
Тогда я проверил, какой контейнер содержит пользователя 1001:
$ docker ps -q | while read container; do
echo "=== $container ==="
docker exec $container ls -la /app/node_modules/next/dist/server/lib/ 2>/dev/null | grep xmrig
done
Вывод:
=== a42f72cb1bc5 ===
drwxr-xr-x 2 nextjs nogroup 4096 Dec 17 05:11 xmrig-6.24.0
Вот оно. Контейнер a42f72cb1bc5 — это и есть мой контейнер Umami. И в нём нашёлся целый каталог xmrig-6.24.0, который лежит там, где должны находиться компоненты сервера на Next.js.
И присутствие команды запуска майнера в списке процессов это подтвердило.
/app/node_modules/next/dist/server/lib/xmrig-6.24.0/xmrig
--url auto.c3pool.org:443
--user 8Bt9BEG98SbBPNTp1svQtDQs7PMztqzGoNQHo58eaUYdf8apDkbzp8HbLJH89fMzzciFQ7fb4ZiqUbymDZR6S9asKHZR6wn
--pass WUZHRkYOHh1GW1RZWBxaWENRX0ZBWVtdSRxQWkBWHg==
--donate-level 0
Кто-то взломал мой контейнер для аналитики и майнил Monero с помощью моего проца. Круто.
Стоп, но я не использую Next.js
Недавно мне на HN попалась статья, где автор приводил ссылку на этот пост с Reddit, в котором описывалась критическая уязвимость Next.js (CVE-2025-66478). Тогда я лишь подумал «да и пофиг, Next.js я всё равно не использую».
Какая детская наивность.
Разве что… Umami написан на Next.js. Я этого не знал и разузнать не удосужился. Упс.
Уязвимость CVE-2025-66478 находилась в механизме десериализации React Server Components. В протоколе «Flight», который RSC использует для сериализации/десериализации данных между клиентом и сервером, была дыра. Атакующий мог отправить на любую конечную точку App Router специально написанный HTTP-запрос с вредоносом, выполняющим любой произвольный код при десериализации на сервере.
Схема всего процесса была такова:
-
Атакующий отправляет сфабрикованный HTTP-запрос на конечную точку в коде Next.js анализатора Umami.
-
RSC десериализует вредоносную полезную нагрузку.
-
В результате злоумышленник получает возможность удалённого выполнения кода.
-
Происходит скачивание и установка крипто-майнеров.
-
Атакующий получает прибыль.
Вот так вот «Я не использую Next.js».
Паника: а не выбрался ли вирус из контейнера?
И здесь я реально запаниковал. Взгляните на этот процесс:
1001 714822 819 3.6 2464788 2423424 ? Sl Dec16 9385:36 /tmp/.XIN-unix/javae
Путь /tmp/.XIN-unix/javae намекает на то, что он уже в файловой системе хоста, а не в контейнере. Это означает, что хакер может получить доступ к моей базе данных, всем переменным среды, рабочим процессам. Тут Claude подсказал мне, что делать:
-
Предположить, что взлом глобальный.
-
Проверить систему на руткиты, бэкдоры, механизмы закрепления.
-
Возможно, собрать систему с нуля.
-
Потратить на это весь чёртов день.
Я проверил наличие механизмов закрепления:
$ crontab -l
no crontab for root
$ systemctl list-unit-files | grep enabled
# ... только легитимные системные службы, ничего подозрительного.
Никаких вредоносных задач cron, никаких фейковых служб systemd под видом nginx или apache. Это же… хорошо?
Но мне всё ещё было неясно: «Вырвался реально вредонос из контейнера или же нет?»
Момент истины
В качестве проверки я решил проверить, присутствует ли на хосте /tmp/.XIN-unix/javae. Если да, то я конкретно попал. Если же его нет, значит, я просто наблюдаю обычное поведение Docker, когда при выполнении ps на хосте он также показывает процессы контейнера, хотя по факту они изолированы.
$ ls -la /tmp/.XIN-unix/javae
ls: cannot access '/tmp/.XIN-unix/javae': No such file or directory
Не вырвался
Или просто всё так выглядит. Можно понизить уровень тревоги с DEFCON1 до «наведения орудий и проведения дополнительных проверок». Гильотину пока готовить рано.
Вредонос целиком находился в контейнере Umami. Очевидно, что при выполнении ps aux на хосте Docker отображаются процессы всех контейнеров, так как они используют одно ядро. Но сами эти процессы замонтированы в собственных пространствах и не могут видеть или трогать систему хоста.
Я проверил, под каким конкретно пользователем работал этот контейнер:
$ docker inspect umami-bkc4kkss848cc4kw4gkw8s44 | grep '"User"'
"User": "nextjs",
$ docker inspect umami-bkc4kkss848cc4kw4gkw8s44 | grep '"Privileged"'
"Privileged": false,
$ docker inspect umami-bkc4kkss848cc4kw4gkw8s44 | grep -A 30 "Mounts"
"Mounts": [],
Что в итоге я узнал, и почему я не в полной жопе:
-
контейнер выполнялся под пользователем
nextjs(UID 1001), а не root, -
контейнер не имел привилегий,
-
к контейнеру не было примонтировано томов.
То есть вирус мог:
-
выполнять процессы внутри контейнера,
-
майнить крипту,
-
сканировать сети (отсюда и жалоба Hetzner),
-
потреблять 100% CPU.
При этом он НЕ мог:
-
получить доступ к файловой системе хоста,
-
устанавливать задачи cron,
-
создавать службы
systemd, -
сохранять присутствие после перезапуска контейнера,
-
проникать в другие контейнеры,
-
устанавливать руткиты.
Изоляция контейнера реально сработала. Класс.
Dockerfiles против автоматически сгенерированных образов
ИМХО, от серьёзных проблем меня уберегли пара моментов, которые отличали мой случай от описанного в указанном посте с Reddit:
-
Для своих приложений я пишу собственные Dockerfiles. Панацеей это, конечно, не является, но если сравнивать с автоматической генерацией, то так я хотя бы лучше понимаю, что внутри.
-
Coolify и Docker, как правило, прибегают к контейнеризации. Я уже понял, что рассчитывать на разделение контейнеров как на средство обеспечения безопасности, не стоит. Но это всё же лучше, чем выполнять всё на хосте.
А что насчёт того поста с Reddit? Тот парень встрял по полной, так как его контейнер выполнялся с правами root, что позволило вредоносу:
-
устанавливать задачи cron для закрепления,
-
создавать службы
systemd, -
выполнять запись в любые части файловой системы,
-
сохраняться после перезагрузки.
Так что в моём случае изоляция в контейнере сработала!
Чего я не сделал, так это не проследил, какие инструменты использовал сам, а какие использовались уже через них в виде зависимостей. Собственно, я установил Umami из раздела сервисов Coolify и даже не настраивал.
Кстати, вины Umami здесь точно нет. Они выпустили исправление для своего свободного ПО ещё неделю назад. Просто у меня не дошли руки его применить.
Исправление
# Остановить и удалить заражённый контейнер.
$ docker stop umami-bkc4kkss848cc4kw4gkw8s44
$ docker rm umami-bkc4kkss848cc4kw4gkw8s44
# Проверить загрузку CPU.
$ uptime
08:45:17 up 55 days, 17:43, 1 user, load average: 0.52, 1.24, 4.83
Потребление CPU вернулось к норме. С момента инцидента прошло два дня, и мой проц теперь чилит при нагрузке около 5%.
Я также включил Uncomplicated Firewall (что следовало сделать давным-давно):
$ sudo ufw default deny incoming
$ sudo ufw default allow outgoing
$ sudo ufw allow ssh
$ sudo ufw allow 80/tcp
$ sudo ufw allow 443/tcp
$ sudo ufw enable
Он блокирует все входящие подключения кроме SSH, HTTP и HTTPS. Больше никаких раскрытых портов PostgreSQL и открытых для интернета портов RabbitMQ. Думаю, это не должно сыграть большую роль, так как 5432-й порт в контейнере Docker не был открыт для хоста. Но всё же проделать это стоило.
Что касается Hetzner, то я отправил им краткое пояснение:
Расследование завершено. Сканирование выполнял заражённый контейнер с инструментом аналитики Umami (CVE-2025-66478).
Этот контейнер выполнялся без прав root, без привилегий доступа и монтирования к хосту. Так что заражение было полностью изолировано. Контейнер я удалил, а заодно укрепил файервол.
В течение часа тикет был закрыт.
Усвоенные уроки
1. Если ты не используешь X, это ещё не значит, что его не используют твои зависимости
Я не пишу приложения на Next.js, но использую сторонние инструменты, которые созданы на этом фреймворке. Когда CVE-2025-66478 обнародовали, я отмахнулся «А, не моя проблема». Зря.
Поэтому следует знать, на чём написаны ваши зависимости. Конкретно этот «простой инструмент аналитики» является полноценным веб-приложением со сложным стеком.
2. Изоляция контейнеров работает (при правильной настройке)
Всё могло оказаться намного хуже. Если бы этот контейнер выполнялся под root, имел доступ к сокету Docker или примонтированные тома с чувствительными данными, то я бы писал уже совсем другую статью о воссоздании всей своей инфраструктуры.
Но мне оказалось достаточно просто удалить один контейнер и вернуться к своим делам.
Пишите собственные Dockerfiles. Вам нужно понимать, от какого пользователя у вас выполняются процессы. Избегайте запускать их под USER root, если только у вас нет на то веских причин. Не монтируйте тома без необходимости. Не давайте контейнерам --privileged доступ.
3. Продуманность
Этот вредонос непохож на те, которые просто автоматически опрашивают /wpadmin при каждом изменении DNS. Он более продуманный:
-
замаскировался по легитимному пути (
/app/node_modules/next/dist/server/lib/), -
использовал неприметные имена процессов (
javae,runnv), -
пытался закрепиться в системе,
-
согласно другим отчётам, даже содержал скрипты-киллеры для уничтожения конкурирующих майнеров.
Но при этом он оказался ограничен изоляцией контейнера. Хорошие практики безопасности побеждают хитроумные вирусы.
4. Важно использовать защиту в глубину
Несмотря на то, что изоляция контейнера сработала, мне всё же следовало:
-
наладить файервол с самого начала (а не откладывать на потом),
-
использовать fail2ban, чтобы предотвратить эти попытки брут-форса SSH,
-
организовать подобающий механизм мониторинга/предупреждения, так как проблему я заметил, только благодаря письму от Hetzner,
-
обновить Umami, когда уязвимость была обнародована.
Что тут сказать. Мне повезло. Изоляция контейнера компенсировала мою собственную лень.
Что я теперь делаю иначе
-
Больше никакого Umami. К чёрту, я передумал. Всё же это была не вина Umami, и их опенсорсное ПО реально классное. Так что я заново загрузил свежую версию.
-
Проверка всех сторонних контейнеров. Теперь я прохожусь по всем используемым программам и проверяю:
-
Под каким пользователем она выполняется?
-
Какие тома к ней примонтированы?
-
Когда она в последний раз обновлялась?
-
Действительно ли она мне нужна?
-
-
Укрепление SSH. Перешёл на аутентификацию только по ключу — теперь никаких паролей — плюс настроил fail2ban.
-
Подобающий мониторинг. Настроил оповещения о потреблении CPU, средней нагрузке и подозрительной сетевой активности. Негоже узнавать о взломе собственной системы от провайдера. Вообще, у меня настроены Grafana и Node Exporter, но какой толк, если я сам не буду за ними следить.
-
Регулярные обновления безопасности. Больше никаких «А, обновлюсь позже». Если обнаружена CVE, я применяю патч или удаляю сервис.
Светлая сторона
По факту этот случай стал хорошим опытом, благодаря которому я:
-
попрактиковался в реагировании на инциденты в реальных условиях (такого мне ещё не доводилось),
-
убедился, что изоляция контейнеров работает,
-
лучше разобрался в пространствах имён Docker, отображении пользователей и границах привилегий,
-
укрепил свою инфраструктуру без риска реальной утраты данных.
Причём на всё про всё я потратил всего 2 часа утром перед работой. Могло быть намного хуже.
Однако мне интересно, сколько Monero я успел намайнить для этого негодяя. Исходя из загрузки CPU и продолжительности…пожалуй, достаточно, чтобы он мог неплохо пообедать. Что ж, приятного тебе аппетита, загадочный хакер! Надеюсь, ты доволен.
Кратко
-
В инструменте аналитики Umami (написан на Next.js) была уязвимость RCE.
-
Через эту уязвимость злоумышленники установили крипто-майнеры.
-
В итоге они майнили Monero 10 дней при загрузке CPU 1000%+.
-
Меня спасла изоляция контейнера, так как он выполнялся без root-прав и не имел примонтированных каталогов.
-
Исправление:
docker rm umamiи включение файервола. -
Вывод: нужно знать, из чего собраны ваши зависимости и правильно конфигурировать контейнеры.
Автор: Bright_Translate
