Merge branch 'develop' into neff-bh-pc

Signed-off-by: Marshall Hallenbeck <Marshall.Hallenbeck@gmail.com>
main
Marshall Hallenbeck 2023-10-29 01:11:51 -04:00 committed by GitHub
commit b60bef6106
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
44 changed files with 555 additions and 373 deletions

View File

@ -1,4 +1,5 @@
name: Lint Python code with ruff
# Caching source: https://gist.github.com/gh640/233a6daf68e9e937115371c0ecd39c61?permalink_comment_id=4529233#gistcomment-4529233
on: [push, pull_request]
@ -11,15 +12,16 @@ jobs:
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: 3.11
- name: Install poetry
run: |
pipx install poetry
poetry --version
poetry env info
- name: Install libraries with dev group
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: 3.11
cache: poetry
cache-dependency-path: poetry.lock
- name: Install dependencies with dev group
run: |
poetry install --with dev
- name: Run ruff

View File

@ -48,6 +48,7 @@ a = Analysis(
'lsassy.parser',
'lsassy.session',
'lsassy.impacketfile',
'bloodhound',
'dns',
'dns.name',
'dns.resolver',

View File

@ -28,34 +28,12 @@ def gen_cli_args():
{highlight('Version', 'red')} : {highlight(VERSION)}
{highlight('Codename', 'red')}: {highlight(CODENAME)}
""",
formatter_class=RawTextHelpFormatter,
)
""", formatter_class=RawTextHelpFormatter)
parser.add_argument(
"-t",
type=int,
dest="threads",
default=100,
help="set how many concurrent threads to use (default: 100)",
)
parser.add_argument(
"--timeout",
default=None,
type=int,
help="max timeout in seconds of each thread (default: None)",
)
parser.add_argument(
"--jitter",
metavar="INTERVAL",
type=str,
help="sets a random delay between each connection (default: None)",
)
parser.add_argument(
"--no-progress",
action="store_true",
help="Not displaying progress bar during scan",
)
parser.add_argument("-t", type=int, dest="threads", default=100, help="set how many concurrent threads to use (default: 100)")
parser.add_argument("--timeout", default=None, type=int, help="max timeout in seconds of each thread (default: None)")
parser.add_argument("--jitter", metavar="INTERVAL", type=str, help="sets a random delay between each connection (default: None)")
parser.add_argument("--no-progress", action="store_true", help="Not displaying progress bar during scan")
parser.add_argument("--verbose", action="store_true", help="enable verbose output")
parser.add_argument("--debug", action="store_true", help="enable debug level information")
parser.add_argument("--version", action="store_true", help="Display nxc version")
@ -64,122 +42,34 @@ def gen_cli_args():
module_parser = argparse.ArgumentParser(add_help=False)
mgroup = module_parser.add_mutually_exclusive_group()
mgroup.add_argument("-M", "--module", action="append", metavar="MODULE", help="module to use")
module_parser.add_argument(
"-o",
metavar="MODULE_OPTION",
nargs="+",
default=[],
dest="module_options",
help="module options",
)
module_parser.add_argument("-o", metavar="MODULE_OPTION", nargs="+", default=[], dest="module_options", help="module options")
module_parser.add_argument("-L", "--list-modules", action="store_true", help="list available modules")
module_parser.add_argument(
"--options",
dest="show_module_options",
action="store_true",
help="display module options",
)
module_parser.add_argument(
"--server",
choices={"http", "https"},
default="https",
help="use the selected server (default: https)",
)
module_parser.add_argument(
"--server-host",
type=str,
default="0.0.0.0",
metavar="HOST",
help="IP to bind the server to (default: 0.0.0.0)",
)
module_parser.add_argument(
"--server-port",
metavar="PORT",
type=int,
help="start the server on the specified port",
)
module_parser.add_argument(
"--connectback-host",
type=str,
metavar="CHOST",
help="IP for the remote system to connect back to (default: same as server-host)",
)
module_parser.add_argument("--options", dest="show_module_options", action="store_true", help="display module options")
module_parser.add_argument("--server", choices={"http", "https"}, default="https", help="use the selected server (default: https)")
module_parser.add_argument("--server-host", type=str, default="0.0.0.0", metavar="HOST", help="IP to bind the server to (default: 0.0.0.0)")
module_parser.add_argument("--server-port", metavar="PORT", type=int, help="start the server on the specified port")
module_parser.add_argument("--connectback-host", type=str, metavar="CHOST", help="IP for the remote system to connect back to (default: same as server-host)")
subparsers = parser.add_subparsers(title="protocols", dest="protocol", description="available protocols")
std_parser = argparse.ArgumentParser(add_help=False)
std_parser.add_argument(
"target",
nargs="+" if not (module_parser.parse_known_args()[0].list_modules or module_parser.parse_known_args()[0].show_module_options) else "*",
type=str,
help="the target IP(s), range(s), CIDR(s), hostname(s), FQDN(s), file(s) containing a list of targets, NMap XML or .Nessus file(s)",
)
std_parser.add_argument(
"-id",
metavar="CRED_ID",
nargs="+",
default=[],
type=str,
dest="cred_id",
help="database credential ID(s) to use for authentication",
)
std_parser.add_argument(
"-u",
metavar="USERNAME",
dest="username",
nargs="+",
default=[],
help="username(s) or file(s) containing usernames",
)
std_parser.add_argument(
"-p",
metavar="PASSWORD",
dest="password",
nargs="+",
default=[],
help="password(s) or file(s) containing passwords",
)
std_parser.add_argument("target", nargs="+" if not (module_parser.parse_known_args()[0].list_modules or module_parser.parse_known_args()[0].show_module_options) else "*", type=str, help="the target IP(s), range(s), CIDR(s), hostname(s), FQDN(s), file(s) containing a list of targets, NMap XML or .Nessus file(s)")
std_parser.add_argument("-id", metavar="CRED_ID", nargs="+", default=[], type=str, dest="cred_id", help="database credential ID(s) to use for authentication")
std_parser.add_argument("-u", metavar="USERNAME", dest="username", nargs="+", default=[], help="username(s) or file(s) containing usernames")
std_parser.add_argument("-p", metavar="PASSWORD", dest="password", nargs="+", default=[], help="password(s) or file(s) containing passwords")
std_parser.add_argument("--ignore-pw-decoding", action="store_true", help="Ignore non UTF-8 characters when decoding the password file")
std_parser.add_argument("-k", "--kerberos", action="store_true", help="Use Kerberos authentication")
std_parser.add_argument("--no-bruteforce", action="store_true", help="No spray when using file for username and password (user1 => password1, user2 => password2")
std_parser.add_argument("--continue-on-success", action="store_true", help="continues authentication attempts even after successes")
std_parser.add_argument(
"--use-kcache",
action="store_true",
help="Use Kerberos authentication from ccache file (KRB5CCNAME)",
)
std_parser.add_argument("--use-kcache", action="store_true", help="Use Kerberos authentication from ccache file (KRB5CCNAME)")
std_parser.add_argument("--log", metavar="LOG", help="Export result into a custom file")
std_parser.add_argument(
"--aesKey",
metavar="AESKEY",
nargs="+",
help="AES key to use for Kerberos Authentication (128 or 256 bits)",
)
std_parser.add_argument(
"--kdcHost",
metavar="KDCHOST",
help="FQDN of the domain controller. If omitted it will use the domain part (FQDN) specified in the target parameter",
)
std_parser.add_argument("--aesKey", metavar="AESKEY", nargs="+", help="AES key to use for Kerberos Authentication (128 or 256 bits)")
std_parser.add_argument("--kdcHost", metavar="KDCHOST", help="FQDN of the domain controller. If omitted it will use the domain part (FQDN) specified in the target parameter")
fail_group = std_parser.add_mutually_exclusive_group()
fail_group.add_argument(
"--gfail-limit",
metavar="LIMIT",
type=int,
help="max number of global failed login attempts",
)
fail_group.add_argument(
"--ufail-limit",
metavar="LIMIT",
type=int,
help="max number of failed login attempts per username",
)
fail_group.add_argument(
"--fail-limit",
metavar="LIMIT",
type=int,
help="max number of failed login attempts per host",
)
fail_group.add_argument("--gfail-limit", metavar="LIMIT", type=int, help="max number of global failed login attempts")
fail_group.add_argument("--ufail-limit", metavar="LIMIT", type=int, help="max number of failed login attempts per username")
fail_group.add_argument("--fail-limit", metavar="LIMIT", type=int, help="max number of failed login attempts per host")
p_loader = ProtocolLoader()
protocols = p_loader.get_protocols()

View File

@ -393,12 +393,16 @@ class connection:
with sem:
if cred_type == "plaintext":
if self.args.kerberos:
self.logger.debug("Trying to authenticate using Kerberos")
return self.kerberos_login(domain, username, secret, "", "", self.kdcHost, False)
elif hasattr(self.args, "domain"): # Some protocolls don't use domain for login
elif hasattr(self.args, "domain"): # Some protocols don't use domain for login
self.logger.debug("Trying to authenticate using plaintext with domain")
return self.plaintext_login(domain, username, secret)
elif self.args.protocol == "ssh":
self.logger.debug("Trying to authenticate using plaintext over SSH")
return self.plaintext_login(username, secret, data)
else:
self.logger.debug("Trying to authenticate using plaintext")
return self.plaintext_login(username, secret)
elif cred_type == "hash":
if self.args.kerberos:
@ -406,7 +410,6 @@ class connection:
return self.hash_login(domain, username, secret)
elif cred_type == "aesKey":
return self.kerberos_login(domain, username, "", "", secret, self.kdcHost, False)
return None
def login(self):
"""Try to login using the credentials specified in the command line or in the database.
@ -441,6 +444,7 @@ class connection:
data.extend(parsed_data)
if self.args.use_kcache:
self.logger.debug("Trying to authenticate using Kerberos cache")
with sem:
username = self.args.username[0] if len(self.args.username) else ""
password = self.args.password[0] if len(self.args.password) else ""
@ -455,7 +459,6 @@ class connection:
owned[user_index] = True
if not self.args.continue_on_success:
return True
return None
else:
if len(username) != len(secret):
self.logger.error("Number provided of usernames and passwords/hashes do not match!")
@ -465,7 +468,6 @@ class connection:
owned[user_index] = True
if not self.args.continue_on_success:
return True
return None
def mark_pwned(self):
return highlight(f"({pwned_label})" if self.admin_privs else "")

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -19,4 +19,3 @@ def get_desktop_uagent(uagent=None):
return desktop_uagents[random.choice(desktop_uagents.keys())]
elif uagent:
return desktop_uagents[uagent]
return None

View File

@ -13,4 +13,3 @@ def highlight(text, color="yellow"):
return f"{colored(text, 'yellow', attrs=['bold'])}"
elif color == "red":
return f"{colored(text, 'red', attrs=['bold'])}"
return None

View File

@ -77,4 +77,3 @@ def which(cmd, mode=os.F_OK | os.X_OK, path=None):
name = os.path.join(p, thefile)
if _access_check(name, mode):
return name
return None

View File

@ -58,7 +58,6 @@ class ModuleLoader:
except Exception as e:
self.logger.fail(f"Failed loading module at {module_path}: {e}")
self.logger.debug(traceback.format_exc())
return None
def init_module(self, module_path):
"""Initialize a module for execution"""
@ -85,7 +84,6 @@ class ModuleLoader:
else:
self.logger.fail(f"Module {module.name.upper()} is not supported for protocol {self.args.protocol}")
sys.exit(1)
return None
def get_module_info(self, module_path):
"""Get the path, description, and options from a module"""
@ -101,6 +99,7 @@ class ModuleLoader:
"supported_protocols": module_spec.supported_protocols,
"opsec_safe": module_spec.opsec_safe,
"multiple_hosts": module_spec.multiple_hosts,
"requires_admin": bool(hasattr(module_spec, "on_admin_login") and callable(module_spec.on_admin_login)),
}
}
if self.module_is_sane(module_spec, module_path):
@ -108,7 +107,6 @@ class ModuleLoader:
except Exception as e:
self.logger.fail(f"Failed loading module at {module_path}: {e}")
self.logger.debug(traceback.format_exc())
return None
def list_modules(self):
"""List modules without initializing them"""

View File

@ -76,7 +76,5 @@ class NXCModule:
except socket.gaierror:
context.log.debug("Missing IP")
context.log.highlight(f"{answer[0]} ({answer[1]}) (No IP Found)")
return None
else:
context.log.success(f"Unable to find any computers with the text {self.TEXT}")
return None

View File

@ -78,8 +78,6 @@ class NXCModule:
context.log.success("Found following users: ")
for answer in answers:
context.log.highlight(f"User: {answer[0]} description: {answer[1]}")
return None
return None
def filter_answer(self, context, answers):
# No option to filter
@ -103,7 +101,7 @@ class NXCModule:
conditionPasswordPolicy = False
if self.regex.search(description):
conditionPasswordPolicy = True
if (conditionFilter == self.FILTER) and (conditionPasswordPolicy == self.PASSWORDPOLICY):
answersFiltered.append([answer[0], description])
return answersFiltered

View File

@ -62,8 +62,6 @@ class NXCModule:
context.log.success("Found the following members of the " + self.GROUP + " group:")
for answer in self.answers:
context.log.highlight(f"{answer[0]}")
return None
return None
# Carry out an LDAP search for the Group with the supplied Group name

View File

@ -84,5 +84,3 @@ class NXCModule:
group_name = group_parts[0].split("=")[1]
context.log.highlight(f"{group_name}")
return None
return None

View File

@ -231,7 +231,6 @@ class NXCModule:
self.save_credentials(context, connection, cred["domain"], cred["username"], cred["password"], cred["lmhash"], cred["nthash"])
global credentials_data
credentials_data = credentials_output
return None
def spider_pcs(self, context, connection, cursor, dbconnection, driver):
cursor.execute("SELECT * from admin_users WHERE hash is not NULL")

File diff suppressed because one or more lines are too long

View File

@ -48,9 +48,7 @@ class NXCModule:
keepass_process_id = row[0]
keepass_process_username = row[1]
keepass_process_name = row[2]
context.log.highlight(
f'Found process "{keepass_process_name}" with PID {keepass_process_id} (user {keepass_process_username})'
)
context.log.highlight(f'Found process "{keepass_process_name}" with PID {keepass_process_id} (user {keepass_process_username})')
if row_number == 0:
context.log.display("No KeePass-related process was found")

View File

@ -51,7 +51,6 @@ class NXCModule:
# LDAPS bind successful
# because channel binding is not enforced
return False
return None
# Conduct a bind to LDAPS with channel binding supported
# but intentionally miscalculated. In the case that and
@ -74,10 +73,8 @@ class NXCModule:
return False
elif err is not None:
context.log.fail("ERROR while connecting to " + str(connection.domain) + ": " + str(err))
return None
elif err is None:
return False
return None
# Domain Controllers do not have a certificate setup for
# LDAPS on port 636 by default. If this has not been setup,
@ -128,10 +125,8 @@ class NXCModule:
sys.exit()
elif err is None:
return False
return None
else:
context.log.fail(str(err))
return None
# Run trough all our code blocks to determine LDAP signing and channel binding settings.
stype = asyauthSecret.PASS if not connection.nthash else asyauthSecret.NT

View File

@ -117,7 +117,6 @@ class NXCModule:
context.log.debug("Calling process_credentials")
self.process_credentials(context, connection, credentials_output)
return None
def process_credentials(self, context, connection, credentials):
if len(credentials) == 0:

View File

@ -139,7 +139,6 @@ class NXCModule:
else:
self.context.log.display(f"{user.username} can impersonate: {grantor.username}")
return self.browse_path(context, initial_user, grantor)
return None
def query_and_get_output(self, query):
return self.mssql_conn.sql_query(query)
@ -354,8 +353,6 @@ class NXCModule:
if db in trusted_databases:
return db
return None
def do_dbowner_privesc(self, database, exec_as=""):
"""
Executes a series of SQL queries to perform a database owner privilege escalation.

File diff suppressed because one or more lines are too long

View File

@ -88,8 +88,5 @@ class NXCModule:
value = self.convert_time_field(field, pso[field])
context.log.highlight(f"{field}: {value}")
context.log.highlight("-----")
return None
else:
context.log.info("No Password Settings Objects (PSO) found.")
return None

View File

@ -137,8 +137,6 @@ class SMBSpiderPlus:
if self.reconnect():
return self.get_remote_file(share, path)
return None
def read_chunk(self, remote_file, chunk_size=CHUNK_SIZE):
"""Reads the next chunk of data from the provided remote file using the specified chunk size.
If a `SessionError` is encountered, it retries up to 3 times by reconnecting the SMB connection.

File diff suppressed because one or more lines are too long

View File

@ -37,6 +37,7 @@ if platform != "win32":
resource.setrlimit(resource.RLIMIT_NOFILE, file_limit)
def create_db_engine(db_path):
return sqlalchemy.create_engine(f"sqlite:///{db_path}", isolation_level="AUTOCOMMIT", future=True)
@ -165,8 +166,13 @@ def main():
modules = loader.list_modules()
if args.list_modules:
nxc_logger.highlight("LOW PRIVILEGE MODULES")
for name, props in sorted(modules.items()):
if args.protocol in props["supported_protocols"]:
if args.protocol in props["supported_protocols"] and not props["requires_admin"]:
nxc_logger.display(f"{name:<25} {props['description']}")
nxc_logger.highlight("\nHIGH PRIVILEGE MODULES (requires admin privs)")
for name, props in sorted(modules.items()):
if args.protocol in props["supported_protocols"] and props["requires_admin"]:
nxc_logger.display(f"{name:<25} {props['description']}")
exit(0)
elif args.module and args.show_module_options:

View File

@ -1,9 +1,9 @@
import os
from nxc.config import process_secret
from nxc.connection import connection
from nxc.helpers.logger import highlight
from nxc.logger import NXCAdapter
from ftplib import FTP
from ftplib import FTP, error_perm
class ftp(connection):
def __init__(self, args, db, host):
@ -69,30 +69,102 @@ class ftp(connection):
host_id = self.db.get_hosts(self.host)[0].id
self.db.add_loggedin_relation(cred_id, host_id)
if username in ["anonymous", ""] and password in ["", "-"]:
if username in ["anonymous", ""]:
self.logger.success(f"{username}:{process_secret(password)} {highlight('- Anonymous Login!')}")
else:
self.logger.success(f"{username}:{process_secret(password)}")
if self.args.ls:
files = self.list_directory_full()
self.logger.display("Directory Listing")
for file in files:
self.logger.highlight(file)
# If the default directory is specified, then we will list the current directory
if self.args.ls == ".":
files = self.list_directory_full()
# If files is false, then we encountered an exception
if not files:
return False
# If there are files, then we can list the files
self.logger.display("Directory Listing")
for file in files:
self.logger.highlight(file)
else:
# If the default directory is not specified, then we will list the specified directory
self.logger.display(f"Directory Listing for {self.args.ls}")
# Change to the specified directory
try:
self.conn.cwd(self.args.ls)
except error_perm as error_message:
self.logger.fail(f"Failed to change directory. Response: ({error_message})")
self.conn.close()
return False
# List the files in the specified directory
files = self.list_directory_full()
for file in files:
self.logger.highlight(file)
if self.args.get:
self.get_file(f"{self.args.get}")
if self.args.put:
self.put_file(self.args.put[0], self.args.put[1])
if not self.args.continue_on_success:
self.conn.close()
return True
self.conn.close()
return None
def list_directory_full(self):
# in the future we can use mlsd/nlst if we want, but this gives a full output like `ls -la`
# ftplib's "dir" prints directly to stdout, and "nlst" only returns the folder name, not full details
files = []
self.conn.retrlines("LIST", callback=files.append)
try:
self.conn.retrlines("LIST", callback=files.append)
except error_perm as error_message:
self.logger.fail(f"Failed to list directory. Response: ({error_message})")
self.conn.close()
return False
return files
def get_file(self, filename):
# Extract the filename from the path
downloaded_file = filename.split("/")[-1]
try:
# Check if the current connection is ASCII (ASCII does not support .size())
if self.conn.encoding == "utf-8":
# Switch the connection to binary
self.conn.sendcmd("TYPE I")
# Check if the file exists
self.conn.size(filename)
# Attempt to download the file
self.conn.retrbinary(f"RETR {filename}", open(downloaded_file, "wb").write) # noqa: SIM115
except error_perm as error_message:
self.logger.fail(f"Failed to download the file. Response: ({error_message})")
self.conn.close()
return False
except FileNotFoundError:
self.logger.fail("Failed to download the file. Response: (No such file or directory.)")
self.conn.close()
return False
# Check if the file was downloaded
if os.path.isfile(downloaded_file):
self.logger.success(f"Downloaded: {filename}")
else:
self.logger.fail(f"Failed to download: {filename}")
def put_file(self, local_file, remote_file):
try:
# Attempt to upload the file
self.conn.storbinary(f"STOR {remote_file}", open(local_file, "rb")) # noqa: SIM115
except error_perm as error_message:
self.logger.fail(f"Failed to upload file. Response: ({error_message})")
return False
except FileNotFoundError:
self.logger.fail(f"Failed to upload file. {local_file} does not exist locally.")
return False
# Check if the file was uploaded
if self.conn.size(remote_file) > 0:
self.logger.success(f"Uploaded: {local_file} to {remote_file}")
else:
self.logger.fail(f"Failed to upload: {local_file} to {remote_file}")
def supported_commands(self):
raw_supported_commands = self.conn.sendcmd("HELP")
supported_commands = [item for sublist in (x.split() for x in raw_supported_commands.split("\n")[1:-1]) for item in sublist]

View File

@ -138,7 +138,6 @@ class database:
if updated_ids:
nxc_logger.debug(f"add_host() - Host IDs Updated: {updated_ids}")
return updated_ids
return None
def add_credential(self, username, password):
"""Check if this credential has already been added to the database, if not add it in."""
@ -205,9 +204,7 @@ class database:
self.CredentialsTable.c.password == password,
)
results = self.sess.execute(q).first()
if results is None:
return None
else:
if results is not None:
return results.id
def get_credentials(self, filter_term=None):
@ -291,7 +288,6 @@ class database:
return inserted_id_results[0].id
except Exception as e:
nxc_logger.debug(f"Error inserting LoggedinRelation: {e}")
return None
def get_loggedin_relations(self, cred_id=None, host_id=None):
q = select(self.LoggedinRelationsTable) # .returning(self.LoggedinRelationsTable.c.id)

View File

@ -3,5 +3,7 @@ def proto_args(parser, std_parser, module_parser):
ftp_parser.add_argument("--port", type=int, default=21, help="FTP port (default: 21)")
cgroup = ftp_parser.add_argument_group("FTP Access", "Options for enumerating your access")
cgroup.add_argument("--ls", action="store_true", help="List files in the directory")
cgroup.add_argument("--ls", metavar="DIRECTORY", nargs="?", const=".", help="List files in the directory, ex: --ls or --ls Directory")
cgroup.add_argument("--get", metavar="FILE", help="Download a file, ex: --get fileName.txt")
cgroup.add_argument("--put", metavar=("LOCAL_FILE", "REMOTE_FILE"), nargs=2, help="Upload a file, ex: --put inputFileName.txt outputFileName.txt")
return parser

View File

@ -846,7 +846,6 @@ class ldap(connection):
resp = self.search(search_filter, attributes, 0)
if resp == []:
self.logger.highlight("No entries found!")
return None
elif resp:
answers = []
self.logger.display(f"Total of records returned {len(resp):d}")
@ -896,10 +895,8 @@ class ldap(connection):
return True
else:
self.logger.highlight("No entries found!")
return None
else:
self.logger.fail("Error with the LDAP account used")
return None
def kerberoasting(self):
# Building the search filter
@ -996,9 +993,7 @@ class ldap(connection):
return True
else:
self.logger.highlight("No entries found!")
return None
self.logger.fail("Error with the LDAP account used")
return None
def trusted_for_delegation(self):
# Building the search filter
@ -1129,7 +1124,6 @@ class ldap(connection):
self.logger.highlight(f"User: {value[0]} Status: {value[5]}")
else:
self.logger.fail("No entries found!")
return None
def admin_count(self):
# Building the search filter

View File

@ -243,4 +243,9 @@ class KerberosAttacks:
return None
# Let's output the TGT enc-part/cipher in Hashcat format, in case somebody wants to use it.
return "$krb5asrep$%d$%s@%s:%s$%s" % (as_rep["enc-part"]["etype"], client_name, domain, hexlify(as_rep["enc-part"]["cipher"].asOctets()[:12]).decode(), hexlify(as_rep["enc-part"]["cipher"].asOctets()[12:]).decode()) if as_rep["enc-part"]["etype"] == 17 or as_rep["enc-part"]["etype"] == 18 else "$krb5asrep$%d$%s@%s:%s$%s" % (as_rep["enc-part"]["etype"], client_name, domain, hexlify(as_rep["enc-part"]["cipher"].asOctets()[:16]).decode(), hexlify(as_rep["enc-part"]["cipher"].asOctets()[16:]).decode())
hash_tgt = f"$krb5asrep${as_rep['enc-part']['etype']}${client_name}@{domain}:"
if as_rep["enc-part"]["etype"] in (17, 18):
hash_tgt += f"{hexlify(as_rep['enc-part']['cipher'].asOctets()[:12]).decode()}${hexlify(as_rep['enc-part']['cipher'].asOctets()[12:]).decode()}"
else:
hash_tgt += f"{hexlify(as_rep['enc-part']['cipher'].asOctets()[:16]).decode()}${hexlify(as_rep['enc-part']['cipher'].asOctets()[16:]).decode()}"
return hash_tgt

View File

@ -651,7 +651,22 @@ class smb(connection):
current_method = method
if method == "wmiexec":
try:
exec_method = WMIEXEC(self.host if not self.kerberos else self.hostname + "." + self.domain, self.smb_share_name, self.username, self.password, self.domain, self.conn, self.kerberos, self.aesKey, self.kdcHost, self.hash, self.args.share, logger=self.logger, timeout=self.args.dcom_timeout, tries=self.args.get_output_tries)
exec_method = WMIEXEC(
self.host if not self.kerberos else self.hostname + "." + self.domain,
self.smb_share_name,
self.username,
self.password,
self.domain,
self.conn,
self.kerberos,
self.aesKey,
self.kdcHost,
self.hash,
self.args.share,
logger=self.logger,
timeout=self.args.dcom_timeout,
tries=self.args.get_output_tries
)
self.logger.info("Executed command via wmiexec")
break
except Exception:
@ -660,7 +675,19 @@ class smb(connection):
continue
elif method == "mmcexec":
try:
exec_method = MMCEXEC(self.host if not self.kerberos else self.hostname + "." + self.domain, self.smb_share_name, self.username, self.password, self.domain, self.conn, self.args.share, self.hash, self.logger, self.args.get_output_tries, self.args.dcom_timeout)
exec_method = MMCEXEC(
self.host if not self.kerberos else self.hostname + "." + self.domain,
self.smb_share_name,
self.username,
self.password,
self.domain,
self.conn,
self.args.share,
self.hash,
self.logger,
self.args.get_output_tries,
self.args.dcom_timeout
)
self.logger.info("Executed command via mmcexec")
break
except Exception:
@ -669,7 +696,20 @@ class smb(connection):
continue
elif method == "atexec":
try:
exec_method = TSCH_EXEC(self.host if not self.kerberos else self.hostname + "." + self.domain, self.smb_share_name, self.username, self.password, self.domain, self.kerberos, self.aesKey, self.kdcHost, self.hash, self.logger, self.args.get_output_tries, self.args.share)
exec_method = TSCH_EXEC(
self.host if not self.kerberos else self.hostname + "." + self.domain,
self.smb_share_name,
self.username,
self.password,
self.domain,
self.kerberos,
self.aesKey,
self.kdcHost,
self.hash,
self.logger,
self.args.get_output_tries,
self.args.share
)
self.logger.info("Executed command via atexec")
break
except Exception:
@ -678,7 +718,23 @@ class smb(connection):
continue
elif method == "smbexec":
try:
exec_method = SMBEXEC(self.host if not self.kerberos else self.hostname + "." + self.domain, self.smb_share_name, self.conn, self.args.port, self.username, self.password, self.domain, self.kerberos, self.aesKey, self.kdcHost, self.hash, self.args.share, self.args.port, self.logger, self.args.get_output_tries)
exec_method = SMBEXEC(
self.host if not self.kerberos else self.hostname + "." + self.domain,
self.smb_share_name,
self.conn,
self.args.port,
self.username,
self.password,
self.domain,
self.kerberos,
self.aesKey,
self.kdcHost,
self.hash,
self.args.share,
self.args.port,
self.logger,
self.args.get_output_tries
)
self.logger.info("Executed command via smbexec")
break
except Exception:
@ -1560,7 +1616,6 @@ class smb(connection):
credential.password,
credential.url,
)
return None
@requires_admin
def lsa(self):

View File

@ -289,7 +289,6 @@ class database:
if updated_ids:
nxc_logger.debug(f"add_host() - Host IDs Updated: {updated_ids}")
return updated_ids
return None
def add_credential(self, credtype, domain, username, password, group_id=None, pillaged_from=None):
"""Check if this credential has already been added to the database, if not add it in."""
@ -452,9 +451,7 @@ class database:
if user_domain:
q = select(self.HostsTable).filter(func.lower(self.HostsTable.c.id) == func.lower(user_domain))
results = self.conn.execute(q).all()
return len(results) > 0
return None
def is_host_valid(self, host_id):
"""Check if this host ID is valid."""
@ -826,7 +823,6 @@ class database:
return inserted_id_results[0].id
except Exception as e:
nxc_logger.debug(f"Error inserting LoggedinRelation: {e}")
return None
def get_loggedin_relations(self, user_id=None, host_id=None):
q = select(self.LoggedinRelationsTable) # .returning(self.LoggedinRelationsTable.c.id)
@ -897,7 +893,6 @@ class database:
if updated_ids:
nxc_logger.debug(f"add_check() - Checks IDs Updated: {updated_ids}")
return updated_ids
return None
def add_check_result(self, host_id, check_id, secure, reasons):
"""Check if this check result has already been added to the database, if not, add it in."""
@ -910,4 +905,3 @@ class database:
if updated_ids:
nxc_logger.debug(f"add_check_result() - Check Results IDs Updated: {updated_ids}")
return updated_ids
return None

View File

@ -150,7 +150,6 @@ class FirefoxTriage:
fh.close()
return b""
fh.close()
return None
def is_master_password_correct(self, key_data, master_password=b""):
try:
@ -236,9 +235,4 @@ class FirefoxTriage:
# 04 is OCTETSTRING, 0x0e is length == 14
encrypted_value = decoded_item[0][1].asOctets()
cipher = AES.new(key, AES.MODE_CBC, iv)
decrypted = cipher.decrypt(encrypted_value)
if decrypted is not None:
return decrypted
else:
return None
return None
return cipher.decrypt(encrypted_value)

View File

@ -155,7 +155,6 @@ class SAMRQuery:
return resp["ServerHandle"]
else:
nxc_logger.debug("Error creating Samr handle")
return None
def get_domains(self):
"""Calls the hSamrEnumerateDomainsInSamServer() method directly with list comprehension and extracts the "Name" value from each element in the "Buffer" list."""

View File

@ -132,7 +132,6 @@ class SMBEXEC:
pass
self.get_output_remote()
return None
def get_output_remote(self):
if self.__retOutput is False:

View File

@ -1,12 +1,12 @@
import paramiko
import re
import uuid
import logging
import time
from io import StringIO
import paramiko
from nxc.config import process_secret
from nxc.connection import connection
from nxc.helpers.logger import highlight
from nxc.connection import connection, highlight
from nxc.logger import NXCAdapter
from paramiko.ssh_exception import (
AuthenticationException,
@ -18,11 +18,30 @@ from paramiko.ssh_exception import (
class ssh(connection):
def __init__(self, args, db, host):
self.protocol = "SSH"
self.remote_version = None
self.server_os = None
self.remote_version = "Unknown SSH Version"
self.server_os_platform = "Linux"
self.user_principal = "root"
super().__init__(args, db, host)
def proto_flow(self):
self.logger.debug("Kicking off proto_flow")
self.proto_logger()
if self.create_conn_obj():
self.enum_host_info()
self.print_host_info()
if self.remote_version == "Unknown SSH Version":
self.conn.close()
return
if self.login():
if hasattr(self.args, "module") and self.args.module:
self.call_modules()
else:
self.call_cmd_args()
self.conn.close()
def proto_logger(self):
logging.getLogger("paramiko").disabled = True
logging.getLogger("paramiko.transport").disabled = True
self.logger = NXCAdapter(
extra={
"protocol": "SSH",
@ -31,28 +50,22 @@ class ssh(connection):
"hostname": self.hostname,
}
)
logging.getLogger("paramiko").setLevel(logging.WARNING)
def print_host_info(self):
self.logger.display(self.remote_version)
self.logger.display(self.remote_version if self.remote_version != "Unknown SSH Version" else f"{self.remote_version}, skipping...")
return True
def enum_host_info(self):
self.remote_version = self.conn._transport.remote_version
if self.conn._transport.remote_version:
self.remote_version = self.conn._transport.remote_version
self.logger.debug(f"Remote version: {self.remote_version}")
self.server_os = ""
if self.args.remote_enum:
stdin, stdout, stderr = self.conn.exec_command("uname -r")
self.server_os = stdout.read().decode("utf-8")
self.logger.debug(f"OS retrieved: {self.server_os}")
self.db.add_host(self.host, self.args.port, self.remote_version, os=self.server_os)
self.db.add_host(self.host, self.args.port, self.remote_version)
def create_conn_obj(self):
self.conn = paramiko.SSHClient()
self.conn.set_missing_host_key_policy(paramiko.AutoAddPolicy())
try:
self.conn.connect(self.host, port=self.args.port)
self.conn.connect(self.host, port=self.args.port, timeout=self.args.ssh_timeout, look_for_keys=False)
except AuthenticationException:
return True
except SSHException:
@ -62,123 +75,222 @@ class ssh(connection):
except OSError:
return False
def client_close(self):
self.conn.close()
def check_if_admin(self):
self.admin_privs = False
if self.args.sudo_check:
self.check_if_admin_sudo()
return
# we could add in another method to check by piping in the password to sudo
# but that might be too much of an opsec concern - maybe add in a flag to do more checks?
stdin, stdout, stderr = self.conn.exec_command("id")
if stdout.read().decode("utf-8").find("uid=0(root)") != -1:
self.logger.info("Determined user is root via `id` command")
self.admin_privs = True
return True
stdin, stdout, stderr = self.conn.exec_command("sudo -ln | grep 'NOPASSWD: ALL'")
if stdout.read().decode("utf-8").find("NOPASSWD: ALL") != -1:
self.logger.info("Determined user is root via `sudo -ln` command")
self.admin_privs = True
return True
return None
self.logger.info("Determined user is root via `id; sudo -ln` command")
_, stdout, _ = self.conn.exec_command("id; sudo -ln 2>&1")
stdout = stdout.read().decode(self.args.codec, errors="ignore")
admin_flag = {
"(root)": [True, None],
"NOPASSWD: ALL": [True, None],
"(ALL : ALL) ALL": [True, None],
"(sudo)": [False, f"Current user: '{self.username}' was in 'sudo' group, please try '--sudo-check' to check if user can run sudo shell"]
}
for keyword in admin_flag:
match = re.findall(re.escape(keyword), stdout)
if match:
self.logger.info(f"User: '{self.username}' matched keyword: {match[0]}")
self.admin_privs = admin_flag[match[0]][0]
if not self.admin_privs:
tips = admin_flag[match[0]][1]
else:
break
if not self.admin_privs and "tips" in locals():
self.logger.display(tips)
return
def check_if_admin_sudo(self):
if not self.password:
self.logger.error("Check admin with sudo does not support using a private key")
return
if self.args.sudo_check_method:
method = self.args.sudo_check_method
self.logger.info(f"Doing sudo check with method: {method}")
if method == "sudo-stdin":
_, stdout, _ = self.conn.exec_command("sudo --help")
stdout = stdout.read().decode(self.args.codec, errors="ignore")
# Read sudo help docs and find "stdin"
if "stdin" in stdout:
shadow_backup = f"/tmp/{uuid.uuid4()}"
# sudo support stdin password
self.conn.exec_command(f"echo {self.password} | sudo -S cp /etc/shadow {shadow_backup} >/dev/null 2>&1 &")
self.conn.exec_command(f"echo {self.password} | sudo -S chmod 777 {shadow_backup} >/dev/null 2>&1 &")
tries = 1
while True:
self.logger.info(f"Checking {shadow_backup} if it existed")
_, _, stderr = self.conn.exec_command(f"ls {shadow_backup}")
if tries >= self.args.get_output_tries:
self.logger.info(f"The file {shadow_backup} does not exist, the pipe may be hanging. Increase the number of tries with the option '--get-output-tries' or change other method with '--sudo-check-method'. If it's still failing, maybe sudo shell does not work with the current user")
break
if stderr.read().decode("utf-8"):
time.sleep(2)
tries += 1
else:
self.logger.info(f"{shadow_backup} existed")
self.admin_privs = True
break
self.logger.info(f"Remove up temporary files {shadow_backup}")
self.conn.exec_command(f"echo {self.password} | sudo -S rm -rf {shadow_backup}")
else:
self.logger.error("Command: 'sudo' not support stdin mode, running command with 'sudo' failed")
return
else:
_, stdout, _ = self.conn.exec_command("mkfifo --help")
stdout = stdout.read().decode(self.args.codec, errors="ignore")
# check if user can execute mkfifo
if "Create named pipes" in stdout:
self.logger.info("Command: 'mkfifo' available")
pipe_stdin = f"/tmp/systemd-{uuid.uuid4()}"
pipe_stdout = f"/tmp/systemd-{uuid.uuid4()}"
shadow_backup = f"/tmp/{uuid.uuid4()}"
self.conn.exec_command(f"mkfifo {pipe_stdin}; tail -f {pipe_stdin} | /bin/sh 2>&1 > {pipe_stdout} >/dev/null 2>&1 &")
# 'script -qc /bin/sh /dev/null' means "upgrade" the shell, like reverse shell from netcat
self.conn.exec_command(f"echo 'script -qc /bin/sh /dev/null' > {pipe_stdin}")
self.conn.exec_command(f"echo 'sudo -s' > {pipe_stdin} && echo '{self.password}' > {pipe_stdin}")
# Sometime the pipe will hanging(only happen with paramiko)
# Can't get "whoami" or "id" result in pipe_stdout, maybe something wrong using pipe with paramiko
# But one thing I can confirm, is the command was executed even can't get result from pipe_stdout
tries = 1
self.logger.info(f"Copy /etc/shadow to {shadow_backup} if pass the sudo auth")
while True:
self.logger.info(f"Checking {shadow_backup} if it existed")
_, _, stderr = self.conn.exec_command(f"ls {shadow_backup}")
if tries >= self.args.get_output_tries:
self.logger.info(f"The file {shadow_backup} does not exist, the pipe may be hanging. Increase the number of tries with the option '--get-output-tries' or change other method with '--sudo-check-method'. If it's still failing, maybe sudo shell does not work with the current user")
break
if stderr.read().decode("utf-8"):
time.sleep(2)
self.conn.exec_command(f"echo 'cp /etc/shadow {shadow_backup} && chmod 777 {shadow_backup}' > {pipe_stdin}")
tries += 1
else:
self.logger.info(f"{shadow_backup} existed")
self.admin_privs = True
break
self.logger.info(f"Remove up temporary files {shadow_backup} {pipe_stdin} {pipe_stdout}")
self.conn.exec_command(f"echo 'rm -rf {shadow_backup}' > {pipe_stdin} && rm -rf {pipe_stdin} {pipe_stdout}")
else:
self.logger.error("Command: 'mkfifo' unavailable, running command with 'sudo' failed")
return
def plaintext_login(self, username, password, private_key=None):
self.username = username
self.password = password
private_key = ""
stdout = None
try:
if self.args.key_file or private_key:
pkey = paramiko.RSAKey.from_private_key(StringIO(private_key)) if private_key else paramiko.RSAKey.from_private_key_file(self.args.key_file)
self.logger.debug("Logging in with key")
self.conn.connect(
self.host,
port=self.args.port,
username=username,
passphrase=password if password != "" else None,
pkey=pkey,
look_for_keys=False,
allow_agent=False,
)
if private_key:
cred_id = self.db.add_credential(
"key",
username,
password if password != "" else "",
key=private_key,
)
else:
if self.args.key_file:
with open(self.args.key_file) as f:
key_data = f.read()
cred_id = self.db.add_credential(
"key",
username,
password if password != "" else "",
key=key_data,
)
else:
self.logger.debug("Logging in with password")
self.conn.connect(
self.host,
port=self.args.port,
username=username,
password=password,
look_for_keys=False,
allow_agent=False,
private_key = f.read()
pkey = paramiko.RSAKey.from_private_key(StringIO(private_key), password)
self.conn._transport.auth_publickey(username, pkey)
cred_id = self.db.add_credential(
"key",
username,
password if password != "" else "",
key=private_key,
)
else:
self.logger.debug(f"Logging {self.host} with username: {self.username}, password: {self.password}")
self.conn._transport.auth_password(username, password, fallback=True)
cred_id = self.db.add_credential("plaintext", username, password)
# Some IOT devices will not raise exception in self.conn._transport.auth_password / self.conn._transport.auth_publickey
_, stdout, _ = self.conn.exec_command("id")
stdout = stdout.read().decode(self.args.codec, errors="ignore")
except Exception as e:
if self.args.key_file:
password = f"{process_secret(password)} (keyfile: {self.args.key_file})"
if "OpenSSH private key file checkints do not match" in str(e):
self.logger.fail(f"{username}:{password} - Could not decrypt key file, wrong password")
else:
self.logger.fail(f"{username}:{password} {e}")
self.conn.close()
return False
else:
shell_access = False
host_id = self.db.get_hosts(self.host)[0].id
if self.check_if_admin():
shell_access = True
self.logger.debug(f"User {username} logged in successfully and is root!")
if self.args.key_file:
self.db.add_admin_user("key", username, password, host_id=host_id, cred_id=cred_id)
if not stdout:
_, stdout, _ = self.conn.exec_command("whoami /priv")
stdout = stdout.read().decode(self.args.codec, errors="ignore")
self.server_os_platform = "Windows"
self.user_principal = "admin"
if "SeDebugPrivilege" in stdout:
self.admin_privs = True
elif "SeUndockPrivilege" in stdout:
self.admin_privs = True
self.user_principal = "admin (UAC)"
else:
self.db.add_admin_user(
"plaintext",
username,
password,
host_id=host_id,
cred_id=cred_id,
)
# non admin (low priv)
self.user_principal = "admin (low priv)"
if not stdout:
self.logger.debug(f"User: {self.username} can't get a basic shell")
self.server_os_platform = "Network Devices"
shell_access = False
else:
stdin, stdout, stderr = self.conn.exec_command("id")
output = stdout.read().decode("utf-8")
if not output:
self.logger.debug("User cannot get a shell")
shell_access = False
else:
shell_access = True
shell_access = True
self.db.add_loggedin_relation(cred_id, host_id, shell=shell_access)
if shell_access and self.server_os_platform == "Linux":
self.check_if_admin()
if self.admin_privs:
self.logger.debug(f"User {username} logged in successfully and is root!")
if self.args.key_file:
self.db.add_admin_user("key", username, password, host_id=host_id, cred_id=cred_id)
else:
self.db.add_admin_user(
"plaintext",
username,
password,
host_id=host_id,
cred_id=cred_id,
)
if self.args.key_file:
password = f"{password} (keyfile: {self.args.key_file})"
password = f"{process_secret(password)} (keyfile: {self.args.key_file})"
display_shell_access = " - shell access!" if shell_access else ""
display_shell_access = "{} {} {}".format(
f"({self.user_principal})" if self.admin_privs else f"(non {self.user_principal})",
self.server_os_platform,
"- Shell access!" if shell_access else ""
)
self.logger.success(f"{username}:{password} {self.mark_pwned()} {highlight(display_shell_access)}")
self.logger.success(f"{username}:{process_secret(password)} {self.mark_pwned()}{highlight(display_shell_access)}")
return True
except (
AuthenticationException,
NoValidConnectionsError,
ConnectionResetError,
) as e:
self.logger.fail(f"{username}:{process_secret(password)} {e}")
self.client_close()
return False
except Exception as e:
self.logger.exception(e)
self.client_close()
return False
def execute(self, payload=None, output=False):
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
try:
command = payload if payload is not None else self.args.execute
stdin, stdout, stderr = self.conn.exec_command(command)
except AttributeError:
return ""
if output:
_, stdout, _ = self.conn.exec_command(f"{payload} 2>&1")
stdout = stdout.read().decode(self.args.codec, errors="ignore")
except Exception as e:
self.logger.fail(f"Execute command failed, error: {e!s}")
return False
else:
self.logger.success("Executed command")
for line in stdout:
self.logger.highlight(line.strip())
if get_output:
for line in stdout.split("\n"):
self.logger.highlight(line.strip("\n"))
return stdout
return None

View File

@ -167,7 +167,6 @@ class database:
if updated_ids:
nxc_logger.debug(f"add_host() - Host IDs Updated: {updated_ids}")
return updated_ids
return None
def add_credential(self, credtype, username, password, key=None):
"""Check if this credential has already been added to the database, if not add it in."""
@ -267,7 +266,14 @@ class database:
add_links = []
creds_q = select(self.CredentialsTable)
creds_q = creds_q.filter(self.CredentialsTable.c.id == cred_id) if cred_id else creds_q.filter(func.lower(self.CredentialsTable.c.credtype) == func.lower(credtype), func.lower(self.CredentialsTable.c.username) == func.lower(username), self.CredentialsTable.c.password == secret)
if cred_id: # noqa: SIM108
creds_q = creds_q.filter(self.CredentialsTable.c.id == cred_id)
else:
creds_q = creds_q.filter(
func.lower(self.CredentialsTable.c.credtype) == func.lower(credtype),
func.lower(self.CredentialsTable.c.username) == func.lower(username),
self.CredentialsTable.c.password == secret,
)
creds = self.sess.execute(creds_q)
hosts = self.get_hosts(host_id)
@ -343,9 +349,7 @@ class database:
self.CredentialsTable.c.credtype == cred_type,
)
results = self.sess.execute(q).first()
if results is None:
return None
else:
if results is not None:
return results.id
def is_host_valid(self, host_id):
@ -414,7 +418,6 @@ class database:
return inserted_id_results[0].id
except Exception as e:
nxc_logger.debug(f"Error inserting LoggedinRelation: {e}")
return None
def get_loggedin_relations(self, cred_id=None, host_id=None, shell=None):
q = select(self.LoggedinRelationsTable) # .returning(self.LoggedinRelationsTable.c.id)

View File

@ -1,11 +1,37 @@
from argparse import _StoreAction
def proto_args(parser, std_parser, module_parser):
ssh_parser = parser.add_parser("ssh", help="own stuff using SSH", parents=[std_parser, module_parser])
ssh_parser.add_argument("--key-file", type=str, help="Authenticate using the specified private key. Treats the password parameter as the key's passphrase.")
ssh_parser.add_argument("--port", type=int, default=22, help="SSH port (default: 22)")
ssh_parser.add_argument("--ssh-timeout", help="SSH connection timeout, default is %(default)s secondes", type=int, default=15)
sudo_check_arg = ssh_parser.add_argument("--sudo-check", action="store_true", help="Check user privilege with sudo")
sudo_check_method_arg = ssh_parser.add_argument("--sudo-check-method", action=get_conditional_action(_StoreAction), make_required=[], choices={"sudo-stdin", "mkfifo"}, default="sudo-stdin", help="method to do with sudo check, default is '%(default)s (mkfifo is non-stable, probably you need to execute once again if it failed)'")
ssh_parser.add_argument("--get-output-tries", help="Number of times with sudo command tries to get results, default is %(default)s", type=int, default=5)
sudo_check_method_arg.make_required.append(sudo_check_arg)
cgroup = ssh_parser.add_argument_group("Command Execution", "Options for executing commands")
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("--no-output", action="store_true", help="do not retrieve command output")
cgroup.add_argument("-x", metavar="COMMAND", dest="execute", help="execute the specified command")
cgroup.add_argument("--remote-enum", action="store_true", help="executes remote commands for enumeration")
return parser
def get_conditional_action(baseAction):
class ConditionalAction(baseAction):
def __init__(self, option_strings, dest, **kwargs):
x = kwargs.pop("make_required", [])
super().__init__(option_strings, dest, **kwargs)
self.make_required = x
def __call__(self, parser, namespace, values, option_string=None):
for x in self.make_required:
x.required = True
super().__call__(parser, namespace, values, option_string)
return ConditionalAction

View File

@ -191,8 +191,8 @@ class winrm(connection):
for url in endpoints:
try:
self.logger.debug(f"Requesting URL: {url}")
res = requests.post(url, verify=False, timeout=self.args.http_timeout) # noqa: F841
self.logger.debug("Received response code: {res.status_code}")
res = requests.post(url, verify=False, timeout=self.args.http_timeout)
self.logger.debug(f"Received response code: {res.status_code}")
self.endpoint = url
if self.endpoint.startswith("https://"):
self.logger.extra["port"] = self.args.port if self.args.port else 5986
@ -251,7 +251,6 @@ class winrm(connection):
def hash_login(self, domain, username, ntlm_hash):
try:
lmhash = "00000000000000000000000000000000:"
nthash = ""
@ -304,21 +303,26 @@ class winrm(connection):
def execute(self, payload=None, get_output=False):
try:
self.logger.debug(f"Connection: {self.conn}, and type: {type(self.conn)}")
r = self.conn.execute_cmd(self.args.execute, encoding=self.args.codec)
except Exception:
self.logger.info("Cannot execute command, probably because user is not local admin, but powershell command should be ok!")
r = self.conn.execute_ps(self.args.execute)
self.logger.success("Executed command")
buf = StringIO(r[0]).readlines()
for line in buf:
self.logger.highlight(line.strip())
self.logger.success("Executed command")
buf = StringIO(r[0]).readlines()
for line in buf:
self.logger.highlight(line.strip())
except Exception as e:
self.logger.debug(f"Error executing command: {e}")
self.logger.fail("Cannot execute command, probably because user is not local admin, but running via powershell (-X) may work")
def ps_execute(self, payload=None, get_output=False):
r = self.conn.execute_ps(self.args.ps_execute)
self.logger.success("Executed command")
buf = StringIO(r[0]).readlines()
for line in buf:
self.logger.highlight(line.strip())
try:
r = self.conn.execute_ps(self.args.ps_execute)
self.logger.success("Executed command")
buf = StringIO(r[0]).readlines()
for line in buf:
self.logger.highlight(line.strip())
except Exception as e:
self.logger.debug(f"Error executing command: {e}")
self.logger.fail("Command execution failed")
def sam(self):
self.conn.execute_cmd("reg save HKLM\SAM C:\\windows\\temp\\SAM && reg save HKLM\SYSTEM C:\\windows\\temp\\SYSTEM")

View File

@ -222,7 +222,15 @@ class database:
add_links = []
creds_q = select(self.UsersTable)
creds_q = creds_q.filter(self.UsersTable.c.id == user_id) if user_id else creds_q.filter(func.lower(self.UsersTable.c.credtype) == func.lower(credtype), func.lower(self.UsersTable.c.domain) == func.lower(domain), func.lower(self.UsersTable.c.username) == func.lower(username), self.UsersTable.c.password == password)
if user_id: # noqa: SIM108
creds_q = creds_q.filter(self.UsersTable.c.id == user_id)
else:
creds_q = creds_q.filter(
func.lower(self.UsersTable.c.credtype) == func.lower(credtype),
func.lower(self.UsersTable.c.domain) == func.lower(domain),
func.lower(self.UsersTable.c.username) == func.lower(username),
self.UsersTable.c.password == password,
)
users = self.conn.execute(creds_q)
hosts = self.get_hosts(host)
@ -299,7 +307,6 @@ class database:
results = self.conn.execute(q).all()
return len(results) > 0
return None
def is_host_valid(self, host_id):
"""Check if this host ID is valid."""

18
poetry.lock generated
View File

@ -1776,6 +1776,16 @@ tqdm = "*"
unicrypto = ">=0.0.10,<=0.1.0"
winacl = ">=0.1.7,<=0.2.0"
[[package]]
name = "pyreadline"
version = "2.1"
description = "A python implmementation of GNU readline."
optional = false
python-versions = "*"
files = [
{file = "pyreadline-2.1.zip", hash = "sha256:4530592fc2e85b25b1a9f79664433da09237c1a270e4d78ea5aa3a2c7229e2d1"},
]
[[package]]
name = "pyrsistent"
version = "0.19.3"
@ -2218,13 +2228,13 @@ widechars = ["wcwidth"]
[[package]]
name = "termcolor"
version = "2.3.0"
version = "2.0.1"
description = "ANSI color formatting for output in terminal"
optional = false
python-versions = ">=3.7"
files = [
{file = "termcolor-2.3.0-py3-none-any.whl", hash = "sha256:3afb05607b89aed0ffe25202399ee0867ad4d3cb4180d98aaf8eefa6a5f7d475"},
{file = "termcolor-2.3.0.tar.gz", hash = "sha256:b5b08f68937f138fe92f6c089b99f1e2da0ae56c52b78bf7075fd95420fd9a5a"},
{file = "termcolor-2.0.1-py3-none-any.whl", hash = "sha256:7e597f9de8e001a3208c4132938597413b9da45382b6f1d150cff8d062b7aaa3"},
{file = "termcolor-2.0.1.tar.gz", hash = "sha256:6b2cf769e93364a2676e1de56a7c0cff2cf5bd07f37e9cc80b0dd6320ebfe388"},
]
[package.extras]
@ -2384,4 +2394,4 @@ testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more
[metadata]
lock-version = "2.0"
python-versions = "^3.7.0"
content-hash = "97e57f05fadf905356205302c5404c4cab8dc3be9649674b54920af3869f9b4a"
content-hash = "1ff7892bac10d1469e83c63d338eeca5964a19f39704651cb71a90e045ebb16b"

View File

@ -37,7 +37,7 @@ python = "^3.7.0"
requests = ">=2.27.1"
beautifulsoup4 = ">=4.11,<5"
lsassy = ">=3.1.8"
termcolor = "^2.3.0"
termcolor = "2.0.1"
msgpack = "^1.0.0"
neo4j = "^4.1.1" # do not upgrade this until performance regression issues in 5 are fixed (as of 9/23)
pylnk3 = "^0.4.2"
@ -63,6 +63,7 @@ rich = "^13.3.5"
python-libnmap = "^0.7.3"
resource = "^0.2.1"
oscrypto = { git = "https://github.com/Pennyw0rth/oscrypto" } # Pypi version currently broken, see: https://github.com/wbond/oscrypto/issues/78 (as of 9/23)
pyreadline = "^2.1" # for the build - impacket imports its hidden from the builder so an error occurs
ruff = "=0.0.292"
[tool.poetry.group.dev.dependencies]
@ -80,7 +81,7 @@ build-backend = "poetry.core.masonry.api"
# Other options: pep8-naming (N), flake8-annotations (ANN), flake8-blind-except (BLE), flake8-commas (COM), flake8-pyi (PYI), flake8-pytest-style (PT), flake8-unused-arguments (ARG), etc
# Should tackle flake8-use-pathlib (PTH) at some point
select = ["E", "F", "D", "UP", "YTT", "ASYNC", "B", "A", "C4", "ISC", "ICN", "PIE", "PT", "Q", "RSE", "RET", "SIM", "TID", "ERA", "FLY", "PERF", "FURB", "LOG", "RUF"]
ignore = [ "E501", "F405", "D100", "D101", "D102", "D103", "D104", "D105", "D106", "D107", "D203", "D204", "D205", "D212", "D213", "D400", "D401", "D415", "D417", "D419", "RET505", "RET506", "RET507", "RET508", "PERF203", "RUF012"]
ignore = [ "E501", "F405", "D100", "D101", "D102", "D103", "D104", "D105", "D106", "D107", "D203", "D204", "D205", "D212", "D213", "D400", "D401", "D415", "D417", "D419", "RET503", "RET505", "RET506", "RET507", "RET508", "PERF203", "RUF012"]
# Allow autofix for all enabled rules (when `--fix`) is provided.
fixable = ["ALL"]

1
tests/data/test_file.txt Normal file
View File

@ -0,0 +1 @@
Test file used to test FTP upload and download

View File

@ -207,15 +207,22 @@ netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M web_de
netexec rdp TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS # need an extra space after this command due to regex
netexec rdp TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --nla-screenshot
##### SSH - Default test passwords and random key; switch these out if you want correct authentication
netexec ssh TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD
netexec ssh TARGET_HOST -u USERNAME -p PASSWORD
netexec ssh TARGET_HOST -u data/test_users.txt -p data/test_passwords.txt --no-bruteforce
netexec ssh TARGET_HOST -u data/test_users.txt -p data/test_passwords.txt --no-bruteforce --continue-on-success
netexec ssh TARGET_HOST -u data/test_users.txt -p data/test_passwords.txt
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 USERNAME -p PASSWORD --key-file data/test_key.priv
netexec ssh TARGET_HOST -u USERNAME -p '' --key-file data/test_key.priv
netexec ssh TARGET_HOST -u USERNAME -p PASSWORD --sudo-check
netexec ssh TARGET_HOST -u USERNAME -p PASSWORD --sudo-check --sudo-check-method sudo-stdin
netexec ssh TARGET_HOST -u USERNAME -p PASSWORD --sudo-check --sudo-check-method sudo-stdin --get-output-tries 10
netexec ssh TARGET_HOST -u USERNAME -p PASSWORD --sudo-check --sudo-check-method mkfifo
netexec ssh TARGET_HOST -u USERNAME -p PASSWORD --sudo-check --sudo-check-method mkfifo --get-output-tries 10
##### 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 USERNAME -p PASSWORD
netexec ftp TARGET_HOST -u USERNAME -p PASSWORD --ls
netexec ftp TARGET_HOST -u USERNAME -p PASSWORD --put data/test_file.txt test_file.txt
netexec ftp TARGET_HOST -u USERNAME -p PASSWORD --get test_file.txt
netexec ftp TARGET_HOST -u data/test_users.txt -p data/test_passwords.txt --no-bruteforce
netexec ftp TARGET_HOST -u data/test_users.txt -p data/test_passwords.txt --no-bruteforce --continue-on-success
netexec ftp TARGET_HOST -u data/test_users.txt -p data/test_passwords.txt
netexec ftp TARGET_HOST -u data/test_users.txt -p data/test_passwords.txt