
Prueba de concepto y detalles para CVE-2025-21298
Esto es una prueba de concepto para CVE-2025-21298 - Vulnerabilidad de ejecución remota de código en Windows OLE (CVSS 9.8). Esto es un PoC de corrupción de memoria, no un exploit.
Diff completo del parche mediante ghidriff: ENLACE
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 con el formato apropiado e insertarlos en el flujo "CONTENTS" en el mismo almacenamiento. Recibe un puntero IStorage a un objeto de almacenamiento y tres argumentos más bien poco importantes.
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;
// 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;
}
El problema está en la variable pstmContents. Inicialmente se usa para almacenar el puntero al objeto de flujo "CONTENTS" que se crea al principio de la función. El flujo se destruye inmediatamente después de crearse y el puntero almacenado en pstmContents se libera (lo que lo libera en coml2.dll!ExposedStream::~ExposedStream). Sin embargo, la variable sigue conteniendo el puntero liberado. Más adelante en la función, la variable puede reutilizarse para almacenar de nuevo 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 tiene en cuenta que UtReadOlePresStmHeader puede fallar - si eso ocurre, pstmContents seguirá apuntando al puntero liberado y caeremos en el código de limpieza, que liberará el puntero de nuevo. Como tal, se producirá 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 el puntero que contiene inicialmente sea liberado.
En el repositorio hay un archivo rtf que reproduce la vulnerabilidad. Probé abriendo el archivo en MS Word, pero también puedes probarlo con otras aplicaciones que analicen datos RTF (p. ej. Outlook). Es posible que se pueda explotar mediante otros formatos que incrustan objetos OLE, no lo he probado.
Video:
Publicaré los detalles más adelante.