
Una Vulnerabilidad Crítica de OLE en Windows sin Clics. Esta es una prueba de concepto para CVE-2025-21298 - Vulnerabilidad de Ejecución Remota de Código en OLE de Windows (CVSS 9.8). Este es un PoC de corrupción de memoria.
Esta es una prueba de concepto para CVE-2025-21298 - Vulnerabilidad de ejecución remota de código en Windows OLE (CVSS 9.8). Se trata de una PoC de corrupción de memoria, no de un exploit.
La vulnerabilidad se encuentra en ole32.dll!UtOlePresStmToContentsStm. El propósito de la función es convertir datos en un flujo "OlePres" dentro de un almacenamiento OLE en datos formateados adecuadamente e insertarlos en el flujo "CONTENTS" del mismo almacenamiento. Recibe un puntero IStorage a un objeto de almacenamiento y tres argumentos más bien irrelevantes.
A continuación podemos ver la implementación de la función con un diff del parche de enero de 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;
// Crea un flujo "CONTENTS" en el almacenamiento y lo almacena en pstmContents
if ( (lpVtbl->CreateStream)(pstg, L"CONTENTS", 18LL, 0LL, 0, &pstmContents) )
return 0LL;
// Libera inmediatamente pstmContents, no lo vamos a usar ahora
(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);
// Libera pstmContents si aún existe, necesitamos limpiar
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;
}
El problema está en la variable pstmContents. Inicialmente se usa para almacenar el puntero al objeto de flujo "CONTENTS" que se crea al inicio de la función. El flujo se destruye inmediatamente después de ser creado y el puntero almacenado en pstmContents se libera (lo que lo libera en coml2.dll!ExposedStream::~ExposedStream). Sin embargo, la variable todavía contiene el puntero liberado. Más adelante en la función, la variable puede reutilizarse para almacenar nuevamente el puntero al flujo "CONTENTS"; debido a esto, hay código de limpieza al final de la función que libera el puntero en caso de que esté almacenado en la variable. El código no contempla que UtReadOlePresStmHeader pueda fallar; si eso sucede, pstmContents seguirá apuntando al puntero liberado y caeremos en el código de limpieza, que liberará el puntero nuevamente. Por lo tanto, se produce una situación de doble liberación.
Como se puede ver en el diff del parche, Microsoft solucionó el problema estableciendo pstmContents a cero después de que se libera el puntero que contiene inicialmente.
En el repositorio hay un archivo rtf que reproduce la vulnerabilidad. Lo probé abriendo el archivo en MS Word, pero también puedes probarlo con otras aplicaciones que analicen datos RTF (por ejemplo, Outlook). Es posible que se pueda explotar a través de otros formatos que incrusten objetos OLE, no lo he probado.
Video: