1# SPDX-License-Identifier: GPL-2.0-only 2# This file is part of Scapy 3# See https://scapy.net/ for more information 4# Copyright (C) Gabriel Potter 5 6""" 7SMB 1 / 2 Client Automaton 8 9 10.. note:: 11 You will find more complete documentation for this layer over at 12 `SMB <https://scapy.readthedocs.io/en/latest/layers/smb.html#client>`_ 13""" 14 15import io 16import os 17import pathlib 18import socket 19import time 20import threading 21 22from scapy.automaton import ATMT, Automaton, ObjectPipe 23from scapy.base_classes import Net 24from scapy.config import conf 25from scapy.error import Scapy_Exception 26from scapy.fields import UTCTimeField 27from scapy.supersocket import SuperSocket 28from scapy.utils import ( 29 CLIUtil, 30 pretty_list, 31 human_size, 32 valid_ip, 33 valid_ip6, 34) 35from scapy.volatile import RandUUID 36 37from scapy.layers.dcerpc import NDRUnion, find_dcerpc_interface 38from scapy.layers.gssapi import ( 39 GSS_S_COMPLETE, 40 GSS_S_CONTINUE_NEEDED, 41 GSS_C_FLAGS, 42) 43from scapy.layers.inet6 import Net6 44from scapy.layers.kerberos import ( 45 KerberosSSP, 46 krb_as_and_tgs, 47 _parse_upn, 48) 49from scapy.layers.msrpce.raw.ms_srvs import ( 50 LPSHARE_ENUM_STRUCT, 51 NetrShareEnum_Request, 52 NetrShareEnum_Response, 53 SHARE_INFO_1_CONTAINER, 54) 55from scapy.layers.ntlm import ( 56 NTLMSSP, 57 MD4le, 58) 59from scapy.layers.smb import ( 60 SMBNegotiate_Request, 61 SMBNegotiate_Response_Extended_Security, 62 SMBNegotiate_Response_Security, 63 SMBSession_Null, 64 SMBSession_Setup_AndX_Request, 65 SMBSession_Setup_AndX_Request_Extended_Security, 66 SMBSession_Setup_AndX_Response, 67 SMBSession_Setup_AndX_Response_Extended_Security, 68 SMB_Dialect, 69 SMB_Header, 70) 71from scapy.layers.smb2 import ( 72 DirectTCP, 73 FileAllInformation, 74 FileIdBothDirectoryInformation, 75 SMB_DIALECTS, 76 SMB2_Change_Notify_Request, 77 SMB2_Change_Notify_Response, 78 SMB2_Close_Request, 79 SMB2_Close_Response, 80 SMB2_Create_Context, 81 SMB2_CREATE_DURABLE_HANDLE_REQUEST_V2, 82 SMB2_CREATE_REQUEST_LEASE_V2, 83 SMB2_CREATE_REQUEST_LEASE, 84 SMB2_Create_Request, 85 SMB2_Create_Response, 86 SMB2_Encryption_Capabilities, 87 SMB2_ENCRYPTION_CIPHERS, 88 SMB2_Error_Response, 89 SMB2_Header, 90 SMB2_IOCTL_Request, 91 SMB2_IOCTL_Response, 92 SMB2_Negotiate_Context, 93 SMB2_Negotiate_Protocol_Request, 94 SMB2_Negotiate_Protocol_Response, 95 SMB2_Netname_Negotiate_Context_ID, 96 SMB2_Preauth_Integrity_Capabilities, 97 SMB2_Query_Directory_Request, 98 SMB2_Query_Directory_Response, 99 SMB2_Query_Info_Request, 100 SMB2_Query_Info_Response, 101 SMB2_Read_Request, 102 SMB2_Read_Response, 103 SMB2_Session_Setup_Request, 104 SMB2_Session_Setup_Response, 105 SMB2_SIGNING_ALGORITHMS, 106 SMB2_Signing_Capabilities, 107 SMB2_Tree_Connect_Request, 108 SMB2_Tree_Connect_Response, 109 SMB2_Tree_Disconnect_Request, 110 SMB2_Tree_Disconnect_Response, 111 SMB2_Write_Request, 112 SMB2_Write_Response, 113 SMBStreamSocket, 114 SRVSVC_SHARE_TYPES, 115 STATUS_ERREF, 116) 117from scapy.layers.spnego import SPNEGOSSP 118 119 120class SMB_Client(Automaton): 121 """ 122 SMB client automaton 123 124 :param sock: the SMBStreamSocket to use 125 :param ssp: the SSP to use 126 127 All other options (in caps) are optional, and SMB specific: 128 129 :param REQUIRE_SIGNATURE: set 'Require Signature' 130 :param MIN_DIALECT: minimum SMB dialect. Defaults to 0x0202 (2.0.2) 131 :param MAX_DIALECT: maximum SMB dialect. Defaults to 0x0311 (3.1.1) 132 :param DIALECTS: list of supported SMB2 dialects. 133 Constructed from MIN_DIALECT, MAX_DIALECT otherwise. 134 """ 135 136 port = 445 137 cls = DirectTCP 138 139 def __init__(self, sock, ssp=None, *args, **kwargs): 140 # Various SMB client arguments 141 self.EXTENDED_SECURITY = kwargs.pop("EXTENDED_SECURITY", True) 142 self.USE_SMB1 = kwargs.pop("USE_SMB1", False) 143 self.REQUIRE_SIGNATURE = kwargs.pop("REQUIRE_SIGNATURE", False) 144 self.RETRY = kwargs.pop("RETRY", 0) # optionally: retry n times session setup 145 self.SMB2 = kwargs.pop("SMB2", False) # optionally: start directly in SMB2 146 self.SERVER_NAME = kwargs.pop("SERVER_NAME", "") 147 # Store supported dialects 148 if "DIALECTS" in kwargs: 149 self.DIALECTS = kwargs.pop("DIALECTS") 150 else: 151 MIN_DIALECT = kwargs.pop("MIN_DIALECT", 0x0202) 152 self.MAX_DIALECT = kwargs.pop("MAX_DIALECT", 0x0311) 153 self.DIALECTS = sorted( 154 [ 155 x 156 for x in [0x0202, 0x0210, 0x0300, 0x0302, 0x0311] 157 if x >= MIN_DIALECT and x <= self.MAX_DIALECT 158 ] 159 ) 160 # Internal Session information 161 self.IsGuest = False 162 self.ErrorStatus = None 163 self.NegotiateCapabilities = None 164 self.GUID = RandUUID()._fix() 165 self.SequenceWindow = (0, 0) # keep track of allowed MIDs 166 self.MaxTransactionSize = 0 167 self.MaxReadSize = 0 168 self.MaxWriteSize = 0 169 if ssp is None: 170 # We got no SSP. Assuming the server allows anonymous 171 ssp = SPNEGOSSP( 172 [ 173 NTLMSSP( 174 UPN="guest", 175 HASHNT=b"", 176 ) 177 ] 178 ) 179 # Initialize 180 kwargs["sock"] = sock 181 Automaton.__init__( 182 self, 183 *args, 184 **kwargs, 185 ) 186 if self.is_atmt_socket: 187 self.smb_sock_ready = threading.Event() 188 # Set session options 189 self.session.ssp = ssp 190 self.session.SecurityMode = kwargs.pop( 191 "SECURITY_MODE", 192 3 if self.REQUIRE_SIGNATURE else int(bool(ssp)), 193 ) 194 self.session.Dialect = self.MAX_DIALECT 195 196 @classmethod 197 def from_tcpsock(cls, sock, **kwargs): 198 return cls.smblink( 199 None, 200 SMBStreamSocket(sock, DirectTCP), 201 **kwargs, 202 ) 203 204 @property 205 def session(self): 206 # session shorthand 207 return self.sock.session 208 209 def send(self, pkt): 210 # Calculate what CreditCharge to send. 211 if self.session.Dialect > 0x0202 and isinstance(pkt.payload, SMB2_Header): 212 # [MS-SMB2] sect 3.2.4.1.5 213 typ = type(pkt.payload.payload) 214 if typ is SMB2_Negotiate_Protocol_Request: 215 # See [MS-SMB2] 3.2.4.1.2 note 216 pkt.CreditCharge = 0 217 elif typ in [ 218 SMB2_Read_Request, 219 SMB2_Write_Request, 220 SMB2_IOCTL_Request, 221 SMB2_Query_Directory_Request, 222 SMB2_Change_Notify_Request, 223 SMB2_Query_Info_Request, 224 ]: 225 # [MS-SMB2] 3.1.5.2 226 # "For READ, WRITE, IOCTL, and QUERY_DIRECTORY requests" 227 # "CHANGE_NOTIFY, QUERY_INFO, or SET_INFO" 228 if typ == SMB2_Read_Request: 229 Length = pkt.payload.Length 230 elif typ == SMB2_Write_Request: 231 Length = len(pkt.payload.Data) 232 elif typ == SMB2_IOCTL_Request: 233 # [MS-SMB2] 3.3.5.15 234 Length = max(len(pkt.payload.Input), pkt.payload.MaxOutputResponse) 235 elif typ in [ 236 SMB2_Query_Directory_Request, 237 SMB2_Change_Notify_Request, 238 SMB2_Query_Info_Request, 239 ]: 240 Length = pkt.payload.OutputBufferLength 241 else: 242 raise RuntimeError("impossible case") 243 pkt.CreditCharge = 1 + (Length - 1) // 65536 244 else: 245 # "For all other requests, the client MUST set CreditCharge to 1" 246 pkt.CreditCharge = 1 247 # [MS-SMB2] 3.2.4.1.2 248 pkt.CreditRequest = pkt.CreditCharge + 1 # this code is a bit lazy 249 # Get first available message ID: [MS-SMB2] 3.2.4.1.3 and 3.2.4.1.5 250 pkt.MID = self.SequenceWindow[0] 251 return super(SMB_Client, self).send(pkt) 252 253 @ATMT.state(initial=1) 254 def BEGIN(self): 255 pass 256 257 @ATMT.condition(BEGIN) 258 def continue_smb2(self): 259 if self.SMB2: # Directly started in SMB2 260 self.smb_header = DirectTCP() / SMB2_Header(PID=0xFEFF) 261 raise self.SMB2_NEGOTIATE() 262 263 @ATMT.condition(BEGIN, prio=1) 264 def send_negotiate(self): 265 raise self.SENT_NEGOTIATE() 266 267 @ATMT.action(send_negotiate) 268 def on_negotiate(self): 269 # [MS-SMB2] sect 3.2.4.2.2.1 - Multi-Protocol Negotiate 270 self.smb_header = DirectTCP() / SMB_Header( 271 Flags2=( 272 "LONG_NAMES+EAS+NT_STATUS+UNICODE+" 273 "SMB_SECURITY_SIGNATURE+EXTENDED_SECURITY" 274 ), 275 TID=0xFFFF, 276 PIDLow=0xFEFF, 277 UID=0, 278 MID=0, 279 ) 280 if self.EXTENDED_SECURITY: 281 self.smb_header.Flags2 += "EXTENDED_SECURITY" 282 pkt = self.smb_header.copy() / SMBNegotiate_Request( 283 Dialects=[ 284 SMB_Dialect(DialectString=x) 285 for x in [ 286 "PC NETWORK PROGRAM 1.0", 287 "LANMAN1.0", 288 "Windows for Workgroups 3.1a", 289 "LM1.2X002", 290 "LANMAN2.1", 291 "NT LM 0.12", 292 ] 293 + (["SMB 2.002", "SMB 2.???"] if not self.USE_SMB1 else []) 294 ], 295 ) 296 if not self.EXTENDED_SECURITY: 297 pkt.Flags2 -= "EXTENDED_SECURITY" 298 pkt[SMB_Header].Flags2 = ( 299 pkt[SMB_Header].Flags2 300 - "SMB_SECURITY_SIGNATURE" 301 + "SMB_SECURITY_SIGNATURE_REQUIRED+IS_LONG_NAME" 302 ) 303 self.send(pkt) 304 305 @ATMT.state() 306 def SENT_NEGOTIATE(self): 307 pass 308 309 @ATMT.state() 310 def SMB2_NEGOTIATE(self): 311 pass 312 313 @ATMT.condition(SMB2_NEGOTIATE) 314 def send_negotiate_smb2(self): 315 raise self.SENT_NEGOTIATE() 316 317 @ATMT.action(send_negotiate_smb2) 318 def on_negotiate_smb2(self): 319 # [MS-SMB2] sect 3.2.4.2.2.2 - SMB2-Only Negotiate 320 pkt = self.smb_header.copy() / SMB2_Negotiate_Protocol_Request( 321 Dialects=self.DIALECTS, 322 SecurityMode=self.session.SecurityMode, 323 ) 324 if self.MAX_DIALECT >= 0x0210: 325 # "If the client implements the SMB 2.1 or SMB 3.x dialect, ClientGuid 326 # MUST be set to the global ClientGuid value" 327 pkt.ClientGUID = self.GUID 328 # Capabilities: same as [MS-SMB2] 3.3.5.4 329 self.NegotiateCapabilities = "+".join( 330 [ 331 "DFS", 332 "LEASING", 333 "LARGE_MTU", 334 ] 335 ) 336 if self.MAX_DIALECT >= 0x0300: 337 # "if Connection.Dialect belongs to the SMB 3.x dialect family ..." 338 self.NegotiateCapabilities += "+" + "+".join( 339 [ 340 "MULTI_CHANNEL", 341 "PERSISTENT_HANDLES", 342 "DIRECTORY_LEASING", 343 ] 344 ) 345 if self.MAX_DIALECT >= 0x0300: 346 # "If the client implements the SMB 3.x dialect family, the client MUST 347 # set the Capabilities field as follows" 348 self.NegotiateCapabilities += "+ENCRYPTION" 349 if self.MAX_DIALECT >= 0x0311: 350 # "If the client implements the SMB 3.1.1 dialect, it MUST do" 351 pkt.NegotiateContexts = [ 352 SMB2_Negotiate_Context() 353 / SMB2_Preauth_Integrity_Capabilities( 354 # SHA-512 by default 355 HashAlgorithms=[self.session.PreauthIntegrityHashId], 356 Salt=self.session.Salt, 357 ), 358 SMB2_Negotiate_Context() 359 / SMB2_Encryption_Capabilities( 360 # AES-128-CCM by default 361 Ciphers=[self.session.CipherId], 362 ), 363 # TODO support compression and RDMA 364 SMB2_Negotiate_Context() 365 / SMB2_Netname_Negotiate_Context_ID( 366 NetName=self.SERVER_NAME, 367 ), 368 SMB2_Negotiate_Context() 369 / SMB2_Signing_Capabilities( 370 # AES-128-CCM by default 371 SigningAlgorithms=[self.session.SigningAlgorithmId], 372 ), 373 ] 374 pkt.Capabilities = self.NegotiateCapabilities 375 # Send 376 self.send(pkt) 377 # If required, compute sessions 378 self.session.computeSMBConnectionPreauth( 379 bytes(pkt[SMB2_Header]), # nego request 380 ) 381 382 @ATMT.receive_condition(SENT_NEGOTIATE) 383 def receive_negotiate_response(self, pkt): 384 if ( 385 SMBNegotiate_Response_Extended_Security in pkt 386 or SMB2_Negotiate_Protocol_Response in pkt 387 ): 388 # Extended SMB1 / SMB2 389 try: 390 ssp_blob = pkt.SecurityBlob # eventually SPNEGO server initiation 391 except AttributeError: 392 ssp_blob = None 393 if ( 394 SMB2_Negotiate_Protocol_Response in pkt 395 and pkt.DialectRevision & 0xFF == 0xFF 396 ): 397 # Version is SMB X.??? 398 # [MS-SMB2] 3.2.5.2 399 # If the DialectRevision field in the SMB2 NEGOTIATE Response is 400 # 0x02FF ... the client MUST allocate sequence number 1 from 401 # Connection.SequenceWindow, and MUST set MessageId field of the 402 # SMB2 header to 1. 403 self.SequenceWindow = (1, 1) 404 self.smb_header = DirectTCP() / SMB2_Header(PID=0xFEFF, MID=1) 405 self.SMB2 = True # We're now using SMB2 to talk to the server 406 raise self.SMB2_NEGOTIATE() 407 else: 408 if SMB2_Negotiate_Protocol_Response in pkt: 409 # SMB2 was negotiated ! 410 self.session.Dialect = pkt.DialectRevision 411 # If required, compute sessions 412 self.session.computeSMBConnectionPreauth( 413 bytes(pkt[SMB2_Header]), # nego response 414 ) 415 # Process max sizes 416 self.MaxReadSize = pkt.MaxReadSize 417 self.MaxTransactionSize = pkt.MaxTransactionSize 418 self.MaxWriteSize = pkt.MaxWriteSize 419 # Process NegotiateContext 420 if self.session.Dialect >= 0x0311 and pkt.NegotiateContextsCount: 421 for ngctx in pkt.NegotiateContexts: 422 if ngctx.ContextType == 0x0002: 423 # SMB2_ENCRYPTION_CAPABILITIES 424 self.session.CipherId = SMB2_ENCRYPTION_CIPHERS[ 425 ngctx.Ciphers[0] 426 ] 427 elif ngctx.ContextType == 0x0008: 428 # SMB2_SIGNING_CAPABILITIES 429 self.session.SigningAlgorithmId = ( 430 SMB2_SIGNING_ALGORITHMS[ngctx.SigningAlgorithms[0]] 431 ) 432 self.update_smbheader(pkt) 433 raise self.NEGOTIATED(ssp_blob) 434 elif SMBNegotiate_Response_Security in pkt: 435 # Non-extended SMB1 436 # Never tested. FIXME. probably broken 437 raise self.NEGOTIATED(pkt.Challenge) 438 439 @ATMT.state() 440 def NEGOTIATED(self, ssp_blob=None): 441 # Negotiated ! We now know the Dialect 442 if self.session.Dialect > 0x0202: 443 # [MS-SMB2] sect 3.2.5.1.4 444 self.smb_header.CreditRequest = 1 445 # Begin session establishment 446 ssp_tuple = self.session.ssp.GSS_Init_sec_context( 447 self.session.sspcontext, 448 ssp_blob, 449 req_flags=( 450 GSS_C_FLAGS.GSS_C_MUTUAL_FLAG 451 | ( 452 GSS_C_FLAGS.GSS_C_INTEG_FLAG 453 if self.session.SecurityMode != 0 454 else 0 455 ) 456 ), 457 ) 458 return ssp_tuple 459 460 def update_smbheader(self, pkt): 461 """ 462 Called when receiving a SMB2 packet to update the current smb_header 463 """ 464 # Some values should not be updated when ASYNC 465 if not pkt.Flags.SMB2_FLAGS_ASYNC_COMMAND: 466 # Update IDs 467 self.smb_header.SessionId = pkt.SessionId 468 self.smb_header.TID = pkt.TID 469 self.smb_header.PID = pkt.PID 470 # [MS-SMB2] 3.2.5.1.4 471 self.SequenceWindow = ( 472 self.SequenceWindow[0] + max(pkt.CreditCharge, 1), 473 self.SequenceWindow[1] + pkt.CreditRequest, 474 ) 475 476 # DEV: add a condition on NEGOTIATED with prio=0 477 478 @ATMT.condition(NEGOTIATED, prio=1) 479 def should_send_setup_andx_request(self, ssp_tuple): 480 _, _, negResult = ssp_tuple 481 if negResult not in [GSS_S_COMPLETE, GSS_S_CONTINUE_NEEDED]: 482 raise ValueError("Internal error: the SSP completed with an error.") 483 raise self.SENT_SETUP_ANDX_REQUEST().action_parameters(ssp_tuple) 484 485 @ATMT.state() 486 def SENT_SETUP_ANDX_REQUEST(self): 487 pass 488 489 @ATMT.action(should_send_setup_andx_request) 490 def send_setup_andx_request(self, ssp_tuple): 491 self.session.sspcontext, token, negResult = ssp_tuple 492 if self.SMB2 and negResult == GSS_S_CONTINUE_NEEDED: 493 # New session: force 0 494 self.SessionId = 0 495 if self.SMB2 or self.EXTENDED_SECURITY: 496 # SMB1 extended / SMB2 497 if self.SMB2: 498 # SMB2 499 pkt = self.smb_header.copy() / SMB2_Session_Setup_Request( 500 Capabilities="DFS", 501 SecurityMode=self.session.SecurityMode, 502 ) 503 else: 504 # SMB1 extended 505 pkt = ( 506 self.smb_header.copy() 507 / SMBSession_Setup_AndX_Request_Extended_Security( 508 ServerCapabilities=( 509 "UNICODE+NT_SMBS+STATUS32+LEVEL_II_OPLOCKS+" 510 "DYNAMIC_REAUTH+EXTENDED_SECURITY" 511 ), 512 NativeOS=b"", 513 NativeLanMan=b"", 514 ) 515 ) 516 pkt.SecurityBlob = token 517 else: 518 # Non-extended security. 519 pkt = self.smb_header.copy() / SMBSession_Setup_AndX_Request( 520 ServerCapabilities="UNICODE+NT_SMBS+STATUS32+LEVEL_II_OPLOCKS", 521 NativeOS=b"", 522 NativeLanMan=b"", 523 OEMPassword=b"\0" * 24, 524 UnicodePassword=token, 525 ) 526 self.send(pkt) 527 if self.SMB2: 528 # If required, compute sessions 529 self.session.computeSMBSessionPreauth( 530 bytes(pkt[SMB2_Header]), # session request 531 ) 532 533 @ATMT.receive_condition(SENT_SETUP_ANDX_REQUEST) 534 def receive_setup_andx_response(self, pkt): 535 if ( 536 SMBSession_Null in pkt 537 or SMBSession_Setup_AndX_Response_Extended_Security in pkt 538 or SMBSession_Setup_AndX_Response in pkt 539 ): 540 # SMB1 541 if SMBSession_Null in pkt: 542 # Likely an error 543 raise self.NEGOTIATED() 544 # Logging 545 if pkt.Status != 0 and pkt.Status != 0xC0000016: 546 # Not SUCCESS nor MORE_PROCESSING_REQUIRED: log 547 self.ErrorStatus = pkt.sprintf("%SMB2_Header.Status%") 548 self.debug( 549 lvl=1, 550 msg=conf.color_theme.red( 551 pkt.sprintf("SMB Session Setup Response: %SMB2_Header.Status%") 552 ), 553 ) 554 if self.SMB2: 555 self.update_smbheader(pkt) 556 # Cases depending on the response packet 557 if ( 558 SMBSession_Setup_AndX_Response_Extended_Security in pkt 559 or SMB2_Session_Setup_Response in pkt 560 ): 561 # The server assigns us a SessionId 562 self.smb_header.SessionId = pkt.SessionId 563 # SMB1 extended / SMB2 564 if pkt.Status == 0: # Authenticated 565 if SMB2_Session_Setup_Response in pkt and pkt.SessionFlags.IS_GUEST: 566 # We were 'authenticated' in GUEST 567 self.IsGuest = True 568 raise self.AUTHENTICATED(pkt.SecurityBlob) 569 else: 570 if SMB2_Header in pkt: 571 # If required, compute sessions 572 self.session.computeSMBSessionPreauth( 573 bytes(pkt[SMB2_Header]), # session response 574 ) 575 # Ongoing auth 576 raise self.NEGOTIATED(pkt.SecurityBlob) 577 elif SMBSession_Setup_AndX_Response_Extended_Security in pkt: 578 # SMB1 non-extended 579 pass 580 elif SMB2_Error_Response in pkt: 581 # Authentication failure 582 self.session.sspcontext.clifailure() 583 # Reset Session preauth (SMB 3.1.1) 584 self.session.SessionPreauthIntegrityHashValue = None 585 if not self.RETRY: 586 raise self.AUTH_FAILED() 587 self.debug(lvl=2, msg="RETRY: %s" % self.RETRY) 588 self.RETRY -= 1 589 raise self.NEGOTIATED() 590 591 @ATMT.state(final=1) 592 def AUTH_FAILED(self): 593 self.smb_sock_ready.set() 594 595 @ATMT.state() 596 def AUTHENTICATED(self, ssp_blob=None): 597 self.session.sspcontext, _, status = self.session.ssp.GSS_Init_sec_context( 598 self.session.sspcontext, ssp_blob 599 ) 600 if status != GSS_S_COMPLETE: 601 raise ValueError("Internal error: the SSP completed with an error.") 602 # Authentication was successful 603 self.session.computeSMBSessionKey() 604 if self.IsGuest: 605 # When authenticated in Guest, the sessionkey the client has is invalid 606 self.session.SMBSessionKey = None 607 608 # DEV: add a condition on AUTHENTICATED with prio=0 609 610 @ATMT.condition(AUTHENTICATED, prio=1) 611 def authenticated_post_actions(self): 612 raise self.SOCKET_BIND() 613 614 # Plain SMB Socket 615 616 @ATMT.state() 617 def SOCKET_BIND(self): 618 self.smb_sock_ready.set() 619 620 @ATMT.condition(SOCKET_BIND) 621 def start_smb_socket(self): 622 raise self.SOCKET_MODE_SMB() 623 624 @ATMT.state() 625 def SOCKET_MODE_SMB(self): 626 pass 627 628 @ATMT.receive_condition(SOCKET_MODE_SMB) 629 def incoming_data_received_smb(self, pkt): 630 raise self.SOCKET_MODE_SMB().action_parameters(pkt) 631 632 @ATMT.action(incoming_data_received_smb) 633 def receive_data_smb(self, pkt): 634 resp = pkt[SMB2_Header].payload 635 if isinstance(resp, SMB2_Error_Response): 636 if pkt.Status == 0x00000103: # STATUS_PENDING 637 # answer is coming later.. just wait... 638 return 639 if pkt.Status == 0x0000010B: # STATUS_NOTIFY_CLEANUP 640 # this is a notify cleanup. ignore 641 return 642 self.update_smbheader(pkt) 643 # Add the status to the response as metadata 644 resp.NTStatus = pkt.sprintf("%SMB2_Header.Status%") 645 self.oi.smbpipe.send(resp) 646 647 @ATMT.ioevent(SOCKET_MODE_SMB, name="smbpipe", as_supersocket="smblink") 648 def outgoing_data_received_smb(self, fd): 649 raise self.SOCKET_MODE_SMB().action_parameters(fd.recv()) 650 651 @ATMT.action(outgoing_data_received_smb) 652 def send_data(self, d): 653 self.send(self.smb_header.copy() / d) 654 655 656class SMB_SOCKET(SuperSocket): 657 """ 658 Mid-level wrapper over SMB_Client.smblink that provides some basic SMB 659 client functions, such as tree connect, directory query, etc. 660 """ 661 662 def __init__(self, smbsock, use_ioctl=True, timeout=3): 663 self.ins = smbsock 664 self.timeout = timeout 665 if not self.ins.atmt.smb_sock_ready.wait(timeout=timeout): 666 raise TimeoutError( 667 "The SMB handshake timed out ! (enable debug=1 for logs)" 668 ) 669 if self.ins.atmt.ErrorStatus: 670 raise Scapy_Exception( 671 "SMB Session Setup failed: %s" % self.ins.atmt.ErrorStatus 672 ) 673 674 @classmethod 675 def from_tcpsock(cls, sock, **kwargs): 676 """ 677 Wraps the tcp socket in a SMB_Client.smblink first, then into the 678 SMB_SOCKET/SMB_RPC_SOCKET 679 """ 680 return cls( 681 use_ioctl=kwargs.pop("use_ioctl", True), 682 timeout=kwargs.pop("timeout", 3), 683 smbsock=SMB_Client.from_tcpsock(sock, **kwargs), 684 ) 685 686 @property 687 def session(self): 688 return self.ins.atmt.session 689 690 def set_TID(self, TID): 691 """ 692 Set the TID (Tree ID). 693 This can be called before sending a packet 694 """ 695 self.ins.atmt.smb_header.TID = TID 696 697 def get_TID(self): 698 """ 699 Get the current TID from the underlying socket 700 """ 701 return self.ins.atmt.smb_header.TID 702 703 def tree_connect(self, name): 704 """ 705 Send a TreeConnect request 706 """ 707 resp = self.ins.sr1( 708 SMB2_Tree_Connect_Request( 709 Buffer=[ 710 ( 711 "Path", 712 "\\\\%s\\%s" 713 % ( 714 self.session.sspcontext.ServerHostname, 715 name, 716 ), 717 ) 718 ] 719 ), 720 verbose=False, 721 timeout=self.timeout, 722 ) 723 if not resp: 724 raise ValueError("TreeConnect timed out !") 725 if SMB2_Tree_Connect_Response not in resp: 726 raise ValueError("Failed TreeConnect ! %s" % resp.NTStatus) 727 return self.get_TID() 728 729 def tree_disconnect(self): 730 """ 731 Send a TreeDisconnect request 732 """ 733 resp = self.ins.sr1( 734 SMB2_Tree_Disconnect_Request(), 735 verbose=False, 736 timeout=self.timeout, 737 ) 738 if not resp: 739 raise ValueError("TreeDisconnect timed out !") 740 if SMB2_Tree_Disconnect_Response not in resp: 741 raise ValueError("Failed TreeDisconnect ! %s" % resp.NTStatus) 742 743 def create_request( 744 self, 745 name, 746 mode="r", 747 type="pipe", 748 extra_create_options=[], 749 extra_desired_access=[], 750 ): 751 """ 752 Open a file/pipe by its name 753 754 :param name: the name of the file or named pipe. e.g. 'srvsvc' 755 """ 756 ShareAccess = [] 757 DesiredAccess = [] 758 # Common params depending on the access 759 if "r" in mode: 760 ShareAccess.append("FILE_SHARE_READ") 761 DesiredAccess.extend(["FILE_READ_DATA", "FILE_READ_ATTRIBUTES"]) 762 if "w" in mode: 763 ShareAccess.append("FILE_SHARE_WRITE") 764 DesiredAccess.extend(["FILE_WRITE_DATA", "FILE_WRITE_ATTRIBUTES"]) 765 if "d" in mode: 766 ShareAccess.append("FILE_SHARE_DELETE") 767 # Params depending on the type 768 FileAttributes = [] 769 CreateOptions = [] 770 CreateContexts = [] 771 CreateDisposition = "FILE_OPEN" 772 if type == "folder": 773 FileAttributes.append("FILE_ATTRIBUTE_DIRECTORY") 774 CreateOptions.append("FILE_DIRECTORY_FILE") 775 elif type in ["file", "pipe"]: 776 CreateOptions = ["FILE_NON_DIRECTORY_FILE"] 777 if "r" in mode: 778 DesiredAccess.extend(["FILE_READ_EA", "READ_CONTROL", "SYNCHRONIZE"]) 779 if "w" in mode: 780 CreateDisposition = "FILE_OVERWRITE_IF" 781 DesiredAccess.append("FILE_WRITE_EA") 782 if "d" in mode: 783 DesiredAccess.append("DELETE") 784 CreateOptions.append("FILE_DELETE_ON_CLOSE") 785 if type == "file": 786 FileAttributes.append("FILE_ATTRIBUTE_NORMAL") 787 elif type: 788 raise ValueError("Unknown type: %s" % type) 789 # [MS-SMB2] 3.2.4.3.8 790 RequestedOplockLevel = 0 791 if self.session.Dialect >= 0x0300: 792 RequestedOplockLevel = "SMB2_OPLOCK_LEVEL_LEASE" 793 elif self.session.Dialect >= 0x0210 and type == "file": 794 RequestedOplockLevel = "SMB2_OPLOCK_LEVEL_LEASE" 795 # SMB 3.X 796 if self.session.Dialect >= 0x0300 and type in ["file", "folder"]: 797 CreateContexts.extend( 798 [ 799 # [SMB2] sect 3.2.4.3.5 800 SMB2_Create_Context( 801 Name=b"DH2Q", 802 Data=SMB2_CREATE_DURABLE_HANDLE_REQUEST_V2( 803 CreateGuid=RandUUID()._fix() 804 ), 805 ), 806 # [SMB2] sect 3.2.4.3.9 807 SMB2_Create_Context( 808 Name=b"MxAc", 809 ), 810 # [SMB2] sect 3.2.4.3.10 811 SMB2_Create_Context( 812 Name=b"QFid", 813 ), 814 # [SMB2] sect 3.2.4.3.8 815 SMB2_Create_Context( 816 Name=b"RqLs", 817 Data=SMB2_CREATE_REQUEST_LEASE_V2(LeaseKey=RandUUID()._fix()), 818 ), 819 ] 820 ) 821 elif self.session.Dialect == 0x0210 and type == "file": 822 CreateContexts.extend( 823 [ 824 # [SMB2] sect 3.2.4.3.8 825 SMB2_Create_Context( 826 Name=b"RqLs", 827 Data=SMB2_CREATE_REQUEST_LEASE(LeaseKey=RandUUID()._fix()), 828 ), 829 ] 830 ) 831 # Extra options 832 if extra_create_options: 833 CreateOptions.extend(extra_create_options) 834 if extra_desired_access: 835 DesiredAccess.extend(extra_desired_access) 836 # Request 837 resp = self.ins.sr1( 838 SMB2_Create_Request( 839 ImpersonationLevel="Impersonation", 840 DesiredAccess="+".join(DesiredAccess), 841 CreateDisposition=CreateDisposition, 842 CreateOptions="+".join(CreateOptions), 843 ShareAccess="+".join(ShareAccess), 844 FileAttributes="+".join(FileAttributes), 845 CreateContexts=CreateContexts, 846 RequestedOplockLevel=RequestedOplockLevel, 847 Name=name, 848 ), 849 verbose=0, 850 timeout=self.timeout, 851 ) 852 if not resp: 853 raise ValueError("CreateRequest timed out !") 854 if SMB2_Create_Response not in resp: 855 raise ValueError("Failed CreateRequest ! %s" % resp.NTStatus) 856 return resp[SMB2_Create_Response].FileId 857 858 def close_request(self, FileId): 859 """ 860 Close the FileId 861 """ 862 pkt = SMB2_Close_Request(FileId=FileId) 863 resp = self.ins.sr1(pkt, verbose=0, timeout=self.timeout) 864 if not resp: 865 raise ValueError("CloseRequest timed out !") 866 if SMB2_Close_Response not in resp: 867 raise ValueError("Failed CloseRequest ! %s" % resp.NTStatus) 868 869 def read_request(self, FileId, Length, Offset=0): 870 """ 871 Read request 872 """ 873 resp = self.ins.sr1( 874 SMB2_Read_Request( 875 FileId=FileId, 876 Length=Length, 877 Offset=Offset, 878 ), 879 verbose=0, 880 timeout=self.timeout, 881 ) 882 if not resp: 883 raise ValueError("ReadRequest timed out !") 884 if SMB2_Read_Response not in resp: 885 raise ValueError("Failed ReadRequest ! %s" % resp.NTStatus) 886 return resp.Data 887 888 def write_request(self, Data, FileId, Offset=0): 889 """ 890 Write request 891 """ 892 resp = self.ins.sr1( 893 SMB2_Write_Request( 894 FileId=FileId, 895 Data=Data, 896 Offset=Offset, 897 ), 898 verbose=0, 899 timeout=self.timeout, 900 ) 901 if not resp: 902 raise ValueError("WriteRequest timed out !") 903 if SMB2_Write_Response not in resp: 904 raise ValueError("Failed WriteRequest ! %s" % resp.NTStatus) 905 return resp.Count 906 907 def query_directory(self, FileId, FileName="*"): 908 """ 909 Query the Directory with FileId 910 """ 911 results = [] 912 Flags = "SMB2_RESTART_SCANS" 913 while True: 914 pkt = SMB2_Query_Directory_Request( 915 FileInformationClass="FileIdBothDirectoryInformation", 916 FileId=FileId, 917 FileName=FileName, 918 Flags=Flags, 919 ) 920 resp = self.ins.sr1(pkt, verbose=0, timeout=self.timeout) 921 Flags = 0 # only the first one is RESTART_SCANS 922 if not resp: 923 raise ValueError("QueryDirectory timed out !") 924 if SMB2_Error_Response in resp: 925 break 926 elif SMB2_Query_Directory_Response not in resp: 927 raise ValueError("Failed QueryDirectory ! %s" % resp.NTStatus) 928 res = FileIdBothDirectoryInformation(resp.Output) 929 results.extend( 930 [ 931 ( 932 x.FileName, 933 x.FileAttributes, 934 x.EndOfFile, 935 x.LastWriteTime, 936 ) 937 for x in res.files 938 ] 939 ) 940 return results 941 942 def query_info(self, FileId, InfoType, FileInfoClass, AdditionalInformation=0): 943 """ 944 Query the Info 945 """ 946 pkt = SMB2_Query_Info_Request( 947 InfoType=InfoType, 948 FileInfoClass=FileInfoClass, 949 OutputBufferLength=65535, 950 FileId=FileId, 951 AdditionalInformation=AdditionalInformation, 952 ) 953 resp = self.ins.sr1(pkt, verbose=0, timeout=self.timeout) 954 if not resp: 955 raise ValueError("QueryInfo timed out !") 956 if SMB2_Query_Info_Response not in resp: 957 raise ValueError("Failed QueryInfo ! %s" % resp.NTStatus) 958 return resp.Output 959 960 def changenotify(self, FileId): 961 """ 962 Register change notify 963 """ 964 pkt = SMB2_Change_Notify_Request( 965 Flags="SMB2_WATCH_TREE", 966 OutputBufferLength=65535, 967 FileId=FileId, 968 CompletionFilter=0x0FFF, 969 ) 970 # we can wait forever, not a problem in this one 971 resp = self.ins.sr1(pkt, verbose=0, chainCC=True) 972 if SMB2_Change_Notify_Response not in resp: 973 raise ValueError("Failed ChangeNotify ! %s" % resp.NTStatus) 974 return resp.Output 975 976 977class SMB_RPC_SOCKET(ObjectPipe, SMB_SOCKET): 978 """ 979 Extends SMB_SOCKET (which is a wrapper over SMB_Client.smblink) to send 980 DCE/RPC messages (bind, reqs, etc.) 981 982 This is usable as a normal SuperSocket (sr1, etc.) and performs the 983 wrapping of the DCE/RPC messages into SMB2_Write/Read packets. 984 """ 985 986 def __init__(self, smbsock, use_ioctl=True, timeout=3): 987 self.use_ioctl = use_ioctl 988 ObjectPipe.__init__(self, "SMB_RPC_SOCKET") 989 SMB_SOCKET.__init__(self, smbsock, timeout=timeout) 990 991 def open_pipe(self, name): 992 self.PipeFileId = self.create_request(name, mode="rw", type="pipe") 993 994 def close_pipe(self): 995 self.close_request(self.PipeFileId) 996 self.PipeFileId = None 997 998 def send(self, x): 999 """ 1000 Internal ObjectPipe function. 1001 """ 1002 # Reminder: this class is an ObjectPipe, it's just a queue 1003 if self.use_ioctl: 1004 # Use IOCTLRequest 1005 pkt = SMB2_IOCTL_Request( 1006 FileId=self.PipeFileId, 1007 Flags="SMB2_0_IOCTL_IS_FSCTL", 1008 CtlCode="FSCTL_PIPE_TRANSCEIVE", 1009 ) 1010 pkt.Input = bytes(x) 1011 resp = self.ins.sr1(pkt, verbose=0) 1012 if SMB2_IOCTL_Response not in resp: 1013 raise ValueError("Failed reading IOCTL_Response ! %s" % resp.NTStatus) 1014 data = bytes(resp.Output) 1015 # Handle BUFFER_OVERFLOW (big DCE/RPC response) 1016 while resp.NTStatus == "STATUS_BUFFER_OVERFLOW": 1017 # Retrieve DCE/RPC full size 1018 resp = self.ins.sr1( 1019 SMB2_Read_Request( 1020 FileId=self.PipeFileId, 1021 ), 1022 verbose=0, 1023 ) 1024 data += resp.Data 1025 super(SMB_RPC_SOCKET, self).send(data) 1026 else: 1027 # Use WriteRequest/ReadRequest 1028 pkt = SMB2_Write_Request( 1029 FileId=self.PipeFileId, 1030 ) 1031 pkt.Data = bytes(x) 1032 # We send the Write Request 1033 resp = self.ins.sr1(pkt, verbose=0) 1034 if SMB2_Write_Response not in resp: 1035 raise ValueError("Failed sending WriteResponse ! %s" % resp.NTStatus) 1036 # We send a Read Request afterwards 1037 resp = self.ins.sr1( 1038 SMB2_Read_Request( 1039 FileId=self.PipeFileId, 1040 ), 1041 verbose=0, 1042 ) 1043 if SMB2_Read_Response not in resp: 1044 raise ValueError("Failed reading ReadResponse ! %s" % resp.NTStatus) 1045 data = bytes(resp.Data) 1046 # Handle BUFFER_OVERFLOW (big DCE/RPC response) 1047 while resp.NTStatus == "STATUS_BUFFER_OVERFLOW": 1048 # Retrieve DCE/RPC full size 1049 resp = self.ins.sr1( 1050 SMB2_Read_Request( 1051 FileId=self.PipeFileId, 1052 ), 1053 verbose=0, 1054 ) 1055 data += resp.Data 1056 super(SMB_RPC_SOCKET, self).send(data) 1057 1058 def close(self): 1059 SMB_SOCKET.close(self) 1060 ObjectPipe.close(self) 1061 1062 1063@conf.commands.register 1064class smbclient(CLIUtil): 1065 r""" 1066 A simple smbclient CLI 1067 1068 :param target: can be a hostname, the IPv4 or the IPv6 to connect to 1069 :param UPN: the upn to use (DOMAIN/USER, DOMAIN\USER, USER@DOMAIN or USER) 1070 :param guest: use guest mode (over NTLM) 1071 :param ssp: if provided, use this SSP for auth. 1072 :param kerberos: if available, whether to use Kerberos or not 1073 :param kerberos_required: require kerberos 1074 :param port: the TCP port. default 445 1075 :param password: (string) if provided, used for auth 1076 :param HashNt: (bytes) if provided, used for auth (NTLM) 1077 :param ST: if provided, the service ticket to use (Kerberos) 1078 :param KEY: if provided, the session key associated to the ticket (Kerberos) 1079 :param cli: CLI mode (default True). False to use for scripting 1080 """ 1081 1082 def __init__( 1083 self, 1084 target: str, 1085 UPN: str = None, 1086 password: str = None, 1087 guest: bool = False, 1088 kerberos: bool = True, 1089 kerberos_required: bool = False, 1090 HashNt: str = None, 1091 port: int = 445, 1092 timeout: int = 2, 1093 debug: int = 0, 1094 ssp=None, 1095 ST=None, 1096 KEY=None, 1097 cli=True, 1098 # SMB arguments 1099 **kwargs, 1100 ): 1101 if cli: 1102 self._depcheck() 1103 hostname = None 1104 # Check if target is a hostname / Check IP 1105 if ":" in target: 1106 family = socket.AF_INET6 1107 if not valid_ip6(target): 1108 hostname = target 1109 target = str(Net6(target)) 1110 else: 1111 family = socket.AF_INET 1112 if not valid_ip(target): 1113 hostname = target 1114 target = str(Net(target)) 1115 assert UPN or ssp or guest, "Either UPN, ssp or guest must be provided !" 1116 # Do we need to build a SSP? 1117 if ssp is None: 1118 # Create the SSP (only if not guest mode) 1119 if not guest: 1120 # Check UPN 1121 try: 1122 _, realm = _parse_upn(UPN) 1123 if realm == ".": 1124 # Local 1125 kerberos = False 1126 except ValueError: 1127 # not a UPN: NTLM 1128 kerberos = False 1129 # Do we need to ask the password? 1130 if HashNt is None and password is None and ST is None: 1131 # yes. 1132 from prompt_toolkit import prompt 1133 1134 password = prompt("Password: ", is_password=True) 1135 ssps = [] 1136 # Kerberos 1137 if kerberos and hostname: 1138 if ST is None: 1139 resp = krb_as_and_tgs( 1140 upn=UPN, 1141 spn="cifs/%s" % hostname, 1142 password=password, 1143 debug=debug, 1144 ) 1145 if resp is not None: 1146 ST, KEY = resp.tgsrep.ticket, resp.sessionkey 1147 if ST: 1148 ssps.append(KerberosSSP(UPN=UPN, ST=ST, KEY=KEY, debug=debug)) 1149 elif kerberos_required: 1150 raise ValueError( 1151 "Kerberos required but target isn't a hostname !" 1152 ) 1153 elif kerberos_required: 1154 raise ValueError( 1155 "Kerberos required but domain not specified in the UPN, " 1156 "or target isn't a hostname !" 1157 ) 1158 # NTLM 1159 if not kerberos_required: 1160 if HashNt is None and password is not None: 1161 HashNt = MD4le(password) 1162 ssps.append(NTLMSSP(UPN=UPN, HASHNT=HashNt)) 1163 # Build the SSP 1164 ssp = SPNEGOSSP(ssps) 1165 else: 1166 # Guest mode 1167 ssp = None 1168 # Open socket 1169 sock = socket.socket(family, socket.SOCK_STREAM) 1170 # Configure socket for SMB: 1171 # - TCP KEEPALIVE, TCP_KEEPIDLE and TCP_KEEPINTVL. Against a Windows server this 1172 # isn't necessary, but samba kills the socket VERY fast otherwise. 1173 # - set TCP_NODELAY to disable Nagle's algorithm (we're streaming data) 1174 sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1) 1175 sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) 1176 sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, 10) 1177 sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, 10) 1178 # Timeout & connect 1179 sock.settimeout(timeout) 1180 sock.connect((target, port)) 1181 self.extra_create_options = [] 1182 # Wrap with the automaton 1183 self.timeout = timeout 1184 kwargs.setdefault("SERVER_NAME", target) 1185 self.sock = SMB_Client.from_tcpsock( 1186 sock, 1187 ssp=ssp, 1188 debug=debug, 1189 **kwargs, 1190 ) 1191 try: 1192 # Wrap with SMB_SOCKET 1193 self.smbsock = SMB_SOCKET(self.sock) 1194 # Wait for either the atmt to fail, or the smb_sock_ready to timeout 1195 _t = time.time() 1196 while True: 1197 if self.sock.atmt.smb_sock_ready.is_set(): 1198 # yay 1199 break 1200 if not self.sock.atmt.isrunning(): 1201 status = self.sock.atmt.get("Status") 1202 raise Scapy_Exception( 1203 "%s with status %s" 1204 % ( 1205 self.sock.atmt.state.state, 1206 STATUS_ERREF.get(status, hex(status)), 1207 ) 1208 ) 1209 if time.time() - _t > timeout: 1210 self.sock.close() 1211 raise TimeoutError("The SMB handshake timed out.") 1212 time.sleep(0.1) 1213 except Exception: 1214 # Something bad happened, end the socket/automaton 1215 self.sock.close() 1216 raise 1217 1218 # For some usages, we will also need the RPC wrapper 1219 from scapy.layers.msrpce.rpcclient import DCERPC_Client 1220 1221 self.rpcclient = DCERPC_Client.from_smblink(self.sock, ndr64=False, verb=False) 1222 # We have a valid smb connection ! 1223 print( 1224 "%s authentication successful using %s%s !" 1225 % ( 1226 SMB_DIALECTS.get( 1227 self.smbsock.session.Dialect, 1228 "SMB %s" % self.smbsock.session.Dialect, 1229 ), 1230 repr(self.smbsock.session.sspcontext), 1231 " as GUEST" if self.sock.atmt.IsGuest else "", 1232 ) 1233 ) 1234 # Now define some variables for our CLI 1235 self.pwd = pathlib.PureWindowsPath("/") 1236 self.localpwd = pathlib.Path(".").resolve() 1237 self.current_tree = None 1238 self.ls_cache = {} # cache the listing of the current directory 1239 self.sh_cache = [] # cache the shares 1240 # Start CLI 1241 if cli: 1242 self.loop(debug=debug) 1243 1244 def ps1(self): 1245 return r"smb: \%s> " % self.normalize_path(self.pwd) 1246 1247 def close(self): 1248 print("Connection closed") 1249 self.smbsock.close() 1250 1251 def _require_share(self, silent=False): 1252 if self.current_tree is None: 1253 if not silent: 1254 print("No share selected ! Try 'shares' then 'use'.") 1255 return True 1256 1257 def collapse_path(self, path): 1258 # the amount of pathlib.wtf you need to do to resolve .. on all platforms 1259 # is ridiculous 1260 return pathlib.PureWindowsPath(os.path.normpath(path.as_posix())) 1261 1262 def normalize_path(self, path): 1263 """ 1264 Normalize path for CIFS usage 1265 """ 1266 return str(self.collapse_path(path)).lstrip("\\") 1267 1268 @CLIUtil.addcommand() 1269 def shares(self): 1270 """ 1271 List the shares available 1272 """ 1273 # Poll cache 1274 if self.sh_cache: 1275 return self.sh_cache 1276 # One of the 'hardest' considering it's an RPC 1277 self.rpcclient.open_smbpipe("srvsvc") 1278 self.rpcclient.bind(find_dcerpc_interface("srvsvc")) 1279 req = NetrShareEnum_Request( 1280 InfoStruct=LPSHARE_ENUM_STRUCT( 1281 Level=1, 1282 ShareInfo=NDRUnion( 1283 tag=1, 1284 value=SHARE_INFO_1_CONTAINER(Buffer=None), 1285 ), 1286 ), 1287 PreferedMaximumLength=0xFFFFFFFF, 1288 ) 1289 resp = self.rpcclient.sr1_req(req, timeout=self.timeout) 1290 self.rpcclient.close_smbpipe() 1291 if not isinstance(resp, NetrShareEnum_Response): 1292 raise ValueError("NetrShareEnum_Request failed !") 1293 results = [] 1294 for share in resp.valueof("InfoStruct.ShareInfo.Buffer"): 1295 shi1_type = share.valueof("shi1_type") & 0x0FFFFFFF 1296 results.append( 1297 ( 1298 share.valueof("shi1_netname").decode(), 1299 SRVSVC_SHARE_TYPES.get(shi1_type, shi1_type), 1300 share.valueof("shi1_remark").decode(), 1301 ) 1302 ) 1303 self.sh_cache = results # cache 1304 return results 1305 1306 @CLIUtil.addoutput(shares) 1307 def shares_output(self, results): 1308 """ 1309 Print the output of 'shares' 1310 """ 1311 print(pretty_list(results, [("ShareName", "ShareType", "Comment")])) 1312 1313 @CLIUtil.addcommand() 1314 def use(self, share): 1315 """ 1316 Open a share 1317 """ 1318 self.current_tree = self.smbsock.tree_connect(share) 1319 self.pwd = pathlib.PureWindowsPath("/") 1320 self.ls_cache.clear() 1321 1322 @CLIUtil.addcomplete(use) 1323 def use_complete(self, share): 1324 """ 1325 Auto-complete 'use' 1326 """ 1327 return [ 1328 x[0] for x in self.shares() if x[0].startswith(share) and x[0] != "IPC$" 1329 ] 1330 1331 def _parsepath(self, arg, remote=True): 1332 """ 1333 Parse a path. Returns the parent folder and file name 1334 """ 1335 # Find parent directory if it exists 1336 elt = (pathlib.PureWindowsPath if remote else pathlib.Path)(arg) 1337 eltpar = (pathlib.PureWindowsPath if remote else pathlib.Path)(".") 1338 eltname = elt.name 1339 if arg.endswith("/") or arg.endswith("\\"): 1340 eltpar = elt 1341 eltname = "" 1342 elif elt.parent and elt.parent.name or elt.is_absolute(): 1343 eltpar = elt.parent 1344 return eltpar, eltname 1345 1346 def _fs_complete(self, arg, cond=None): 1347 """ 1348 Return a listing of the remote files for completion purposes 1349 """ 1350 if cond is None: 1351 cond = lambda _: True 1352 eltpar, eltname = self._parsepath(arg) 1353 # ls in that directory 1354 try: 1355 files = self.ls(parent=eltpar) 1356 except ValueError: 1357 return [] 1358 return [ 1359 str(eltpar / x[0]) 1360 for x in files 1361 if ( 1362 x[0].lower().startswith(eltname.lower()) 1363 and x[0] not in [".", ".."] 1364 and cond(x[1]) 1365 ) 1366 ] 1367 1368 def _dir_complete(self, arg): 1369 """ 1370 Return a directories of remote files for completion purposes 1371 """ 1372 results = self._fs_complete( 1373 arg, 1374 cond=lambda x: x.FILE_ATTRIBUTE_DIRECTORY, 1375 ) 1376 if len(results) == 1 and results[0].startswith(arg): 1377 # skip through folders 1378 return [results[0] + "\\"] 1379 return results 1380 1381 @CLIUtil.addcommand(spaces=True) 1382 def ls(self, parent=None): 1383 """ 1384 List the files in the remote directory 1385 -t: sort by timestamp 1386 -S: sort by size 1387 -r: reverse while sorting 1388 """ 1389 if self._require_share(): 1390 return 1391 # Get pwd of the ls 1392 pwd = self.pwd 1393 if parent is not None: 1394 pwd /= parent 1395 pwd = self.normalize_path(pwd) 1396 # Poll the cache 1397 if self.ls_cache and pwd in self.ls_cache: 1398 return self.ls_cache[pwd] 1399 self.smbsock.set_TID(self.current_tree) 1400 # Open folder 1401 fileId = self.smbsock.create_request( 1402 pwd, 1403 type="folder", 1404 extra_create_options=self.extra_create_options, 1405 ) 1406 # Query the folder 1407 files = self.smbsock.query_directory(fileId) 1408 # Close the folder 1409 self.smbsock.close_request(fileId) 1410 self.ls_cache[pwd] = files # Store cache 1411 return files 1412 1413 @CLIUtil.addoutput(ls) 1414 def ls_output(self, results, *, t=False, S=False, r=False): 1415 """ 1416 Print the output of 'ls' 1417 """ 1418 fld = UTCTimeField( 1419 "", None, fmt="<Q", epoch=[1601, 1, 1, 0, 0, 0], custom_scaling=1e7 1420 ) 1421 if t: 1422 # Sort by time 1423 results.sort(key=lambda x: -x[3]) 1424 if S: 1425 # Sort by size 1426 results.sort(key=lambda x: -x[2]) 1427 if r: 1428 # Reverse sort 1429 results = results[::-1] 1430 results = [ 1431 ( 1432 x[0], 1433 "+".join(y.lstrip("FILE_ATTRIBUTE_") for y in str(x[1]).split("+")), 1434 human_size(x[2]), 1435 fld.i2repr(None, x[3]), 1436 ) 1437 for x in results 1438 ] 1439 print( 1440 pretty_list( 1441 results, 1442 [("FileName", "FileAttributes", "EndOfFile", "LastWriteTime")], 1443 sortBy=None, 1444 ) 1445 ) 1446 1447 @CLIUtil.addcomplete(ls) 1448 def ls_complete(self, folder): 1449 """ 1450 Auto-complete ls 1451 """ 1452 if self._require_share(silent=True): 1453 return [] 1454 return self._dir_complete(folder) 1455 1456 @CLIUtil.addcommand(spaces=True) 1457 def cd(self, folder): 1458 """ 1459 Change the remote current directory 1460 """ 1461 if self._require_share(): 1462 return 1463 if not folder: 1464 # show mode 1465 return str(self.pwd) 1466 self.pwd /= folder 1467 self.pwd = self.collapse_path(self.pwd) 1468 self.ls_cache.clear() 1469 1470 @CLIUtil.addcomplete(cd) 1471 def cd_complete(self, folder): 1472 """ 1473 Auto-complete cd 1474 """ 1475 if self._require_share(silent=True): 1476 return [] 1477 return self._dir_complete(folder) 1478 1479 def _lfs_complete(self, arg, cond): 1480 """ 1481 Return a listing of local files for completion purposes 1482 """ 1483 eltpar, eltname = self._parsepath(arg, remote=False) 1484 eltpar = self.localpwd / eltpar 1485 return [ 1486 # trickery so that ../<TAB> works 1487 str(eltpar / x.name) 1488 for x in eltpar.resolve().glob("*") 1489 if (x.name.lower().startswith(eltname.lower()) and cond(x)) 1490 ] 1491 1492 @CLIUtil.addoutput(cd) 1493 def cd_output(self, result): 1494 """ 1495 Print the output of 'cd' 1496 """ 1497 if result: 1498 print(result) 1499 1500 @CLIUtil.addcommand() 1501 def lls(self): 1502 """ 1503 List the files in the local directory 1504 """ 1505 return list(self.localpwd.glob("*")) 1506 1507 @CLIUtil.addoutput(lls) 1508 def lls_output(self, results): 1509 """ 1510 Print the output of 'lls' 1511 """ 1512 results = [ 1513 ( 1514 x.name, 1515 human_size(stat.st_size), 1516 time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(stat.st_mtime)), 1517 ) 1518 for x, stat in ((x, x.stat()) for x in results) 1519 ] 1520 print( 1521 pretty_list(results, [("FileName", "File Size", "Last Modification Time")]) 1522 ) 1523 1524 @CLIUtil.addcommand(spaces=True) 1525 def lcd(self, folder): 1526 """ 1527 Change the local current directory 1528 """ 1529 if not folder: 1530 # show mode 1531 return str(self.localpwd) 1532 self.localpwd /= folder 1533 self.localpwd = self.localpwd.resolve() 1534 1535 @CLIUtil.addcomplete(lcd) 1536 def lcd_complete(self, folder): 1537 """ 1538 Auto-complete lcd 1539 """ 1540 return self._lfs_complete(folder, lambda x: x.is_dir()) 1541 1542 @CLIUtil.addoutput(lcd) 1543 def lcd_output(self, result): 1544 """ 1545 Print the output of 'lcd' 1546 """ 1547 if result: 1548 print(result) 1549 1550 def _get_file(self, file, fd): 1551 """ 1552 Gets the file bytes from a remote host 1553 """ 1554 # Get pwd of the ls 1555 fpath = self.pwd / file 1556 self.smbsock.set_TID(self.current_tree) 1557 # Open file 1558 fileId = self.smbsock.create_request( 1559 self.normalize_path(fpath), 1560 type="file", 1561 extra_create_options=[ 1562 "FILE_SEQUENTIAL_ONLY", 1563 ] 1564 + self.extra_create_options, 1565 ) 1566 # Get the file size 1567 info = FileAllInformation( 1568 self.smbsock.query_info( 1569 FileId=fileId, 1570 InfoType="SMB2_0_INFO_FILE", 1571 FileInfoClass="FileAllInformation", 1572 ) 1573 ) 1574 length = info.StandardInformation.EndOfFile 1575 offset = 0 1576 # Read the file 1577 while length: 1578 lengthRead = min(self.sock.atmt.MaxReadSize, length) 1579 fd.write( 1580 self.smbsock.read_request(fileId, Length=lengthRead, Offset=offset) 1581 ) 1582 offset += lengthRead 1583 length -= lengthRead 1584 # Close the file 1585 self.smbsock.close_request(fileId) 1586 return offset 1587 1588 def _send_file(self, fname, fd): 1589 """ 1590 Send the file bytes to a remote host 1591 """ 1592 # Get destination file 1593 fpath = self.pwd / fname 1594 self.smbsock.set_TID(self.current_tree) 1595 # Open file 1596 fileId = self.smbsock.create_request( 1597 self.normalize_path(fpath), 1598 type="file", 1599 mode="w", 1600 extra_create_options=self.extra_create_options, 1601 ) 1602 # Send the file 1603 offset = 0 1604 while True: 1605 data = fd.read(self.sock.atmt.MaxWriteSize) 1606 if not data: 1607 # end of file 1608 break 1609 offset += self.smbsock.write_request( 1610 Data=data, 1611 FileId=fileId, 1612 Offset=offset, 1613 ) 1614 # Close the file 1615 self.smbsock.close_request(fileId) 1616 return offset 1617 1618 def _getr(self, directory, _root, _verb=True): 1619 """ 1620 Internal recursive function to get a directory 1621 1622 :param directory: the remote directory to get 1623 :param _root: locally, the directory to store any found files 1624 """ 1625 size = 0 1626 if not _root.exists(): 1627 _root.mkdir() 1628 # ls the directory 1629 for x in self.ls(parent=directory): 1630 if x[0] in [".", ".."]: 1631 # Discard . and .. 1632 continue 1633 remote = directory / x[0] 1634 local = _root / x[0] 1635 try: 1636 if x[1].FILE_ATTRIBUTE_DIRECTORY: 1637 # Sub-directory 1638 size += self._getr(remote, local) 1639 else: 1640 # Sub-file 1641 size += self.get(remote, local)[1] 1642 if _verb: 1643 print(remote) 1644 except ValueError as ex: 1645 if _verb: 1646 print(conf.color_theme.red(remote), "->", str(ex)) 1647 return size 1648 1649 @CLIUtil.addcommand(spaces=True, globsupport=True) 1650 def get(self, file, _dest=None, _verb=True, *, r=False): 1651 """ 1652 Retrieve a file 1653 -r: recursively download a directory 1654 """ 1655 if self._require_share(): 1656 return 1657 if r: 1658 dirpar, dirname = self._parsepath(file) 1659 return file, self._getr( 1660 dirpar / dirname, # Remotely 1661 _root=self.localpwd / dirname, # Locally 1662 _verb=_verb, 1663 ) 1664 else: 1665 fname = pathlib.PureWindowsPath(file).name 1666 # Write the buffer 1667 if _dest is None: 1668 _dest = self.localpwd / fname 1669 with _dest.open("wb") as fd: 1670 size = self._get_file(file, fd) 1671 return fname, size 1672 1673 @CLIUtil.addoutput(get) 1674 def get_output(self, info): 1675 """ 1676 Print the output of 'get' 1677 """ 1678 print("Retrieved '%s' of size %s" % (info[0], human_size(info[1]))) 1679 1680 @CLIUtil.addcomplete(get) 1681 def get_complete(self, file): 1682 """ 1683 Auto-complete get 1684 """ 1685 if self._require_share(silent=True): 1686 return [] 1687 return self._fs_complete(file) 1688 1689 @CLIUtil.addcommand(spaces=True, globsupport=True) 1690 def cat(self, file): 1691 """ 1692 Print a file 1693 """ 1694 if self._require_share(): 1695 return 1696 # Write the buffer to buffer 1697 buf = io.BytesIO() 1698 self._get_file(file, buf) 1699 return buf.getvalue() 1700 1701 @CLIUtil.addoutput(cat) 1702 def cat_output(self, result): 1703 """ 1704 Print the output of 'cat' 1705 """ 1706 print(result.decode(errors="backslashreplace")) 1707 1708 @CLIUtil.addcomplete(cat) 1709 def cat_complete(self, file): 1710 """ 1711 Auto-complete cat 1712 """ 1713 if self._require_share(silent=True): 1714 return [] 1715 return self._fs_complete(file) 1716 1717 @CLIUtil.addcommand(spaces=True) 1718 def put(self, file): 1719 """ 1720 Upload a file 1721 """ 1722 if self._require_share(): 1723 return 1724 local_file = self.localpwd / file 1725 if local_file.is_dir(): 1726 # Directory 1727 raise ValueError("put on dir not impl") 1728 else: 1729 fname = pathlib.Path(file).name 1730 with local_file.open("rb") as fd: 1731 size = self._send_file(fname, fd) 1732 self.ls_cache.clear() 1733 return fname, size 1734 1735 @CLIUtil.addcomplete(put) 1736 def put_complete(self, folder): 1737 """ 1738 Auto-complete put 1739 """ 1740 return self._lfs_complete(folder, lambda x: not x.is_dir()) 1741 1742 @CLIUtil.addcommand(spaces=True) 1743 def rm(self, file): 1744 """ 1745 Delete a file 1746 """ 1747 if self._require_share(): 1748 return 1749 # Get pwd of the ls 1750 fpath = self.pwd / file 1751 self.smbsock.set_TID(self.current_tree) 1752 # Open file 1753 fileId = self.smbsock.create_request( 1754 self.normalize_path(fpath), 1755 type="file", 1756 mode="d", 1757 extra_create_options=self.extra_create_options, 1758 ) 1759 # Close the file 1760 self.smbsock.close_request(fileId) 1761 self.ls_cache.clear() 1762 return fpath.name 1763 1764 @CLIUtil.addcomplete(rm) 1765 def rm_complete(self, file): 1766 """ 1767 Auto-complete rm 1768 """ 1769 if self._require_share(silent=True): 1770 return [] 1771 return self._fs_complete(file) 1772 1773 @CLIUtil.addcommand() 1774 def backup(self): 1775 """ 1776 Turn on or off backup intent 1777 """ 1778 if "FILE_OPEN_FOR_BACKUP_INTENT" in self.extra_create_options: 1779 print("Backup Intent: Off") 1780 self.extra_create_options.remove("FILE_OPEN_FOR_BACKUP_INTENT") 1781 else: 1782 print("Backup Intent: On") 1783 self.extra_create_options.append("FILE_OPEN_FOR_BACKUP_INTENT") 1784 1785 1786if __name__ == "__main__": 1787 from scapy.utils import AutoArgparse 1788 1789 AutoArgparse(smbclient) 1790