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