stihl не предоставил(а) никакой дополнительной информации.
Что делать в конце пентеста FreeIPA — когда пароль получен, а доступа к контроллеру домена по SSH нет или там стоит грозная защита, не дающая сдампить id2entry.db и наслаждаться красивым отчетом? В случае с обычной Active Directory ответ очевиден — DCSync, и дело с концом, но для FreeIPA таких ресерчей нет... Что ж, подержи мое пиво, оставшееся у меня после Для просмотра ссылки Войди или Зарегистрируйся CVE-2024-3183.
Для просмотра ссылки Войдиили Зарегистрируйся
Это исследование получило первое место на Для просмотра ссылки Войдиили Зарегистрируйся в категории «Пробив инфраструктуры». Соревнование ежегодно проводится компанией Awillix.
Мое новое исследование напрямую не связано с предыдущим, но из одного вылилось другое, да и на проектах они применяются совместно.
Почему я подался и в номинацию Out of Scope? Считаю, что тут ценнее сам ресерч, а не место его применения.
Рассказ я попытался максимально сократить и убрать технические подробности, которые могут тебе помешать следить за сутью.
Тогда откроем исходный код, это нам сильно облегчает анализ по сравнению с тем же Microsoft Domain Controller. И в исходном коде можно увидеть некоторое количество OID, отвечающих за репликацию.
Это только часть OID, отвечающих за репликацию
Однако дело упрощается тем, что репликация здесь вынесена в отдельный Для просмотра ссылки Войдиили Зарегистрируйся из ldap/servers/plugins/replication. Можно изучить его исходники и понаблюдать за трафиком в процессе репликации двух контроллеров доменов. Сделав это, я установил несколько фактов:
Вспомним оригинальный ресерч по DCShadow и начнем собирать необходимую информацию:
Сначала посмотрим на запись трафика репликации двух контроллеров доменов и выясним, что они используют аутентификацию через Kerberos SASL bind. Что примечательно, TLS не используется, так как трафик шифруется сессионным ключом. Расшифровать трафик можно, используя AES-ключ сервиса LDAP контроллера домена (во FreeIPA каждый сервис имеет свой ключ).
Теперь посмотрим чистый трафик и увидим, что используются запросы с OID 2.16.840.1.113730.3.5.12 и 2.16.840.1.113730.3.5.6 (Wireshark не совсем точно отображает расшифровку этого OID). Первый запрос отвечает за начало репликации, а второй — за передачу записи.
Фрагмент расшифрованного трафика репликации
Отлично! Теперь у нас есть ответы на ряд вопросов. Также заметим, что сначала происходит сверка схемы и содержимого корня LDAP.
Но как же все‑таки объявить всему миру, что мы теперь DC? А вот тут уже на помощь приходит исходный код FreeIPA, документация и анализ схемы LDAP. Опущу ненужные детали и приведу только основную информацию.
Как реплики общаются между собой? Казалось бы, мы знаем, как это работает в MS DC: сайты, одноуровневые контроллеры домена и так далее. Но тут нам надо забыть все, что мы знаем, и начать все с начала. Да и логику придется отбросить — она тут будет только мешать.
Представим простую ситуацию: есть три DC, и все отвечают за один домен (соответственно, реплицируют между собой, в этом рассказе опустим все четыре варианта репликации и сосредоточимся на основном — двустороннем). Что будет, если один из DC выключится? Продолжат ли общаться между собой другие два?
Если тебе кажется, что ответ очевиден (а в случае с MS DC это так), то ты ошибаешься. Из данного мной условия нельзя получить однозначный ответ! Неожиданно, да? Все просто: во FreeIPA (а точнее, в 389 DS) есть такое понятие, как Replica Topology, — грубо говоря, мы сами настраиваем, кто с кем и как будет реплицироваться.
Рассмотрим пример с тремя серверами.
Пример топологии репликации для трех серверов
При такой схеме очевидно, что при выходе из строя сервера A два других не будут реплицироваться, отношения сами не перестроятся. Из этого Для просмотра ссылки Войдиили Зарегистрируйся рождаются рекомендации для четвертого, пятого и последующих серверов.
Теперь ответ достаточно очевиден: нам надо просто взять и добавиться к какому‑нибудь существующему серверу в топологию. В LDAP за это отвечает участок по пути cn=mapping tree,cn=Config, где прописываются пути, которые будут реплицироваться.
В каждой схеме есть каталог cn=replica, где и добавляются связи. Вот пример полного пути:
Можно с уверенностью сказать, что на вопрос «куда добавлять?» мы ответили. Но остается вопрос «что добавлять?». Основные атрибуты, которые нас интересуют, — это:
С вопросом «что» тоже разобрались. Но что делать дальше? Программировать... Начал я с того, что поднимал свой 389 DS, используя библиотеки FreeIPA, — благо это не так сложно, и в итоге мы получаем полную базу данных домена без всяких заморочек.
Самое главное — это установить 389 DS, потом добавить файл ldif из каталога freeipa и активировать плагин, отвечающий за репликацию.
Настройка 389 DS с помощью Python
Содержимое id2entry, которое формирует 389 DS
Именно так я и доставал данные с контроллеров домена на проектах в крупных компаниях с хорошим SOC! Плюс этого подхода в том, что в итоге мы получаем готовый файл id2entry, так как 389 DS сохраняет все в удобном для нас виде.
Но в идеале хотелось получить готовый инструмент без необходимости ставить свой 389 DS.
Поэтому, вооружившись питоном и библиотекой ldap3, я начал разработку собственного сервера LDAP.
Главное, что он в нашем случае должен уметь, — это отвечать на два запроса:
Итак, теперь для эксплуатации нам нужны всего лишь:
Пример работы утилиты на тестовом стенде
К сожалению, пока тулза не до конца готова, да и проекты с FreeIPA мне пока что больше не попадались. Например, в утилите нет автоматического добавления записи в конфиг, но все же я планирую ее доработать и опубликовать.
И напоследок отвечу на главный вопрос: «А какие права все‑таки нужны для атаки?» Все, что нам нужно, — правило ACL для записи по пути cn=Config.... Но во FreeIPA не все так просто, ACL в LDAP генерируются на основе специальных прав, и нас интересуют эти:
По умолчанию такие права есть у группы ipa servers, службы LDAP на контроллерах домена и, конечно, членов группы admins.
Так я пришел к анализу всех ACL, которые создаются в домене по умолчанию, и то, что я обнаружил, заставило продолжить исследование в неожиданном направлении.
aci: (target = "ldap:///krbprincipalname=/($dn)@TEST.LOCAL, cn=services,cn=accounts,dc=test,dc=local")(targetfilter = "(objectClass=ipaService)") (version3.0; acl "Hosts can add own services"; allow(add) userdn= "ldap:///fqdn=($dn),cn=computers, cn=accounts,dc=test,dc=local"
Если вкратце, то каждый хост может создать запись в LDAP вот по такому пути:
krbprincipalname=/($dn)@TEST.LOCAL,cn=services,cn=accounts,dc=test,dc=local*
То есть имя как у хоста, а название сервиса — любое. А что с атрибутами? А они могут быть вообще любые! Что забавно, менять их мы уже не сможем. И тут запахло уязвимостью.
Действительно, в ходе исследования атрибутов пользователей домена оказалось, что у admin не установлено значение krbCanonicalName, но при этом есть значение атрибута krbPrincipalName. И это позволяет нам создать сервис с «правильным» krbPrincipalName и нашим krbCanonicalName. А дальше аккуратно перейдем к эксплуатации.
Теперь создадим произвольный сервис test с атрибутом krbCanonicalName, равным admin@REALM.LOCAL. Если, например, попробовать указать любого другого пользователя, то будет ошибка, сообщающая, что этот атрибут должен быть уникальным.
После добавления сервиса надо запросить для него ключи AES для Kerberos. Сделать это можно через ipa-getkeytab, что позволено нам, как владельцам сервиса.
Запрашиваем билет от имени этого сервиса, используя его обычное имя — test/host.realm.local@REALM.LOCAL. Теперь посмотрим, что нам покажет klist, и видим, что билет‑то выписан на <admin@REALM.LOCAL>! Оказывается, MIT Kerberos при запросе билета смотрит в krbPrincipalName, а значение в билет берет из атрибута krbCanonicalName.
И тут внимательный читатель должен воскликнуть: «А как же PAC?!» Не переживай! MIT Kerberos во FreeIPA, конечно, добавляет PAC в билеты, но если очень сильно попросить, то уберет (в kinit даже есть флаг --no-request-pac). И с этим билетом мы можем спокойно сходить в LDAP и глянуть, кто мы теперь такие.
Полная эксплуатация — на скриншоте ниже. Замечу, что DC9 — это эмуляция обычного хоста в домене, не имеющего дополнительных привилегий.
Компрометация домена
Обрати внимание на дату: именно в этот день я отправил информацию вендору (Red Hat), однако он исправлял уязвимость четыре месяца — до конца июня.
Баг получил идентификатор Для просмотра ссылки Войдиили Зарегистрируйся. В информации об обновлении на сайтах Для просмотра ссылки Войди или Зарегистрируйся и Для просмотра ссылки Войди или Зарегистрируйся ни в каком виде не упомянуто, что именно приводит к уязвимости: обошлись лишь формулировками вроде «ошибка». Ну хоть CVSS присвоили большой — 9,1.
Для просмотра ссылки Войдиили Зарегистрируйся
И упомянули в релизе!
Для просмотра ссылки Войдиили Зарегистрируйся
Собственно, повторился мой опыт отправки этому же вендору уязвимости CVE-2024-3183.
А может быть, можно еще и вносить изменения в LDAP через этот механизм? Продолжение следует.
Для просмотра ссылки Войди
Это исследование получило первое место на Для просмотра ссылки Войди
Мое новое исследование напрямую не связано с предыдущим, но из одного вылилось другое, да и на проектах они применяются совместно.
Почему я подался и в номинацию Out of Scope? Считаю, что тут ценнее сам ресерч, а не место его применения.
Рассказ я попытался максимально сократить и убрать технические подробности, которые могут тебе помешать следить за сутью.
DCSync
Что ж, начнем разбираться, как работает репликация в 389 Directory Server. Именно этот продукт отвечает за сервер LDAP во FreeIPA. Давай заглянем в документацию... Впрочем, не заглянем, потому что ее нет!Тогда откроем исходный код, это нам сильно облегчает анализ по сравнению с тем же Microsoft Domain Controller. И в исходном коде можно увидеть некоторое количество OID, отвечающих за репликацию.
Однако дело упрощается тем, что репликация здесь вынесена в отдельный Для просмотра ссылки Войди
- В отличие от MS DC нельзя запросить изменения, можно только прийти с новыми.
- Контроллеры домена не используют RPC (собственно, во FreeIPA вообще такого нет).
- Если меняются значения атрибутов, репликация происходит сразу по инициативе контроллера домена, на котором произошло изменение.
Вспомним оригинальный ресерч по DCShadow и начнем собирать необходимую информацию:
- Как контроллер домена обращается к другому?
- Что нужно, чтобы нас восприняли как другой DC?
- Как нам обработать запрос от другого DC и сохранить результат?
- Какие права нужны для атаки?
Сначала посмотрим на запись трафика репликации двух контроллеров доменов и выясним, что они используют аутентификацию через Kerberos SASL bind. Что примечательно, TLS не используется, так как трафик шифруется сессионным ключом. Расшифровать трафик можно, используя AES-ключ сервиса LDAP контроллера домена (во FreeIPA каждый сервис имеет свой ключ).
Теперь посмотрим чистый трафик и увидим, что используются запросы с OID 2.16.840.1.113730.3.5.12 и 2.16.840.1.113730.3.5.6 (Wireshark не совсем точно отображает расшифровку этого OID). Первый запрос отвечает за начало репликации, а второй — за передачу записи.
Отлично! Теперь у нас есть ответы на ряд вопросов. Также заметим, что сначала происходит сверка схемы и содержимого корня LDAP.
Но как же все‑таки объявить всему миру, что мы теперь DC? А вот тут уже на помощь приходит исходный код FreeIPA, документация и анализ схемы LDAP. Опущу ненужные детали и приведу только основную информацию.
Как реплики общаются между собой? Казалось бы, мы знаем, как это работает в MS DC: сайты, одноуровневые контроллеры домена и так далее. Но тут нам надо забыть все, что мы знаем, и начать все с начала. Да и логику придется отбросить — она тут будет только мешать.
Представим простую ситуацию: есть три DC, и все отвечают за один домен (соответственно, реплицируют между собой, в этом рассказе опустим все четыре варианта репликации и сосредоточимся на основном — двустороннем). Что будет, если один из DC выключится? Продолжат ли общаться между собой другие два?
Если тебе кажется, что ответ очевиден (а в случае с MS DC это так), то ты ошибаешься. Из данного мной условия нельзя получить однозначный ответ! Неожиданно, да? Все просто: во FreeIPA (а точнее, в 389 DS) есть такое понятие, как Replica Topology, — грубо говоря, мы сами настраиваем, кто с кем и как будет реплицироваться.
Рассмотрим пример с тремя серверами.
При такой схеме очевидно, что при выходе из строя сервера A два других не будут реплицироваться, отношения сами не перестроятся. Из этого Для просмотра ссылки Войди
Теперь ответ достаточно очевиден: нам надо просто взять и добавиться к какому‑нибудь существующему серверу в топологию. В LDAP за это отвечает участок по пути cn=mapping tree,cn=Config, где прописываются пути, которые будут реплицироваться.
info
Во FreeIPA реплицироваться могут отдельные пути LDAP, например только cn=accounts,dc=realm,dc=local. Глобально это нужно, чтобы отделить данные CA от объектов домена.В каждой схеме есть каталог cn=replica, где и добавляются связи. Вот пример полного пути:
cn=meTodc2.test.local,cn=replica,cn=dc\=test\,dc\=local,cn=mapping tree,cn=Config
Можно с уверенностью сказать, что на вопрос «куда добавлять?» мы ответили. Но остается вопрос «что добавлять?». Основные атрибуты, которые нас интересуют, — это:
- nsDS5ReplicaBindMethod — отвечает за тип аутентификации на конечном сервере (по умолчанию SASL/GSSAPI, то есть через Kerberos), но мы упростим себе жизнь и сделаем empty;
- nsDS5ReplicaHost — адрес нашего псевдо-DC;
- nsDS5ReplicaPort — порт нашего псевдо-DC (чтобы нам не требовался root на хосте, можно установить значение > 1024);
- nsds5replicaTimeout — время, через которое происходит репликация (вообще, при добавлении записи репликация должна происходить сразу, но на всякий случай можно устанавливать в 1).
С вопросом «что» тоже разобрались. Но что делать дальше? Программировать... Начал я с того, что поднимал свой 389 DS, используя библиотеки FreeIPA, — благо это не так сложно, и в итоге мы получаем полную базу данных домена без всяких заморочек.
Самое главное — это установить 389 DS, потом добавить файл ldif из каталога freeipa и активировать плагин, отвечающий за репликацию.
Именно так я и доставал данные с контроллеров домена на проектах в крупных компаниях с хорошим SOC! Плюс этого подхода в том, что в итоге мы получаем готовый файл id2entry, так как 389 DS сохраняет все в удобном для нас виде.
Но в идеале хотелось получить готовый инструмент без необходимости ставить свой 389 DS.
Поэтому, вооружившись питоном и библиотекой ldap3, я начал разработку собственного сервера LDAP.
Главное, что он в нашем случае должен уметь, — это отвечать на два запроса:
- Search (отдавать правильную схему и корень);
- с OID 2.16.840.1.113730.3.5.12 (старт репликации).
Итак, теперь для эксплуатации нам нужны всего лишь:
- права на запись по пути в LDAP;
- возможность DC обратиться к нам на порт (сетевая доступность);
- установленный Python!
К сожалению, пока тулза не до конца готова, да и проекты с FreeIPA мне пока что больше не попадались. Например, в утилите нет автоматического добавления записи в конфиг, но все же я планирую ее доработать и опубликовать.
И напоследок отвечу на главный вопрос: «А какие права все‑таки нужны для атаки?» Все, что нам нужно, — правило ACL для записи по пути cn=Config.... Но во FreeIPA не все так просто, ACL в LDAP генерируются на основе специальных прав, и нас интересуют эти:
- REPLICATION MANAGERS;
- REPLICATION ADMINISTRATORS;
- ADD REPLICATION AGREEMENTS;
- MODIFY REPLICATION AGREEMENTS.
По умолчанию такие права есть у группы ipa servers, службы LDAP на контроллерах домена и, конечно, членов группы admins.
info
Этот способ компрометации домена использовался на проекте, что дало мне возможность «по‑тихому» забрать домен.Так я пришел к анализу всех ACL, которые создаются в домене по умолчанию, и то, что я обнаружил, заставило продолжить исследование в неожиданном направлении.
Что за CVE-2025-4404?
Итак, нам нужны специфические привилегии (ACL в LDAP). И тут у меня возник вопрос: а безопасны ли вообще ACL, которые существуют по умолчанию? После выгрузки их в домен контроллера я обратил внимание на такую строку:aci: (target = "ldap:///krbprincipalname=/($dn)@TEST.LOCAL, cn=services,cn=accounts,dc=test,dc=local")(targetfilter = "(objectClass=ipaService)") (version3.0; acl "Hosts can add own services"; allow(add) userdn= "ldap:///fqdn=($dn),cn=computers, cn=accounts,dc=test,dc=local"
Если вкратце, то каждый хост может создать запись в LDAP вот по такому пути:
krbprincipalname=/($dn)@TEST.LOCAL,cn=services,cn=accounts,dc=test,dc=local*
То есть имя как у хоста, а название сервиса — любое. А что с атрибутами? А они могут быть вообще любые! Что забавно, менять их мы уже не сможем. И тут запахло уязвимостью.
Действительно, в ходе исследования атрибутов пользователей домена оказалось, что у admin не установлено значение krbCanonicalName, но при этом есть значение атрибута krbPrincipalName. И это позволяет нам создать сервис с «правильным» krbPrincipalName и нашим krbCanonicalName. А дальше аккуратно перейдем к эксплуатации.
Путь эксплуатации
Для эксплуатации уязвимости сначала необходимо получить TGT на любой доменный хост, чаще всего после получения привилегий root. Судя по моему опыту пентестов с инфраструктурой на базе FreeIPA, это частая ситуация. Так что оставим этот шаг и сделаем заветный kinit --k --t /etc/krb5.keytab.Теперь создадим произвольный сервис test с атрибутом krbCanonicalName, равным admin@REALM.LOCAL. Если, например, попробовать указать любого другого пользователя, то будет ошибка, сообщающая, что этот атрибут должен быть уникальным.
После добавления сервиса надо запросить для него ключи AES для Kerberos. Сделать это можно через ipa-getkeytab, что позволено нам, как владельцам сервиса.
Запрашиваем билет от имени этого сервиса, используя его обычное имя — test/host.realm.local@REALM.LOCAL. Теперь посмотрим, что нам покажет klist, и видим, что билет‑то выписан на <admin@REALM.LOCAL>! Оказывается, MIT Kerberos при запросе билета смотрит в krbPrincipalName, а значение в билет берет из атрибута krbCanonicalName.
И тут внимательный читатель должен воскликнуть: «А как же PAC?!» Не переживай! MIT Kerberos во FreeIPA, конечно, добавляет PAC в билеты, но если очень сильно попросить, то уберет (в kinit даже есть флаг --no-request-pac). И с этим билетом мы можем спокойно сходить в LDAP и глянуть, кто мы теперь такие.
Полная эксплуатация — на скриншоте ниже. Замечу, что DC9 — это эмуляция обычного хоста в домене, не имеющего дополнительных привилегий.
Обрати внимание на дату: именно в этот день я отправил информацию вендору (Red Hat), однако он исправлял уязвимость четыре месяца — до конца июня.
Общение с вендором
Итак, спустя четыре месяца в Red Hat исправили уязвимость, добавив значение атрибуту krbCanonicalName для администратора домена.Баг получил идентификатор Для просмотра ссылки Войди
Для просмотра ссылки Войди
И упомянули в релизе!
Для просмотра ссылки Войди
Собственно, повторился мой опыт отправки этому же вендору уязвимости CVE-2024-3183.
Выводы
Итак, что мы узнали:- В исходном коде можно найти то, чего нет в документации (очень помогла возможность загрузки его в gdb).
- Изобретать свои способы репликации — норма для разрабов.
- С FreeIPA надо возвращаться к DCShadow.
- Реализовывать свой LDAP-сервер на Python весело и не особенно просто, зато работает.
- «Ошибки» разработчиков приводят ко все новым и новым CVE.
- В билете отсутствует PAC (хотя тут его можно было бы подделать, но это уже совсем другая история).
- Я добавил работы «синим» командам, пусть не расслабляются!
А может быть, можно еще и вносить изменения в LDAP через этот механизм? Продолжение следует.