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