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-21298Windows 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:

A situation conducive to UAF
Fig. 1. A situation conducive to UAF.

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 Memory corruption PoC

Fig. 2. 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:

Patched UtOlePresStmToContentsStm
Fig. 3. Patched UtOlePresStmToContentsStm.
Writing 0 into pstmContents
Fig. 4. Writing «0» into pstmContents.

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.

CreateStream call
Fig. 5. CreateStream call.

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

Buffer contents before CreateStream
Fig. 6. Buffer contents before CreateStream.
Buffer contents after CreateStream
Fig. 7. Buffer contents after CreateStream.

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

Release method call
Fig. 8. Release method call.
Buffer contents after Release
Fig. 9. Buffer contents after Release.

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

OpenStream call
Fig. 10. OpenStream call.

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

Contents of pstmContents and pstmOlePres
Fig. 11. Contents of pstmContents and pstmOlePres.

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

UtReadOlePresStmHeader call
Fig. 12. UtReadOlePresStmHeader call.

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

Final block of UtOlePresStmToContentsStm
Fig. 13. Final block of UtOlePresStmToContentsStm.

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.

Failed to retrieve the address
Fig. 14. Failed to retrieve the address.

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:

Value that triggers the error
Fig. 15. Value that triggers the error.

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:

Return from UtReadOlePresStmHeader
Fig. 16. Return from UtReadOlePresStmHeader.

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:

  1. An IStream is created, and a pointer to the object is written into pstmContents.
  2. The Release method frees the memory, but the pointer is still stored in pstmContents.
  3. An IStream is created in the freed slot, and the pointer is written into pstmOlePres.
  4. pstmOlePres and pstmContents point to the same memory location.
  5. UtReadOlePresStmHeader returns a non-zero value (an error).
  6. 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.

Paranoid Security How Attackers Abuse Signed Drivers to Take Over Infrastructure. Using BYOVD to Bypass PPL Protection Mechanisms in Windows. February 5
Vulnerability Research How Attackers Abuse Signed Drivers to Take Over Infrastructure. Using BYOVD to Bypass PPL Protection Mechanisms in Windows.
Paranoid Security Microsoft Patch Tuesday Analysis – January 2026 January 13
MS Patch Tuesday Microsoft Patch Tuesday Analysis – January 2026
Paranoid Security FortiOS 8.0 firmware analysis & rootfs decryption January 12
FortiOS 8.0 firmware analysis & rootfs decryption