• 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 server as per [MS-RPCE]
8"""
9
10import socket
11import threading
12from collections import deque
13
14from scapy.arch import get_if_addr
15from scapy.config import conf
16from scapy.data import MTU
17from scapy.volatile import RandShort
18
19from scapy.layers.dcerpc import (
20    DceRpc5,
21    DceRpcSession,
22    DceRpc5Bind,
23    DceRpc5BindAck,
24    DceRpc5BindNak,
25    DceRpc5Auth3,
26    DceRpc5AlterContext,
27    DceRpc5AlterContextResp,
28    DceRpc5Result,
29    DceRpc5Request,
30    DceRpc5Response,
31    DceRpc5TransferSyntax,
32    DceRpc5PortAny,
33    CommonAuthVerifier,
34    DCE_RPC_INTERFACES,
35    DCERPC_Transport,
36    RPC_C_AUTHN_LEVEL,
37)
38
39# RPC
40from scapy.layers.msrpce.ept import (
41    ept_map_Request,
42    ept_map_Response,
43    twr_p_t,
44    protocol_tower_t,
45    prot_and_addr_t,
46)
47
48
49class _DCERPC_Server_metaclass(type):
50    def __new__(cls, name, bases, dct):
51        dct.setdefault(
52            "dcerpc_commands",
53            {x.dcerpc_command: x for x in dct.values() if hasattr(x, "dcerpc_command")},
54        )
55        return type.__new__(cls, name, bases, dct)
56
57
58class DCERPC_Server(metaclass=_DCERPC_Server_metaclass):
59    def __init__(
60        self,
61        transport,
62        ndr64=False,
63        verb=True,
64        local_ip=None,
65        port=None,
66        portmap=None,
67        **kwargs,
68    ):
69        self.transport = transport
70        self.session = DceRpcSession(**kwargs)
71        self.queue = deque()
72        self.ndr64 = ndr64
73        if ndr64:
74            self.ndr_name = "NDR64"
75        else:
76            self.ndr_name = "NDR 2.0"
77        # For endpoint mapper. TODO: improve separation/handling of SMB/IP etc
78        self.local_ip = local_ip
79        self.port = port
80        self.portmap = portmap or {}
81        self.verb = verb
82
83    def loop(self, sock):
84        while True:
85            pkt = sock.recv(MTU)
86            if not pkt:
87                break
88            self.recv(pkt)
89            # send all possible responses
90            while True:
91                resp = self.get_response()
92                if not resp:
93                    break
94                sock.send(bytes(resp))
95
96    @staticmethod
97    def answer(reqcls):
98        """
99        A decorator that registers a DCE/RPC responder to a command.
100        See the DCE/RPC documentation.
101
102        :param reqcls: the DCE/RPC packet class to respond to
103        """
104
105        def deco(func):
106            func.dcerpc_command = reqcls
107            return func
108
109        return deco
110
111    def extend(self, server_cls):
112        """
113        Extend a DCE/RPC server into another
114        """
115        self.dcerpc_commands.update(server_cls.dcerpc_commands)
116
117    def make_reply(self, req):
118        cls = req[DceRpc5Request].payload.__class__
119        if cls in self.dcerpc_commands:
120            # call handler
121            return self.dcerpc_commands[cls](self, req)
122        return None
123
124    @classmethod
125    def spawn(cls, transport, iface=None, port=135, bg=False, **kwargs):
126        """
127        Spawn a DCE/RPC server
128
129        :param transport: one of DCERPC_Transport
130        :param iface: the interface to spawn it on (default: conf.iface)
131        :param port: the port to spawn it on (for IP_TCP or the SMB server)
132        :param bg: background mode? (default: False)
133        """
134        if transport == DCERPC_Transport.NCACN_IP_TCP:
135            # IP/TCP case
136            ssock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
137            local_ip = get_if_addr(iface or conf.iface)
138            try:
139                ssock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
140            except OSError:
141                pass
142            ssock.bind((local_ip, port))
143            ssock.listen(5)
144            sockets = []
145            if kwargs.get("verb", True):
146                print(
147                    conf.color_theme.green(
148                        "Server %s started. Waiting..." % cls.__name__
149                    )
150                )
151
152            def _run():
153                # Wait for clients forever
154                try:
155                    while True:
156                        clientsocket, address = ssock.accept()
157                        sockets.append(clientsocket)
158                        print(
159                            conf.color_theme.gold(
160                                "\u2503 Connection received from %s" % repr(address)
161                            )
162                        )
163                        server = cls(
164                            DCERPC_Transport.NCACN_IP_TCP,
165                            local_ip=local_ip,
166                            port=port,
167                            **kwargs,
168                        )
169                        threading.Thread(
170                            target=server.loop, args=(clientsocket,)
171                        ).start()
172                except KeyboardInterrupt:
173                    print("X Exiting.")
174                    ssock.shutdown(socket.SHUT_RDWR)
175                except OSError:
176                    print("X Server closed.")
177                finally:
178                    for sock in sockets:
179                        try:
180                            sock.shutdown(socket.SHUT_RDWR)
181                            sock.close()
182                        except Exception:
183                            pass
184                    ssock.close()
185
186            if bg:
187                # Background
188                threading.Thread(target=_run).start()
189                return ssock
190            else:
191                # Non-background
192                _run()
193        elif transport == DCERPC_Transport.NCACN_NP:
194            # SMB case
195            from scapy.layers.smbserver import SMB_Server
196
197            kwargs.setdefault("shares", [])  # do not expose files by default
198            return SMB_Server.spawn(
199                iface=iface or conf.iface,
200                port=port,
201                bg=bg,
202                # Important: pass the DCE/RPC server
203                DCERPC_SERVER_CLS=cls,
204                # SMB parameters
205                **kwargs,
206            )
207        else:
208            raise ValueError("Unsupported transport :(")
209
210    def recv(self, data):
211        if isinstance(data, bytes):
212            req = DceRpc5(data)
213        else:
214            req = data
215        # If the packet has padding, it contains several fragments
216        pad = None
217        if conf.padding_layer in req:
218            pad = req[conf.padding_layer].load
219            req[conf.padding_layer].underlayer.remove_payload()
220        # Ask the DCE/RPC session to process it (match interface, etc.)
221        req = self.session.in_pkt(req)
222        hdr = DceRpc5(
223            endian=req.endian,
224            encoding=req.encoding,
225            float=req.float,
226            call_id=req.call_id,
227        )
228        # Now process the packet based on the DCE/RPC type
229        if DceRpc5Bind in req or DceRpc5AlterContext in req or DceRpc5Auth3 in req:
230            # Log
231            if self.verb:
232                print(
233                    conf.color_theme.opening(
234                        "<< %s" % req.payload.__class__.__name__
235                        + (
236                            " (with %s%s)"
237                            % (
238                                self.session.ssp.__class__.__name__,
239                                (
240                                    f" - {self.session.auth_level.name}"
241                                    if self.session.auth_level is not None
242                                    else ""
243                                ),
244                            )
245                            if self.session.ssp
246                            else ""
247                        )
248                    )
249                )
250            if not self.session.rpc_bind_interface:
251                # The session did not find a matching interface !
252                self.queue.extend(self.session.out_pkt(hdr / DceRpc5BindNak()))
253                if self.verb:
254                    print(conf.color_theme.fail("! DceRpc5BindNak (unknown interface)"))
255            else:
256                auth_value, status = None, 0
257                if (
258                    self.session.ssp
259                    and req.auth_verifier
260                    and req.auth_verifier.auth_value
261                ):
262                    (
263                        self.session.sspcontext,
264                        auth_value,
265                        status,
266                    ) = self.session.ssp.GSS_Accept_sec_context(
267                        self.session.sspcontext, req.auth_verifier.auth_value
268                    )
269                    self.session.auth_level = RPC_C_AUTHN_LEVEL(
270                        req.auth_verifier.auth_level
271                    )
272                    self.session.auth_context_id = req.auth_verifier.auth_context_id
273                    if DceRpc5Auth3 in req:
274                        # Auth 3 stops here (no server response) !
275                        if status != 0:
276                            print(conf.color_theme.fail("! DceRpc5Auth3 failed"))
277                        if pad is not None:
278                            self.recv(pad)
279                        return
280                    # auth_verifier here contains the SSP nego packets
281                    # (whereas it usually contains the verifiers)
282                    if auth_value is not None:
283                        hdr.auth_verifier = CommonAuthVerifier(
284                            auth_type=req.auth_verifier.auth_type,
285                            auth_level=req.auth_verifier.auth_level,
286                            auth_context_id=req.auth_verifier.auth_context_id,
287                            auth_value=auth_value,
288                        )
289
290                def get_result(ctx):
291                    name = ctx.transfer_syntaxes[0].sprintf("%if_uuid%")
292                    if name == self.ndr_name:
293                        # Acceptance
294                        return DceRpc5Result(
295                            result=0,
296                            reason=0,
297                            transfer_syntax=DceRpc5TransferSyntax(
298                                if_uuid=ctx.transfer_syntaxes[0].if_uuid,
299                                if_version=ctx.transfer_syntaxes[0].if_version,
300                            ),
301                        )
302                    elif name == "Bind Time Feature Negotiation":
303                        return DceRpc5Result(
304                            result=3,
305                            reason=3,
306                            transfer_syntax=DceRpc5TransferSyntax(
307                                if_uuid="NULL",
308                                if_version=0,
309                            ),
310                        )
311                    else:
312                        # Reject
313                        return DceRpc5Result(
314                            result=2,
315                            reason=2,
316                            transfer_syntax=DceRpc5TransferSyntax(
317                                if_uuid="NULL",
318                                if_version=0,
319                            ),
320                        )
321
322                results = [get_result(x) for x in req.context_elem]
323                if self.port is None:
324                    # Piped
325                    port_spec = (
326                        b"\\\\PIPE\\\\%s\0"
327                        % self.session.rpc_bind_interface.name.encode()
328                    )
329                else:
330                    # IP
331                    port_spec = str(self.port).encode() + b"\x00"
332                if DceRpc5Bind in req:
333                    cls = DceRpc5BindAck
334                else:
335                    cls = DceRpc5AlterContextResp
336                self.queue.extend(
337                    self.session.out_pkt(
338                        hdr
339                        / cls(
340                            assoc_group_id=RandShort(),
341                            sec_addr=DceRpc5PortAny(
342                                port_spec=port_spec,
343                            ),
344                            results=results,
345                        ),
346                    )
347                )
348                if self.verb:
349                    print(
350                        conf.color_theme.success(
351                            f">> {cls.__name__} {self.session.rpc_bind_interface.name}"
352                            f" is on port '{port_spec.decode()}' using {self.ndr_name}"
353                        )
354                    )
355        elif DceRpc5Request in req:
356            if self.verb:
357                print(
358                    conf.color_theme.opening(
359                        "<< REQUEST: %s"
360                        % req[DceRpc5Request].payload.__class__.__name__
361                    )
362                )
363            # Can be any RPC request !
364            resp = self.make_reply(req)
365            if resp:
366                self.queue.extend(
367                    self.session.out_pkt(
368                        hdr
369                        / DceRpc5Response(
370                            alloc_hint=len(resp),
371                            cont_id=req.cont_id,
372                        )
373                        / resp,
374                    )
375                )
376                if self.verb:
377                    print(
378                        conf.color_theme.success(
379                            ">> RESPONSE: %s" % (resp.__class__.__name__)
380                        )
381                    )
382        # If there was padding, process the second frag
383        if pad is not None:
384            self.recv(pad)
385
386    def get_response(self):
387        try:
388            return self.queue.popleft()
389        except IndexError:
390            return None
391
392    # Endpoint mapper
393
394    @answer.__func__(ept_map_Request)  # hack for Python <= 3.9
395    def ept_map(self, req):
396        """
397        Answer to ept_map_Request.
398        """
399        if self.transport != DCERPC_Transport.NCACN_IP_TCP:
400            raise ValueError("Unimplemented")
401
402        tower = protocol_tower_t(
403            req[ept_map_Request].valueof("map_tower.tower_octet_string")
404        )
405        uuid = tower.floors[0].uuid
406        if_version = (tower.floors[0].rhs << 16) | tower.floors[0].version
407
408        # Check for results in our portmap
409        port = None
410        if (uuid, if_version) in DCE_RPC_INTERFACES:
411            interface = DCE_RPC_INTERFACES[(uuid, if_version)]
412            if interface in self.portmap:
413                port = self.portmap[interface]
414
415        if port is not None:
416            # Found result
417            resp_tower = twr_p_t(
418                tower_octet_string=bytes(
419                    protocol_tower_t(
420                        floors=[
421                            tower.floors[0],  # UUID
422                            tower.floors[1],  # NDR version
423                            tower.floors[2],  # RPC version
424                            prot_and_addr_t(
425                                lhs_length=1,
426                                protocol_identifier="NCACN_IP_TCP",
427                                rhs_length=2,
428                                rhs=port,
429                            ),
430                            prot_and_addr_t(
431                                lhs_length=1,
432                                protocol_identifier="IP",
433                                rhs_length=4,
434                                rhs=self.local_ip or "0.0.0.0",
435                            ),
436                        ]
437                    )
438                )
439            )
440            resp = ept_map_Response(ITowers=[resp_tower], ndr64=self.ndr64)
441            resp.ITowers.max_count = req.max_towers  # ugh
442        else:
443            # No result found
444            pass
445        return resp
446