Associated Vulnerability
Description
CVE-2025-21298
Readme
# 🛑 CVE-2025-21298 – Critical Zero-Click RCE in Microsoft Windows OLE
---
## 📌 Overview
* **📂 Component:** Microsoft Windows OLE (Object Linking and Embedding) – specifically in `ole32.dll`
* **🐞 Vulnerability Type:** Double-free memory corruption
* **📌 Function Affected:** `UtOlePresStmToContentsStm`
* **🔥 Severity:** CVSS 3.1 score **9.8** – Critical
* **📅 Patch Release:** January 2025 Microsoft Security Updates
* **📥 Attack Vector:** Specially crafted RTF file
* **⚠️ Interaction Required:** None (zero-click, triggered on preview)
* **🎯 Primary Targets:** Outlook users, but also Word or any OLE-capable application
---
## ⚙️ Technical Details
1. **Double-Free Bug**
* When processing an RTF file containing certain embedded OLE objects, `ole32.dll` fails to properly manage memory cleanup.
* The `pstmContents` pointer is freed twice under specific conditions, leading to heap corruption.
2. **Exploitation Path**
* An attacker crafts a malicious RTF containing payload data that manipulates OLE structures.
* When the victim **previews** the email in Outlook, the RTF renderer calls OLE functions.
* The double-free allows overwriting heap metadata or function pointers.
* Attacker-controlled code is executed in the context of the user — potentially SYSTEM if chained with privilege escalation.
3. **Why It’s Dangerous**
* **Zero-click** → user doesn’t need to open the file manually.
* **Email-borne** → can be mass-exploited.
* **Widespread Reach** → affects almost all supported Windows versions.
---
## 💻 Affected Systems
* **Windows 10:** Versions 1507 → 22H2
* **Windows 11:** Up to 24H2
* **Windows Server:** 2008, 2008 R2, 2012, 2012 R2, 2016, 2019, 2022, 2025
---
## 🛡️ Mitigation
1. **Install Microsoft’s January 2025 patch** — fully fixes the issue.
2. If patching is delayed:
* 📜 Configure Outlook to display all emails as **plain text**.
* 📦 Block `.rtf` attachments at the mail gateway or via endpoint policies.
* 🔒 Disable preview pane for untrusted messages.
---
## 👀 Detection & Monitoring
* **Endpoint:** Look for processes loading `ole32.dll` when handling `.rtf` files from untrusted sources.
* **Network:** Monitor inbound email traffic for `.rtf` attachments with abnormal OLE object streams.
* **Security Tools:** Use EDR rules or Sigma detections for RTF OLE exploitation patterns.
---
## 📊 Risk Summary Table
| 📝 Aspect | 📌 Details |
| ------------------ | --------------------------------------------- |
| Vulnerability Type | Zero-click RCE via OLE double-free |
| Severity | 🔥 CVSS 9.8 – Critical |
| Attack Vector | Previewing crafted RTF in Outlook / Word |
| Impact | 🖥️ Remote code execution |
| Affected Systems | Windows 10, 11, Server 2008–2025 |
| Fix | 🛡️ January 2025 Patch |
| Workarounds | Block RTF, plain-text emails, disable preview |
---
## 🧠 Exploit Chain Diagram (Text Form)
```
📩 Attacker sends crafted RTF email
↓
📬 Victim receives email in Outlook
↓
👀 Victim previews message (no click required)
↓
🧩 RTF renderer calls OLE32.dll → double-free in UtOlePresStmToContentsStm
↓
💥 Memory corruption → payload execution
↓
🎯 Attacker gains code execution on target system
```
---
## 🕷️ 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:
```json
__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.
---
## ⚠️ **Disclaimer**
This information about **CVE-2025-21298** is provided **strictly for educational and defensive purposes**.
Any attempt to exploit this vulnerability on systems without **explicit, prior, written authorization** is **illegal** and may result in **criminal charges**, civil liability, or both.
The goal is to help security professionals, researchers, and system administrators **understand, detect, and mitigate** the issue — **not** to encourage malicious activity.
Always conduct testing in a **controlled lab environment** or on systems you fully own and control.
File Snapshot
[4.0K] /data/pocs/ef40022ea093c35d868b951694eec13fb5da8639
├── [ 221] CVE-2025-21298.rtf
└── [8.5K] README.md
0 directories, 2 files
Remarks
1. It is advised to access via the original source first.
2. Local POC snapshots are reserved for subscribers — if the original source is unavailable, the local mirror is part of the paid plan.
3. Mirroring, verifying, and maintaining this POC archive takes ongoing effort, so local snapshots are a paid feature. Your subscription keeps the archive online — thank you for the support. View subscription plans →