Дело о таинственном майнере: детективная история одного взлома
Реальное расследование: swap забит под завязку, процесс ест 2.3 ГБ и 162% процессора. Кто он, откуда пришёл и через какую дверь вошёл. С разбором и ссылками на теорию по каждой главе.
miki/delo-o-tainstvennom-maynere-detektivnaya-istoriya-odnogo-vzl · v4
Реальное расследование: swap забит под завязку, процесс ест 2.3 ГБ и 162% процессора. Кто он, откуда пришёл и через какую дверь вошёл. С разбором и ссылками на теорию по каждой главе.
Детективная история по мотивам реальных событий
Всё началось с записки, какие пишут в понедельник утром:
«Надо подчистить swap и как-то его увеличить, и что-то предпринять для наблюдения за ним.»
Обычная уборка. Полчаса работы. Никто ещё не знал, что это начало одного из самых захватывающих расследований в истории этого сервера.
Имена процессов не изменены — они настоящие, и знать их в лицо полезно. Адреса, имена серверов и проектов убраны.
Детектив — он же системный администратор — включил терминал и набрал free -h. То, что он увидел, заставило его нахмуриться.
«Странно, — подумал детектив. — Swap под завязку. Надо копать глубже.»
Он набрал smem -s swap -r и стал просматривать список. Первые строки были знакомы: свои сервисы, свои процессы, всё на местах. Он прокрутил ниже — и замер.
«Это что ещё за kdevtmpfsi? И kinsing? Процессы с такими именами не должны работать в /tmp. И кто этот пользователь с номером 70?»
Он проверил процессор:
162 процента. Полтора ядра. Постоянно.
«Так вот кто съедает все ресурсы. Но кто ты и откуда пришёл, загадочный незнакомец?»
На полях: почему
free -hвсегда выглядит страшноКолонка
freeпочти всегда близка к нулю, и это нормально: Linux отдаёт неиспользуемую память под кэш файловой системы и мгновенно освобождает её, когда она нужна приложениям. Смотреть надо наavailable, а не наfree. Подробный разбор — Linux ate my RAM и устройство памяти в ядре.И про имя.
kdevtmpfsi— это не случайный набор букв. В ядре Linux есть настоящий потокkdevtmpfs, он занимается файловой системой устройств. Одна лишняя буква на конце — и взгляд администратора скользит мимо. Расчёт именно на это.
Детектив начал собирать улики. Команда за командой он выстраивал картину.
Улика первая: где они прячутся.
«Они прячутся внутри контейнера! Какая хитрость.»
Улика вторая: что это за файлы.
«Пять с половиной мегабайт и два. Плюс файл блокировки.» Детектив задумался. Файл блокировки означал одно: злоумышленник позаботился, чтобы две копии майнера не мешали друг другу. Это была не случайная поделка.
Улика третья: самая неприятная.
Детектив перечитал дату дважды.
«Восьмое октября. Это было пять дней назад. Сколько же времени эта тварь здесь работала?»
Логи показывали одно и то же снова и снова: система задыхалась от нехватки памяти, ядро убивало прожорливый процесс — а он возрождался, как феникс из пепла.
«Интересно... кто же тебя воскрешает?»
На полях: три вещи из этой главы
Что такое OOM killer. Когда память кончается, ядро выбирает процесс с худшим счётом и убивает его, чтобы спасти систему. Это не сбой, а последняя линия обороны. Запись в логе — надёжный признак, что кто-то ест больше положенного.
Почему путь такой длинный.
/var/lib/docker/overlay2/...— это слои файловой системы контейнера. Всё, что процесс внутри записал поверх образа, оказывается здесь, на диске хоста. Как это устроено — драйвер overlay2.Кто такой kinsing. Это не самоделка, а известное семейство, охотящееся именно на плохо закрытые контейнерные окружения. Технический разбор — Threat Alert: Kinsing от исследователей Aqua Security. Приём в целом каталогизирован как Resource Hijacking, T1496.
Самый важный вопрос был ещё не задан: как они сюда попали?
Детектив исследовал слои Docker и по идентификатору вышел на конкретный контейнер.
«PostgreSQL?! Как база данных может запускать вирусы?»
Но детектив был опытным следователем. Прежде чем строить теории, он проверил очевидное — порты.
Он смотрел на эти четыре нуля довольно долго.
«Эврика. Вот она, дыра.»
На полях: четыре нуля, которые всё решают
0.0.0.0означает «слушать на всех сетевых интерфейсах». На домашнем ноутбуке за роутером это практически то же самое, что localhost, — снаружи всё равно не достучаться. На сервере с публичным адресом среди этих интерфейсов есть тот, который смотрит в интернет.Одна и та же строка, два противоположных смысла. Именно поэтому конфигурация, отлично работавшая на машине разработчика, оказалась дверью нараспашку.
Запись в compose Кто может подключиться "5432:5432"весь интернет "127.0.0.1:5432:5432"только сам сервер раздела portsнеттолько контейнеры в той же сети Документация: раздел ports.
И отдельно — про файрвол. Мысль «у меня же стоит ufw» здесь не работает. Из документации Docker: «Docker routes container traffic in the
nattable, which means that packets are diverted before it reaches theINPUTandOUTPUTchains that ufw uses.» Пакеты уходят по назначению раньше, чем доберутся до правил ufw. Проверять надо снаружи:nc -zv адрес 5432.
Детектив залез в логи самого PostgreSQL и обнаружил золото:
«Base64. Посмотрим, что там спрятано.»
Он расшифровал строку — и увидел чужой почерк:
Первые три строки заставили детектива усмехнуться. Скрипт начинал работу с того, что убивал чужих майнеров. На взломанных серверах, оказывается, бывает тесно.
А дальше он нашёл саму команду входа — и это было самое интересное во всём деле:
Обычный SQL. Никакого взлома, никакой хитрой уязвимости. Атакующий просто вежливо попросил базу данных выполнить команду — и база данных выполнила.
Детектив откинулся на спинку кресла и восстановил картину:
Первая атака: 8 октября. Длительность: пять дней. Ущерб: 2.3 ГБ памяти, 162% процессора и неизвестное количество украденного электричества.
На полях: почему это не считается уязвимостью
COPY ... TO PROGRAMработает ровно так, как задумано. Это штатная возможность PostgreSQL: выгрузить результат запроса не в файл, а на вход внешней программе — удобно для конвейеров обработки данных. Описание: SQL COPY.Доступ к ней имеет суперпользователь и обладатели роли
pg_execute_server_program. А пользовательpostgresиз стандартного образа — как раз суперпользователь.Отсюда вывод, который стоит запомнить накрепко: доступ к порту базы данных равен возможности выполнять команды на сервере. Не «прочитать таблицы» — а выполнять команды. По последствиям это то же самое, что оставить открытым SSH без пароля.
Теперь, когда детектив знал всю правду, пришло время действовать.
Шаг первый: ликвидация.
Он проверил память:
Шаг второй: закрыть дверь. И вот здесь детектив сделал паузу, потому что это был единственный шаг, без которого всё остальное не имело смысла.
Шаг третий: поставить наблюдение. Сторож за памятью. Поиск подозрительных процессов по расписанию. fail2ban на SSH. Оповещение, если ядро снова начнёт кого-то убивать за прожорливость.
Шаг четвёртый, которого в первой версии этой истории не было, а он обязателен: сменить все секреты. Если кто-то пять дней выполнял команды от имени базы, он мог прочитать переменные окружения, файлы конфигурации, ключи и содержимое таблиц. Сервер вычищен — а пароли по-прежнему те же.
Детектив запустил memstat и посмотрел на экран.
«Дело закрыто, — улыбнулся он. — Но я буду наблюдать.»
На полях: две ловушки этой главы
Порядок важнее скорости. Убить процесс — самое заметное действие и самое бесполезное, если дверь осталась открытой. Пока порт доступен, тот же бот вернётся через час, и появится ощущение, что заражение неизлечимо. Сначала дверь, потом уборка.
restartне меняет порты. Проброс задаётся в момент создания контейнера. Люди правят compose, делаютdocker compose restart, видят порт на месте и решают, что правка не сработала. Нужноdocker compose up -d --force-recreate.Что стоит поставить один раз и забыть: fail2ban от перебора паролей, а из общих рекомендаций по контейнерам — OWASP Docker Security Cheat Sheet.
Когда буря улеглась, детектив задался последним вопросом: «А кто же впустил злодеев в дом?»
Он открыл древний фолиант под названием Git History и начал листать страницы прошлого.
«Grafted? Это же обрезанная история!» — воскликнул детектив. — «Улик просто нет на диске. Надо достать остальные.»
И тут открылась правда. Полная история файла:
Руки дрожали от волнения, когда он набирал:
И вот она — улика века:
Самый первый коммит проекта. Тот самый, который никто никогда не читает внимательно, потому что там «просто начальная настройка».
Оставалась последняя зацепка. Самая важная.
«А кто же автор этого коммита?»
Детектив прочитал имя. Потом перечитал его ещё раз.
Имя было его собственное.
В комнате стало очень тихо. Пять дней расследования, три отчёта, анализ вредоносного кода, раскодировка base64, раскопки в слоях Docker — и всё это затем, чтобы выйти на самого себя.
Лучший сыщик города долго смотрел в экран.
— Ну здравствуй, — сказал он наконец.
На полях: инструменты этой главы
graftedв выводеgit logозначает, что репозиторий склонировали с--depth 1— распространённая настройка в системах развёртывания ради скорости. История не удалена, а просто не скачана. Проверить:git rev-parse --is-shallow-repository, вернуть:git fetch --unshallow.Как искать строку по всей истории. Детектив листал вручную, но есть способ быстрее:
bash1git log -S'5432:5432' --format='%h %ad %an %s' --date=shortФлаг
-S(в документации его называют pickaxe, «кирка») находит коммиты, где число вхождений строки изменилось — то есть где она появилась или исчезла. Есть ещё-G: он ищет по регулярному выражению во всём тексте изменений, включая простые перемещения строк. Для вопроса «когда это появилось» нужен именно-S. Подробности — git log.
Составлять психологический портрет преступника оказалось неловко: детектив слишком хорошо знал подозреваемого.
Мотива не было. Злого умысла не было. Была конфигурация из раздела «быстрый старт» — той самой, что пишут под «запусти у себя и посмотри, как работает». Порты наружу, чтобы можно было подключиться клиентом. Пароль попроще. Никаких ограничений. Для своей задачи такой пример абсолютно правилен.
А потом его развернули на публичном сервере. Без единого изменения.
«Классическое „у меня работает“», — записал детектив и ненадолго задумался о том, как объективно расследовать дело, где ты сам и следователь, и подозреваемый, и потерпевший.
Соблазн был велик. В первом варианте отчёта появилась даже разбивка ответственности в процентах — семьдесят на тридцать, в собственную пользу. Перечитав этот абзац на свежую голову, детектив его вычеркнул.
Потому что вопрос «кто это написал» не меняет ровным счётом ничего — особенно когда ответ известен заранее. А вот другой вопрос меняет:
Почему эта строка доехала до боевого сервера, ни во что не упершись?
Ни ревью. Ни проверка при сборке. Ни чеклист перед выкаткой. Значит, не было ни одного из трёх.
И ответ на этот вопрос помещается в одну строку:
Она проверяет каждую выкатку, работает по выходным и никогда не устаёт. В отличие от внимания.
На полях: почему разбор без виноватых работает лучше
Идея не в мягкости, а в статистике: наказанный человек больше не повторит эту ошибку, но её повторит кто-то другой, кто о разборе не слышал. Зато у названного виноватым появляется стимул в следующий раз промолчать о странности — и следующий инцидент найдут позже.
Классический текст на эту тему — глава Postmortem Culture из книги Google о надёжности систем.
Что можно поставить в конвейер за один вечер:
- Trivy — ищет известные уязвимости в образах;
- Hadolint — линтер для Dockerfile;
- Docker Bench Security — проверка настроек хоста по общепринятым требованиям.
Все трое в первый запуск дадут много шума. Это нормально: разбирается критичное, остальное уходит в задачи.
Детектив закрыл ноутбук.
Виновник найден — и находился всё это время в комнате. Мотив установлен: пример из документации, принятый за готовое решение. Улики сохранены: в истории git навсегда.
Система защищена: порт закрыт, секреты сменены, сторож дежурит, оповещения настроены.
«Самое неприятное в этой работе, — подумал он, глядя в окно, — что преступник почти всегда оказывается тобой месяц назад. Зато лучшая защита — это знания. И теперь они есть.»
- Порты баз данных не публикуются наружу. Ни 5432, ни 6379, ни 3306, ни 27017.
- Пример из документации — не конфигурация для боя. Между ними всегда есть работа.
- Файрвол не закроет то, что опубликовал Docker. Проверяй снаружи.
- Сигнал обычно уже есть. Пять дней потеряли не потому, что система молчала, а потому, что её никто не слушал.
- После взлома меняются все секреты. Иначе второй визит будет тише первого.
- Вывод из инцидента — это строка кода, а не фамилия. Даже если фамилия твоя.
«В мире Docker нет мелочей. Каждый проброшенный порт — это дверь. И каждая дверь должна быть закрыта на замок.»
Дело закрыто.
