Skip to content

Commit 0806923

Browse files
feat(smb): add --get-folder command for downloading remote directories
Add download_file(), download_folder(), and get_folder() methods to the SMB protocol. Refactor get_file_single() to use download_file() with automatic fallback from READ to READ/WRITE access on sharing violations. New CLI args: --get-folder, --recursive, --ignore-empty-folders Example: nxc smb target -u user -p pass --share SYSVOL --get-folder 'domain\Policies' ./output --recursive
1 parent 67d90e0 commit 0806923

2 files changed

Lines changed: 89 additions & 10 deletions

File tree

nxc/protocols/smb.py

Lines changed: 86 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
from impacket.dcerpc.v5.dtypes import NULL
3636
from impacket.dcerpc.v5.dcomrt import DCOMConnection
3737
from impacket.dcerpc.v5.dcom.wmi import CLSID_WbemLevel1Login, IID_IWbemLevel1Login, IWbemLevel1Login
38-
from impacket.smb3structs import FILE_SHARE_WRITE, FILE_SHARE_DELETE, SMB2_0_IOCTL_IS_FSCTL
38+
from impacket.smb3structs import FILE_READ_DATA, FILE_WRITE_DATA, FILE_SHARE_WRITE, FILE_SHARE_DELETE, SMB2_0_IOCTL_IS_FSCTL
3939
from impacket.dcerpc.v5 import tsts as TSTS
4040

4141
from nxc.config import process_secret, host_info_colors, check_guest_account
@@ -1937,24 +1937,100 @@ def put_file(self):
19371937
for src, dest in self.args.put_file:
19381938
self.put_file_single(src, dest)
19391939

1940-
def get_file_single(self, remote_path, download_path):
1940+
def download_file(self, share_name, remote_path, dest_file, access_mode=FILE_READ_DATA):
1941+
try:
1942+
self.logger.debug(f"Getting file from {share_name}:{remote_path} with access mode {access_mode}")
1943+
self.conn.getFile(share_name, remote_path, dest_file, shareAccessMode=access_mode)
1944+
return True
1945+
except SessionError as e:
1946+
if "STATUS_SHARING_VIOLATION" in str(e):
1947+
self.logger.debug(f"Sharing violation on {remote_path}: {e}")
1948+
else:
1949+
self.logger.debug(f"SessionError when attempting to download file {remote_path}: {e}")
1950+
return False
1951+
except Exception as e:
1952+
self.logger.debug(f"Other error when attempting to download file {remote_path}: {e}")
1953+
return False
1954+
1955+
def get_file_single(self, remote_path, download_path, silent=False):
19411956
share_name = self.args.share
1942-
self.logger.display(f'Copying "{remote_path}" to "{download_path}"')
1957+
if not silent:
1958+
self.logger.display(f"Copying '{remote_path}' to '{download_path}'")
19431959
if self.args.append_host:
19441960
download_path = f"{self.hostname}-{remote_path}"
19451961
with open(download_path, "wb+") as file:
1946-
try:
1947-
self.conn.getFile(share_name, remote_path, file.write)
1948-
self.logger.success(f'File "{remote_path}" was downloaded to "{download_path}"')
1949-
except Exception as e:
1950-
self.logger.fail(f'Error writing file "{remote_path}" from share "{share_name}": {e}')
1951-
if os.path.getsize(download_path) == 0:
1952-
os.remove(download_path)
1962+
if self.download_file(share_name, remote_path, file.write):
1963+
if not silent:
1964+
self.logger.success(f"File '{remote_path}' was downloaded to '{download_path}'")
1965+
else:
1966+
self.logger.debug("Opening with READ alone failed, trying to open file with READ/WRITE access")
1967+
if self.download_file(share_name, remote_path, file.write, FILE_READ_DATA | FILE_WRITE_DATA):
1968+
if not silent:
1969+
self.logger.success(f"File '{remote_path}' was downloaded to '{download_path}'")
1970+
else:
1971+
if not silent:
1972+
self.logger.fail(f"Error downloading file '{remote_path}' from share '{share_name}'")
19531973

19541974
def get_file(self):
19551975
for src, dest in self.args.get_file:
19561976
self.get_file_single(src, dest)
19571977

1978+
def download_folder(self, folder, dest, recursive=False, silent=False, base_dir=None, ignore_empty=False):
1979+
self.logger.debug(f"Downloading folder with args: {folder}, {dest}, Recursive: {recursive}, Silent: {silent}, Base dir: {base_dir}, Ignore empty: {ignore_empty}")
1980+
normalized_folder = ntpath.normpath(folder)
1981+
base_folder = os.path.basename(normalized_folder)
1982+
self.logger.debug(f"Base folder: {base_folder}")
1983+
1984+
try:
1985+
items = self.conn.listPath(self.args.share, ntpath.join(folder, "*"))
1986+
except SessionError as e:
1987+
self.logger.error(f"Error listing folder '{folder}': {e}")
1988+
return
1989+
self.logger.debug(f"{len(items)} items in folder: {items}")
1990+
1991+
filtered_items = [item for item in items if item.get_longname() not in [".", ".."]]
1992+
1993+
# create local directory structure regardless of content; download empty folders by default
1994+
# change the Windows path to Linux and then join it with the base directory to get our actual save path
1995+
relative_path = os.path.join(*folder.replace(base_dir or folder, "").lstrip("\\").split("\\"))
1996+
local_folder_path = os.path.join(dest, relative_path)
1997+
1998+
if not filtered_items and ignore_empty:
1999+
self.logger.debug(f"Skipping empty folder '{folder}'")
2000+
return
2001+
2002+
# create the directory for this folder
2003+
os.makedirs(local_folder_path, exist_ok=True)
2004+
if not filtered_items and not silent:
2005+
self.logger.display(f"Created empty directory '{local_folder_path}'")
2006+
2007+
for item in filtered_items:
2008+
item_name = item.get_longname()
2009+
dir_path = ntpath.normpath(ntpath.join(normalized_folder, item_name))
2010+
self.logger.debug(f"Parsing item: {item_name}, {dir_path}")
2011+
2012+
if item.is_directory() and recursive:
2013+
self.logger.debug(f"Found new directory to parse: {dir_path}")
2014+
self.download_folder(dir_path, dest, recursive, silent, base_dir or folder, ignore_empty)
2015+
elif not item.is_directory():
2016+
remote_file_path = ntpath.join(folder, item_name)
2017+
local_file_path = os.path.join(local_folder_path, item_name)
2018+
self.logger.debug(f"{dest=} {remote_file_path=} {relative_path=} {local_folder_path=} {local_file_path=}")
2019+
2020+
try:
2021+
self.get_file_single(remote_file_path, local_file_path, silent)
2022+
except FileNotFoundError:
2023+
self.logger.fail(f"Error downloading file '{remote_file_path}' due to file not found (probably a race condition between listing and downloading)")
2024+
2025+
def get_folder(self):
2026+
recursive = self.args.recursive
2027+
ignore_empty = getattr(self.args, "ignore_empty_folders", False)
2028+
self.logger.debug(f"Recursive option set to {recursive}")
2029+
self.logger.debug(f"Ignore empty folders option set to {ignore_empty}")
2030+
for folder, dest in self.args.get_folder:
2031+
self.download_folder(folder, dest, recursive, False, None, ignore_empty)
2032+
self.logger.success(f"Folder '{folder}' was downloaded to '{dest}'")
2033+
19582034
def enable_remoteops(self, regsecret=False):
19592035
try:
19602036
if regsecret:

nxc/protocols/smb/proto_args.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,9 @@ def proto_args(parser, parents):
9090
files_group = smb_parser.add_argument_group("File Operations")
9191
files_group.add_argument("--put-file", action="append", nargs=2, metavar="FILE", help="Put a local file into remote target, ex: whoami.txt \\\\Windows\\\\Temp\\\\whoami.txt")
9292
files_group.add_argument("--get-file", action="append", nargs=2, metavar="FILE", help="Get a remote file, ex: \\\\Windows\\\\Temp\\\\whoami.txt whoami.txt")
93+
files_group.add_argument("--get-folder", action="append", nargs=2, metavar="DIR", help="Get a remote directory, ex: \\\\Windows\\\\Temp\\\\testing testing")
94+
files_group.add_argument("--recursive", default=False, action="store_true", help="Recursively get a folder")
95+
files_group.add_argument("--ignore-empty-folders", default=False, action="store_true", help="Ignore empty folders when downloading")
9396
files_group.add_argument("--append-host", action="store_true", help="append the host to the get-file filename")
9497

9598
cmd_exec_group = smb_parser.add_argument_group("Command Execution")

0 commit comments

Comments
 (0)