Мы спросили, багхантеры они или нет, они сказали «Нет»

Мы спросили, багхантеры они или нет, они сказали «Нет»

Всем привет от команды 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;

используйте в работе только корпоративные устройства с настроенным аудитом и сбором телеметрии в системы мониторинга.

используйте в работе только корпоративные устройства с настроенным аудитом и сбором телеметрии в системы мониторинга.

Авторы:

Валерия Шотт, ведущий аналитик группы киберкриминалистики «Инфосистемы Джет»

Даниил Кирьяков, ведущий аналитик группы исследования киберугроз «Инфосистемы Джет»

← Cybersecurity