
Una vulnerabilità critica zero-click in Windows OLE. Questa è una proof-of-concept per CVE-2025-21298 - Vulnerabilità di esecuzione remota di codice in Windows OLE (CVSS 9.8). Questo è un PoC di corruzione della memoria.
Questa è una proof-of-concept per CVE-2025-21298 - Windows OLE Remote Code Execution Vulnerability (CVSS 9.8). Si tratta di una PoC di corruzione della memoria, non di un exploit.
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" nello 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 si trova 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 libera in coml2.dll!ExposedStream::~ExposedStream). Tuttavia, la variabile contiene ancora il puntatore deallocato. Più avanti nella funzione, la variabile può essere riutilizzata per memorizzare nuovamente il puntatore al flusso "CONTENTS" - per questo motivo, alla fine della funzione c'è un codice di pulizia che rilascia il puntatore nel caso in cui sia ancora memorizzato nella variabile. Il codice non tiene conto del fatto che UtReadOlePresStmHeader potrebbe fallire: in tal caso, pstmContents punterà ancora al puntatore deallocato e si arriverà al codice di pulizia, che rilascerà di nuovo il puntatore. Si verifica quindi 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 inizialmente contenuto è stato rilasciato.
Nel repository è presente un file rtf che riproduce la vulnerabilità. L'ho testato aprendo il file in MS Word, ma puoi provarlo anche con altre applicazioni che analizzano dati RTF (ad es. Outlook). Lo sfruttamento tramite altri formati che incorporano oggetti OLE potrebbe essere possibile, ma non l'ho provato.
Video: