From 1bc2cd2dadf5f6cab76e4a782abc2f9fd68ad43e Mon Sep 17 00:00:00 2001 From: mpgn Date: Wed, 7 Sep 2022 10:49:55 -0400 Subject: [PATCH] Add module scan-subnet --- cme/modules/scan-network.py | 350 ++++++++++++++++++++++++++++++++++++ 1 file changed, 350 insertions(+) create mode 100644 cme/modules/scan-network.py diff --git a/cme/modules/scan-network.py b/cme/modules/scan-network.py new file mode 100644 index 00000000..77f338b6 --- /dev/null +++ b/cme/modules/scan-network.py @@ -0,0 +1,350 @@ +# Credit to https://twitter.com/snovvcrash/status/1550518555438891009 +# Credit to https://github.com/dirkjanm/adidnsdump @_dirkjan +# module by @mpgn_x64 + +from struct import unpack, pack +from impacket.structure import Structure +from ldap3 import NTLM, Server, Connection, ALL, LEVEL, BASE, MODIFY_DELETE, MODIFY_ADD, MODIFY_REPLACE, Tls +from datetime import datetime +from builtins import str +import socket +import codecs +import dns.resolver +import dns.name + +def get_dns_zones(connection, root, debug=False): + connection.search(root, '(objectClass=dnsZone)', search_scope=LEVEL, attributes=['dc']) + zones = [] + for entry in connection.response: + if entry['type'] != 'searchResEntry': + continue + zones.append(entry['attributes']['dc']) + return zones + +def get_dns_resolver(server, context): + # Create a resolver object + dnsresolver = dns.resolver.Resolver() + # Is our host an IP? In that case make sure the server IP is used + # if not assume lookups are working already + try: + if server.startswith('ldap://'): + server = server[7:] + if server.startswith('ldaps://'): + server = server[8:] + socket.inet_aton(server) + dnsresolver.nameservers = [server] + except socket.error: + context.info('Using System DNS to resolve unknown entries. Make sure resolving your target domain works here or specify an IP'\ + ' as target host to use that server for queries') + return dnsresolver + +def ldap2domain(ldap): + return re.sub(',DC=', '.', ldap[ldap.find('dc='):], flags=re.I)[3:] + +def new_record(rtype, serial): + nr = DNS_RECORD() + nr['Type'] = rtype + nr['Serial'] = serial + nr['TtlSeconds'] = 180 + # From authoritive zone + nr['Rank'] = 240 + return nr + +# From: https://docs.microsoft.com/en-us/windows/win32/dns/dns-constants +RECORD_TYPE_MAPPING = { + 0: 'ZERO', + 1: 'A', + 2: 'NS', + 5: 'CNAME', + 6: 'SOA', + 12: 'PTR', + #15: 'MX', + #16: 'TXT', + 28: 'AAAA', + 33: 'SRV' +} + +def searchResEntry_to_dict(results): + data = {} + for attr in results['attributes']: + key = str(attr['type']) + value = str(attr['vals'][0]) + data[key] = value + return data + + +class CMEModule: + + name = 'get-network' + description = "" + supported_protocols = ['ldap'] + opsec_safe = True + multiple_hosts = True + + def options(self, context, module_options): + """ + ALL Get DNS and IP (default: false) + ONLY_HOSTS Get DNS only (no ip) (default: false) + """ + + self.showall = False + self.showhosts = False + self.showip = True + + if module_options and 'ALL' in module_options: + if module_options['ALL'].lower() == "true" or module_options['ALL'] == "1": + self.showall = True + else: + print("Could not parse ALL option.") + if module_options and 'IP' in module_options: + if module_options['IP'].lower() == "true" or module_options['IP'] == "1": + self.showip = True + else: + print("Could not parse ONLY_HOSTS option.") + if module_options and 'ONLY_HOSTS' in module_options: + if module_options['ONLY_HOSTS'].lower() == "true" or module_options['ONLY_HOSTS'] == "1": + self.showhosts = True + else: + print("Could not parse ONLY_HOSTS option.") + + + def on_login(self, context, connection): + zone = ldap2domain(connection.baseDN) + dnsroot = 'CN=MicrosoftDNS,DC=DomainDnsZones,%s' % connection.baseDN + searchtarget = 'DC=%s,%s' % (zone, dnsroot) + context.log.info('Querying zone for records') + sfilter = '(DC=*)' + + try: + list_sites = connection.ldapConnection.search( + searchBase=searchtarget, + searchFilter=sfilter, + attributes=['dnsRecord','dNSTombstoned','name'], + sizeLimit=100000 + ) + except ldap.LDAPSearchError as e: + if e.getErrorString().find('sizeLimitExceeded') >= 0: + logging.debug('sizeLimitExceeded exception caught, giving up and processing the data received') + # We reached the sizeLimit, process the answers we have already and that's it. Until we implement + # paged queries + list_sites = e.getAnswers() + pass + else: + raise + targetentry = None + dnsresolver = get_dns_resolver(connection.host, context.log) + + outdata = [] + + for item in list_sites: + if isinstance(item, ldapasn1_impacket.SearchResultEntry) is not True: + continue + site = searchResEntry_to_dict(item) + recordname = site['name'] + + if "dnsRecord" in site: + record = bytes(site['dnsRecord'].encode('latin1')) + dr = DNS_RECORD(record) + if RECORD_TYPE_MAPPING[dr['Type']] == "A": + if dr['Type'] == 1: + address = DNS_RPC_RECORD_A(dr['Data']) + outdata.append({'name':recordname, 'type': RECORD_TYPE_MAPPING[dr['Type']], 'value': address.formatCanonical()}) + if dr['Type'] in [a for a in RECORD_TYPE_MAPPING if RECORD_TYPE_MAPPING[a] in ['CNAME', 'NS', 'PTR']]: + address = DNS_RPC_RECORD_NODE_NAME(dr['Data']) + outdata.append({'name':recordname, 'type':RECORD_TYPE_MAPPING[dr['Type']], 'value': address[list(address.fields)[0]].toFqdn()}) + elif dr['Type'] == 28: + address = DNS_RPC_RECORD_AAAA(dr['Data']) + outdata.append({'name':recordname, 'type':RECORD_TYPE_MAPPING[dr['Type']], 'value': address.formatCanonical()}) + + context.log.highlight('Found %d records' % len(outdata)) + path = os.path.expanduser('~/.cme/logs/{}_network_{}.log'.format(connection.domain, datetime.now().strftime("%Y-%m-%d_%H%M%S"))) + with codecs.open(path, 'w', 'utf-8') as outfile: + for row in outdata: + if self.showhosts: + outfile.write('{}\n'.format(row['name'] + '.' + connection.domain)) + elif self.showall: + outfile.write('{} \t {}\n'.format(row['name'] + '.' + connection.domain, row['value'])) + else: + outfile.write('{}\n'.format(row['value'])) + context.log.success('Dumped {} records to {}'.format(len(outdata), path)) + if not self.showall and not self.showhosts: + context.log.info("To extract CIDR from the {} ip, run the following command: cat your_file | mapcidr -aa -silent | mapcidr -a -silent".format(len(outdata))) + + + +class DNS_RECORD(Structure): + """ + dnsRecord - used in LDAP + [MS-DNSP] section 2.3.2.2 + """ + structure = ( + ('DataLength', 'L'), + ('Reserved', 'H'), + ('wRecordCount', '>H'), + ('dwFlags', '>L'), + ('dwChildCount', '>L'), + ('dnsNodeName', ':') + ) + +class DNS_RPC_RECORD_A(Structure): + """ + DNS_RPC_RECORD_A + [MS-DNSP] section 2.2.2.2.4.1 + """ + structure = ( + ('address', ':'), + ) + + def formatCanonical(self): + return socket.inet_ntoa(self['address']) + + def fromCanonical(self, canonical): + self['address'] = socket.inet_aton(canonical) + + +class DNS_RPC_RECORD_NODE_NAME(Structure): + """ + DNS_RPC_RECORD_NODE_NAME + [MS-DNSP] section 2.2.2.2.4.2 + """ + structure = ( + ('nameNode', ':', DNS_COUNT_NAME), + ) + +class DNS_RPC_RECORD_SOA(Structure): + """ + DNS_RPC_RECORD_SOA + [MS-DNSP] section 2.2.2.2.4.3 + """ + structure = ( + ('dwSerialNo', '>L'), + ('dwRefresh', '>L'), + ('dwRetry', '>L'), + ('dwExpire', '>L'), + ('dwMinimumTtl', '>L'), + ('namePrimaryServer', ':', DNS_COUNT_NAME), + ('zoneAdminEmail', ':', DNS_COUNT_NAME) + ) + +class DNS_RPC_RECORD_NULL(Structure): + """ + DNS_RPC_RECORD_NULL + [MS-DNSP] section 2.2.2.2.4.4 + """ + structure = ( + ('bData', ':'), + ) + +# Some missing structures here that I skipped + +class DNS_RPC_RECORD_NAME_PREFERENCE(Structure): + """ + DNS_RPC_RECORD_NAME_PREFERENCE + [MS-DNSP] section 2.2.2.2.4.8 + """ + structure = ( + ('wPreference', '>H'), + ('nameExchange', ':', DNS_COUNT_NAME) + ) + +# Some missing structures here that I skipped + +class DNS_RPC_RECORD_AAAA(Structure): + """ + DNS_RPC_RECORD_AAAA + [MS-DNSP] section 2.2.2.2.4.17 + """ + structure = ( + ('ipv6Address', '16s'), + ) + + def formatCanonical(self): + return socket.inet_ntop(socket.AF_INET6, self['ipv6Address']) + +class DNS_RPC_RECORD_SRV(Structure): + """ + DNS_RPC_RECORD_SRV + [MS-DNSP] section 2.2.2.2.4.18 + """ + structure = ( + ('wPriority', '>H'), + ('wWeight', '>H'), + ('wPort', '>H'), + ('nameTarget', ':', DNS_COUNT_NAME) + ) + +class DNS_RPC_RECORD_TS(Structure): + """ + DNS_RPC_RECORD_TS + [MS-DNSP] section 2.2.2.2.4.23 + """ + structure = ( + ('entombedTime', '