
A Critical Windows OLE Zero-Click Vulnerability. This is a proof-of-concept for CVE-2025-21298 - Windows OLE Remote Code Execution Vulnerability (CVSS 9.8). This is a memory corruption PoC
이것은 CVE-2025-21298 - Windows OLE 원격 코드 실행 취약점 (CVSS 9.8)에 대한 개념 증명(PoC)입니다. 이는 메모리 손상 PoC이며, 익스플로잇이 아닙니다.
취약점은 ole32.dll!UtOlePresStmToContentsStm에 있습니다. 함수의 목적은 OLE 저장소 내의 'OlePres' 스트림에 있는 데이터를 적절한 형식의 데이터로 변환하여 동일한 저장소의 'CONTENTS' 스트림에 삽입하는 것입니다. 이 함수는 저장소 객체에 대한 IStorage 포인터와 세 개의 비교적 중요하지 않은 인수를 받습니다.
아래에서 2025년 1월 패치의 diff와 함께 함수의 구현을 확인할 수 있습니다:
__int64 __fastcall UtOlePresStmToContentsStm(IStorage *pstg, wchar_t *puiStatus, __int64 a3, unsigned int *lpszPresStm)
{
struct IStorageVtbl *lpVtbl; // rax
int v7; // r14d
+ bool IsEnabled; // al
IStream *v10; // rcx
bool v11; // zf
struct IStorageVtbl *v12; // rax
int v13; // ebx
HRESULT v14; // eax
const wchar_t *v15; // rdx
IStream *pstmContents; // [rsp+40h] [rbp-19h] BYREF
IStream *pstmOlePres; // [rsp+48h] [rbp-11h] BYREF
tagFORMATETC foretc; // [rsp+50h] [rbp-9h] BYREF
tagHDIBFILEHDR hdfh; // [rsp+70h] [rbp+17h] BYREF
*lpszPresStm = 0;
lpVtbl = pstg->lpVtbl;
pstmContents = 0LL;
v7 = 1;
// Create a "CONTENTS" stream in the storage and store it into pstmContents
if ( (lpVtbl->CreateStream)(pstg, L"CONTENTS", 18LL, 0LL, 0, &pstmContents) )
return 0LL;
// Immediately release pstmContents, we're not going to be using it right now
(pstmContents->lpVtbl->Release)(pstmContents);
+ IsEnabled = wil::details::FeatureImpl<__WilFeatureTraits_Feature_3047977275>::__private_IsEnabled(&`wil::Feature<__WilFeatureTraits_Feature_3047977275>::GetImpl'::`2'::impl);
+ v10 = pstmContents;
+ v11 = !IsEnabled;
v12 = pstg->lpVtbl;
+ if ( !v11 )
+ v10 = 0LL;
+ pstmContents = v10;
(v12->DestroyElement)(pstg, L"CONTENTS");
v13 = (pstg->lpVtbl->OpenStream)(pstg, &OlePres, 0LL, 16LL, 0, &pstmOlePres);// 2nd option to fail -> no OlePres stream
if ( v13 )
{
*lpszPresStm |= 1u;
if ( (pstg->lpVtbl->OpenStream)(pstg, L"CONTENTS", 0LL, 16LL, 0, &pstmContents) )
{
*lpszPresStm |= 2u;
}
else
{
(pstmContents->lpVtbl->Release)(pstmContents);
+ wil::details::FeatureImpl<__WilFeatureTraits_Feature_3047977275>::__private_IsEnabled(&`wil::Feature<__WilFeatureTraits_Feature_3047977275>::GetImpl'::`2'::impl);
}
return v13;
}
foretc.ptd = 0LL;
v13 = UtReadOlePresStmHeader(pstmOlePres, &foretc, 0LL, 0LL);
if ( v13 >= 0 )
{
v13 = (pstmOlePres->lpVtbl->Read)(pstmOlePres, &hdfh, 16LL);
if ( v13 >= 0 )
{
v13 = OpenOrCreateStream(pstg, L"CONTENTS", &pstmContents);
if ( v13 < 0 )
{
*lpszPresStm |= 2u;
goto $errRtn_197;
}
if ( foretc.dwAspect == 4 )
{
*lpszPresStm |= 4u;
v7 = 0;
v13 = 0;
goto $errRtn_197;
}
if ( foretc.cfFormat == 8 )
{
v14 = UtDIBStmToDIBFileStm(pstmOlePres, hdfh.dwSize, pstmContents);
LABEL_19:
v13 = v14;
goto $errRtn_197;
}
if ( foretc.cfFormat == 3 )
{
v14 = UtMFStmToPlaceableMFStm(pstmOlePres, hdfh.dwSize, hdfh.dwWidth, hdfh.dwHeight, pstmContents);
goto LABEL_19;
}
v13 = -2147221398;
}
}
$errRtn_197:
if ( pstmOlePres )
(pstmOlePres->lpVtbl->Release)(pstmOlePres);
// Release pstmContents if it still exists, we need to clean up
if ( pstmContents )
(pstmContents->lpVtbl->Release)(pstmContents);
if ( foretc.ptd )
CoTaskMemFree(foretc.ptd);
if ( v13 )
{
v15 = L"CONTENTS";
goto LABEL_31;
}
if ( v7 )
{
v15 = &OlePres;
LABEL_31:
(pstg->lpVtbl->DestroyElement)(pstg, v15);
}
return v13;
}
문제는 pstmContents 변수에 있습니다. 처음에는 함수 시작 시 생성된 'CONTENTS' 스트림 객체에 대한 포인터를 저장하는 데 사용됩니다. 스트림은 생성된 직후에 파괴되고 pstmContents에 저장된 포인터는 해제됩니다(coml2.dll!ExposedStream::~ExposedStream에서 해제됨). 그러나 변수는 여전히 해제된 포인터를 포함하고 있습니다.
함수 아래쪽에서 변수가 다시 'CONTENTS' 스트림에 대한 포인터를 저장하는 데 재사용될 수 있습니다. 이로 인해 함수 끝에 변수에 저장된 경우 포인터를 해제하는 정리 코드가 있습니다. 코드는 UtReadOlePresStmHeader가 실패할 수 있다는 사실을 고려하지 않습니다. 그런 경우 pstmContents는 여전히 해제된 포인터를 가리키고 정리 코드로 넘어가 포인터를 다시 해제하게 됩니다. 따라서 이중 해제(double-free) 상황이 발생합니다.
패치 diff에서 볼 수 있듯이, Microsoft는 초기에 포함된 포인터가 해제된 후 pstmContents를 0으로 설정하여 문제를 해결했습니다.
저장소에는 취약점을 재현하는 rtf 파일이 있습니다. MS Word에서 파일을 열어 테스트했지만, RTF 데이터를 파싱하는 다른 응용 프로그램(예: Outlook)에서도 테스트할 수 있습니다. OLE 객체를 포함하는 다른 형식을 통한 익스플로잇도 가능할 수 있지만, 시도해보지 않았습니다.
동영상: