smbclientng.core.SMBSession
1#!/usr/bin/env python3 2# -*- coding: utf-8 -*- 3# File name : smbclient-ng.py 4# Author : Podalirius (@podalirius_) 5# Date created : 20 may 2024 6 7 8import io 9import impacket.smbconnection 10import ntpath 11import os 12import random 13import re 14import sys 15import traceback 16from smbclientng.core.LocalFileIO import LocalFileIO 17from smbclientng.core.utils import b_filesize, STYPE_MASK, is_port_open 18 19 20class SMBSession(object): 21 """ 22 Represents an SMB session for interacting with an SMB server. 23 24 This class provides methods to manage and interact with an SMB server, including 25 connecting to the server, listing shares, uploading and downloading files, and 26 managing directories and files on the server. It handles session initialization, 27 authentication, and cleanup. 28 29 Attributes: 30 host (str): The hostname or IP address of the SMB server. 31 port (int): The port number on which the SMB server is listening. 32 credentials (dict): Authentication credentials for the SMB server. 33 config (dict, optional): Configuration options for the SMB session. 34 smbClient (impacket.smbconnection.SMBConnection): The SMB connection instance. 35 connected (bool): Connection status to the SMB server. 36 available_shares (dict): A dictionary of available SMB shares. 37 smb_share (str): The current SMB share in use. 38 smb_cwd (str): The current working directory on the SMB share. 39 smb_tree_id (int): The tree ID of the connected SMB share. 40 41 Methods: 42 close_smb_session(): Closes the current SMB session. 43 init_smb_session(): Initializes the SMB session with the server. 44 list_shares(): Lists all shares available on the SMB server. 45 set_share(shareName): Sets the current SMB share. 46 set_cwd(path): Sets the current working directory on the SMB share. 47 put_file(localpath): Uploads a file to the current SMB share. 48 get_file(remotepath, localpath): Downloads a file from the SMB share. 49 mkdir(path): Creates a directory on the SMB share. 50 rmdir(path): Removes a directory from the SMB share. 51 rm(path): Removes a file from the SMB share. 52 read_file(path): Reads a file from the SMB share. 53 test_rights(sharename): Tests read and write access rights on a share. 54 """ 55 56 def __init__(self, host, port, credentials, config=None, logger=None): 57 super(SMBSession, self).__init__() 58 # Objects 59 self.config = config 60 self.logger = logger 61 62 # Target server 63 self.host = host 64 # Target port (by default on 445) 65 self.port = port 66 67 # Credentials 68 self.credentials = credentials 69 70 self.smbClient = None 71 self.connected = False 72 73 self.available_shares = {} 74 self.smb_share = None 75 self.smb_cwd = "" 76 self.smb_tree_id = None 77 78 self.list_shares() 79 80 # Connect and disconnect SMB session 81 82 def close_smb_session(self): 83 """ 84 Closes the current SMB session by disconnecting the SMB client. 85 86 This method ensures that the SMB client connection is properly closed. It checks if the client is connected 87 and if so, it closes the connection and resets the connection status. 88 89 Raises: 90 Exception: If the SMB client is not initialized or if there's an error during the disconnection process. 91 """ 92 93 if self.smbClient is not None: 94 if self.connected: 95 self.smbClient.close() 96 self.connected = False 97 self.logger.debug("[+] SMB connection closed successfully.") 98 else: 99 self.logger.debug("[!] No active SMB connection to close.") 100 else: 101 raise Exception("SMB client is not initialized.") 102 103 def init_smb_session(self): 104 """ 105 Initializes and establishes a session with the SMB server. 106 107 This method sets up the SMB connection using either Kerberos or NTLM authentication based on the configuration. 108 It attempts to connect to the SMB server specified by the `address` attribute and authenticate using the credentials provided during the object's initialization. 109 110 The method will print debug information if the `debug` attribute is set to True. Upon successful connection and authentication, it sets the `connected` attribute to True. 111 112 Returns: 113 bool: True if the connection and authentication are successful, False otherwise. 114 """ 115 116 self.connected = False 117 118 self.logger.debug("[>] Connecting to remote SMB server '%s' ... " % self.host) 119 120 try: 121 if is_port_open(self.host, self.port): 122 self.smbClient = impacket.smbconnection.SMBConnection( 123 remoteName=self.host, 124 remoteHost=self.host, 125 sess_port=int(self.port) 126 ) 127 else: 128 self.connected = False 129 except OSError as err: 130 if self.config.debug: 131 traceback.print_exc() 132 self.logger.error(err) 133 self.smbClient = None 134 135 if self.smbClient is not None: 136 if self.credentials.use_kerberos: 137 self.logger.debug("[>] Authenticating as '%s\\%s' with kerberos ... " % (self.credentials.domain, self.credentials.username)) 138 try: 139 self.connected = self.smbClient.kerberosLogin( 140 user=self.credentials.username, 141 password=self.credentials.password, 142 domain=self.credentials.domain, 143 lmhash=self.credentials.lm_hex, 144 nthash=self.credentials.nt_hex, 145 aesKey=self.credentials.aesKey, 146 kdcHost=self.credentials.kdcHost 147 ) 148 except impacket.smbconnection.SessionError as err: 149 if self.config.debug: 150 traceback.print_exc() 151 self.logger.error("Could not login: %s" % err) 152 self.connected = False 153 154 else: 155 self.logger.debug("[>] Authenticating as '%s\\%s' with NTLM ... " % (self.credentials.domain, self.credentials.username)) 156 157 try: 158 self.connected = self.smbClient.login( 159 user=self.credentials.username, 160 password=self.credentials.password, 161 domain=self.credentials.domain, 162 lmhash=self.credentials.lm_hex, 163 nthash=self.credentials.nt_hex 164 ) 165 except impacket.smbconnection.SessionError as err: 166 if self.config.debug: 167 traceback.print_exc() 168 self.logger.error("Could not login: %s" % err) 169 self.connected = False 170 171 if self.connected: 172 self.logger.print("[+] Successfully authenticated to '%s' as '%s\\%s'!" % (self.host, self.credentials.domain, self.credentials.username)) 173 else: 174 self.logger.error("Failed to authenticate to '%s' as '%s\\%s'!" % (self.host, self.credentials.domain, self.credentials.username)) 175 176 return self.connected 177 178 def ping_smb_session(self): 179 """ 180 Tests the connectivity to the SMB server by sending an echo command. 181 182 This method attempts to send an echo command to the SMB server to check if the session is still active. 183 It updates the `connected` attribute of the class based on the success or failure of the echo command. 184 185 Returns: 186 bool: True if the echo command succeeds (indicating the session is active), False otherwise. 187 """ 188 189 if not is_port_open(self.host, self.port): 190 self.connected = False 191 else: 192 try: 193 self.smbClient.getSMBServer().echo() 194 except Exception as e: 195 self.connected = False 196 197 return self.connected 198 199 # Operations 200 201 def find(self, paths=[], callback=None): 202 """ 203 Finds files and directories on the SMB share based on the provided paths and executes a callback function on each entry. 204 205 This method traverses the specified paths on the SMB share, recursively exploring directories and invoking the callback 206 function on each file or directory found. The callback function is called with three arguments: the entry object, the 207 full path of the entry, and the current depth of recursion. 208 209 Args: 210 paths (list, optional): A list of paths to start the search from. Defaults to an empty list. 211 callback (function, optional): A function to be called on each entry found. The function should accept three arguments: 212 the entry object, the full path of the entry, and the current depth of recursion. Defaults to None. 213 214 Note: 215 If the callback function is None, the method will print an error message and return without performing any action. 216 """ 217 218 def recurse_action(paths=[], depth=0, callback=None): 219 if callback is None: 220 return [] 221 222 next_directories_to_explore = [] 223 224 for path in paths: 225 remote_smb_path = ntpath.normpath(self.smb_cwd + ntpath.sep + path) 226 entries = [] 227 228 try: 229 entries = self.smbClient.listPath( 230 shareName=self.smb_share, 231 path=(remote_smb_path + ntpath.sep + '*') 232 ) 233 except impacket.smbconnection.SessionError as err: 234 continue 235 # Remove dot names 236 entries = [e for e in entries if e.get_longname() not in [".", ".."]] 237 # Sort the entries ignoring case 238 entries = sorted(entries, key=lambda x:x.get_longname().lower()) 239 240 for entry in entries: 241 if entry.is_directory(): 242 fullpath = path + ntpath.sep + entry.get_longname() + ntpath.sep 243 next_directories_to_explore.append(fullpath) 244 else: 245 fullpath = path + ntpath.sep + entry.get_longname() 246 fullpath = re.sub(r'\\\\+', r'\\', fullpath) 247 callback(entry, fullpath, depth) 248 249 return next_directories_to_explore 250 # 251 if callback is not None: 252 depth = 0 253 while len(paths) != 0: 254 paths = recurse_action( 255 paths=paths, 256 depth=depth, 257 callback=callback 258 ) 259 depth = depth + 1 260 else: 261 self.logger.error("SMBSession.find(), callback function cannot be None.") 262 263 def get_file(self, path=None, keepRemotePath=False): 264 """ 265 Retrieves a file from the specified path on the SMB share. 266 267 This method attempts to retrieve a file from the given path within the currently connected SMB share. 268 If the path points to a directory, it skips the retrieval. It handles file retrieval by creating a local 269 file object and writing the contents of the remote file to it using the SMB client's getFile method. 270 271 Parameters: 272 path (str): The path of the file to retrieve. If None, uses the current smb_path. 273 274 Returns: 275 None 276 """ 277 278 # Parse path 279 path = path.replace('/', ntpath.sep) 280 if ntpath.sep in path: 281 tmp_search_path = ntpath.normpath(self.smb_cwd + ntpath.sep + ntpath.dirname(path)) 282 else: 283 tmp_search_path = ntpath.normpath(self.smb_cwd + ntpath.sep) 284 # Parse filename 285 filename = ntpath.basename(path) 286 287 # Search for the file 288 matches = self.smbClient.listPath( 289 shareName=self.smb_share, 290 path=tmp_search_path + ntpath.sep + '*' 291 ) 292 293 # Filter the entries 294 matching_entries = [] 295 for entry in matches: 296 if entry.is_directory(): 297 # Skip directories 298 continue 299 if entry.get_longname() == filename: 300 matching_entries.append(entry) 301 elif '*' in filename: 302 regexp = filename.replace('.', '\\.').replace('*', '.*') 303 if re.match(regexp, entry.get_longname()): 304 matching_entries.append(entry) 305 306 matching_entries = sorted(list(set(matching_entries)), key=lambda x: x.get_longname()) 307 308 for entry in matching_entries: 309 if entry.is_directory(): 310 self.logger.debug("[>] Skipping '%s' because it is a directory." % (tmp_search_path + ntpath.sep + entry.get_longname())) 311 else: 312 try: 313 if ntpath.sep in path: 314 outputfile = ntpath.dirname(path) + ntpath.sep + entry.get_longname() 315 else: 316 outputfile = entry.get_longname() 317 f = LocalFileIO( 318 mode="wb", 319 path=outputfile, 320 expected_size=entry.get_filesize(), 321 debug=self.config.debug, 322 keepRemotePath=keepRemotePath 323 ) 324 self.smbClient.getFile( 325 shareName=self.smb_share, 326 pathName=tmp_search_path + ntpath.sep + entry.get_longname(), 327 callback=f.write 328 ) 329 f.close() 330 except (BrokenPipeError, KeyboardInterrupt) as e: 331 f.close() 332 print("\x1b[v\x1b[o\r[!] Interrupted.") 333 self.close_smb_session() 334 self.init_smb_session() 335 336 return None 337 338 def get_file_recursively(self, path=None): 339 """ 340 Recursively retrieves files from a specified path on the SMB share. 341 342 This method navigates through all directories starting from the given path, 343 and downloads all files found. It handles directories recursively, ensuring 344 that all nested files are retrieved. The method skips over directory entries 345 and handles errors gracefully, attempting to continue the operation where possible. 346 347 Parameters: 348 path (str): The initial directory path from which to start the recursive file retrieval. 349 If None, it starts from the root of the configured SMB share. 350 """ 351 352 def recurse_action(base_dir="", path=[]): 353 if len(base_dir) == 0: 354 remote_smb_path = ntpath.sep.join(path) 355 else: 356 remote_smb_path = base_dir + ntpath.sep + ntpath.sep.join(path) 357 remote_smb_path = ntpath.normpath(remote_smb_path) 358 359 entries = self.smbClient.listPath( 360 shareName=self.smb_share, 361 path=remote_smb_path + '\\*' 362 ) 363 if len(entries) != 0: 364 files = [entry for entry in entries if not entry.is_directory()] 365 directories = [entry for entry in entries if entry.is_directory() and entry.get_longname() not in [".", ".."]] 366 367 # Files 368 if len(files) != 0: 369 self.logger.print("[>] Retrieving files of '%s'" % remote_smb_path) 370 for entry_file in files: 371 if not entry_file.is_directory(): 372 f = LocalFileIO( 373 mode="wb", 374 path=remote_smb_path + ntpath.sep + entry_file.get_longname(), 375 expected_size=entry_file.get_filesize(), 376 keepRemotePath=True, 377 debug=self.config.debug 378 ) 379 try: 380 self.smbClient.getFile( 381 shareName=self.smb_share, 382 pathName=remote_smb_path + ntpath.sep + entry_file.get_longname(), 383 callback=f.write 384 ) 385 f.close() 386 except BrokenPipeError as err: 387 f.set_error(message="[bold red]Failed downloading '%s': %s" % (f.path, err)) 388 f.close(remove=True) 389 break 390 except Exception as err: 391 f.set_error(message="[bold red]Failed downloading '%s': %s" % (f.path, err)) 392 f.close(remove=True) 393 394 # Directories 395 for entry_directory in directories: 396 if entry_directory.is_directory(): 397 recurse_action( 398 base_dir=self.smb_cwd, 399 path=path+[entry_directory.get_longname()] 400 ) 401 # Entrypoint 402 try: 403 recurse_action( 404 base_dir=self.smb_cwd, 405 path=[path] 406 ) 407 except (BrokenPipeError, KeyboardInterrupt) as e: 408 print("\x1b[v\x1b[o\r[!] Interrupted.") 409 self.close_smb_session() 410 self.init_smb_session() 411 412 def get_entry(self, path=None): 413 """ 414 Retrieves information about a specific entry located at the provided path on the SMB share. 415 416 This method checks if the specified path exists on the SMB share. If the path exists, it retrieves the details of the entry at that path, including the directory name and file name. If the entry is found, it returns the entry object; otherwise, it returns None. 417 418 Args: 419 path (str): The path of the entry to retrieve information about. 420 421 Returns: 422 Entry: An object representing the entry at the specified path, or None if the entry is not found. 423 """ 424 425 if self.path_exists(path=path): 426 matches = self.smbClient.listPath( 427 shareName=self.smb_share, 428 path=path 429 ) 430 431 if len(matches) == 1: 432 return matches[0] 433 else: 434 return None 435 else: 436 return None 437 438 def info(self, share=True, server=True): 439 """ 440 Displays information about the server and optionally the shares. 441 442 This method prints detailed information about the server's characteristics such as NetBIOS names, DNS details, OS information, and SMB capabilities. If the `share` parameter is set to True and a share is currently set, it will also attempt to display information about the share. 443 444 Parameters: 445 share (bool): If True, display information about the current share. 446 server (bool): If True, display information about the server. 447 448 Returns: 449 None 450 """ 451 452 if server: 453 if self.config.no_colors: 454 self.logger.print("[+] Server:") 455 self.logger.print(" ├─NetBIOS:") 456 self.logger.print(" │ ├─ NetBIOS Hostname ──────── : %s" % (self.smbClient.getServerName())) 457 self.logger.print(" │ └─ NetBIOS Domain ────────── : %s" % (self.smbClient.getServerDomain())) 458 self.logger.print(" ├─DNS:") 459 self.logger.print(" │ ├─ DNS Hostname ──────────── : %s" % (self.smbClient.getServerDNSHostName())) 460 self.logger.print(" │ └─ DNS Domain ────────────── : %s" % (self.smbClient.getServerDNSDomainName())) 461 self.logger.print(" ├─OS:") 462 self.logger.print(" │ ├─ OS Name ───────────────── : %s" % (self.smbClient.getServerOS())) 463 self.logger.print(" │ └─ OS Version ────────────── : %s.%s.%s" % (self.smbClient.getServerOSMajor(), self.smbClient.getServerOSMinor(), self.smbClient.getServerOSBuild())) 464 self.logger.print(" ├─Server:") 465 self.logger.print(" │ ├─ Signing Required ──────── : %s" % (self.smbClient.isSigningRequired())) 466 self.logger.print(" │ ├─ Login Required ────────── : %s" % (self.smbClient.isLoginRequired())) 467 self.logger.print(" │ ├─ Supports NTLMv2 ───────── : %s" % (self.smbClient.doesSupportNTLMv2())) 468 MaxReadSize = self.smbClient.getIOCapabilities()["MaxReadSize"] 469 self.logger.print(" │ ├─ Max size of read chunk ── : %d bytes (%s)" % (MaxReadSize, b_filesize(MaxReadSize))) 470 MaxWriteSize = self.smbClient.getIOCapabilities()["MaxWriteSize"] 471 self.logger.print(" │ └─ Max size of write chunk ─ : %d bytes (%s)" % (MaxWriteSize, b_filesize(MaxWriteSize))) 472 self.logger.print(" └─") 473 else: 474 self.logger.print("[+] Server:") 475 self.logger.print(" ├─NetBIOS:") 476 self.logger.print(" │ ├─ \x1b[94mNetBIOS Hostname\x1b[0m \x1b[90m────────\x1b[0m : \x1b[93m%s\x1b[0m" % (self.smbClient.getServerName())) 477 self.logger.print(" │ └─ \x1b[94mNetBIOS Domain\x1b[0m \x1b[90m──────────\x1b[0m : \x1b[93m%s\x1b[0m" % (self.smbClient.getServerDomain())) 478 self.logger.print(" ├─DNS:") 479 self.logger.print(" │ ├─ \x1b[94mDNS Hostname\x1b[0m \x1b[90m────────────\x1b[0m : \x1b[93m%s\x1b[0m" % (self.smbClient.getServerDNSHostName())) 480 self.logger.print(" │ └─ \x1b[94mDNS Domain\x1b[0m \x1b[90m──────────────\x1b[0m : \x1b[93m%s\x1b[0m" % (self.smbClient.getServerDNSDomainName())) 481 self.logger.print(" ├─OS:") 482 self.logger.print(" │ ├─ \x1b[94mOS Name\x1b[0m \x1b[90m─────────────────\x1b[0m : \x1b[93m%s\x1b[0m" % (self.smbClient.getServerOS())) 483 self.logger.print(" │ └─ \x1b[94mOS Version\x1b[0m \x1b[90m──────────────\x1b[0m : \x1b[93m%s.%s.%s\x1b[0m" % (self.smbClient.getServerOSMajor(), self.smbClient.getServerOSMinor(), self.smbClient.getServerOSBuild())) 484 self.logger.print(" ├─Server:") 485 self.logger.print(" │ ├─ \x1b[94mSigning Required\x1b[0m \x1b[90m────────\x1b[0m : \x1b[93m%s\x1b[0m" % (self.smbClient.isSigningRequired())) 486 self.logger.print(" │ ├─ \x1b[94mLogin Required\x1b[0m \x1b[90m──────────\x1b[0m : \x1b[93m%s\x1b[0m" % (self.smbClient.isLoginRequired())) 487 self.logger.print(" │ ├─ \x1b[94mSupports NTLMv2\x1b[0m \x1b[90m─────────\x1b[0m : \x1b[93m%s\x1b[0m" % (self.smbClient.doesSupportNTLMv2())) 488 MaxReadSize = self.smbClient.getIOCapabilities()["MaxReadSize"] 489 self.logger.print(" │ ├─ \x1b[94mMax size of read chunk\x1b[0m \x1b[90m──\x1b[0m : \x1b[93m%d bytes (%s)\x1b[0m" % (MaxReadSize, b_filesize(MaxReadSize))) 490 MaxWriteSize = self.smbClient.getIOCapabilities()["MaxWriteSize"] 491 self.logger.print(" │ └─ \x1b[94mMax size of write chunk\x1b[0m \x1b[90m─\x1b[0m : \x1b[93m%d bytes (%s)\x1b[0m" % (MaxWriteSize, b_filesize(MaxWriteSize))) 492 self.logger.print(" └─") 493 494 if share and self.smb_share is not None: 495 share_name = self.available_shares.get(self.smb_share.lower(), "")["name"] 496 share_comment = self.available_shares.get(self.smb_share.lower(), "")["comment"] 497 share_type = self.available_shares.get(self.smb_share.lower(), "")["type"] 498 share_type =', '.join([s.replace("STYPE_","") for s in share_type]) 499 share_rawtype = self.available_shares.get(self.smb_share.lower(), "")["rawtype"] 500 if self.config.no_colors: 501 self.logger.print("\n[+] Share:") 502 self.logger.print(" ├─ Name ──────────── : %s" % (share_name)) 503 self.logger.print(" ├─ Description ───── : %s" % (share_comment)) 504 self.logger.print(" ├─ Type ──────────── : %s" % (share_type)) 505 self.logger.print(" └─ Raw type value ── : %s" % (share_rawtype)) 506 else: 507 self.logger.print("\n[+] Share:") 508 self.logger.print(" ├─ \x1b[94mName\x1b[0m \x1b[90m────────────\x1b[0m : \x1b[93m%s\x1b[0m" % (share_name)) 509 self.logger.print(" ├─ \x1b[94mDescription\x1b[0m \x1b[90m─────\x1b[0m : \x1b[93m%s\x1b[0m" % (share_comment)) 510 self.logger.print(" ├─ \x1b[94mType\x1b[0m \x1b[90m────────────\x1b[0m : \x1b[93m%s\x1b[0m" % (share_type)) 511 self.logger.print(" └─ \x1b[94mRaw type value\x1b[0m \x1b[90m──\x1b[0m : \x1b[93m%s\x1b[0m" % (share_rawtype)) 512 513 def list_contents(self, path=None): 514 """ 515 Lists the contents of a specified directory on the SMB share. 516 517 This method retrieves the contents of a directory specified by `shareName` and `path`. If `shareName` or `path` 518 is not provided, it defaults to the instance's current SMB share or path. The method returns a dictionary with 519 the long names of the files and directories as keys and their respective SMB entry objects as values. 520 521 Args: 522 shareName (str, optional): The name of the SMB share. Defaults to the current SMB share if None. 523 path (str, optional): The directory path to list contents from. Defaults to the current path if None. 524 525 Returns: 526 dict: A dictionary with file and directory names as keys and their SMB entry objects as values. 527 """ 528 529 dest_path = [self.smb_cwd.rstrip(ntpath.sep),] 530 if path is not None and len(path) > 0: 531 dest_path.append(path.rstrip(ntpath.sep)) 532 dest_path.append('*') 533 path = ntpath.sep.join(dest_path) 534 535 contents = {} 536 entries = self.smbClient.listPath( 537 shareName=self.smb_share, 538 path=path 539 ) 540 for entry in entries: 541 contents[entry.get_longname()] = entry 542 543 return contents 544 545 def list_shares(self): 546 """ 547 Lists all the shares available on the connected SMB server. 548 549 This method queries the SMB server to retrieve a list of all available shares. It populates the `shares` dictionary 550 with key-value pairs where the key is the share name and the value is a dictionary containing details about the share 551 such as its name, type, raw type, and any comments associated with the share. 552 553 Returns: 554 dict: A dictionary containing information about each share available on the server. 555 """ 556 557 self.available_shares = {} 558 559 if self.connected: 560 if self.smbClient is not None: 561 resp = self.smbClient.listShares() 562 563 for share in resp: 564 # SHARE_INFO_1 structure (lmshare.h) 565 # https://learn.microsoft.com/en-us/windows/win32/api/lmshare/ns-lmshare-share_info_1 566 sharename = share["shi1_netname"][:-1] 567 sharecomment = share["shi1_remark"][:-1] 568 sharetype = share["shi1_type"] 569 570 self.available_shares[sharename.lower()] = { 571 "name": sharename, 572 "type": STYPE_MASK(sharetype), 573 "rawtype": sharetype, 574 "comment": sharecomment 575 } 576 else: 577 self.logger.error("Error: SMBSession.smbClient is None.") 578 579 return self.available_shares 580 581 def mkdir(self, path=None): 582 """ 583 Creates a directory at the specified path on the SMB share. 584 585 This method takes a path and attempts to create the directory structure on the SMB share. If the path includes 586 nested directories, it will create each directory in the sequence. If a directory already exists, it will skip 587 the creation for that directory without raising an error. 588 589 Args: 590 path (str, optional): The full path of the directory to create on the SMB share. Defaults to None. 591 592 Note: 593 The path should use forward slashes ('/') which will be converted to backslashes (ntpath.sep) for SMB compatibility. 594 """ 595 596 if path is not None: 597 # Prepare path 598 path = path.replace('/',ntpath.sep) 599 if ntpath.sep in path: 600 path = path.strip(ntpath.sep).split(ntpath.sep) 601 else: 602 path = [path] 603 604 # Create each dir in the path 605 for depth in range(1, len(path)+1): 606 tmp_path = ntpath.sep.join(path[:depth]) 607 try: 608 self.smbClient.createDirectory( 609 shareName=self.smb_share, 610 pathName=ntpath.normpath(self.smb_cwd + ntpath.sep + tmp_path + ntpath.sep) 611 ) 612 except impacket.smbconnection.SessionError as err: 613 if err.getErrorCode() == 0xc0000035: 614 # STATUS_OBJECT_NAME_COLLISION 615 # Remote directory already created, this is normal 616 # Src: https://github.com/fortra/impacket/blob/269ce69872f0e8f2188a80addb0c39fedfa6dcb8/impacket/nt_errors.py#L268C9-L268C19 617 pass 618 else: 619 self.logger.error("Failed to create directory '%s': %s" % (tmp_path, err)) 620 if self.config.debug: 621 traceback.print_exc() 622 else: 623 pass 624 625 def mount(self, local_mount_point, remote_path): 626 """ 627 Generates the command to mount an SMB share on different platforms. 628 629 This method takes the local mount point and the remote path of the SMB share and generates the appropriate mount command based on the platform. 630 It constructs the mount command using the provided parameters and executes it using the os.system() function. 631 632 Args: 633 local_mount_point (str): The local directory where the SMB share will be mounted. 634 remote_path (str): The remote path on the SMB share to be mounted. 635 636 Note: 637 - For Windows platform, the command uses 'net use' to mount the share. 638 - For Linux platform, the command uses 'mount' to mount the share. 639 - For macOS platform, the command uses 'mount_smbfs' to mount the share. 640 - If the platform is not supported, an error message is displayed. 641 642 Returns: 643 None 644 """ 645 646 if not os.path.exists(local_mount_point): 647 pass 648 649 if sys.platform.startswith('win'): 650 remote_path = remote_path.replace('/',ntpath.sep) 651 command = f"net use {local_mount_point} \\\\{self.host}\\{self.smb_share}\\{remote_path}" 652 653 elif sys.platform.startswith('linux'): 654 remote_path = remote_path.replace(ntpath.sep,'/') 655 command = f"mount -t cifs //{self.host}/{self.smb_share}/{remote_path} {local_mount_point} -o username={self.credentials.username},password={self.credentials.password}" 656 657 elif sys.platform.startswith('darwin'): 658 remote_path = remote_path.replace(ntpath.sep,'/') 659 command = f"mount_smbfs //{self.credentials.username}:{self.credentials.password}@{self.host}/{self.smb_share}/{remote_path} {local_mount_point}" 660 661 else: 662 command = None 663 self.logger.error("Unsupported platform for mounting SMB share.") 664 665 if command is not None: 666 if self.config.debug: 667 self.logger.debug("Executing: %s" % command) 668 os.system(command) 669 670 def path_exists(self, path=None): 671 """ 672 Checks if the specified path exists on the SMB share. 673 674 This method determines if a given path exists on the SMB share by attempting to list the contents of the path. 675 If the path listing is successful and returns one or more entries, the path is considered to exist. 676 677 Args: 678 path (str, optional): The path to check on the SMB share. Defaults to None. 679 680 Returns: 681 bool: True if the path exists, False otherwise or if an error occurs. 682 """ 683 684 if path is not None: 685 path = path.replace('*','') 686 path = path.replace('/', ntpath.sep) 687 try: 688 contents = self.smbClient.listPath( 689 shareName=self.smb_share, 690 path=ntpath.normpath(self.smb_cwd + ntpath.sep + path + ntpath.sep) 691 ) 692 return (len(contents) != 0) 693 except Exception as e: 694 return False 695 else: 696 return False 697 698 def path_isdir(self, pathFromRoot=None): 699 """ 700 Checks if the specified path is a directory on the SMB share. 701 702 This method determines if a given path corresponds to a directory on the SMB share. It does this by listing the 703 contents of the path and filtering for entries that match the basename of the path and are marked as directories. 704 705 Args: 706 path (str, optional): The path to check on the SMB share. Defaults to None. 707 708 Returns: 709 bool: True if the path is a directory, False otherwise or if an error occurs. 710 """ 711 712 if pathFromRoot is not None: 713 # Strip wildcards to avoid injections 714 path = pathFromRoot.replace('*','') 715 # Replace slashes if any 716 path = path.replace('/', ntpath.sep) 717 718 # Normalize path and strip leading backslash 719 path = ntpath.normpath(path + ntpath.sep).lstrip(ntpath.sep) 720 721 if path.strip() in ['', '.', '..']: 722 # By defininition they exist on the filesystem 723 return True 724 else: 725 try: 726 contents = self.smbClient.listPath( 727 shareName=self.smb_share, 728 path=path+'*' 729 ) 730 # Filter on directories 731 contents = [ 732 c for c in contents 733 if c.get_longname() == ntpath.basename(path) and c.is_directory() 734 ] 735 return (len(contents) != 0) 736 except Exception as e: 737 return False 738 else: 739 return False 740 741 def path_isfile(self, pathFromRoot=None): 742 """ 743 Checks if the specified path is a file on the SMB share. 744 745 This method determines if a given path corresponds to a file on the SMB share. It does this by listing the 746 contents of the path and filtering for entries that match the basename of the path and are not marked as directories. 747 748 Args: 749 path (str, optional): The path to check on the SMB share. Defaults to None. 750 751 Returns: 752 bool: True if the path is a file, False otherwise or if an error occurs. 753 """ 754 755 if pathFromRoot is not None: 756 # Strip wildcards to avoid injections 757 path = pathFromRoot.replace('*','') 758 # Replace slashes if any 759 path = path.replace('/', ntpath.sep) 760 761 # Normalize path and strip leading backslash 762 path = ntpath.normpath(path + ntpath.sep).lstrip(ntpath.sep) 763 764 try: 765 contents = self.smbClient.listPath( 766 shareName=self.smb_share, 767 path=ntpath.dirname(path) + ntpath.sep + '*' 768 ) 769 # Filter on files 770 contents = [ 771 c for c in contents 772 if c.get_longname() == ntpath.basename(path) and not c.is_directory() 773 ] 774 return (len(contents) != 0) 775 except Exception as e: 776 return False 777 else: 778 return False 779 780 def put_file(self, localpath=None): 781 """ 782 Uploads a single file to the SMB share. 783 784 This method takes a local file path, opens the file, and uploads it to the SMB share at the specified path. 785 It handles exceptions such as broken pipe errors or keyboard interrupts by closing and reinitializing the SMB session. 786 General exceptions are caught and logged, with a traceback provided if debugging is enabled. 787 788 Args: 789 localpath (str, optional): The local file path of the file to be uploaded. Defaults to None. 790 """ 791 792 # Parse path 793 localpath = localpath.replace('/', os.path.sep) 794 if os.path.sep in localpath: 795 if localpath.startswith(os.path.sep): 796 # Absolute path 797 tmp_search_path = os.path.normpath(localpath) 798 else: 799 # Relative path 800 tmp_search_path = os.path.normpath(os.getcwd() + os.path.sep + os.path.dirname(localpath)) 801 else: 802 tmp_search_path = os.path.normpath(os.getcwd() + os.path.sep) 803 804 # Parse filename 805 filename = os.path.basename(localpath) 806 807 # Search for the file 808 matches = os.listdir(tmp_search_path) 809 # Filter the entries 810 matching_entries = [] 811 for entry in matches: 812 if entry == filename: 813 matching_entries.append(entry) 814 elif '*' in filename: 815 regexp = filename.replace('.', '\\.').replace('*', '.*') 816 if re.match(regexp, entry): 817 matching_entries.append(entry) 818 819 matching_entries = sorted(list(set(matching_entries))) 820 821 # Loop and upload 822 for localpath in matching_entries: 823 if os.path.exists(localpath): 824 if os.path.isfile(localpath): 825 try: 826 localfile = os.path.basename(localpath) 827 f = LocalFileIO( 828 mode="rb", 829 path=localpath, 830 debug=self.config.debug 831 ) 832 self.smbClient.putFile( 833 shareName=self.smb_share, 834 pathName=ntpath.normpath(self.smb_cwd + ntpath.sep + localfile + ntpath.sep), 835 callback=f.read 836 ) 837 f.close() 838 839 except (BrokenPipeError, KeyboardInterrupt) as err: 840 self.logger.error("Interrupted.") 841 self.close_smb_session() 842 self.init_smb_session() 843 844 except (Exception, PermissionError) as err: 845 f.set_error(message="[bold red]Failed uploading '%s': %s" % (f.path, err)) 846 f.close(remove=False) 847 if self.config.debug: 848 traceback.print_exc() 849 else: 850 # [!] The specified localpath is a directory. Use 'put -r <directory>' instead. 851 pass 852 else: 853 # [!] The specified localpath does not exist. 854 pass 855 856 def put_file_recursively(self, localpath=None): 857 """ 858 Recursively uploads files from a specified local directory to the SMB share. 859 860 This method walks through the given local directory and all its subdirectories, uploading each file to the 861 corresponding directory structure on the SMB share. It first checks if the local path is a directory. If it is, 862 it iterates over all files and directories within the local path, creating necessary directories on the SMB share 863 and uploading files. If the local path is not a directory, it prints an error message. 864 865 Args: 866 localpath (str, optional): The local directory path from which files will be uploaded. Defaults to None. 867 """ 868 869 if os.path.exists(localpath): 870 if os.path.isdir(localpath): 871 # Iterate over all files and directories within the local path 872 local_files = {} 873 for root, dirs, files in os.walk(localpath): 874 if len(files) != 0: 875 local_files[root] = files 876 877 # Iterate over the found files 878 for local_dir_path in sorted(local_files.keys()): 879 self.logger.print("[>] Putting files of '%s'" % local_dir_path) 880 881 # Create remote directory 882 remote_dir_path = local_dir_path.replace(os.path.sep, ntpath.sep) 883 self.mkdir( 884 path=ntpath.normpath(remote_dir_path + ntpath.sep) 885 ) 886 887 for local_file_path in local_files[local_dir_path]: 888 try: 889 f = LocalFileIO( 890 mode="rb", 891 path=local_dir_path + os.path.sep + local_file_path, 892 debug=self.config.debug 893 ) 894 self.smbClient.putFile( 895 shareName=self.smb_share, 896 pathName=ntpath.normpath(self.smb_cwd + ntpath.sep + remote_dir_path + ntpath.sep + local_file_path), 897 callback=f.read 898 ) 899 f.close() 900 901 except (BrokenPipeError, KeyboardInterrupt) as err: 902 self.logger.error("Interrupted.") 903 self.close_smb_session() 904 self.init_smb_session() 905 906 except (Exception, PermissionError) as err: 907 f.set_error(message="[bold red]Failed uploading '%s': %s" % (f.path, err)) 908 f.close(remove=False) 909 if self.config.debug: 910 traceback.print_exc() 911 else: 912 self.logger.error("The specified localpath is a file. Use 'put <file>' instead.") 913 else: 914 self.logger.error("The specified localpath does not exist.") 915 916 def read_file(self, path=None): 917 """ 918 Reads a file from the SMB share. 919 920 This method attempts to read the contents of a file specified by the `path` parameter from the SMB share. 921 It constructs the full path to the file, checks if the path is a valid file, and then reads the file content 922 into a byte stream which is returned to the caller. 923 924 Args: 925 path (str, optional): The path of the file to be read from the SMB share. Defaults to None. 926 927 Returns: 928 bytes: The content of the file as a byte stream, or None if the file does not exist or an error occurs. 929 """ 930 931 if self.path_isfile(pathFromRoot=path): 932 path = path.replace('/', ntpath.sep) 933 if path.startswith(ntpath.sep): 934 # Absolute path 935 tmp_file_path = ntpath.normpath(path) 936 else: 937 # Relative path 938 tmp_file_path = ntpath.normpath(self.smb_cwd + ntpath.sep + path) 939 tmp_file_path = tmp_file_path.lstrip(ntpath.sep) 940 941 fh = io.BytesIO() 942 try: 943 # opening the files in streams instead of mounting shares allows 944 # for running the script from unprivileged containers 945 self.smbClient.getFile(self.smb_share, tmp_file_path, fh.write) 946 except impacket.smbconnection.SessionError as e: 947 return None 948 rawdata = fh.getvalue() 949 fh.close() 950 return rawdata 951 else: 952 return None 953 954 def rmdir(self, path=None): 955 """ 956 Removes a directory from the SMB share at the specified path. 957 958 This method attempts to delete a directory located at the given path on the SMB share. If the operation fails, 959 it prints an error message indicating the failure and the reason. If debugging is enabled, it also prints 960 the stack trace of the exception. 961 962 Args: 963 path (str, optional): The path of the directory to be removed on the SMB share. Defaults to None. 964 """ 965 try: 966 self.smbClient.deleteDirectory( 967 shareName=self.smb_share, 968 pathName=ntpath.normpath(self.smb_cwd + ntpath.sep + path), 969 ) 970 except Exception as err: 971 self.logger.error("Failed to remove directory '%s': %s" % (path, err)) 972 if self.config.debug: 973 traceback.print_exc() 974 975 def rm(self, path=None): 976 """ 977 Removes a file from the SMB share at the specified path. 978 979 This method attempts to delete a file located at the given path on the SMB share. If the operation fails, 980 it prints an error message indicating the failure and the reason. If debugging is enabled, it also prints 981 the stack trace of the exception. 982 983 Args: 984 path (str, optional): The path of the file to be removed on the SMB share. Defaults to None. 985 """ 986 987 # Parse path 988 path = path.replace('/', ntpath.sep) 989 if ntpath.sep in path: 990 tmp_search_path = ntpath.normpath(self.smb_cwd + ntpath.sep + ntpath.dirname(path)) 991 else: 992 tmp_search_path = ntpath.normpath(self.smb_cwd + ntpath.sep) 993 # Parse filename 994 filename = ntpath.basename(path) 995 996 # Search for the file 997 matches = self.smbClient.listPath( 998 shareName=self.smb_share, 999 path=tmp_search_path + ntpath.sep + '*' 1000 ) 1001 1002 # Filter the entries 1003 matching_entries = [] 1004 for entry in matches: 1005 if entry.is_directory(): 1006 # Skip directories 1007 continue 1008 if entry.get_longname() == filename: 1009 matching_entries.append(entry) 1010 elif '*' in filename: 1011 regexp = filename.replace('.', '\\.').replace('*', '.*') 1012 if re.match(regexp, entry.get_longname()): 1013 matching_entries.append(entry) 1014 1015 matching_entries = sorted(list(set(matching_entries)), key=lambda x: x.get_longname()) 1016 1017 for entry in matching_entries: 1018 try: 1019 self.smbClient.deleteFile( 1020 shareName=self.smb_share, 1021 pathName=ntpath.normpath(tmp_search_path + ntpath.sep + entry.get_longname()), 1022 ) 1023 except Exception as err: 1024 self.logger.error("Failed to remove file '%s': %s" % (path, err)) 1025 if self.config.debug: 1026 traceback.print_exc() 1027 1028 def tree(self, path=None): 1029 """ 1030 Recursively lists the directory structure of the SMB share starting from the specified path. 1031 1032 This function prints a visual representation of the directory tree of the remote SMB share. It uses 1033 recursion to navigate through directories and lists all files and subdirectories in each directory. 1034 The output is color-coded and formatted to enhance readability, with directories highlighted in cyan. 1035 1036 Args: 1037 path (str, optional): The starting path on the SMB share from which to begin listing the tree. 1038 Defaults to the root of the current share. 1039 """ 1040 1041 def recurse_action(base_dir="", path=[], prompt=[]): 1042 bars = ["│ ", "├── ", "└── "] 1043 1044 remote_smb_path = ntpath.normpath(base_dir + ntpath.sep + ntpath.sep.join(path)) 1045 1046 entries = [] 1047 try: 1048 entries = self.smbClient.listPath( 1049 shareName=self.smb_share, 1050 path=remote_smb_path+'\\*' 1051 ) 1052 except impacket.smbconnection.SessionError as err: 1053 code, const, text = err.getErrorCode(), err.getErrorString()[0], err.getErrorString()[1] 1054 errmsg = "Error 0x%08x (%s): %s" % (code, const, text) 1055 if self.config.no_colors: 1056 self.logger.print("%s%s" % (''.join(prompt+[bars[2]]), errmsg)) 1057 else: 1058 self.logger.print("%s\x1b[1;91m%s\x1b[0m" % (''.join(prompt+[bars[2]]), errmsg)) 1059 return 1060 1061 entries = [e for e in entries if e.get_longname() not in [".", ".."]] 1062 entries = sorted(entries, key=lambda x:x.get_longname()) 1063 1064 # 1065 if len(entries) > 1: 1066 index = 0 1067 for entry in entries: 1068 index += 1 1069 # This is the first entry 1070 if index == 0: 1071 if entry.is_directory(): 1072 if self.config.no_colors: 1073 self.logger.print("%s%s\\" % (''.join(prompt+[bars[1]]), entry.get_longname())) 1074 else: 1075 self.logger.print("%s\x1b[1;96m%s\x1b[0m\\" % (''.join(prompt+[bars[1]]), entry.get_longname())) 1076 recurse_action( 1077 base_dir=base_dir, 1078 path=path+[entry.get_longname()], 1079 prompt=prompt+["│ "] 1080 ) 1081 else: 1082 if self.config.no_colors: 1083 self.logger.print("%s%s" % (''.join(prompt+[bars[1]]), entry.get_longname())) 1084 else: 1085 self.logger.print("%s\x1b[1m%s\x1b[0m" % (''.join(prompt+[bars[1]]), entry.get_longname())) 1086 1087 # This is the last entry 1088 elif index == len(entries): 1089 if entry.is_directory(): 1090 if self.config.no_colors: 1091 self.logger.print("%s%s\\" % (''.join(prompt+[bars[2]]), entry.get_longname())) 1092 else: 1093 self.logger.print("%s\x1b[1;96m%s\x1b[0m\\" % (''.join(prompt+[bars[2]]), entry.get_longname())) 1094 recurse_action( 1095 base_dir=base_dir, 1096 path=path+[entry.get_longname()], 1097 prompt=prompt+[" "] 1098 ) 1099 else: 1100 if self.config.no_colors: 1101 self.logger.print("%s%s" % (''.join(prompt+[bars[2]]), entry.get_longname())) 1102 else: 1103 self.logger.print("%s\x1b[1m%s\x1b[0m" % (''.join(prompt+[bars[2]]), entry.get_longname())) 1104 1105 # These are entries in the middle 1106 else: 1107 if entry.is_directory(): 1108 if self.config.no_colors: 1109 self.logger.print("%s%s\\" % (''.join(prompt+[bars[1]]), entry.get_longname())) 1110 else: 1111 self.logger.print("%s\x1b[1;96m%s\x1b[0m\\" % (''.join(prompt+[bars[1]]), entry.get_longname())) 1112 recurse_action( 1113 base_dir=base_dir, 1114 path=path+[entry.get_longname()], 1115 prompt=prompt+["│ "] 1116 ) 1117 else: 1118 if self.config.no_colors: 1119 self.logger.print("%s%s" % (''.join(prompt+[bars[1]]), entry.get_longname())) 1120 else: 1121 self.logger.print("%s\x1b[1m%s\x1b[0m" % (''.join(prompt+[bars[1]]), entry.get_longname())) 1122 1123 # 1124 elif len(entries) == 1: 1125 entry = entries[0] 1126 if entry.is_directory(): 1127 if self.config.no_colors: 1128 self.logger.print("%s%s\\" % (''.join(prompt+[bars[2]]), entry.get_longname())) 1129 else: 1130 self.logger.print("%s\x1b[1;96m%s\x1b[0m\\" % (''.join(prompt+[bars[2]]), entry.get_longname())) 1131 recurse_action( 1132 base_dir=base_dir, 1133 path=path+[entry.get_longname()], 1134 prompt=prompt+[" "] 1135 ) 1136 else: 1137 if self.config.no_colors: 1138 self.logger.print("%s%s" % (''.join(prompt+[bars[2]]), entry.get_longname())) 1139 else: 1140 self.logger.print("%s\x1b[1m%s\x1b[0m" % (''.join(prompt+[bars[2]]), entry.get_longname())) 1141 1142 # Entrypoint 1143 try: 1144 if self.config.no_colors: 1145 self.logger.print("%s\\" % path) 1146 else: 1147 self.logger.print("\x1b[1;96m%s\x1b[0m\\" % path) 1148 recurse_action( 1149 base_dir=self.smb_cwd, 1150 path=[path], 1151 prompt=[""] 1152 ) 1153 except (BrokenPipeError, KeyboardInterrupt) as e: 1154 self.logger.error("Interrupted.") 1155 self.close_smb_session() 1156 self.init_smb_session() 1157 1158 def umount(self, local_mount_point): 1159 """ 1160 Unmounts the specified local mount point of the remote share. 1161 1162 This method unmounts the specified local mount point of the remote share based on the platform. 1163 It supports Windows, Linux, and macOS platforms for unmounting. 1164 1165 Parameters: 1166 local_mount_point (str): The local mount point to unmount. 1167 1168 Raises: 1169 None 1170 """ 1171 1172 if os.path.exists(local_mount_point): 1173 if sys.platform.startswith('win'): 1174 command = f"net use {local_mount_point} /delete" 1175 1176 elif sys.platform.startswith('linux') or sys.platform.startswith('darwin'): 1177 command = f"umount {local_mount_point}" 1178 1179 else: 1180 command = None 1181 self.logger.error("Unsupported platform for unmounting SMB share.") 1182 1183 if command is not None: 1184 self.logger.debug("Executing: %s" % command) 1185 os.system(command) 1186 else: 1187 self.logger.error("Cannot unmount a non existing path.") 1188 1189 # Other functions 1190 1191 def test_rights(self, sharename): 1192 """ 1193 Tests the read and write access rights of the current SMB session. 1194 1195 This method checks the read and write access rights of the current SMB session by attempting to list paths and create/delete temporary directories. 1196 1197 Returns: 1198 dict: A dictionary containing the read and write access rights status. 1199 - "readable" (bool): Indicates if the session has read access rights. 1200 - "writable" (bool): Indicates if the session has write access rights. 1201 """ 1202 1203 # Restore the current share 1204 current_share = self.smb_share 1205 self.set_share(shareName=sharename) 1206 1207 access_rights = {"readable": False, "writable": False} 1208 try: 1209 self.smbClient.listPath(self.smb_share, '*', password=None) 1210 access_rights["readable"] = True 1211 except impacket.smbconnection.SessionError as e: 1212 access_rights["readable"] = False 1213 1214 try: 1215 temp_dir = ntpath.normpath("\\" + ''.join([random.choice("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPRSTUVWXYZ0123456759") for k in range(16)])) 1216 self.smbClient.createDirectory(self.smb_share, temp_dir) 1217 self.smbClient.deleteDirectory(self.smb_share, temp_dir) 1218 access_rights["writable"] = True 1219 except impacket.smbconnection.SessionError as e: 1220 access_rights["writable"] = False 1221 1222 # Restore the current share 1223 self.set_share(shareName=current_share) 1224 1225 return access_rights 1226 1227 # Setter / Getter 1228 1229 def set_share(self, shareName): 1230 """ 1231 Sets the current SMB share to the specified share name. 1232 1233 This method updates the SMB session to use the specified share name. It checks if the share name is valid 1234 and updates the smb_share attribute of the SMBSession instance. 1235 1236 Parameters: 1237 shareName (str): The name of the share to set as the current SMB share. 1238 1239 Raises: 1240 ValueError: If the shareName is None or an empty string. 1241 """ 1242 1243 if shareName is not None: 1244 self.list_shares() 1245 if shareName.lower() in self.available_shares.keys(): 1246 # Doing this in order to keep the case of the share adevertised by the remote machine 1247 self.smb_share = self.available_shares[shareName.lower()]["name"] 1248 self.smb_cwd = "" 1249 # Connects the tree 1250 self.smb_tree_id = self.smbClient.connectTree(self.smb_share) 1251 else: 1252 self.logger.error("Could not set share '%s', it does not exist remotely." % shareName) 1253 else: 1254 self.smb_share = None 1255 1256 def set_cwd(self, path=None): 1257 """ 1258 Sets the current working directory on the SMB share to the specified path. 1259 1260 This method updates the current working directory (cwd) of the SMB session to the given path if it is a valid directory. 1261 If the specified path is not a directory, the cwd remains unchanged. 1262 1263 Parameters: 1264 path (str): The path to set as the current working directory. 1265 1266 Raises: 1267 ValueError: If the specified path is not a directory. 1268 """ 1269 1270 if path is not None: 1271 # Set path separators to ntpath sep 1272 if '/' in path: 1273 path = path.replace('/', ntpath.sep) 1274 1275 if path.startswith(ntpath.sep): 1276 # Absolute path 1277 path = path + ntpath.sep 1278 else: 1279 # Relative path to the CWD 1280 if len(self.smb_cwd) == 0: 1281 path = path + ntpath.sep 1282 else: 1283 path = self.smb_cwd + ntpath.sep + path 1284 1285 # Path normalization 1286 path = ntpath.normpath(path) 1287 path = re.sub(r'\\+', r'\\', path) 1288 1289 if path in ["", ".", ".."]: 1290 self.smb_cwd = "" 1291 else: 1292 if self.path_isdir(pathFromRoot=path.strip(ntpath.sep)): 1293 # Path exists on the remote 1294 self.smb_cwd = ntpath.normpath(path) 1295 else: 1296 # Path does not exists or is not a directory on the remote 1297 self.logger.error("Remote directory '%s' does not exist." % path)
21class SMBSession(object): 22 """ 23 Represents an SMB session for interacting with an SMB server. 24 25 This class provides methods to manage and interact with an SMB server, including 26 connecting to the server, listing shares, uploading and downloading files, and 27 managing directories and files on the server. It handles session initialization, 28 authentication, and cleanup. 29 30 Attributes: 31 host (str): The hostname or IP address of the SMB server. 32 port (int): The port number on which the SMB server is listening. 33 credentials (dict): Authentication credentials for the SMB server. 34 config (dict, optional): Configuration options for the SMB session. 35 smbClient (impacket.smbconnection.SMBConnection): The SMB connection instance. 36 connected (bool): Connection status to the SMB server. 37 available_shares (dict): A dictionary of available SMB shares. 38 smb_share (str): The current SMB share in use. 39 smb_cwd (str): The current working directory on the SMB share. 40 smb_tree_id (int): The tree ID of the connected SMB share. 41 42 Methods: 43 close_smb_session(): Closes the current SMB session. 44 init_smb_session(): Initializes the SMB session with the server. 45 list_shares(): Lists all shares available on the SMB server. 46 set_share(shareName): Sets the current SMB share. 47 set_cwd(path): Sets the current working directory on the SMB share. 48 put_file(localpath): Uploads a file to the current SMB share. 49 get_file(remotepath, localpath): Downloads a file from the SMB share. 50 mkdir(path): Creates a directory on the SMB share. 51 rmdir(path): Removes a directory from the SMB share. 52 rm(path): Removes a file from the SMB share. 53 read_file(path): Reads a file from the SMB share. 54 test_rights(sharename): Tests read and write access rights on a share. 55 """ 56 57 def __init__(self, host, port, credentials, config=None, logger=None): 58 super(SMBSession, self).__init__() 59 # Objects 60 self.config = config 61 self.logger = logger 62 63 # Target server 64 self.host = host 65 # Target port (by default on 445) 66 self.port = port 67 68 # Credentials 69 self.credentials = credentials 70 71 self.smbClient = None 72 self.connected = False 73 74 self.available_shares = {} 75 self.smb_share = None 76 self.smb_cwd = "" 77 self.smb_tree_id = None 78 79 self.list_shares() 80 81 # Connect and disconnect SMB session 82 83 def close_smb_session(self): 84 """ 85 Closes the current SMB session by disconnecting the SMB client. 86 87 This method ensures that the SMB client connection is properly closed. It checks if the client is connected 88 and if so, it closes the connection and resets the connection status. 89 90 Raises: 91 Exception: If the SMB client is not initialized or if there's an error during the disconnection process. 92 """ 93 94 if self.smbClient is not None: 95 if self.connected: 96 self.smbClient.close() 97 self.connected = False 98 self.logger.debug("[+] SMB connection closed successfully.") 99 else: 100 self.logger.debug("[!] No active SMB connection to close.") 101 else: 102 raise Exception("SMB client is not initialized.") 103 104 def init_smb_session(self): 105 """ 106 Initializes and establishes a session with the SMB server. 107 108 This method sets up the SMB connection using either Kerberos or NTLM authentication based on the configuration. 109 It attempts to connect to the SMB server specified by the `address` attribute and authenticate using the credentials provided during the object's initialization. 110 111 The method will print debug information if the `debug` attribute is set to True. Upon successful connection and authentication, it sets the `connected` attribute to True. 112 113 Returns: 114 bool: True if the connection and authentication are successful, False otherwise. 115 """ 116 117 self.connected = False 118 119 self.logger.debug("[>] Connecting to remote SMB server '%s' ... " % self.host) 120 121 try: 122 if is_port_open(self.host, self.port): 123 self.smbClient = impacket.smbconnection.SMBConnection( 124 remoteName=self.host, 125 remoteHost=self.host, 126 sess_port=int(self.port) 127 ) 128 else: 129 self.connected = False 130 except OSError as err: 131 if self.config.debug: 132 traceback.print_exc() 133 self.logger.error(err) 134 self.smbClient = None 135 136 if self.smbClient is not None: 137 if self.credentials.use_kerberos: 138 self.logger.debug("[>] Authenticating as '%s\\%s' with kerberos ... " % (self.credentials.domain, self.credentials.username)) 139 try: 140 self.connected = self.smbClient.kerberosLogin( 141 user=self.credentials.username, 142 password=self.credentials.password, 143 domain=self.credentials.domain, 144 lmhash=self.credentials.lm_hex, 145 nthash=self.credentials.nt_hex, 146 aesKey=self.credentials.aesKey, 147 kdcHost=self.credentials.kdcHost 148 ) 149 except impacket.smbconnection.SessionError as err: 150 if self.config.debug: 151 traceback.print_exc() 152 self.logger.error("Could not login: %s" % err) 153 self.connected = False 154 155 else: 156 self.logger.debug("[>] Authenticating as '%s\\%s' with NTLM ... " % (self.credentials.domain, self.credentials.username)) 157 158 try: 159 self.connected = self.smbClient.login( 160 user=self.credentials.username, 161 password=self.credentials.password, 162 domain=self.credentials.domain, 163 lmhash=self.credentials.lm_hex, 164 nthash=self.credentials.nt_hex 165 ) 166 except impacket.smbconnection.SessionError as err: 167 if self.config.debug: 168 traceback.print_exc() 169 self.logger.error("Could not login: %s" % err) 170 self.connected = False 171 172 if self.connected: 173 self.logger.print("[+] Successfully authenticated to '%s' as '%s\\%s'!" % (self.host, self.credentials.domain, self.credentials.username)) 174 else: 175 self.logger.error("Failed to authenticate to '%s' as '%s\\%s'!" % (self.host, self.credentials.domain, self.credentials.username)) 176 177 return self.connected 178 179 def ping_smb_session(self): 180 """ 181 Tests the connectivity to the SMB server by sending an echo command. 182 183 This method attempts to send an echo command to the SMB server to check if the session is still active. 184 It updates the `connected` attribute of the class based on the success or failure of the echo command. 185 186 Returns: 187 bool: True if the echo command succeeds (indicating the session is active), False otherwise. 188 """ 189 190 if not is_port_open(self.host, self.port): 191 self.connected = False 192 else: 193 try: 194 self.smbClient.getSMBServer().echo() 195 except Exception as e: 196 self.connected = False 197 198 return self.connected 199 200 # Operations 201 202 def find(self, paths=[], callback=None): 203 """ 204 Finds files and directories on the SMB share based on the provided paths and executes a callback function on each entry. 205 206 This method traverses the specified paths on the SMB share, recursively exploring directories and invoking the callback 207 function on each file or directory found. The callback function is called with three arguments: the entry object, the 208 full path of the entry, and the current depth of recursion. 209 210 Args: 211 paths (list, optional): A list of paths to start the search from. Defaults to an empty list. 212 callback (function, optional): A function to be called on each entry found. The function should accept three arguments: 213 the entry object, the full path of the entry, and the current depth of recursion. Defaults to None. 214 215 Note: 216 If the callback function is None, the method will print an error message and return without performing any action. 217 """ 218 219 def recurse_action(paths=[], depth=0, callback=None): 220 if callback is None: 221 return [] 222 223 next_directories_to_explore = [] 224 225 for path in paths: 226 remote_smb_path = ntpath.normpath(self.smb_cwd + ntpath.sep + path) 227 entries = [] 228 229 try: 230 entries = self.smbClient.listPath( 231 shareName=self.smb_share, 232 path=(remote_smb_path + ntpath.sep + '*') 233 ) 234 except impacket.smbconnection.SessionError as err: 235 continue 236 # Remove dot names 237 entries = [e for e in entries if e.get_longname() not in [".", ".."]] 238 # Sort the entries ignoring case 239 entries = sorted(entries, key=lambda x:x.get_longname().lower()) 240 241 for entry in entries: 242 if entry.is_directory(): 243 fullpath = path + ntpath.sep + entry.get_longname() + ntpath.sep 244 next_directories_to_explore.append(fullpath) 245 else: 246 fullpath = path + ntpath.sep + entry.get_longname() 247 fullpath = re.sub(r'\\\\+', r'\\', fullpath) 248 callback(entry, fullpath, depth) 249 250 return next_directories_to_explore 251 # 252 if callback is not None: 253 depth = 0 254 while len(paths) != 0: 255 paths = recurse_action( 256 paths=paths, 257 depth=depth, 258 callback=callback 259 ) 260 depth = depth + 1 261 else: 262 self.logger.error("SMBSession.find(), callback function cannot be None.") 263 264 def get_file(self, path=None, keepRemotePath=False): 265 """ 266 Retrieves a file from the specified path on the SMB share. 267 268 This method attempts to retrieve a file from the given path within the currently connected SMB share. 269 If the path points to a directory, it skips the retrieval. It handles file retrieval by creating a local 270 file object and writing the contents of the remote file to it using the SMB client's getFile method. 271 272 Parameters: 273 path (str): The path of the file to retrieve. If None, uses the current smb_path. 274 275 Returns: 276 None 277 """ 278 279 # Parse path 280 path = path.replace('/', ntpath.sep) 281 if ntpath.sep in path: 282 tmp_search_path = ntpath.normpath(self.smb_cwd + ntpath.sep + ntpath.dirname(path)) 283 else: 284 tmp_search_path = ntpath.normpath(self.smb_cwd + ntpath.sep) 285 # Parse filename 286 filename = ntpath.basename(path) 287 288 # Search for the file 289 matches = self.smbClient.listPath( 290 shareName=self.smb_share, 291 path=tmp_search_path + ntpath.sep + '*' 292 ) 293 294 # Filter the entries 295 matching_entries = [] 296 for entry in matches: 297 if entry.is_directory(): 298 # Skip directories 299 continue 300 if entry.get_longname() == filename: 301 matching_entries.append(entry) 302 elif '*' in filename: 303 regexp = filename.replace('.', '\\.').replace('*', '.*') 304 if re.match(regexp, entry.get_longname()): 305 matching_entries.append(entry) 306 307 matching_entries = sorted(list(set(matching_entries)), key=lambda x: x.get_longname()) 308 309 for entry in matching_entries: 310 if entry.is_directory(): 311 self.logger.debug("[>] Skipping '%s' because it is a directory." % (tmp_search_path + ntpath.sep + entry.get_longname())) 312 else: 313 try: 314 if ntpath.sep in path: 315 outputfile = ntpath.dirname(path) + ntpath.sep + entry.get_longname() 316 else: 317 outputfile = entry.get_longname() 318 f = LocalFileIO( 319 mode="wb", 320 path=outputfile, 321 expected_size=entry.get_filesize(), 322 debug=self.config.debug, 323 keepRemotePath=keepRemotePath 324 ) 325 self.smbClient.getFile( 326 shareName=self.smb_share, 327 pathName=tmp_search_path + ntpath.sep + entry.get_longname(), 328 callback=f.write 329 ) 330 f.close() 331 except (BrokenPipeError, KeyboardInterrupt) as e: 332 f.close() 333 print("\x1b[v\x1b[o\r[!] Interrupted.") 334 self.close_smb_session() 335 self.init_smb_session() 336 337 return None 338 339 def get_file_recursively(self, path=None): 340 """ 341 Recursively retrieves files from a specified path on the SMB share. 342 343 This method navigates through all directories starting from the given path, 344 and downloads all files found. It handles directories recursively, ensuring 345 that all nested files are retrieved. The method skips over directory entries 346 and handles errors gracefully, attempting to continue the operation where possible. 347 348 Parameters: 349 path (str): The initial directory path from which to start the recursive file retrieval. 350 If None, it starts from the root of the configured SMB share. 351 """ 352 353 def recurse_action(base_dir="", path=[]): 354 if len(base_dir) == 0: 355 remote_smb_path = ntpath.sep.join(path) 356 else: 357 remote_smb_path = base_dir + ntpath.sep + ntpath.sep.join(path) 358 remote_smb_path = ntpath.normpath(remote_smb_path) 359 360 entries = self.smbClient.listPath( 361 shareName=self.smb_share, 362 path=remote_smb_path + '\\*' 363 ) 364 if len(entries) != 0: 365 files = [entry for entry in entries if not entry.is_directory()] 366 directories = [entry for entry in entries if entry.is_directory() and entry.get_longname() not in [".", ".."]] 367 368 # Files 369 if len(files) != 0: 370 self.logger.print("[>] Retrieving files of '%s'" % remote_smb_path) 371 for entry_file in files: 372 if not entry_file.is_directory(): 373 f = LocalFileIO( 374 mode="wb", 375 path=remote_smb_path + ntpath.sep + entry_file.get_longname(), 376 expected_size=entry_file.get_filesize(), 377 keepRemotePath=True, 378 debug=self.config.debug 379 ) 380 try: 381 self.smbClient.getFile( 382 shareName=self.smb_share, 383 pathName=remote_smb_path + ntpath.sep + entry_file.get_longname(), 384 callback=f.write 385 ) 386 f.close() 387 except BrokenPipeError as err: 388 f.set_error(message="[bold red]Failed downloading '%s': %s" % (f.path, err)) 389 f.close(remove=True) 390 break 391 except Exception as err: 392 f.set_error(message="[bold red]Failed downloading '%s': %s" % (f.path, err)) 393 f.close(remove=True) 394 395 # Directories 396 for entry_directory in directories: 397 if entry_directory.is_directory(): 398 recurse_action( 399 base_dir=self.smb_cwd, 400 path=path+[entry_directory.get_longname()] 401 ) 402 # Entrypoint 403 try: 404 recurse_action( 405 base_dir=self.smb_cwd, 406 path=[path] 407 ) 408 except (BrokenPipeError, KeyboardInterrupt) as e: 409 print("\x1b[v\x1b[o\r[!] Interrupted.") 410 self.close_smb_session() 411 self.init_smb_session() 412 413 def get_entry(self, path=None): 414 """ 415 Retrieves information about a specific entry located at the provided path on the SMB share. 416 417 This method checks if the specified path exists on the SMB share. If the path exists, it retrieves the details of the entry at that path, including the directory name and file name. If the entry is found, it returns the entry object; otherwise, it returns None. 418 419 Args: 420 path (str): The path of the entry to retrieve information about. 421 422 Returns: 423 Entry: An object representing the entry at the specified path, or None if the entry is not found. 424 """ 425 426 if self.path_exists(path=path): 427 matches = self.smbClient.listPath( 428 shareName=self.smb_share, 429 path=path 430 ) 431 432 if len(matches) == 1: 433 return matches[0] 434 else: 435 return None 436 else: 437 return None 438 439 def info(self, share=True, server=True): 440 """ 441 Displays information about the server and optionally the shares. 442 443 This method prints detailed information about the server's characteristics such as NetBIOS names, DNS details, OS information, and SMB capabilities. If the `share` parameter is set to True and a share is currently set, it will also attempt to display information about the share. 444 445 Parameters: 446 share (bool): If True, display information about the current share. 447 server (bool): If True, display information about the server. 448 449 Returns: 450 None 451 """ 452 453 if server: 454 if self.config.no_colors: 455 self.logger.print("[+] Server:") 456 self.logger.print(" ├─NetBIOS:") 457 self.logger.print(" │ ├─ NetBIOS Hostname ──────── : %s" % (self.smbClient.getServerName())) 458 self.logger.print(" │ └─ NetBIOS Domain ────────── : %s" % (self.smbClient.getServerDomain())) 459 self.logger.print(" ├─DNS:") 460 self.logger.print(" │ ├─ DNS Hostname ──────────── : %s" % (self.smbClient.getServerDNSHostName())) 461 self.logger.print(" │ └─ DNS Domain ────────────── : %s" % (self.smbClient.getServerDNSDomainName())) 462 self.logger.print(" ├─OS:") 463 self.logger.print(" │ ├─ OS Name ───────────────── : %s" % (self.smbClient.getServerOS())) 464 self.logger.print(" │ └─ OS Version ────────────── : %s.%s.%s" % (self.smbClient.getServerOSMajor(), self.smbClient.getServerOSMinor(), self.smbClient.getServerOSBuild())) 465 self.logger.print(" ├─Server:") 466 self.logger.print(" │ ├─ Signing Required ──────── : %s" % (self.smbClient.isSigningRequired())) 467 self.logger.print(" │ ├─ Login Required ────────── : %s" % (self.smbClient.isLoginRequired())) 468 self.logger.print(" │ ├─ Supports NTLMv2 ───────── : %s" % (self.smbClient.doesSupportNTLMv2())) 469 MaxReadSize = self.smbClient.getIOCapabilities()["MaxReadSize"] 470 self.logger.print(" │ ├─ Max size of read chunk ── : %d bytes (%s)" % (MaxReadSize, b_filesize(MaxReadSize))) 471 MaxWriteSize = self.smbClient.getIOCapabilities()["MaxWriteSize"] 472 self.logger.print(" │ └─ Max size of write chunk ─ : %d bytes (%s)" % (MaxWriteSize, b_filesize(MaxWriteSize))) 473 self.logger.print(" └─") 474 else: 475 self.logger.print("[+] Server:") 476 self.logger.print(" ├─NetBIOS:") 477 self.logger.print(" │ ├─ \x1b[94mNetBIOS Hostname\x1b[0m \x1b[90m────────\x1b[0m : \x1b[93m%s\x1b[0m" % (self.smbClient.getServerName())) 478 self.logger.print(" │ └─ \x1b[94mNetBIOS Domain\x1b[0m \x1b[90m──────────\x1b[0m : \x1b[93m%s\x1b[0m" % (self.smbClient.getServerDomain())) 479 self.logger.print(" ├─DNS:") 480 self.logger.print(" │ ├─ \x1b[94mDNS Hostname\x1b[0m \x1b[90m────────────\x1b[0m : \x1b[93m%s\x1b[0m" % (self.smbClient.getServerDNSHostName())) 481 self.logger.print(" │ └─ \x1b[94mDNS Domain\x1b[0m \x1b[90m──────────────\x1b[0m : \x1b[93m%s\x1b[0m" % (self.smbClient.getServerDNSDomainName())) 482 self.logger.print(" ├─OS:") 483 self.logger.print(" │ ├─ \x1b[94mOS Name\x1b[0m \x1b[90m─────────────────\x1b[0m : \x1b[93m%s\x1b[0m" % (self.smbClient.getServerOS())) 484 self.logger.print(" │ └─ \x1b[94mOS Version\x1b[0m \x1b[90m──────────────\x1b[0m : \x1b[93m%s.%s.%s\x1b[0m" % (self.smbClient.getServerOSMajor(), self.smbClient.getServerOSMinor(), self.smbClient.getServerOSBuild())) 485 self.logger.print(" ├─Server:") 486 self.logger.print(" │ ├─ \x1b[94mSigning Required\x1b[0m \x1b[90m────────\x1b[0m : \x1b[93m%s\x1b[0m" % (self.smbClient.isSigningRequired())) 487 self.logger.print(" │ ├─ \x1b[94mLogin Required\x1b[0m \x1b[90m──────────\x1b[0m : \x1b[93m%s\x1b[0m" % (self.smbClient.isLoginRequired())) 488 self.logger.print(" │ ├─ \x1b[94mSupports NTLMv2\x1b[0m \x1b[90m─────────\x1b[0m : \x1b[93m%s\x1b[0m" % (self.smbClient.doesSupportNTLMv2())) 489 MaxReadSize = self.smbClient.getIOCapabilities()["MaxReadSize"] 490 self.logger.print(" │ ├─ \x1b[94mMax size of read chunk\x1b[0m \x1b[90m──\x1b[0m : \x1b[93m%d bytes (%s)\x1b[0m" % (MaxReadSize, b_filesize(MaxReadSize))) 491 MaxWriteSize = self.smbClient.getIOCapabilities()["MaxWriteSize"] 492 self.logger.print(" │ └─ \x1b[94mMax size of write chunk\x1b[0m \x1b[90m─\x1b[0m : \x1b[93m%d bytes (%s)\x1b[0m" % (MaxWriteSize, b_filesize(MaxWriteSize))) 493 self.logger.print(" └─") 494 495 if share and self.smb_share is not None: 496 share_name = self.available_shares.get(self.smb_share.lower(), "")["name"] 497 share_comment = self.available_shares.get(self.smb_share.lower(), "")["comment"] 498 share_type = self.available_shares.get(self.smb_share.lower(), "")["type"] 499 share_type =', '.join([s.replace("STYPE_","") for s in share_type]) 500 share_rawtype = self.available_shares.get(self.smb_share.lower(), "")["rawtype"] 501 if self.config.no_colors: 502 self.logger.print("\n[+] Share:") 503 self.logger.print(" ├─ Name ──────────── : %s" % (share_name)) 504 self.logger.print(" ├─ Description ───── : %s" % (share_comment)) 505 self.logger.print(" ├─ Type ──────────── : %s" % (share_type)) 506 self.logger.print(" └─ Raw type value ── : %s" % (share_rawtype)) 507 else: 508 self.logger.print("\n[+] Share:") 509 self.logger.print(" ├─ \x1b[94mName\x1b[0m \x1b[90m────────────\x1b[0m : \x1b[93m%s\x1b[0m" % (share_name)) 510 self.logger.print(" ├─ \x1b[94mDescription\x1b[0m \x1b[90m─────\x1b[0m : \x1b[93m%s\x1b[0m" % (share_comment)) 511 self.logger.print(" ├─ \x1b[94mType\x1b[0m \x1b[90m────────────\x1b[0m : \x1b[93m%s\x1b[0m" % (share_type)) 512 self.logger.print(" └─ \x1b[94mRaw type value\x1b[0m \x1b[90m──\x1b[0m : \x1b[93m%s\x1b[0m" % (share_rawtype)) 513 514 def list_contents(self, path=None): 515 """ 516 Lists the contents of a specified directory on the SMB share. 517 518 This method retrieves the contents of a directory specified by `shareName` and `path`. If `shareName` or `path` 519 is not provided, it defaults to the instance's current SMB share or path. The method returns a dictionary with 520 the long names of the files and directories as keys and their respective SMB entry objects as values. 521 522 Args: 523 shareName (str, optional): The name of the SMB share. Defaults to the current SMB share if None. 524 path (str, optional): The directory path to list contents from. Defaults to the current path if None. 525 526 Returns: 527 dict: A dictionary with file and directory names as keys and their SMB entry objects as values. 528 """ 529 530 dest_path = [self.smb_cwd.rstrip(ntpath.sep),] 531 if path is not None and len(path) > 0: 532 dest_path.append(path.rstrip(ntpath.sep)) 533 dest_path.append('*') 534 path = ntpath.sep.join(dest_path) 535 536 contents = {} 537 entries = self.smbClient.listPath( 538 shareName=self.smb_share, 539 path=path 540 ) 541 for entry in entries: 542 contents[entry.get_longname()] = entry 543 544 return contents 545 546 def list_shares(self): 547 """ 548 Lists all the shares available on the connected SMB server. 549 550 This method queries the SMB server to retrieve a list of all available shares. It populates the `shares` dictionary 551 with key-value pairs where the key is the share name and the value is a dictionary containing details about the share 552 such as its name, type, raw type, and any comments associated with the share. 553 554 Returns: 555 dict: A dictionary containing information about each share available on the server. 556 """ 557 558 self.available_shares = {} 559 560 if self.connected: 561 if self.smbClient is not None: 562 resp = self.smbClient.listShares() 563 564 for share in resp: 565 # SHARE_INFO_1 structure (lmshare.h) 566 # https://learn.microsoft.com/en-us/windows/win32/api/lmshare/ns-lmshare-share_info_1 567 sharename = share["shi1_netname"][:-1] 568 sharecomment = share["shi1_remark"][:-1] 569 sharetype = share["shi1_type"] 570 571 self.available_shares[sharename.lower()] = { 572 "name": sharename, 573 "type": STYPE_MASK(sharetype), 574 "rawtype": sharetype, 575 "comment": sharecomment 576 } 577 else: 578 self.logger.error("Error: SMBSession.smbClient is None.") 579 580 return self.available_shares 581 582 def mkdir(self, path=None): 583 """ 584 Creates a directory at the specified path on the SMB share. 585 586 This method takes a path and attempts to create the directory structure on the SMB share. If the path includes 587 nested directories, it will create each directory in the sequence. If a directory already exists, it will skip 588 the creation for that directory without raising an error. 589 590 Args: 591 path (str, optional): The full path of the directory to create on the SMB share. Defaults to None. 592 593 Note: 594 The path should use forward slashes ('/') which will be converted to backslashes (ntpath.sep) for SMB compatibility. 595 """ 596 597 if path is not None: 598 # Prepare path 599 path = path.replace('/',ntpath.sep) 600 if ntpath.sep in path: 601 path = path.strip(ntpath.sep).split(ntpath.sep) 602 else: 603 path = [path] 604 605 # Create each dir in the path 606 for depth in range(1, len(path)+1): 607 tmp_path = ntpath.sep.join(path[:depth]) 608 try: 609 self.smbClient.createDirectory( 610 shareName=self.smb_share, 611 pathName=ntpath.normpath(self.smb_cwd + ntpath.sep + tmp_path + ntpath.sep) 612 ) 613 except impacket.smbconnection.SessionError as err: 614 if err.getErrorCode() == 0xc0000035: 615 # STATUS_OBJECT_NAME_COLLISION 616 # Remote directory already created, this is normal 617 # Src: https://github.com/fortra/impacket/blob/269ce69872f0e8f2188a80addb0c39fedfa6dcb8/impacket/nt_errors.py#L268C9-L268C19 618 pass 619 else: 620 self.logger.error("Failed to create directory '%s': %s" % (tmp_path, err)) 621 if self.config.debug: 622 traceback.print_exc() 623 else: 624 pass 625 626 def mount(self, local_mount_point, remote_path): 627 """ 628 Generates the command to mount an SMB share on different platforms. 629 630 This method takes the local mount point and the remote path of the SMB share and generates the appropriate mount command based on the platform. 631 It constructs the mount command using the provided parameters and executes it using the os.system() function. 632 633 Args: 634 local_mount_point (str): The local directory where the SMB share will be mounted. 635 remote_path (str): The remote path on the SMB share to be mounted. 636 637 Note: 638 - For Windows platform, the command uses 'net use' to mount the share. 639 - For Linux platform, the command uses 'mount' to mount the share. 640 - For macOS platform, the command uses 'mount_smbfs' to mount the share. 641 - If the platform is not supported, an error message is displayed. 642 643 Returns: 644 None 645 """ 646 647 if not os.path.exists(local_mount_point): 648 pass 649 650 if sys.platform.startswith('win'): 651 remote_path = remote_path.replace('/',ntpath.sep) 652 command = f"net use {local_mount_point} \\\\{self.host}\\{self.smb_share}\\{remote_path}" 653 654 elif sys.platform.startswith('linux'): 655 remote_path = remote_path.replace(ntpath.sep,'/') 656 command = f"mount -t cifs //{self.host}/{self.smb_share}/{remote_path} {local_mount_point} -o username={self.credentials.username},password={self.credentials.password}" 657 658 elif sys.platform.startswith('darwin'): 659 remote_path = remote_path.replace(ntpath.sep,'/') 660 command = f"mount_smbfs //{self.credentials.username}:{self.credentials.password}@{self.host}/{self.smb_share}/{remote_path} {local_mount_point}" 661 662 else: 663 command = None 664 self.logger.error("Unsupported platform for mounting SMB share.") 665 666 if command is not None: 667 if self.config.debug: 668 self.logger.debug("Executing: %s" % command) 669 os.system(command) 670 671 def path_exists(self, path=None): 672 """ 673 Checks if the specified path exists on the SMB share. 674 675 This method determines if a given path exists on the SMB share by attempting to list the contents of the path. 676 If the path listing is successful and returns one or more entries, the path is considered to exist. 677 678 Args: 679 path (str, optional): The path to check on the SMB share. Defaults to None. 680 681 Returns: 682 bool: True if the path exists, False otherwise or if an error occurs. 683 """ 684 685 if path is not None: 686 path = path.replace('*','') 687 path = path.replace('/', ntpath.sep) 688 try: 689 contents = self.smbClient.listPath( 690 shareName=self.smb_share, 691 path=ntpath.normpath(self.smb_cwd + ntpath.sep + path + ntpath.sep) 692 ) 693 return (len(contents) != 0) 694 except Exception as e: 695 return False 696 else: 697 return False 698 699 def path_isdir(self, pathFromRoot=None): 700 """ 701 Checks if the specified path is a directory on the SMB share. 702 703 This method determines if a given path corresponds to a directory on the SMB share. It does this by listing the 704 contents of the path and filtering for entries that match the basename of the path and are marked as directories. 705 706 Args: 707 path (str, optional): The path to check on the SMB share. Defaults to None. 708 709 Returns: 710 bool: True if the path is a directory, False otherwise or if an error occurs. 711 """ 712 713 if pathFromRoot is not None: 714 # Strip wildcards to avoid injections 715 path = pathFromRoot.replace('*','') 716 # Replace slashes if any 717 path = path.replace('/', ntpath.sep) 718 719 # Normalize path and strip leading backslash 720 path = ntpath.normpath(path + ntpath.sep).lstrip(ntpath.sep) 721 722 if path.strip() in ['', '.', '..']: 723 # By defininition they exist on the filesystem 724 return True 725 else: 726 try: 727 contents = self.smbClient.listPath( 728 shareName=self.smb_share, 729 path=path+'*' 730 ) 731 # Filter on directories 732 contents = [ 733 c for c in contents 734 if c.get_longname() == ntpath.basename(path) and c.is_directory() 735 ] 736 return (len(contents) != 0) 737 except Exception as e: 738 return False 739 else: 740 return False 741 742 def path_isfile(self, pathFromRoot=None): 743 """ 744 Checks if the specified path is a file on the SMB share. 745 746 This method determines if a given path corresponds to a file on the SMB share. It does this by listing the 747 contents of the path and filtering for entries that match the basename of the path and are not marked as directories. 748 749 Args: 750 path (str, optional): The path to check on the SMB share. Defaults to None. 751 752 Returns: 753 bool: True if the path is a file, False otherwise or if an error occurs. 754 """ 755 756 if pathFromRoot is not None: 757 # Strip wildcards to avoid injections 758 path = pathFromRoot.replace('*','') 759 # Replace slashes if any 760 path = path.replace('/', ntpath.sep) 761 762 # Normalize path and strip leading backslash 763 path = ntpath.normpath(path + ntpath.sep).lstrip(ntpath.sep) 764 765 try: 766 contents = self.smbClient.listPath( 767 shareName=self.smb_share, 768 path=ntpath.dirname(path) + ntpath.sep + '*' 769 ) 770 # Filter on files 771 contents = [ 772 c for c in contents 773 if c.get_longname() == ntpath.basename(path) and not c.is_directory() 774 ] 775 return (len(contents) != 0) 776 except Exception as e: 777 return False 778 else: 779 return False 780 781 def put_file(self, localpath=None): 782 """ 783 Uploads a single file to the SMB share. 784 785 This method takes a local file path, opens the file, and uploads it to the SMB share at the specified path. 786 It handles exceptions such as broken pipe errors or keyboard interrupts by closing and reinitializing the SMB session. 787 General exceptions are caught and logged, with a traceback provided if debugging is enabled. 788 789 Args: 790 localpath (str, optional): The local file path of the file to be uploaded. Defaults to None. 791 """ 792 793 # Parse path 794 localpath = localpath.replace('/', os.path.sep) 795 if os.path.sep in localpath: 796 if localpath.startswith(os.path.sep): 797 # Absolute path 798 tmp_search_path = os.path.normpath(localpath) 799 else: 800 # Relative path 801 tmp_search_path = os.path.normpath(os.getcwd() + os.path.sep + os.path.dirname(localpath)) 802 else: 803 tmp_search_path = os.path.normpath(os.getcwd() + os.path.sep) 804 805 # Parse filename 806 filename = os.path.basename(localpath) 807 808 # Search for the file 809 matches = os.listdir(tmp_search_path) 810 # Filter the entries 811 matching_entries = [] 812 for entry in matches: 813 if entry == filename: 814 matching_entries.append(entry) 815 elif '*' in filename: 816 regexp = filename.replace('.', '\\.').replace('*', '.*') 817 if re.match(regexp, entry): 818 matching_entries.append(entry) 819 820 matching_entries = sorted(list(set(matching_entries))) 821 822 # Loop and upload 823 for localpath in matching_entries: 824 if os.path.exists(localpath): 825 if os.path.isfile(localpath): 826 try: 827 localfile = os.path.basename(localpath) 828 f = LocalFileIO( 829 mode="rb", 830 path=localpath, 831 debug=self.config.debug 832 ) 833 self.smbClient.putFile( 834 shareName=self.smb_share, 835 pathName=ntpath.normpath(self.smb_cwd + ntpath.sep + localfile + ntpath.sep), 836 callback=f.read 837 ) 838 f.close() 839 840 except (BrokenPipeError, KeyboardInterrupt) as err: 841 self.logger.error("Interrupted.") 842 self.close_smb_session() 843 self.init_smb_session() 844 845 except (Exception, PermissionError) as err: 846 f.set_error(message="[bold red]Failed uploading '%s': %s" % (f.path, err)) 847 f.close(remove=False) 848 if self.config.debug: 849 traceback.print_exc() 850 else: 851 # [!] The specified localpath is a directory. Use 'put -r <directory>' instead. 852 pass 853 else: 854 # [!] The specified localpath does not exist. 855 pass 856 857 def put_file_recursively(self, localpath=None): 858 """ 859 Recursively uploads files from a specified local directory to the SMB share. 860 861 This method walks through the given local directory and all its subdirectories, uploading each file to the 862 corresponding directory structure on the SMB share. It first checks if the local path is a directory. If it is, 863 it iterates over all files and directories within the local path, creating necessary directories on the SMB share 864 and uploading files. If the local path is not a directory, it prints an error message. 865 866 Args: 867 localpath (str, optional): The local directory path from which files will be uploaded. Defaults to None. 868 """ 869 870 if os.path.exists(localpath): 871 if os.path.isdir(localpath): 872 # Iterate over all files and directories within the local path 873 local_files = {} 874 for root, dirs, files in os.walk(localpath): 875 if len(files) != 0: 876 local_files[root] = files 877 878 # Iterate over the found files 879 for local_dir_path in sorted(local_files.keys()): 880 self.logger.print("[>] Putting files of '%s'" % local_dir_path) 881 882 # Create remote directory 883 remote_dir_path = local_dir_path.replace(os.path.sep, ntpath.sep) 884 self.mkdir( 885 path=ntpath.normpath(remote_dir_path + ntpath.sep) 886 ) 887 888 for local_file_path in local_files[local_dir_path]: 889 try: 890 f = LocalFileIO( 891 mode="rb", 892 path=local_dir_path + os.path.sep + local_file_path, 893 debug=self.config.debug 894 ) 895 self.smbClient.putFile( 896 shareName=self.smb_share, 897 pathName=ntpath.normpath(self.smb_cwd + ntpath.sep + remote_dir_path + ntpath.sep + local_file_path), 898 callback=f.read 899 ) 900 f.close() 901 902 except (BrokenPipeError, KeyboardInterrupt) as err: 903 self.logger.error("Interrupted.") 904 self.close_smb_session() 905 self.init_smb_session() 906 907 except (Exception, PermissionError) as err: 908 f.set_error(message="[bold red]Failed uploading '%s': %s" % (f.path, err)) 909 f.close(remove=False) 910 if self.config.debug: 911 traceback.print_exc() 912 else: 913 self.logger.error("The specified localpath is a file. Use 'put <file>' instead.") 914 else: 915 self.logger.error("The specified localpath does not exist.") 916 917 def read_file(self, path=None): 918 """ 919 Reads a file from the SMB share. 920 921 This method attempts to read the contents of a file specified by the `path` parameter from the SMB share. 922 It constructs the full path to the file, checks if the path is a valid file, and then reads the file content 923 into a byte stream which is returned to the caller. 924 925 Args: 926 path (str, optional): The path of the file to be read from the SMB share. Defaults to None. 927 928 Returns: 929 bytes: The content of the file as a byte stream, or None if the file does not exist or an error occurs. 930 """ 931 932 if self.path_isfile(pathFromRoot=path): 933 path = path.replace('/', ntpath.sep) 934 if path.startswith(ntpath.sep): 935 # Absolute path 936 tmp_file_path = ntpath.normpath(path) 937 else: 938 # Relative path 939 tmp_file_path = ntpath.normpath(self.smb_cwd + ntpath.sep + path) 940 tmp_file_path = tmp_file_path.lstrip(ntpath.sep) 941 942 fh = io.BytesIO() 943 try: 944 # opening the files in streams instead of mounting shares allows 945 # for running the script from unprivileged containers 946 self.smbClient.getFile(self.smb_share, tmp_file_path, fh.write) 947 except impacket.smbconnection.SessionError as e: 948 return None 949 rawdata = fh.getvalue() 950 fh.close() 951 return rawdata 952 else: 953 return None 954 955 def rmdir(self, path=None): 956 """ 957 Removes a directory from the SMB share at the specified path. 958 959 This method attempts to delete a directory located at the given path on the SMB share. If the operation fails, 960 it prints an error message indicating the failure and the reason. If debugging is enabled, it also prints 961 the stack trace of the exception. 962 963 Args: 964 path (str, optional): The path of the directory to be removed on the SMB share. Defaults to None. 965 """ 966 try: 967 self.smbClient.deleteDirectory( 968 shareName=self.smb_share, 969 pathName=ntpath.normpath(self.smb_cwd + ntpath.sep + path), 970 ) 971 except Exception as err: 972 self.logger.error("Failed to remove directory '%s': %s" % (path, err)) 973 if self.config.debug: 974 traceback.print_exc() 975 976 def rm(self, path=None): 977 """ 978 Removes a file from the SMB share at the specified path. 979 980 This method attempts to delete a file located at the given path on the SMB share. If the operation fails, 981 it prints an error message indicating the failure and the reason. If debugging is enabled, it also prints 982 the stack trace of the exception. 983 984 Args: 985 path (str, optional): The path of the file to be removed on the SMB share. Defaults to None. 986 """ 987 988 # Parse path 989 path = path.replace('/', ntpath.sep) 990 if ntpath.sep in path: 991 tmp_search_path = ntpath.normpath(self.smb_cwd + ntpath.sep + ntpath.dirname(path)) 992 else: 993 tmp_search_path = ntpath.normpath(self.smb_cwd + ntpath.sep) 994 # Parse filename 995 filename = ntpath.basename(path) 996 997 # Search for the file 998 matches = self.smbClient.listPath( 999 shareName=self.smb_share, 1000 path=tmp_search_path + ntpath.sep + '*' 1001 ) 1002 1003 # Filter the entries 1004 matching_entries = [] 1005 for entry in matches: 1006 if entry.is_directory(): 1007 # Skip directories 1008 continue 1009 if entry.get_longname() == filename: 1010 matching_entries.append(entry) 1011 elif '*' in filename: 1012 regexp = filename.replace('.', '\\.').replace('*', '.*') 1013 if re.match(regexp, entry.get_longname()): 1014 matching_entries.append(entry) 1015 1016 matching_entries = sorted(list(set(matching_entries)), key=lambda x: x.get_longname()) 1017 1018 for entry in matching_entries: 1019 try: 1020 self.smbClient.deleteFile( 1021 shareName=self.smb_share, 1022 pathName=ntpath.normpath(tmp_search_path + ntpath.sep + entry.get_longname()), 1023 ) 1024 except Exception as err: 1025 self.logger.error("Failed to remove file '%s': %s" % (path, err)) 1026 if self.config.debug: 1027 traceback.print_exc() 1028 1029 def tree(self, path=None): 1030 """ 1031 Recursively lists the directory structure of the SMB share starting from the specified path. 1032 1033 This function prints a visual representation of the directory tree of the remote SMB share. It uses 1034 recursion to navigate through directories and lists all files and subdirectories in each directory. 1035 The output is color-coded and formatted to enhance readability, with directories highlighted in cyan. 1036 1037 Args: 1038 path (str, optional): The starting path on the SMB share from which to begin listing the tree. 1039 Defaults to the root of the current share. 1040 """ 1041 1042 def recurse_action(base_dir="", path=[], prompt=[]): 1043 bars = ["│ ", "├── ", "└── "] 1044 1045 remote_smb_path = ntpath.normpath(base_dir + ntpath.sep + ntpath.sep.join(path)) 1046 1047 entries = [] 1048 try: 1049 entries = self.smbClient.listPath( 1050 shareName=self.smb_share, 1051 path=remote_smb_path+'\\*' 1052 ) 1053 except impacket.smbconnection.SessionError as err: 1054 code, const, text = err.getErrorCode(), err.getErrorString()[0], err.getErrorString()[1] 1055 errmsg = "Error 0x%08x (%s): %s" % (code, const, text) 1056 if self.config.no_colors: 1057 self.logger.print("%s%s" % (''.join(prompt+[bars[2]]), errmsg)) 1058 else: 1059 self.logger.print("%s\x1b[1;91m%s\x1b[0m" % (''.join(prompt+[bars[2]]), errmsg)) 1060 return 1061 1062 entries = [e for e in entries if e.get_longname() not in [".", ".."]] 1063 entries = sorted(entries, key=lambda x:x.get_longname()) 1064 1065 # 1066 if len(entries) > 1: 1067 index = 0 1068 for entry in entries: 1069 index += 1 1070 # This is the first entry 1071 if index == 0: 1072 if entry.is_directory(): 1073 if self.config.no_colors: 1074 self.logger.print("%s%s\\" % (''.join(prompt+[bars[1]]), entry.get_longname())) 1075 else: 1076 self.logger.print("%s\x1b[1;96m%s\x1b[0m\\" % (''.join(prompt+[bars[1]]), entry.get_longname())) 1077 recurse_action( 1078 base_dir=base_dir, 1079 path=path+[entry.get_longname()], 1080 prompt=prompt+["│ "] 1081 ) 1082 else: 1083 if self.config.no_colors: 1084 self.logger.print("%s%s" % (''.join(prompt+[bars[1]]), entry.get_longname())) 1085 else: 1086 self.logger.print("%s\x1b[1m%s\x1b[0m" % (''.join(prompt+[bars[1]]), entry.get_longname())) 1087 1088 # This is the last entry 1089 elif index == len(entries): 1090 if entry.is_directory(): 1091 if self.config.no_colors: 1092 self.logger.print("%s%s\\" % (''.join(prompt+[bars[2]]), entry.get_longname())) 1093 else: 1094 self.logger.print("%s\x1b[1;96m%s\x1b[0m\\" % (''.join(prompt+[bars[2]]), entry.get_longname())) 1095 recurse_action( 1096 base_dir=base_dir, 1097 path=path+[entry.get_longname()], 1098 prompt=prompt+[" "] 1099 ) 1100 else: 1101 if self.config.no_colors: 1102 self.logger.print("%s%s" % (''.join(prompt+[bars[2]]), entry.get_longname())) 1103 else: 1104 self.logger.print("%s\x1b[1m%s\x1b[0m" % (''.join(prompt+[bars[2]]), entry.get_longname())) 1105 1106 # These are entries in the middle 1107 else: 1108 if entry.is_directory(): 1109 if self.config.no_colors: 1110 self.logger.print("%s%s\\" % (''.join(prompt+[bars[1]]), entry.get_longname())) 1111 else: 1112 self.logger.print("%s\x1b[1;96m%s\x1b[0m\\" % (''.join(prompt+[bars[1]]), entry.get_longname())) 1113 recurse_action( 1114 base_dir=base_dir, 1115 path=path+[entry.get_longname()], 1116 prompt=prompt+["│ "] 1117 ) 1118 else: 1119 if self.config.no_colors: 1120 self.logger.print("%s%s" % (''.join(prompt+[bars[1]]), entry.get_longname())) 1121 else: 1122 self.logger.print("%s\x1b[1m%s\x1b[0m" % (''.join(prompt+[bars[1]]), entry.get_longname())) 1123 1124 # 1125 elif len(entries) == 1: 1126 entry = entries[0] 1127 if entry.is_directory(): 1128 if self.config.no_colors: 1129 self.logger.print("%s%s\\" % (''.join(prompt+[bars[2]]), entry.get_longname())) 1130 else: 1131 self.logger.print("%s\x1b[1;96m%s\x1b[0m\\" % (''.join(prompt+[bars[2]]), entry.get_longname())) 1132 recurse_action( 1133 base_dir=base_dir, 1134 path=path+[entry.get_longname()], 1135 prompt=prompt+[" "] 1136 ) 1137 else: 1138 if self.config.no_colors: 1139 self.logger.print("%s%s" % (''.join(prompt+[bars[2]]), entry.get_longname())) 1140 else: 1141 self.logger.print("%s\x1b[1m%s\x1b[0m" % (''.join(prompt+[bars[2]]), entry.get_longname())) 1142 1143 # Entrypoint 1144 try: 1145 if self.config.no_colors: 1146 self.logger.print("%s\\" % path) 1147 else: 1148 self.logger.print("\x1b[1;96m%s\x1b[0m\\" % path) 1149 recurse_action( 1150 base_dir=self.smb_cwd, 1151 path=[path], 1152 prompt=[""] 1153 ) 1154 except (BrokenPipeError, KeyboardInterrupt) as e: 1155 self.logger.error("Interrupted.") 1156 self.close_smb_session() 1157 self.init_smb_session() 1158 1159 def umount(self, local_mount_point): 1160 """ 1161 Unmounts the specified local mount point of the remote share. 1162 1163 This method unmounts the specified local mount point of the remote share based on the platform. 1164 It supports Windows, Linux, and macOS platforms for unmounting. 1165 1166 Parameters: 1167 local_mount_point (str): The local mount point to unmount. 1168 1169 Raises: 1170 None 1171 """ 1172 1173 if os.path.exists(local_mount_point): 1174 if sys.platform.startswith('win'): 1175 command = f"net use {local_mount_point} /delete" 1176 1177 elif sys.platform.startswith('linux') or sys.platform.startswith('darwin'): 1178 command = f"umount {local_mount_point}" 1179 1180 else: 1181 command = None 1182 self.logger.error("Unsupported platform for unmounting SMB share.") 1183 1184 if command is not None: 1185 self.logger.debug("Executing: %s" % command) 1186 os.system(command) 1187 else: 1188 self.logger.error("Cannot unmount a non existing path.") 1189 1190 # Other functions 1191 1192 def test_rights(self, sharename): 1193 """ 1194 Tests the read and write access rights of the current SMB session. 1195 1196 This method checks the read and write access rights of the current SMB session by attempting to list paths and create/delete temporary directories. 1197 1198 Returns: 1199 dict: A dictionary containing the read and write access rights status. 1200 - "readable" (bool): Indicates if the session has read access rights. 1201 - "writable" (bool): Indicates if the session has write access rights. 1202 """ 1203 1204 # Restore the current share 1205 current_share = self.smb_share 1206 self.set_share(shareName=sharename) 1207 1208 access_rights = {"readable": False, "writable": False} 1209 try: 1210 self.smbClient.listPath(self.smb_share, '*', password=None) 1211 access_rights["readable"] = True 1212 except impacket.smbconnection.SessionError as e: 1213 access_rights["readable"] = False 1214 1215 try: 1216 temp_dir = ntpath.normpath("\\" + ''.join([random.choice("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPRSTUVWXYZ0123456759") for k in range(16)])) 1217 self.smbClient.createDirectory(self.smb_share, temp_dir) 1218 self.smbClient.deleteDirectory(self.smb_share, temp_dir) 1219 access_rights["writable"] = True 1220 except impacket.smbconnection.SessionError as e: 1221 access_rights["writable"] = False 1222 1223 # Restore the current share 1224 self.set_share(shareName=current_share) 1225 1226 return access_rights 1227 1228 # Setter / Getter 1229 1230 def set_share(self, shareName): 1231 """ 1232 Sets the current SMB share to the specified share name. 1233 1234 This method updates the SMB session to use the specified share name. It checks if the share name is valid 1235 and updates the smb_share attribute of the SMBSession instance. 1236 1237 Parameters: 1238 shareName (str): The name of the share to set as the current SMB share. 1239 1240 Raises: 1241 ValueError: If the shareName is None or an empty string. 1242 """ 1243 1244 if shareName is not None: 1245 self.list_shares() 1246 if shareName.lower() in self.available_shares.keys(): 1247 # Doing this in order to keep the case of the share adevertised by the remote machine 1248 self.smb_share = self.available_shares[shareName.lower()]["name"] 1249 self.smb_cwd = "" 1250 # Connects the tree 1251 self.smb_tree_id = self.smbClient.connectTree(self.smb_share) 1252 else: 1253 self.logger.error("Could not set share '%s', it does not exist remotely." % shareName) 1254 else: 1255 self.smb_share = None 1256 1257 def set_cwd(self, path=None): 1258 """ 1259 Sets the current working directory on the SMB share to the specified path. 1260 1261 This method updates the current working directory (cwd) of the SMB session to the given path if it is a valid directory. 1262 If the specified path is not a directory, the cwd remains unchanged. 1263 1264 Parameters: 1265 path (str): The path to set as the current working directory. 1266 1267 Raises: 1268 ValueError: If the specified path is not a directory. 1269 """ 1270 1271 if path is not None: 1272 # Set path separators to ntpath sep 1273 if '/' in path: 1274 path = path.replace('/', ntpath.sep) 1275 1276 if path.startswith(ntpath.sep): 1277 # Absolute path 1278 path = path + ntpath.sep 1279 else: 1280 # Relative path to the CWD 1281 if len(self.smb_cwd) == 0: 1282 path = path + ntpath.sep 1283 else: 1284 path = self.smb_cwd + ntpath.sep + path 1285 1286 # Path normalization 1287 path = ntpath.normpath(path) 1288 path = re.sub(r'\\+', r'\\', path) 1289 1290 if path in ["", ".", ".."]: 1291 self.smb_cwd = "" 1292 else: 1293 if self.path_isdir(pathFromRoot=path.strip(ntpath.sep)): 1294 # Path exists on the remote 1295 self.smb_cwd = ntpath.normpath(path) 1296 else: 1297 # Path does not exists or is not a directory on the remote 1298 self.logger.error("Remote directory '%s' does not exist." % path)
Represents an SMB session for interacting with an SMB server.
This class provides methods to manage and interact with an SMB server, including connecting to the server, listing shares, uploading and downloading files, and managing directories and files on the server. It handles session initialization, authentication, and cleanup.
Attributes: host (str): The hostname or IP address of the SMB server. port (int): The port number on which the SMB server is listening. credentials (dict): Authentication credentials for the SMB server. config (dict, optional): Configuration options for the SMB session. smbClient (impacket.smbconnection.SMBConnection): The SMB connection instance. connected (bool): Connection status to the SMB server. available_shares (dict): A dictionary of available SMB shares. smb_share (str): The current SMB share in use. smb_cwd (str): The current working directory on the SMB share. smb_tree_id (int): The tree ID of the connected SMB share.
Methods: close_smb_session(): Closes the current SMB session. init_smb_session(): Initializes the SMB session with the server. list_shares(): Lists all shares available on the SMB server. set_share(shareName): Sets the current SMB share. set_cwd(path): Sets the current working directory on the SMB share. put_file(localpath): Uploads a file to the current SMB share. get_file(remotepath, localpath): Downloads a file from the SMB share. mkdir(path): Creates a directory on the SMB share. rmdir(path): Removes a directory from the SMB share. rm(path): Removes a file from the SMB share. read_file(path): Reads a file from the SMB share. test_rights(sharename): Tests read and write access rights on a share.
57 def __init__(self, host, port, credentials, config=None, logger=None): 58 super(SMBSession, self).__init__() 59 # Objects 60 self.config = config 61 self.logger = logger 62 63 # Target server 64 self.host = host 65 # Target port (by default on 445) 66 self.port = port 67 68 # Credentials 69 self.credentials = credentials 70 71 self.smbClient = None 72 self.connected = False 73 74 self.available_shares = {} 75 self.smb_share = None 76 self.smb_cwd = "" 77 self.smb_tree_id = None 78 79 self.list_shares()
83 def close_smb_session(self): 84 """ 85 Closes the current SMB session by disconnecting the SMB client. 86 87 This method ensures that the SMB client connection is properly closed. It checks if the client is connected 88 and if so, it closes the connection and resets the connection status. 89 90 Raises: 91 Exception: If the SMB client is not initialized or if there's an error during the disconnection process. 92 """ 93 94 if self.smbClient is not None: 95 if self.connected: 96 self.smbClient.close() 97 self.connected = False 98 self.logger.debug("[+] SMB connection closed successfully.") 99 else: 100 self.logger.debug("[!] No active SMB connection to close.") 101 else: 102 raise Exception("SMB client is not initialized.")
Closes the current SMB session by disconnecting the SMB client.
This method ensures that the SMB client connection is properly closed. It checks if the client is connected and if so, it closes the connection and resets the connection status.
Raises: Exception: If the SMB client is not initialized or if there's an error during the disconnection process.
104 def init_smb_session(self): 105 """ 106 Initializes and establishes a session with the SMB server. 107 108 This method sets up the SMB connection using either Kerberos or NTLM authentication based on the configuration. 109 It attempts to connect to the SMB server specified by the `address` attribute and authenticate using the credentials provided during the object's initialization. 110 111 The method will print debug information if the `debug` attribute is set to True. Upon successful connection and authentication, it sets the `connected` attribute to True. 112 113 Returns: 114 bool: True if the connection and authentication are successful, False otherwise. 115 """ 116 117 self.connected = False 118 119 self.logger.debug("[>] Connecting to remote SMB server '%s' ... " % self.host) 120 121 try: 122 if is_port_open(self.host, self.port): 123 self.smbClient = impacket.smbconnection.SMBConnection( 124 remoteName=self.host, 125 remoteHost=self.host, 126 sess_port=int(self.port) 127 ) 128 else: 129 self.connected = False 130 except OSError as err: 131 if self.config.debug: 132 traceback.print_exc() 133 self.logger.error(err) 134 self.smbClient = None 135 136 if self.smbClient is not None: 137 if self.credentials.use_kerberos: 138 self.logger.debug("[>] Authenticating as '%s\\%s' with kerberos ... " % (self.credentials.domain, self.credentials.username)) 139 try: 140 self.connected = self.smbClient.kerberosLogin( 141 user=self.credentials.username, 142 password=self.credentials.password, 143 domain=self.credentials.domain, 144 lmhash=self.credentials.lm_hex, 145 nthash=self.credentials.nt_hex, 146 aesKey=self.credentials.aesKey, 147 kdcHost=self.credentials.kdcHost 148 ) 149 except impacket.smbconnection.SessionError as err: 150 if self.config.debug: 151 traceback.print_exc() 152 self.logger.error("Could not login: %s" % err) 153 self.connected = False 154 155 else: 156 self.logger.debug("[>] Authenticating as '%s\\%s' with NTLM ... " % (self.credentials.domain, self.credentials.username)) 157 158 try: 159 self.connected = self.smbClient.login( 160 user=self.credentials.username, 161 password=self.credentials.password, 162 domain=self.credentials.domain, 163 lmhash=self.credentials.lm_hex, 164 nthash=self.credentials.nt_hex 165 ) 166 except impacket.smbconnection.SessionError as err: 167 if self.config.debug: 168 traceback.print_exc() 169 self.logger.error("Could not login: %s" % err) 170 self.connected = False 171 172 if self.connected: 173 self.logger.print("[+] Successfully authenticated to '%s' as '%s\\%s'!" % (self.host, self.credentials.domain, self.credentials.username)) 174 else: 175 self.logger.error("Failed to authenticate to '%s' as '%s\\%s'!" % (self.host, self.credentials.domain, self.credentials.username)) 176 177 return self.connected
Initializes and establishes a session with the SMB server.
This method sets up the SMB connection using either Kerberos or NTLM authentication based on the configuration.
It attempts to connect to the SMB server specified by the address attribute and authenticate using the credentials provided during the object's initialization.
The method will print debug information if the debug attribute is set to True. Upon successful connection and authentication, it sets the connected attribute to True.
Returns: bool: True if the connection and authentication are successful, False otherwise.
179 def ping_smb_session(self): 180 """ 181 Tests the connectivity to the SMB server by sending an echo command. 182 183 This method attempts to send an echo command to the SMB server to check if the session is still active. 184 It updates the `connected` attribute of the class based on the success or failure of the echo command. 185 186 Returns: 187 bool: True if the echo command succeeds (indicating the session is active), False otherwise. 188 """ 189 190 if not is_port_open(self.host, self.port): 191 self.connected = False 192 else: 193 try: 194 self.smbClient.getSMBServer().echo() 195 except Exception as e: 196 self.connected = False 197 198 return self.connected
Tests the connectivity to the SMB server by sending an echo command.
This method attempts to send an echo command to the SMB server to check if the session is still active.
It updates the connected attribute of the class based on the success or failure of the echo command.
Returns: bool: True if the echo command succeeds (indicating the session is active), False otherwise.
202 def find(self, paths=[], callback=None): 203 """ 204 Finds files and directories on the SMB share based on the provided paths and executes a callback function on each entry. 205 206 This method traverses the specified paths on the SMB share, recursively exploring directories and invoking the callback 207 function on each file or directory found. The callback function is called with three arguments: the entry object, the 208 full path of the entry, and the current depth of recursion. 209 210 Args: 211 paths (list, optional): A list of paths to start the search from. Defaults to an empty list. 212 callback (function, optional): A function to be called on each entry found. The function should accept three arguments: 213 the entry object, the full path of the entry, and the current depth of recursion. Defaults to None. 214 215 Note: 216 If the callback function is None, the method will print an error message and return without performing any action. 217 """ 218 219 def recurse_action(paths=[], depth=0, callback=None): 220 if callback is None: 221 return [] 222 223 next_directories_to_explore = [] 224 225 for path in paths: 226 remote_smb_path = ntpath.normpath(self.smb_cwd + ntpath.sep + path) 227 entries = [] 228 229 try: 230 entries = self.smbClient.listPath( 231 shareName=self.smb_share, 232 path=(remote_smb_path + ntpath.sep + '*') 233 ) 234 except impacket.smbconnection.SessionError as err: 235 continue 236 # Remove dot names 237 entries = [e for e in entries if e.get_longname() not in [".", ".."]] 238 # Sort the entries ignoring case 239 entries = sorted(entries, key=lambda x:x.get_longname().lower()) 240 241 for entry in entries: 242 if entry.is_directory(): 243 fullpath = path + ntpath.sep + entry.get_longname() + ntpath.sep 244 next_directories_to_explore.append(fullpath) 245 else: 246 fullpath = path + ntpath.sep + entry.get_longname() 247 fullpath = re.sub(r'\\\\+', r'\\', fullpath) 248 callback(entry, fullpath, depth) 249 250 return next_directories_to_explore 251 # 252 if callback is not None: 253 depth = 0 254 while len(paths) != 0: 255 paths = recurse_action( 256 paths=paths, 257 depth=depth, 258 callback=callback 259 ) 260 depth = depth + 1 261 else: 262 self.logger.error("SMBSession.find(), callback function cannot be None.")
Finds files and directories on the SMB share based on the provided paths and executes a callback function on each entry.
This method traverses the specified paths on the SMB share, recursively exploring directories and invoking the callback function on each file or directory found. The callback function is called with three arguments: the entry object, the full path of the entry, and the current depth of recursion.
Args: paths (list, optional): A list of paths to start the search from. Defaults to an empty list. callback (function, optional): A function to be called on each entry found. The function should accept three arguments: the entry object, the full path of the entry, and the current depth of recursion. Defaults to None.
Note: If the callback function is None, the method will print an error message and return without performing any action.
264 def get_file(self, path=None, keepRemotePath=False): 265 """ 266 Retrieves a file from the specified path on the SMB share. 267 268 This method attempts to retrieve a file from the given path within the currently connected SMB share. 269 If the path points to a directory, it skips the retrieval. It handles file retrieval by creating a local 270 file object and writing the contents of the remote file to it using the SMB client's getFile method. 271 272 Parameters: 273 path (str): The path of the file to retrieve. If None, uses the current smb_path. 274 275 Returns: 276 None 277 """ 278 279 # Parse path 280 path = path.replace('/', ntpath.sep) 281 if ntpath.sep in path: 282 tmp_search_path = ntpath.normpath(self.smb_cwd + ntpath.sep + ntpath.dirname(path)) 283 else: 284 tmp_search_path = ntpath.normpath(self.smb_cwd + ntpath.sep) 285 # Parse filename 286 filename = ntpath.basename(path) 287 288 # Search for the file 289 matches = self.smbClient.listPath( 290 shareName=self.smb_share, 291 path=tmp_search_path + ntpath.sep + '*' 292 ) 293 294 # Filter the entries 295 matching_entries = [] 296 for entry in matches: 297 if entry.is_directory(): 298 # Skip directories 299 continue 300 if entry.get_longname() == filename: 301 matching_entries.append(entry) 302 elif '*' in filename: 303 regexp = filename.replace('.', '\\.').replace('*', '.*') 304 if re.match(regexp, entry.get_longname()): 305 matching_entries.append(entry) 306 307 matching_entries = sorted(list(set(matching_entries)), key=lambda x: x.get_longname()) 308 309 for entry in matching_entries: 310 if entry.is_directory(): 311 self.logger.debug("[>] Skipping '%s' because it is a directory." % (tmp_search_path + ntpath.sep + entry.get_longname())) 312 else: 313 try: 314 if ntpath.sep in path: 315 outputfile = ntpath.dirname(path) + ntpath.sep + entry.get_longname() 316 else: 317 outputfile = entry.get_longname() 318 f = LocalFileIO( 319 mode="wb", 320 path=outputfile, 321 expected_size=entry.get_filesize(), 322 debug=self.config.debug, 323 keepRemotePath=keepRemotePath 324 ) 325 self.smbClient.getFile( 326 shareName=self.smb_share, 327 pathName=tmp_search_path + ntpath.sep + entry.get_longname(), 328 callback=f.write 329 ) 330 f.close() 331 except (BrokenPipeError, KeyboardInterrupt) as e: 332 f.close() 333 print("\x1b[v\x1b[o\r[!] Interrupted.") 334 self.close_smb_session() 335 self.init_smb_session() 336 337 return None
Retrieves a file from the specified path on the SMB share.
This method attempts to retrieve a file from the given path within the currently connected SMB share. If the path points to a directory, it skips the retrieval. It handles file retrieval by creating a local file object and writing the contents of the remote file to it using the SMB client's getFile method.
Parameters: path (str): The path of the file to retrieve. If None, uses the current smb_path.
Returns: None
339 def get_file_recursively(self, path=None): 340 """ 341 Recursively retrieves files from a specified path on the SMB share. 342 343 This method navigates through all directories starting from the given path, 344 and downloads all files found. It handles directories recursively, ensuring 345 that all nested files are retrieved. The method skips over directory entries 346 and handles errors gracefully, attempting to continue the operation where possible. 347 348 Parameters: 349 path (str): The initial directory path from which to start the recursive file retrieval. 350 If None, it starts from the root of the configured SMB share. 351 """ 352 353 def recurse_action(base_dir="", path=[]): 354 if len(base_dir) == 0: 355 remote_smb_path = ntpath.sep.join(path) 356 else: 357 remote_smb_path = base_dir + ntpath.sep + ntpath.sep.join(path) 358 remote_smb_path = ntpath.normpath(remote_smb_path) 359 360 entries = self.smbClient.listPath( 361 shareName=self.smb_share, 362 path=remote_smb_path + '\\*' 363 ) 364 if len(entries) != 0: 365 files = [entry for entry in entries if not entry.is_directory()] 366 directories = [entry for entry in entries if entry.is_directory() and entry.get_longname() not in [".", ".."]] 367 368 # Files 369 if len(files) != 0: 370 self.logger.print("[>] Retrieving files of '%s'" % remote_smb_path) 371 for entry_file in files: 372 if not entry_file.is_directory(): 373 f = LocalFileIO( 374 mode="wb", 375 path=remote_smb_path + ntpath.sep + entry_file.get_longname(), 376 expected_size=entry_file.get_filesize(), 377 keepRemotePath=True, 378 debug=self.config.debug 379 ) 380 try: 381 self.smbClient.getFile( 382 shareName=self.smb_share, 383 pathName=remote_smb_path + ntpath.sep + entry_file.get_longname(), 384 callback=f.write 385 ) 386 f.close() 387 except BrokenPipeError as err: 388 f.set_error(message="[bold red]Failed downloading '%s': %s" % (f.path, err)) 389 f.close(remove=True) 390 break 391 except Exception as err: 392 f.set_error(message="[bold red]Failed downloading '%s': %s" % (f.path, err)) 393 f.close(remove=True) 394 395 # Directories 396 for entry_directory in directories: 397 if entry_directory.is_directory(): 398 recurse_action( 399 base_dir=self.smb_cwd, 400 path=path+[entry_directory.get_longname()] 401 ) 402 # Entrypoint 403 try: 404 recurse_action( 405 base_dir=self.smb_cwd, 406 path=[path] 407 ) 408 except (BrokenPipeError, KeyboardInterrupt) as e: 409 print("\x1b[v\x1b[o\r[!] Interrupted.") 410 self.close_smb_session() 411 self.init_smb_session()
Recursively retrieves files from a specified path on the SMB share.
This method navigates through all directories starting from the given path, and downloads all files found. It handles directories recursively, ensuring that all nested files are retrieved. The method skips over directory entries and handles errors gracefully, attempting to continue the operation where possible.
Parameters: path (str): The initial directory path from which to start the recursive file retrieval. If None, it starts from the root of the configured SMB share.
413 def get_entry(self, path=None): 414 """ 415 Retrieves information about a specific entry located at the provided path on the SMB share. 416 417 This method checks if the specified path exists on the SMB share. If the path exists, it retrieves the details of the entry at that path, including the directory name and file name. If the entry is found, it returns the entry object; otherwise, it returns None. 418 419 Args: 420 path (str): The path of the entry to retrieve information about. 421 422 Returns: 423 Entry: An object representing the entry at the specified path, or None if the entry is not found. 424 """ 425 426 if self.path_exists(path=path): 427 matches = self.smbClient.listPath( 428 shareName=self.smb_share, 429 path=path 430 ) 431 432 if len(matches) == 1: 433 return matches[0] 434 else: 435 return None 436 else: 437 return None
Retrieves information about a specific entry located at the provided path on the SMB share.
This method checks if the specified path exists on the SMB share. If the path exists, it retrieves the details of the entry at that path, including the directory name and file name. If the entry is found, it returns the entry object; otherwise, it returns None.
Args: path (str): The path of the entry to retrieve information about.
Returns: Entry: An object representing the entry at the specified path, or None if the entry is not found.
439 def info(self, share=True, server=True): 440 """ 441 Displays information about the server and optionally the shares. 442 443 This method prints detailed information about the server's characteristics such as NetBIOS names, DNS details, OS information, and SMB capabilities. If the `share` parameter is set to True and a share is currently set, it will also attempt to display information about the share. 444 445 Parameters: 446 share (bool): If True, display information about the current share. 447 server (bool): If True, display information about the server. 448 449 Returns: 450 None 451 """ 452 453 if server: 454 if self.config.no_colors: 455 self.logger.print("[+] Server:") 456 self.logger.print(" ├─NetBIOS:") 457 self.logger.print(" │ ├─ NetBIOS Hostname ──────── : %s" % (self.smbClient.getServerName())) 458 self.logger.print(" │ └─ NetBIOS Domain ────────── : %s" % (self.smbClient.getServerDomain())) 459 self.logger.print(" ├─DNS:") 460 self.logger.print(" │ ├─ DNS Hostname ──────────── : %s" % (self.smbClient.getServerDNSHostName())) 461 self.logger.print(" │ └─ DNS Domain ────────────── : %s" % (self.smbClient.getServerDNSDomainName())) 462 self.logger.print(" ├─OS:") 463 self.logger.print(" │ ├─ OS Name ───────────────── : %s" % (self.smbClient.getServerOS())) 464 self.logger.print(" │ └─ OS Version ────────────── : %s.%s.%s" % (self.smbClient.getServerOSMajor(), self.smbClient.getServerOSMinor(), self.smbClient.getServerOSBuild())) 465 self.logger.print(" ├─Server:") 466 self.logger.print(" │ ├─ Signing Required ──────── : %s" % (self.smbClient.isSigningRequired())) 467 self.logger.print(" │ ├─ Login Required ────────── : %s" % (self.smbClient.isLoginRequired())) 468 self.logger.print(" │ ├─ Supports NTLMv2 ───────── : %s" % (self.smbClient.doesSupportNTLMv2())) 469 MaxReadSize = self.smbClient.getIOCapabilities()["MaxReadSize"] 470 self.logger.print(" │ ├─ Max size of read chunk ── : %d bytes (%s)" % (MaxReadSize, b_filesize(MaxReadSize))) 471 MaxWriteSize = self.smbClient.getIOCapabilities()["MaxWriteSize"] 472 self.logger.print(" │ └─ Max size of write chunk ─ : %d bytes (%s)" % (MaxWriteSize, b_filesize(MaxWriteSize))) 473 self.logger.print(" └─") 474 else: 475 self.logger.print("[+] Server:") 476 self.logger.print(" ├─NetBIOS:") 477 self.logger.print(" │ ├─ \x1b[94mNetBIOS Hostname\x1b[0m \x1b[90m────────\x1b[0m : \x1b[93m%s\x1b[0m" % (self.smbClient.getServerName())) 478 self.logger.print(" │ └─ \x1b[94mNetBIOS Domain\x1b[0m \x1b[90m──────────\x1b[0m : \x1b[93m%s\x1b[0m" % (self.smbClient.getServerDomain())) 479 self.logger.print(" ├─DNS:") 480 self.logger.print(" │ ├─ \x1b[94mDNS Hostname\x1b[0m \x1b[90m────────────\x1b[0m : \x1b[93m%s\x1b[0m" % (self.smbClient.getServerDNSHostName())) 481 self.logger.print(" │ └─ \x1b[94mDNS Domain\x1b[0m \x1b[90m──────────────\x1b[0m : \x1b[93m%s\x1b[0m" % (self.smbClient.getServerDNSDomainName())) 482 self.logger.print(" ├─OS:") 483 self.logger.print(" │ ├─ \x1b[94mOS Name\x1b[0m \x1b[90m─────────────────\x1b[0m : \x1b[93m%s\x1b[0m" % (self.smbClient.getServerOS())) 484 self.logger.print(" │ └─ \x1b[94mOS Version\x1b[0m \x1b[90m──────────────\x1b[0m : \x1b[93m%s.%s.%s\x1b[0m" % (self.smbClient.getServerOSMajor(), self.smbClient.getServerOSMinor(), self.smbClient.getServerOSBuild())) 485 self.logger.print(" ├─Server:") 486 self.logger.print(" │ ├─ \x1b[94mSigning Required\x1b[0m \x1b[90m────────\x1b[0m : \x1b[93m%s\x1b[0m" % (self.smbClient.isSigningRequired())) 487 self.logger.print(" │ ├─ \x1b[94mLogin Required\x1b[0m \x1b[90m──────────\x1b[0m : \x1b[93m%s\x1b[0m" % (self.smbClient.isLoginRequired())) 488 self.logger.print(" │ ├─ \x1b[94mSupports NTLMv2\x1b[0m \x1b[90m─────────\x1b[0m : \x1b[93m%s\x1b[0m" % (self.smbClient.doesSupportNTLMv2())) 489 MaxReadSize = self.smbClient.getIOCapabilities()["MaxReadSize"] 490 self.logger.print(" │ ├─ \x1b[94mMax size of read chunk\x1b[0m \x1b[90m──\x1b[0m : \x1b[93m%d bytes (%s)\x1b[0m" % (MaxReadSize, b_filesize(MaxReadSize))) 491 MaxWriteSize = self.smbClient.getIOCapabilities()["MaxWriteSize"] 492 self.logger.print(" │ └─ \x1b[94mMax size of write chunk\x1b[0m \x1b[90m─\x1b[0m : \x1b[93m%d bytes (%s)\x1b[0m" % (MaxWriteSize, b_filesize(MaxWriteSize))) 493 self.logger.print(" └─") 494 495 if share and self.smb_share is not None: 496 share_name = self.available_shares.get(self.smb_share.lower(), "")["name"] 497 share_comment = self.available_shares.get(self.smb_share.lower(), "")["comment"] 498 share_type = self.available_shares.get(self.smb_share.lower(), "")["type"] 499 share_type =', '.join([s.replace("STYPE_","") for s in share_type]) 500 share_rawtype = self.available_shares.get(self.smb_share.lower(), "")["rawtype"] 501 if self.config.no_colors: 502 self.logger.print("\n[+] Share:") 503 self.logger.print(" ├─ Name ──────────── : %s" % (share_name)) 504 self.logger.print(" ├─ Description ───── : %s" % (share_comment)) 505 self.logger.print(" ├─ Type ──────────── : %s" % (share_type)) 506 self.logger.print(" └─ Raw type value ── : %s" % (share_rawtype)) 507 else: 508 self.logger.print("\n[+] Share:") 509 self.logger.print(" ├─ \x1b[94mName\x1b[0m \x1b[90m────────────\x1b[0m : \x1b[93m%s\x1b[0m" % (share_name)) 510 self.logger.print(" ├─ \x1b[94mDescription\x1b[0m \x1b[90m─────\x1b[0m : \x1b[93m%s\x1b[0m" % (share_comment)) 511 self.logger.print(" ├─ \x1b[94mType\x1b[0m \x1b[90m────────────\x1b[0m : \x1b[93m%s\x1b[0m" % (share_type)) 512 self.logger.print(" └─ \x1b[94mRaw type value\x1b[0m \x1b[90m──\x1b[0m : \x1b[93m%s\x1b[0m" % (share_rawtype))
Displays information about the server and optionally the shares.
This method prints detailed information about the server's characteristics such as NetBIOS names, DNS details, OS information, and SMB capabilities. If the share parameter is set to True and a share is currently set, it will also attempt to display information about the share.
Parameters: share (bool): If True, display information about the current share. server (bool): If True, display information about the server.
Returns: None
514 def list_contents(self, path=None): 515 """ 516 Lists the contents of a specified directory on the SMB share. 517 518 This method retrieves the contents of a directory specified by `shareName` and `path`. If `shareName` or `path` 519 is not provided, it defaults to the instance's current SMB share or path. The method returns a dictionary with 520 the long names of the files and directories as keys and their respective SMB entry objects as values. 521 522 Args: 523 shareName (str, optional): The name of the SMB share. Defaults to the current SMB share if None. 524 path (str, optional): The directory path to list contents from. Defaults to the current path if None. 525 526 Returns: 527 dict: A dictionary with file and directory names as keys and their SMB entry objects as values. 528 """ 529 530 dest_path = [self.smb_cwd.rstrip(ntpath.sep),] 531 if path is not None and len(path) > 0: 532 dest_path.append(path.rstrip(ntpath.sep)) 533 dest_path.append('*') 534 path = ntpath.sep.join(dest_path) 535 536 contents = {} 537 entries = self.smbClient.listPath( 538 shareName=self.smb_share, 539 path=path 540 ) 541 for entry in entries: 542 contents[entry.get_longname()] = entry 543 544 return contents
Lists the contents of a specified directory on the SMB share.
This method retrieves the contents of a directory specified by shareName and path. If shareName or path
is not provided, it defaults to the instance's current SMB share or path. The method returns a dictionary with
the long names of the files and directories as keys and their respective SMB entry objects as values.
Args: shareName (str, optional): The name of the SMB share. Defaults to the current SMB share if None. path (str, optional): The directory path to list contents from. Defaults to the current path if None.
Returns: dict: A dictionary with file and directory names as keys and their SMB entry objects as values.
582 def mkdir(self, path=None): 583 """ 584 Creates a directory at the specified path on the SMB share. 585 586 This method takes a path and attempts to create the directory structure on the SMB share. If the path includes 587 nested directories, it will create each directory in the sequence. If a directory already exists, it will skip 588 the creation for that directory without raising an error. 589 590 Args: 591 path (str, optional): The full path of the directory to create on the SMB share. Defaults to None. 592 593 Note: 594 The path should use forward slashes ('/') which will be converted to backslashes (ntpath.sep) for SMB compatibility. 595 """ 596 597 if path is not None: 598 # Prepare path 599 path = path.replace('/',ntpath.sep) 600 if ntpath.sep in path: 601 path = path.strip(ntpath.sep).split(ntpath.sep) 602 else: 603 path = [path] 604 605 # Create each dir in the path 606 for depth in range(1, len(path)+1): 607 tmp_path = ntpath.sep.join(path[:depth]) 608 try: 609 self.smbClient.createDirectory( 610 shareName=self.smb_share, 611 pathName=ntpath.normpath(self.smb_cwd + ntpath.sep + tmp_path + ntpath.sep) 612 ) 613 except impacket.smbconnection.SessionError as err: 614 if err.getErrorCode() == 0xc0000035: 615 # STATUS_OBJECT_NAME_COLLISION 616 # Remote directory already created, this is normal 617 # Src: https://github.com/fortra/impacket/blob/269ce69872f0e8f2188a80addb0c39fedfa6dcb8/impacket/nt_errors.py#L268C9-L268C19 618 pass 619 else: 620 self.logger.error("Failed to create directory '%s': %s" % (tmp_path, err)) 621 if self.config.debug: 622 traceback.print_exc() 623 else: 624 pass
Creates a directory at the specified path on the SMB share.
This method takes a path and attempts to create the directory structure on the SMB share. If the path includes nested directories, it will create each directory in the sequence. If a directory already exists, it will skip the creation for that directory without raising an error.
Args: path (str, optional): The full path of the directory to create on the SMB share. Defaults to None.
Note: The path should use forward slashes ('/') which will be converted to backslashes (ntpath.sep) for SMB compatibility.
626 def mount(self, local_mount_point, remote_path): 627 """ 628 Generates the command to mount an SMB share on different platforms. 629 630 This method takes the local mount point and the remote path of the SMB share and generates the appropriate mount command based on the platform. 631 It constructs the mount command using the provided parameters and executes it using the os.system() function. 632 633 Args: 634 local_mount_point (str): The local directory where the SMB share will be mounted. 635 remote_path (str): The remote path on the SMB share to be mounted. 636 637 Note: 638 - For Windows platform, the command uses 'net use' to mount the share. 639 - For Linux platform, the command uses 'mount' to mount the share. 640 - For macOS platform, the command uses 'mount_smbfs' to mount the share. 641 - If the platform is not supported, an error message is displayed. 642 643 Returns: 644 None 645 """ 646 647 if not os.path.exists(local_mount_point): 648 pass 649 650 if sys.platform.startswith('win'): 651 remote_path = remote_path.replace('/',ntpath.sep) 652 command = f"net use {local_mount_point} \\\\{self.host}\\{self.smb_share}\\{remote_path}" 653 654 elif sys.platform.startswith('linux'): 655 remote_path = remote_path.replace(ntpath.sep,'/') 656 command = f"mount -t cifs //{self.host}/{self.smb_share}/{remote_path} {local_mount_point} -o username={self.credentials.username},password={self.credentials.password}" 657 658 elif sys.platform.startswith('darwin'): 659 remote_path = remote_path.replace(ntpath.sep,'/') 660 command = f"mount_smbfs //{self.credentials.username}:{self.credentials.password}@{self.host}/{self.smb_share}/{remote_path} {local_mount_point}" 661 662 else: 663 command = None 664 self.logger.error("Unsupported platform for mounting SMB share.") 665 666 if command is not None: 667 if self.config.debug: 668 self.logger.debug("Executing: %s" % command) 669 os.system(command)
Generates the command to mount an SMB share on different platforms.
This method takes the local mount point and the remote path of the SMB share and generates the appropriate mount command based on the platform. It constructs the mount command using the provided parameters and executes it using the os.system() function.
Args: local_mount_point (str): The local directory where the SMB share will be mounted. remote_path (str): The remote path on the SMB share to be mounted.
Note: - For Windows platform, the command uses 'net use' to mount the share. - For Linux platform, the command uses 'mount' to mount the share. - For macOS platform, the command uses 'mount_smbfs' to mount the share. - If the platform is not supported, an error message is displayed.
Returns: None
671 def path_exists(self, path=None): 672 """ 673 Checks if the specified path exists on the SMB share. 674 675 This method determines if a given path exists on the SMB share by attempting to list the contents of the path. 676 If the path listing is successful and returns one or more entries, the path is considered to exist. 677 678 Args: 679 path (str, optional): The path to check on the SMB share. Defaults to None. 680 681 Returns: 682 bool: True if the path exists, False otherwise or if an error occurs. 683 """ 684 685 if path is not None: 686 path = path.replace('*','') 687 path = path.replace('/', ntpath.sep) 688 try: 689 contents = self.smbClient.listPath( 690 shareName=self.smb_share, 691 path=ntpath.normpath(self.smb_cwd + ntpath.sep + path + ntpath.sep) 692 ) 693 return (len(contents) != 0) 694 except Exception as e: 695 return False 696 else: 697 return False
Checks if the specified path exists on the SMB share.
This method determines if a given path exists on the SMB share by attempting to list the contents of the path. If the path listing is successful and returns one or more entries, the path is considered to exist.
Args: path (str, optional): The path to check on the SMB share. Defaults to None.
Returns: bool: True if the path exists, False otherwise or if an error occurs.
699 def path_isdir(self, pathFromRoot=None): 700 """ 701 Checks if the specified path is a directory on the SMB share. 702 703 This method determines if a given path corresponds to a directory on the SMB share. It does this by listing the 704 contents of the path and filtering for entries that match the basename of the path and are marked as directories. 705 706 Args: 707 path (str, optional): The path to check on the SMB share. Defaults to None. 708 709 Returns: 710 bool: True if the path is a directory, False otherwise or if an error occurs. 711 """ 712 713 if pathFromRoot is not None: 714 # Strip wildcards to avoid injections 715 path = pathFromRoot.replace('*','') 716 # Replace slashes if any 717 path = path.replace('/', ntpath.sep) 718 719 # Normalize path and strip leading backslash 720 path = ntpath.normpath(path + ntpath.sep).lstrip(ntpath.sep) 721 722 if path.strip() in ['', '.', '..']: 723 # By defininition they exist on the filesystem 724 return True 725 else: 726 try: 727 contents = self.smbClient.listPath( 728 shareName=self.smb_share, 729 path=path+'*' 730 ) 731 # Filter on directories 732 contents = [ 733 c for c in contents 734 if c.get_longname() == ntpath.basename(path) and c.is_directory() 735 ] 736 return (len(contents) != 0) 737 except Exception as e: 738 return False 739 else: 740 return False
Checks if the specified path is a directory on the SMB share.
This method determines if a given path corresponds to a directory on the SMB share. It does this by listing the contents of the path and filtering for entries that match the basename of the path and are marked as directories.
Args: path (str, optional): The path to check on the SMB share. Defaults to None.
Returns: bool: True if the path is a directory, False otherwise or if an error occurs.
742 def path_isfile(self, pathFromRoot=None): 743 """ 744 Checks if the specified path is a file on the SMB share. 745 746 This method determines if a given path corresponds to a file on the SMB share. It does this by listing the 747 contents of the path and filtering for entries that match the basename of the path and are not marked as directories. 748 749 Args: 750 path (str, optional): The path to check on the SMB share. Defaults to None. 751 752 Returns: 753 bool: True if the path is a file, False otherwise or if an error occurs. 754 """ 755 756 if pathFromRoot is not None: 757 # Strip wildcards to avoid injections 758 path = pathFromRoot.replace('*','') 759 # Replace slashes if any 760 path = path.replace('/', ntpath.sep) 761 762 # Normalize path and strip leading backslash 763 path = ntpath.normpath(path + ntpath.sep).lstrip(ntpath.sep) 764 765 try: 766 contents = self.smbClient.listPath( 767 shareName=self.smb_share, 768 path=ntpath.dirname(path) + ntpath.sep + '*' 769 ) 770 # Filter on files 771 contents = [ 772 c for c in contents 773 if c.get_longname() == ntpath.basename(path) and not c.is_directory() 774 ] 775 return (len(contents) != 0) 776 except Exception as e: 777 return False 778 else: 779 return False
Checks if the specified path is a file on the SMB share.
This method determines if a given path corresponds to a file on the SMB share. It does this by listing the contents of the path and filtering for entries that match the basename of the path and are not marked as directories.
Args: path (str, optional): The path to check on the SMB share. Defaults to None.
Returns: bool: True if the path is a file, False otherwise or if an error occurs.
781 def put_file(self, localpath=None): 782 """ 783 Uploads a single file to the SMB share. 784 785 This method takes a local file path, opens the file, and uploads it to the SMB share at the specified path. 786 It handles exceptions such as broken pipe errors or keyboard interrupts by closing and reinitializing the SMB session. 787 General exceptions are caught and logged, with a traceback provided if debugging is enabled. 788 789 Args: 790 localpath (str, optional): The local file path of the file to be uploaded. Defaults to None. 791 """ 792 793 # Parse path 794 localpath = localpath.replace('/', os.path.sep) 795 if os.path.sep in localpath: 796 if localpath.startswith(os.path.sep): 797 # Absolute path 798 tmp_search_path = os.path.normpath(localpath) 799 else: 800 # Relative path 801 tmp_search_path = os.path.normpath(os.getcwd() + os.path.sep + os.path.dirname(localpath)) 802 else: 803 tmp_search_path = os.path.normpath(os.getcwd() + os.path.sep) 804 805 # Parse filename 806 filename = os.path.basename(localpath) 807 808 # Search for the file 809 matches = os.listdir(tmp_search_path) 810 # Filter the entries 811 matching_entries = [] 812 for entry in matches: 813 if entry == filename: 814 matching_entries.append(entry) 815 elif '*' in filename: 816 regexp = filename.replace('.', '\\.').replace('*', '.*') 817 if re.match(regexp, entry): 818 matching_entries.append(entry) 819 820 matching_entries = sorted(list(set(matching_entries))) 821 822 # Loop and upload 823 for localpath in matching_entries: 824 if os.path.exists(localpath): 825 if os.path.isfile(localpath): 826 try: 827 localfile = os.path.basename(localpath) 828 f = LocalFileIO( 829 mode="rb", 830 path=localpath, 831 debug=self.config.debug 832 ) 833 self.smbClient.putFile( 834 shareName=self.smb_share, 835 pathName=ntpath.normpath(self.smb_cwd + ntpath.sep + localfile + ntpath.sep), 836 callback=f.read 837 ) 838 f.close() 839 840 except (BrokenPipeError, KeyboardInterrupt) as err: 841 self.logger.error("Interrupted.") 842 self.close_smb_session() 843 self.init_smb_session() 844 845 except (Exception, PermissionError) as err: 846 f.set_error(message="[bold red]Failed uploading '%s': %s" % (f.path, err)) 847 f.close(remove=False) 848 if self.config.debug: 849 traceback.print_exc() 850 else: 851 # [!] The specified localpath is a directory. Use 'put -r <directory>' instead. 852 pass 853 else: 854 # [!] The specified localpath does not exist. 855 pass
Uploads a single file to the SMB share.
This method takes a local file path, opens the file, and uploads it to the SMB share at the specified path. It handles exceptions such as broken pipe errors or keyboard interrupts by closing and reinitializing the SMB session. General exceptions are caught and logged, with a traceback provided if debugging is enabled.
Args: localpath (str, optional): The local file path of the file to be uploaded. Defaults to None.
857 def put_file_recursively(self, localpath=None): 858 """ 859 Recursively uploads files from a specified local directory to the SMB share. 860 861 This method walks through the given local directory and all its subdirectories, uploading each file to the 862 corresponding directory structure on the SMB share. It first checks if the local path is a directory. If it is, 863 it iterates over all files and directories within the local path, creating necessary directories on the SMB share 864 and uploading files. If the local path is not a directory, it prints an error message. 865 866 Args: 867 localpath (str, optional): The local directory path from which files will be uploaded. Defaults to None. 868 """ 869 870 if os.path.exists(localpath): 871 if os.path.isdir(localpath): 872 # Iterate over all files and directories within the local path 873 local_files = {} 874 for root, dirs, files in os.walk(localpath): 875 if len(files) != 0: 876 local_files[root] = files 877 878 # Iterate over the found files 879 for local_dir_path in sorted(local_files.keys()): 880 self.logger.print("[>] Putting files of '%s'" % local_dir_path) 881 882 # Create remote directory 883 remote_dir_path = local_dir_path.replace(os.path.sep, ntpath.sep) 884 self.mkdir( 885 path=ntpath.normpath(remote_dir_path + ntpath.sep) 886 ) 887 888 for local_file_path in local_files[local_dir_path]: 889 try: 890 f = LocalFileIO( 891 mode="rb", 892 path=local_dir_path + os.path.sep + local_file_path, 893 debug=self.config.debug 894 ) 895 self.smbClient.putFile( 896 shareName=self.smb_share, 897 pathName=ntpath.normpath(self.smb_cwd + ntpath.sep + remote_dir_path + ntpath.sep + local_file_path), 898 callback=f.read 899 ) 900 f.close() 901 902 except (BrokenPipeError, KeyboardInterrupt) as err: 903 self.logger.error("Interrupted.") 904 self.close_smb_session() 905 self.init_smb_session() 906 907 except (Exception, PermissionError) as err: 908 f.set_error(message="[bold red]Failed uploading '%s': %s" % (f.path, err)) 909 f.close(remove=False) 910 if self.config.debug: 911 traceback.print_exc() 912 else: 913 self.logger.error("The specified localpath is a file. Use 'put <file>' instead.") 914 else: 915 self.logger.error("The specified localpath does not exist.")
Recursively uploads files from a specified local directory to the SMB share.
This method walks through the given local directory and all its subdirectories, uploading each file to the corresponding directory structure on the SMB share. It first checks if the local path is a directory. If it is, it iterates over all files and directories within the local path, creating necessary directories on the SMB share and uploading files. If the local path is not a directory, it prints an error message.
Args: localpath (str, optional): The local directory path from which files will be uploaded. Defaults to None.
917 def read_file(self, path=None): 918 """ 919 Reads a file from the SMB share. 920 921 This method attempts to read the contents of a file specified by the `path` parameter from the SMB share. 922 It constructs the full path to the file, checks if the path is a valid file, and then reads the file content 923 into a byte stream which is returned to the caller. 924 925 Args: 926 path (str, optional): The path of the file to be read from the SMB share. Defaults to None. 927 928 Returns: 929 bytes: The content of the file as a byte stream, or None if the file does not exist or an error occurs. 930 """ 931 932 if self.path_isfile(pathFromRoot=path): 933 path = path.replace('/', ntpath.sep) 934 if path.startswith(ntpath.sep): 935 # Absolute path 936 tmp_file_path = ntpath.normpath(path) 937 else: 938 # Relative path 939 tmp_file_path = ntpath.normpath(self.smb_cwd + ntpath.sep + path) 940 tmp_file_path = tmp_file_path.lstrip(ntpath.sep) 941 942 fh = io.BytesIO() 943 try: 944 # opening the files in streams instead of mounting shares allows 945 # for running the script from unprivileged containers 946 self.smbClient.getFile(self.smb_share, tmp_file_path, fh.write) 947 except impacket.smbconnection.SessionError as e: 948 return None 949 rawdata = fh.getvalue() 950 fh.close() 951 return rawdata 952 else: 953 return None
Reads a file from the SMB share.
This method attempts to read the contents of a file specified by the path parameter from the SMB share.
It constructs the full path to the file, checks if the path is a valid file, and then reads the file content
into a byte stream which is returned to the caller.
Args: path (str, optional): The path of the file to be read from the SMB share. Defaults to None.
Returns: bytes: The content of the file as a byte stream, or None if the file does not exist or an error occurs.
955 def rmdir(self, path=None): 956 """ 957 Removes a directory from the SMB share at the specified path. 958 959 This method attempts to delete a directory located at the given path on the SMB share. If the operation fails, 960 it prints an error message indicating the failure and the reason. If debugging is enabled, it also prints 961 the stack trace of the exception. 962 963 Args: 964 path (str, optional): The path of the directory to be removed on the SMB share. Defaults to None. 965 """ 966 try: 967 self.smbClient.deleteDirectory( 968 shareName=self.smb_share, 969 pathName=ntpath.normpath(self.smb_cwd + ntpath.sep + path), 970 ) 971 except Exception as err: 972 self.logger.error("Failed to remove directory '%s': %s" % (path, err)) 973 if self.config.debug: 974 traceback.print_exc()
Removes a directory from the SMB share at the specified path.
This method attempts to delete a directory located at the given path on the SMB share. If the operation fails, it prints an error message indicating the failure and the reason. If debugging is enabled, it also prints the stack trace of the exception.
Args: path (str, optional): The path of the directory to be removed on the SMB share. Defaults to None.
976 def rm(self, path=None): 977 """ 978 Removes a file from the SMB share at the specified path. 979 980 This method attempts to delete a file located at the given path on the SMB share. If the operation fails, 981 it prints an error message indicating the failure and the reason. If debugging is enabled, it also prints 982 the stack trace of the exception. 983 984 Args: 985 path (str, optional): The path of the file to be removed on the SMB share. Defaults to None. 986 """ 987 988 # Parse path 989 path = path.replace('/', ntpath.sep) 990 if ntpath.sep in path: 991 tmp_search_path = ntpath.normpath(self.smb_cwd + ntpath.sep + ntpath.dirname(path)) 992 else: 993 tmp_search_path = ntpath.normpath(self.smb_cwd + ntpath.sep) 994 # Parse filename 995 filename = ntpath.basename(path) 996 997 # Search for the file 998 matches = self.smbClient.listPath( 999 shareName=self.smb_share, 1000 path=tmp_search_path + ntpath.sep + '*' 1001 ) 1002 1003 # Filter the entries 1004 matching_entries = [] 1005 for entry in matches: 1006 if entry.is_directory(): 1007 # Skip directories 1008 continue 1009 if entry.get_longname() == filename: 1010 matching_entries.append(entry) 1011 elif '*' in filename: 1012 regexp = filename.replace('.', '\\.').replace('*', '.*') 1013 if re.match(regexp, entry.get_longname()): 1014 matching_entries.append(entry) 1015 1016 matching_entries = sorted(list(set(matching_entries)), key=lambda x: x.get_longname()) 1017 1018 for entry in matching_entries: 1019 try: 1020 self.smbClient.deleteFile( 1021 shareName=self.smb_share, 1022 pathName=ntpath.normpath(tmp_search_path + ntpath.sep + entry.get_longname()), 1023 ) 1024 except Exception as err: 1025 self.logger.error("Failed to remove file '%s': %s" % (path, err)) 1026 if self.config.debug: 1027 traceback.print_exc()
Removes a file from the SMB share at the specified path.
This method attempts to delete a file located at the given path on the SMB share. If the operation fails, it prints an error message indicating the failure and the reason. If debugging is enabled, it also prints the stack trace of the exception.
Args: path (str, optional): The path of the file to be removed on the SMB share. Defaults to None.
1029 def tree(self, path=None): 1030 """ 1031 Recursively lists the directory structure of the SMB share starting from the specified path. 1032 1033 This function prints a visual representation of the directory tree of the remote SMB share. It uses 1034 recursion to navigate through directories and lists all files and subdirectories in each directory. 1035 The output is color-coded and formatted to enhance readability, with directories highlighted in cyan. 1036 1037 Args: 1038 path (str, optional): The starting path on the SMB share from which to begin listing the tree. 1039 Defaults to the root of the current share. 1040 """ 1041 1042 def recurse_action(base_dir="", path=[], prompt=[]): 1043 bars = ["│ ", "├── ", "└── "] 1044 1045 remote_smb_path = ntpath.normpath(base_dir + ntpath.sep + ntpath.sep.join(path)) 1046 1047 entries = [] 1048 try: 1049 entries = self.smbClient.listPath( 1050 shareName=self.smb_share, 1051 path=remote_smb_path+'\\*' 1052 ) 1053 except impacket.smbconnection.SessionError as err: 1054 code, const, text = err.getErrorCode(), err.getErrorString()[0], err.getErrorString()[1] 1055 errmsg = "Error 0x%08x (%s): %s" % (code, const, text) 1056 if self.config.no_colors: 1057 self.logger.print("%s%s" % (''.join(prompt+[bars[2]]), errmsg)) 1058 else: 1059 self.logger.print("%s\x1b[1;91m%s\x1b[0m" % (''.join(prompt+[bars[2]]), errmsg)) 1060 return 1061 1062 entries = [e for e in entries if e.get_longname() not in [".", ".."]] 1063 entries = sorted(entries, key=lambda x:x.get_longname()) 1064 1065 # 1066 if len(entries) > 1: 1067 index = 0 1068 for entry in entries: 1069 index += 1 1070 # This is the first entry 1071 if index == 0: 1072 if entry.is_directory(): 1073 if self.config.no_colors: 1074 self.logger.print("%s%s\\" % (''.join(prompt+[bars[1]]), entry.get_longname())) 1075 else: 1076 self.logger.print("%s\x1b[1;96m%s\x1b[0m\\" % (''.join(prompt+[bars[1]]), entry.get_longname())) 1077 recurse_action( 1078 base_dir=base_dir, 1079 path=path+[entry.get_longname()], 1080 prompt=prompt+["│ "] 1081 ) 1082 else: 1083 if self.config.no_colors: 1084 self.logger.print("%s%s" % (''.join(prompt+[bars[1]]), entry.get_longname())) 1085 else: 1086 self.logger.print("%s\x1b[1m%s\x1b[0m" % (''.join(prompt+[bars[1]]), entry.get_longname())) 1087 1088 # This is the last entry 1089 elif index == len(entries): 1090 if entry.is_directory(): 1091 if self.config.no_colors: 1092 self.logger.print("%s%s\\" % (''.join(prompt+[bars[2]]), entry.get_longname())) 1093 else: 1094 self.logger.print("%s\x1b[1;96m%s\x1b[0m\\" % (''.join(prompt+[bars[2]]), entry.get_longname())) 1095 recurse_action( 1096 base_dir=base_dir, 1097 path=path+[entry.get_longname()], 1098 prompt=prompt+[" "] 1099 ) 1100 else: 1101 if self.config.no_colors: 1102 self.logger.print("%s%s" % (''.join(prompt+[bars[2]]), entry.get_longname())) 1103 else: 1104 self.logger.print("%s\x1b[1m%s\x1b[0m" % (''.join(prompt+[bars[2]]), entry.get_longname())) 1105 1106 # These are entries in the middle 1107 else: 1108 if entry.is_directory(): 1109 if self.config.no_colors: 1110 self.logger.print("%s%s\\" % (''.join(prompt+[bars[1]]), entry.get_longname())) 1111 else: 1112 self.logger.print("%s\x1b[1;96m%s\x1b[0m\\" % (''.join(prompt+[bars[1]]), entry.get_longname())) 1113 recurse_action( 1114 base_dir=base_dir, 1115 path=path+[entry.get_longname()], 1116 prompt=prompt+["│ "] 1117 ) 1118 else: 1119 if self.config.no_colors: 1120 self.logger.print("%s%s" % (''.join(prompt+[bars[1]]), entry.get_longname())) 1121 else: 1122 self.logger.print("%s\x1b[1m%s\x1b[0m" % (''.join(prompt+[bars[1]]), entry.get_longname())) 1123 1124 # 1125 elif len(entries) == 1: 1126 entry = entries[0] 1127 if entry.is_directory(): 1128 if self.config.no_colors: 1129 self.logger.print("%s%s\\" % (''.join(prompt+[bars[2]]), entry.get_longname())) 1130 else: 1131 self.logger.print("%s\x1b[1;96m%s\x1b[0m\\" % (''.join(prompt+[bars[2]]), entry.get_longname())) 1132 recurse_action( 1133 base_dir=base_dir, 1134 path=path+[entry.get_longname()], 1135 prompt=prompt+[" "] 1136 ) 1137 else: 1138 if self.config.no_colors: 1139 self.logger.print("%s%s" % (''.join(prompt+[bars[2]]), entry.get_longname())) 1140 else: 1141 self.logger.print("%s\x1b[1m%s\x1b[0m" % (''.join(prompt+[bars[2]]), entry.get_longname())) 1142 1143 # Entrypoint 1144 try: 1145 if self.config.no_colors: 1146 self.logger.print("%s\\" % path) 1147 else: 1148 self.logger.print("\x1b[1;96m%s\x1b[0m\\" % path) 1149 recurse_action( 1150 base_dir=self.smb_cwd, 1151 path=[path], 1152 prompt=[""] 1153 ) 1154 except (BrokenPipeError, KeyboardInterrupt) as e: 1155 self.logger.error("Interrupted.") 1156 self.close_smb_session() 1157 self.init_smb_session()
Recursively lists the directory structure of the SMB share starting from the specified path.
This function prints a visual representation of the directory tree of the remote SMB share. It uses recursion to navigate through directories and lists all files and subdirectories in each directory. The output is color-coded and formatted to enhance readability, with directories highlighted in cyan.
Args: path (str, optional): The starting path on the SMB share from which to begin listing the tree. Defaults to the root of the current share.
1159 def umount(self, local_mount_point): 1160 """ 1161 Unmounts the specified local mount point of the remote share. 1162 1163 This method unmounts the specified local mount point of the remote share based on the platform. 1164 It supports Windows, Linux, and macOS platforms for unmounting. 1165 1166 Parameters: 1167 local_mount_point (str): The local mount point to unmount. 1168 1169 Raises: 1170 None 1171 """ 1172 1173 if os.path.exists(local_mount_point): 1174 if sys.platform.startswith('win'): 1175 command = f"net use {local_mount_point} /delete" 1176 1177 elif sys.platform.startswith('linux') or sys.platform.startswith('darwin'): 1178 command = f"umount {local_mount_point}" 1179 1180 else: 1181 command = None 1182 self.logger.error("Unsupported platform for unmounting SMB share.") 1183 1184 if command is not None: 1185 self.logger.debug("Executing: %s" % command) 1186 os.system(command) 1187 else: 1188 self.logger.error("Cannot unmount a non existing path.")
Unmounts the specified local mount point of the remote share.
This method unmounts the specified local mount point of the remote share based on the platform. It supports Windows, Linux, and macOS platforms for unmounting.
Parameters: local_mount_point (str): The local mount point to unmount.
Raises: None
1192 def test_rights(self, sharename): 1193 """ 1194 Tests the read and write access rights of the current SMB session. 1195 1196 This method checks the read and write access rights of the current SMB session by attempting to list paths and create/delete temporary directories. 1197 1198 Returns: 1199 dict: A dictionary containing the read and write access rights status. 1200 - "readable" (bool): Indicates if the session has read access rights. 1201 - "writable" (bool): Indicates if the session has write access rights. 1202 """ 1203 1204 # Restore the current share 1205 current_share = self.smb_share 1206 self.set_share(shareName=sharename) 1207 1208 access_rights = {"readable": False, "writable": False} 1209 try: 1210 self.smbClient.listPath(self.smb_share, '*', password=None) 1211 access_rights["readable"] = True 1212 except impacket.smbconnection.SessionError as e: 1213 access_rights["readable"] = False 1214 1215 try: 1216 temp_dir = ntpath.normpath("\\" + ''.join([random.choice("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPRSTUVWXYZ0123456759") for k in range(16)])) 1217 self.smbClient.createDirectory(self.smb_share, temp_dir) 1218 self.smbClient.deleteDirectory(self.smb_share, temp_dir) 1219 access_rights["writable"] = True 1220 except impacket.smbconnection.SessionError as e: 1221 access_rights["writable"] = False 1222 1223 # Restore the current share 1224 self.set_share(shareName=current_share) 1225 1226 return access_rights
Tests the read and write access rights of the current SMB session.
This method checks the read and write access rights of the current SMB session by attempting to list paths and create/delete temporary directories.
Returns: dict: A dictionary containing the read and write access rights status. - "readable" (bool): Indicates if the session has read access rights. - "writable" (bool): Indicates if the session has write access rights.
1257 def set_cwd(self, path=None): 1258 """ 1259 Sets the current working directory on the SMB share to the specified path. 1260 1261 This method updates the current working directory (cwd) of the SMB session to the given path if it is a valid directory. 1262 If the specified path is not a directory, the cwd remains unchanged. 1263 1264 Parameters: 1265 path (str): The path to set as the current working directory. 1266 1267 Raises: 1268 ValueError: If the specified path is not a directory. 1269 """ 1270 1271 if path is not None: 1272 # Set path separators to ntpath sep 1273 if '/' in path: 1274 path = path.replace('/', ntpath.sep) 1275 1276 if path.startswith(ntpath.sep): 1277 # Absolute path 1278 path = path + ntpath.sep 1279 else: 1280 # Relative path to the CWD 1281 if len(self.smb_cwd) == 0: 1282 path = path + ntpath.sep 1283 else: 1284 path = self.smb_cwd + ntpath.sep + path 1285 1286 # Path normalization 1287 path = ntpath.normpath(path) 1288 path = re.sub(r'\\+', r'\\', path) 1289 1290 if path in ["", ".", ".."]: 1291 self.smb_cwd = "" 1292 else: 1293 if self.path_isdir(pathFromRoot=path.strip(ntpath.sep)): 1294 # Path exists on the remote 1295 self.smb_cwd = ntpath.normpath(path) 1296 else: 1297 # Path does not exists or is not a directory on the remote 1298 self.logger.error("Remote directory '%s' does not exist." % path)
Sets the current working directory on the SMB share to the specified path.
This method updates the current working directory (cwd) of the SMB session to the given path if it is a valid directory. If the specified path is not a directory, the cwd remains unchanged.
Parameters: path (str): The path to set as the current working directory.
Raises: ValueError: If the specified path is not a directory.