这是 CVE-2025-21298 - Windows OLE 远程代码执行漏洞 (CVSS 9.8) 的概念验证。这是一个内存损坏的 PoC,而非利用代码。
该漏洞位于 ole32.dll!UtOlePresStmToContentsStm 中。该函数的作用是将 OLE 存储中 "OlePres" 流内的数据转换为适当格式的数据,并插入到同一存储的 "CONTENTS" 流中。它接收一个指向存储对象的 IStorage 指针以及三个不太重要的参数。
下面我们可以看到该函数的实现,以及来自 2025 年 1 月补丁的差异对比:
__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;
// 在存储中创建 "CONTENTS" 流并将其存入 pstmContents
if ( (lpVtbl->CreateStream)(pstg, L"CONTENTS", 18LL, 0LL, 0, &pstmContents) )
return 0LL;
// 立即释放 pstmContents,我们暂时不使用它
(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);// 第二个可能的失败点 -> 没有 OlePres 流
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);
// 如果 pstmContents 仍然存在则释放它,我们需要清理
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;
}
问题出在 pstmContents 变量上。最初它用于存储函数开头创建的 "CONTENTS" 流对象的指针。该流在创建后立即被销毁,并且 pstmContents 中存储的指针被释放(在 coml2.dll!ExposedStream::~ExposedStream 中释放)。然而,该变量仍然包含已释放的指针。在函数后续部分,该变量可能被复用于再次存储 "CONTENTS" 流的指针——因此,函数末尾存在清理代码,如果指针存储在变量中则将其释放。但代码未能考虑到 UtReadOlePresStmHeader 可能失败的情况——如果发生这种情况,pstmContents 仍会指向已释放的指针,并且我们会执行到清理代码,再次释放该指针。因此,会发生双重释放情况。
从补丁差异中可以看出,微软通过在其最初包含的指针被释放后将 pstmContents 设置为零来修复该问题。
仓库中包含一个 rtf 文件,该文件可复现此漏洞。我通过用 MS Word 打开该文件进行了测试,但你也可以使用其他解析 RTF 数据的应用程序(例如 Outlook)进行测试。通过其他包含 OLE 对象的格式进行利用也可能可行,我尚未尝试。
视频:
我稍后会公布详细信息。