关联漏洞
标题:
Microsoft OLE 资源管理错误漏洞
(CVE-2025-21298)
描述:Microsoft OLE是美国微软(Microsoft)公司的一种面向对象的技术。 Microsoft OLE存在资源管理错误漏洞。攻击者利用该漏洞可以远程执行代码。以下产品和版本受到影响:Windows Server 2019 (Server Core installation),Windows Server 2022,Windows Server 2022 (Server Core installation),Windows 10 Version 21H2 for 32-bit Systems,Wi
描述
Proof of concept & details for CVE-2025-21298
介绍
# content
This is a proof-of-concept for [CVE-2025-21298 - Windows OLE Remote Code Execution Vulnerability (CVSS 9.8)](https://msrc.microsoft.com/update-guide/vulnerability/CVE-2025-21298). This is a **memory corruption PoC**, not an exploit.
Full patch diff via [ghidriff](https://github.com/clearbluejar/ghidriff): [LINK](diff/ole32_dec24.dll-ole32_jan25.dll.ghidriff.md)
# vulnerability
The vulnerability is located in `ole32.dll!UtOlePresStmToContentsStm`. The purpose of the function is ti convert data in an "OlePres" stream within an OLE storage into appropriately formatted data and insert it into the "CONTENTS" stream in the same storage. It receives an `IStorage` pointer to a storage object and three rather unimportant arguments.
Below we can see the implementation of the function with a diff from the Jan 2025 patch:
```diff
__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;
}
```
The problem is in the `pstmContents` variable. Initially it's used to store the pointer to the "CONTENTS" stream object that's created at the beginning of the function. The stream is immediately destroyed after being created and the pointer stored in `pstmContents` is released (which frees it in `coml2.dll!ExposedStream::~ExposedStream`). However, the variable still contains the free'd pointer. Further down in the function, the variable may be reused to store the pointer to the "CONTENTS" stream again - because of this, there's cleanup code at the end of the function that releases the pointer in case it's stored in the variable. The code fails to account for the fact that `UtReadOlePresStmHeader` may fail - if that happens, `pstmContents` will still point towards the free'd pointer and we'll fall through to the cleanup code, which will release the pointer again. As such, a double-free situation will happen.
As can be seen in the patch diff, Microsoft fixed the issue by setting `pstmContents` to zero after the pointer it contains initially is released.
# Reproducing
In the repo is an rtf file which reproduces the vulnerability. I tested by opening the file in MS Word but you can also test it with other applications that parse RTF data (e.g. outlook). Exploitation through other formats which embed OLE objects may be possible, I haven't tried.
Video:
[poc.webm](https://github.com/user-attachments/assets/2acc7ea4-f6cb-402b-9148-528535d94042)
# Details
I will publish details later.
文件快照
[4.0K] /data/pocs/f7ad0e3ca9e4f67a242205b65a4ecad4e2bcbeec
├── [4.0K] diff
│ └── [ 96K] ole32_dec24.dll-ole32_jan25.dll.ghidriff.md
├── [1.0K] LICENSE
├── [4.0K] poc
│ └── [ 221] cve-2025-21298-poc.rtf
├── [5.4K] README.md
└── [4.0K] video
└── [5.6M] poc.webm
3 directories, 5 files
备注
1. 建议优先通过来源进行访问。
2. 如果因为来源失效或无法访问,请发送邮箱到 f.jinxu#gmail.com 索取本地快照(把 # 换成 @)。
3. 神龙已为您对POC代码进行快照,为了长期维护,请考虑为本地POC付费,感谢您的支持。