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 2 Server Automaton 8 9This provides a [MS-SMB2] server that can: 10- serve files 11- host a DCE/RPC server 12 13This is a Scapy Automaton that is supposedly easily extendable. 14 15.. note:: 16 You will find more complete documentation for this layer over at 17 `SMB <https://scapy.readthedocs.io/en/latest/layers/smb.html#server>`_ 18""" 19 20import hashlib 21import pathlib 22import socket 23import struct 24import time 25 26from scapy.arch import get_if_addr 27from scapy.automaton import ATMT, Automaton 28from scapy.config import conf 29from scapy.error import log_runtime, log_interactive 30from scapy.volatile import RandUUID 31 32from scapy.layers.dcerpc import ( 33 DCERPC_Transport, 34 NDRUnion, 35) 36from scapy.layers.gssapi import ( 37 GSS_S_COMPLETE, 38 GSS_S_CONTINUE_NEEDED, 39 GSS_S_CREDENTIALS_EXPIRED, 40) 41from scapy.layers.msrpce.rpcserver import DCERPC_Server 42from scapy.layers.ntlm import ( 43 NTLMSSP, 44) 45from scapy.layers.smb import ( 46 SMBNegotiate_Request, 47 SMBNegotiate_Response_Extended_Security, 48 SMBNegotiate_Response_Security, 49 SMBSession_Null, 50 SMBSession_Setup_AndX_Request, 51 SMBSession_Setup_AndX_Request_Extended_Security, 52 SMBSession_Setup_AndX_Response, 53 SMBSession_Setup_AndX_Response_Extended_Security, 54 SMBTree_Connect_AndX, 55 SMB_Header, 56) 57from scapy.layers.smb2 import ( 58 DFS_REFERRAL_ENTRY1, 59 DFS_REFERRAL_V3, 60 DirectTCP, 61 FILE_BOTH_DIR_INFORMATION, 62 FILE_FULL_DIR_INFORMATION, 63 FILE_ID_BOTH_DIR_INFORMATION, 64 FILE_NAME_INFORMATION, 65 FileAllInformation, 66 FileAlternateNameInformation, 67 FileBasicInformation, 68 FileEaInformation, 69 FileFsAttributeInformation, 70 FileFsSizeInformation, 71 FileFsVolumeInformation, 72 FileIdBothDirectoryInformation, 73 FileInternalInformation, 74 FileNetworkOpenInformation, 75 FileStandardInformation, 76 FileStreamInformation, 77 NETWORK_INTERFACE_INFO, 78 SECURITY_DESCRIPTOR, 79 SMB2_Cancel_Request, 80 SMB2_Change_Notify_Request, 81 SMB2_Change_Notify_Response, 82 SMB2_Close_Request, 83 SMB2_Close_Response, 84 SMB2_Create_Context, 85 SMB2_CREATE_DURABLE_HANDLE_RESPONSE_V2, 86 SMB2_CREATE_QUERY_MAXIMAL_ACCESS_RESPONSE, 87 SMB2_CREATE_QUERY_ON_DISK_ID, 88 SMB2_Create_Request, 89 SMB2_Create_Response, 90 SMB2_Echo_Request, 91 SMB2_Echo_Response, 92 SMB2_Encryption_Capabilities, 93 SMB2_Error_Response, 94 SMB2_FILEID, 95 SMB2_Header, 96 SMB2_IOCTL_Network_Interface_Info, 97 SMB2_IOCTL_Request, 98 SMB2_IOCTL_RESP_GET_DFS_Referral, 99 SMB2_IOCTL_Response, 100 SMB2_IOCTL_Validate_Negotiate_Info_Response, 101 SMB2_Negotiate_Context, 102 SMB2_Negotiate_Protocol_Request, 103 SMB2_Negotiate_Protocol_Response, 104 SMB2_Preauth_Integrity_Capabilities, 105 SMB2_Query_Directory_Request, 106 SMB2_Query_Directory_Response, 107 SMB2_Query_Info_Request, 108 SMB2_Query_Info_Response, 109 SMB2_Read_Request, 110 SMB2_Read_Response, 111 SMB2_Session_Logoff_Request, 112 SMB2_Session_Logoff_Response, 113 SMB2_Session_Setup_Request, 114 SMB2_Session_Setup_Response, 115 SMB2_Set_Info_Request, 116 SMB2_Set_Info_Response, 117 SMB2_Signing_Capabilities, 118 SMB2_Tree_Connect_Request, 119 SMB2_Tree_Connect_Response, 120 SMB2_Tree_Disconnect_Request, 121 SMB2_Tree_Disconnect_Response, 122 SMB2_Write_Request, 123 SMB2_Write_Response, 124 SMBStreamSocket, 125 SOCKADDR_STORAGE, 126 SRVSVC_SHARE_TYPES, 127) 128from scapy.layers.spnego import SPNEGOSSP 129 130# Import DCE/RPC 131from scapy.layers.msrpce.raw.ms_srvs import ( 132 LPSERVER_INFO_101, 133 LPSHARE_ENUM_STRUCT, 134 LPSHARE_INFO_1, 135 NetrServerGetInfo_Request, 136 NetrServerGetInfo_Response, 137 NetrShareEnum_Request, 138 NetrShareEnum_Response, 139 NetrShareGetInfo_Request, 140 NetrShareGetInfo_Response, 141 SHARE_INFO_1_CONTAINER, 142) 143from scapy.layers.msrpce.raw.ms_wkst import ( 144 LPWKSTA_INFO_100, 145 NetrWkstaGetInfo_Request, 146 NetrWkstaGetInfo_Response, 147) 148 149 150class SMBShare: 151 """ 152 A class used to define a share, used by SMB_Server 153 154 :param name: the share name 155 :param path: the path the the folder hosted by the share 156 :param type: (optional) share type per [MS-SRVS] sect 2.2.2.4 157 :param remark: (optional) a description of the share 158 """ 159 160 def __init__(self, name, path=".", type=None, remark=""): 161 # Set the default type 162 if type is None: 163 type = 0 # DISKTREE 164 if name.endswith("$"): 165 type &= 0x80000000 # SPECIAL 166 # Lower case the name for resolution 167 self._name = name.lower() 168 # Resolve path 169 self.path = pathlib.Path(path).resolve() 170 # props 171 self.name = name 172 self.type = type 173 self.remark = remark 174 175 def __repr__(self): 176 type = SRVSVC_SHARE_TYPES[self.type & 0x0FFFFFFF] 177 if self.type & 0x80000000: 178 type = "SPECIAL+" + type 179 if self.type & 0x40000000: 180 type = "TEMPORARY+" + type 181 return "<SMBShare %s [%s]%s = %s>" % ( 182 self.name, 183 type, 184 self.remark and (" '%s'" % self.remark) or "", 185 str(self.path), 186 ) 187 188 189# The SMB Automaton 190 191 192class SMB_Server(Automaton): 193 """ 194 SMB server automaton 195 196 :param shares: the shares to serve. By default, share nothing. 197 Note that IPC$ is appended. 198 :param ssp: the SSP to use 199 200 All other options (in caps) are optional, and SMB specific: 201 202 :param ANONYMOUS_LOGIN: mark the clients as anonymous 203 :param GUEST_LOGIN: mark the clients as guest 204 :param REQUIRE_SIGNATURE: set 'Require Signature' 205 :param MAX_DIALECT: maximum SMB dialect. Defaults to 0x0311 (3.1.1) 206 :param TREE_SHARE_FLAGS: flags to announce on Tree_Connect_Response 207 :param TREE_CAPABILITIES: capabilities to announce on Tree_Connect_Response 208 :param TREE_MAXIMAL_ACCESS: maximal access to announce on Tree_Connect_Response 209 :param FILE_MAXIMAL_ACCESS: maximal access to announce in MxAc Create Context 210 """ 211 212 pkt_cls = DirectTCP 213 socketcls = SMBStreamSocket 214 215 def __init__(self, shares=[], ssp=None, verb=True, readonly=True, *args, **kwargs): 216 self.verb = verb 217 if "sock" not in kwargs: 218 raise ValueError( 219 "SMB_Server cannot be started directly ! Use SMB_Server.spawn" 220 ) 221 # Various SMB server arguments 222 self.ANONYMOUS_LOGIN = kwargs.pop("ANONYMOUS_LOGIN", False) 223 self.GUEST_LOGIN = kwargs.pop("GUEST_LOGIN", None) 224 self.EXTENDED_SECURITY = kwargs.pop("EXTENDED_SECURITY", True) 225 self.USE_SMB1 = kwargs.pop("USE_SMB1", False) 226 self.REQUIRE_SIGNATURE = kwargs.pop("REQUIRE_SIGNATURE", False) 227 self.MAX_DIALECT = kwargs.pop("MAX_DIALECT", 0x0311) 228 self.TREE_SHARE_FLAGS = kwargs.pop( 229 "TREE_SHARE_FLAGS", "FORCE_LEVELII_OPLOCK+RESTRICT_EXCLUSIVE_OPENS" 230 ) 231 self.TREE_CAPABILITIES = kwargs.pop("TREE_CAPABILITIES", 0) 232 self.TREE_MAXIMAL_ACCESS = kwargs.pop( 233 "TREE_MAXIMAL_ACCESS", 234 "+".join( 235 [ 236 "FILE_READ_DATA", 237 "FILE_WRITE_DATA", 238 "FILE_APPEND_DATA", 239 "FILE_READ_EA", 240 "FILE_WRITE_EA", 241 "FILE_EXECUTE", 242 "FILE_DELETE_CHILD", 243 "FILE_READ_ATTRIBUTES", 244 "FILE_WRITE_ATTRIBUTES", 245 "DELETE", 246 "READ_CONTROL", 247 "WRITE_DAC", 248 "WRITE_OWNER", 249 "SYNCHRONIZE", 250 ] 251 ), 252 ) 253 self.FILE_MAXIMAL_ACCESS = kwargs.pop( 254 # Read-only 255 "FILE_MAXIMAL_ACCESS", 256 "+".join( 257 [ 258 "FILE_READ_DATA", 259 "FILE_READ_EA", 260 "FILE_EXECUTE", 261 "FILE_READ_ATTRIBUTES", 262 "READ_CONTROL", 263 "SYNCHRONIZE", 264 ] 265 ), 266 ) 267 self.LOCAL_IPS = kwargs.pop( 268 "LOCAL_IPS", [get_if_addr(kwargs.get("iface", conf.iface) or conf.iface)] 269 ) 270 self.DOMAIN_REFERRALS = kwargs.pop("DOMAIN_REFERRALS", []) 271 if self.USE_SMB1: 272 log_runtime.warning("Serving SMB1 is not supported :/") 273 self.readonly = readonly 274 # We don't want to update the parent shares argument 275 self.shares = shares.copy() 276 # Append the IPC$ share 277 self.shares.append( 278 SMBShare( 279 name="IPC$", 280 type=0x80000003, # SPECIAL+IPC 281 remark="Remote IPC", 282 ) 283 ) 284 # Initialize the DCE/RPC server for SMB 285 self.rpc_server = SMB_DCERPC_Server( 286 DCERPC_Transport.NCACN_NP, 287 shares=self.shares, 288 verb=self.verb, 289 ) 290 # Extend it if another DCE/RPC server is provided 291 if "DCERPC_SERVER_CLS" in kwargs: 292 self.rpc_server.extend(kwargs.pop("DCERPC_SERVER_CLS")) 293 # Internal Session information 294 self.SMB2 = False 295 self.NegotiateCapabilities = None 296 self.GUID = RandUUID()._fix() 297 # Compounds are handled on receiving by the StreamSocket, 298 # and on aggregated in a CompoundQueue to be sent in one go 299 self.NextCompound = False 300 self.CompoundedHandle = None 301 # SSP provider 302 if ssp is None: 303 # No SSP => fallback on NTLM with guest 304 ssp = SPNEGOSSP( 305 [ 306 NTLMSSP( 307 USE_MIC=False, 308 DO_NOT_CHECK_LOGIN=True, 309 ), 310 ] 311 ) 312 if self.GUEST_LOGIN is None: 313 self.GUEST_LOGIN = True 314 # Initialize 315 Automaton.__init__(self, *args, **kwargs) 316 # Set session options 317 self.session.ssp = ssp 318 self.session.SecurityMode = kwargs.pop( 319 "SECURITY_MODE", 320 3 if self.REQUIRE_SIGNATURE else bool(ssp), 321 ) 322 323 @property 324 def session(self): 325 # session shorthand 326 return self.sock.session 327 328 def vprint(self, s=""): 329 """ 330 Verbose print (if enabled) 331 """ 332 if self.verb: 333 if conf.interactive: 334 log_interactive.info("> %s", s) 335 else: 336 print("> %s" % s) 337 338 def send(self, pkt): 339 return super(SMB_Server, self).send(pkt, Compounded=self.NextCompound) 340 341 @ATMT.state(initial=1) 342 def BEGIN(self): 343 self.authenticated = False 344 345 @ATMT.receive_condition(BEGIN) 346 def received_negotiate(self, pkt): 347 if SMBNegotiate_Request in pkt: 348 raise self.NEGOTIATED().action_parameters(pkt) 349 350 @ATMT.receive_condition(BEGIN) 351 def received_negotiate_smb2_begin(self, pkt): 352 if SMB2_Negotiate_Protocol_Request in pkt: 353 self.SMB2 = True 354 raise self.NEGOTIATED().action_parameters(pkt) 355 356 @ATMT.action(received_negotiate_smb2_begin) 357 def on_negotiate_smb2_begin(self, pkt): 358 self.on_negotiate(pkt) 359 360 @ATMT.action(received_negotiate) 361 def on_negotiate(self, pkt): 362 self.session.sspcontext, spnego_token = self.session.ssp.NegTokenInit2() 363 # Build negotiate response 364 DialectIndex = None 365 DialectRevision = None 366 if SMB2_Negotiate_Protocol_Request in pkt: 367 # SMB2 368 DialectRevisions = pkt[SMB2_Negotiate_Protocol_Request].Dialects 369 DialectRevisions = [x for x in DialectRevisions if x <= self.MAX_DIALECT] 370 DialectRevisions.sort(reverse=True) 371 if DialectRevisions: 372 DialectRevision = DialectRevisions[0] 373 else: 374 # SMB1 375 DialectIndexes = [ 376 x.DialectString for x in pkt[SMBNegotiate_Request].Dialects 377 ] 378 if self.USE_SMB1: 379 # Enforce SMB1 380 DialectIndex = DialectIndexes.index(b"NT LM 0.12") 381 else: 382 # Find a value matching SMB2, fallback to SMB1 383 for key, rev in [(b"SMB 2.???", 0x02FF), (b"SMB 2.002", 0x0202)]: 384 try: 385 DialectIndex = DialectIndexes.index(key) 386 DialectRevision = rev 387 self.SMB2 = True 388 break 389 except ValueError: 390 pass 391 else: 392 DialectIndex = DialectIndexes.index(b"NT LM 0.12") 393 if DialectRevision and DialectRevision & 0xFF != 0xFF: 394 # Version isn't SMB X.??? 395 self.session.Dialect = DialectRevision 396 cls = None 397 if self.SMB2: 398 # SMB2 399 cls = SMB2_Negotiate_Protocol_Response 400 self.smb_header = DirectTCP() / SMB2_Header( 401 Flags="SMB2_FLAGS_SERVER_TO_REDIR", 402 CreditRequest=1, 403 CreditCharge=1, 404 ) 405 if SMB2_Negotiate_Protocol_Request in pkt: 406 self.update_smbheader(pkt) 407 else: 408 # SMB1 409 self.smb_header = DirectTCP() / SMB_Header( 410 Flags="REPLY+CASE_INSENSITIVE+CANONICALIZED_PATHS", 411 Flags2=( 412 "LONG_NAMES+EAS+NT_STATUS+SMB_SECURITY_SIGNATURE+" 413 "UNICODE+EXTENDED_SECURITY" 414 ), 415 TID=pkt.TID, 416 MID=pkt.MID, 417 UID=pkt.UID, 418 PIDLow=pkt.PIDLow, 419 ) 420 if self.EXTENDED_SECURITY: 421 cls = SMBNegotiate_Response_Extended_Security 422 else: 423 cls = SMBNegotiate_Response_Security 424 if DialectRevision is None and DialectIndex is None: 425 # No common dialect found. 426 if self.SMB2: 427 resp = self.smb_header.copy() / SMB2_Error_Response() 428 resp.Command = "SMB2_NEGOTIATE" 429 else: 430 resp = self.smb_header.copy() / SMBSession_Null() 431 resp.Command = "SMB_COM_NEGOTIATE" 432 resp.Status = "STATUS_NOT_SUPPORTED" 433 self.send(resp) 434 return 435 if self.SMB2: # SMB2 436 # Capabilities: [MS-SMB2] 3.3.5.4 437 self.NegotiateCapabilities = "+".join( 438 [ 439 "DFS", 440 "LEASING", 441 "LARGE_MTU", 442 ] 443 ) 444 if DialectRevision >= 0x0300: 445 # "if Connection.Dialect belongs to the SMB 3.x dialect family, 446 # the server supports..." 447 self.NegotiateCapabilities += "+" + "+".join( 448 [ 449 "MULTI_CHANNEL", 450 "PERSISTENT_HANDLES", 451 "DIRECTORY_LEASING", 452 ] 453 ) 454 if DialectRevision in [0x0300, 0x0302]: 455 # "if Connection.Dialect is "3.0" or "3.0.2""... 456 # Note: 3.1.1 uses the ENCRYPT_DATA flag in Tree Connect Response 457 self.NegotiateCapabilities += "+ENCRYPTION" 458 # Build response 459 resp = self.smb_header.copy() / cls( 460 DialectRevision=DialectRevision, 461 SecurityMode=self.session.SecurityMode, 462 ServerTime=(time.time() + 11644473600) * 1e7, 463 ServerStartTime=0, 464 MaxTransactionSize=65536, 465 MaxReadSize=65536, 466 MaxWriteSize=65536, 467 Capabilities=self.NegotiateCapabilities, 468 ) 469 # SMB >= 3.0.0 470 if DialectRevision >= 0x0300: 471 # [MS-SMB2] sect 3.3.5.3.1 note 253 472 resp.MaxTransactionSize = 0x800000 473 resp.MaxReadSize = 0x800000 474 resp.MaxWriteSize = 0x800000 475 # SMB 3.1.1 476 if DialectRevision >= 0x0311: 477 resp.NegotiateContexts = [ 478 # Preauth capabilities 479 SMB2_Negotiate_Context() 480 / SMB2_Preauth_Integrity_Capabilities( 481 # SHA-512 by default 482 HashAlgorithms=[self.session.PreauthIntegrityHashId], 483 Salt=self.session.Salt, 484 ), 485 # Encryption capabilities 486 SMB2_Negotiate_Context() 487 / SMB2_Encryption_Capabilities( 488 # AES-128-CCM by default 489 Ciphers=[self.session.CipherId], 490 ), 491 # Signing capabilities 492 SMB2_Negotiate_Context() 493 / SMB2_Signing_Capabilities( 494 # AES-128-CCM by default 495 SigningAlgorithms=[self.session.SigningAlgorithmId], 496 ), 497 ] 498 else: 499 # SMB1 500 resp = self.smb_header.copy() / cls( 501 DialectIndex=DialectIndex, 502 ServerCapabilities=( 503 "UNICODE+LARGE_FILES+NT_SMBS+RPC_REMOTE_APIS+STATUS32+" 504 "LEVEL_II_OPLOCKS+LOCK_AND_READ+NT_FIND+" 505 "LWIO+INFOLEVEL_PASSTHRU+LARGE_READX+LARGE_WRITEX" 506 ), 507 SecurityMode=self.session.SecurityMode, 508 ServerTime=(time.time() + 11644473600) * 1e7, 509 ServerTimeZone=0x3C, 510 ) 511 if self.EXTENDED_SECURITY: 512 resp.ServerCapabilities += "EXTENDED_SECURITY" 513 if self.EXTENDED_SECURITY or self.SMB2: 514 # Extended SMB1 / SMB2 515 resp.GUID = self.GUID 516 # Add security blob 517 resp.SecurityBlob = spnego_token 518 else: 519 # Non-extended SMB1 520 # FIXME never tested. 521 resp.SecurityBlob = spnego_token 522 resp.Flags2 -= "EXTENDED_SECURITY" 523 if not self.SMB2: 524 resp[SMB_Header].Flags2 = ( 525 resp[SMB_Header].Flags2 526 - "SMB_SECURITY_SIGNATURE" 527 + "SMB_SECURITY_SIGNATURE_REQUIRED+IS_LONG_NAME" 528 ) 529 if SMB2_Header in pkt: 530 # If required, compute sessions 531 self.session.computeSMBConnectionPreauth( 532 bytes(pkt[SMB2_Header]), # nego request 533 bytes(resp[SMB2_Header]), # nego response 534 ) 535 self.send(resp) 536 537 @ATMT.state() 538 def NEGOTIATED(self): 539 pass 540 541 def update_smbheader(self, pkt): 542 """ 543 Called when receiving a SMB2 packet to update the current smb_header 544 """ 545 # [MS-SMB2] sect 3.2.5.1.4 - always grant client its credits 546 self.smb_header.CreditRequest = pkt.CreditRequest 547 # [MS-SMB2] sect 3.3.4.1 548 # "the server SHOULD set the CreditCharge field in the SMB2 header 549 # of the response to the CreditCharge value in the SMB2 header of the request." 550 self.smb_header.CreditCharge = pkt.CreditCharge 551 # If the packet has a NextCommand, set NextCompound to True 552 self.NextCompound = bool(pkt.NextCommand) 553 # [MS-SMB2] sect 3.3.5.2.7.2 554 # Add SMB2_FLAGS_RELATED_OPERATIONS to the response if present 555 if pkt.Flags.SMB2_FLAGS_RELATED_OPERATIONS: 556 self.smb_header.Flags += "SMB2_FLAGS_RELATED_OPERATIONS" 557 else: 558 self.smb_header.Flags -= "SMB2_FLAGS_RELATED_OPERATIONS" 559 # [MS-SMB2] sect 2.2.1.2 - Priority 560 if (self.session.Dialect or 0) >= 0x0311: 561 self.smb_header.Flags &= 0xFF8F 562 self.smb_header.Flags |= int(pkt.Flags) & 0x70 563 # Update IDs 564 self.smb_header.SessionId = pkt.SessionId 565 self.smb_header.TID = pkt.TID 566 self.smb_header.MID = pkt.MID 567 self.smb_header.PID = pkt.PID 568 569 @ATMT.receive_condition(NEGOTIATED) 570 def received_negotiate_smb2(self, pkt): 571 if SMB2_Negotiate_Protocol_Request in pkt: 572 raise self.NEGOTIATED().action_parameters(pkt) 573 574 @ATMT.action(received_negotiate_smb2) 575 def on_negotiate_smb2(self, pkt): 576 self.on_negotiate(pkt) 577 578 @ATMT.receive_condition(NEGOTIATED) 579 def receive_setup_andx_request(self, pkt): 580 if ( 581 SMBSession_Setup_AndX_Request_Extended_Security in pkt 582 or SMBSession_Setup_AndX_Request in pkt 583 ): 584 # SMB1 585 if SMBSession_Setup_AndX_Request_Extended_Security in pkt: 586 # Extended 587 ssp_blob = pkt.SecurityBlob 588 else: 589 # Non-extended 590 ssp_blob = pkt[SMBSession_Setup_AndX_Request].UnicodePassword 591 raise self.RECEIVED_SETUP_ANDX_REQUEST().action_parameters(pkt, ssp_blob) 592 elif SMB2_Session_Setup_Request in pkt: 593 # SMB2 594 ssp_blob = pkt.SecurityBlob 595 raise self.RECEIVED_SETUP_ANDX_REQUEST().action_parameters(pkt, ssp_blob) 596 597 @ATMT.state() 598 def RECEIVED_SETUP_ANDX_REQUEST(self): 599 pass 600 601 @ATMT.action(receive_setup_andx_request) 602 def on_setup_andx_request(self, pkt, ssp_blob): 603 self.session.sspcontext, tok, status = self.session.ssp.GSS_Accept_sec_context( 604 self.session.sspcontext, ssp_blob 605 ) 606 self.update_smbheader(pkt) 607 if SMB2_Session_Setup_Request in pkt: 608 # SMB2 609 self.smb_header.SessionId = 0x0001000000000015 610 if status not in [GSS_S_CONTINUE_NEEDED, GSS_S_COMPLETE]: 611 # Error 612 if SMB2_Session_Setup_Request in pkt: 613 # SMB2 614 resp = self.smb_header.copy() / SMB2_Session_Setup_Response() 615 # Set security blob (if any) 616 resp.SecurityBlob = tok 617 else: 618 # SMB1 619 resp = self.smb_header.copy() / SMBSession_Null() 620 # Map some GSS return codes to NTStatus 621 if status == GSS_S_CREDENTIALS_EXPIRED: 622 resp.Status = "STATUS_PASSWORD_EXPIRED" 623 else: 624 resp.Status = "STATUS_LOGON_FAILURE" 625 # Reset Session preauth (SMB 3.1.1) 626 self.session.SessionPreauthIntegrityHashValue = None 627 else: 628 # Negotiation 629 if ( 630 SMBSession_Setup_AndX_Request_Extended_Security in pkt 631 or SMB2_Session_Setup_Request in pkt 632 ): 633 # SMB1 extended / SMB2 634 if SMB2_Session_Setup_Request in pkt: 635 # SMB2 636 resp = self.smb_header.copy() / SMB2_Session_Setup_Response() 637 if self.GUEST_LOGIN: 638 resp.SessionFlags = "IS_GUEST" 639 if self.ANONYMOUS_LOGIN: 640 resp.SessionFlags = "IS_NULL" 641 else: 642 # SMB1 extended 643 resp = ( 644 self.smb_header.copy() 645 / SMBSession_Setup_AndX_Response_Extended_Security( 646 NativeOS="Windows 4.0", 647 NativeLanMan="Windows 4.0", 648 ) 649 ) 650 if self.GUEST_LOGIN: 651 resp.Action = "SMB_SETUP_GUEST" 652 # Set security blob 653 resp.SecurityBlob = tok 654 elif SMBSession_Setup_AndX_Request in pkt: 655 # Non-extended 656 resp = self.smb_header.copy() / SMBSession_Setup_AndX_Response( 657 NativeOS="Windows 4.0", 658 NativeLanMan="Windows 4.0", 659 ) 660 resp.Status = 0x0 if (status == GSS_S_COMPLETE) else 0xC0000016 661 # We have a response. If required, compute sessions 662 if status == GSS_S_CONTINUE_NEEDED: 663 # the setup session response is used in hash 664 self.session.computeSMBSessionPreauth( 665 bytes(pkt[SMB2_Header]), # session setup request 666 bytes(resp[SMB2_Header]), # session setup response 667 ) 668 else: 669 # the setup session response is not used in hash 670 self.session.computeSMBSessionPreauth( 671 bytes(pkt[SMB2_Header]), # session setup request 672 ) 673 if status == GSS_S_COMPLETE: 674 # Authentication was successful 675 self.session.computeSMBSessionKey() 676 self.authenticated = True 677 # and send 678 self.send(resp) 679 680 @ATMT.condition(RECEIVED_SETUP_ANDX_REQUEST) 681 def wait_for_next_request(self): 682 if self.authenticated: 683 self.vprint( 684 "User authenticated %s!" % (self.GUEST_LOGIN and " as guest" or "") 685 ) 686 raise self.AUTHENTICATED() 687 else: 688 raise self.NEGOTIATED() 689 690 @ATMT.state() 691 def AUTHENTICATED(self): 692 """Dev: overload this""" 693 pass 694 695 # DEV: add a condition on AUTHENTICATED with prio=0 696 697 @ATMT.condition(AUTHENTICATED, prio=1) 698 def should_serve(self): 699 # Serve files 700 self.current_trees = {} 701 self.current_handles = {} 702 self.enumerate_index = {} # used for query directory enumeration 703 self.tree_id = 0 704 self.base_time_t = self.current_smb_time() 705 raise self.SERVING() 706 707 def _ioctl_error(self, Status="STATUS_NOT_SUPPORTED"): 708 pkt = self.smb_header.copy() / SMB2_Error_Response(ErrorData=b"\xff") 709 pkt.Status = Status 710 pkt.Command = "SMB2_IOCTL" 711 self.send(pkt) 712 713 @ATMT.state(final=1) 714 def END(self): 715 self.end() 716 717 # SERVE FILES 718 719 def current_tree(self): 720 """ 721 Return the current tree name 722 """ 723 return self.current_trees[self.smb_header.TID] 724 725 def root_path(self): 726 """ 727 Return the root path of the current tree 728 """ 729 curtree = self.current_tree() 730 try: 731 share_path = next(x.path for x in self.shares if x._name == curtree.lower()) 732 except StopIteration: 733 return None 734 return pathlib.Path(share_path).resolve() 735 736 @ATMT.state() 737 def SERVING(self): 738 """ 739 Main state when serving files 740 """ 741 pass 742 743 @ATMT.receive_condition(SERVING) 744 def receive_logoff_request(self, pkt): 745 if SMB2_Session_Logoff_Request in pkt: 746 raise self.NEGOTIATED().action_parameters(pkt) 747 748 @ATMT.action(receive_logoff_request) 749 def send_logoff_response(self, pkt): 750 self.update_smbheader(pkt) 751 self.send(self.smb_header.copy() / SMB2_Session_Logoff_Response()) 752 753 @ATMT.receive_condition(SERVING) 754 def receive_setup_andx_request_in_serving(self, pkt): 755 self.receive_setup_andx_request(pkt) 756 757 @ATMT.receive_condition(SERVING) 758 def is_smb1_tree(self, pkt): 759 if SMBTree_Connect_AndX in pkt: 760 # Unsupported 761 log_runtime.warning("Tree request in SMB1: unimplemented. Quit") 762 raise self.END() 763 764 @ATMT.receive_condition(SERVING) 765 def receive_tree_connect(self, pkt): 766 if SMB2_Tree_Connect_Request in pkt: 767 tree_name = pkt[SMB2_Tree_Connect_Request].Path.split("\\")[-1] 768 raise self.SERVING().action_parameters(pkt, tree_name) 769 770 @ATMT.action(receive_tree_connect) 771 def send_tree_connect_response(self, pkt, tree_name): 772 self.update_smbheader(pkt) 773 # Check the tree name against the shares we're serving 774 if not any(x._name == tree_name.lower() for x in self.shares): 775 # Unknown tree 776 resp = self.smb_header.copy() / SMB2_Error_Response() 777 resp.Command = "SMB2_TREE_CONNECT" 778 resp.Status = "STATUS_BAD_NETWORK_NAME" 779 self.send(resp) 780 return 781 # Add tree to current trees 782 if tree_name not in self.current_trees: 783 self.tree_id += 1 784 self.smb_header.TID = self.tree_id 785 self.current_trees[self.smb_header.TID] = tree_name 786 self.vprint("Tree Connect on: %s" % tree_name) 787 self.send( 788 self.smb_header 789 / SMB2_Tree_Connect_Response( 790 ShareType="PIPE" if self.current_tree() == "IPC$" else "DISK", 791 ShareFlags="AUTO_CACHING+NO_CACHING" 792 if self.current_tree() == "IPC$" 793 else self.TREE_SHARE_FLAGS, 794 Capabilities=0 795 if self.current_tree() == "IPC$" 796 else self.TREE_CAPABILITIES, 797 MaximalAccess=self.TREE_MAXIMAL_ACCESS, 798 ) 799 ) 800 801 @ATMT.receive_condition(SERVING) 802 def receive_ioctl(self, pkt): 803 if SMB2_IOCTL_Request in pkt: 804 raise self.SERVING().action_parameters(pkt) 805 806 @ATMT.action(receive_ioctl) 807 def send_ioctl_response(self, pkt): 808 self.update_smbheader(pkt) 809 if pkt.CtlCode == 0x11C017: 810 # FSCTL_PIPE_TRANSCEIVE 811 self.rpc_server.recv(pkt.Input.load) 812 self.send( 813 self.smb_header.copy() 814 / SMB2_IOCTL_Response( 815 CtlCode=0x11C017, 816 FileId=pkt[SMB2_IOCTL_Request].FileId, 817 Buffer=[("Output", self.rpc_server.get_response())], 818 ) 819 ) 820 elif pkt.CtlCode == 0x00140204 and self.session.sspcontext.SessionKey: 821 # FSCTL_VALIDATE_NEGOTIATE_INFO 822 # This is a security measure asking the server to validate 823 # what flags were negotiated during the SMBNegotiate exchange. 824 # This packet is ALWAYS signed, and expects a signed response. 825 826 # https://docs.microsoft.com/en-us/archive/blogs/openspecification/smb3-secure-dialect-negotiation 827 # > "Down-level servers (pre-Windows 2012) will return 828 # > STATUS_NOT_SUPPORTED or STATUS_INVALID_DEVICE_REQUEST 829 # > since they do not allow or implement 830 # > FSCTL_VALIDATE_NEGOTIATE_INFO. 831 # > The client should accept the 832 # > response provided it's properly signed". 833 834 if (self.session.Dialect or 0) < 0x0300: 835 # SMB < 3 isn't supposed to support FSCTL_VALIDATE_NEGOTIATE_INFO 836 self._ioctl_error(Status="STATUS_FILE_CLOSED") 837 return 838 839 # SMB3 840 self.send( 841 self.smb_header.copy() 842 / SMB2_IOCTL_Response( 843 CtlCode=0x00140204, 844 FileId=pkt[SMB2_IOCTL_Request].FileId, 845 Buffer=[ 846 ( 847 "Output", 848 SMB2_IOCTL_Validate_Negotiate_Info_Response( 849 GUID=self.GUID, 850 DialectRevision=self.session.Dialect, 851 SecurityMode=self.session.SecurityMode, 852 Capabilities=self.NegotiateCapabilities, 853 ), 854 ) 855 ], 856 ) 857 ) 858 elif pkt.CtlCode == 0x001401FC: 859 # FSCTL_QUERY_NETWORK_INTERFACE_INFO 860 self.send( 861 self.smb_header.copy() 862 / SMB2_IOCTL_Response( 863 CtlCode=0x001401FC, 864 FileId=pkt[SMB2_IOCTL_Request].FileId, 865 Output=SMB2_IOCTL_Network_Interface_Info( 866 interfaces=[ 867 NETWORK_INTERFACE_INFO( 868 SockAddr_Storage=SOCKADDR_STORAGE( 869 Family=0x0002, 870 IPv4Adddress=x, 871 ) 872 ) 873 for x in self.LOCAL_IPS 874 ] 875 ), 876 ) 877 ) 878 elif pkt.CtlCode == 0x00060194: 879 # FSCTL_DFS_GET_REFERRALS 880 if ( 881 self.DOMAIN_REFERRALS 882 and not pkt[SMB2_IOCTL_Request].Input.RequestFileName 883 ): 884 # Requesting domain referrals 885 self.send( 886 self.smb_header.copy() 887 / SMB2_IOCTL_Response( 888 CtlCode=0x00060194, 889 FileId=pkt[SMB2_IOCTL_Request].FileId, 890 Output=SMB2_IOCTL_RESP_GET_DFS_Referral( 891 ReferralEntries=[ 892 DFS_REFERRAL_V3( 893 ReferralEntryFlags="NameListReferral", 894 TimeToLive=600, 895 ) 896 for _ in self.DOMAIN_REFERRALS 897 ], 898 ReferralBuffer=[ 899 DFS_REFERRAL_ENTRY1(SpecialName=name) 900 for name in self.DOMAIN_REFERRALS 901 ], 902 ), 903 ) 904 ) 905 return 906 resp = self.smb_header.copy() / SMB2_Error_Response() 907 resp.Command = "SMB2_IOCTL" 908 resp.Status = "STATUS_FS_DRIVER_REQUIRED" 909 self.send(resp) 910 else: 911 # Among other things, FSCTL_VALIDATE_NEGOTIATE_INFO 912 self._ioctl_error(Status="STATUS_NOT_SUPPORTED") 913 914 @ATMT.receive_condition(SERVING) 915 def receive_create_file(self, pkt): 916 if SMB2_Create_Request in pkt: 917 raise self.SERVING().action_parameters(pkt) 918 919 PIPES_TABLE = { 920 "srvsvc": SMB2_FILEID(Persistent=0x4000000012, Volatile=0x4000000001), 921 "wkssvc": SMB2_FILEID(Persistent=0x4000000013, Volatile=0x4000000002), 922 "NETLOGON": SMB2_FILEID(Persistent=0x4000000014, Volatile=0x4000000003), 923 } 924 925 # special handle in case of compounded requests ([MS-SMB2] 3.2.4.1.4) 926 # that points to the chained opened file handle 927 LAST_HANDLE = SMB2_FILEID( 928 Persistent=0xFFFFFFFFFFFFFFFF, Volatile=0xFFFFFFFFFFFFFFFF 929 ) 930 931 def current_smb_time(self): 932 return ( 933 FileNetworkOpenInformation().get_field("CreationTime").i2m(None, None) 934 - 864000000000 # one day ago 935 ) 936 937 def make_file_id(self, fname): 938 """ 939 Generate deterministic FileId based on the fname 940 """ 941 hash = hashlib.md5((fname or "").encode()).digest() 942 return 0x4000000000 | struct.unpack("<I", hash[:4])[0] 943 944 def lookup_file(self, fname, durable_handle=None, create=False, createOptions=None): 945 """ 946 Lookup the file and build it's SMB2_FILEID 947 """ 948 root = self.root_path() 949 if isinstance(fname, pathlib.Path): 950 path = fname 951 fname = path.name 952 else: 953 path = root / (fname or "").replace("\\", "/") 954 path = path.resolve() 955 # Word of caution: this check ONLY works because root and path have been 956 # resolve(). Be careful 957 # Note: symbolic links are currently unsupported. 958 if root not in path.parents and path != root: 959 raise FileNotFoundError 960 if path.is_reserved(): 961 raise FileNotFoundError 962 if not path.exists(): 963 if create and createOptions: 964 if createOptions.FILE_DIRECTORY_FILE: 965 # Folder creation 966 path.mkdir() 967 self.vprint("Created folder:" + fname) 968 else: 969 # File creation 970 path.touch() 971 self.vprint("Created file:" + fname) 972 else: 973 raise FileNotFoundError 974 if durable_handle is None: 975 handle = SMB2_FILEID( 976 Persistent=self.make_file_id(fname) + self.smb_header.MID, 977 ) 978 else: 979 # We were given a durable handle. Use it 980 handle = durable_handle 981 attrs = { 982 "CreationTime": self.base_time_t, 983 "LastAccessTime": self.base_time_t, 984 "LastWriteTime": self.base_time_t, 985 "ChangeTime": self.base_time_t, 986 "EndOfFile": 0, 987 "AllocationSize": 0, 988 } 989 path_stat = path.stat() 990 attrs["EndOfFile"] = attrs["AllocationSize"] = path_stat.st_size 991 if fname is None: 992 # special case 993 attrs["FileAttributes"] = "+".join( 994 [ 995 "FILE_ATTRIBUTE_HIDDEN", 996 "FILE_ATTRIBUTE_SYSTEM", 997 "FILE_ATTRIBUTE_DIRECTORY", 998 ] 999 ) 1000 elif path.is_dir(): 1001 attrs["FileAttributes"] = "FILE_ATTRIBUTE_DIRECTORY" 1002 else: 1003 attrs["FileAttributes"] = "FILE_ATTRIBUTE_ARCHIVE" 1004 self.current_handles[handle] = ( 1005 path, # file path 1006 attrs, # file attributes 1007 ) 1008 self.enumerate_index[handle] = 0 1009 return handle 1010 1011 def set_compounded_handle(self, handle): 1012 """ 1013 Mark a handle as the current one being compounded. 1014 """ 1015 self.CompoundedHandle = handle 1016 1017 def get_file_id(self, pkt): 1018 """ 1019 Return the FileId attribute of pkt, accounting for compounded requests. 1020 """ 1021 fid = pkt.FileId 1022 if fid == self.LAST_HANDLE: 1023 return self.CompoundedHandle 1024 return fid 1025 1026 def lookup_folder(self, handle, filter, offset, cls): 1027 """ 1028 Lookup a folder handle 1029 """ 1030 path = self.current_handles[handle][0] 1031 self.vprint("Query directory: " + str(path)) 1032 self.current_handles[handle][1]["LastAccessTime"] = self.current_smb_time() 1033 if not path.is_dir(): 1034 raise NotADirectoryError 1035 return sorted( 1036 [ 1037 cls(FileName=x.name, **self.current_handles[self.lookup_file(x)][1]) 1038 for x in path.glob(filter) 1039 # Note: symbolic links are unsupported because it's hard to check 1040 # for path traversal on them. 1041 if not x.is_symlink() 1042 ] 1043 + [ 1044 cls( 1045 FileAttributes=("FILE_ATTRIBUTE_DIRECTORY"), 1046 FileName=".", 1047 ) 1048 ] 1049 + ( 1050 [ 1051 cls( 1052 FileAttributes=("FILE_ATTRIBUTE_DIRECTORY"), 1053 FileName="..", 1054 ) 1055 ] 1056 if path.resolve() != self.root_path() 1057 else [] 1058 ), 1059 key=lambda x: x.FileName, 1060 )[offset:] 1061 1062 @ATMT.action(receive_create_file) 1063 def send_create_file_response(self, pkt): 1064 """ 1065 Handle CreateFile request 1066 1067 See [MS-SMB2] 3.3.5.9 () 1068 """ 1069 self.update_smbheader(pkt) 1070 if pkt[SMB2_Create_Request].NameLen: 1071 fname = pkt[SMB2_Create_Request].Name 1072 else: 1073 fname = None 1074 if fname: 1075 self.vprint("Opened: " + fname) 1076 if self.current_tree() == "IPC$": 1077 # Special IPC$ case: opening a pipe 1078 FILE_ID = self.PIPES_TABLE.get(fname, None) 1079 if FILE_ID: 1080 attrs = { 1081 "CreationTime": 0, 1082 "LastAccessTime": 0, 1083 "LastWriteTime": 0, 1084 "ChangeTime": 0, 1085 "EndOfFile": 0, 1086 "AllocationSize": 4096, 1087 } 1088 self.current_handles[FILE_ID] = ( 1089 fname, 1090 attrs, 1091 ) 1092 self.send( 1093 self.smb_header.copy() 1094 / SMB2_Create_Response( 1095 OplockLevel=pkt.RequestedOplockLevel, 1096 FileId=FILE_ID, 1097 **attrs, 1098 ) 1099 ) 1100 else: 1101 # NOT_FOUND 1102 resp = self.smb_header.copy() / SMB2_Error_Response() 1103 resp.Command = "SMB2_CREATE" 1104 resp.Status = "STATUS_OBJECT_NAME_NOT_FOUND" 1105 self.send(resp) 1106 return 1107 else: 1108 # Check if there is a Durable Handle Reconnect Request 1109 durable_handle = None 1110 if pkt[SMB2_Create_Request].CreateContextsLen: 1111 try: 1112 durable_handle = next( 1113 x.Data.FileId 1114 for x in pkt[SMB2_Create_Request].CreateContexts 1115 if x.Name == b"DH2C" 1116 ) 1117 except StopIteration: 1118 pass 1119 # Lookup file handle 1120 try: 1121 handle = self.lookup_file(fname, durable_handle=durable_handle) 1122 except FileNotFoundError: 1123 # NOT_FOUND 1124 if pkt[SMB2_Create_Request].CreateDisposition in [ 1125 0x00000002, # FILE_CREATE 1126 0x00000005, # FILE_OVERWRITE_IF 1127 ]: 1128 if self.readonly: 1129 resp = self.smb_header.copy() / SMB2_Error_Response() 1130 resp.Command = "SMB2_CREATE" 1131 resp.Status = "STATUS_ACCESS_DENIED" 1132 self.send(resp) 1133 return 1134 else: 1135 # Create file 1136 handle = self.lookup_file( 1137 fname, 1138 durable_handle=durable_handle, 1139 create=True, 1140 createOptions=pkt[SMB2_Create_Request].CreateOptions, 1141 ) 1142 else: 1143 resp = self.smb_header.copy() / SMB2_Error_Response() 1144 resp.Command = "SMB2_CREATE" 1145 resp.Status = "STATUS_OBJECT_NAME_NOT_FOUND" 1146 self.send(resp) 1147 return 1148 # Store compounded handle 1149 self.set_compounded_handle(handle) 1150 # Build response 1151 attrs = self.current_handles[handle][1] 1152 resp = self.smb_header.copy() / SMB2_Create_Response( 1153 OplockLevel=pkt.RequestedOplockLevel, 1154 FileId=handle, 1155 **attrs, 1156 ) 1157 # Handle the various chain elements 1158 if pkt[SMB2_Create_Request].CreateContextsLen: 1159 CreateContexts = [] 1160 # Note: failing to provide context elements when the client asks for 1161 # them will make the windows implementation fall into a weird 1162 # "the-server-is-dumb" mode. So provide them 'quoi qu'il en coûte'. 1163 for elt in pkt[SMB2_Create_Request].CreateContexts: 1164 if elt.Name == b"QFid": 1165 # [MS-SMB2] sect 3.3.5.9.9 1166 CreateContexts.append( 1167 SMB2_Create_Context( 1168 Name=b"QFid", 1169 Data=SMB2_CREATE_QUERY_ON_DISK_ID( 1170 DiskFileId=self.make_file_id(fname), 1171 VolumeId=0xBA39CD11, 1172 ), 1173 ) 1174 ) 1175 elif elt.Name == b"MxAc": 1176 # [MS-SMB2] sect 3.3.5.9.5 1177 CreateContexts.append( 1178 SMB2_Create_Context( 1179 Name=b"MxAc", 1180 Data=SMB2_CREATE_QUERY_MAXIMAL_ACCESS_RESPONSE( 1181 QueryStatus=0, 1182 MaximalAccess=self.FILE_MAXIMAL_ACCESS, 1183 ), 1184 ) 1185 ) 1186 elif elt.Name == b"DH2Q": 1187 # [MS-SMB2] sect 3.3.5.9.10 1188 if "FILE_ATTRIBUTE_DIRECTORY" in attrs["FileAttributes"]: 1189 continue 1190 CreateContexts.append( 1191 SMB2_Create_Context( 1192 Name=b"DH2Q", 1193 Data=SMB2_CREATE_DURABLE_HANDLE_RESPONSE_V2( 1194 Timeout=180000 1195 ), 1196 ) 1197 ) 1198 elif elt.Name == b"RqLs": 1199 # [MS-SMB2] sect 3.3.5.9.11 1200 # TODO: hmm, we are probably supposed to do something here 1201 CreateContexts.append( 1202 SMB2_Create_Context( 1203 Name=b"RqLs", 1204 Data=elt.Data, 1205 ) 1206 ) 1207 resp.CreateContexts = CreateContexts 1208 self.send(resp) 1209 1210 @ATMT.receive_condition(SERVING) 1211 def receive_change_notify_info(self, pkt): 1212 if SMB2_Change_Notify_Request in pkt: 1213 raise self.SERVING().action_parameters(pkt) 1214 1215 @ATMT.action(receive_change_notify_info) 1216 def send_change_notify_info_response(self, pkt): 1217 # [MS-SMB2] sect 3.3.5.19 1218 # "If the underlying object store does not support change notifications, the 1219 # server MUST fail this request with STATUS_NOT_SUPPORTED." 1220 self.update_smbheader(pkt) 1221 resp = self.smb_header.copy() / SMB2_Error_Response() 1222 resp.Command = "SMB2_CHANGE_NOTIFY" 1223 # ScapyFS doesn't support notifications 1224 resp.Status = "STATUS_NOT_SUPPORTED" 1225 self.send(resp) 1226 1227 @ATMT.receive_condition(SERVING) 1228 def receive_query_directory_info(self, pkt): 1229 if SMB2_Query_Directory_Request in pkt: 1230 raise self.SERVING().action_parameters(pkt) 1231 1232 @ATMT.action(receive_query_directory_info) 1233 def send_query_directory_response(self, pkt): 1234 self.update_smbheader(pkt) 1235 if not pkt.FileNameLen: 1236 # this is broken. 1237 return 1238 query = pkt.FileName 1239 fid = self.get_file_id(pkt) 1240 # Check for handled FileInformationClass 1241 # 0x02: FileFullDirectoryInformation 1242 # 0x03: FileBothDirectoryInformation 1243 # 0x25: FileIdBothDirectoryInformation 1244 if pkt.FileInformationClass not in [0x02, 0x03, 0x25]: 1245 # Unknown FileInformationClass 1246 resp = self.smb_header.copy() / SMB2_Error_Response() 1247 resp.Command = "SMB2_QUERY_DIRECTORY" 1248 resp.Status = "STATUS_INVALID_INFO_CLASS" 1249 self.send(resp) 1250 return 1251 # Handle SMB2_RESTART_SCANS 1252 if pkt[SMB2_Query_Directory_Request].Flags.SMB2_RESTART_SCANS: 1253 self.enumerate_index[fid] = 0 1254 # Lookup the files 1255 try: 1256 files = self.lookup_folder( 1257 fid, 1258 query, 1259 self.enumerate_index[fid], 1260 { 1261 0x02: FILE_FULL_DIR_INFORMATION, 1262 0x03: FILE_BOTH_DIR_INFORMATION, 1263 0x25: FILE_ID_BOTH_DIR_INFORMATION, 1264 }[pkt.FileInformationClass], 1265 ) 1266 except NotADirectoryError: 1267 resp = self.smb_header.copy() / SMB2_Error_Response() 1268 resp.Command = "SMB2_QUERY_DIRECTORY" 1269 resp.Status = "STATUS_INVALID_PARAMETER" 1270 self.send(resp) 1271 return 1272 if not files: 1273 # No more files ! 1274 self.enumerate_index[fid] = 0 1275 resp = self.smb_header.copy() / SMB2_Error_Response() 1276 resp.Command = "SMB2_QUERY_DIRECTORY" 1277 resp.Status = "STATUS_NO_MORE_FILES" 1278 self.send(resp) 1279 return 1280 # Handle SMB2_RETURN_SINGLE_ENTRY 1281 if pkt[SMB2_Query_Directory_Request].Flags.SMB2_RETURN_SINGLE_ENTRY: 1282 files = files[:1] 1283 # Increment index 1284 self.enumerate_index[fid] += len(files) 1285 # Build response based on the FileInformationClass 1286 fileinfo = FileIdBothDirectoryInformation( 1287 files=files, 1288 ) 1289 self.send( 1290 self.smb_header.copy() 1291 / SMB2_Query_Directory_Response(Buffer=[("Output", fileinfo)]) 1292 ) 1293 1294 @ATMT.receive_condition(SERVING) 1295 def receive_query_info(self, pkt): 1296 if SMB2_Query_Info_Request in pkt: 1297 raise self.SERVING().action_parameters(pkt) 1298 1299 @ATMT.action(receive_query_info) 1300 def send_query_info_response(self, pkt): 1301 self.update_smbheader(pkt) 1302 # [MS-FSCC] + [MS-SMB2] sect 2.2.37 / 3.3.5.20.1 1303 fid = self.get_file_id(pkt) 1304 if pkt.InfoType == 0x01: # SMB2_0_INFO_FILE 1305 if pkt.FileInfoClass == 0x05: # FileStandardInformation 1306 attrs = self.current_handles[fid][1] 1307 fileinfo = FileStandardInformation( 1308 EndOfFile=attrs["EndOfFile"], 1309 AllocationSize=attrs["AllocationSize"], 1310 ) 1311 elif pkt.FileInfoClass == 0x06: # FileInternalInformation 1312 pth = self.current_handles[fid][0] 1313 fileinfo = FileInternalInformation( 1314 IndexNumber=hash(pth) & 0xFFFFFFFFFFFFFFFF, 1315 ) 1316 elif pkt.FileInfoClass == 0x07: # FileEaInformation 1317 fileinfo = FileEaInformation() 1318 elif pkt.FileInfoClass == 0x12: # FileAllInformation 1319 attrs = self.current_handles[fid][1] 1320 fileinfo = FileAllInformation( 1321 BasicInformation=FileBasicInformation( 1322 CreationTime=attrs["CreationTime"], 1323 LastAccessTime=attrs["LastAccessTime"], 1324 LastWriteTime=attrs["LastWriteTime"], 1325 ChangeTime=attrs["ChangeTime"], 1326 FileAttributes=attrs["FileAttributes"], 1327 ), 1328 StandardInformation=FileStandardInformation( 1329 EndOfFile=attrs["EndOfFile"], 1330 AllocationSize=attrs["AllocationSize"], 1331 ), 1332 ) 1333 elif pkt.FileInfoClass == 0x15: # FileAlternateNameInformation 1334 pth = self.current_handles[fid][0] 1335 fileinfo = FileAlternateNameInformation( 1336 FileName=pth.name, 1337 ) 1338 elif pkt.FileInfoClass == 0x16: # FileStreamInformation 1339 attrs = self.current_handles[fid][1] 1340 fileinfo = FileStreamInformation( 1341 StreamSize=attrs["EndOfFile"], 1342 StreamAllocationSize=attrs["AllocationSize"], 1343 ) 1344 elif pkt.FileInfoClass == 0x22: # FileNetworkOpenInformation 1345 attrs = self.current_handles[fid][1] 1346 fileinfo = FileNetworkOpenInformation( 1347 **attrs, 1348 ) 1349 elif pkt.FileInfoClass == 0x30: # FileNormalizedNameInformation 1350 pth = self.current_handles[fid][0] 1351 fileinfo = FILE_NAME_INFORMATION( 1352 FileName=pth.name, 1353 ) 1354 else: 1355 log_runtime.warning( 1356 "Unimplemented: %s" 1357 % pkt[SMB2_Query_Info_Request].sprintf("%InfoType% %FileInfoClass%") 1358 ) 1359 return 1360 elif pkt.InfoType == 0x02: # SMB2_0_INFO_FILESYSTEM 1361 # [MS-FSCC] sect 2.5 1362 if pkt.FileInfoClass == 0x01: # FileFsVolumeInformation 1363 fileinfo = FileFsVolumeInformation() 1364 elif pkt.FileInfoClass == 0x03: # FileFsSizeInformation 1365 fileinfo = FileFsSizeInformation() 1366 elif pkt.FileInfoClass == 0x05: # FileFsAttributeInformation 1367 fileinfo = FileFsAttributeInformation( 1368 FileSystemAttributes=0x88000F, 1369 ) 1370 elif pkt.FileInfoClass == 0x07: # FileEaInformation 1371 fileinfo = FileEaInformation() 1372 else: 1373 log_runtime.warning( 1374 "Unimplemented: %s" 1375 % pkt[SMB2_Query_Info_Request].sprintf("%InfoType% %FileInfoClass%") 1376 ) 1377 return 1378 elif pkt.InfoType == 0x03: # SMB2_0_INFO_SECURITY 1379 # [MS-FSCC] 2.4.6 1380 fileinfo = SECURITY_DESCRIPTOR() 1381 # TODO: fill it 1382 if pkt.AdditionalInformation.OWNER_SECURITY_INFORMATION: 1383 pass 1384 if pkt.AdditionalInformation.GROUP_SECURITY_INFORMATION: 1385 pass 1386 if pkt.AdditionalInformation.DACL_SECURITY_INFORMATION: 1387 pass 1388 if pkt.AdditionalInformation.SACL_SECURITY_INFORMATION: 1389 pass 1390 # Observed: 1391 if ( 1392 pkt.AdditionalInformation.OWNER_SECURITY_INFORMATION 1393 or pkt.AdditionalInformation.SACL_SECURITY_INFORMATION 1394 or pkt.AdditionalInformation.GROUP_SECURITY_INFORMATION 1395 or pkt.AdditionalInformation.DACL_SECURITY_INFORMATION 1396 ): 1397 pkt = self.smb_header.copy() / SMB2_Error_Response(ErrorData=b"\xff") 1398 pkt.Status = "STATUS_ACCESS_DENIED" 1399 pkt.Command = "SMB2_QUERY_INFO" 1400 self.send(pkt) 1401 return 1402 if pkt.AdditionalInformation.ATTRIBUTE_SECURITY_INFORMATION: 1403 fileinfo.Control = 0x8800 1404 self.send( 1405 self.smb_header.copy() 1406 / SMB2_Query_Info_Response(Buffer=[("Output", fileinfo)]) 1407 ) 1408 1409 @ATMT.receive_condition(SERVING) 1410 def receive_set_info_request(self, pkt): 1411 if SMB2_Set_Info_Request in pkt: 1412 raise self.SERVING().action_parameters(pkt) 1413 1414 @ATMT.action(receive_set_info_request) 1415 def send_set_info_response(self, pkt): 1416 self.update_smbheader(pkt) 1417 self.send(self.smb_header.copy() / SMB2_Set_Info_Response()) 1418 1419 @ATMT.receive_condition(SERVING) 1420 def receive_write_request(self, pkt): 1421 if SMB2_Write_Request in pkt: 1422 raise self.SERVING().action_parameters(pkt) 1423 1424 @ATMT.action(receive_write_request) 1425 def send_write_response(self, pkt): 1426 self.update_smbheader(pkt) 1427 resp = SMB2_Write_Response(Count=len(pkt.Data)) 1428 fid = self.get_file_id(pkt) 1429 if self.current_tree() == "IPC$": 1430 if fid in self.PIPES_TABLE.values(): 1431 # A pipe 1432 self.rpc_server.recv(pkt.Data) 1433 else: 1434 if self.readonly: 1435 # Read only ! 1436 resp = SMB2_Error_Response() 1437 resp.Command = "SMB2_WRITE" 1438 resp.Status = "ERROR_FILE_READ_ONLY" 1439 else: 1440 # Write file 1441 pth, _ = self.current_handles[fid] 1442 length = pkt[SMB2_Write_Request].DataLen 1443 off = pkt[SMB2_Write_Request].Offset 1444 self.vprint("Writing %s bytes at %s" % (length, off)) 1445 with open(pth, "r+b") as fd: 1446 fd.seek(off) 1447 resp.Count = fd.write(pkt[SMB2_Write_Request].Data) 1448 self.send(self.smb_header.copy() / resp) 1449 1450 @ATMT.receive_condition(SERVING) 1451 def receive_read_request(self, pkt): 1452 if SMB2_Read_Request in pkt: 1453 raise self.SERVING().action_parameters(pkt) 1454 1455 @ATMT.action(receive_read_request) 1456 def send_read_response(self, pkt): 1457 self.update_smbheader(pkt) 1458 resp = SMB2_Read_Response() 1459 fid = self.get_file_id(pkt) 1460 if self.current_tree() == "IPC$": 1461 # Read output from DCE/RPC server 1462 r = self.rpc_server.get_response() 1463 resp.Data = bytes(r) 1464 else: 1465 # Read file and send content 1466 pth, _ = self.current_handles[fid] 1467 length = pkt[SMB2_Read_Request].Length 1468 off = pkt[SMB2_Read_Request].Offset 1469 self.vprint("Reading %s bytes at %s" % (length, off)) 1470 with open(pth, "rb") as fd: 1471 fd.seek(off) 1472 resp.Data = fd.read(length) 1473 self.send(self.smb_header.copy() / resp) 1474 1475 @ATMT.receive_condition(SERVING) 1476 def receive_close_request(self, pkt): 1477 if SMB2_Close_Request in pkt: 1478 raise self.SERVING().action_parameters(pkt) 1479 1480 @ATMT.action(receive_close_request) 1481 def send_close_response(self, pkt): 1482 self.update_smbheader(pkt) 1483 if self.current_tree() != "IPC$": 1484 fid = self.get_file_id(pkt) 1485 pth, attrs = self.current_handles[fid] 1486 if pth: 1487 self.vprint("Closed: " + str(pth)) 1488 del self.current_handles[fid] 1489 del self.enumerate_index[fid] 1490 self.send( 1491 self.smb_header.copy() 1492 / SMB2_Close_Response( 1493 Flags=pkt[SMB2_Close_Request].Flags, 1494 **attrs, 1495 ) 1496 ) 1497 else: 1498 self.send(self.smb_header.copy() / SMB2_Close_Response()) 1499 1500 @ATMT.receive_condition(SERVING) 1501 def receive_tree_disconnect_request(self, pkt): 1502 if SMB2_Tree_Disconnect_Request in pkt: 1503 raise self.SERVING().action_parameters(pkt) 1504 1505 @ATMT.action(receive_tree_disconnect_request) 1506 def send_tree_disconnect_response(self, pkt): 1507 self.update_smbheader(pkt) 1508 try: 1509 del self.current_trees[self.smb_header.TID] # clear tree 1510 resp = self.smb_header.copy() / SMB2_Tree_Disconnect_Response() 1511 except KeyError: 1512 resp = self.smb_header.copy() / SMB2_Error_Response() 1513 resp.Command = "SMB2_TREE_DISCONNECT" 1514 resp.Status = "STATUS_NETWORK_NAME_DELETED" 1515 self.send(resp) 1516 1517 @ATMT.receive_condition(SERVING) 1518 def receive_cancel_request(self, pkt): 1519 if SMB2_Cancel_Request in pkt: 1520 raise self.SERVING().action_parameters(pkt) 1521 1522 @ATMT.action(receive_cancel_request) 1523 def send_notify_cancel_response(self, pkt): 1524 self.update_smbheader(pkt) 1525 resp = self.smb_header.copy() / SMB2_Change_Notify_Response() 1526 resp.Status = "STATUS_CANCELLED" 1527 self.send(resp) 1528 1529 @ATMT.receive_condition(SERVING) 1530 def receive_echo_request(self, pkt): 1531 if SMB2_Echo_Request in pkt: 1532 raise self.SERVING().action_parameters(pkt) 1533 1534 @ATMT.action(receive_echo_request) 1535 def send_echo_reply(self, pkt): 1536 self.update_smbheader(pkt) 1537 self.send(self.smb_header.copy() / SMB2_Echo_Response()) 1538 1539 1540# DCE/RPC server for SMB 1541 1542 1543class SMB_DCERPC_Server(DCERPC_Server): 1544 """ 1545 DCE/RPC server than handles the minimum RPCs for SMB to work: 1546 """ 1547 1548 def __init__(self, *args, **kwargs): 1549 self.shares = kwargs.pop("shares") 1550 super(SMB_DCERPC_Server, self).__init__(*args, **kwargs) 1551 1552 @DCERPC_Server.answer(NetrShareEnum_Request) 1553 def netr_share_enum(self, req): 1554 """ 1555 NetrShareEnum [MS-SRVS] 1556 "retrieves information about each shared resource on a server." 1557 """ 1558 nbEntries = len(self.shares) 1559 return NetrShareEnum_Response( 1560 InfoStruct=LPSHARE_ENUM_STRUCT( 1561 Level=1, 1562 ShareInfo=NDRUnion( 1563 tag=1, 1564 value=SHARE_INFO_1_CONTAINER( 1565 Buffer=[ 1566 # Add shares 1567 LPSHARE_INFO_1( 1568 shi1_netname=x.name, 1569 shi1_type=x.type, 1570 shi1_remark=x.remark, 1571 ) 1572 for x in self.shares 1573 ], 1574 EntriesRead=nbEntries, 1575 ), 1576 ), 1577 ), 1578 TotalEntries=nbEntries, 1579 ndr64=self.ndr64, 1580 ) 1581 1582 @DCERPC_Server.answer(NetrWkstaGetInfo_Request) 1583 def netr_wksta_getinfo(self, req): 1584 """ 1585 NetrWkstaGetInfo [MS-SRVS] 1586 "returns information about the configuration of a workstation." 1587 """ 1588 return NetrWkstaGetInfo_Response( 1589 WkstaInfo=NDRUnion( 1590 tag=100, 1591 value=LPWKSTA_INFO_100( 1592 wki100_platform_id=500, # NT 1593 wki100_ver_major=5, 1594 ), 1595 ), 1596 ndr64=self.ndr64, 1597 ) 1598 1599 @DCERPC_Server.answer(NetrServerGetInfo_Request) 1600 def netr_server_getinfo(self, req): 1601 """ 1602 NetrServerGetInfo [MS-WKST] 1603 "retrieves current configuration information for CIFS and 1604 SMB Version 1.0 servers." 1605 """ 1606 return NetrServerGetInfo_Response( 1607 ServerInfo=NDRUnion( 1608 tag=101, 1609 value=LPSERVER_INFO_101( 1610 sv101_platform_id=500, # NT 1611 sv101_name=req.ServerName.value.value[0].value, 1612 sv101_version_major=6, 1613 sv101_version_minor=1, 1614 sv101_type=1, # Workstation 1615 ), 1616 ), 1617 ndr64=self.ndr64, 1618 ) 1619 1620 @DCERPC_Server.answer(NetrShareGetInfo_Request) 1621 def netr_share_getinfo(self, req): 1622 """ 1623 NetrShareGetInfo [MS-SRVS] 1624 "retrieves information about a particular shared resource on a server." 1625 """ 1626 return NetrShareGetInfo_Response( 1627 ShareInfo=NDRUnion( 1628 tag=1, 1629 value=LPSHARE_INFO_1( 1630 shi1_netname=req.NetName.value[0].value, 1631 shi1_type=0, 1632 shi1_remark=b"", 1633 ), 1634 ), 1635 ndr64=self.ndr64, 1636 ) 1637 1638 1639# Util 1640 1641 1642class smbserver: 1643 r""" 1644 Spawns a simple smbserver 1645 1646 smbserver parameters: 1647 1648 :param shares: the list of shares to announce. Note that IPC$ is appended. 1649 By default, a 'Scapy' share on './' 1650 :param port: (optional) the port to bind on, default 445 1651 :param iface: (optional) the interface to bind on, default conf.iface 1652 :param readonly: (optional) whether the server is read-only or not. default True 1653 :param ssp: (optional) the SSP to use. See the examples below. 1654 Default NTLM with guest 1655 1656 Many more SMB-specific parameters are available in help(SMB_Server) 1657 """ 1658 1659 def __init__( 1660 self, 1661 shares=None, 1662 iface: str = None, 1663 port: int = 445, 1664 verb: int = 2, 1665 readonly: bool = True, 1666 # SMB arguments 1667 ssp=None, 1668 **kwargs, 1669 ): 1670 # Default Share 1671 if shares is None: 1672 shares = [ 1673 SMBShare( 1674 name="Scapy", path=".", remark="Scapy's SMB server default share" 1675 ) 1676 ] 1677 # Verb 1678 if verb >= 2: 1679 log_runtime.info("-- Scapy %s SMB Server --" % conf.version) 1680 log_runtime.info( 1681 "SSP: %s. Read-Only: %s. Serving %s shares:" 1682 % ( 1683 conf.color_theme.yellow(ssp or "NTLM (guest)"), 1684 ( 1685 conf.color_theme.yellow("YES") 1686 if readonly 1687 else conf.color_theme.format("NO", "bg_red+white") 1688 ), 1689 conf.color_theme.red(len(shares)), 1690 ) 1691 ) 1692 for share in shares: 1693 log_runtime.info(" * %s" % share) 1694 # Start SMB Server 1695 self.srv = SMB_Server.spawn( 1696 # TCP server 1697 port=port, 1698 iface=iface or conf.loopback_name, 1699 verb=verb, 1700 # SMB server 1701 ssp=ssp, 1702 shares=shares, 1703 readonly=readonly, 1704 # SMB arguments 1705 **kwargs, 1706 ) 1707 1708 def close(self): 1709 """ 1710 Close the smbserver if started in background mode (bg=True) 1711 """ 1712 if self.srv: 1713 try: 1714 self.srv.shutdown(socket.SHUT_RDWR) 1715 except OSError: 1716 pass 1717 self.srv.close() 1718 1719 1720if __name__ == "__main__": 1721 from scapy.utils import AutoArgparse 1722 1723 AutoArgparse(smbserver) 1724