mirror of
https://github.com/Pennyw0rth/NetExec.git
synced 2026-11-08 02:08:18 -10:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
61b51145d4 | ||
|
|
47292c512c | ||
|
|
0806923cc5 |
No files matched your search
@@ -0,0 +1,13 @@
|
||||
from pathlib import PurePosixPath
|
||||
|
||||
|
||||
def sanitize_filename(name: str) -> str:
|
||||
"""Strip path traversal components from an SMB filename.
|
||||
|
||||
Follows the pattern from spider_plus.py — filters '..' and '.' from
|
||||
PurePosixPath.parts to prevent directory traversal attacks from
|
||||
malicious SMB servers.
|
||||
"""
|
||||
parts = PurePosixPath(name.replace("\\", "/")).parts
|
||||
clean = [p for p in parts if p not in ("..", ".", "/")]
|
||||
return str(PurePosixPath(*clean)) if clean else ""
|
||||
+97
-10
@@ -4,6 +4,9 @@ import os
|
||||
import re
|
||||
import struct
|
||||
import ipaddress
|
||||
from pathlib import Path
|
||||
|
||||
from nxc.helpers.path import sanitize_filename
|
||||
from Cryptodome.Hash import MD4
|
||||
from textwrap import dedent
|
||||
|
||||
@@ -35,7 +38,7 @@ from impacket.krb5 import constants
|
||||
from impacket.dcerpc.v5.dtypes import NULL
|
||||
from impacket.dcerpc.v5.dcomrt import DCOMConnection
|
||||
from impacket.dcerpc.v5.dcom.wmi import CLSID_WbemLevel1Login, IID_IWbemLevel1Login, IWbemLevel1Login
|
||||
from impacket.smb3structs import FILE_SHARE_WRITE, FILE_SHARE_DELETE, SMB2_0_IOCTL_IS_FSCTL
|
||||
from impacket.smb3structs import FILE_READ_DATA, FILE_WRITE_DATA, FILE_SHARE_WRITE, FILE_SHARE_DELETE, SMB2_0_IOCTL_IS_FSCTL
|
||||
from impacket.dcerpc.v5 import tsts as TSTS
|
||||
|
||||
from nxc.config import process_secret, host_info_colors, check_guest_account
|
||||
@@ -1937,24 +1940,108 @@ class smb(connection):
|
||||
for src, dest in self.args.put_file:
|
||||
self.put_file_single(src, dest)
|
||||
|
||||
def get_file_single(self, remote_path, download_path):
|
||||
def download_file(self, share_name, remote_path, dest_file, access_mode=FILE_READ_DATA):
|
||||
try:
|
||||
self.logger.debug(f"Getting file from {share_name}:{remote_path} with access mode {access_mode}")
|
||||
self.conn.getFile(share_name, remote_path, dest_file, shareAccessMode=access_mode)
|
||||
return True
|
||||
except SessionError as e:
|
||||
if "STATUS_SHARING_VIOLATION" in str(e):
|
||||
self.logger.debug(f"Sharing violation on {remote_path}: {e}")
|
||||
else:
|
||||
self.logger.debug(f"SessionError when attempting to download file {remote_path}: {e}")
|
||||
return False
|
||||
except Exception as e:
|
||||
self.logger.debug(f"Other error when attempting to download file {remote_path}: {e}")
|
||||
return False
|
||||
|
||||
def get_file_single(self, remote_path, download_path, silent=False):
|
||||
share_name = self.args.share
|
||||
self.logger.display(f'Copying "{remote_path}" to "{download_path}"')
|
||||
if not silent:
|
||||
self.logger.display(f"Copying '{remote_path}' to '{download_path}'")
|
||||
if self.args.append_host:
|
||||
download_path = f"{self.hostname}-{remote_path}"
|
||||
with open(download_path, "wb+") as file:
|
||||
try:
|
||||
self.conn.getFile(share_name, remote_path, file.write)
|
||||
self.logger.success(f'File "{remote_path}" was downloaded to "{download_path}"')
|
||||
except Exception as e:
|
||||
self.logger.fail(f'Error writing file "{remote_path}" from share "{share_name}": {e}')
|
||||
if os.path.getsize(download_path) == 0:
|
||||
os.remove(download_path)
|
||||
if self.download_file(share_name, remote_path, file.write):
|
||||
if not silent:
|
||||
self.logger.success(f"File '{remote_path}' was downloaded to '{download_path}'")
|
||||
else:
|
||||
self.logger.debug("Opening with READ alone failed, trying to open file with READ/WRITE access")
|
||||
if self.download_file(share_name, remote_path, file.write, FILE_READ_DATA | FILE_WRITE_DATA):
|
||||
if not silent:
|
||||
self.logger.success(f"File '{remote_path}' was downloaded to '{download_path}'")
|
||||
else:
|
||||
if not silent:
|
||||
self.logger.fail(f"Error downloading file '{remote_path}' from share '{share_name}'")
|
||||
|
||||
def get_file(self):
|
||||
for src, dest in self.args.get_file:
|
||||
self.get_file_single(src, dest)
|
||||
|
||||
def download_folder(self, folder, dest, recursive=False, silent=False, base_dir=None, ignore_empty=False):
|
||||
self.logger.debug(f"Downloading folder with args: {folder}, {dest}, Recursive: {recursive}, Silent: {silent}, Base dir: {base_dir}, Ignore empty: {ignore_empty}")
|
||||
normalized_folder = ntpath.normpath(folder)
|
||||
base_folder = os.path.basename(normalized_folder)
|
||||
self.logger.debug(f"Base folder: {base_folder}")
|
||||
|
||||
try:
|
||||
items = self.conn.listPath(self.args.share, ntpath.join(folder, "*"))
|
||||
except SessionError as e:
|
||||
self.logger.error(f"Error listing folder '{folder}': {e}")
|
||||
return
|
||||
self.logger.debug(f"{len(items)} items in folder: {items}")
|
||||
|
||||
filtered_items = [item for item in items if item.get_longname() not in [".", ".."]]
|
||||
|
||||
# create local directory structure regardless of content; download empty folders by default
|
||||
# change the Windows path to Linux and then join it with the base directory to get our actual save path
|
||||
relative_path = os.path.join(*folder.replace(base_dir or folder, "").lstrip("\\").split("\\"))
|
||||
local_folder_path = os.path.join(dest, relative_path)
|
||||
|
||||
if not filtered_items and ignore_empty:
|
||||
self.logger.debug(f"Skipping empty folder '{folder}'")
|
||||
return
|
||||
|
||||
# create the directory for this folder
|
||||
os.makedirs(local_folder_path, exist_ok=True)
|
||||
if not filtered_items and not silent:
|
||||
self.logger.display(f"Created empty directory '{local_folder_path}'")
|
||||
|
||||
for item in filtered_items:
|
||||
item_name = sanitize_filename(item.get_longname())
|
||||
if not item_name:
|
||||
self.logger.fail(f"Path traversal detected in '{item.get_longname()}', skipping")
|
||||
continue
|
||||
dir_path = ntpath.normpath(ntpath.join(normalized_folder, item_name))
|
||||
self.logger.debug(f"Parsing item: {item_name}, {dir_path}")
|
||||
|
||||
if item.is_directory() and recursive:
|
||||
self.logger.debug(f"Found new directory to parse: {dir_path}")
|
||||
self.download_folder(dir_path, dest, recursive, silent, base_dir or folder, ignore_empty)
|
||||
elif not item.is_directory():
|
||||
remote_file_path = ntpath.join(folder, item_name)
|
||||
local_file_path = os.path.join(local_folder_path, item_name)
|
||||
# Defense-in-depth: verify path stays under destination
|
||||
resolved = Path(local_file_path).resolve()
|
||||
if not str(resolved).startswith(str(Path(dest).resolve()) + os.sep):
|
||||
self.logger.fail(f"Path traversal detected in '{item_name}', skipping")
|
||||
continue
|
||||
self.logger.debug(f"{dest=} {remote_file_path=} {relative_path=} {local_folder_path=} {local_file_path=}")
|
||||
|
||||
try:
|
||||
self.get_file_single(remote_file_path, local_file_path, silent)
|
||||
except FileNotFoundError:
|
||||
self.logger.fail(f"Error downloading file '{remote_file_path}' due to file not found (probably a race condition between listing and downloading)")
|
||||
|
||||
def get_folder(self):
|
||||
recursive = self.args.recursive
|
||||
ignore_empty = getattr(self.args, "ignore_empty_folders", False)
|
||||
self.logger.debug(f"Recursive option set to {recursive}")
|
||||
self.logger.debug(f"Ignore empty folders option set to {ignore_empty}")
|
||||
for folder, dest in self.args.get_folder:
|
||||
self.download_folder(folder, dest, recursive, False, None, ignore_empty)
|
||||
self.logger.success(f"Folder '{folder}' was downloaded to '{dest}'")
|
||||
|
||||
def enable_remoteops(self, regsecret=False):
|
||||
try:
|
||||
if regsecret:
|
||||
|
||||
@@ -90,6 +90,9 @@ def proto_args(parser, parents):
|
||||
files_group = smb_parser.add_argument_group("File Operations")
|
||||
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")
|
||||
files_group.add_argument("--get-file", action="append", nargs=2, metavar="FILE", help="Get a remote file, ex: \\\\Windows\\\\Temp\\\\whoami.txt whoami.txt")
|
||||
files_group.add_argument("--get-folder", action="append", nargs=2, metavar="DIR", help="Get a remote directory, ex: \\\\Windows\\\\Temp\\\\testing testing")
|
||||
files_group.add_argument("--recursive", default=False, action="store_true", help="Recursively get a folder")
|
||||
files_group.add_argument("--ignore-empty-folders", default=False, action="store_true", help="Ignore empty folders when downloading")
|
||||
files_group.add_argument("--append-host", action="store_true", help="append the host to the get-file filename")
|
||||
|
||||
cmd_exec_group = smb_parser.add_argument_group("Command Execution")
|
||||
|
||||
@@ -43,6 +43,7 @@ netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -x ipconfig
|
||||
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --put-file TEST_NORMAL_FILE \\Windows\\Temp\\test_file.txt
|
||||
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --put-file TEST_NORMAL_FILE \\Windows\\Temp\\test_file.txt --put-file TEST_NORMAL_FILE \\Windows\\Temp\\test_file2.txt
|
||||
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --get-file \\Windows\\Temp\\test_file.txt /tmp/test_file.txt
|
||||
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --get-folder \\Windows\\Temp\\ /tmp/test_folder/
|
||||
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --no-admin-check
|
||||
##### SMB PowerShell
|
||||
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -X ipconfig
|
||||
|
||||
Reference in new issue
Block a user