
Prova di concetto e dettagli per CVE-2025-21298
Questa è una proof-of-concept per CVE-2025-21298 - Vulnerabilità di esecuzione remota di codice in Windows OLE (CVSS 9.8). Questa è una PoC di corruzione della memoria, non un exploit.
Diff completo della patch tramite ghidriff: LINK
La vulnerabilità si trova in ole32.dll!UtOlePresStmToContentsStm. Lo scopo della funzione è convertire i dati in un flusso "OlePres" all'interno di un archivio OLE in dati formattati opportunamente e inserirli nel flusso "CONTENTS" dello stesso archivio. Riceve un puntatore IStorage a un oggetto di archiviazione e tre argomenti piuttosto irrilevanti.
Di seguito possiamo vedere l'implementazione della funzione con un diff della patch di gennaio 2025:
__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;
}
Il problema sta nella variabile pstmContents. Inizialmente viene usata per memorizzare il puntatore all'oggetto flusso "CONTENTS" creato all'inizio della funzione. Il flusso viene immediatamente distrutto dopo essere stato creato e il puntatore memorizzato in pstmContents viene rilasciato (il che lo dealloca in coml2.dll!ExposedStream::~ExposedStream). Tuttavia, la variabile contiene ancora il puntatore deallocato. Più avanti nella funzione, la variabile può essere riutilizzata per memorizzare di nuovo il puntatore al flusso "CONTENTS" - per questo motivo, alla fine della funzione c'è un codice di pulizia che rilascia il puntatore nel caso sia memorizzato nella variabile. Il codice non tiene conto del fatto che UtReadOlePresStmHeader potrebbe fallire - se ciò accade, pstmContents punterà ancora al puntatore deallocato e si arriverà al codice di pulizia, che rilascerà di nuovo il puntatore. Di conseguenza, si verifica una situazione di double-free.
Come si può vedere nel diff della patch, Microsoft ha risolto il problema impostando pstmContents a zero dopo che il puntatore che contiene inizialmente viene rilasciato.
Nel repository è presente un file rtf che riproduce la vulnerabilità. Ho testato aprendo il file in MS Word, ma puoi testarlo anche con altre applicazioni che analizzano dati RTF (ad esempio outlook). Lo sfruttamento tramite altri formati che incorporano oggetti OLE potrebbe essere possibile; non ci ho provato.
Video:
Pubblicherò i dettagli più avanti.