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 client as per [MS-RPCE] 8""" 9 10import uuid 11import socket 12 13from scapy.config import conf 14 15from scapy.layers.dcerpc import ( 16 DceRpc5, 17 DceRpc5AlterContext, 18 DceRpc5AlterContextResp, 19 DceRpc5Auth3, 20 DceRpc5Bind, 21 DceRpc5BindAck, 22 DceRpc5BindNak, 23 DceRpc5Context, 24 DceRpc5Fault, 25 DceRpc5Request, 26 DceRpc5Response, 27 DceRpc5AbstractSyntax, 28 DceRpc5TransferSyntax, 29 DceRpcSocket, 30 DCERPC_Transport, 31 find_dcerpc_interface, 32 CommonAuthVerifier, 33 DCE_C_AUTHN_LEVEL, 34 # NDR 35 NDRPointer, 36 NDRContextHandle, 37) 38from scapy.layers.gssapi import ( 39 SSP, 40 GSS_S_FAILURE, 41 GSS_S_COMPLETE, 42 GSS_S_CONTINUE_NEEDED, 43 GSS_C_FLAGS, 44) 45from scapy.layers.smb2 import STATUS_ERREF 46from scapy.layers.smbclient import ( 47 SMB_RPC_SOCKET, 48) 49 50# RPC 51from scapy.layers.msrpce.ept import ( 52 ept_map_Request, 53 ept_map_Response, 54 twr_p_t, 55 protocol_tower_t, 56 prot_and_addr_t, 57 UUID, 58) 59 60 61class DCERPC_Client(object): 62 """ 63 A basic DCE/RPC client 64 65 :param ndr64: Should ask for NDR64 when binding (default False) 66 """ 67 68 def __init__(self, transport, ndr64=False, ndrendian="little", verb=True, **kwargs): 69 self.sock = None 70 self.transport = transport 71 assert isinstance( 72 transport, DCERPC_Transport 73 ), "transport must be from DCERPC_Transport" 74 self.call_id = 0 75 self.cont_id = 0 76 self.ndr64 = ndr64 77 self.ndrendian = ndrendian 78 self.verb = verb 79 self.auth_level = kwargs.pop("auth_level", DCE_C_AUTHN_LEVEL.NONE) 80 self.auth_context_id = kwargs.pop("auth_context_id", 0) 81 self.ssp = kwargs.pop("ssp", None) # type: SSP 82 self.sspcontext = None 83 self.dcesockargs = kwargs 84 85 @classmethod 86 def from_smblink(cls, smbcli, smb_kwargs={}, **kwargs): 87 """ 88 Build a DCERPC_Client from a SMB_Client.smblink directly 89 """ 90 client = DCERPC_Client(DCERPC_Transport.NCACN_NP, **kwargs) 91 sock = client.smbrpcsock = SMB_RPC_SOCKET(smbcli, **smb_kwargs) 92 client.sock = DceRpcSocket( 93 sock, 94 DceRpc5, 95 ssp=client.ssp, 96 auth_level=client.auth_level, 97 auth_context_id=client.auth_context_id, 98 **client.dcesockargs, 99 ) 100 return client 101 102 def connect(self, ip, port=None, timeout=5, smb_kwargs={}): 103 """ 104 Initiate a connection 105 """ 106 if port is None: 107 if self.transport == DCERPC_Transport.NCACN_IP_TCP: # IP/TCP 108 port = 135 109 elif self.transport == DCERPC_Transport.NCACN_NP: # SMB 110 port = 445 111 else: 112 raise ValueError( 113 "Can't guess the port for transport: %s" % self.transport 114 ) 115 sock = socket.socket() 116 sock.settimeout(timeout) 117 if self.verb: 118 print( 119 "\u2503 Connecting to %s on port %s via %s..." 120 % (ip, port, repr(self.transport)) 121 ) 122 sock.connect((ip, port)) 123 if self.verb: 124 print( 125 conf.color_theme.green( 126 "\u2514 Connected from %s" % repr(sock.getsockname()) 127 ) 128 ) 129 if self.transport == DCERPC_Transport.NCACN_NP: # SMB 130 # We pack the socket into a SMB_RPC_SOCKET 131 sock = self.smbrpcsock = SMB_RPC_SOCKET.from_tcpsock( 132 sock, ssp=self.ssp, **smb_kwargs 133 ) 134 self.sock = DceRpcSocket(sock, DceRpc5, **self.dcesockargs) 135 elif self.transport == DCERPC_Transport.NCACN_IP_TCP: 136 self.sock = DceRpcSocket( 137 sock, 138 DceRpc5, 139 ssp=self.ssp, 140 auth_level=self.auth_level, 141 auth_context_id=self.auth_context_id, 142 **self.dcesockargs, 143 ) 144 145 def close(self): 146 if self.verb: 147 print("X Connection closed\n") 148 self.sock.close() 149 150 def sr1(self, pkt, **kwargs): 151 self.call_id += 1 152 pkt = ( 153 DceRpc5( 154 call_id=self.call_id, 155 pfc_flags="PFC_FIRST_FRAG+PFC_LAST_FRAG", 156 endian=self.ndrendian, 157 auth_verifier=kwargs.pop("auth_verifier", None), 158 ) 159 / pkt 160 ) 161 if "pfc_flags" in kwargs: 162 pkt.pfc_flags = kwargs.pop("pfc_flags") 163 return self.sock.sr1(pkt, verbose=0, **kwargs) 164 165 def send(self, pkt, **kwargs): 166 self.call_id += 1 167 pkt = ( 168 DceRpc5( 169 call_id=self.call_id, 170 pfc_flags="PFC_FIRST_FRAG+PFC_LAST_FRAG", 171 endian=self.ndrendian, 172 auth_verifier=kwargs.pop("auth_verifier", None), 173 ) 174 / pkt 175 ) 176 if "pfc_flags" in kwargs: 177 pkt.pfc_flags = kwargs.pop("pfc_flags") 178 return self.sock.send(pkt, **kwargs) 179 180 def sr1_req(self, pkt, **kwargs): 181 if self.verb: 182 print(conf.color_theme.opening(">> REQUEST: %s" % pkt.__class__.__name__)) 183 # Send/receive 184 resp = self.sr1( 185 DceRpc5Request(cont_id=self.cont_id, alloc_hint=len(pkt)) / pkt, 186 **kwargs, 187 ) 188 if DceRpc5Response in resp: 189 if self.verb: 190 print( 191 conf.color_theme.success( 192 "<< RESPONSE: %s" 193 % (resp[DceRpc5Response].payload.__class__.__name__) 194 ) 195 ) 196 return resp[DceRpc5Response].payload 197 else: 198 if self.verb: 199 if DceRpc5Fault in resp: 200 if resp[DceRpc5Fault].payload and not isinstance( 201 resp[DceRpc5Fault].payload, conf.raw_layer 202 ): 203 resp[DceRpc5Fault].payload.show() 204 if resp.status == 0x00000005: 205 print(conf.color_theme.fail("! nca_s_fault_access_denied")) 206 elif resp.status == 0x00000721: 207 print( 208 conf.color_theme.fail( 209 "! nca_s_fault_sec_pkg_error " 210 "(error in checksum/encryption)" 211 ) 212 ) 213 else: 214 print( 215 conf.color_theme.fail( 216 "! %s" % STATUS_ERREF.get(resp.status, "Failure") 217 ) 218 ) 219 resp.show() 220 return 221 return resp 222 223 def get_bind_context(self, interface): 224 return [ 225 DceRpc5Context( 226 cont_id=0, 227 abstract_syntax=DceRpc5AbstractSyntax( 228 if_uuid=interface.uuid, 229 if_version=interface.if_version, 230 ), 231 transfer_syntaxes=[ 232 DceRpc5TransferSyntax( 233 # NDR 2.0 32-bit 234 if_uuid="NDR 2.0", 235 if_version=2, 236 ) 237 ], 238 ), 239 ] + ( 240 [ 241 DceRpc5Context( 242 cont_id=1, 243 abstract_syntax=DceRpc5AbstractSyntax( 244 if_uuid=interface.uuid, 245 if_version=interface.if_version, 246 ), 247 transfer_syntaxes=[ 248 DceRpc5TransferSyntax( 249 # NDR64 250 if_uuid="NDR64", 251 if_version=1, 252 ) 253 ], 254 ), 255 DceRpc5Context( 256 cont_id=2, 257 abstract_syntax=DceRpc5AbstractSyntax( 258 if_uuid=interface.uuid, 259 if_version=interface.if_version, 260 ), 261 transfer_syntaxes=[ 262 DceRpc5TransferSyntax( 263 if_uuid=uuid.UUID("6cb71c2c-9812-4540-0300-000000000000"), 264 if_version=1, 265 ) 266 ], 267 ), 268 ] 269 if self.ndr64 270 else [] 271 ) 272 273 def _bind(self, interface, reqcls, respcls): 274 # Build a security context: [MS-RPCE] 3.3.1.5.2 275 if self.verb: 276 print( 277 conf.color_theme.opening( 278 ">> %s on %s" % (reqcls.__name__, interface) 279 + (" (with %s)" % self.ssp.__class__.__name__ if self.ssp else "") 280 ) 281 ) 282 if not self.ssp or ( 283 self.transport == DCERPC_Transport.NCACN_NP 284 and self.auth_level < DCE_C_AUTHN_LEVEL.PKT_INTEGRITY 285 ): 286 # NCACN_NP = SMB without INTEGRITY/PRIVACY does not bind the RPC securely, 287 # again as it has already authenticated during the SMB Session Setup 288 resp = self.sr1( 289 reqcls(context_elem=self.get_bind_context(interface)), 290 auth_verifier=None, 291 ) 292 status = GSS_S_COMPLETE 293 else: 294 # Perform authentication 295 self.sspcontext, token, status = self.ssp.GSS_Init_sec_context( 296 self.sspcontext, 297 req_flags=( 298 # SSPs need to be instantiated with some special flags 299 # for DCE/RPC usages. 300 GSS_C_FLAGS.GSS_C_DCE_STYLE 301 | GSS_C_FLAGS.GSS_C_REPLAY_FLAG 302 | GSS_C_FLAGS.GSS_C_SEQUENCE_FLAG 303 | GSS_C_FLAGS.GSS_C_MUTUAL_FLAG 304 | ( 305 GSS_C_FLAGS.GSS_C_INTEG_FLAG 306 if self.auth_level >= DCE_C_AUTHN_LEVEL.PKT_INTEGRITY 307 else 0 308 ) 309 | ( 310 GSS_C_FLAGS.GSS_C_CONF_FLAG 311 if self.auth_level >= DCE_C_AUTHN_LEVEL.PKT_PRIVACY 312 else 0 313 ) 314 ), 315 ) 316 if status not in [GSS_S_CONTINUE_NEEDED, GSS_S_COMPLETE]: 317 # Authentication failed. 318 self.sspcontext.clifailure() 319 return False 320 resp = self.sr1( 321 reqcls(context_elem=self.get_bind_context(interface)), 322 auth_verifier=( 323 None 324 if not self.sspcontext 325 else CommonAuthVerifier( 326 auth_type=self.ssp.auth_type, 327 auth_level=self.auth_level, 328 auth_context_id=self.auth_context_id, 329 auth_value=token, 330 ) 331 ), 332 pfc_flags=( 333 "PFC_FIRST_FRAG+PFC_LAST_FRAG" 334 + ( 335 # If the SSP supports "Header Signing", advertise it 336 "+PFC_SUPPORT_HEADER_SIGN" 337 if self.ssp is not None 338 and self.sock.session.support_header_signing 339 else "" 340 ) 341 ), 342 ) 343 if respcls not in resp: 344 token = None 345 status = GSS_S_FAILURE 346 else: 347 # Call the underlying SSP 348 self.sspcontext, token, status = self.ssp.GSS_Init_sec_context( 349 self.sspcontext, val=resp.auth_verifier.auth_value 350 ) 351 if status in [GSS_S_CONTINUE_NEEDED, GSS_S_COMPLETE]: 352 # Authentication should continue 353 if token and self.ssp.LegsAmount(self.sspcontext) % 2 == 1: 354 # AUTH 3 for certain SSPs (e.g. NTLM) 355 # "The server MUST NOT respond to an rpc_auth_3 PDU" 356 self.send( 357 DceRpc5Auth3(), 358 auth_verifier=CommonAuthVerifier( 359 auth_type=self.ssp.auth_type, 360 auth_level=self.auth_level, 361 auth_context_id=self.auth_context_id, 362 auth_value=token, 363 ), 364 ) 365 status = GSS_S_COMPLETE 366 else: 367 # Authentication can continue in two ways: 368 # - through DceRpc5Auth3 (e.g. NTLM) 369 # - through DceRpc5AlterContext (e.g. Kerberos) 370 while token: 371 respcls = DceRpc5AlterContextResp 372 resp = self.sr1( 373 DceRpc5AlterContext( 374 context_elem=self.get_bind_context(interface) 375 ), 376 auth_verifier=CommonAuthVerifier( 377 auth_type=self.ssp.auth_type, 378 auth_level=self.auth_level, 379 auth_context_id=self.auth_context_id, 380 auth_value=token, 381 ), 382 ) 383 if respcls not in resp: 384 status = GSS_S_FAILURE 385 break 386 if resp.auth_verifier is None: 387 status = GSS_S_COMPLETE 388 break 389 self.sspcontext, token, status = self.ssp.GSS_Init_sec_context( 390 self.sspcontext, val=resp.auth_verifier.auth_value 391 ) 392 # Check context acceptance 393 if ( 394 status == GSS_S_COMPLETE 395 and respcls in resp 396 and any(x.result == 0 for x in resp.results[: int(self.ndr64) + 1]) 397 ): 398 self.call_id = 0 # reset call id 399 port = resp.sec_addr.port_spec.decode() 400 ndr = self.sock.session.ndr64 and "NDR64" or "NDR32" 401 self.cont_id = int(self.sock.session.ndr64) # ctx 0 for NDR32, 1 for NDR64 402 if self.verb: 403 print( 404 conf.color_theme.success( 405 f"<< {respcls.__name__} port '{port}' using {ndr}" 406 ) 407 ) 408 self.sock.session.sspcontext = self.sspcontext 409 return True 410 else: 411 if self.verb: 412 if DceRpc5BindNak in resp: 413 err_msg = resp.sprintf( 414 "reject_reason: %DceRpc5BindNak.provider_reject_reason%" 415 ) 416 print(conf.color_theme.fail("! Bind_nak (%s)" % err_msg)) 417 if DceRpc5BindNak in resp: 418 if resp[DceRpc5BindNak].payload and not isinstance( 419 resp[DceRpc5BindNak].payload, conf.raw_layer 420 ): 421 resp[DceRpc5BindNak].payload.show() 422 elif DceRpc5Fault in resp: 423 if resp.status == 0x00000005: 424 print(conf.color_theme.fail("! nca_s_fault_access_denied")) 425 elif resp.status == 0x00000721: 426 print( 427 conf.color_theme.fail( 428 "! nca_s_fault_sec_pkg_error " 429 "(error in checksum/encryption)" 430 ) 431 ) 432 else: 433 print( 434 conf.color_theme.fail( 435 "! %s" % STATUS_ERREF.get(resp.status, "Failure") 436 ) 437 ) 438 resp.show() 439 if DceRpc5Fault in resp: 440 if resp[DceRpc5Fault].payload and not isinstance( 441 resp[DceRpc5Fault].payload, conf.raw_layer 442 ): 443 resp[DceRpc5Fault].payload.show() 444 else: 445 print(conf.color_theme.fail("! Failure")) 446 resp.show() 447 return False 448 449 def bind(self, interface): 450 """ 451 Bind the client to an interface 452 """ 453 return self._bind(interface, DceRpc5Bind, DceRpc5BindAck) 454 455 def alter_context(self, interface): 456 """ 457 Alter context: post-bind context negotiation 458 """ 459 return self._bind(interface, DceRpc5AlterContext, DceRpc5AlterContextResp) 460 461 def bind_or_alter(self, interface): 462 """ 463 Bind the client to an interface or alter the context if already bound 464 """ 465 if not self.sock.session.rpc_bind_interface: 466 # No interface is bound 467 self.bind(interface) 468 else: 469 # An interface is already bound 470 self.alter_context(interface) 471 472 def open_smbpipe(self, name): 473 """ 474 Open a certain filehandle with the SMB automaton 475 """ 476 self.ipc_tid = self.smbrpcsock.tree_connect("IPC$") 477 self.smbrpcsock.open_pipe(name) 478 479 def close_smbpipe(self): 480 """ 481 Close the previously opened pipe 482 """ 483 self.smbrpcsock.set_TID(self.ipc_tid) 484 self.smbrpcsock.close_pipe() 485 self.smbrpcsock.tree_disconnect() 486 487 def connect_and_bind( 488 self, 489 ip, 490 interface, 491 port=None, 492 smb_kwargs={}, 493 ): 494 """ 495 Asks the Endpoint Mapper what address to use to connect to the interface, 496 then uses connect() followed by a bind() 497 """ 498 if self.transport == DCERPC_Transport.NCACN_IP_TCP: 499 # IP/TCP 500 # 1. ask the endpoint mapper (port 135) for the IP:PORT 501 endpoints = get_endpoint( 502 ip, 503 interface, 504 ndrendian=self.ndrendian, 505 verb=self.verb, 506 ) 507 if endpoints: 508 ip, port = endpoints[0] 509 else: 510 return 511 # 2. Connect to that IP:PORT 512 self.connect(ip, port=port) 513 elif self.transport == DCERPC_Transport.NCACN_NP: 514 # SMB 515 # 1. ask the endpoint mapper (over SMB) for the namedpipe 516 endpoints = get_endpoint( 517 ip, 518 interface, 519 transport=self.transport, 520 ndrendian=self.ndrendian, 521 verb=self.verb, 522 smb_kwargs=smb_kwargs, 523 ) 524 if endpoints: 525 pipename = endpoints[0].lstrip("\\pipe\\") 526 else: 527 return 528 # 2. connect to the SMB server 529 self.connect(ip, port=port, smb_kwargs=smb_kwargs) 530 # 3. open the new named pipe 531 self.open_smbpipe(pipename) 532 # Bind in RPC 533 self.bind(interface) 534 535 def epm_map(self, interface): 536 """ 537 Calls ept_map (the EndPoint Manager) 538 """ 539 if self.ndr64: 540 ndr_uuid = "NDR64" 541 ndr_version = 1 542 else: 543 ndr_uuid = "NDR 2.0" 544 ndr_version = 2 545 pkt = self.sr1_req( 546 ept_map_Request( 547 obj=NDRPointer( 548 referent_id=1, 549 value=UUID( 550 Data1=0, 551 Data2=0, 552 Data3=0, 553 Data4=None, 554 ), 555 ), 556 map_tower=NDRPointer( 557 referent_id=2, 558 value=twr_p_t( 559 tower_octet_string=bytes( 560 protocol_tower_t( 561 floors=[ 562 prot_and_addr_t( 563 lhs_length=19, 564 protocol_identifier=0xD, 565 uuid=interface.uuid, 566 version=interface.major_version, 567 rhs_length=2, 568 rhs=interface.minor_version, 569 ), 570 prot_and_addr_t( 571 lhs_length=19, 572 protocol_identifier=0xD, 573 uuid=ndr_uuid, 574 version=ndr_version, 575 rhs_length=2, 576 rhs=0, 577 ), 578 prot_and_addr_t( 579 lhs_length=1, 580 protocol_identifier="RPC connection-oriented protocol", # noqa: E501 581 rhs_length=2, 582 rhs=0, 583 ), 584 { 585 DCERPC_Transport.NCACN_IP_TCP: ( 586 prot_and_addr_t( 587 lhs_length=1, 588 protocol_identifier="NCACN_IP_TCP", 589 rhs_length=2, 590 rhs=135, 591 ) 592 ), 593 DCERPC_Transport.NCACN_NP: ( 594 prot_and_addr_t( 595 lhs_length=1, 596 protocol_identifier="NCACN_NP", 597 rhs_length=2, 598 rhs=b"0\x00", 599 ) 600 ), 601 }[self.transport], 602 { 603 DCERPC_Transport.NCACN_IP_TCP: ( 604 prot_and_addr_t( 605 lhs_length=1, 606 protocol_identifier="IP", 607 rhs_length=4, 608 rhs="0.0.0.0", 609 ) 610 ), 611 DCERPC_Transport.NCACN_NP: ( 612 prot_and_addr_t( 613 lhs_length=1, 614 protocol_identifier="NCACN_NB", 615 rhs_length=10, 616 rhs=b"127.0.0.1\x00", 617 ) 618 ), 619 }[self.transport], 620 ], 621 ) 622 ), 623 ), 624 ), 625 entry_handle=NDRContextHandle( 626 attributes=0, 627 uuid=b"\x00" * 16, 628 ), 629 max_towers=500, 630 ndr64=self.ndr64, 631 ndrendian=self.ndrendian, 632 ) 633 ) 634 if pkt and ept_map_Response in pkt: 635 status = pkt[ept_map_Response].status 636 # [MS-RPCE] sect 2.2.1.2.5 637 if status == 0x00000000: 638 towers = [ 639 protocol_tower_t(x.value.tower_octet_string) 640 for x in pkt[ept_map_Response].ITowers.value[0].value 641 ] 642 # Let's do some checks to know we know what we're doing 643 endpoints = [] 644 for t in towers: 645 if t.floors[0].uuid != interface.uuid: 646 if self.verb: 647 print( 648 conf.color_theme.fail( 649 "! Server answered with a different interface." 650 ) 651 ) 652 raise ValueError 653 if t.floors[1].sprintf("%uuid%") != ndr_uuid: 654 if self.verb: 655 print( 656 conf.color_theme.fail( 657 "! Server answered with a different NDR version." 658 ) 659 ) 660 raise ValueError 661 if self.transport == DCERPC_Transport.NCACN_IP_TCP: 662 endpoints.append((t.floors[4].rhs, t.floors[3].rhs)) 663 elif self.transport == DCERPC_Transport.NCACN_NP: 664 endpoints.append(t.floors[3].rhs.rstrip(b"\x00").decode()) 665 return endpoints 666 elif status == 0x16C9A0D6: 667 if self.verb: 668 pkt.show() 669 print( 670 conf.color_theme.fail( 671 "! Server errored: 'There are no elements that satisfy" 672 " the specified search criteria'." 673 ) 674 ) 675 raise ValueError 676 print(conf.color_theme.fail("! Failure.")) 677 if pkt: 678 pkt.show() 679 raise ValueError("EPM Map failed") 680 681 682def get_endpoint( 683 ip, 684 interface, 685 transport=DCERPC_Transport.NCACN_IP_TCP, 686 ndrendian="little", 687 verb=True, 688 smb_kwargs={}, 689): 690 """ 691 Call the endpoint mapper on a remote IP to find an interface 692 693 :param ip: 694 :param interface: 695 :param mode: 696 :param verb: 697 698 :return: a list of connection tuples for this interface 699 """ 700 client = DCERPC_Client( 701 transport, 702 ndr64=False, 703 ndrendian=ndrendian, 704 verb=verb, 705 ) # EPM only works with NDR32 706 client.connect(ip, smb_kwargs=smb_kwargs) 707 if transport == DCERPC_Transport.NCACN_NP: # SMB 708 client.open_smbpipe("epmapper") 709 client.bind(find_dcerpc_interface("ept")) 710 endpoints = client.epm_map(interface) 711 client.close() 712 return endpoints 713