
Proof of Concept & Details für CVE-2025-21298
Dies ist ein Proof-of-Concept für CVE-2025-21298 - Windows OLE Remote Code Execution Vulnerability (CVSS 9.8). Es handelt sich um einen Memory-Corruption-PoC, nicht um einen Exploit.
Vollständiger Patch-Diff via ghidriff: LINK
Die Schwachstelle befindet sich in ole32.dll!UtOlePresStmToContentsStm. Der Zweck der Funktion ist es, Daten in einem "OlePres"-Stream innerhalb eines OLE-Speichers in angemessen formatierte Daten umzuwandeln und sie in den "CONTENTS"-Stream im selben Speicher einzufügen. Sie erhält einen IStorage-Pointer auf ein Speicherobjekt und drei eher unwichtige Argumente.
Unten sehen wir die Implementierung der Funktion mit einem Diff vom Januar 2025 Patch:
__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;
}
Das Problem liegt in der Variable pstmContents. Zunächst wird sie verwendet, um den Pointer auf das "CONTENTS"-Stream-Objekt zu speichern, das zu Beginn der Funktion erstellt wird. Der Stream wird unmittelbar nach der Erstellung zerstört und der in pstmContents gespeicherte Pointer wird freigegeben (was ihn in coml2.dll!ExposedStream::~ExposedStream freigibt). Allerdings enthält die Variable immer noch den freigegebenen Pointer. Weiter unten in der Funktion kann die Variable erneut verwendet werden, um den Pointer auf den "CONTENTS"-Stream zu speichern – aus diesem Grund gibt es am Ende der Funktion einen Aufräumcode, der den Pointer freigibt, falls er in der Variable gespeichert ist. Der Code berücksichtigt nicht die Tatsache, dass UtReadOlePresStmHeader fehlschlagen kann – wenn das passiert, zeigt pstmContents immer noch auf den freigegebenen Pointer und wir gelangen in den Aufräumcode, der den Pointer erneut freigibt. Somit kommt es zu einer Double-Free-Situation.
Wie im Patch-Diff zu sehen ist, hat Microsoft das Problem behoben, indem pstmContents auf Null gesetzt wird, nachdem der darin enthaltene Pointer initial freigegeben wurde.
Im Repository befindet sich eine RTF-Datei, die die Schwachstelle reproduziert. Ich habe es getestet, indem ich die Datei in MS Word geöffnet habe, aber Sie können es auch mit anderen Anwendungen testen, die RTF-Daten parsen (z.B. Outlook). Eine Ausnutzung durch andere Formate, die OLE-Objekte einbetten, ist möglicherweise möglich, habe ich nicht getestet.
Video:
Details werde ich später veröffentlichen.