Введение

Доброе утро, добрый день, вечер и добрая ночь, Хабр! Меня зовут Миша, впрочем в детстве я звал себя «Мо». В свободное от сна, еды и спорта время я работаю ведущим экспертом по тестированию на проникновение в команде CICADA8 Центра инноваций МТС Future Crew.

Я уверен, вы не раз слышали про Kerberos Relay, однако у большинства специалистов достаточно поверхностное представление об этой атаке, ведь при попытке гуглежа вываливается материал от Google Project Zero, который так с нуля осилить сразу-то и не получится :) Альтернатив, в принципе, нет, поэтому статьи гордо отправляются в очередной пыльный деревянный комод с надписью «Потом разберусь».

У меня была точно такая же ситуация.

Все изменилось после НЕвыхода POC на атакy CertifiedDCOM, старшего брата ADCSCoercePotato. Мне стало невыносимо больно от того, что я не в состоянии эксплуатировать интересную уязвимость, поэтому было принято волевое решение о написании POC самостоятельно.

В статье познакомлю вас с атакой Kerberos Relay, расскажу про разработанный инструмент RemoteKrbRelay и рассмотрю варианты использования для исследовательских целей.

TL;DR

Краткое представление

Тулзу можно использовать как в режиме "кнопка тык — домен админ" (Ctrl+C - Ctrl+V команды ниже. Имена компьютеров заменить не забудьте :) ), так и в исследовательском режиме для обнаружения новых уязвимых DCOM-объектов в среде AD.

На момент написания статьи (04.10.2024) задокументированы две атаки, основной принцип работы которых зиждется на ретрансляции Kerberos аутентификации и уязвимых DCOM-объектах.

Под уязвимым DCOM-объектом понимается DCOM-объект, на который установлены неверные права (кривой DACL).

Сама аутентификация прилетает к нам потому, что мы по-особенному создаем DCOM-объект. И буквально требуем от целевой системы (на которой происходит создание) прийти к нам и отдать свои креды :)

CertifiedDCOM

CertifiedDCOM — возможность ретрансляции Kerberos аутентификации сервера AD CS (компьютерной учетной записи). CLSID уязвимого объекта: d99e6e74-fc88-11d0-b498-00a0c90312f3.

Импакт: релей на LDAP с настройкой RBCD/ShadowCreds/сменой пароля, как следствие, захват ADCS и повышение привилегий в доменной среде.

Требования (если в системе никто ничего не ворошил, связанное с правами на DCOM): членство в одной из групп: Certificate Service DCOM Access, Performance Log Users, Distributed COM Users .

Реализация:

# Настройка RBCD на ADCS
# Атакуем adcs.root.apchi , осуществляем релей Kerberos на dc01.root.apchi. После атаки с FAKEMACHINE$ появится возможность захвата adcs.root.apchi через RBCD
.\RemoteKrbRelay.exe -rbcd -victim adcs.root.apchi -target dc01.root.apchi -clsid d99e6e74-fc88-11d0-b498-00a0c90312f3 -cn FAKEMACHINE$
Пример атаки. В данном случае ADCS сервер стоит на dc01.root.apchi.
Пример атаки. В данном случае ADCS сервер стоит на dc01.root.apchi.

Демонстрацию атаки можно увидеть здесь.

SilverPotato

SilverPotato — также релей Kerberos-аутентификации, но не системы, а пользователя, чья сессия присутствует на устройстве. CLSID уязвимого объекта: F87B28F1-DA9A-4F35-8EC0-800EFCF26B83.

Импакт: захват сессии пользователя, повышение привилегий в домене. Ретрансляция в LDAP с настройкой RBCD/ShadowCred/смены пароля пользователя/LAPS, при наличии соответствующих прав у атакуемого пользователя.

Требования (если в системе никто ничего не ворошил, связанное с правами на DCOM): членство в одной из групп:Performance Log Users, Distributed COM Users.

Реализация:

# Злоупотребление сессий номер 1 на устройстве dc01.root.apchi с ретрансляцией аутентификации на LDAP на dc01.root.apchi и сбросом пароля пользователя Administrator
.\RemoteKrbRelay.exe -chp -victim dc01.root.apchi -target dc01.root.apchi -clsid f87b28f1-da9a-4f35-8ec0-800efcf26b83 -chpuser Administrator -chppass Lolkekcheb123! -secure -session 1

# Добавление пользователя dcom в группу администраторов домена (в сессии 1 сидит домен админ)
.\RemoteKrbRelay.exe -addgroupmember -group "Администраторы домена" -groupuser "dcom" -victim dc01.vostok.street -target dc01.vostok.street -clsid f87b28f1-da9a-4f35-8ec0-800efcf26b83 -session 1
Пример эксплуатации с добавлением в группу
Пример эксплуатации с добавлением в группу

Отмечу, что вариантов осуществления атаки много и полностью они приведены на нашем Github.

Предыстория исследования

Уходящая зима 2024 года оказалась в моем далеком уральском регионе скупа на приятно падающий с неба белый снег. Последние дни февраля шли своим чередом, не предвещая интересных приключений. Тем теплым утром 26-го числа я проснулся чуть раньше обычного. Подобно истинному компьютерщику, вместо утреннего нежного поцелуя жены с последующим завтраком в постель я открыл Telegram-каналы по ИбЭ.

В ещё сонные глаза попала статья Hello: I’m your ADCS server and I want to authenticate against you. Бегло прочитав, я понял, что эту Бастилию сходу не взять и нужно плотно разбираться в атаке. Впрочем, утро только начиналось, и впереди был целый день.

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

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

Вкратце атака основывалась на том, что если у нас есть пользователь с членством в группе Certificate Service DCOM Access, то мы можем затриггерить через уязвимый DCOM-объект ADCS сервер, получить NetNTLM-аутентификацию и успешно ее ретранслировать в какой-нибудь очередной сервис MS, на который вездесущие безопасники забыли засунуть принудительную подпись.

Как видите, ничего умопомрачительного нет. Еще и нейминг неправильный!!!! *Potato — атаки на повышение привилегий от пользователя с SeImpersonatePrivilege до системы. Впрочем, куда мне умничать........

За сытным обедом последовал не менее долгожданный ужин, второй ужин, кефир перед сном, бутерброд перед сном, чай после бутерброда (смыть кефир), чистка зубок и крепкий, здоровый, восьмичасовой храп до утра.

Так прошло 27 февраля.

Алгоритм повторился и на следующий день: жена вновь осталась без утреннего поцелуя — вышел новый пост. Анонс доклада на Black Hat Asia, который назывался CertifiedDCOM: The Privilege Escalation Journey to Domain Admin with DCOM.

Анонс доклада
Анонс доклада

Особых подробностей по ссылке не было, стандартно: «мы все сломали, а вы все чините, хы». Однако я обратил внимание на скриншот. Это же интерфейс KrbRelay! Сразу же открываю код, начинаю в нем копаться и..... Безрезультатно. Ничего не понятно. Закрыл ноутбук, пошел завтракать.

Увы не последний день зимы текущего високосного года также быстро срезался секундными стрелками суровых часов, изготовленных где-то на производственной окраине необъятного Тяньцзиня. Однако доклад упорно не хотел забываться, а голова продолжала думать: «Как же это работает?»

Все-таки лень взяла вверх, и я решил дождаться выступления на Black Hat.

Так прошел февраль, наступил март, восьмое марта ? ? ? (причастных с прошедшим), зарплата, снова зарплата, апрель, зарплата и..... долгожданный доклад!

Tianze Ding поделился интересным теоретическим материалом, показал, что у него есть Proof-Of-Concept, но, к великому сожалению товарищей хакеров, забыл приложить POC к выступлению. И даже релизнуть после Black Hat. И даже после-после всех самых последних недельных афтепати....

Скажу честно, меня почему-то это задело. Что-то внутри моего, чуть меньше центнера массой, тела провернулось и не хотело вставать обратно.

Единственным вариантом закрыть гештальт было только одно — написать POC самостоятельно. Чем, собственно, и занялся.

До конца самого противоречивого на Урале месяца свободное время я посвящал изучению всех существующих в сети материалов про атаки на DCOM и в особенности Kerberos Relay.

Почему противоречивый? Только в этом месяце вы можете наблюдать картину: последний айфон рисует на своем AMOLED 4K ULTRA HD семидюймовом дисплее обнадеживающие +20 градусов, когда в то же время стоящие на остановке девушки, радостно сменившие теплые штаны с меховым подкладом из собачьей шерсти на короткие розовые, красные, желтые и деловые черные юбки, наслаждаются из ниоткуда решившим пойти снегом.

Любоваться этим мне было некогда — нужно писать POC.

Уже в первые числа мая я точно знал, как это работает, из чего растут ноги, руки, нос и даже на каком месте был давно отрезанный аппендикс. Свои похождения начал с ADCSCoercePotato.

Оригинальный сплойтец требует предоставления учетных данных для триггера ADCS, но далеко не всегда у нас есть креды. В таком случае логичнее было бы использовать интегрированный механизм SSO винды :) Помните, это когда она автоматически проходит аутентификацию на желаемом для нас эндпоинте? Чуть правлю код, добавляю пару аргументов, кидаю PR, но автор гордо игнорирует предложенные изменения.

Плюс-минус тогда же начал пилить POC на CertifiedDCOM. На тот момент POC должен был быть достаточно «узким». Релей ADCS, по конкретному CLSID, как в оригинальном исполнении. На первых релизах и тестах были обнаружены множественные варианты развития инструмента и я подумал: «Зачем мимикрировать под чужой POC, если можно сделать полностью свой?». Так появились первые строчки RemoteKrbRelay.

Мимо беспощадно пролетали последние теплые беззаботные майские дни. Впереди были июнь, сессия и пхд. Последнее мероприятие на порядок важнее первых двух — я ж с докладом :))) 22 мая получил билеты, ваучер на проживание в отеле (ещё каком! Radisson Collection), оформил отпуск — жизнь только начиналась! Прошел паспортный контроль в аэропорту, сидел в «Кольцово» на самом обычном кресле не менее обычного зала ожидания. От бесконечного числа офисных планктонов в пиджачках и порядком обгоревших в Египетском недельном отпуске туристов меня отличал разве что ярко оранжевый апельсиновый фреш — небольшая традиция при вылете, вместо коньяка, для спокойствия. Где-то там, в бесконечной суете небесного вокзала, прерываемой редкими «Пассажиры, опаздывающие на рейс...», «Выход на посадку изменен...», «Обо всех подозрительных предметах сообщайте в полицию», родилась одна из первых версий RemoteKrbRelay.

В куче тусовок, сменяемых докладами именитых спикеров, помноженных на очередные такие же полуночные беспросветные движухи, родилось множество вариантов развития инструмента.

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

Грянул майский гром и веселье бурною, пьянящею волной

Окатило. Эй, вставай-ка и попрыгай вслед за мной

Пролетел май, наступил июнь, я стал одним целым с KrbRelay и KrbRelayUp, а любая свежая статья на decoder.cloud становилась не более, чем очередным вызовом для написания нового POC.

В конце концов, 25 июня выложил весь код RemoteKrbRelay, вместе с чекерами. Такие дела.

Принцип ретрансляции Kerberos

Большинство методов принудительной аутентификации делятся на две большие категории:

  • Method-Based — злоупотребление методами конкретного объекта. Будь то COM, DCOM, RPC-сервер. Например, PetitPotam — сборная солянка из кривых прав на интерфейс df1941c5-fe89-4e79-bf10-463657acf44d, внутри которого есть метод, принимающий UNC Path. Отсюда и креды.

  • Creation-Based — злоупотребление особенностями поведения винды при создании объекта. Например, RemoteKrbRelay строится на поднятии фейкового DCOM-сервера и захвата аутентификации на нем, а вот ADCSCoercePotato ничего не поднимает, а ловит аутентификацию из Initial OXID Resolution Request. Особое внимание будет уделяться именно этому способу.

Method-Based

Большинство публично доступных методов принудительной аутентификации построены на одном и том же фундаменте. Исследователи, используя RPCView, натыкивают интересный RPC-интерфейс и извлекают доступные методы. Простой вариант — из IDL-файлов каким-нибудь idl_scraper, сложный — реверсят идой. Затем остается найти нужный функционал, который мог бы привести к утечке учетных данных. Например, механизм открытия файла. Остается разве что сделать import impacket, определить все структуры на питоне (да-да :D) и вызвать метод.

Вместо RPCView кто-то отдает предпочтение автоматизации через NtObjectManager с его Get-RpcServer (который принимает пайплайном список любых файлов) или извлечению списка серверов из EndpointMapper (RPCDump), вместо анализа GlobalRpcServer. У каждого метода есть свои плюсы и минусы. Погружаться и дискутировать можно долго, отличное исследование и сравнения провели здесь.

По моему мнению самая крутая связка: Powershell с NtObjectManager, а докрутка через RPCView. Например, так можно извлечь список RPC-серверов из SYSTEM32.

PS C:\Users\user> $rpc = ls "C:\Windows\system32\*" -Include "*.dll","*.exe" | Get-RpcServer [-DbgHelpPath "C:\Tools\WindowsSDK\WindowsKits\10\Debuggers\x64\dbghelp.dll"]

Приятной десертной сладостью идет флаг -ParseClients , через который тулзятина достанет клиентов RPC-сервера.

$rpc = ls C:\Windows\System32\\* | Get-RpcServer –ParseClients -DbgHelpPath "C:\Tools\WindowsSDK\WindowsKits\10\Debuggers\x64\dbghelp.dll"

Информация о клиентах и серверах теперь будет лежать в переменной $rpc. Оттуда уже извлекаются все файлы, использующие конкретный UUID.

$rpc | ? {($_.InterfaceId -eq 'e3514235-4b06-11d1-ab04-00c04fc2dcd2')} | Select FilePath

# Только сервера
  
$rpc | ? {($_.Client -eq $False) -and ($_.InterfaceId -eq 'e3514235-4b06-11d1-ab04-00c04fc2dcd2')} | Select FilePath

Наконец, вариант «книжного червя-зануды». Он мне, кстати даже больше по душе. Открываете msdn и начинаете ковыряться в документации MS-* протоколов. Демо: находим MS-EVEN, у него есть ElfrOpenBELW(), а в BackupFileName можно положить NT Object Path.... Profit :)

Небольшое представление подобных методов триггера я делал тут.

Впрочем, аутентификацию Kerberos, которую можно релеить и которая прилетает по UNC Path, пока не изобрели. Или не делятся. Поэтому все эти способы отлично подходят для NTLM Relay атак.

Creation-Based

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

При создании статьи я стоял на некотором распутье: вариант А. опустить и забыть механизм инстанцирования DCOM-объектов и сразу перейти к описанию триггера, однако это привело бы к множеству вопросов: «а чо почему а зачем винда так делает» и внятного ответа сходу нагуглить бы не получилось, поэтому появился вариант Б: сделать лонгрид, но расписать все максимально подробно (или хотя бы приложить достаточное количество ссылок). Его я и выбрал. Там еще в стороне затесался вариант В: плюнуть и не писать статью :) Но мой внутренний гуманитарий запретил даже думать об этом.

Это я (на пони) перед созданием статьи
Это я (на пони) перед созданием статьи

Итак, начать нужно с описания механизма удаленного создания DCOM-объектов. Сами DCOMы это те же COMы, просто доступные удаленно. Remotely, как говорят наши зарубежные коллеги. COMы же — набор классов с интерфейсами, через которые можно дергать методы. Если мне не изменяет память, механизм изначально создавался для офисного пакета MS (да и назывался OLE) и нужен был для обмена данными между документами. Но все как-то пошло-поехало и теперь COM — неотъемлемая часть системы Windows.

Скажу честно, компонентов внутри COMа много: COM Class, COM Interface, COM Client, CLSID, IPID, ProdID, IID, AppID, Dispatching, Object Exporter, COM Stub, Proxy, Factories. А еще магические IDispatch, IUnknown и (о, ужас!) моникеры. Описывать все это... Пожалейте, я же не энциклопедия :)

Если вы ни разу не работали с COM, то рекомендую изучить ресурсы:

Для наглядной демонстрации я взял картинки со своих слайдов на прошедшей UnderConf. У нас есть роскошный клиент, желающий использовать COM-функционал, и чуть менее роскошный сервер, на котором этот самый функционал есть.

Начало создания
Начало создания

Для создания объекта клиент передает CLSID (идентификатор COM-класса) и IID (идентификатор интерфейса). Зачем еще IID? Внутри одного COM-класса может быть реализовано несколько интерфейсов. Всякие очкарики скажут, что можно отдавать IID IUnknown, а потом доставать нужный интерфейс через IDispatch->QueryInterface() , но давайте не будем усложнять :))

Удаленное инстанцирование чаще всего делается через функцию CoCreateInstanceEx().

HRESULT CoCreateInstanceEx(
  [in]      REFCLSID     Clsid,
  [in]      IUnknown     *punkOuter,
  [in]      DWORD        dwClsCtx,
  [in]      COSERVERINFO *pServerInfo,
  [in]      DWORD        dwCount,
  [in, out] MULTI_QI     *pResults
);
  • Clsid - CLSID создаваемого COM-класса;

  • punkOuter - NULL. Тут всякие агрегаты используются, это нам не надо;

  • dwClsCtx - контекст, в котором должен выполняться исполняемый код. Например, в случае удаленного инстанцирования COM-объектов, указывается CLSCTX_REMOTE_SERVER. Существуют и иные значения, все варианты смотри в перечисляемом типе CLSCTX;

  • pServerInfo — информация об удаленном сервере, на котором запускается COM-объект: IP, креды для подключения;

  • dwCount — количество создаваемых объектов;

  • pResults — сюда попадут интерфейсы на созданные объекты.

В дампе WireShark запрос выглядит так.

Начало создания (Wireshark)
Начало создания (Wireshark)

Следующим шагом сервер проверяет, имеет ли право клиент создавать DCOM-объект. Для удаленного создания требуются права RemoteLaunch (запуск COM-сервера), RemoteActivation (инстанцирование нового объекта).

Проверка прав клиента
Проверка прав клиента

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

Передача OBJREF
Передача OBJREF

В дампе сетевого трафика ответ сервера выглядит так.

Передача OBJREF (Wireshark)
Передача OBJREF (Wireshark)

С помощью OBJREF у клиента появляется возможность подключения к DCOM-серверу с последующим началом взаимодействия с нужным функционалом. В С-структурном виде OBJREF определен следующим образом.

typedef unsigned __int64 OXID;
typedef unsigned __int64 OID;
typedef GUID           IPID;
  
  
typedef struct tagSTDOBJREF  {
    unsigned long flags;
    unsigned long cPublicRefs;
    OXID oxid;
    OID  oid;
    IPID ipid;
} STDOBJREF;
  • flags и cPublicRefs — дополнительные флаги и количество ссылок на соответствующий серверный объект. Подробнее про эти поля смотри тут;

  • oxid — Object Exporter Identifier. Уникальный идентификатор экспортируемого объекта. По этому идентификатору клиент получит RPC String Binding и подключится к удаленному серверу;

  • oid — Object Identifier. Уникальный идентификатор конкретного объекта. Чтобы клиент, после подключения по RPC String Binding, знал, куда ему идти дальше для получения доступа до нужного DCOM-класса;

  • ipid — Interface Pointer Identifier. Уникальный идентификатор конкретного интерфейса объекта.

Благодаря этим трем параметрам, клиент может однозначно подключиться к целевому DCOM-классу на удаленном сервере. Сама структура OBJREF содержит чуть больше полезных данных, и, если быть честным, то описанный STDOBJREF входит в структуру OBJREF.

typedef struct tagOBJREF {
	unsigned long signature;
	unsigned long flags;
	GUID        iid;
	union {
		struct {
			STDOBJREF     std;
			DUALSTRINGARRAY saResAddr;
		} u_standard;
		struct {
			STDOBJREF     std;
			CLSID         clsid;
			DUALSTRINGARRAY saResAddr;
		} u_handler;
		struct {
			CLSID         clsid;
			unsigned long   cbExtension;
			unsigned long   size;
			byte* pData;
		} u_custom;
		struct {
			STDOBJREF     std;
			unsigned long   Signature1;
			DUALSTRINGARRAY saResAddr;
			unsigned long   nElms;
			unsigned long   Signature2;
			DATAELEMENT   ElmArray;
		} u_extended;
	} u_objref;
} OBJREF, * LPOBJREF;
  • signature — всегда одинаковая: MEOW ;

                       _ |\_
                       \` ..\
                  __,.-" =__Y=
                ."        )
          _    /   ,    \/\_
         ((____|    )_-\ \_-`
    meow  `-----'`-----` `--`

  • flags — указывает, на какую из структур ссылается u_objref;

  • iid — идентификатор интерфейса, который будет использоваться.

Как видите, у самого OBJREF есть несколько форматов: standart, handler, custom, extended. Отличия разве что в передаваемых данных. Отмечу, что в эксплойте используется стандартный OBJREF (внутри flags значение OBJREF_STANDART ).

Выше я обратил ваше внимание на то, что по OXID идет получение RPC String Binding. А что это такое и зачем оно нужно?

Казалось бы, клиент знает айпи адрес, иди подключайся и не парь мозги. Не тут-то было. RPC-сервера в Windows могут работать поверх разных транспортов: TCP, UDP и даже именованные каналы. Разве что поверх DNS не умеют (ещё чего, это ж не маячки кобальт страйка).

Соответственно, разные RPC-сервера слушают поверх разных транспортов. И клиенту нужно подключаться к нужной конечной точке (RPC String Binding), если он хочет достучаться до целевого RPC-сервера. RPC String Binding'и нигде в OBJREF не возвращаются, но возвращается OXID, по которому можно получить RPC String Binding.

Вопрос 2. Откуда клиент получает RPC String Binding?

Обратите внимание на поле saResAddr . В нем указывается OXID Resolver. То есть, компьютер, который может разрезолвить конкретный OXID в RPC String BInding. Представлено в виде DUALSTRINGARRAY, документация тут.

Таким образом, клиент на руки получает: OBJREF, внутри него OXID и адрес OXID Resolver'а. Для подключения нужен RPC String Binding, поэтому клиент идет резолвить OXID к OXID Resolver'у. Сравнивайте это с процессом резолва DNS. У нас есть любимый 8.8.8.8 (OXID Resolver), который знает айпишник (RPC String Binding) сайта google.com (OXID).

Итак, клиент идет резолвить OXID.

Клиент идет резолвить OXID
Клиент идет резолвить OXID

Этот запрос попадает на службу Endpoint Mapper сервера. Внутри нее хранится сопоставление между OXID'ом и RPC String Binding'ами. Сервер возвращает RPC String Binding'и клиенту.

Получение RPC String Binding'ов
Получение RPC String Binding'ов

В сетевом плане все выглядит вот так.

Отдача RPC String Binding'ов клиенту от сервера
Отдача RPC String Binding'ов клиенту от сервера

После чего клиент может биндиться к удаленной тачке и дергать СОМ-функционал.

Успешное взаимодействие
Успешное взаимодействие

Где же тут уязвимость? Существует функция CoGetInstanceFromIStorage(), которая позволяет инстанцировать объект из так называемого Storage Object. Фактически, такое сериализованное представление объекта, для удобства передачи.

HRESULT CoGetInstanceFromIStorage(
  [in, optional] COSERVERINFO *pServerInfo,
  [in, optional] CLSID        *pClsid,
  [in, optional] IUnknown     *punkOuter,
  [in]           DWORD        dwClsCtx,
  [in]           IStorage     *pstg,
  [in]           DWORD        dwCount,
  [in, out]      MULTI_QI     *pResults
);

Здесь видим уже известные нам pServerInfo, pResults, pClsid и так далее. pstg — IStorage , из которого нужно создать объект. Было обнаружено, что в IStorage успешно можно засунуть и OBJREF, что приведет к его демаршализации на стороне сервера.

В свою очередь, это ведет к резолву OXIDов и подключению по возвращаемым RPC String Binding. Аутентификация здесь будет в двух местах:

  • Initial OXID Resolution Request— при попытке резолва OXIDов клиент должен пройти аутентификацию на OXID Resolver'е. Отсюда у атакующего появляется возможность, если он затриггерит демаршаллинг OBJREF на стороне другого компьютера, захватить его креды;

  • Fake DCOM Server — этот способ уже на шаг дальше. Перехват аутентификации из Initial OXID Resolution Request несколько сложен, поэтому есть вариант легитимно зарегистрироваться в службе Endpoint Mapper, заставив её отдавать RPC String Binding на наш DCOM-сервер. Клиент придет на DCOM-сервер, а DCOM-сервер может потребовать аутентификацию. Отсюда и будут креды. Причем этот DCOM-сервер может потребовать пройти аутентификацию на любом SPN. Например, никто не помешает сказать клиенту, мол, если хочешь сходить ко мне, то будь добр предоставить мне TGS-билет на службу LDAP\dc01.office.local. Сейчас возможно запутаться между терминами клиент-сервер. В данном случае клиентом будет выступать тот компьютер, против которого была вызвана функция CoGetInstanseFromIStorage(), а сервером, соответственно, атакующий.

Давайте с рисунками. Атакующий вызывает функцию CoGetInstanceFromIStorage().

Вызов функции
Вызов функции

Так сделано в том числе и в RemoteKrbRelay. Инструмент можно запускать как с доменной машины, так и через runas /netonly. В качестве жертвы был выбран абстрактный компьютер с именем victim. Релеить будем на Target.

victim получает OBJREF, демаршализирует, видит OXID, пытается по OXID получить RPC String Binding.

Попытка получения RPC String Binding
Попытка получения RPC String Binding

Мы, так как являемся OXID Resolver'ом, просим пройти на нас аутентификацию. Служба всегда одна, тут строго RPCSS.

Требование об аутентификации
Требование об аутентификации

Клиент получает этот запрос, идет к KDC, получает TGS на нас и аутентифицируется.

Прохождение аутентификации на OXID Resolver'е
Прохождение аутентификации на OXID Resolver'е

Наш OXID Resolver возвращает RPC String Binding'и. Это был Initial OXID Resolution Request. Однако где здесь Relay-атака?

Хакер может отдать SPN в специальном формате, что приведет к некоторым ошибкам в его парсинге. Victim в таком случае получит TGS на Target, но отдаст этот TGS на Hacker.

Initial OXID Resolution Request Relay
Initial OXID Resolution Request Relay
Получение TGS на Target (Initial Oxid Resolution Request Attack)
Получение TGS на Target (Initial Oxid Resolution Request Attack)

После чего атакующий может провести AnySPN-атаку, так как поле SPN в TGS-билете не подписывается, и сходить на Target.

AnySPN -атака + Relay (Initial Oxid Resolution Request Attack)
AnySPN -атака + Relay (Initial Oxid Resolution Request Attack)
Получение доступа (Initial Oxid Resolution Request Attack)
Получение доступа (Initial Oxid Resolution Request Attack)

Подробнее про этот механизм можно почитать тут. Как видите, метод несколько сложен, поэтому зачастую применяется обынчый Fake DCOM Server.

Вернемся к нему. Жертве выдали RPC String Binding'и на наш DCOM Server. Она обращается по ним.

Обращение по RPC String Binding
Обращение по RPC String Binding

У DCOM-сервера есть возможность затребовать клиентскую аутентификацию, чем мы и пользуемся :) Причем, как я уже говорил, SPN может быть любой. Поэтому просим получить TGS на ldap/target.

Запрос аутентификации от лица DCOM-сервера
Запрос аутентификации от лица DCOM-сервера

Жертва получает эту просьбу, идет за TGS к KDC, после чего отдает его нам. А мы уже идем с тикетом на нужную службу.

Передача TGS-билета от victim к нам
Передача TGS-билета от victim к нам
Передача TGS-билета на целевую службу
Передача TGS-билета на целевую службу

И, так как TGS валидный, мы получаем доступ на службу от лица victim через релей керберос аутентификации.

Успешное получение доступа
Успешное получение доступа

Примерно так работает Creation-Based Method :)

Особенности

Есть некоторые особенности. Вы без проблем можете использовать ранее обнаруженные DCOM-объекты, но если интересуетесь поиском новых, то помните о некоторых подводных камнях.

Запуск от имени нужного пользователя

COM-сервера запускаются от имени конкретного пользователя, эта настройка называется RunAs. Глянуть можно через dcomcnfg.

Варианты запуска
Варианты запуска

Нам подходят значения:

  • The Interactive User — это пользователь, который в данный момент вошел в систему на компьютере, где запущен COM-сервер. Если на компьютер не зашел ни один пользователь, то СОМ-сервер не будет запущен. COM-объекты, настроенные следующим образом, можно использовать для абуза через кросс-сессионную активацию (параметр -session X в RemoteKrbRelay). Разбор подобного метода злоупотребления я рассматривал на medium;

  • The System Account — аккаунт системы, COM-сервер будет запущен от лица NT AUTHORITY\SYSTEM ;

  • This User — пользователь, чьи учетные данные предоставлены. Ни разу не видел такую настройку.

Не подходит только одно значение — The Launching User. Это учетная запись пользователя, от лица которого осуществляется обращение к COM-серверу.

Права на запуск, инстанцирование и использование

Существует набор прав, которые определяют, кто может обращаться к функционалу того или иного COM-класса. Подразделяются на Local и Remote права. Local — обращение с текущего устройства, Remote — удаленно.

Каждый тип делится на три категории:

  • Launch — право на запуск COM-сервера;

  • Activation — право на создание новых экземпляров;

  • Access — право получения доступа к уже работающему объекту. Зачастую дублирует Activation.

Launch и Activation права
Launch и Activation права
Access права
Access права

Соответственно, для удаленного вызова требуются RemoteLaunch, RemoteActivation, RemoteAccess-права. Для локального LPE достаточно всего того же, но с префиксом Local. Иногда даже LocalLaunch не требуется, если COM-сервер с автозапуском.

Однако поверх этих прав стоят дефолтные права. Так скажем, имеют больший приоритет, переопределяют. Их можно глянуть, например, через OleViewDotnet.

Просмотр дефолтных прав
Просмотр дефолтных прав

Помио того, существуют еще SecurityLimits (на msdn фигурируют как Launch Restrictions), их тоже стоит учитывать.

Просмотр SecurityLimits
Просмотр SecurityLimits

Лежат эти настройки внутри HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Ole .

Уровень имперсонации и аутентификации

DCOM очень тесно связан с уровнями имперсонации и аутентификации. Зачастую эти параметры остаются без переопределения. И используются дефолтные. Стандартное значение уровня имперсонации: RPC_C_IMP_LEVEL_IDENTIFY. Уровня аутентификации — RPC_C_AUTHN_LEVEL_CONNECT . Уровень имперсонации контролирует, какой доступ может получить сервер, при имперсонации клиента, а уровень аутентификации позволяет настроить безопасность соединения: подпись, шифрование.

Стандартные уровни имперсонации и аутентификации
Стандартные уровни имперсонации и аутентификации

Уровни имперсонации и аутентификации плотно связаны друг с другом. В зависимости от настроек определяется и целевой сервис, на который мы можем релеить. Существует отличная табличка, которая наглядно демонстрирует связь между этими двумя параметрами.

Target of Relay Attack

Authentication Level

Impersonation Level

LDAP/LDAPS

RPC_C_AUTHN_LEVEL_CONNECT

RPC_C_IMP_LEVEL_IDENTIFY

SMB

>= RPC_C_AUTHN_LEVEL_CONNECT

RPC_C_IMP_LEVEL_IMPERSONATE

ADCS HTTP(S)

>= RPC_C_AUTHN_LEVEL_CONNECT

RPC_C_IMP_LEVEL_IMPERSONATE

ADCS MS-ICPR

>= RPC_C_AUTHN_LEVEL_CONNECT

RPC_C_IMP_LEVEL_IMPERSONATE

Однако это свидетельствует о том, что мы по дефолту можем релеить только в LDAP. Не стоит отчаиваться потому, что уровень имперсонации может быть переопределен несколькими способами:

  • Через вызов функции CoInitializeSecurity();

  • Через реестр;

  • В резолве оксидов.

Переопределение через реестр выглядит следующим образом:

Переопределение уровня имперсонации
Переопределение уровня имперсонации

В случае CoInitializeSecurity() сам DCOM-сервер может поставить нужный dwAuthnLevel . Но самое крутое — при резолве оксидов принудительно стоит RPC_C_IMP_LEVEL_IMPERSONATE .

Джеймс Фуршоу отреверсил и поделился находкой
Джеймс Фуршоу отреверсил и поделился находкой

Таким образом, релей во время резолва оксидов более предпочтителен, хоть и чуть более сложный. Зато у нас появляется больший уровень имперсонации.

Поиск потенциальных объектов

Для поиска потенциальных объектов был разработан инструмент Checkerv2.0. Почему 2.0? В репозитории лежит еще и первая версия, но она немного кривовата, а удалять жалко.... Так вот, инструмент позволяет автоматически распарсить все существующие на системе COM-объекты и вывести информацию о правах на них. Экспорт в csv и xlsx-формате, что упрощает поиск потенциальных интересных уязвимых таргетов.

Вывод инструмента
Вывод инструмента

Многие безопасники строят свой процесс исследования на автоматизации, что ж... Тогда обратите внимание на OleViewDotnet, доступный в том числе в виде Powershell-модуля. Использовать проще простого.

Import-Module .\OleViewDotNet.psd1
Get-ComProcess -DbgHelpPath 'C:\dbghelp.dll'

Затем уже магией where $_.X фильтруете данные в попытке обнаружить уязвимые объекты.

Использование OleViewDotnet
Использование OleViewDotnet

Злоупотребление через Fake DCOM Server

Если рассматривать с технической точки зрения, то 80% эксплойтов используют именно вредоносный DCOM-сервер. В случае заинтересованности в релее кербероса используется специальная структура SOLE_AUTHENTICATION_SERVICE, в которой указывается SPN целевой службы, на которую хотим получить TGS-тикет.

var svcs = new SOLE_AUTHENTICATION_SERVICE[] {    
  new SOLE_AUTHENTICATION_SERVICE() {      
    dwAuthnSvc = RpcAuthenticationType.Kerberos,     
    pPrincipalName = "HOST/DC.domain.com"    
    }
};
var str = SetProcessModuleName("System");
try
{   CoInitializeSecurity(IntPtr.Zero, svcs.Length, svcs,  
                         IntPtr.Zero, AuthnLevel.RPC_C_AUTHN_LEVEL_DEFAULT,        
                         ImpLevel.RPC_C_IMP_LEVEL_IMPERSONATE, IntPtr.Zero,        
                         EOLE_AUTHENTICATION_CAPABILITIES.EOAC_DYNAMIC_CLOAKING,       
                         IntPtr.Zero);
}
finally
{    SetProcessModuleName(str);
}

Несложно догадаться, что через этот код получится достать тикет на HOST\DC.domain.com. В эксплойте используется тут.

Альтернативный вариант — поймать NetNTLM-аутентификацию. Тогда будет достаточно вызова функции RpcServerRegisterAuthInfo() с передачей в качестве параметра AuthnSvc значения RPC_C_AUTHN_WINNT. Используют тут.

Для контроля процесса аутентификации и перехвата учетных данных следует добавить простой человеческий хук-хрюк. В любой ехешник, так или иначе связанный с безопасностью, RPC и всем этим ужасом, подгружаются специальные RPC Security Packages. Подобно стандартным пакетам безопасности (которые я обозревал тут, они обеспечивают механизмы построения контекста, аутентификации, шифрования и подписи.

Чтобы поймать передаваемые креды нужно всего лишь поставить собственный обработчик поверх функции AcceptSecurityContext(). В эксплойте делаем это здесь. Функция-обработчик тут.

Код для установки хука
Код для установки хука

Ок, получать аутентификацию научились, но как отдать RPC String Binding? Здесь два варианта: поднять собственный OXID Resolver, либо использовать концепцию локального порта (легитимно зарегистрироваться в службе Endpoint Mapper).

В случае первого варианта будут появляться некоторые сложности. Windows не будет ходить к тем OXID Resolver'ам, которые висят не на 135 порту — одна из митигаций от эксплойта RottenPotato. Напомню принцип его работы.

Принцип работы RottenPotato
Принцип работы RottenPotato

Инструмент триггерил демаршаллинг OBJREF-объекта, причем использовался CLSID COM-сервера, который запускается от лица системы. В качестве IP-адреса OXID Resolver'а стоял локальный хост, порт 6666. На этот самый порт прилетала аутентификация (помните? на OXID Resolver'е тоже аутентифицируются), ну и осуществлялась ретрансляция аутентификации, выстраивался контекст и поднималась консолька от лица системы.

Вышел патч, но когда это останавливало хакеров? Появился RemotePotato0. Для обхода митигации фейковый оксид резолвер также поднимался на левом (отличном от 135) порту, однако на него настраивалась ретрансляция со 135 порта сторонней машинки, например, нашей Kali. Внутрь OBJREF помещали не айпишник локалхоста, а айпишник Kali, Windows смело шел на Kali:135, а оттуда каким-нибудь socat'ом все запросы смело пробрасывались обратно на атакуемую тачку, уже на любой, подходящий для нас порт.

Пример эксплуатации. Спасибо за скрин :)
Пример эксплуатации. Спасибо @snovvcrashза скрин :)

В контексте концепции локального порта все может казаться проще. Впрочем, и тут есть детали:

  1. Брандмауэр. Вылезет сразу же, как только у нашей программы появится сетевая активность;

  2. Занятые порты.

Первая проблема достаточно распространенная. Наша программка начнет коммуницировать по сети и опа, Windows Security Alert.

Windows Security Alert
Windows Security Alert

Одним из вариантов обхода можно попробовать мимикрировать под легитимные приложения, которые уже разрешены в брандмауэере. Как понять, какие программы разрешены? В таком случае попробуйте обратиться к COM-объекту HNetCfg.FwMgr с его методом IsPortAllowed() для проверки разрешенности порта конкретному приложению. Для целей демонстрации сделан скриптец, он просто осуществляет поиск того, какой исполняемый файл внутри C:\Windows\System32 имеет право биндиться на 12345 порту:

function Test-IsPortOpen {
    param(
        [string]$Name,
        [int]$Port
    )
    $mgr = New-Object -ComObject "HNetCfg.FwMgr"
    $allow = $null
    $mgr.IsPortAllowed($Name, 2, $Port, "", 6, [ref]$allow, $null)
    $allow
}

foreach($f in $(ls "$env:WINDIR\system32\*.exe")) {    
    if (Test-IsPortOpen $f.FullName 12345) {
        Write-Host $f.Fullname
    }
}

После подбора разрешенного порта и имени файла (по дефолту это System / svchost.exe) происходит привязка по этим данным нашего DCOM-сервера в службе Endpoint Mapper с помощью функции RpcServerUseProtseqEp(). Чтобы казаться легитимным приложением делается обычный Module Name Spoofing. Код с регистрацией тут, со спуфингом здесь.

POCи

С вариантом с поднятием фейкового DCOM сервера есть несколько POCов:

Во всех этих инструментах учетные данные извлекаются путем хука на AcceptSecurityContext().

Пример восстановления токена из JuicyPotatoNG
Пример восстановления токена из JuicyPotatoNG

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

Initial OXID Resolution Request

В случае, если мы не хотим возиться с поднятием фейкового DCOM-сервера и сразу хотим аутентификацию хватать на OXID Resolver'е, то нам не нужно думать о миллионе вещей! Если быть честными, то даже OXID Resolver поднимать необязательно — отправляй в сокет нужные битики и тебе прилетит аутентификация. Подобного метода придерживаются почти все работающие картошки:

  • RoguePotato — работает с socat, имперсонирует УЗ через RpcImpersonateClient();

  • RemotePotato0 — работает в связке с socat;

  • ADCSCoercePotato — также связка с socat;

  • RottenPotato — раньше работал исправно, но ввели патч с отказом ходить не на 135 порт, поэтому POC пал смертью храбых патчей;

  • JuicyPotato — не работает, потому что не 135 порт...

Например, вот так выглядит перехват учетных данных с последующим релеем в ADCSCoercePotato. Фактически, идет парсинг RAW TCP-пакетов по сигнатурам NTLM-аутентификации.

Как сделан перехват аутентификации из Initial OXID Resolution Request в ADCSCoercePotato
Как сделан перехват аутентификации из Initial OXID Resolution Request в ADCSCoercePotato

Что общего между этими эксплойтами? Тут NTLM. А статья про что? Про Kerberos. Для релея Kerberos из Initial OXID Resolution Request есть два POCа:

  • potato.py — частичная реализация. Здесь только триггер аутентификации через CoGetInstanceFromIStorage(). С вас поднятие OXID Resolver'а, на котором будем ловить креды. Пример кода искать тут;

    Пример использования potato.py
    Пример использования potato.py

Буквально в момент создания статьи вышел еще один, рабочий! KrbRelay-SMBServer. Этот POC на шаг выше potato.py. Он не просто перехватывает аутентификацию во время OXID резолва, он проецирует эту багу на стандартные методы триггера NTLM-аутентификации. Как это работает?

  1. У нас есть набор методов для триггера NetNTLM-аутентификации: PrinterBug, PetitPotam, EFSCoerce и тп;

  2. Вместо IP-адреса указывается строка в специальном формате;

adcs-mylab1UWhRCAAAAAAAAAAAAAAAAAAAAAAAAAAAAwbEAYBAAAA
  1. Парсинг этой строки, из-за особенностей поведения функции CredMarshalTargetInfo() пакета аутентификации Kerberos, приводит к тому, что идет получение TGS-билета на adcs-mylab, однако TGS передается по этому длинному DNS-имени;

  2. Мы регистрируем это длинное DNS-имя;

  3. Триггерим аутентификацию;

  4. Получаем TGS и релеим его в AP-REQ;

  5. Success

Использование KrbRelay-SMBServer
Использование KrbRelay-SMBServer

Иными словами, инструмент KrbRelay-SMBServer начал абузить непосредственно механизм парсинга SPN внутри функции CredMarshalTargetInfo(). Баг подсвечивал Джеймс Фуршоу в одной из статей. Это позволяет нам, как атакующим, сделать триггер на хост DCSPB1UWhRCAAAAAAAAAAAAAAAAAAAAAAAAAAwbEAYBAAAAA, пакет аутентификации получит это значение, обрежет 1UWhRCAAAAAAAAAAAAAAAAAAAAAAAAAAwbEAYBAAAAA, получит тикет на DCSPB и придет с этим тикетом на DCSPB1UWhRCAAAAAAAAAAAAAAAAAAAAAAAAAAwbEAYBAAAAA. С нас, как атакующих нужно два действия:
а) зарегистрировать DNS-запись DCSPB1UWhRCAAAAAAAAAAAAAAAAAAAAAAAAAAwbEAYBAAAAA, которая будет указывать на нашу машинку
б) поднять на нашей машинке слушателя, который поймает TGS-билет и передаст его в AP-REQ, осуществив ретрансляцию.

С добавлением DNS-записи могут возникнуть сложности, но не нужно отчаиваться, ведь DNS-запись можно попробовать добавить через KrbJack (это когда выставлен флаг ZONE_UPDATE_UNSECURE).

Что там по защитам

Неофициально MS ввел патчи от Kerberos Relay осенью 2022 года. Патчи заключались в принудительной установке значения RPC_C_AUTHN_LEVEL_PKT_INTEGRITY на COM-объекты, которые абузили атакующие.

Пример патча
Пример патча

Это на корню режет атаку, так как будет принудительно использоваться подпись, что предотвращает релей атаку. Однако сильного действия патч не возымел. Атакующие продолжали находить новые CLSIDы для абуза, а MS вновь втихую их патчил.

Затем появился анонс — отныне принудительный DCOM Hardening, стандартный Authentication level установлен в RPC_C_AUTHN_LEVEL_PKT_INTEGRITY. Я решил проверить. Скачал Windows 11, поставил все ласт апдейты, зашел в DCOMCNFG и.... ничего. Значения старые. То ли я, то ли лыжи....

Пример настроек на Windows 11
Пример настроек на Windows 11

Заключение

Примерно так работает Kerberos Relay атака. Опять же, атакующие продолжают находить недокументированные особенности системы, пишут эксплойты, а MS вновь все патчит. Однако далеко не все патчи действительно предотвращают атаку на корню, ведь являются «временным пластырем», который отвалится через пару недель плотного ресерча.

Спасибо :)

Комментарии (5)


  1. sigprof
    07.10.2024 19:55

    This User — пользователь, чьи учетные данные предоставлены. Ни разу не видел такую настройку.

    А я видел — есть костыльный способ заставить COM-объекты MS Office работать в случае, когда к ним обращаются из задания планировщика, при этом ни одного активного сеанса пользователя на машине нет.


  1. exchange12rocks
    07.10.2024 19:55

    Скачал Windows 11, поставил все ласт апдейты, зашел в DCOMCNFG и.... ничего. Значения старые.

    Насколько я понимаю, теперь просто все запросы принудительно должны соответствовать уровню RPC_C_AUTHN_LEVEL_PKT_INTEGRITY или выше. Даже если в том окне стоит уровень ниже, влиять это не будет.


  1. sshikov
    07.10.2024 19:55

    Я правильно понял, что эта уязвимость - Windows only, так как фактически для эксплуатации используется DCOM?


    1. MichelleVermishelle Автор
      07.10.2024 19:55

      Ага, только для Windows


      1. sshikov
        07.10.2024 19:55

        Тогда название хорошо бы немного уточнить. Керберос не сводится к AD и Windows. И получается что в нем самом (в виде FreeIPA скажем) такой уязвимости нет.

        Впрочем, там написано "Разработка под Windows"...