Analysis of the Use-After-Free vulnerability in ole32.dll (CVE-2025-21298)
Introduction
In January 2025, Microsoft released a security update that fixed several vulnerabilities, including CVE-2025-21298 — Windows OLE Remote Code Execution Vulnerability. This vulnerability was rated 9.8 on the CVSS scale, and the maximum impact was classified as remote code execution (Remote Code Execution — RCE).
Within a few weeks, a Proof of Concept (PoC) was published on GitHub, and many security companies released write-ups about this vulnerability. However, several months have passed since then, and as of late March 2025 a working exploit had not been published (apart from the PoC), and no exploitation of this vulnerability in the wild (in the wild, ITW) had been observed.
This raises a question: is this vulnerability really as frightening as it was described at first? In this article, we will take a detailed look at CVE-2025-21298, the PoC published on GitHub, and try to answer the question: «Is it possible to achieve RCE when exploiting this vulnerability?»
Vulnerability description
CVE-2025-21298 — is a CWE-416: Use After Free vulnerability that does not require user interaction (zero-click vulnerability) in the Windows Object Linking and Embedding (OLE) technology. This technology is used in a large number of Windows tools — from office documents to email. To exploit this vulnerability, an attacker must craft a malicious RTF file and send it to the victim.
A Use-After-Free (UAF) vulnerability is a class of issues where a program accesses an already freed memory region, which attackers can use to execute arbitrary code.
Consider the following situation:

After free(A) is executed, that memory range can be allocated again if needed. And if the variable A is used later during program execution—for example, in condition checks or when calling methods of objects stored in the allocated memory—a UAF vulnerability occurs. The risk is highest if an attacker controls the contents of variable C (for instance, the memory is allocated for a user name) — in that case they can influence the program’s control flow and even fully take over execution.
Proof of Concept
Let’s return to our vulnerability: within a week after the fix was released, a GitHub repository appeared with a PoC for CVE-2025-21298 and additional information. The researcher performed version diffing; the results were also published openly, the root cause was described, and a memory corruption PoC 
Technical details
So, the vulnerability turned out to be in ole32.dll, in the UtOlePresStmToContentsStm function. When fixing the issue, only a few lines were added to this function; their main purpose is to clear pstmContents after calling Release:


Based on this, we can assume the problem was the value stored in that variable. Now let’s see how this function behaved before the fix. To do this, we’ll use WinDbg and run winword.exe under the debugger with the PoC RTF document.
Initially, a stream is created — an IStream object, whose address is written into the pstmContents variable. It stores a pointer to the heap where the object’s contents are located.

If CreateStream fails, the function terminates. In my case, 0x17560530 is written into pstmContents.


If the stream is created successfully, the Release method is called immediately for this object, freeing the memory.


Next, a stream is opened — an IStream object, whose address is written into the pstmOlePres variable.

If after calling OpenStream we inspect pstmContents and pstmOlePres, we will see that they contain a pointer to the same address.

Then, if the stream is opened successfully, the UtReadOlePresStmHeader function is called, which processes the stream header.

In case of an error, execution jumps to the function’s final block, which frees the memory allocated for pstmContents and pstmOlePres.

Because both variables point to the same address in memory (to the same object), after calling Release for pstmOlePres, the program, while trying to obtain the Release method address for pstmContents, attempts to read a value at an address that has become garbage after the object was freed.

In this case, the PoC is needed to satisfy two conditions: first, to drive program execution into UtOlePresStmToContentsStm, and second, to trigger an error in UtReadOlePresStmHeader. We won’t focus on the first condition, since doing so would require fully dissecting the RTF document structure and more, which is outside the scope of this article. Instead, we’ll focus on the second condition — the error in UtReadOlePresStmHeader. The only thing worth mentioning is that an RTF document consists of tags and their contents, and the objdata tag contains bytes written in hexadecimal form. The problematic value that causes the error is stored in these two bytes:

They define the STM format (I can’t be 100% sure, but the closest definition I found was software transactional memory).
The program implements support for all values except value 2:

This function has other cases that result in a non-zero return value, but they do not affect the behavior of the target function UtOlePresStmToContentsStm.
So, let’s summarize what happens in the vulnerable function when triggering the bug:
- An IStream is created, and a pointer to the object is written into pstmContents.
- The Release method frees the memory, but the pointer is still stored in pstmContents.
- An IStream is created in the freed slot, and the pointer is written into pstmOlePres.
- pstmOlePres and pstmContents point to the same memory location.
- UtReadOlePresStmHeader returns a non-zero value (an error).
- The Release method frees the memory for pstmOlePres.
An attempt is made to dereference a pointer in freed memory (computing the address of the Release method for a subsequent call).
To hijack control flow, we would need to write attacker-controlled data into the freed slot between the two Release calls (for Type Confusion), but the problem is that both methods are executed back-to-back, leaving no opportunity to write our own value. This makes control-flow hijacking impossible in this particular case. This is the main issue with CVE-2025-21298: the two critical Release calls in the cleanup block (Fig. 13) execute strictly one after another, without any intermediate calls or operations that would give an attacker a «window» for memory manipulation.
Conclusion
Thus, analysis of the PoC and the ole32.dll code shows that CVE-2025-21298 is indeed a Use-After-Free vulnerability that can lead to a crash (DoS) when opening a specially crafted RTF file.
However, due to the specifics of the code in UtOlePresStmToContentsStm — namely, the second Release call occurring immediately after the first—there is no window for exploitation and for swapping data in freed memory. This makes achieving RCE via this specific path extremely unlikely.
The high CVSS rating was likely assigned based on the potential danger of a UAF in a critical OLE component with a zero-click attack vector, without accounting for the concrete exploitability challenges of this particular case. The lack of ITW exploits only supports this conclusion.