Меня взломали! Утром мой сервер начал майнить Monero

в 13:01, , рубрики: docker, Monero, ruvds_перевод, umami, информационная безопасность, контейнеризвация, криптовалюта, майнинг, уязвимости
I got hacked, my server started mining Monero this morning.

Недавно моё утро началось с такого вот прекрасного e-mail от Hetzner:

Уважаемый, м-р Джей Сандерс,

У нас есть свидетельства того, что с вашего сервера была произведена атака. Пожалуйста, примите необходимые меры для решения проблемы и избежания подобного в будущем.

Также просим вас прислать краткое пояснение с описанием того, как такое могло произойти, и что вы собираетесь в этом отношении предпринять. Если последующие шаги не будут успешно выполнены, ваш сервер может быть заблокирован в любой момент после 2025-12-17 12:46:15 +0100.

К письму прилагалось подтверждение того, что с моего сервера было выполнено сканирование некоего IP-диапазона в Таиланде. Отлично. Никакого вам «Здравствуйте», а лишь заявление о злоупотреблении и угроза отключить всю инфраструктуру через 4 часа.

Немного предыстории. Я использую сервер Hetzner, которым управляю с помощью Coolify. Это мой небольшой уголок в интернете, на котором работают все мои системы:

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-запрос с вредоносом, выполняющим любой произвольный код при десериализации на сервере.

Схема всего процесса была такова:

  1. Атакующий отправляет сфабрикованный HTTP-запрос на конечную точку в коде Next.js анализатора Umami.

  2. RSC десериализует вредоносную полезную нагрузку.

  3. В результате злоумышленник получает возможность удалённого выполнения кода.

  4. Происходит скачивание и установка крипто-майнеров.

  5. Атакующий получает прибыль.

Вот так вот «Я не использую Next.js».

Паника: а не выбрался ли вирус из контейнера?

И здесь я реально запаниковал. Взгляните на этот процесс:

1001      714822  819  3.6 2464788 2423424 ?     Sl   Dec16 9385:36 /tmp/.XIN-unix/javae

Путь /tmp/.XIN-unix/javae намекает на то, что он уже в файловой системе хоста, а не в контейнере. Это означает, что хакер может получить доступ к моей базе данных, всем переменным среды, рабочим процессам. Тут Claude подсказал мне, что делать:

  1. Предположить, что взлом глобальный.

  2. Проверить систему на руткиты, бэкдоры, механизмы закрепления.

  3. Возможно, собрать систему с нуля.

  4. Потратить на это весь чёртов день.

Я проверил наличие механизмов закрепления:

$ 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/),

  • использовал неприметные имена процессов (javaerunnv),

  • пытался закрепиться в системе,

  • согласно другим отчётам, даже содержал скрипты-киллеры для уничтожения конкурирующих майнеров.

Но при этом он оказался ограничен изоляцией контейнера. Хорошие практики безопасности побеждают хитроумные вирусы.

4. Важно использовать защиту в глубину

Несмотря на то, что изоляция контейнера сработала, мне всё же следовало:

  • наладить файервол с самого начала (а не откладывать на потом),

  • использовать fail2ban, чтобы предотвратить эти попытки брут-форса SSH,

  • организовать подобающий механизм мониторинга/предупреждения, так как проблему я заметил, только благодаря письму от Hetzner,

  • обновить Umami, когда уязвимость была обнародована.

Что тут сказать. Мне повезло. Изоляция контейнера компенсировала мою собственную лень.

Что я теперь делаю иначе

  1. Больше никакого Umami. К чёрту, я передумал. Всё же это была не вина Umami, и их опенсорсное ПО реально классное. Так что я заново загрузил свежую версию.

  2. Проверка всех сторонних контейнеров. Теперь я прохожусь по всем используемым программам и проверяю:

    1. Под каким пользователем она выполняется?

    2. Какие тома к ней примонтированы?

    3. Когда она в последний раз обновлялась?

    4. Действительно ли она мне нужна?

  3. Укрепление SSH. Перешёл на аутентификацию только по ключу — теперь никаких паролей — плюс настроил fail2ban.

  4. Подобающий мониторинг. Настроил оповещения о потреблении CPU, средней нагрузке и подозрительной сетевой активности. Негоже узнавать о взломе собственной системы от провайдера. Вообще, у меня настроены Grafana и Node Exporter, но какой толк, если я сам не буду за ними следить.

  5. Регулярные обновления безопасности. Больше никаких «А, обновлюсь позже». Если обнаружена CVE, я применяю патч или удаляю сервис.

Светлая сторона

По факту этот случай стал хорошим опытом, благодаря которому я:

  • попрактиковался в реагировании на инциденты в реальных условиях (такого мне ещё не доводилось),

  • убедился, что изоляция контейнеров работает,

  • лучше разобрался в пространствах имён Docker, отображении пользователей и границах привилегий,

  • укрепил свою инфраструктуру без риска реальной утраты данных.

Причём на всё про всё я потратил всего 2 часа утром перед работой. Могло быть намного хуже.

Однако мне интересно, сколько Monero я успел намайнить для этого негодяя. Исходя из загрузки CPU и продолжительности…пожалуй, достаточно, чтобы он мог неплохо пообедать. Что ж, приятного тебе аппетита, загадочный хакер! Надеюсь, ты доволен.

Кратко

  • В инструменте аналитики Umami (написан на Next.js) была уязвимость RCE.

  • Через эту уязвимость злоумышленники установили крипто-майнеры.

  • В итоге они майнили Monero 10 дней при загрузке CPU 1000%+.

  • Меня спасла изоляция контейнера, так как он выполнялся без root-прав и не имел примонтированных каталогов.

  • Исправление: docker rm umami и включение файервола.

  • Вывод: нужно знать, из чего собраны ваши зависимости и правильно конфигурировать контейнеры.

Автор: Bright_Translate

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js