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