Skip to content

Part VI New Interfaces & Case Studies

impacket has settled into roughly one major release a year: 0.12.0 (Sep 2024), 0.13.0 (Oct 2025), 0.13.1 (May 2026), with master (0.14.0.dev) still absorbing new protocols. This part picks up where Parts I-V left off: it covers the RPC interfaces and support libraries added by those releases that the main text does not yet cover, then walks through two recent public vulnerabilities - BadSuccessor (CVE-2025-53779) and CVE-2025-33073 - to show how to build your own PoC on top of impacket.

Chapter 8 New Interfaces and Support Libraries in impacket 0.12-0.14

The modules added in this cycle are listed below. Most of them are not "another SAMR"; rather they fill the gaps on the AD attack chain that previously required custom code or third-party tooling: certificate enrollment, group key distribution, authentication negotiation, authorization enumeration, and ACL manipulation.

Module Spec Introduced One-line purpose
dcerpc/v5/icpr.py [MS-ICPR] ICertPassage 0.13.0 Submit certificate requests to an enterprise CA over RPC (AD CS)
dcerpc/v5/gkdi.py [MS-GKDI] Group Key Distribution 0.12.0 Fetch the group key envelope (DNSSEC root key protection / DPAPI-NG)
negoex.py [MS-NEGOEX] SPNEGO Extended Negotiation 0.14.0.dev (master, unreleased) Parse and build SPNEGO extended negotiation (NEGOEX) messages
dcerpc/v5/raa.py [MS-RAA] Remote Authorization API master Server-side authorization decision for a security descriptor
dcerpc/v5/scmr.py [MS-SCMR] Service Control Manager long-standing Remote service management (the layer under psexec / smbexec)
acl.py - 0.13.1 Reusable ACL read/write helpers for SMB/NTFS
dpapi_ng.py - 0.12+ DPAPI Next Generation (CNG) decryption for gMSA / dMSA / LAPSv2

Note: scmr.py is not new, but the main text never covered it alone; it is included here because it is key to understanding why lateral movement so often ends in "start a service". negoex.py currently lives only on master (0.14.0.dev) and is not in a release yet - cite the specific commit when you use it.

8.1 [MS-ICPR] icpr.py - Certificate Enrollment over RPC (the AD CS channel)

[MS-ICPR] (ICertPassage Remote Protocol) defines the RPC interface a client uses to submit a certificate request to an enterprise Certificate Authority (CA). Its RPC interface is named ICertPassage, normally exposed on the named pipe \pipe\cert (and discoverable via EPM). Its significance in AD CS attacks is that the "relay to the CA and enroll" path no longer has to go through the HTTP enrollment endpoint (the classic ESC8 route) - it can be driven directly over RPC.

from impacket.uuid import uuidtup_to_bin

MSRPC_UUID_ICPR = uuidtup_to_bin(("91ae6020-9e3c-11cf-8d7c-00aa00c091be", "0.0"))

The interface defines a single opnum 0 method, CertServerRequest. Its request and response reuse the CERTTRANSBLOB structure from [MS-WCCE] 2.2.2.2 (a length plus a byte array):

class CERTTRANSBLOB(NDRSTRUCT):
    structure = (
        ("cb", ULONG),      # byte array length
        ("pb", PBYTE),      # byte array contents
    )

class CertServerRequest(NDRCALL):
    opnum = 0
    structure = (
        ("dwFlags", DWORD),
        ("pwszAuthority", LPWSTR),        # CA name (may be empty, server default)
        ("pdwRequestId", DWORD),          # request id, 0 on first call
        ("pctbAttribs", CERTTRANSBLOB),   # newline-separated attributes, e.g. CertificateTemplate:User
        ("pctbRequest", CERTTRANSBLOB),   # DER-encoded PKCS#10 CSR
    )

class CertServerRequestResponse(NDRCALL):
    structure = (
        ("pdwRequestId", DWORD),
        ("pdwDisposition", ULONG),        # disposition: 3=issued, 5=pending
        ("pctbCert", CERTTRANSBLOB),
        ("pctbEncodedCert", CERTTRANSBLOB),       # issued certificate (DER)
        ("pctbDispositionMessage", CERTTRANSBLOB),
    )

The module ends with a helper, hCertServerRequest, that bundles the whole "join the attributes, convert the CSR, send the request, interpret the disposition" flow:

def hCertServerRequest(
    dce: DCERPC_v5,
    csr: bytes,
    attributes: List[str],
    request_id: int = 0,
    ca: str = ""
) -> str:
    attribs = checkNullString("\n".join(attributes)).encode("utf-16le")
    pctb_attribs = CERTTRANSBLOB()
    pctb_attribs["cb"] = len(attribs)
    pctb_attribs["pb"] = attribs

    pctb_request = CERTTRANSBLOB()
    pctb_request["cb"] = len(csr)
    pctb_request["pb"] = csr

    request = CertServerRequest()
    request["dwFlags"] = 0
    request["pwszAuthority"] = checkNullString(ca)
    request["pdwRequestId"] = request_id
    request["pctbAttribs"] = pctb_attribs
    request["pctbRequest"] = pctb_request

    response = dce.request(request)
    # ... pdwDisposition == 3 means success, == 5 means pending approval
    return b"".join(response["pctbEncodedCert"]["pb"])

The minimal skeleton of a hand-written PoC follows: generate a key and a CSR with cryptography, then bind to ICertPassage and submit. example/icpr_enroll_cert.py is a complete, runnable version.

# eg.example/icpr_enroll_cert.py (excerpt)
from impacket.dcerpc.v5 import transport, epm, icpr
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_LEVEL_PKT_PRIVACY

def enroll(target, ca_name, template, username, password, domain):
    # 1. Resolve the dynamic ICertPassage endpoint via EPM, over ncacn_ip_tcp
    binding = epm.hept_map(target, icpr.MSRPC_UUID_ICPR, protocol="ncacn_ip_tcp")
    rpctransport = transport.DCERPCTransportFactory(binding)
    rpctransport.set_credentials(username, password, domain)
    dce = rpctransport.get_dce_rpc()
    dce.connect()
    dce.bind(icpr.MSRPC_UUID_ICPR)

    # 2. Generate a private key and a CSR with cryptography
    key, csr_der = build_csr(template)

    # 3. Submit: attributes are newline-separated; ca is the CA name (netbios\CA or its name)
    cert_der = icpr.hCertServerRequest(dce, csr_der, ["CertificateTemplate:%s" % template], ca=ca_name)
    dce.disconnect()
    return key, cert_der

In the wild, rpcattack.py inside ntlmrelayx uses exactly this interface for its "relay to ICPR and enroll" attack (ICPRRPCAttack): after obtaining a relayed session it builds a CSR with ADCSAttack.generate_csr, calls icpr.hCertServerRequest(...), and writes the certificate out as PKCS#12 (pfx). Because ICPR is a plaintext RPC channel, if the CA enforces encryption / EPA the path fails - which is why attackers still prefer the HTTP endpoint, but it also shows that closing one channel does not close the whole attack surface.

8.2 [MS-GKDI] gkdi.py - Group Key Distribution Protocol

[MS-GKDI] (Group Key Distribution Protocol) is how a client requests a "group key envelope" (Group Key Envelope, GKE) from the domain KDS (Key Distribution Service) root key. It serves two typical scenarios: DNSSEC root key protection, and DPAPI-NG (CNG) - that is, decryption of the "managed passwords" behind gMSA, dMSA and Windows LAPS v2.

MSRPC_UUID_GKDI = uuidtup_to_bin(('B9785960-524F-11DF-8B6D-83DCDED72085', '1.0'))

This interface also has a single opnum 0 method, GkdiRpcGetKey:

class GkdiRpcGetKey(NDRCALL):
    opnum = 0
    structure = (
        ('cbTargetSD', ULONG),       # length of the target security descriptor
        ('pbTargetSD', BYTE_ARRAY),  # target security descriptor: the server decides from it whether you may read the key
        ('pRootKeyID', PGUID),
        ('L0KeyID', LONG),
        ('L1KeyID', LONG),
        ('L2KeyID', LONG),
    )

The helper GkdiGetKey is straightforward - pass the target security descriptor and the three index levels:

def GkdiGetKey(dce, target_sd, l0=-1, l1=-1, l2=-1, root_key_id=NULL):
    request = GkdiRpcGetKey()
    request['cbTargetSD'] = len(target_sd)
    request['pbTargetSD'] = target_sd.getData()
    request['pRootKeyID'] = root_key_id
    request['L0KeyID'] = l0
    request['L1KeyID'] = l1
    request['L2KeyID'] = l2
    return dce.request(request)

The response is a byte string that must be parsed by GroupKeyEnvelope. The genuinely useful fields are L1Key / L2Key plus the KDF and encryption parameters - the raw material for deriving the concrete data key (KEK):

class GroupKeyEnvelope(Structure):
    structure = (
        ('Version', '<L=0'),
        ('Magic', '<L=0'),
        # ... L0Index / L1Index / L2Index / RootKeyId ...
        ('KdfAlgo', ':'), ('KdfPara', ':', KDFParameter),
        ('SecAlgo', ':'), ('SecPara', ':'),
        ('L1Key', ':'), ('L2Key', ':'),
    )

The call path (see getLAPSv2Decrypt in examples/GetLAPSPassword.py) is fixed: parse the KeyIdentifier (carrying L0/L1/L2 and RootKeyId) out of the LAPS blob read from LDAP, then resolve GKDI's dynamic ncacn_ip_tcp endpoint with hept_map and bind:

from impacket.dcerpc.v5.gkdi import MSRPC_UUID_GKDI, GkdiGetKey, GroupKeyEnvelope
from impacket.dpapi_ng import EncryptedPasswordBlob, KeyIdentifier, compute_kek, unwrap_cek, decrypt_plaintext

stringBinding = hept_map(destHost=target, remoteIf=MSRPC_UUID_GKDI, protocol='ncacn_ip_tcp')
resp = GkdiGetKey(dce, target_sd=target_sd, l0=key_id['L0Index'], l1=key_id['L1Index'],
                  l2=key_id['L2Index'], root_key_id=key_id['RootKeyId'])
gke = GroupKeyEnvelope(b''.join(resp['pbbOut']))
kek = compute_kek(gke, key_id)

The actual decryption once you hold the GKE belongs to DPAPI-NG (see section 8.7). example/gkdi_get_key.py is a standalone runnable example that fetches a key.

8.3 [MS-NEGOEX] negoex.py - SPNEGO Extended Negotiation

[MS-NEGOEX] (SPNEGO Extended Negotiation Security Mechanism) extends SPNEGO: plain SPNEGO does only a single OID exchange and cannot express "authentication mechanisms that need several round trips", nor metadata; NEGOEX adds an independent state machine for both. Inside SPNEGO, NEGOEX is identified by OID 1.3.6.1.4.1.311.2.2.30 and its messages carry the little-endian magic NEGOEXTS (0x535458454f47454e).

MESSAGE_SIGNATURE = b'NEGOEXTS'
NEGOEX_OID = b'\x2b\x06\x01\x04\x01\x82\x37\x02\x02\x1e'
AUTH_SCHEME_PKU2U = uuid.UUID('235f69ad-73fb-4dbc-8203-0629e739339b')
CHECKSUM_SCHEME_RFC3961 = 1

NEGOEX defines eight message types, all preceded by a MessageHeader (signature + type + sequence number + header length + total length + ConversationId):

class MESSAGE_TYPE(IntEnum):
    INITIATOR_NEGO   = 0   # initiator negotiation (advertises supported mechanisms)
    ACCEPTOR_NEGO    = 1   # acceptor negotiation
    INITIATOR_META_DATA = 2
    ACCEPTOR_META_DATA  = 3
    CHALLENGE        = 4   # mechanism-specific challenge data
    AP_REQUEST       = 5   # mechanism-specific authentication request data
    VERIFY           = 6   # integrity check (RFC 3961 checksum)
    ALERT            = 7   # alert

Every message type is wrapped in a Structure subclass - NegoMessage, ExchangeMessage, VerifyMessage, AlertMessage - and the module provides parseNegoExToken (split a concatenated NEGOEX token into its messages) and createNegoMessage / createExchangeMessage / createVerifyMessage / createAlertMessage (build messages). The actual exchange is driven by the NegoExContext state machine:

class NegoExContext(object):
    """Drives a NEGOEX negotiation as either initiator or acceptor."""

    def registerAuthScheme(self, scheme): ...          # register a locally supported mechanism

    def createInitialToken(self, optimisticToken=None):
        # build INITIATOR_NEGO; an optimisticToken can save a round trip ([MS-NEGOEX] 3.1.5.4)
        ...

    def processToken(self, data):
        # parse the peer token, advance the state machine, return the exchange payload for the mechanism
        ...

    def _processVerify(self, verifyMsg):
        # validate the VERIFY RFC3961 checksum using the key the scheme provides
        keyUsage = NEGOEX_KEYUSAGE_ACCEPTOR if self.isInitiator else NEGOEX_KEYUSAGE_INITIATOR
        expected = make_checksum(checksumType, Key(enctype, keyBytes), keyUsage, b''.join(self._messageHistory))
        ...

The checksum uses the RFC 3961 key usage numbers: 23 when signed by the initiator, 25 when signed by the acceptor. parseNegoExToken is the most practical piece here - it splits a NEGOEX token into an ordered list of messages, each carrying its raw bytes (raw_data), which is convenient for protocol analysis or checksum recomputation:

from impacket.negoex import parseNegoExToken, MESSAGE_TYPE

for pm in parseNegoExToken(token_bytes):
    if pm.getMessageType() == MESSAGE_TYPE.INITIATOR_NEGO:
        print(pm.message.getAuthSchemeList())   # peer-supported auth scheme GUID list

For attack research, NEGOEX matters because it puts the alternative authentication mechanisms on the table: in 0.13.1, ntlmrelayx deliberately stopped advertising NEGOEX to SMB peers (ChangeLog: "avoiding unsupported NEGOEX advertisement"), precisely because an inappropriate NEGOEX advertisement changes the negotiation outcome and interferes with relaying. Understanding this state machine is the key to judging "will this negotiation end in Kerberos or NTLM". example/negoex_parse_token.py shows how to recompute the checksum from a hex token.

Version note: as of writing, negoex.py is still an unreleased master feature (0.14.0.dev); the API may shift, so use it for research and parsing rather than production.

8.4 [MS-RAA] raa.py - Remote Authorization Enumeration

[MS-RAA] (Remote Authorization API Protocol) lifts the local Windows Authz API onto RPC. Its most elegant property: you can send a security descriptor to the server and directly ask "for this identity (SID), what does this descriptor actually grant?" - with the server performing all inheritance and ScopedPolicyID stripping. This is far more precise than hand-rolling DACL logic locally, and is especially useful for permission confirmation before an attack like BadSuccessor.

MSRPC_UUID_RAA = uuidtup_to_bin(('0b1c2170-5732-4e0e-8cd3-d9b16f3b84d7', '0.0'))

# Two object UUIDs: the latter disables the server-side stripping
# of SYSTEM_SCOPED_POLICY_ID_ACEs from the security descriptor.
RAA_OBJECT_UUID_DEFAULT           = '9a81c2bd-a525-471d-a4ed-49907c0b23da'
RAA_OBJECT_UUID_NO_SCOPED_POLICY  = '5fc860e0-6f6e-4fc2-83cd-46324f25e90b'

The interface has seven opnums:

OPNUMS = {
    0 : (AuthzrFreeContext, AuthzrFreeContextResponse),
    1 : (AuthzrInitializeContextFromSid, AuthzrInitializeContextFromSidResponse),   # SID -> authz context
    2 : (AuthzrInitializeCompoundContext, AuthzrInitializeCompoundContextResponse), # user+device compound context
    3 : (AuthzrAccessCheck, AuthzrAccessCheckResponse),   # the core: access decision on a security descriptor
    4 : (AuthzGetInformationFromContext, AuthzGetInformationFromContextResponse),   # group memberships / claims
    5 : (AuthzrModifyClaims, AuthzrModifyClaimsResponse),
    6 : (AuthzrModifySids, AuthzrModifySidsResponse),
}

The test case (tests/dcerpc/test_raa.py) shows the interface runs over ncacn_ip_tcp and requires RPC_C_AUTHN_LEVEL_PKT_PRIVACY, since the server must act on the caller's identity. The typical flow is to turn a SID into a context with hAuthzrInitializeContextFromSid, then call hAuthzrAccessCheck with the security descriptor and the desired access mask:

sid_ctx = raa.hAuthzrInitializeContextFromSid(dce, target_sid, flags=raa.AUTHZ_COMPUTE_PRIVILEGES)
resp = raa.hAuthzrAccessCheck(dce, sid_ctx['ContextHandle'], securityDescriptor, desiredAccess,
                              objectTypeList=[], resultListLength=1)
granted = resp['pReply']['GrantedAccessMask'][0]   # the server-computed final authorization result

With it, questions such as "does this ordinary user have CreateChild (0x1) on this OU, or the right to create an msDS-DelegatedManagedServiceAccount" can be answered by the server directly, instead of you implementing a (bug-prone) ACL inheritance algorithm yourself. example/raa_enum_permissions.py uses it to decide whether a given SID can create child objects under a target OU.

8.5 [MS-SCMR] scmr.py - Service Control Manager

[MS-SCMR] is the remote service management protocol, on the \pipe\svcctl endpoint. It is the layer under psexec.py / smbexec.py / services.py - lateral movement in a domain almost always ends in "start a service on the target", so it cannot be skipped.

MSRPC_UUID_SCMR = uuidtup_to_bin(('367ABB81-9844-35F1-AD32-98F038001003', '2.0'))

The module ships a coherent set of helpers covering the service lifecycle:

from impacket.dcerpc.v5 import scmr

# open the SCM database -> open/create a service -> start/control it
scHandle = scmr.hROpenSCManagerW(dce, 'DUMMY\x00', 'ServicesActive\x00', scmr.SC_MANAGER_ALL_ACCESS)['lpScHandle']
svcHandle = scmr.hROpenServiceW(dce, scHandle, 'MyService\x00')
scmr.hRQueryServiceStatus(dce, svcHandle)
scmr.hRQueryServiceConfigW(dce, svcHandle)      # query the binary path and other config
scmr.hRChangeServiceConfigW(dce, svcHandle, lpBinaryPathName='C:\\evil.exe\x00')
scmr.hRStartServiceW(dce, svcHandle)

A common "write your own script" scenario is to enumerate services and note those with a writable binary path for DLL hijacking / service replacement:

resp = scmr.hREnumServicesStatusW(dce, scHandle)
for svc in resp['lpServiceStatus']:
    cfg = scmr.hRQueryServiceConfigW(dce, scmr.hROpenServiceW(dce, scHandle, svc['lpServiceName'])['lpServiceHandle'])
    print(svc['lpServiceName'], cfg['lpServiceConfig']['lpBinaryPathName'])

Version note: 0.13.1 fixed SCMR "failure action" marshaling (ChangeLog #2046/#2152); if RChangeServiceConfig2W used to throw when setting failure actions on an older version, upgrading resolves it.

8.6 acl.py - ACL Helpers for SMB/NTFS

0.13.1 added a reusable ACL helper module, impacket/acl.py (by Gefen Altshuler). Previously, modifying a remote file's ACL meant constantly converting between ldaptypes and smb3structs; now this is wrapped into classes such as SecurityAttributes and SMBFileACL, reusing ldaptypes' ACE/DACL structures.

# Supported permission shorthands mapped to NT access masks
SUPPORTED_PERMISSIONS = {
    "R": 0x00120089,   # read
    "W": 0x00100116,   # write
    "D": 0x00110000,   # delete
    "X": 0x00000020,   # execute
    "F": 0x001F01FF,   # full control
}

It targets the "read/write file security descriptors over SMB" scenario and pairs with the ACL management that 0.13.1 added to smbclient.py. (use the ACL commands built into upstream examples/smbclient.py; there is no need to re-implement this locally.) The point of this module is that it extends "ACL manipulation" from the LDAP scenario to the SMB file scenario, filling the gap left by dacledit (LDAP only) with no SMB-side tool.

8.7 dpapi_ng.py - DPAPI Next Generation (CNG)

impacket/dpapi_ng.py implements the DPAPI-NG (a.k.a. DPAPI CNG) decryption flow. It is upstream/downstream of gkdi.py: GKDI fetches the group key envelope, and dpapi_ng uses it to unprotect the data. It covers gMSA / dMSA msDS-ManagedPassword, Windows LAPS v2 msLAPS-EncryptedPassword, and certificate private keys (msDS-KeyCredentialLink).

The key functions form a clear chain:

def SP800_108_Counter(master, key_len, prf, num_keys=None, label=b'', context=b''): ...
def compute_kdf_context(key_guid, l0, l1, l2): ...
def compute_l2_key(key_id: KeyIdentifier, gke: GroupKeyEnvelope): ...
def compute_kek(gke: GroupKeyEnvelope, key_id: KeyIdentifier): ...      # derive the KEK from the GKE
def aes_unwrap(wrapping_key: bytes, wrapped_key: bytes): ...
def unwrap_cek(kek, encrypted_cek): ...
def decrypt_plaintext(cek, iv, encrypted_blob): ...

The KDF uses NIST SP 800-108 counter mode (SP800_108_Counter) with fixed labels such as KDS service (for KEK derivation):

KDS_SERVICE_LABEL  = "KDS service\0".encode("utf-16-le")
KEK_PUBLIC_KEY_LABEL = "KDS public key\0".encode("utf-16le")

The full decryption chain can be read straight from examples/GetLAPSPassword.py:

# 1. read msLAPS-EncryptedPassword from LDAP (EncryptedPasswordBlob)
blob = EncryptedPasswordBlob(rawEncryptedLAPSBlob)
key_id = blob['KeyIdentifier']
# 2. fetch the GroupKeyEnvelope via GKDI (see 8.2)
gke = GroupKeyEnvelope(b''.join(GkdiGetKey(...)['pbbOut']))
# 3. derive the KEK -> unwrap the CEK -> decrypt the plaintext
kek = compute_kek(gke, key_id)
cek = unwrap_cek(kek, blob['EncryptedCek'])
plaintext = decrypt_plaintext(cek, blob['Iv'], blob['EncryptedBlob'])

This chain is the generic recipe for reading Windows LAPS v2 and gMSA/dMSA managed passwords. A complete runnable example is upstream examples/GetLAPSPassword.py (its getLAPSv2Decrypt is exactly this chain).