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)
class SMBSession:
  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.

SMBSession(host, port, credentials, config=None, logger=None)
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()
config
logger
host
port
credentials
smbClient
connected
available_shares
smb_share
smb_cwd
smb_tree_id
def close_smb_session(self):
 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.

def init_smb_session(self):
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.

def ping_smb_session(self):
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.

def find(self, paths=[], callback=None):
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.

def get_file(self, path=None, keepRemotePath=False):
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

def get_file_recursively(self, path=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.

def get_entry(self, path=None):
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.

def info(self, share=True, server=True):
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

def list_contents(self, path=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.

def list_shares(self):
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

Lists all the shares available on the connected SMB server.

This method queries the SMB server to retrieve a list of all available shares. It populates the shares dictionary with key-value pairs where the key is the share name and the value is a dictionary containing details about the share such as its name, type, raw type, and any comments associated with the share.

Returns: dict: A dictionary containing information about each share available on the server.

def mkdir(self, path=None):
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.

def mount(self, local_mount_point, remote_path):
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

def path_exists(self, path=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.

def path_isdir(self, pathFromRoot=None):
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.

def path_isfile(self, pathFromRoot=None):
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.

def put_file(self, localpath=None):
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.

def put_file_recursively(self, localpath=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.

def read_file(self, path=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.

def rmdir(self, path=None):
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.

def rm(self, path=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.

def tree(self, path=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.

def umount(self, local_mount_point):
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

def test_rights(self, sharename):
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.

def set_share(self, shareName):
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

Sets the current SMB share to the specified share name.

This method updates the SMB session to use the specified share name. It checks if the share name is valid and updates the smb_share attribute of the SMBSession instance.

Parameters: shareName (str): The name of the share to set as the current SMB share.

Raises: ValueError: If the shareName is None or an empty string.

def set_cwd(self, path=None):
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.