diff --git a/nxc/modules/obsolete.py b/nxc/modules/obsolete.py index cfccec96..09fa6678 100644 --- a/nxc/modules/obsolete.py +++ b/nxc/modules/obsolete.py @@ -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")