From 662a27c129fc234465f67d2875a6477bc30a052d Mon Sep 17 00:00:00 2001 From: gpotter2 <10530980+gpotter2@users.noreply.github.com> Date: Wed, 11 Mar 2026 12:47:09 +0100 Subject: [PATCH 1/7] DCE/RPC: respect the association group for call_id --- scapy/layers/dcerpc.py | 19 ++++++++++++++----- scapy/layers/msrpce/msdcom.py | 13 ++++++++++++- scapy/layers/msrpce/rpcclient.py | 12 ++++++------ 3 files changed, 32 insertions(+), 12 deletions(-) diff --git a/scapy/layers/dcerpc.py b/scapy/layers/dcerpc.py index efa7e8e676b..20a2d1d5376 100644 --- a/scapy/layers/dcerpc.py +++ b/scapy/layers/dcerpc.py @@ -180,6 +180,8 @@ } DCE_RPC_INTERFACES_NAMES = {} DCE_RPC_INTERFACES_NAMES_rev = {} +COM_INTERFACES_NAMES = {} +COM_INTERFACES_NAMES_rev = {} class DCERPC_Transport(IntEnum): @@ -1350,6 +1352,8 @@ def register_com_interface(name, uuid, opnums): # bind for build for opnum, operations in opnums.items(): bind_top_down(DceRpc5Request, operations.request, opnum=opnum) + COM_INTERFACES_NAMES[uuid] = name + COM_INTERFACES_NAMES_rev[name.lower()] = uuid def find_com_interface(name) -> ComInterface: @@ -2824,6 +2828,7 @@ def __init__(self, *args, **kwargs): self.sent_cont_ids = [] self.cont_id = 0 # Currently selected context self.auth_context_id = 0 # Currently selected authentication context + self.assoc_group_id = 0 # Currently selected association group self.map_callid_opnum = {} self.frags = collections.defaultdict(lambda: b"") self.sniffsspcontexts = {} # Unfinished contexts for passive @@ -2869,6 +2874,8 @@ def _up_pkt(self, pkt): finally: self.sent_cont_ids = [] + self.assoc_group_id = pkt.assoc_group_id + # Endianness self.ndrendian = {0: "big", 1: "little"}[pkt[DceRpc5].endian] @@ -2878,18 +2885,20 @@ def _up_pkt(self, pkt): elif DceRpc5Request in pkt: # request => match opnum with callID opnum = pkt.opnum + uid = (self.assoc_group_id, pkt.call_id) if self.rpc_bind_is_com: - self.map_callid_opnum[pkt.call_id] = ( + self.map_callid_opnum[uid] = ( opnum, pkt[DceRpc5Request].payload.payload, ) else: - self.map_callid_opnum[pkt.call_id] = opnum, pkt[DceRpc5Request].payload + self.map_callid_opnum[uid] = opnum, pkt[DceRpc5Request].payload elif DceRpc5Response in pkt: # response => get opnum from table + uid = (self.assoc_group_id, pkt.call_id) try: - opnum, opts["request_packet"] = self.map_callid_opnum[pkt.call_id] - del self.map_callid_opnum[pkt.call_id] + opnum, opts["request_packet"] = self.map_callid_opnum[uid] + del self.map_callid_opnum[uid] except KeyError: log_runtime.info("Unknown call_id %s in DCE/RPC session" % pkt.call_id) # Bind / Alter request/response specific @@ -2912,7 +2921,7 @@ def _defragment(self, pkt, body=None): """ Function to defragment DCE/RPC packets. """ - uid = pkt.call_id + uid = (self.assoc_group_id, pkt.call_id) if pkt.pfc_flags.PFC_FIRST_FRAG and pkt.pfc_flags.PFC_LAST_FRAG: # Not fragmented return body diff --git a/scapy/layers/msrpce/msdcom.py b/scapy/layers/msrpce/msdcom.py index a6cadfe5008..eda0f8d940d 100644 --- a/scapy/layers/msrpce/msdcom.py +++ b/scapy/layers/msrpce/msdcom.py @@ -30,6 +30,7 @@ PadField, StrLenField, StrNullFieldUtf16, + UUIDEnumField, UUIDField, XShortField, XStrFixedLenField, @@ -37,6 +38,8 @@ from scapy.volatile import RandUUID from scapy.layers.dcerpc import ( + COM_INTERFACES_NAMES_rev, + COM_INTERFACES_NAMES, ComInterface, DCE_C_AUTHN_LEVEL, DCE_RPC_PROTOCOL_IDENTIFIERS, @@ -445,7 +448,15 @@ class OBJREF(Packet): fields_desc = [ XStrFixedLenField("signature", b"MEOW", length=4), # :3 LEIntField("flags", 0x04), - UUIDField("iid", IID_IActivationPropertiesIn, uuid_fmt=UUIDField.FORMAT_LE), + UUIDEnumField( + "iid", + IID_IActivationPropertiesIn, + ( + COM_INTERFACES_NAMES.get, + lambda x: COM_INTERFACES_NAMES_rev.get(x.lower()), + ), + uuid_fmt=UUIDField.FORMAT_LE, + ), ] diff --git a/scapy/layers/msrpce/rpcclient.py b/scapy/layers/msrpce/rpcclient.py index 2e0454340a5..ea5396c583b 100644 --- a/scapy/layers/msrpce/rpcclient.py +++ b/scapy/layers/msrpce/rpcclient.py @@ -7,6 +7,7 @@ DCE/RPC client as per [MS-RPCE] """ +import collections import uuid import socket @@ -101,7 +102,7 @@ def __init__( ), "transport must be from DCERPC_Transport" # Counters - self.call_id = 0 + self.call_ids = collections.defaultdict(lambda: 0) # by assoc_group_id self.next_cont_id = 0 # next available context id self.next_auth_contex_id = 0 # next available auth context id @@ -271,10 +272,10 @@ def sr1(self, pkt, **kwargs): The DCE/RPC header is added automatically. """ - self.call_id += 1 + self.call_ids[self.session.assoc_group_id] += 1 pkt = ( DceRpc5( - call_id=self.call_id, + call_id=self.call_ids[self.session.assoc_group_id], pfc_flags="PFC_FIRST_FRAG+PFC_LAST_FRAG", endian=self.ndrendian, auth_verifier=kwargs.pop("auth_verifier", None), @@ -295,10 +296,10 @@ def send(self, pkt, **kwargs): The DCE/RPC header is added automatically. """ - self.call_id += 1 + self.call_ids[self.session.assoc_group_id] += 1 pkt = ( DceRpc5( - call_id=self.call_id, + call_id=self.call_ids[self.session.assoc_group_id], pfc_flags="PFC_FIRST_FRAG+PFC_LAST_FRAG", endian=self.ndrendian, auth_verifier=kwargs.pop("auth_verifier", None), @@ -655,7 +656,6 @@ def _bind( and respcls in resp and self._check_bind_context(interface, resp.results) ): - self.call_id = 0 # reset call id port = resp.sec_addr.port_spec.decode() ndr = self.session.ndr64 and "NDR64" or "NDR32" self.ndr64 = self.session.ndr64 From a33cb5f63774076328a19022d064e75421c699ef Mon Sep 17 00:00:00 2001 From: gpotter2 <10530980+gpotter2@users.noreply.github.com> Date: Wed, 11 Mar 2026 13:16:03 +0100 Subject: [PATCH 2/7] Fix codespell typo --- scapy/layers/ms_nrtp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scapy/layers/ms_nrtp.py b/scapy/layers/ms_nrtp.py index d97abb18e77..2af3c6b1d05 100644 --- a/scapy/layers/ms_nrtp.py +++ b/scapy/layers/ms_nrtp.py @@ -712,7 +712,7 @@ def _member_type_infos_cb(pkt, lst, cur, remain): except StopIteration: return None typeEnum = BinaryTypeEnum(typeEnum) - # Return BinaryTypeEnum tainted with a pre-selected type. + # Return BinaryTypeEnum tainted with a preselected type. return functools.partial( NRBFAdditionalInfo, bintype=typeEnum, From 3940376d2a3c757e8ddf2cb86cccea8373549c58 Mon Sep 17 00:00:00 2001 From: gpotter2 <10530980+gpotter2@users.noreply.github.com> Date: Wed, 11 Mar 2026 14:49:13 +0100 Subject: [PATCH 3/7] DCE/RPC: use IDENTIFY by default --- scapy/layers/msrpce/msdcom.py | 50 ++++++++++++++++++++++++++++---- scapy/layers/msrpce/rpcclient.py | 43 ++++++++++++++++++++------- scapy/layers/ntlm.py | 7 +++++ 3 files changed, 85 insertions(+), 15 deletions(-) diff --git a/scapy/layers/msrpce/msdcom.py b/scapy/layers/msrpce/msdcom.py index eda0f8d940d..d60bc7ed834 100644 --- a/scapy/layers/msrpce/msdcom.py +++ b/scapy/layers/msrpce/msdcom.py @@ -63,6 +63,7 @@ NDRShortField, NDRSignedIntField, RPC_C_AUTHN, + RPC_C_IMP_LEVEL, ) from scapy.utils import valid_ip6, valid_ip from scapy.layers.msrpce.rpcclient import DCERPC_Client, DCERPC_Transport @@ -746,6 +747,7 @@ class OXID_Entry: def __init__(self): self.oxid: Optional[int] = None self.bindingInfo: Optional[Tuple[str, int]] = None + self.target_name: str = None self.authnHint: DCE_C_AUTHN_LEVEL = DCE_C_AUTHN_LEVEL.CONNECT self.version: Optional[COMVERSION] = None self.ipid_IRemUnknown: Optional[uuid.UUID] = None @@ -788,6 +790,7 @@ def sr1_req( iface: ComInterface, ssp=None, auth_level=None, + impersonation_type=None, timeout=None, **kwargs, ): @@ -799,6 +802,7 @@ def sr1_req( :param ssp: (optional) non default SSP to use to connect to the object exporter :param auth_level: (optional) non default authn level to use + :param impersonation_type: (optional) non default impersonation type to use :param timeout: (optional) timeout for the connection """ # Look for this object's entry @@ -828,6 +832,7 @@ def sr1_req( pkt=pkt, ssp=ssp, auth_level=auth_level, + impersonation_type=impersonation_type, timeout=timeout, **kwargs, ) @@ -869,6 +874,12 @@ def __init__(self, cid: GUID = None, verb=True, **kwargs): if "auth_level" not in kwargs and "ssp" in kwargs: kwargs["auth_level"] = DCE_C_AUTHN_LEVEL.PKT_INTEGRITY + # DCOM_Client handles the activations. + # [MS-RPCE] sect 3.2.4.1.1.2 : "it MUST specify a default impersonation + # level of at leastRPC_C_IMPL_LEVEL_IMPERSONATE" + if "impersonation_type" not in kwargs and "ssp" in kwargs: + kwargs["impersonation_type"] = RPC_C_IMP_LEVEL.IMPERSONATE + super(DCOM_Client, self).__init__( DCERPC_Transport.NCACN_IP_TCP, ndr64=False, @@ -919,7 +930,10 @@ def _RemoteCreateInstanceOrGetClassObject( raise ValueError("Must specify at least one interface !") # Bind IObjectExporter if not already - self.bind_or_alter(find_dcerpc_interface("IRemoteSCMActivator")) + self.bind_or_alter( + find_dcerpc_interface("IRemoteSCMActivator"), + target_name="rpcss/" + self.host, + ) # [MS-DCOM] sect 3.1.2.5.2.3.3 - Issuing the Activation Request @@ -1045,8 +1059,9 @@ def _RemoteCreateInstanceOrGetClassObject( ) # Set RPC bindings from the activation request - binds, _ = _ParseStringArray(remoteReply.valueof("pdsaOxidBindings")) + binds, secs = _ParseStringArray(remoteReply.valueof("pdsaOxidBindings")) entry.bindingInfo = self._ChoseRPCBinding(binds) + entry.target_name = self._CalculateTargetName(secs) if PropsOutInfo in prop: # Information about the interfaces that the client requested @@ -1236,6 +1251,21 @@ def _ChoseRPCBinding(self, bindings: List[STRINGBINDING]): return host, port raise ValueError("No valid bindings available !") + def _CalculateTargetName(self, secs: List[SECURITYBINDING]): + """ + 3.2.4.2 ORPC Invocations - Find SPN from aPrincName + """ + if self.ssp is None or not secs: + return None + + for sec in secs: + # "if the aPrincName field is nonempty" + if sec.wAuthnSvc == self.ssp.auth_type and sec.aPrincName: + return sec.aPrincName + + # "if the aPrincName field is empty, the client MUST NOT specify an SPN" + return None + def UnmarshallObjectReference( self, mifaceptr: MInterfacePointer, iid: ComInterface ): @@ -1272,7 +1302,10 @@ def ResolveOxid2( client.connect(host, port=port) # Bind IObjectExporter if not already - client.bind_or_alter(find_dcerpc_interface("IObjectExporter")) + client.bind_or_alter( + find_dcerpc_interface("IObjectExporter"), + target_name="rpcss/" + self.host, + ) try: # Perform ResolveOxid2 @@ -1304,8 +1337,9 @@ def ResolveOxid2( ) # Set RPC bindings from the oxid request - binds, _ = _ParseStringArray(resp.valueof("ppdsaOxidBindings")) + binds, secs = _ParseStringArray(resp.valueof("ppdsaOxidBindings")) entry.bindingInfo = self._ChoseRPCBinding(binds) + entry.target_name = self._CalculateTargetName(secs) # Update the OXID table if entry.oxid not in self.OXID_table: @@ -1353,6 +1387,7 @@ def sr1_orpc_req( ipid: uuid.UUID, ssp=None, auth_level=None, + impersonation_type=None, timeout=5, **kwargs, ): @@ -1364,6 +1399,7 @@ def sr1_orpc_req( :param ssp: (optional) non default SSP to use to connect to the object exporter :param auth_level: (optional) non default authn level to use + :param impersonation_type: (optional) non default impersonation type to use :param timeout: (optional) timeout for the connection """ # [MS-DCOM] sect 3.2.4.2 @@ -1396,6 +1432,7 @@ def sr1_orpc_req( DCERPC_Transport.NCACN_IP_TCP, ssp=ssp or self.ssp, auth_level=auth_level or oxid_entry.authnHint, + impersonation_type=impersonation_type or self.impersonation_type, verb=self.verb, ) @@ -1406,7 +1443,10 @@ def sr1_orpc_req( ) # Bind the COM interface - resolver_entry.client.bind_or_alter(ipid_entry.iface) + resolver_entry.client.bind_or_alter( + ipid_entry.iface, + target_name=oxid_entry.target_name, + ) # We need to set the NDR very late, after the bind pkt.ndr64 = resolver_entry.client.ndr64 diff --git a/scapy/layers/msrpce/rpcclient.py b/scapy/layers/msrpce/rpcclient.py index ea5396c583b..8ba09113687 100644 --- a/scapy/layers/msrpce/rpcclient.py +++ b/scapy/layers/msrpce/rpcclient.py @@ -502,6 +502,7 @@ def _bind( interface: Union[DceRpcInterface, ComInterface], reqcls, respcls, + target_name: Optional[str] = None, ) -> bool: """ Internal: used to send a bind/alter request @@ -560,7 +561,7 @@ def _bind( else 0 ) ), - target_name="host/" + self.host, + target_name=target_name or ("host/" + self.host), ) if status not in [GSS_S_CONTINUE_NEEDED, GSS_S_COMPLETE]: @@ -602,7 +603,7 @@ def _bind( self.sspcontext, token, status = self.ssp.GSS_Init_sec_context( self.sspcontext, input_token=resp.auth_verifier.auth_value, - target_name="host/" + self.host, + target_name=target_name or ("host/" + self.host), ) if status in [GSS_S_CONTINUE_NEEDED, GSS_S_COMPLETE]: @@ -645,7 +646,7 @@ def _bind( self.sspcontext, token, status = self.ssp.GSS_Init_sec_context( self.sspcontext, input_token=resp.auth_verifier.auth_value, - target_name="host/" + self.host, + target_name=target_name or ("host/" + self.host), ) else: log_runtime.error("GSS_Init_sec_context failed with %s !" % status) @@ -704,23 +705,45 @@ def _bind( resp.show() return False - def bind(self, interface: Union[DceRpcInterface, ComInterface]) -> bool: + def bind( + self, + interface: Union[DceRpcInterface, ComInterface], + target_name: Optional[str] = None, + ) -> bool: """ Bind the client to an interface :param interface: the DceRpcInterface object """ - return self._bind(interface, DceRpc5Bind, DceRpc5BindAck) + return self._bind( + interface, + DceRpc5Bind, + DceRpc5BindAck, + target_name=target_name, + ) - def alter_context(self, interface: Union[DceRpcInterface, ComInterface]) -> bool: + def alter_context( + self, + interface: Union[DceRpcInterface, ComInterface], + target_name: Optional[str] = None, + ) -> bool: """ Alter context: post-bind context negotiation :param interface: the DceRpcInterface object """ - return self._bind(interface, DceRpc5AlterContext, DceRpc5AlterContextResp) + return self._bind( + interface, + DceRpc5AlterContext, + DceRpc5AlterContextResp, + target_name=target_name, + ) - def bind_or_alter(self, interface: Union[DceRpcInterface, ComInterface]) -> bool: + def bind_or_alter( + self, + interface: Union[DceRpcInterface, ComInterface], + target_name: Optional[str] = None, + ) -> bool: """ Bind the client to an interface or alter the context if already bound @@ -728,10 +751,10 @@ def bind_or_alter(self, interface: Union[DceRpcInterface, ComInterface]) -> bool """ if not self.session.rpc_bind_interface: # No interface is bound - return self.bind(interface) + return self.bind(interface, target_name=target_name) elif self.session.rpc_bind_interface != interface: # An interface is already bound - return self.alter_context(interface) + return self.alter_context(interface, target_name=target_name) return True def open_smbpipe(self, name: str): diff --git a/scapy/layers/ntlm.py b/scapy/layers/ntlm.py index 4e0d956be72..fbba9a24e94 100644 --- a/scapy/layers/ntlm.py +++ b/scapy/layers/ntlm.py @@ -1584,6 +1584,13 @@ def GSS_Init_sec_context( if Context.flags & GSS_C_FLAGS.GSS_C_CONF_FLAG else [] ) + + ( + [ + "NEGOTIATE_IDENTIFY", + ] + if Context.flags & GSS_C_FLAGS.GSS_C_IDENTIFY_FLAG + else [] + ) ), ProductMajorVersion=10, ProductMinorVersion=0, From 163ff6f2d3f3da1458d84a328816f1ce0a53c175 Mon Sep 17 00:00:00 2001 From: gpotter2 <10530980+gpotter2@users.noreply.github.com> Date: Wed, 11 Mar 2026 15:01:46 +0100 Subject: [PATCH 4/7] Minor doc fix --- doc/scapy/layers/dcerpc.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/scapy/layers/dcerpc.rst b/doc/scapy/layers/dcerpc.rst index 068eac1d8ef..c2e4290756a 100644 --- a/doc/scapy/layers/dcerpc.rst +++ b/doc/scapy/layers/dcerpc.rst @@ -313,7 +313,7 @@ There are extensions to the :class:`~scapy.layers.msrpce.rpcclient.DCERPC_Client client.negotiate_sessionkey(bytes.fromhex("77777777777777777777777777777777")) client.close() -- the :class:`~scapy.layers.msrpce.msdcom.DCOM_Client` (unfinished) +- the :class:`~scapy.layers.msrpce.msdcom.DCOM_Client`. More details are available in `DCOM `_ Server ------ From 7604799aab78196366eee36c4271a213b43c674c Mon Sep 17 00:00:00 2001 From: gpotter2 <10530980+gpotter2@users.noreply.github.com> Date: Wed, 11 Mar 2026 15:02:48 +0100 Subject: [PATCH 5/7] Fix ACCESS_DENIED for random DCE/RPC calls --- scapy/layers/msrpce/rpcclient.py | 1 + 1 file changed, 1 insertion(+) diff --git a/scapy/layers/msrpce/rpcclient.py b/scapy/layers/msrpce/rpcclient.py index 8ba09113687..5f40ddec50e 100644 --- a/scapy/layers/msrpce/rpcclient.py +++ b/scapy/layers/msrpce/rpcclient.py @@ -347,6 +347,7 @@ def sr1_req(self, pkt, **kwargs): DceRpcSecVTCommand(SEC_VT_COMMAND_END=1) / DceRpcSecVTPcontext( InterfaceId=self.session.rpc_bind_interface.uuid, + Version=self.session.rpc_bind_interface.if_version, TransferSyntax="NDR64" if self.ndr64 else "NDR 2.0", TransferVersion=1 if self.ndr64 else 2, ) From 18c2db139fe17f4cdd79040ce3055cb3fe5d3f0c Mon Sep 17 00:00:00 2001 From: gpotter2 <10530980+gpotter2@users.noreply.github.com> Date: Wed, 11 Mar 2026 15:16:29 +0100 Subject: [PATCH 6/7] Another minor target_name fix --- scapy/layers/msrpce/msnrpc.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/scapy/layers/msrpce/msnrpc.py b/scapy/layers/msrpce/msnrpc.py index 20e2355c18c..1b47623e6ad 100644 --- a/scapy/layers/msrpce/msnrpc.py +++ b/scapy/layers/msrpce/msnrpc.py @@ -51,7 +51,6 @@ PNETLOGON_CREDENTIAL, ) - if conf.crypto_valid: from cryptography.hazmat.primitives import hashes, hmac from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes @@ -71,7 +70,6 @@ Optional, ) - # --- RFC # [MS-NRPC] sect 3.1.4.2 @@ -853,11 +851,12 @@ def establish_secure_channel( else: self.ssp = self.sock.session.ssp = KerberosSSP( UPN=UPN, - SPN="netlogon/" + DC_FQDN, PASSWORD=PASSWORD, KEY=KEY, ) - if not self.bind_or_alter(self.interface): + # [MS-NRPC] note <185> "Windows uses netlogon/" + target_name = "netlogon/" + DC_FQDN + if not self.bind_or_alter(self.interface, target_name=target_name): raise ValueError("Bind failed !") # Send AuthenticateKerberos request From 86c5fc4508675920a3c0e5cdca439f8db579f0f1 Mon Sep 17 00:00:00 2001 From: gpotter2 <10530980+gpotter2@users.noreply.github.com> Date: Wed, 11 Mar 2026 15:20:14 +0100 Subject: [PATCH 7/7] Fix assoc_group on server --- scapy/layers/msrpce/rpcserver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scapy/layers/msrpce/rpcserver.py b/scapy/layers/msrpce/rpcserver.py index 0569a5304bb..8b65164c2cc 100644 --- a/scapy/layers/msrpce/rpcserver.py +++ b/scapy/layers/msrpce/rpcserver.py @@ -388,7 +388,7 @@ def recv(self, data): self.session.out_pkt( hdr / cls( - assoc_group_id=RandShort(), + assoc_group_id=int(RandShort()), sec_addr=DceRpc5PortAny( port_spec=port_spec, ),