• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# SPDX-License-Identifier: GPL-2.0-or-later
2# This file is part of Scapy
3# See https://scapy.net/ for more information
4# Copyright (C) Gabriel Potter
5
6"""
7DCE/RPC client as per [MS-RPCE]
8"""
9
10import uuid
11import socket
12
13from scapy.config import conf
14
15from scapy.layers.dcerpc import (
16    DceRpc5,
17    DceRpc5AlterContext,
18    DceRpc5AlterContextResp,
19    DceRpc5Auth3,
20    DceRpc5Bind,
21    DceRpc5BindAck,
22    DceRpc5BindNak,
23    DceRpc5Context,
24    DceRpc5Fault,
25    DceRpc5Request,
26    DceRpc5Response,
27    DceRpc5AbstractSyntax,
28    DceRpc5TransferSyntax,
29    DceRpcSocket,
30    DCERPC_Transport,
31    find_dcerpc_interface,
32    CommonAuthVerifier,
33    DCE_C_AUTHN_LEVEL,
34    # NDR
35    NDRPointer,
36    NDRContextHandle,
37)
38from scapy.layers.gssapi import (
39    SSP,
40    GSS_S_FAILURE,
41    GSS_S_COMPLETE,
42    GSS_S_CONTINUE_NEEDED,
43    GSS_C_FLAGS,
44)
45from scapy.layers.smb2 import STATUS_ERREF
46from scapy.layers.smbclient import (
47    SMB_RPC_SOCKET,
48)
49
50# RPC
51from scapy.layers.msrpce.ept import (
52    ept_map_Request,
53    ept_map_Response,
54    twr_p_t,
55    protocol_tower_t,
56    prot_and_addr_t,
57    UUID,
58)
59
60
61class DCERPC_Client(object):
62    """
63    A basic DCE/RPC client
64
65    :param ndr64: Should ask for NDR64 when binding (default False)
66    """
67
68    def __init__(self, transport, ndr64=False, ndrendian="little", verb=True, **kwargs):
69        self.sock = None
70        self.transport = transport
71        assert isinstance(
72            transport, DCERPC_Transport
73        ), "transport must be from DCERPC_Transport"
74        self.call_id = 0
75        self.cont_id = 0
76        self.ndr64 = ndr64
77        self.ndrendian = ndrendian
78        self.verb = verb
79        self.auth_level = kwargs.pop("auth_level", DCE_C_AUTHN_LEVEL.NONE)
80        self.auth_context_id = kwargs.pop("auth_context_id", 0)
81        self.ssp = kwargs.pop("ssp", None)  # type: SSP
82        self.sspcontext = None
83        self.dcesockargs = kwargs
84
85    @classmethod
86    def from_smblink(cls, smbcli, smb_kwargs={}, **kwargs):
87        """
88        Build a DCERPC_Client from a SMB_Client.smblink directly
89        """
90        client = DCERPC_Client(DCERPC_Transport.NCACN_NP, **kwargs)
91        sock = client.smbrpcsock = SMB_RPC_SOCKET(smbcli, **smb_kwargs)
92        client.sock = DceRpcSocket(
93            sock,
94            DceRpc5,
95            ssp=client.ssp,
96            auth_level=client.auth_level,
97            auth_context_id=client.auth_context_id,
98            **client.dcesockargs,
99        )
100        return client
101
102    def connect(self, ip, port=None, timeout=5, smb_kwargs={}):
103        """
104        Initiate a connection
105        """
106        if port is None:
107            if self.transport == DCERPC_Transport.NCACN_IP_TCP:  # IP/TCP
108                port = 135
109            elif self.transport == DCERPC_Transport.NCACN_NP:  # SMB
110                port = 445
111            else:
112                raise ValueError(
113                    "Can't guess the port for transport: %s" % self.transport
114                )
115        sock = socket.socket()
116        sock.settimeout(timeout)
117        if self.verb:
118            print(
119                "\u2503 Connecting to %s on port %s via %s..."
120                % (ip, port, repr(self.transport))
121            )
122        sock.connect((ip, port))
123        if self.verb:
124            print(
125                conf.color_theme.green(
126                    "\u2514 Connected from %s" % repr(sock.getsockname())
127                )
128            )
129        if self.transport == DCERPC_Transport.NCACN_NP:  # SMB
130            # We pack the socket into a SMB_RPC_SOCKET
131            sock = self.smbrpcsock = SMB_RPC_SOCKET.from_tcpsock(
132                sock, ssp=self.ssp, **smb_kwargs
133            )
134            self.sock = DceRpcSocket(sock, DceRpc5, **self.dcesockargs)
135        elif self.transport == DCERPC_Transport.NCACN_IP_TCP:
136            self.sock = DceRpcSocket(
137                sock,
138                DceRpc5,
139                ssp=self.ssp,
140                auth_level=self.auth_level,
141                auth_context_id=self.auth_context_id,
142                **self.dcesockargs,
143            )
144
145    def close(self):
146        if self.verb:
147            print("X Connection closed\n")
148        self.sock.close()
149
150    def sr1(self, pkt, **kwargs):
151        self.call_id += 1
152        pkt = (
153            DceRpc5(
154                call_id=self.call_id,
155                pfc_flags="PFC_FIRST_FRAG+PFC_LAST_FRAG",
156                endian=self.ndrendian,
157                auth_verifier=kwargs.pop("auth_verifier", None),
158            )
159            / pkt
160        )
161        if "pfc_flags" in kwargs:
162            pkt.pfc_flags = kwargs.pop("pfc_flags")
163        return self.sock.sr1(pkt, verbose=0, **kwargs)
164
165    def send(self, pkt, **kwargs):
166        self.call_id += 1
167        pkt = (
168            DceRpc5(
169                call_id=self.call_id,
170                pfc_flags="PFC_FIRST_FRAG+PFC_LAST_FRAG",
171                endian=self.ndrendian,
172                auth_verifier=kwargs.pop("auth_verifier", None),
173            )
174            / pkt
175        )
176        if "pfc_flags" in kwargs:
177            pkt.pfc_flags = kwargs.pop("pfc_flags")
178        return self.sock.send(pkt, **kwargs)
179
180    def sr1_req(self, pkt, **kwargs):
181        if self.verb:
182            print(conf.color_theme.opening(">> REQUEST: %s" % pkt.__class__.__name__))
183        # Send/receive
184        resp = self.sr1(
185            DceRpc5Request(cont_id=self.cont_id, alloc_hint=len(pkt)) / pkt,
186            **kwargs,
187        )
188        if DceRpc5Response in resp:
189            if self.verb:
190                print(
191                    conf.color_theme.success(
192                        "<< RESPONSE: %s"
193                        % (resp[DceRpc5Response].payload.__class__.__name__)
194                    )
195                )
196            return resp[DceRpc5Response].payload
197        else:
198            if self.verb:
199                if DceRpc5Fault in resp:
200                    if resp[DceRpc5Fault].payload and not isinstance(
201                        resp[DceRpc5Fault].payload, conf.raw_layer
202                    ):
203                        resp[DceRpc5Fault].payload.show()
204                    if resp.status == 0x00000005:
205                        print(conf.color_theme.fail("! nca_s_fault_access_denied"))
206                    elif resp.status == 0x00000721:
207                        print(
208                            conf.color_theme.fail(
209                                "! nca_s_fault_sec_pkg_error "
210                                "(error in checksum/encryption)"
211                            )
212                        )
213                    else:
214                        print(
215                            conf.color_theme.fail(
216                                "! %s" % STATUS_ERREF.get(resp.status, "Failure")
217                            )
218                        )
219                        resp.show()
220                return
221            return resp
222
223    def get_bind_context(self, interface):
224        return [
225            DceRpc5Context(
226                cont_id=0,
227                abstract_syntax=DceRpc5AbstractSyntax(
228                    if_uuid=interface.uuid,
229                    if_version=interface.if_version,
230                ),
231                transfer_syntaxes=[
232                    DceRpc5TransferSyntax(
233                        # NDR 2.0 32-bit
234                        if_uuid="NDR 2.0",
235                        if_version=2,
236                    )
237                ],
238            ),
239        ] + (
240            [
241                DceRpc5Context(
242                    cont_id=1,
243                    abstract_syntax=DceRpc5AbstractSyntax(
244                        if_uuid=interface.uuid,
245                        if_version=interface.if_version,
246                    ),
247                    transfer_syntaxes=[
248                        DceRpc5TransferSyntax(
249                            # NDR64
250                            if_uuid="NDR64",
251                            if_version=1,
252                        )
253                    ],
254                ),
255                DceRpc5Context(
256                    cont_id=2,
257                    abstract_syntax=DceRpc5AbstractSyntax(
258                        if_uuid=interface.uuid,
259                        if_version=interface.if_version,
260                    ),
261                    transfer_syntaxes=[
262                        DceRpc5TransferSyntax(
263                            if_uuid=uuid.UUID("6cb71c2c-9812-4540-0300-000000000000"),
264                            if_version=1,
265                        )
266                    ],
267                ),
268            ]
269            if self.ndr64
270            else []
271        )
272
273    def _bind(self, interface, reqcls, respcls):
274        # Build a security context: [MS-RPCE] 3.3.1.5.2
275        if self.verb:
276            print(
277                conf.color_theme.opening(
278                    ">> %s on %s" % (reqcls.__name__, interface)
279                    + (" (with %s)" % self.ssp.__class__.__name__ if self.ssp else "")
280                )
281            )
282        if not self.ssp or (
283            self.transport == DCERPC_Transport.NCACN_NP
284            and self.auth_level < DCE_C_AUTHN_LEVEL.PKT_INTEGRITY
285        ):
286            # NCACN_NP = SMB without INTEGRITY/PRIVACY does not bind the RPC securely,
287            # again as it has already authenticated during the SMB Session Setup
288            resp = self.sr1(
289                reqcls(context_elem=self.get_bind_context(interface)),
290                auth_verifier=None,
291            )
292            status = GSS_S_COMPLETE
293        else:
294            # Perform authentication
295            self.sspcontext, token, status = self.ssp.GSS_Init_sec_context(
296                self.sspcontext,
297                req_flags=(
298                    # SSPs need to be instantiated with some special flags
299                    # for DCE/RPC usages.
300                    GSS_C_FLAGS.GSS_C_DCE_STYLE
301                    | GSS_C_FLAGS.GSS_C_REPLAY_FLAG
302                    | GSS_C_FLAGS.GSS_C_SEQUENCE_FLAG
303                    | GSS_C_FLAGS.GSS_C_MUTUAL_FLAG
304                    | (
305                        GSS_C_FLAGS.GSS_C_INTEG_FLAG
306                        if self.auth_level >= DCE_C_AUTHN_LEVEL.PKT_INTEGRITY
307                        else 0
308                    )
309                    | (
310                        GSS_C_FLAGS.GSS_C_CONF_FLAG
311                        if self.auth_level >= DCE_C_AUTHN_LEVEL.PKT_PRIVACY
312                        else 0
313                    )
314                ),
315            )
316            if status not in [GSS_S_CONTINUE_NEEDED, GSS_S_COMPLETE]:
317                # Authentication failed.
318                self.sspcontext.clifailure()
319                return False
320            resp = self.sr1(
321                reqcls(context_elem=self.get_bind_context(interface)),
322                auth_verifier=(
323                    None
324                    if not self.sspcontext
325                    else CommonAuthVerifier(
326                        auth_type=self.ssp.auth_type,
327                        auth_level=self.auth_level,
328                        auth_context_id=self.auth_context_id,
329                        auth_value=token,
330                    )
331                ),
332                pfc_flags=(
333                    "PFC_FIRST_FRAG+PFC_LAST_FRAG"
334                    + (
335                        # If the SSP supports "Header Signing", advertise it
336                        "+PFC_SUPPORT_HEADER_SIGN"
337                        if self.ssp is not None
338                        and self.sock.session.support_header_signing
339                        else ""
340                    )
341                ),
342            )
343            if respcls not in resp:
344                token = None
345                status = GSS_S_FAILURE
346            else:
347                # Call the underlying SSP
348                self.sspcontext, token, status = self.ssp.GSS_Init_sec_context(
349                    self.sspcontext, val=resp.auth_verifier.auth_value
350                )
351            if status in [GSS_S_CONTINUE_NEEDED, GSS_S_COMPLETE]:
352                # Authentication should continue
353                if token and self.ssp.LegsAmount(self.sspcontext) % 2 == 1:
354                    # AUTH 3 for certain SSPs (e.g. NTLM)
355                    # "The server MUST NOT respond to an rpc_auth_3 PDU"
356                    self.send(
357                        DceRpc5Auth3(),
358                        auth_verifier=CommonAuthVerifier(
359                            auth_type=self.ssp.auth_type,
360                            auth_level=self.auth_level,
361                            auth_context_id=self.auth_context_id,
362                            auth_value=token,
363                        ),
364                    )
365                    status = GSS_S_COMPLETE
366                else:
367                    # Authentication can continue in two ways:
368                    # - through DceRpc5Auth3 (e.g. NTLM)
369                    # - through DceRpc5AlterContext (e.g. Kerberos)
370                    while token:
371                        respcls = DceRpc5AlterContextResp
372                        resp = self.sr1(
373                            DceRpc5AlterContext(
374                                context_elem=self.get_bind_context(interface)
375                            ),
376                            auth_verifier=CommonAuthVerifier(
377                                auth_type=self.ssp.auth_type,
378                                auth_level=self.auth_level,
379                                auth_context_id=self.auth_context_id,
380                                auth_value=token,
381                            ),
382                        )
383                        if respcls not in resp:
384                            status = GSS_S_FAILURE
385                            break
386                        if resp.auth_verifier is None:
387                            status = GSS_S_COMPLETE
388                            break
389                        self.sspcontext, token, status = self.ssp.GSS_Init_sec_context(
390                            self.sspcontext, val=resp.auth_verifier.auth_value
391                        )
392        # Check context acceptance
393        if (
394            status == GSS_S_COMPLETE
395            and respcls in resp
396            and any(x.result == 0 for x in resp.results[: int(self.ndr64) + 1])
397        ):
398            self.call_id = 0  # reset call id
399            port = resp.sec_addr.port_spec.decode()
400            ndr = self.sock.session.ndr64 and "NDR64" or "NDR32"
401            self.cont_id = int(self.sock.session.ndr64)  # ctx 0 for NDR32, 1 for NDR64
402            if self.verb:
403                print(
404                    conf.color_theme.success(
405                        f"<< {respcls.__name__} port '{port}' using {ndr}"
406                    )
407                )
408            self.sock.session.sspcontext = self.sspcontext
409            return True
410        else:
411            if self.verb:
412                if DceRpc5BindNak in resp:
413                    err_msg = resp.sprintf(
414                        "reject_reason: %DceRpc5BindNak.provider_reject_reason%"
415                    )
416                    print(conf.color_theme.fail("! Bind_nak (%s)" % err_msg))
417                    if DceRpc5BindNak in resp:
418                        if resp[DceRpc5BindNak].payload and not isinstance(
419                            resp[DceRpc5BindNak].payload, conf.raw_layer
420                        ):
421                            resp[DceRpc5BindNak].payload.show()
422                elif DceRpc5Fault in resp:
423                    if resp.status == 0x00000005:
424                        print(conf.color_theme.fail("! nca_s_fault_access_denied"))
425                    elif resp.status == 0x00000721:
426                        print(
427                            conf.color_theme.fail(
428                                "! nca_s_fault_sec_pkg_error "
429                                "(error in checksum/encryption)"
430                            )
431                        )
432                    else:
433                        print(
434                            conf.color_theme.fail(
435                                "! %s" % STATUS_ERREF.get(resp.status, "Failure")
436                            )
437                        )
438                        resp.show()
439                    if DceRpc5Fault in resp:
440                        if resp[DceRpc5Fault].payload and not isinstance(
441                            resp[DceRpc5Fault].payload, conf.raw_layer
442                        ):
443                            resp[DceRpc5Fault].payload.show()
444                else:
445                    print(conf.color_theme.fail("! Failure"))
446                    resp.show()
447            return False
448
449    def bind(self, interface):
450        """
451        Bind the client to an interface
452        """
453        return self._bind(interface, DceRpc5Bind, DceRpc5BindAck)
454
455    def alter_context(self, interface):
456        """
457        Alter context: post-bind context negotiation
458        """
459        return self._bind(interface, DceRpc5AlterContext, DceRpc5AlterContextResp)
460
461    def bind_or_alter(self, interface):
462        """
463        Bind the client to an interface or alter the context if already bound
464        """
465        if not self.sock.session.rpc_bind_interface:
466            # No interface is bound
467            self.bind(interface)
468        else:
469            # An interface is already bound
470            self.alter_context(interface)
471
472    def open_smbpipe(self, name):
473        """
474        Open a certain filehandle with the SMB automaton
475        """
476        self.ipc_tid = self.smbrpcsock.tree_connect("IPC$")
477        self.smbrpcsock.open_pipe(name)
478
479    def close_smbpipe(self):
480        """
481        Close the previously opened pipe
482        """
483        self.smbrpcsock.set_TID(self.ipc_tid)
484        self.smbrpcsock.close_pipe()
485        self.smbrpcsock.tree_disconnect()
486
487    def connect_and_bind(
488        self,
489        ip,
490        interface,
491        port=None,
492        smb_kwargs={},
493    ):
494        """
495        Asks the Endpoint Mapper what address to use to connect to the interface,
496        then uses connect() followed by a bind()
497        """
498        if self.transport == DCERPC_Transport.NCACN_IP_TCP:
499            # IP/TCP
500            # 1. ask the endpoint mapper (port 135) for the IP:PORT
501            endpoints = get_endpoint(
502                ip,
503                interface,
504                ndrendian=self.ndrendian,
505                verb=self.verb,
506            )
507            if endpoints:
508                ip, port = endpoints[0]
509            else:
510                return
511            # 2. Connect to that IP:PORT
512            self.connect(ip, port=port)
513        elif self.transport == DCERPC_Transport.NCACN_NP:
514            # SMB
515            # 1. ask the endpoint mapper (over SMB) for the namedpipe
516            endpoints = get_endpoint(
517                ip,
518                interface,
519                transport=self.transport,
520                ndrendian=self.ndrendian,
521                verb=self.verb,
522                smb_kwargs=smb_kwargs,
523            )
524            if endpoints:
525                pipename = endpoints[0].lstrip("\\pipe\\")
526            else:
527                return
528            # 2. connect to the SMB server
529            self.connect(ip, port=port, smb_kwargs=smb_kwargs)
530            # 3. open the new named pipe
531            self.open_smbpipe(pipename)
532        # Bind in RPC
533        self.bind(interface)
534
535    def epm_map(self, interface):
536        """
537        Calls ept_map (the EndPoint Manager)
538        """
539        if self.ndr64:
540            ndr_uuid = "NDR64"
541            ndr_version = 1
542        else:
543            ndr_uuid = "NDR 2.0"
544            ndr_version = 2
545        pkt = self.sr1_req(
546            ept_map_Request(
547                obj=NDRPointer(
548                    referent_id=1,
549                    value=UUID(
550                        Data1=0,
551                        Data2=0,
552                        Data3=0,
553                        Data4=None,
554                    ),
555                ),
556                map_tower=NDRPointer(
557                    referent_id=2,
558                    value=twr_p_t(
559                        tower_octet_string=bytes(
560                            protocol_tower_t(
561                                floors=[
562                                    prot_and_addr_t(
563                                        lhs_length=19,
564                                        protocol_identifier=0xD,
565                                        uuid=interface.uuid,
566                                        version=interface.major_version,
567                                        rhs_length=2,
568                                        rhs=interface.minor_version,
569                                    ),
570                                    prot_and_addr_t(
571                                        lhs_length=19,
572                                        protocol_identifier=0xD,
573                                        uuid=ndr_uuid,
574                                        version=ndr_version,
575                                        rhs_length=2,
576                                        rhs=0,
577                                    ),
578                                    prot_and_addr_t(
579                                        lhs_length=1,
580                                        protocol_identifier="RPC connection-oriented protocol",  # noqa: E501
581                                        rhs_length=2,
582                                        rhs=0,
583                                    ),
584                                    {
585                                        DCERPC_Transport.NCACN_IP_TCP: (
586                                            prot_and_addr_t(
587                                                lhs_length=1,
588                                                protocol_identifier="NCACN_IP_TCP",
589                                                rhs_length=2,
590                                                rhs=135,
591                                            )
592                                        ),
593                                        DCERPC_Transport.NCACN_NP: (
594                                            prot_and_addr_t(
595                                                lhs_length=1,
596                                                protocol_identifier="NCACN_NP",
597                                                rhs_length=2,
598                                                rhs=b"0\x00",
599                                            )
600                                        ),
601                                    }[self.transport],
602                                    {
603                                        DCERPC_Transport.NCACN_IP_TCP: (
604                                            prot_and_addr_t(
605                                                lhs_length=1,
606                                                protocol_identifier="IP",
607                                                rhs_length=4,
608                                                rhs="0.0.0.0",
609                                            )
610                                        ),
611                                        DCERPC_Transport.NCACN_NP: (
612                                            prot_and_addr_t(
613                                                lhs_length=1,
614                                                protocol_identifier="NCACN_NB",
615                                                rhs_length=10,
616                                                rhs=b"127.0.0.1\x00",
617                                            )
618                                        ),
619                                    }[self.transport],
620                                ],
621                            )
622                        ),
623                    ),
624                ),
625                entry_handle=NDRContextHandle(
626                    attributes=0,
627                    uuid=b"\x00" * 16,
628                ),
629                max_towers=500,
630                ndr64=self.ndr64,
631                ndrendian=self.ndrendian,
632            )
633        )
634        if pkt and ept_map_Response in pkt:
635            status = pkt[ept_map_Response].status
636            # [MS-RPCE] sect 2.2.1.2.5
637            if status == 0x00000000:
638                towers = [
639                    protocol_tower_t(x.value.tower_octet_string)
640                    for x in pkt[ept_map_Response].ITowers.value[0].value
641                ]
642                # Let's do some checks to know we know what we're doing
643                endpoints = []
644                for t in towers:
645                    if t.floors[0].uuid != interface.uuid:
646                        if self.verb:
647                            print(
648                                conf.color_theme.fail(
649                                    "! Server answered with a different interface."
650                                )
651                            )
652                        raise ValueError
653                    if t.floors[1].sprintf("%uuid%") != ndr_uuid:
654                        if self.verb:
655                            print(
656                                conf.color_theme.fail(
657                                    "! Server answered with a different NDR version."
658                                )
659                            )
660                        raise ValueError
661                    if self.transport == DCERPC_Transport.NCACN_IP_TCP:
662                        endpoints.append((t.floors[4].rhs, t.floors[3].rhs))
663                    elif self.transport == DCERPC_Transport.NCACN_NP:
664                        endpoints.append(t.floors[3].rhs.rstrip(b"\x00").decode())
665                return endpoints
666            elif status == 0x16C9A0D6:
667                if self.verb:
668                    pkt.show()
669                    print(
670                        conf.color_theme.fail(
671                            "! Server errored: 'There are no elements that satisfy"
672                            " the specified search criteria'."
673                        )
674                    )
675                raise ValueError
676        print(conf.color_theme.fail("! Failure."))
677        if pkt:
678            pkt.show()
679        raise ValueError("EPM Map failed")
680
681
682def get_endpoint(
683    ip,
684    interface,
685    transport=DCERPC_Transport.NCACN_IP_TCP,
686    ndrendian="little",
687    verb=True,
688    smb_kwargs={},
689):
690    """
691    Call the endpoint mapper on a remote IP to find an interface
692
693    :param ip:
694    :param interface:
695    :param mode:
696    :param verb:
697
698    :return: a list of connection tuples for this interface
699    """
700    client = DCERPC_Client(
701        transport,
702        ndr64=False,
703        ndrendian=ndrendian,
704        verb=verb,
705    )  # EPM only works with NDR32
706    client.connect(ip, smb_kwargs=smb_kwargs)
707    if transport == DCERPC_Transport.NCACN_NP:  # SMB
708        client.open_smbpipe("epmapper")
709    client.bind(find_dcerpc_interface("ept"))
710    endpoints = client.epm_map(interface)
711    client.close()
712    return endpoints
713