def main(): parser = argparse.ArgumentParser( description= 'Query/modify DNS records for Active Directory integrated DNS via LDAP' ) parser._optionals.title = "Main options" parser._positionals.title = "Required options" #Main parameters #maingroup = parser.add_argument_group("Main options") parser.add_argument( "host", type=native_str, metavar='HOSTNAME', help="Hostname/ip or ldap://host:port connection string to connect to") parser.add_argument("-u", "--user", type=native_str, metavar='USERNAME', help="DOMAIN\\username for authentication.") parser.add_argument( "-p", "--password", type=native_str, metavar='PASSWORD', help="Password or LM:NTLM hash, will prompt if not specified") parser.add_argument( "--forest", action='store_true', help="Search the ForestDnsZones instead of DomainDnsZones") parser.add_argument( "--legacy", action='store_true', help="Search the System partition (legacy DNS storage)") parser.add_argument( "--zone", help="Zone to search in (if different than the current domain)") parser.add_argument( "--print-zones", action='store_true', help= "Only query all zones on the DNS server, no other modifications are made" ) parser.add_argument("-v", "--verbose", action='store_true', help="Show verbose info") parser.add_argument("-d", "--debug", action='store_true', help="Show debug info") parser.add_argument("-r", "--resolve", action='store_true', help="Resolve hidden recoreds via DNS") parser.add_argument("--dns-tcp", action='store_true', help="Use DNS over TCP") parser.add_argument("--include-tombstoned", action='store_true', help="Include tombstoned (deleted) records") parser.add_argument("--ssl", action='store_true', help="Connect to LDAP server using SSL") parser.add_argument( "--referralhosts", action='store_true', help="Allow passthrough authentication to all referral hosts") parser.add_argument( "--dcfilter", action='store_true', help="Use an alternate filter to identify DNS record types") parser.add_argument( "--sslprotocol", type=native_str, help= "SSL version for LDAP connection, can be SSLv23, TLSv1, TLSv1_1 or TLSv1_2" ) args = parser.parse_args() #Prompt for password if not set authentication = None if args.user is not None: authentication = NTLM if not '\\' in args.user: print_f('Username must include a domain, use: DOMAIN\\username') sys.exit(1) if args.password is None: args.password = getpass.getpass() # define the server and the connection s = Server(args.host, get_info=ALL) if args.ssl: s = Server(args.host, get_info=ALL, port=636, use_ssl=True) if args.sslprotocol: v = {'SSLv23': 2, 'TLSv1': 3, 'TLSv1_1': 4, 'TLSv1_2': 5} if args.sslprotocol not in v.keys(): parser.print_help(sys.stderr) sys.exit(1) s = Server(args.host, get_info=ALL, port=636, use_ssl=True, tls=Tls(validate=0, version=v[args.sslprotocol])) if args.referralhosts: s.allowed_referral_hosts = [('*', True)] print_m('Connecting to host...') c = Connection(s, user=args.user, password=args.password, authentication=authentication, auto_referrals=False) print_m('Binding to host') # perform the Bind operation if not c.bind(): print_f('Could not bind with specified credentials') print_f(c.result) sys.exit(1) print_o('Bind OK') domainroot = s.info.other['defaultNamingContext'][0] forestroot = s.info.other['rootDomainNamingContext'][0] if args.forest: dnsroot = 'CN=MicrosoftDNS,DC=ForestDnsZones,%s' % forestroot else: if args.legacy: dnsroot = 'CN=MicrosoftDNS,CN=System,%s' % domainroot else: dnsroot = 'CN=MicrosoftDNS,DC=DomainDnsZones,%s' % domainroot if args.print_zones: domaindnsroot = 'CN=MicrosoftDNS,DC=DomainDnsZones,%s' % domainroot zones = get_dns_zones(c, domaindnsroot, args.verbose) if len(zones) > 0: print_m('Found %d domain DNS zones:' % len(zones)) for zone in zones: print(' %s' % zone) forestdnsroot = 'CN=MicrosoftDNS,DC=ForestDnsZones,%s' % forestroot zones = get_dns_zones(c, forestdnsroot, args.verbose) if len(zones) > 0: print_m('Found %d forest DNS zones (dump with --forest):' % len(zones)) for zone in zones: print(' %s' % zone) legacydnsroot = 'CN=MicrosoftDNS,CN=System,%s' % domainroot zones = get_dns_zones(c, legacydnsroot, args.verbose) if len(zones) > 0: print_m('Found %d legacy DNS zones (dump with --legacy):' % len(zones)) for zone in zones: print(' %s' % zone) return if args.zone: zone = args.zone else: # Default to current domain zone = ldap2domain(domainroot) searchtarget = 'DC=%s,%s' % (zone, dnsroot) print_m('Querying zone for records') sfilter = '(objectClass=*)' if not args.dcfilter else '(DC=*)' c.extend.standard.paged_search( searchtarget, sfilter, search_scope=LEVEL, attributes=['dnsRecord', 'dNSTombstoned', 'name'], paged_size=500, generator=False) targetentry = None if args.resolve: dnsresolver = get_dns_resolver(args.host) else: dnsresolver = None outdata = [] for targetentry in c.response: if targetentry['type'] != 'searchResEntry': print(targetentry) continue if not targetentry['attributes']['name']: # No permission to view those records recordname = targetentry['dn'][3:targetentry['dn']. index(searchtarget) - 1] if not args.resolve: outdata.append({'name': recordname, 'type': '?', 'value': '?'}) if args.verbose: print_o('Found hidden record %s' % recordname) else: # Resolve A query try: res = dnsresolver.query('%s.%s.' % (recordname, zone), 'A', tcp=args.dns_tcp, raise_on_no_answer=False) except (dns.resolver.NoAnswer, dns.resolver.NXDOMAIN, dns.resolver.Timeout, dns.name.EmptyLabel) as e: if args.verbose: print_f(str(e)) print_m( 'Could not resolve node %s (probably no A record assigned to name)' % recordname) outdata.append({ 'name': recordname, 'type': '?', 'value': '?' }) continue if len(res.response.answer) == 0: print_m( 'Could not resolve node %s (probably no A record assigned to name)' % recordname) outdata.append({ 'name': recordname, 'type': '?', 'value': '?' }) continue if args.verbose: print_o('Resolved hidden record %s' % recordname) for answer in res.response.answer: try: outdata.append({ 'name': recordname, 'type': RECORD_TYPE_MAPPING[answer.rdtype], 'value': str(answer[0]) }) except KeyError: print_m('Unexpected record type seen: {}'.format( answer.rdtype)) else: recordname = targetentry['attributes']['name'] if args.verbose: print_o('Found record %s' % targetentry['attributes']['name']) # Skip tombstoned records unless requested if targetentry['attributes'][ 'dNSTombstoned'] and not args.include_tombstoned: continue for record in targetentry['raw_attributes']['dnsRecord']: dr = DNS_RECORD(record) # dr.dump() # print targetentry['dn'] if args.debug: print_record(dr, targetentry['attributes']['dNSTombstoned']) 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'] ]: 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() }) elif dr['Type'] not in [ a for a in RECORD_TYPE_MAPPING if RECORD_TYPE_MAPPING[a] in ['A', 'AAAA,' 'CNAME', 'NS'] ]: if args.debug: print_m('Unexpected record type seen: {}'.format( dr['Type'])) continue print_o('Found %d records' % len(outdata)) with codecs.open('records.csv', 'w', 'utf-8') as outfile: outfile.write('type,name,value\n') for row in outdata: outfile.write('{type},{name},{value}\n'.format(**row))
def main(): parser = argparse.ArgumentParser( description= 'Domain information dumper via LDAP. Dumps users/computers/groups and OS/membership information to HTML/JSON/greppable output.' ) parser._optionals.title = "Main options" parser._positionals.title = "Required options" #Main parameters #maingroup = parser.add_argument_group("Main options") parser.add_argument( "host", type=str, metavar='HOSTNAME', help= "Hostname/ip or ldap://host:port connection string to connect to (use ldaps:// to use SSL)" ) parser.add_argument( "-u", "--user", type=native_str, metavar='USERNAME', help= "DOMAIN\\username for authentication, leave empty for anonymous authentication" ) parser.add_argument( "-p", "--password", type=native_str, metavar='PASSWORD', help="Password or LM:NTLM hash, will prompt if not specified") parser.add_argument( "-at", "--authtype", type=str, choices=['NTLM', 'SIMPLE'], default='NTLM', help="Authentication type (NTLM or SIMPLE, default: NTLM)") parser.add_argument("--ssl", action='store_true', help="Connect to LDAP server using SSL") parser.add_argument( "--referralhosts", action='store_true', help="Allow passthrough authentication to all referral hosts") parser.add_argument( "--sslprotocol", type=native_str, help= "SSL version for LDAP connection, can be SSLv23, TLSv1, TLSv1_1 or TLSv1_2" ) #Output parameters outputgroup = parser.add_argument_group("Output options") outputgroup.add_argument( "-o", "--outdir", type=str, metavar='DIRECTORY', help="Directory in which the dump will be saved (default: current)") outputgroup.add_argument("--no-html", action='store_true', help="Disable HTML output") outputgroup.add_argument("--no-json", action='store_true', help="Disable JSON output") outputgroup.add_argument("--no-grep", action='store_true', help="Disable Greppable output") outputgroup.add_argument( "--grouped-json", action='store_true', default=False, help="Also write json files for grouped files (default: disabled)") outputgroup.add_argument( "-d", "--delimiter", help="Field delimiter for greppable output (default: tab)") #Additional options miscgroup = parser.add_argument_group("Misc options") miscgroup.add_argument( "-r", "--resolve", action='store_true', help= "Resolve computer hostnames (might take a while and cause high traffic on large networks)" ) miscgroup.add_argument( "-n", "--dns-server", help= "Use custom DNS resolver instead of system DNS (try a domain controller IP)" ) miscgroup.add_argument( "-m", "--minimal", action='store_true', default=False, help="Only query minimal set of attributes to limit memmory usage") miscgroup.add_argument( "-t", "--types", type=native_str, default="all", help= "Only perform the specified queries out of: users,groups,computers,policy,trusts" ) args = parser.parse_args() #Create default config cnf = domainDumpConfig() #Dns lookups? if args.resolve: cnf.lookuphostnames = True #Custom dns server? if args.dns_server is not None: cnf.dnsserver = args.dns_server #Minimal attributes? if args.minimal: cnf.minimal = True #Custom separator? if args.delimiter is not None: cnf.grepsplitchar = args.delimiter #Disable html? if args.no_html: cnf.outputhtml = False #Disable json? if args.no_json: cnf.outputjson = False #Disable grep? if args.no_grep: cnf.outputgrep = False #Custom outdir? if args.outdir is not None: cnf.basepath = args.outdir #Do we really need grouped json files? cnf.groupedjson = args.grouped_json queries = ['users', 'groups', 'computers', 'policy', 'trusts'] reports = [a.lstrip().rstrip() for a in args.types.split(',')] if reports == ['all']: reports = queries elif [a for a in reports if a not in queries]: print('Bad output types: ' + ','.join([a for a in reports if a not in queries])) sys.exit(1) cnf.reports = reports #Prompt for password if not set authentication = None if args.user is not None: if args.authtype == 'SIMPLE': authentication = 'SIMPLE' else: authentication = NTLM if not '\\' in args.user: log_warn('Username must include a domain, use: DOMAIN\\username') sys.exit(1) if args.password is None: args.password = getpass.getpass() else: log_info( 'Connecting as anonymous user, dumping will probably fail. Consider specifying a username/password to login with' ) # define the server and the connection s = Server(args.host, get_info=ALL) if args.ssl: s = Server(args.host, get_info=ALL, port=636, use_ssl=True) if args.sslprotocol: v = {'SSLv23': 2, 'TLSv1': 3, 'TLSv1_1': 4, 'TLSv1_2': 5} if args.sslprotocol not in v.keys(): print('Bad SSL Protocol: %s' % (args.sslprotocol)) parser.print_help(sys.stderr) sys.exit(1) s = Server(args.host, get_info=ALL, port=636, use_ssl=True, tls=Tls(validate=0, version=v[args.sslprotocol])) if args.referralhosts: s.allowed_referral_hosts = [('*', True)] log_info('Connecting to host...') c = Connection(s, user=args.user, password=args.password, authentication=authentication) log_info('Binding to host') # perform the Bind operation if not c.bind(): log_warn('Could not bind with specified credentials') log_warn(c.result) sys.exit(1) log_success('Bind OK') log_info('Starting domain dump') #Create domaindumper object dd = domainDumper(s, c, cnf) #Do the actual dumping dd.domainDump() log_success('Domain dump finished')