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