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