Update obsolete.py
Add pwdLastSet Signed-off-by: Shad0wC0ntr0ller <90877534+Shad0wC0ntr0ller@users.noreply.github.com>main
parent
d99ddaaaec
commit
018cc98ed3
|
@ -1,9 +1,12 @@
|
|||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
import socket
|
||||
|
||||
|
||||
class CMEModule:
|
||||
class NXCModule:
|
||||
'''
|
||||
Extract obsolete operating systems from LDAP
|
||||
Module by Brandon Fisher @shad0wcntr0ller
|
||||
|
@ -14,7 +17,24 @@ class CMEModule:
|
|||
opsec_safe = True
|
||||
multiple_hosts = True
|
||||
|
||||
def ldap_time_to_datetime(self, ldap_time):
|
||||
"""
|
||||
Convert an LDAP timestamp to a datetime object.
|
||||
LDAP timestamp is the number of 100-nanosecond intervals since January 1, 1601.
|
||||
"""
|
||||
if ldap_time == '0': # Account for never-set passwords
|
||||
return 'Never'
|
||||
try:
|
||||
# Remove the last 7 digits (fractional seconds) and convert to seconds
|
||||
epoch = datetime(1601, 1, 1) + timedelta(seconds=int(ldap_time) / 10000000)
|
||||
return epoch.strftime('%Y-%m-%d %H:%M:%S')
|
||||
except Exception as e:
|
||||
return 'Conversion Error'
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""
|
||||
No module-specific options required.
|
||||
"""
|
||||
pass
|
||||
|
||||
def on_login(self, context, connection):
|
||||
|
@ -24,64 +44,53 @@ class CMEModule:
|
|||
"(operatingSystem=*Windows 7*)(operatingSystem=*Windows 8*)"
|
||||
"(operatingSystem=*Windows 8.1*)(operatingSystem=*Windows Server 2003*)"
|
||||
"(operatingSystem=*Windows Server 2008*)(operatingSystem=*Windows Server 2012*)))")
|
||||
attributes = ['name', 'operatingSystem', 'dNSHostName']
|
||||
attributes = ['name', 'operatingSystem', 'dNSHostName', 'pwdLastSet']
|
||||
|
||||
try:
|
||||
context.log.debug(f'Search Filter={search_filter}')
|
||||
resp = connection.ldapConnection.search(searchFilter=search_filter, attributes=attributes, sizeLimit=0)
|
||||
except ldap_impacket.LDAPSearchError as e:
|
||||
if 'sizeLimitExceeded' in e.getErrorString():
|
||||
context.log.debug('sizeLimitExceeded exception caught, processing the data received')
|
||||
resp = e.getAnswers()
|
||||
else:
|
||||
context.log.debug(e)
|
||||
return False
|
||||
except Exception as e:
|
||||
context.log.error('LDAP search error:', exc_info=True)
|
||||
return False
|
||||
|
||||
answers = []
|
||||
context.log.debug(f'Total of records returned {len(resp)}')
|
||||
|
||||
for item in resp:
|
||||
if not isinstance(item, ldapasn1_impacket.SearchResultEntry):
|
||||
continue
|
||||
try:
|
||||
name, os, dns_hostname = '', '', ''
|
||||
for attribute in item['attributes']:
|
||||
attr_type = str(attribute['type'])
|
||||
if attr_type == 'name':
|
||||
name = str(attribute['vals'][0])
|
||||
elif attr_type == 'operatingSystem':
|
||||
os = str(attribute['vals'][0])
|
||||
elif attr_type == 'dNSHostName':
|
||||
dns_hostname = str(attribute['vals'][0])
|
||||
|
||||
if dns_hostname and os:
|
||||
answers.append([dns_hostname, os])
|
||||
except Exception as e:
|
||||
context.log.debug("Exception encountered:", exc_info=True)
|
||||
context.log.debug(f'Skipping item, cannot process due to error {str(e)}')
|
||||
for item in resp:
|
||||
if 'attributes' not in item:
|
||||
continue
|
||||
name, os, dns_hostname, pwd_last_set = '', '', '', '0' # Default '0' for pwdLastSet
|
||||
for attribute in item['attributes']:
|
||||
attr_type = str(attribute['type'])
|
||||
if attr_type == 'name':
|
||||
name = str(attribute['vals'][0])
|
||||
elif attr_type == 'operatingSystem':
|
||||
os = str(attribute['vals'][0])
|
||||
elif attr_type == 'dNSHostName':
|
||||
dns_hostname = str(attribute['vals'][0])
|
||||
elif attr_type == 'pwdLastSet':
|
||||
pwd_last_set = str(attribute['vals'][0])
|
||||
|
||||
if dns_hostname and os:
|
||||
pwd_last_set_readable = self.ldap_time_to_datetime(pwd_last_set)
|
||||
try:
|
||||
ip_address = socket.gethostbyname(dns_hostname)
|
||||
answers.append((dns_hostname, ip_address, os, pwd_last_set_readable))
|
||||
except socket.gaierror:
|
||||
answers.append((dns_hostname, "N/A", os, pwd_last_set_readable))
|
||||
|
||||
if answers:
|
||||
|
||||
hostname_parts = answers[0][0].split('.')
|
||||
domain = ".".join(hostname_parts[1:])
|
||||
|
||||
home = Path.home()
|
||||
nxc_path = home / ".nxc"
|
||||
logs_path = nxc_path / 'logs'
|
||||
filename = logs_path / f'{domain}.obsoletehosts.txt'
|
||||
|
||||
context.log.display(f'Obsolete hosts will be saved to {filename}')
|
||||
context.log.success('Found the following obsolete operating systems:')
|
||||
|
||||
for answer in answers:
|
||||
try:
|
||||
ip_address = socket.gethostbyname(answer[0])
|
||||
except socket.gaierror:
|
||||
ip_address = "N/A"
|
||||
obsolete_hosts_count = len(answers)
|
||||
logs_path = Path.home() / ".nxc" / 'logs'
|
||||
logs_path.mkdir(parents=True, exist_ok=True)
|
||||
filename = logs_path / f'{connection.domain}.obsoletehosts.txt'
|
||||
|
||||
context.log.highlight(f'{answer[0]} ({ip_address}) : {answer[1]} ')
|
||||
with open(filename, 'a') as f:
|
||||
f.write(f'{answer[0]}\n')
|
||||
context.log.display(f'{obsolete_hosts_count} Obsolete hosts will be saved to {filename}')
|
||||
with open(filename, 'w') as f:
|
||||
for dns_hostname, ip_address, os, pwd_last_set_readable in answers:
|
||||
log_message = f'{dns_hostname} ({ip_address}) : {os} [pwd-last-set: {pwd_last_set_readable}]'
|
||||
context.log.highlight(log_message)
|
||||
f.write(log_message + '\n')
|
||||
else:
|
||||
context.log.display("No Obsolete Hosts Identified")
|
||||
|
||||
|
|
Loading…
Reference in New Issue