
Preuve de concept et détails pour CVE-2025-21298
Il s'agit d'une preuve de concept pour CVE-2025-21298 - Vulnérabilité d'exécution de code à distance dans Windows OLE (CVSS 9.8). C'est une preuve de concept de corruption mémoire, pas un exploit.
Diff complet du correctif via ghidriff : LIEN
La vulnérabilité se situe dans ole32.dll!UtOlePresStmToContentsStm. Cette fonction a pour but 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 sans importance.
Ci-dessous, nous pouvons voir l'implémentation de la fonction avec un diff du 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 situe 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 » – c'est pourquoi il existe un code de nettoyage à la fin de la fonction qui libère le pointeur s'il est stocké dans la variable. Le code ne tient pas compte du fait que UtReadOlePresStmHeader peut échouer – si cela se produit, pstmContents pointera toujours vers le pointeur libéré et nous tomberons sur le code de nettoyage, qui libérera à nouveau le pointeur. Cela conduit à une situation de double libération (double-free).
Comme on peut le voir dans le diff du correctif, Microsoft a corrigé le problème en mettant pstmContents à zéro après que le pointeur qu'il contient initialement est libéré.
Dans le dépôt se trouve un fichier rtf qui reproduit la vulnérabilité. Je l'ai testé en ouvrant le fichier dans MS Word, mais vous pouvez également le tester avec d'autres applications qui analysent les données RTF (par exemple Outlook). L'exploitation via d'autres formats intégrant des objets OLE est peut-être possible, je n'ai pas essayé.
Vidéo :
Je publierai les détails plus tard.