Files
Pennyw0rth-NetExec/cme/modules/wcc.py
T
2023-05-03 18:20:38 +02:00

601 lines
21 KiB
Python

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from pprint import pprint
from impacket.dcerpc.v5.rpcrt import DCERPCException
from impacket.dcerpc.v5 import rrp, samr, scmr
from impacket.dcerpc.v5.rrp import DCERPCSessionError
from impacket.smbconnection import SessionError as SMBSessionError
#from impacket.dcerpc.v5.dtypes import RPC_UNICODE_STRING, DWORD,
from impacket.examples.secretsdump import RemoteOperations
from impacket.system_errors import *
import operator
import time
OUTDATED_THRESHOLD = 30
class CMEModule:
'''
Windows Configuration Checker
Module by @__fpr
'''
name = 'wcc'
description = 'Check various security configuration items on Windows machines'
supported_protocols = ['smb']
opsec_safe= True #Does the module touch disk?
multiple_hosts = True #Does it make sense to run this module on multiple hosts at a time?
def options(self, context, module_options):
'''
OUTPUT_FORMAT Format for report (Default: 'json')
OUTPUT Path for report (Default: './config_checker.json')
SEP Separator for registry key path components (Default: '\\')
VERBOSE Produce verbose output (Default: False)
'''
self.output = module_options.get('OUTPUT', './config_checker.json')
self.output_format = module_options.get('OUTPUT_FORMAT', 'json')
self.sep = module_options.get('SEP', '\\')
self.verbose = 'VERBOSE' in module_options
self.results = {}
def debug(self, msg, *args):
if self.verbose:
try:
print(f'\x1b[33m{msg}', *args, '\x1b[0m')
except BlockingIOError:
pass
except TypeError:
print('\x1b[33m', repr(msg), *args, '\x1b[0m')
def get_local_users(self, connection):
remoteOps = RemoteOperations(smbConnection=connection.conn, doKerberos=False)
remoteOps.connectSamr(remoteOps.getMachineNameAndDomain()[0])
users = remoteOps.getDomainUsers()
remoteOps.finish()
return dict([(user['RelativeId'], user['Name']) for user in users['Buffer']['Buffer']])
def get_service(self, service_name, connection):
remoteOps = RemoteOperations(smbConnection=connection.conn, doKerberos=False)
machine_name,_ = remoteOps.getMachineNameAndDomain()
remoteOps._RemoteOperations__connectSvcCtl()
dce = remoteOps._RemoteOperations__scmr
scm_handle = scmr.hROpenSCManagerW(dce, machine_name)['lpScHandle']
service_handle = scmr.hROpenServiceW(dce, scm_handle, service_name)['lpServiceHandle']
service_config = scmr.hRQueryServiceConfigW(dce, service_handle)['lpServiceConfig']
service_status = scmr.hRQueryServiceStatus(dce, service_handle)['lpServiceStatus']['dwCurrentState']
remoteOps.finish()
return service_config, service_status
def get_user_info(self, connection, rid=501):
remoteOps = RemoteOperations(smbConnection=connection.conn, doKerberos=False)
machine_name, _ = remoteOps.getMachineNameAndDomain()
remoteOps.connectSamr(machine_name)
dce = remoteOps._RemoteOperations__samr
domain_handle = remoteOps._RemoteOperations__domainHandle
user_handle = samr.hSamrOpenUser(dce, domain_handle, userId=rid)['UserHandle']
user_info = samr.hSamrQueryInformationUser2(dce, user_handle, samr.USER_INFORMATION_CLASS.UserAllInformation)
user_info = user_info['Buffer']['All']
remoteOps.finish()
return user_info
def on_admin_login(self, context, connection):
self.log = connection.logger
self.results.setdefault(connection.host, {'checks':[]})
remoteOps = RemoteOperations(smbConnection=connection.conn, doKerberos=False)
remoteOps.enableRegistry()
dce = remoteOps._RemoteOperations__rrp
self.check_config(dce, connection)
remoteOps.finish()
def check_laps(self, dce, smb):
key_name = 'HKLM\\Software\\Microsoft\\Windows NT\\CurrentVersion\\Winlogon\\GPextensions'
subkeys = self.reg_get_subkeys(dce, key_name)
reasons = []
success = False
laps_path = '\\Program Files\\LAPS\\CSE'
for subkey in subkeys:
value = self.reg_query_value(dce, key_name + '\\' + subkey, 'DllName')
if type(value) == str and 'laps\\cse\\admpwd.dll' in value.lower():
reasons.append(f'{key_name}\\...\\DllName matches AdmPwd.dll')
success = True
laps_path = '\\'.join(value.split('\\')[1:-1])
break
if not success:
reasons.append(f'No match found in {key_name}\\...\\DllName')
l = ls(smb, laps_path)
if l:
reasons.append('Found LAPS folder at ' + laps_path)
else:
success = False
reasons.append('LAPS folder does not exist')
return success, reasons
l = ls(smb, laps_path + '\\AdmPwd.dll')
if l:
reasons.append(f'Found {laps_path}\\AdmPwd.dll')
else:
success = False
reasons.append(f'{laps_path}\\AdmPwd.dll not found')
return success, reasons
def check_last_successful_update(self, connection):
records = connection.wmi(wmi_query='Select TimeGenerated FROM Win32_ReliabilityRecords Where EventIdentifier=19', namespace='root\\cimv2')
most_recent_update_date = records[0]['TimeGenerated']['value']
most_recent_update_date = most_recent_update_date.split('.')[0]
most_recent_update_date = time.strptime(most_recent_update_date, '%Y%m%d%H%M%S')
most_recent_update_date = time.mktime(most_recent_update_date)
now = time.time()
days_since_last_update = (now - most_recent_update_date)//86400
if days_since_last_update <= OUTDATED_THRESHOLD:
return True, [f'Last update was {days_since_last_update} <= {OUTDATED_THRESHOLD} days ago']
else:
return False, [f'Last update was {days_since_last_update} > {OUTDATED_THRESHOLD} days ago']
def check_administrator_name(self, connection):
users = self.get_local_users(connection)
ok = users[500] not in ('Administrator', 'Administrateur')
reasons = [f'Administrator name changed to {users[500]}' if ok else 'Administrator name unchanged']
return ok, reasons
def check_guest_account_disabled(self, connection):
user_info = self.get_user_info(connection)
uac = user_info['UserAccountControl']
disabled = bool(uac & samr.USER_ACCOUNT_DISABLED)
reasons = ['Guest account disabled' if disabled else 'Guest account enabled']
return disabled, reasons
def check_spooler_service(self, connection):
ok = False
service_config, service_status = self.get_service('Spooler', connection)
if service_config['dwStartType'] == scmr.SERVICE_DISABLED:
ok = True
reasons = ['Spooler service disabled']
else:
reasons = ['Spooler service enabled']
if service_status == scmr.SERVICE_RUNNING:
reasons.append('Spooler service running')
elif service_status == scmr.SERVICE_STOPPED:
ok = True
reasons.append('Spooler service not running')
return ok, reasons
def check_wsus_running(self, connection):
ok = True
reasons = []
service_config, service_status = self.get_service('wuauserv', connection)
if service_config['dwStartType'] == scmr.SERVICE_DISABLED:
reasons = ['WSUS service disabled']
elif service_status != scmr.SERVICE_RUNNING:
reasons = ['WSUS service not running']
return ok, reasons
def check_nbtns(self, dce):
key_name = 'HKLM\\SYSTEM\\CurrentControlSet\\Services\\NetBT\\Parameters\\Interfaces'
subkeys = self.reg_get_subkeys(dce, key_name)
success = False
reasons = []
missing = 0
nbtns_enabled = 0
for subkey in subkeys:
value = self.reg_query_value(dce, key_name + '\\' + subkey, 'NetbiosOptions')
if type(value) == DCERPCSessionError:
if value.error_code == ERROR_OBJECT_NOT_FOUND:
missing += 1
continue
if value != 2:
nbtns_enabled += 1
if missing > 0:
reasons.append(f'HKLM\\SYSTEM\\CurrentControlSet\\Services\\NetBT\\Parameters\\Interfaces\\<interface>\\NetbiosOption: value not found on {missing} interfaces')
if nbtns_enabled > 0:
reasons.append(f'NBTNS enabled on {nbtns_enabled} interfaces out of {len(subkeys)}')
if missing == 0 and nbtns_enabled == 0:
success = True
reasons.append('NBTNS disabled on all interfaces')
return success, reasons
def add_result(self, host, result):
self.results[host]['checks'].append({
"Check":result.name,
"Description":result.description,
"Status":'OK' if result.ok else 'KO',
"Reasons":result.reasons
})
def check_config(self, dce, connection):
host = connection.host
module = self
class ConfigCheck:
"""
Class for performing the checks and holding the results
"""
def __init__(self, name, description="", options={}):
self.name = name
self.description = description
self.ok = False
self.reasons = []
self.options = {
'lastWins':False,
'stopOnOK':False,
'stopOnKO':False,
'KOIfMissing':True
}
self.options.update(options)
def check(self, *specs, missing_ok=True):
"""
Perform checks that only require to compare values in the registry with expected values, according to the specs
a spec may be either a 3-tuple: (key name, value name, expected value), or a 4-tuple (key name, value name, expected value, operation), where operation is a function that implements a comparison operator
"""
op = operator.eq
self.ok = True
for spec in specs:
if len(spec) == 3:
(key, value_name, expected_value) = spec
elif len(spec) == 4:
(key, value_name, expected_value, op) = spec
if op == operator.eq:
opstring = '{left} == {right}'
nopstring = '{left} != {right}'
elif op == operator.contains:
opstring = '{left} in {right}'
nopstring = '{left} not in {right}'
elif op == operator.gt:
opstring = '{left} > {right}'
nopstring = '{left} <= {right}'
elif op == operator.ge:
opstring = '{left} >= {right}'
nopstring = '{left} < {right}'
elif op == operator.lt:
opstring = '{left} < {right}'
nopstring = '{left} >= {right}'
elif op == operator.le:
opstring = '{left} <= {right}'
nopstring = '{left} > {right}'
elif op == operator.ne:
opstring = '{left} != {right}'
nopstring = '{left} == {right}'
else:
opstring = f'{op.__name__}({{left}}, {{right}}) == True '
nopstring = f'{op.__name__}({{left}}, {{right}}) == True '
module.debug('Checking if {0}'.format(opstring.format(left=f'{key}\\{value_name}', right=f'{expected_value}')))
value = module.reg_query_value(dce, key, value_name)
module.debug(f'Got value {value}')
if type(value) == DCERPCSessionError:
if missing_ok == False or self.options['KOIfMissing']:
self.ok = False
if value.error_code in (ERROR_NO_MORE_ITEMS, ERROR_FILE_NOT_FOUND):
self.reasons.append(f'{key}: Key not found')
elif value.error_code == ERROR_OBJECT_NOT_FOUND:
self.reasons.append(f'{value_name}: Value not found')
continue
if op(value, expected_value):
if self.options['lastWins']:
self.ok = True
self.reasons.append(opstring.format(left=f'{key}\\{value_name} ({value})', right=expected_value))
else:
self.reasons.append(nopstring.format(left=f'{key}\\{value_name} ({value})', right=expected_value))
self.ok = False
if self.ok and self.options['stopOnOK']:
break
if not self.ok and self.options['stopOnKO']:
break
return self
def wrap_check(self, check_function, *args, **kwargs):
"""
Execute the given check function with the given arguments, and update internal attributes according to the results.
The check function MUST return a boolean and a list of strings
"""
self.ok, self.reasons = check_function(*args, **kwargs)
return self
def log(self):
if self.ok:
module.log.warning(self.name + ': ' + '\x1b[1;32mOK\x1b[0m')
else:
module.log.warning(self.name + ': ' + '\x1b[1;31mKO\x1b[0m')
# TODO: check_applocker
# TODO: check_smb_encryption (maybe check the characteristics of the connection we already have ?)
# TODO: check_bitlockerconf
for result in (
ConfigCheck('Last successful update', 'Checks how old is the last successful update').wrap_check(self.check_last_successful_update, connection),
ConfigCheck('LAPS', 'Checks if LAPS is installed').wrap_check(self.check_laps, dce, connection),
ConfigCheck("Administrator's name", 'Checks if Administror user name has been changed').wrap_check(self.check_administrator_name, connection),
ConfigCheck('UAC configuration', 'Checks if UAC configuration is secure').check((
'HKLM\\Software\\Microsoft\\Windows\\CurrentVersion\\Policies\\System',
'EnableLUA', 1
),(
'HKLM\\Software\\Microsoft\\Windows\\CurrentVersion\\Policies\\System',
'LocalAccountTokenFilterPolicy', 0
)
),
ConfigCheck('Hash storage format', 'Checks if storing hashes in LM format is disabled').check((
'HKLM\\System\\CurrentControlSet\\Control\\Lsa',
'NoLMHash', 1
)
),
ConfigCheck('Always install elevated', 'Checks if AlwaysInstallElevated is disabled').check((
'HKCU\\SOFTWARE\\Policies\\Microsoft\\Windows\\Installer',
'AlwaysInstallElevated', 0
)
),
ConfigCheck('IPv6 preference', 'Checks if IPv6 is preferred over IPv4').check((
'HKLM\\SYSTEM\\CurrentControlSet\\Services\\Tcpip6\\Parameters',
'DisabledComponents', (32, 255), in_
)
),
ConfigCheck('Spooler service', 'Checks if the spooler service is disabled').wrap_check(self.check_spooler_service, connection),
ConfigCheck('WDigest authentication', 'Checks if WDigest authentication is disabled').check((
'HKLM\\SYSTEM\\CurrentControlSet\\Control\\SecurityProviders\\WDigest',
'UseLogonCredential', 0
)
),
ConfigCheck('WSUS configuration', 'Checks if WSUS configuration uses HTTPS')\
.wrap_check(self.check_wsus_running, connection)\
.check((
'HKLM\\Software\\Policies\\Microsoft\\Windows\\WindowsUpdate',
'WUServer', 'https://', startswith
)
)\
.check((
'HKLM\\Software\\Policies\\Microsoft\\Windows\\WindowsUpdate',
'UseWUServer', 0, operator.eq
)
),
ConfigCheck('LSA cache', 'Checks how many logons are kept in the LSA cache').check((
'HKLM\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Winlogon',
'CachedLogonsCount', 2, le
)
),
ConfigCheck('RDP expiration time', 'Checks RDP session timeout').check((
'HKLM\\SOFTWARE\\Policies\\Microsoft\\Windows NT\\Terminal Services',
'MaxDisconnectionTime', 0, operator.gt
),(
'HKCU\\SOFTWARE\\Policies\\Microsoft\\Windows NT\\Terminal Services',
'MaxDisconnectionTime', 0, operator.gt
)
),
ConfigCheck('CredentialGuard', 'Checks if CredentialGuard is enabled').check((
'HKLM\\SYSTEM\\CurrentControlSet\\Control\\DeviceGuard',
'EnableVirtualizationBasedSecurity', 1
),(
'HKLM\\SYSTEM\\CurrentControlSet\\Control\\Lsa',
'LsaCfgFlags', 1
)
),
ConfigCheck('PPL', 'Checks if lsass runs as a protected process').check((
'HKLM\\SYSTEM\\CurrentControlSet\\Control\\Lsa',
'RunAsPPL', 1
)
),
ConfigCheck('PEAP certificate validation', 'Checks if PEAP certificate validation is enabled').check((
'HKLM\\SYSTEM\\CurrentControlSet\\Services\\Rasman\\PPP\\EAP\\13',
'ValidateServerCert', 1
)
),
ConfigCheck('Powershell v2 availability', 'Checks if powershell v2 is available').check((
'HKLM\\SOFTWARE\\Microsoft\\PowerShell\\3\\PowerShellEngine',
'PSCompatibleVersion', '2.0', not_(operator.contains)
)
),
ConfigCheck('NTLMv1', 'Checks if NTLMv1 authentication is disabled').check((
'HKLM\\SYSTEM\\CurrentControlSet\\Control\\Lsa',
'LmCompatibilityLevel', 5, operator.ge
)
),
ConfigCheck('NBTNS', 'Checks if NBTNS is disabled on all interfaces').wrap_check(self.check_nbtns, dce),
ConfigCheck('mDNS', 'Checks if mDNS is disabled').check((
'HKLM\\SYSTEM\\CurrentControlSet\\Services\\DNScache\\Parameters',
'EnableMDNS', 0
)
),
ConfigCheck('SMB signing', 'Checks if SMB signing is enabled').check((
'HKLM\\System\\CurrentControlSet\\Services\\LanmanServer\\Parameters',
'requiresecuritysignature', 1
)
),
ConfigCheck('LDAP signing', 'Checks if LDAP signing is enabled').check((
'HKLM\\SYSTEM\\CurrentControlSet\\Services\\NTDS\\Parameters',
'LDAPServerIntegrity', 2
),(
'HKLM\\SYSTEM\\CurrentControlSet\\Services\\NTDS',
'LdapEnforceChannelBinding', 2
)
),
ConfigCheck('Network selection on lock screen', 'Checks if network selection on lock screen is disabled').check((
'HKLM\\Software\\Policies\\Microsoft\\Windows\\System',
'DontDisplayNetworkSelectionUi', 1
)
),
ConfigCheck('Last logged-on user displayed', 'Checks if display of last logged on user is disabled').check((
'HKLM\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Policies\\System',
'dontdisplaylastusername', 1
)
),
ConfigCheck('RDP authentication', 'Checks RDP authentication configuration (NLA auth and restricted admin mode)').check((
'HKLM\\System\\CurrentControlSet\\Control\\Terminal Server\\WinStations\\RDP-Tcp\\',
'UserAuthentication', 1
),(
'HKLM\\SYSTEM\\CurrentControlSet\\Control\\LSA',
'RestrictedAdminMode', 1
)
),
ConfigCheck('Guest account disabled', 'Checks if the guest account is disabled').wrap_check(self.check_guest_account_disabled, connection),
ConfigCheck('Automatic session lock', 'Checks if the session is automatically locked on after a period of inactivity').check((
'HKCU\\Control Panel\\Desktop',
'ScreenSaverIsSecure', 1
),(
'HKCU\\Control Panel\\Desktop',
'ScreenSaveTimeOut', 300, le
)
),
ConfigCheck('Powershell Execution Policy', 'Checks if the Powershell execution policy is set to "Restricted"', options={'KOIfMissing':False, 'lastWins':True}).check((
'HKLM\\SOFTWARE\\Microsoft\\PowerShell\\1\ShellIds\Microsoft.Powershell',
'ExecutionPolicy', 'Restricted\x00'
),(
'HKCU\\SOFTWARE\\Microsoft\\PowerShell\\1\ShellIds\Microsoft.Powershell',
'ExecutionPolicy', 'Restricted\x00'
)
),
ConfigCheck('Legal notice banner', 'Checks if there is a legal notice banner set').check((
'HKLM\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Policies\\System',
'legalnoticecaption', "", operator.ne
)
)
):
result.log()
self.add_result(host, result)
pprint(self.results)
def _open_root_key(self, dce, root_key):
ans = None
if root_key.upper() == 'HKLM':
ans = rrp.hOpenLocalMachine(dce)
elif root_key.upper() == 'HKCR':
ans = rrp.hOpenClassesRoot(dce)
elif root_key.upper() == 'HKU':
ans = rrp.hOpenUsers(dce)
elif root_key.upper() == 'HKCU':
ans = rrp.hOpenCurrentUser(dce)
elif root_key.upper() == 'HKCC':
ans = rrp.hOpenCurrentConfig(dce)
else:
self.log.error('Invalid root key. Must be one of HKCR, HKCC, HKCU, HKLM or HKU')
return ans
def reg_get_subkeys(self, dce, key_name, separator='\\'):
root_key, subkey = key_name.split(separator, 1)
ans = self._open_root_key(dce, root_key)
subkeys = []
if ans is None:
return ans
root_key_handle = ans['phKey']
try:
ans = rrp.hBaseRegOpenKey(dce, root_key_handle, subkey)
except DCERPCSessionError as e:
self.debug(e)
if e.error_code == ERROR_FILE_NOT_FOUND:
return e
subkey_handle = ans['phkResult']
i = 0
while True:
try:
ans = rrp.hBaseRegEnumKey(dce=dce, hKey=subkey_handle, dwIndex=i)
subkeys.append(ans['lpNameOut'][:-1])
i += 1
except DCERPCSessionError as e:
if e.error_code == ERROR_NO_MORE_ITEMS:
break
else:
self.debug(e)
break
return subkeys
def reg_query_value(self, dce, keyName, valueName=None, separator='\\'):
"""
Query remote registry data for a given registry value
"""
def subkey_values(subkey_handle):
dwIndex = 0
while True:
try:
value_type, value_name, value_data = get_value(subkey_handle, dwIndex)
yield (value_type, value_name, value_data)
dwIndex += 1
except DCERPCSessionError as e:
if e.error_code == ERROR_NO_MORE_ITEMS:
break
else:
self.log.error(f'Received error code {e.error_code}')
return
def get_value(subkey_handle, dwIndex=0):
ans = rrp.hBaseRegEnumValue(dce=dce, hKey=subkey_handle, dwIndex=dwIndex)
value_type = ans['lpType']
value_name = ans['lpValueNameOut']
value_data = ans['lpData']
if value_type in (1, 2, 7):
value_data = b''.join(value_data).decode('utf-16')
else:
value_data = b''.join(value_data)
if value_type == 4:
value_data = int.from_bytes(value_data, 'little')
elif value_type == 5:
value_data = int.from_bytes(value_data, 'big')
elif value_type == 11:
value_data = int.from_bytes(value_data, 'little')
return value_type, value_name[:-1], value_data
root_key, subkey = keyName.split(separator, 1)
ans = self._open_root_key(dce, root_key)
if ans is None:
return ans
root_key_handle = ans['phKey']
try:
ans = rrp.hBaseRegOpenKey(dce, root_key_handle, subkey)
except DCERPCSessionError as e:
self.debug(e)
if e.error_code == ERROR_FILE_NOT_FOUND:
return e
subkey_handle = ans['phkResult']
if valueName is None:
_,_, data = get_value(subkey_handle)
else:
found = False
for _,name,data in subkey_values(subkey_handle):
if name.upper() == valueName.upper():
found = True
break
if not found:
return DCERPCSessionError(error_code=ERROR_OBJECT_NOT_FOUND)
for handle in (root_key_handle, subkey_handle):
rrp.hBaseRegCloseKey(dce, handle)
return data
def le(reg_sz_string, number):
return int(reg_sz_string[:-1]) <= number
def in_(obj, seq):
return obj in seq
def not_(boolean_operator):
def wrapper(*args, **kwargs):
return not boolean_operator(*args, **kwargs)
wrapper.__name__ = f'not_{boolean_operator.__name__}'
return wrapper
def startswith(string, start):
return string.startswith(start)
def ls(smb, path='\\', share='C$'):
l = []
try:
l = smb.conn.listPath(share, path)
except SMBSessionError as e:
if e.getErrorString()[0] == 'STATUS_NO_SUCH_FILE':
pass
else:
raise e
return l