
Une vulnérabilité critique Windows OLE Zero-Click. Ceci est une preuve de concept pour CVE-2025-21298 - Vulnérabilité d'exécution de code à distance Windows OLE (CVSS 9.8). Ceci est un PoC de corruption de mémoire.
Ceci est une preuve de concept pour CVE-2025-21298 - Vulnérabilité d'exécution de code à distance OLE Windows (CVSS 9.8). Il s'agit d'un PoC de corruption mémoire, pas d'un exploit.
La vulnérabilité se situe dans ole32.dll!UtOlePresStmToContentsStm. Le but de la fonction est de convertir les données d'un flux "OlePres" dans un stockage OLE en données correctement formatées et de les insérer dans le flux "CONTENTS" du même stockage. Elle reçoit un pointeur IStorage vers un objet de stockage et trois arguments plutôt secondaires.
Ci-dessous, nous pouvons voir l'implémentation de la fonction avec une différence par rapport au correctif de janvier 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;
}
Le problème se trouve dans la variable pstmContents. Initialement, elle est utilisée pour stocker le pointeur vers l'objet de flux "CONTENTS" créé au début de la fonction. Le flux est immédiatement détruit après sa création et le pointeur stocké dans pstmContents est libéré (ce qui le libère dans coml2.dll!ExposedStream::~ExposedStream). Cependant, la variable contient toujours le pointeur libéré. Plus loin dans la fonction, la variable peut être réutilisée pour stocker à nouveau le pointeur vers le flux "CONTENTS" — à cause de cela, il y a du code de nettoyage à la fin de la fonction qui libère le pointeur au cas où il serait stocké dans la variable. Le code ne prend pas en compte le fait que UtReadOlePresStmHeader peut échouer — si cela se produit, pstmContents pointera toujours vers le pointeur libéré et nous tomberons dans le code de nettoyage, qui libérera à nouveau le pointeur. Il en résulte donc une situation de double libération.
Comme on peut le voir dans le diff du correctif, Microsoft a résolu le problème en mettant pstmContents à zéro après la libération initiale du pointeur qu'il contient.
Dans le dépôt se trouve un fichier rtf qui reproduit la vulnérabilité. J'ai testé en ouvrant le fichier dans MS Word mais vous pouvez aussi le tester avec d'autres applications qui analysent les données RTF (par exemple Outlook). L'exploitation via d'autres formats qui intègrent des objets OLE peut être possible, je n'ai pas essayé.
Vidéo :