
Всем привет от команды DFIR JetCSIRT! Хотим поделиться с вами одним интересным кейсом, эмоции от которого прекрасно описывает эта картинка:
Но обо всем по порядку…
Итак. Заказчик заводит запрос на расследование, в котором говорит, что учетная запись разработчика пушит непонятные коммиты в GitLab. По почте они установили, что пользователь выпустил себе несколько access-токенов, при этом новых входов в веб GitLab в этот период зафиксировано не было. Подозревают, что скомпрометирован личный комп пользователя, с которого он работает с GitLab. Важное дополнение: в коммитах они видят заголовок X-BugBounty , но, со слов Заказчика, они не участвуют в программе багбаунти, поэтому уверены, что так маскируется злоумышленник. Заблокировали учетку разработчика и начали собирать триаж с его АРМ на анализ.
Пока собирались данные, мы успели посмотреть, что зафиксировал наш мониторинг:
скан с узла runner;
скан с узла runner;
ssh-брут с узла runner;
ssh-брут с узла runner;
эксплуатации уязвимостей с узла runner;
эксплуатации уязвимостей с узла runner;
impacket с узла runner…
impacket с узла runner…
Стоит отметить, что GitLab находится вне инфраструктуры Заказчика, поэтому, похоже, злоумышленник запускает вредоносные пейлоады через runner, который расположен уже внутри, тем самым пытается распространиться и повыситься. Мы бьем тревогу, изолируем узел runner и запрашиваем на анализ все необходимые данные с GitLab и затронутых систем.
Что же делает злоумышленник?
В день инцидента в 00:11 он перебирает проекты через gitlab-shell, авторизовавшись с ssh-ключом под УЗ того самого разработчика. Откуда взялся ssh-ключ, спросите вы? Предлагаем пока отложить этот вопрос и вернуться к нему чуть позже.
В 00:24 он уже выпускает себе токен, и начинается самое интересное. В логах jobs видим примерно следующий стиль:
Здесь и далее мы скрываем любые данные, которые атрибутируют злоумышленника или Заказчика:
IP-адрес злоумышленника – 100.100.100.100
Домен злоумышленника – domain.su
Никнейм злоумышленника – Bughunter
УЗ разработчика Заказчика – Вася Пупкин (pupkin)
То есть злоумышленник вносит коммиты в .gitlab-ci.yml, запускает пайплайн, в рамках которого создается job security-test, runner запускает контейнер и в нем выполняет команды из script.
Злоумышленник по базе проводит разведку окружения. Что самое интересное, перед каждой командой он выводит пояснение с помощью echo.
Вам ничего не напоминает «===»? Да-да, так обычно оставляют комменты ChatGPT и аналогичные ИИ-сервисы.
Здесь он закрепляется на узле runner со своим ssh-ключом:
После разведки окружения он сканит подсеть с помощью nmap. Потом по найденным открытым портам начинает обращаться к различным сервисам. Например, подключается к Redis-серверу без аутентификации, выполняет команды разведки, получает список ключей и содержимое записей:
Демонстрирует, что можно изменить конфиг:
Заметили пункт «Restore original»? Думаю, что с этого момента мы все-таки перестанем называть его злоумышленником. Повсюду расставленные теги X-BugBounty, коммиты от ChatGPT, паттерны «нелегитимной» активности по канонам багбаунти, откат к первоначальному состоянию… Поэтому дальше будем называть его багхантером-нелегалом.
Тут он показывает реализацию RCE на все том же Redis:
Пытается сбрутить учетки PostgreSQL стандартными паролями:
Также багхантер подключается к Elasticsearch без аутентификации:
Создает индекс Elasticsearch и записывает туда данные для теста. Все эти действия он выполняет через python-скрипты, завернутые в Base64:
Еще он пробует вытащить персональные данные (email, телефоны, паспорта, адреса, зарплаты и прочее) из индексов Elasticsearch:
Багхантер попадает на хост из контейнера на одном из продуктовых серверов. Подключается к Docker API без аутентификации и создает новый контейнер с host mount, а также альтернативно повышается до уровня хоста с помощью chroot:
А потом он получает доступ к PostgreSQL (без аутентификации) на скомпрометированном продуктовом сервере:
Еще в истории .psql_history находит пароль в открытом виде:
Также багхантер немного анализирует AD через impacket, ищет контроллеры домена, центры сертификации, возможность атак через SMB:
В целом там есть еще несколько интересных атак, демонстрирующих дыры безопасности (сколько раз вы встретили в тексте «без аутентификации»?). Но предлагаем остановиться на этом и перейти к волнующему всех вопросу.
Так откуда взялся ssh-ключ?
Как мы помним, багхантер начал свою активность с ssh-ключом Васи Пупкина (pupkin). Из интересного — он пытался распространиться на другие узлы с украденным ssh-ключом:
Так как закрытый ключ хранится на стороне клиента и не передается по сети (в случае здравомыслия пользователя), то наша рабочая гипотеза — утечка в результате работы стилера на АРМ Васи.
Мы проанализировали артефакты на его узле и не обнаружили следов работы стилера, как и других следов компрометации. Было много ИИ-агентов, которые вызывали подозрения, но ничего конкретного. Также мы подключили наш сервис киберразведки для поиска утекшего ssh-ключа. Ключ не обнаружили.
Начали закрадываться сомнения, что это стилер. Сомнения укрепились после того, как проанализировали логи с сервера gitlab. За неделю до инцидента багхантер осуществлял скан веба gitlab и ssh-брутфорс нескольких учеток на хосте (и pupkin не входил в их число). Как говорится: можно, а зачем? Если есть ключ, бери да заходи с ним. Следовательно, на тот момент ключа еще не было. И он смог найти его за неделю, а наша киберразведка — нет?
Так как найти утечку не удавалось, мы решили подойти чуть-чуть с другой стороны и найти багхантера…
Кто ты, воин?
При анализе коммитов мы видели, что багхантер делает отстук на свой С2-сервер:
В целом с этого же адреса осуществлялась вся его активность, в том числе ssh-подключения.
На этом адресе резолвится домен [masked].domain.su. При переходе на http://100.100.100.100:1337/ci-callback или http:// [masked].domain.su:1337/ci-callback возвращалась строка:
А при классическом обращении на веб открывалась форма входа:
Почему важно, что домен однозначно идентифицируется с багхантером-нелегалом? Потому что адрес 100.100.100.100 принадлежит популярному веб-хостингу и на нем резолвится еще один домен VPN-сервиса. Нам надо было знать наверняка.
Теперь изучаем информацию о домене и находим почтовый адрес того, на кого он зарегистрирован:
Чудеса, мы получили личную почту регистранта.
А что если просто погуглить сочетание BugBounty и никнейм Bughunter? Тогда мы найдем профиль человека на площадке багбаунти по ссылке типа такой https://[masked].com/ru-RU/profile/*Bughunter*/.
PS О том, что это никнейм, мы узнали только после того, как нашли профиль пользователя. Слово, которое мы заменили на Bughunter, не похоже на ник и первоначально не находилось при аналогичном поисковом запросе. Ну, а дальше уже все пошло как по маслу. В профиле площадки багбаунти у него есть ссылка на личный телеграм, в аккаунте закреплен его открытый канал, который он подробно ведет. Из него мы узнали фамилию и имя, а также то, что он активно занимается багбаунти. И что VPN-сервис тоже принадлежит ему.
Преступление и наказание?
Напомним, что за несанкционированный доступ в инфраструктуру и выполнение вредоносных действий предусмотрена уголовная ответственность, согласно нашему законодательству. Даже если это подается под соусом багбаунти — ведь Заказчик не выставлялся на него.
Поэтому мы передали всю найденную информацию Заказчику, чтобы он дальше сам вершил судьбу багхантера-нелегала. Нам сказали «Большое спасибо», а через полчаса вернулись с обратной связью…
Как все было на самом деле?
Оказалось, что Заказчик был выставлен на «закрытую» программу багбаунти на реализацию недопустимого события. И наш багхантер выступал исследователем по данным работам. Теперь вернитесь в начало статьи и посмотрите на суслика...
На самом деле мы, конечно же, не расстроились, потому что доблестный Центр мониторинга и реагирования на инциденты ИБ выполнял свой долг, и считаем, что справились с этим, хоть и не нашли причину утечки ssh-ключа. Но мы были очень близки…
Багхантер нашел опубликованный npm-реестр Заказчика, где была открыта регистрация. Он зарегистрировался в данном сервисе и заменил один из пакетов на свою версию, в которой был вредоносный скрипт postinstall. Через несколько дней разработчик работал в IDE WebStorm и подтянул обновленный пакет. Скрипт собрал информацию об окружении и содержимое каталога ~/.ssh, а затем осуществил эксфильтрацию на C2. Такой вот вышел своего рода supply chain.
А близки мы были, потому что при расследовании видели опубликованный npm-реестр, однако тот пакет не был виден на главной странице, найти его можно было только через поисковую строку. Да и вряд ли без каких-либо улик кто-то пойдет перепроверять содержимое всех пакетов на изменения. Тем более, что по артефактам ОС не было видно явных нелегитимных действий.
Теперь же, зная способ «проникновения», мы провели небольшое исследование — а где в принципе это можно было обнаружить на узле?
В логах npm-cache C:\Users\user\AppData\Local\npm-cache\_logs\timestamp-debug-0 фиксируются две строчки, относящиеся к скрипту:
И… это все. Какие команды выполняются внутри скрипта, нигде не видно. При этом сам скрипт не остается на узле как файловый артефакт. В данном случае мог бы помочь только Sysmon\EDR.
Пример детектирования команды эксфильтрации на С2 с помощью события создания процесса Sysmon 1:
К сожалению, у нас на анализе был личный комп разработчика, на котором не был настроен даже классический аудит.
Подводим итоги
Всем аналитикам, которые столкнутся с аналогичным вектором кражи ssh-ключа и не найдут его в утечках, рекомендуем смотреть, какие пакеты устанавливались на узле разработчика. Обратите внимание на те немногие следы, которые остаются в логах менеджеров пакетов. И потратьте время на анализ последних измененных пакетов в реестре.
А что касается рекомендаций для всех владельцев похожих проблем в инфраструктуре:
не публикуйте в интернет критичные сервисы, необходимые в работе только внутренним пользователям;
не публикуйте в интернет критичные сервисы, необходимые в работе только внутренним пользователям;
в случае необходимости публикации сервисов в интернет используйте многофакторную аутентификацию;
в случае необходимости публикации сервисов в интернет используйте многофакторную аутентификацию;
обеспечьте постоянный контроль опубликованных сервисов и своевременно устраняйте излишние доступы (например, к формам регистрации);
обеспечьте постоянный контроль опубликованных сервисов и своевременно устраняйте излишние доступы (например, к формам регистрации);
не допускайте доступ без аутентификации к критичным ресурсам, даже если они находятся внутри сети;
не допускайте доступ без аутентификации к критичным ресурсам, даже если они находятся внутри сети;
разделите gitlab-runners в зависимости от команд и типов задач;
разделите gitlab-runners в зависимости от команд и типов задач;
разместите gitlab-runners в выделенные подсети разработчиков, настройте жесткие ACL только до необходимых серверов;
разместите gitlab-runners в выделенные подсети разработчиков, настройте жесткие ACL только до необходимых серверов;
используйте подписи коммитов для валидации вносимых изменений в gitlab;
используйте подписи коммитов для валидации вносимых изменений в gitlab;
настройте срок жизни ssh-ключей в администрировании gitlab;
настройте срок жизни ssh-ключей в администрировании gitlab;
используйте в работе только корпоративные устройства с настроенным аудитом и сбором телеметрии в системы мониторинга.
используйте в работе только корпоративные устройства с настроенным аудитом и сбором телеметрии в системы мониторинга.
Авторы:
Валерия Шотт, ведущий аналитик группы киберкриминалистики «Инфосистемы Джет»
Даниил Кирьяков, ведущий аналитик группы исследования киберугроз «Инфосистемы Джет»