#!/usr/bin/env python #################### # # Copyright (c) 2019 Dirk-jan Mollema (@_dirkjan) # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. # # Tool to interact with ADIDNS over LDAP # #################### import sys import argparse import getpass import re import os import socket from struct import unpack, pack from impacket.structure import Structure from impacket.krb5.ccache import CCache from impacket.krb5.kerberosv5 import getKerberosTGT, getKerberosTGS from impacket.krb5.types import Principal from impacket.krb5 import constants from ldap3 import NTLM, Server, Connection, ALL, LEVEL, BASE, MODIFY_DELETE, MODIFY_ADD, MODIFY_REPLACE, SASL, KERBEROS from lib.utils.kerberos import ldap_kerberos import ldap3 from impacket.ldap import ldaptypes import dns.resolver import datetime def print_m(string): sys.stderr.write('\033[94m[-]\033[0m %s\n' % (string)) def print_o(string): sys.stderr.write('\033[92m[+]\033[0m %s\n' % (string)) def print_f(string): sys.stderr.write('\033[91m[!]\033[0m %s\n' % (string)) 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 [MS-DNSP] section 2.2.2.2.4.17 """ structure = ( ('ipv6Address', '16s'), ) 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', ' 0: print_m('Found %d domain DNS zones:' % len(zones)) for zone in zones: print(' %s' % zone) forestdns = 'CN=MicrosoftDNS,DC=ForestDnsZones,%s' % s.info.other['rootDomainNamingContext'][0] zones = get_dns_zones(c, forestdns) if len(zones) > 0: print_m('Found %d forest DNS zones:' % len(zones)) for zone in zones: print(' %s' % zone) return target = args.record if args.zone: zone = args.zone else: # Default to current domain zone = ldap2domain(domainroot) if not target: print_f('You need to specify a target record') return if target.lower().endswith(zone.lower()): target = target[:-(len(zone)+1)] searchtarget = 'DC=%s,%s' % (zone, dnsroot) # print s.info.naming_contexts c.search(searchtarget, '(&(objectClass=dnsNode)(name=%s))' % ldap3.utils.conv.escape_filter_chars(target), attributes=['dnsRecord','dNSTombstoned','name']) targetentry = None for entry in c.response: if entry['type'] != 'searchResEntry': continue targetentry = entry # Check if we have the required data if args.action in ['add', 'modify', 'remove'] and not args.data: print_f('This operation requires you to specify record data with --data') return # Check if we need the target record to exists, and if yes if it does if args.action in ['modify', 'remove', 'ldapdelete', 'resurrect', 'query'] and not targetentry: print_f('Target record not found!') return if args.action == 'query': print_o('Found record %s' % targetentry['attributes']['name']) for record in targetentry['raw_attributes']['dnsRecord']: dr = DNS_RECORD(record) # dr.dump() print(targetentry['dn']) print_record(dr, targetentry['attributes']['dNSTombstoned']) continue elif args.action == 'add': # Only A records for now addtype = 1 # Entry exists if targetentry: if not args.allow_multiple: for record in targetentry['raw_attributes']['dnsRecord']: dr = DNS_RECORD(record) if dr['Type'] == 1: address = DNS_RPC_RECORD_A(dr['Data']) print_f('Record already exists and points to %s. Use --action modify to overwrite or --allow-multiple to override this' % address.formatCanonical()) return False # If we are here, no A records exists yet record = new_record(addtype, get_next_serial(args.dns_ip, args.host, zone,args.tcp)) record['Data'] = DNS_RPC_RECORD_A() record['Data'].fromCanonical(args.data) print_m('Adding extra record') c.modify(targetentry['dn'], {'dnsRecord': [(MODIFY_ADD, record.getData())]}) print_operation_result(c.result) else: node_data = { # Schema is in the root domain (take if from schemaNamingContext to be sure) 'objectCategory': 'CN=Dns-Node,%s' % s.info.other['schemaNamingContext'][0], 'dNSTombstoned': False, 'name': target } record = new_record(addtype, get_next_serial(args.dns_ip, args.host, zone,args.tcp)) record['Data'] = DNS_RPC_RECORD_A() record['Data'].fromCanonical(args.data) record_dn = 'DC=%s,%s' % (target, searchtarget) node_data['dnsRecord'] = [record.getData()] print_m('Adding new record') c.add(record_dn, ['top', 'dnsNode'], node_data) print_operation_result(c.result) elif args.action == 'modify': # Only A records for now addtype = 1 # We already know the entry exists targetrecord = None records = [] for record in targetentry['raw_attributes']['dnsRecord']: dr = DNS_RECORD(record) if dr['Type'] == 1: targetrecord = dr else: records.append(record) if not targetrecord: print_f('No A record exists yet. Use --action add to add it') targetrecord['Serial'] = get_next_serial(args.dns_ip, args.host, zone,args.tcp) targetrecord['Data'] = DNS_RPC_RECORD_A() targetrecord['Data'].fromCanonical(args.data) records.append(targetrecord.getData()) print_m('Modifying record') c.modify(targetentry['dn'], {'dnsRecord': [(MODIFY_REPLACE, records)]}) print_operation_result(c.result) elif args.action == 'remove': addtype = 0 if len(targetentry['raw_attributes']['dnsRecord']) > 1: print_m('Target has multiple records, removing the one specified') targetrecord = None for record in targetentry['raw_attributes']['dnsRecord']: dr = DNS_RECORD(record) if dr['Type'] == 1: tr = DNS_RPC_RECORD_A(dr['Data']) if tr.formatCanonical() == args.data: targetrecord = record if not targetrecord: print_f('Could not find a record with the specified data') return c.modify(targetentry['dn'], {'dnsRecord': [(MODIFY_DELETE, targetrecord)]}) print_operation_result(c.result) else: print_m('Target has only one record, tombstoning it') diff = datetime.datetime.today() - datetime.datetime(1601,1,1) tstime = int(diff.total_seconds()*10000) # Add a null record record = new_record(addtype, get_next_serial(args.dns_ip, args.host, zone,args.tcp)) record['Data'] = DNS_RPC_RECORD_TS() record['Data']['entombedTime'] = tstime c.modify(targetentry['dn'], {'dnsRecord': [(MODIFY_REPLACE, [record.getData()])], 'dNSTombstoned': [(MODIFY_REPLACE, True)]}) print_operation_result(c.result) elif args.action == 'ldapdelete': print_m('Deleting record over LDAP') c.delete(targetentry['dn']) print_operation_result(c.result) elif args.action == 'resurrect': addtype = 0 if len(targetentry['raw_attributes']['dnsRecord']) > 1: print_m('Target has multiple records, I dont know how to handle this.') return else: print_m('Target has only one record, resurrecting it') diff = datetime.datetime.today() - datetime.datetime(1601,1,1) tstime = int(diff.total_seconds()*10000) # Add a null record record = new_record(addtype, get_next_serial(args.dns_ip, args.host, zone,args.tcp)) record['Data'] = DNS_RPC_RECORD_TS() record['Data']['entombedTime'] = tstime c.modify(targetentry['dn'], {'dnsRecord': [(MODIFY_REPLACE, [record.getData()])], 'dNSTombstoned': [(MODIFY_REPLACE, False)]}) print_o('Record resurrected. You will need to (re)add the record with the IP address.') if __name__ == '__main__': main()