diff --git a/windapsearch.py b/windapsearch.py index 67f4f90..3a285d8 100755 --- a/windapsearch.py +++ b/windapsearch.py @@ -7,12 +7,17 @@ import getpass import sys import re -import string +#import string +import binascii from datetime import datetime +try: + maketrans = ''.maketrans +except AttributeError: + from string import maketrans -TEXT_CHARACTERS = "".join(map(chr, range(32, 127)) + list("\n\r\t\b")) -_NULL_TRANS = string.maketrans("", "") +TEXT_CHARACTERS = "".join(list(map(chr, list(range(32, 127)))) + list("\n\r\t\b")) +_NULL_TRANS = maketrans("", "") FUNCTIONALITYLEVELS = { "0": "2000", @@ -45,7 +50,7 @@ class LDAPSearchResult(object): def __init__(self, entry_tuple): (dn, attrs) = entry_tuple if dn: - self.dn = dn + self.dn = dn else: return @@ -55,28 +60,33 @@ def get_attributes(self): return self.attrs def has_attribute(self, attr_name): - return self.attrs.has_key(attr_name) + return attr_name in self.attrs def get_attr_values(self, key): return self.attrs[key] def get_attr_names(self): - return self.attrs.keys() + return list(self.attrs.keys()) def get_dn(self): - return self.dn + return self.dn def pretty_print(self): - attrs = self.attrs.keys() + attrs = list(self.attrs.keys()) for attr in attrs: values = self.get_attr_values(attr) for value in values: - if self.isBinary(value): - value = value.encode('base64').rstrip() - print "{}: {}".format(attr, value) - + #print(value) + if not isinstance(value, str): + try: + value = value.decode() + except: + value = binascii.b2a_hex(value).decode() + #value = value.encode('base64').rstrip() + print("{}: {}".format(attr, value)) + def getCSVLine(self): - attrs = self.attrs.keys() + attrs = list(self.attrs.keys()) lineValues = [] for attr in attrs: values = self.get_attr_values(attr) @@ -90,8 +100,9 @@ def isBinary(self, attr): '''http://stackoverflow.com/questions/1446549/how-to-identify-binary-and-text-files-using-python''' if not attr: return False - if "\0" in attr: - return True + return True + #if "\0" in attr: + # return True t = attr.translate(_NULL_TRANS, TEXT_CHARACTERS) if float(len(t))/float(len(attr)) > 0.30: return True @@ -140,7 +151,7 @@ def getDC_IP(self, domain): try: dc_ip = socket.gethostbyname(domain) except: - print "[!] Unable to locate domain controller IP through host lookup. Please provide manually" + print("[!] Unable to locate domain controller IP through host lookup. Please provide manually") sys.exit(1) self.dc_ip = dc_ip @@ -152,13 +163,13 @@ def getDefaultNamingContext(self): newCon.simple_bind_s('','') res = newCon.search_s("", ldap.SCOPE_BASE, '(objectClass=*)') rootDSE = res[0][1] - except ldap.LDAPError, e: - print "[!] Error retrieving the root DSE" - print "[!] {}".format(e) + except ldap.LDAPError as e: + print("[!] Error retrieving the root DSE") + print("[!] {}".format(e)) sys.exit(1) - if not rootDSE.has_key('defaultNamingContext'): - print "[!] No defaultNamingContext found!" + if 'defaultNamingContext' not in rootDSE: + print("[!] No defaultNamingContext found!") sys.exit(1) defaultNamingContext = rootDSE['defaultNamingContext'][0] @@ -173,17 +184,17 @@ def do_bind(self): self.is_binded = True return True except ldap.INVALID_CREDENTIALS: - print "[!] Error: invalid credentials" + print("[!] Error: invalid credentials") sys.exit(1) - except ldap.LDAPError, e: - print "[!] {}".format(e) + except ldap.LDAPError as e: + print("[!] {}".format(e)) sys.exit(1) def whoami(self): try: current_dn = self.con.whoami_s() - except ldap.LDAPError, e: - print "[!] {}".format(e) + except ldap.LDAPError as e: + print("[!] {}".format(e)) sys.exit(1) return current_dn @@ -195,12 +206,14 @@ def do_ldap_query(self, base_dn, subtree, objectFilter, attrs, page_size=1000): found this script well after i'd written most of this one. oh well ''' more_pages = True - ldap_control = ldap.controls.SimplePagedResultsControl(True, size=page_size, cookie='') allResults = [] while more_pages: + #print(base_dn) + if not isinstance(base_dn, str): + base_dn = base_dn.decode() msgid = self.con.search_ext(base_dn, subtree, objectFilter, attrs, serverctrls=[ldap_control]) result_type, rawResults, message_id, server_controls = self.con.result3(msgid) @@ -245,9 +258,9 @@ def getFunctionalityLevel(self): # rawFunctionality = self.do_ldap_query('', ldap.SCOPE_BASE, objectFilter, attrs) rawData = self.con.search_s('', ldap.SCOPE_BASE, "(objectclass=*)", attrs) functionalityLevels = rawData[0][1] - except Error, e: - print "[!] Error retrieving functionality level" - print "[!] {}".format(e) + except Error as e: + print("[!] Error retrieving functionality level") + print("[!] {}".format(e)) sys.exit(1) return functionalityLevels @@ -260,9 +273,9 @@ def getAllUsers(self, attrs=''): base_dn = self.domainBase try: rawUsers = self.do_ldap_query(base_dn, ldap.SCOPE_SUBTREE, objectFilter, attrs) - except LDAPError, e: - print "[!] Error retrieving users" - print "[!] {}".format(e) + except LDAPError as e: + print("[!] Error retrieving users") + print("[!] {}".format(e)) sys.exit(1) return (self.get_search_results(rawUsers), attrs) @@ -276,9 +289,9 @@ def getAllGroups(self,attrs=''): base_dn = self.domainBase try: rawGroups = self.do_ldap_query(base_dn, ldap.SCOPE_SUBTREE, objectFilter, attrs) - except LDAPError, e: - print "[!] Error retrieving groups" - print "[!] {}".format(e) + except LDAPError as e: + print("[!] Error retrieving groups") + print("[!] {}".format(e)) sys.exit(1) return (self.get_search_results(rawGroups), attrs) @@ -293,9 +306,9 @@ def doFuzzySearch(self, searchTerm, objectCategory=''): base_dn = self.domainBase try: rawResults = self.do_ldap_query(base_dn, ldap.SCOPE_SUBTREE, objectFilter, attrs) - except LDAPError, e: - print "[!] Error retrieving results" - print "[!] {}".format(e) + except LDAPError as e: + print("[!] Error retrieving results") + print("[!] {}".format(e)) sys.exit(1) return self.get_search_results(rawResults) @@ -303,7 +316,7 @@ def doFuzzySearch(self, searchTerm, objectCategory=''): def doCustomSearch(self, base, objectFilter, attrs): try: rawResults = self.do_ldap_query(base, ldap.SCOPE_SUBTREE, objectFilter, attrs) - except LDAPError, e: + except LDAPError as e: "print [!] Error doing search" "print [!] {}".format(e) sys.exit(1) @@ -353,9 +366,9 @@ def getAllComputers(self, attrs=''): try: rawComputers = self.do_ldap_query(base_dn, ldap.SCOPE_SUBTREE, objectFilter, attrs) - except LDAPError, e: - print "[!] Error retrieving computers" - print "[!] {}".format(e) + except LDAPError as e: + print("[!] Error retrieving computers") + print("[!] {}".format(e)) sys.exit(1) return (self.get_search_results(rawComputers), attrs) @@ -373,7 +386,7 @@ def getComputerDict(self, computerResults, ipLookup=False): for attr in computer.get_attr_names(): computerInfo[attr] = ','.join(computer.get_attr_values(attr)) - if computerInfo.has_key('dNSHostName'): + if 'dNSHostName' in computerInfo: hostname = computerInfo['dNSHostName'] else: hostname = computerInfo['cn']+self.domain @@ -394,9 +407,9 @@ def getAdminObjects(self, attrs=''): base_dn = self.domainBase try: rawAdminResults = self.do_ldap_query(base_dn, ldap.SCOPE_SUBTREE, objectFilter, attrs) - except LDAPError, e: - print "[!] Error retrieving admin objects" - print "[!] {}".format(e) + except LDAPError as e: + print("[!] Error retrieving admin objects") + print("[!] {}".format(e)) sys.exit(1) return (self.get_search_results(rawAdminResults), attrs) @@ -407,9 +420,9 @@ def getSPNs(self, attrs=''): base_dn = self.domainBase try: rawSpnResults = self.do_ldap_query(base_dn, ldap.SCOPE_SUBTREE, objectFilter, attrs) - except LDAPError, e: - print "[!] Error retrieving SPNs" - print "[!] {}".format(e) + except LDAPError as e: + print("[!] Error retrieving SPNs") + print("[!] {}".format(e)) sys.exit(1) return (self.get_search_results(rawSpnResults), attrs) @@ -420,9 +433,9 @@ def getUnconstrainedUsers(self, attrs=''): base_dn = self.domainBase try: rawUnconstrainedUsers = self.do_ldap_query(base_dn, ldap.SCOPE_SUBTREE, objectFilter, attrs) - except LDAPError, e: - print "[!] Error retrieving unconstrained users" - print "[!] {}".format(e) + except LDAPError as e: + print("[!] Error retrieving unconstrained users") + print("[!] {}".format(e)) sys.exit(1) return (self.get_search_results(rawUnconstrainedUsers), attrs) @@ -433,9 +446,9 @@ def getUnconstrainedComputers(self, attrs=''): base_dn = self.domainBase try: rawUnconstrainedComputers = self.do_ldap_query(base_dn, ldap.SCOPE_SUBTREE, objectFilter, attrs) - except LDAPError, e: - print "[!] Error retrieving unconstrained computers" - print "[!] {}".format(e) + except LDAPError as e: + print("[!] Error retrieving unconstrained computers") + print("[!] {}".format(e)) sys.exit(1) return (self.get_search_results(rawUnconstrainedComputers), attrs) @@ -446,9 +459,9 @@ def getGPOs(self, attrs=''): base_dn = self.domainBase try: rawGPOs = self.do_ldap_query(base_dn, ldap.SCOPE_SUBTREE, objectFilter, attrs) - except LDAPError, e: - print "[!] Error retrieving GPOs" - print "[!] {}".format(e) + except LDAPError as e: + print("[!] Error retrieving GPOs") + print("[!] {}".format(e)) sys.exit(1) return (self.get_search_results(rawGPOs), attrs) @@ -459,9 +472,9 @@ def doCustomFilterSearch(self, customFilter, attrs=''): base_dn = self.domainBase try: rawResults = self.do_ldap_query(base_dn, ldap.SCOPE_SUBTREE, objectFilter, attrs) - except LDAPError, e: - print "[!] Error retrieving results with custom filter" - print "[!] {}".format(e) + except LDAPError as e: + print("[!] Error retrieving results with custom filter") + print("[!] {}".format(e)) sys.exit(1) return (self.get_search_results(rawResults), attrs) @@ -469,9 +482,9 @@ def doCustomFilterSearch(self, customFilter, attrs=''): def prettyPrintResults(results, showDN=False): for result in results: if showDN: - print result.dn + print(result.dn) result.pretty_print() - print "" + print("") def prettyPrintDictionary(results, attrs=None, separator=","): @@ -480,7 +493,7 @@ def prettyPrintDictionary(results, attrs=None, separator=","): common_attrs = ['cn', 'IP', 'dNSHostName', 'userPrincipalName', 'operatingSystem', 'operatingSystemVersion', 'operatingSystemServicePack'] attrs = [] - for dn, computer in results.iteritems(): + for dn, computer in results.items(): for key in computer: keys.add(key) @@ -490,16 +503,16 @@ def prettyPrintDictionary(results, attrs=None, separator=","): keys.remove(attr) for attr in keys: attrs.append(attr) - print ", ".join(attrs) + print(", ".join(attrs)) - for dn, computer in results.iteritems(): + for dn, computer in results.items(): line = [] for attr in attrs: - if computer.has_key(attr): + if attr in computer: line.append(computer[attr]) else: line.append(' ') - print separator.join(line) + print(separator.join(line)) def writeResults(results, attrs, filename): titleRow = '\t'.join(attrs) @@ -507,12 +520,12 @@ def writeResults(results, attrs, filename): fd.write(titleRow+'\n') for result in results: fd.write(result.getCSVLine()+'\n') - print ("[*] {} written").format(filename) + print(("[*] {} written").format(filename)) def printFunctionalityLevels(levels): - for name, level in levels.items(): - print "[+]\t {}: {}".format(name, FUNCTIONALITYLEVELS[level[0]]) + for name, level in list(levels.items()): + print("[+]\t {}: {}".format(name, FUNCTIONALITYLEVELS[level[0]])) def run(args): @@ -521,7 +534,7 @@ def run(args): if not args.username: username = '' password = '' - print "[+] No username provided. Will try anonymous bind." + print("[+] No username provided. Will try anonymous bind.") else: username = args.username @@ -531,26 +544,26 @@ def run(args): password = args.password if not args.dc_ip: - print "[+] No DC IP provided. Will try to discover via DNS lookup." + print("[+] No DC IP provided. Will try to discover via DNS lookup.") ldapSession = LDAPSession(dc_ip=args.dc_ip, username=username, password=password, domain=args.domain) - print "[+] Using Domain Controller at: {}".format(ldapSession.dc_ip) + print("[+] Using Domain Controller at: {}".format(ldapSession.dc_ip)) - print "[+] Getting defaultNamingContext from Root DSE" - print "[+]\tFound: " + ldapSession.getDefaultNamingContext() + print("[+] Getting defaultNamingContext from Root DSE") + print("[+]\tFound: {}".format(ldapSession.getDefaultNamingContext())) if args.functionality: levels = ldapSession.getFunctionalityLevel() - print "[+] Functionality Levels:" + print("[+] Functionality Levels:") printFunctionalityLevels(levels) - print "[+] Attempting bind" + print("[+] Attempting bind") ldapSession.do_bind() if ldapSession.is_binded: - print "[+]\t...success! Binded as: " - print "[+]\t {}".format(ldapSession.whoami()) + print("[+]\t...success! Binded as: ") + print("[+]\t {}".format(ldapSession.whoami())) attrs = '' @@ -560,45 +573,45 @@ def run(args): attrs = args.attrs.split(',') if args.groups: - print "\n[+] Enumerating all AD groups" + print("\n[+] Enumerating all AD groups") allGroups, searchAttrs = ldapSession.getAllGroups(attrs=attrs) if not allGroups: bye(ldapSession) - print "[+]\tFound {} groups: \n".format(len(allGroups)) + print("[+]\tFound {} groups: \n".format(len(allGroups))) prettyPrintResults(allGroups) if args.output_dir: filename = "{}/{}-groups.tsv".format(args.output_dir, startTime) writeResults(allGroups, searchAttrs, filename) if args.users: - print "\n[+] Enumerating all AD users" + print("\n[+] Enumerating all AD users") allUsers, searchAttrs = ldapSession.getAllUsers(attrs=attrs) if not allUsers: bye(ldapSession) - print "[+]\tFound {} users: \n".format(len(allUsers)) + print("[+]\tFound {} users: \n".format(len(allUsers))) prettyPrintResults(allUsers) if args.output_dir: filename = "{}/{}-users.tsv".format(args.output_dir, startTime) writeResults(allUsers, searchAttrs, filename) if args.privileged_users: - print "[+] Attempting to enumerate all AD privileged users" + print("[+] Attempting to enumerate all AD privileged users") for group in BUILTIN_PRIVILEGED_GROUPS: daDN = "CN={},CN=Users,{}".format(group,ldapSession.domainBase) - print "[+] Using DN: {}".format(daDN) + print("[+] Using DN: {}".format(daDN)) domainAdminResults, searchAttrs = ldapSession.getNestedGroupMemberships(daDN, attrs=attrs) - print "[+]\tFound {} nested users for group {}:\n".format(len(domainAdminResults),group) + print("[+]\tFound {} nested users for group {}:\n".format(len(domainAdminResults),group)) prettyPrintResults(domainAdminResults) if args.output_dir: filename = "{}/{}-{}-users.tsv".format(args.output_dir, startTime, group.replace(" ","_")) writeResults(domainAdminResults, searchAttrs, filename) if args.computers: - print "\n[+] Enumerating all AD computers" + print("\n[+] Enumerating all AD computers") allComputers, searchAttrs = ldapSession.getAllComputers(attrs=attrs) if not allComputers: bye(ldapSession) - print "[+]\tFound {} computers: \n".format(len(allComputers)) + print("[+]\tFound {} computers: \n".format(len(allComputers))) if not args.resolve: prettyPrintResults(allComputers) else: @@ -610,114 +623,114 @@ def run(args): if args.group_name: if not isValidDN(args.group_name): - print "[+] Attempting to enumerate full DN for group: {}".format(args.group_name) + print("[+] Attempting to enumerate full DN for group: {}".format(args.group_name)) searchResults = ldapSession.doFuzzySearch(args.group_name) if not searchResults: - print "[!] Couldn't find any DNs matching {}".format(args.group_name) + print("[!] Couldn't find any DNs matching {}".format(args.group_name)) bye(ldapSession) elif len(searchResults) == 1: groupDN = searchResults[0].dn - print "[+]\t Using DN: {}\n".format(groupDN) + print("[+]\t Using DN: {}\n".format(groupDN)) elif len(searchResults) > 1: groupDN = selectResult(searchResults).dn else: groupDN = args.group_name - print "[+]\t Using DN: {}\n".format(groupDN) + print("[+]\t Using DN: {}\n".format(groupDN)) groupMembers = ldapSession.queryGroupMembership(groupDN) if not groupMembers: - print "[!] Found 0 results" + print("[!] Found 0 results") else: - print "[+]\t Found {} members:\n".format(len(groupMembers)) - for member in groupMembers: print member + print("[+]\t Found {} members:\n".format(len(groupMembers))) + for member in groupMembers: print(member) if args.da: - print "[+] Attempting to enumerate all Domain Admins" + print("[+] Attempting to enumerate all Domain Admins") daDN = "CN=Domain Admins,CN=Users,{}".format(ldapSession.domainBase) - print "[+] Using DN: CN=Domain Admins,CN=Users.{}".format(daDN) + print("[+] Using DN: CN=Domain Admins,CN=Users.{}".format(daDN)) domainAdminResults, searchAttrs = ldapSession.getNestedGroupMemberships(daDN, attrs=attrs) - print "[+]\tFound {} Domain Admins:\n".format(len(domainAdminResults)) + print("[+]\tFound {} Domain Admins:\n".format(len(domainAdminResults))) prettyPrintResults(domainAdminResults) if args.output_dir: filename = "{}/{}-domainadmins.tsv".format(args.output_dir, startTime) writeResults(domainAdminResults, searchAttrs, filename) if args.admin_objects: - print "[+] Attempting to enumerate all admin (protected) objects" + print("[+] Attempting to enumerate all admin (protected) objects") adminResults, searchAttrs = ldapSession.getAdminObjects(attrs=attrs) - print "[+]\tFound {} Admin Objects:\n".format(len(adminResults)) + print("[+]\tFound {} Admin Objects:\n".format(len(adminResults))) prettyPrintResults(adminResults, showDN=True) if args.output_dir: filename = "{}/{}-adminobjects.tsv".format(args.output_dir, startTime) writeResults(adminResults, searchAttrs, filename) if args.spns: - print "[+] Attempting to enumerate all User objects with SPNs" + print("[+] Attempting to enumerate all User objects with SPNs") spnResults, searchAttrs = ldapSession.getSPNs(attrs=attrs) - print "[+]\tFound {} Users with SPNs:\n".format(len(spnResults)) + print("[+]\tFound {} Users with SPNs:\n".format(len(spnResults))) prettyPrintResults(spnResults, showDN=True) if args.output_dir: filename = "{}/{}-spns.tsv".format(args.output_dir, startTime) writeResults(spnResults, searchAttrs, filename) if args.unconstrained_users: - print "[+] Attempting to enumerate all user objects with unconstrained delegation" + print("[+] Attempting to enumerate all user objects with unconstrained delegation") unconstrainedUserResults, searchAttrs = ldapSession.getUnconstrainedUsers(attrs=attrs) - print "[+]\tFound {} Users with unconstrained delegation:\n".format(len(unconstrainedUserResults)) + print("[+]\tFound {} Users with unconstrained delegation:\n".format(len(unconstrainedUserResults))) prettyPrintResults(unconstrainedUserResults, showDN=True) if args.output_dir: filename = "{}/{}-unconstrained-users.tsv".format(args.output_dir, startTime) writeResults(unconstrainedUserResults, searchAttrs, filename) if args.unconstrained_computers: - print "[+] Attempting to enumerate all computer objects with unconstrained delegation" + print("[+] Attempting to enumerate all computer objects with unconstrained delegation") unconstrainedComputerResults, searchAttrs = ldapSession.getUnconstrainedComputers(attrs=attrs) - print "[+]\tFound {} computers with unconstrained delegation:\n".format(len(unconstrainedComputerResults)) + print("[+]\tFound {} computers with unconstrained delegation:\n".format(len(unconstrainedComputerResults))) prettyPrintResults(unconstrainedComputerResults, showDN=True) if args.output_dir: filename = "{}/{}-unconstrained-computers.tsv".format(args.output_dir, startTime) writeResults(unconstrainedComputerResults, searchAttrs, filename) if args.gpos: - print "[+] Attempting to enumerate all group policy objects" + print("[+] Attempting to enumerate all group policy objects") gpoResults, searchAttrs = ldapSession.getGPOs(attrs=attrs) - print "[+]\tFound {} GPOs:\n".format(len(gpoResults)) + print("[+]\tFound {} GPOs:\n".format(len(gpoResults))) prettyPrintResults(gpoResults) if args.output_dir: filename = "{}/{}-gpos.tsv".format(args.output_dir, startTime) writeResults(gpoResults, searchAttrs, filename) if args.custom_filter: - print "[+] Performing custom lookup with filter: \"{}\"".format(args.custom_filter) + print("[+] Performing custom lookup with filter: \"{}\"".format(args.custom_filter)) customResults, searchAttrs = ldapSession.doCustomFilterSearch(args.custom_filter, attrs=attrs) - print "[+]\tFound {} results:\n".format(len(customResults)) + print("[+]\tFound {} results:\n".format(len(customResults))) prettyPrintResults(customResults, showDN=True) if args.output_dir: filename = "{}/{}-custom.tsv".format(args.output_dir, startTime) writeResults(customResults, searchAttrs, filename) if args.search_term: - print "[+] Doing fuzzy search for: \"{}\"".format(args.search_term) + print("[+] Doing fuzzy search for: \"{}\"".format(args.search_term)) searchResults = ldapSession.doFuzzySearch(args.search_term) - print "[+]\tFound {} results:\n".format(len(searchResults)) + print("[+]\tFound {} results:\n".format(len(searchResults))) for result in searchResults: - print result.dn + print(result.dn) if args.lookup: if not isValidDN(args.lookup): - print "[+] Searching for matching DNs for term: \"{}\"".format(args.lookup) + print("[+] Searching for matching DNs for term: \"{}\"".format(args.lookup)) searchResults = ldapSession.doFuzzySearch(args.lookup) if not searchResults: - print "[!] Couldn't find any DNs matching: \"{}\"".format(args.lookup) + print("[!] Couldn't find any DNs matching: \"{}\"".format(args.lookup)) bye(ldapSession) elif len(searchResults) == 1: lookupDN = searchResults[0].dn - print "[+]\t Using DN: {}\n".format(lookupDN) + print("[+]\t Using DN: {}\n".format(lookupDN)) elif len(searchResults) > 1: lookupDN = selectResult(searchResults).dn else: lookupDN = args.lookup - print "[+]\t Using DN: {}\n".format(lookupDN) + print("[+]\t Using DN: {}\n".format(lookupDN)) if not attrs: attrs = ['*'] lookupResults = ldapSession.doCustomSearch(lookupDN, objectFilter="(cn=*)", attrs=attrs) @@ -740,17 +753,17 @@ def isValidDN(testdn): def selectResult(results): - print "[+] Found {} results:\n".format(len(results)) + print("[+] Found {} results:\n".format(len(results))) for number, result in enumerate(results): - print "{}: {}".format(number, result.dn) - print "" - response = raw_input("Which DN do you want to use? : ") + print("{}: {}".format(number, result.dn)) + print("") + response = input("Which DN do you want to use? : ") return results[int(response)] def bye(ldapSession): ldapSession.unbind() - print "\n[*] Bye!" + print("\n[*] Bye!") sys.exit(1) @@ -799,7 +812,7 @@ def bye(ldapSession): args = parser.parse_args() if not (args.domain or args.dc_ip): - print "[!] You must specify either a domain or the IP address of a domain controller" + print("[!] You must specify either a domain or the IP address of a domain controller") sys.exit(1) run(args) diff --git a/windapsearch_p2.py b/windapsearch_p2.py new file mode 100755 index 0000000..67f4f90 --- /dev/null +++ b/windapsearch_p2.py @@ -0,0 +1,817 @@ +#!/usr/bin/env python + +import ldap +from ldap import LDAPError +from ldap.cidict import cidict +import argparse +import getpass +import sys +import re +import string +from datetime import datetime + + +TEXT_CHARACTERS = "".join(map(chr, range(32, 127)) + list("\n\r\t\b")) +_NULL_TRANS = string.maketrans("", "") + +FUNCTIONALITYLEVELS = { + "0": "2000", + "1": "2003 Interim", + "2": "2003", + "3": "2008", + "4": "2008 R2", + "5": "2012", + "6": "2012 R2", + "7": "2016" +} + +# Privileged builtin AD groups relevant to look for +BUILTIN_PRIVILEGED_GROUPS = [ + "Administrators", #Builtin administrators group for the domain + "Domain Admins", # '' + "Enterprise Admins", # '' + "Schema Admins", #Highly privileged builtin group + "Account Operators", # '' + "Backup Operators" # '' +] + +class LDAPSearchResult(object): + """A helper class to work with raw search results + Copied from here: https://www.packtpub.com/books/content/configuring-and-securing-python-ldap-applications-part-2 + """ + + dn = '' + + def __init__(self, entry_tuple): + (dn, attrs) = entry_tuple + if dn: + self.dn = dn + else: + return + + self.attrs = cidict(attrs) + + def get_attributes(self): + return self.attrs + + def has_attribute(self, attr_name): + return self.attrs.has_key(attr_name) + + def get_attr_values(self, key): + return self.attrs[key] + + def get_attr_names(self): + return self.attrs.keys() + + def get_dn(self): + return self.dn + + def pretty_print(self): + attrs = self.attrs.keys() + for attr in attrs: + values = self.get_attr_values(attr) + for value in values: + if self.isBinary(value): + value = value.encode('base64').rstrip() + print "{}: {}".format(attr, value) + + def getCSVLine(self): + attrs = self.attrs.keys() + lineValues = [] + for attr in attrs: + values = self.get_attr_values(attr) + for value in values: + if self.isBinary(value): + value = value.encode('base64').rstrip() + lineValues.append(value) + return "\t".join(lineValues) + + def isBinary(self, attr): + '''http://stackoverflow.com/questions/1446549/how-to-identify-binary-and-text-files-using-python''' + if not attr: + return False + if "\0" in attr: + return True + t = attr.translate(_NULL_TRANS, TEXT_CHARACTERS) + if float(len(t))/float(len(attr)) > 0.30: + return True + return False + + + + + +class LDAPSession(object): + def __init__(self, dc_ip='', username='', password='', domain=''): + + if dc_ip: + self.dc_ip = dc_ip + else: + self.getDC_IP(domain) + + self.username = username + self.password = password + self.domain = domain + + self.con = self.initializeConnection() + self.domainBase = '' + self.is_binded = False + + + def initializeConnection(self): + if not self.dc_ip: + self.dc_ip = self.getDC_IP(self.domain) + + con = ldap.initialize('ldap://{}'.format(self.dc_ip)) + con.set_option(ldap.OPT_REFERRALS, 0) + return con + + def unbind(self): + self.con.unbind() + self.is_binded = False + + def getDC_IP(self, domain): + ''' + if domain is provided, do a _ldap._tcp.domain to try and find DC, or maybe a "host -av domain" eventually ? + if no domain is provided, do a multicast and hope it's in the search domain + if can't find anything, return error and require dc_ip set manually + ''' + import socket + try: + dc_ip = socket.gethostbyname(domain) + except: + print "[!] Unable to locate domain controller IP through host lookup. Please provide manually" + sys.exit(1) + + self.dc_ip = dc_ip + + + def getDefaultNamingContext(self): + try: + newCon = ldap.initialize('ldap://{}'.format(self.dc_ip)) + newCon.simple_bind_s('','') + res = newCon.search_s("", ldap.SCOPE_BASE, '(objectClass=*)') + rootDSE = res[0][1] + except ldap.LDAPError, e: + print "[!] Error retrieving the root DSE" + print "[!] {}".format(e) + sys.exit(1) + + if not rootDSE.has_key('defaultNamingContext'): + print "[!] No defaultNamingContext found!" + sys.exit(1) + + defaultNamingContext = rootDSE['defaultNamingContext'][0] + self.domainBase = defaultNamingContext + newCon.unbind() + return defaultNamingContext + + + def do_bind(self): + try: + self.con.simple_bind_s(self.username, self.password) + self.is_binded = True + return True + except ldap.INVALID_CREDENTIALS: + print "[!] Error: invalid credentials" + sys.exit(1) + except ldap.LDAPError, e: + print "[!] {}".format(e) + sys.exit(1) + + def whoami(self): + try: + current_dn = self.con.whoami_s() + except ldap.LDAPError, e: + print "[!] {}".format(e) + sys.exit(1) + + return current_dn + + def do_ldap_query(self, base_dn, subtree, objectFilter, attrs, page_size=1000): + ''' + actually perform the ldap query, with paging + copied from another LDAP search script I found: https://github.com/CroweCybersecurity/ad-ldap-enum + found this script well after i'd written most of this one. oh well + ''' + more_pages = True + + ldap_control = ldap.controls.SimplePagedResultsControl(True, size=page_size, cookie='') + + allResults = [] + + while more_pages: + msgid = self.con.search_ext(base_dn, subtree, objectFilter, attrs, serverctrls=[ldap_control]) + result_type, rawResults, message_id, server_controls = self.con.result3(msgid) + + allResults += rawResults + + # Get the page control and get the cookie from the control. + page_controls = [c for c in server_controls if c.controlType == ldap.controls.SimplePagedResultsControl.controlType] + + if page_controls: + cookie = page_controls[0].cookie + + if not cookie: + more_pages = False + else: + ldap_control.cookie = cookie + + return allResults + + + def get_search_results(self, results): + '''takes raw results and returns a list of helper objects''' + res = [] + if type(results) == tuple and len(results) == 2: + (code, arr) = results + elif type(results) == list: + arr = results + + if len(results) == 0: + return res + + for item in arr: + resitem = LDAPSearchResult(item) + if resitem.dn: #hack to workaround "blank" results + res.append(resitem) + + return res + + def getFunctionalityLevel(self): + objectFilter = '(objectclass=*)' + attrs = ['domainFunctionality', 'forestFunctionality', 'domainControllerFunctionality'] + try: + # rawFunctionality = self.do_ldap_query('', ldap.SCOPE_BASE, objectFilter, attrs) + rawData = self.con.search_s('', ldap.SCOPE_BASE, "(objectclass=*)", attrs) + functionalityLevels = rawData[0][1] + except Error, e: + print "[!] Error retrieving functionality level" + print "[!] {}".format(e) + sys.exit(1) + + return functionalityLevels + + def getAllUsers(self, attrs=''): + if not attrs: + attrs = ['cn', 'userPrincipalName'] + + objectFilter = '(objectCategory=user)' + base_dn = self.domainBase + try: + rawUsers = self.do_ldap_query(base_dn, ldap.SCOPE_SUBTREE, objectFilter, attrs) + except LDAPError, e: + print "[!] Error retrieving users" + print "[!] {}".format(e) + sys.exit(1) + + return (self.get_search_results(rawUsers), attrs) + + + def getAllGroups(self,attrs=''): + if not attrs: + attrs = ['distinguishedName', 'cn'] + + objectFilter = '(objectCategory=group)' + base_dn = self.domainBase + try: + rawGroups = self.do_ldap_query(base_dn, ldap.SCOPE_SUBTREE, objectFilter, attrs) + except LDAPError, e: + print "[!] Error retrieving groups" + print "[!] {}".format(e) + sys.exit(1) + + return (self.get_search_results(rawGroups), attrs) + + + def doFuzzySearch(self, searchTerm, objectCategory=''): + if objectCategory: + objectFilter = '(&(objectCategory={})(anr={}))'.format(objectCategory, searchTerm) + else: + objectFilter = '(anr={})'.format(searchTerm) + attrs = ['dn'] + base_dn = self.domainBase + try: + rawResults = self.do_ldap_query(base_dn, ldap.SCOPE_SUBTREE, objectFilter, attrs) + except LDAPError, e: + print "[!] Error retrieving results" + print "[!] {}".format(e) + sys.exit(1) + return self.get_search_results(rawResults) + + + def doCustomSearch(self, base, objectFilter, attrs): + try: + rawResults = self.do_ldap_query(base, ldap.SCOPE_SUBTREE, objectFilter, attrs) + except LDAPError, e: + "print [!] Error doing search" + "print [!] {}".format(e) + sys.exit(1) + + return self.get_search_results(rawResults) + + + def queryGroupMembership(self, groupDN, getUPNs=False): + objectFilter = '(objectCategory=group)' + attrs = ['member'] + results = self.doCustomSearch(groupDN, objectFilter, attrs) + if not results: + return False + members = [] + for result in results: + if not result.has_attribute('member'): + break + members = members + result.get_attr_values('member') + if getUPNs: + membernames = {} + for member in members: + upnresult = self.doCustomSearch(member, '(objectCategory=user)', ['userPrincipalName']) + upn = upnresult[0].get_attr_values('userPrincipalName') if upnresult[0].has_attribute('userPrincipalName') else '' + membernames[member] = upn + return membernames + else: + return members + + def getNestedGroupMemberships(self, groupDN, attrs=''): + '''see here for more details: + https://labs.mwrinfosecurity.com/blog/active-directory-users-in-nested-groups-reconnaissance/ + ''' + objectFilter = "(&(objectClass=user)(memberof:1.2.840.113556.1.4.1941:={}))".format(groupDN) + if not attrs: + attrs = ['cn', 'userPrincipalName'] + base_dn = self.domainBase + results = self.doCustomSearch(base_dn, objectFilter, attrs) + return (results, attrs) + + + def getAllComputers(self, attrs=''): + if not attrs: + attrs = ['cn', 'dNSHostName', 'operatingSystem', 'operatingSystemVersion', 'operatingSystemServicePack'] + + objectFilter = '(objectClass=Computer)' + base_dn = self.domainBase + + try: + rawComputers = self.do_ldap_query(base_dn, ldap.SCOPE_SUBTREE, objectFilter, attrs) + except LDAPError, e: + print "[!] Error retrieving computers" + print "[!] {}".format(e) + sys.exit(1) + + return (self.get_search_results(rawComputers), attrs) + + + def getComputerDict(self, computerResults, ipLookup=False): + '''returns dict object of computers and attributes + if iplookup speficied will add IP addresses through simple host lookup + returns dictionary of computers in the domain with DN as key''' + import socket + computersDict = {} + for computer in computerResults: + computerInfo = {} + dn = computer.dn + for attr in computer.get_attr_names(): + computerInfo[attr] = ','.join(computer.get_attr_values(attr)) + + if computerInfo.has_key('dNSHostName'): + hostname = computerInfo['dNSHostName'] + else: + hostname = computerInfo['cn']+self.domain + + try: + computerInfo['IP'] = socket.gethostbyname(hostname) + except: + computerInfo['IP'] = "" + + computersDict[dn] = computerInfo + + return computersDict + + def getAdminObjects(self, attrs=''): + if not attrs: + attrs = ['dn'] + objectFilter = 'adminCount=1' + base_dn = self.domainBase + try: + rawAdminResults = self.do_ldap_query(base_dn, ldap.SCOPE_SUBTREE, objectFilter, attrs) + except LDAPError, e: + print "[!] Error retrieving admin objects" + print "[!] {}".format(e) + sys.exit(1) + return (self.get_search_results(rawAdminResults), attrs) + + def getSPNs(self, attrs=''): + if not attrs: + attrs = ['dn'] + objectFilter= "(&(&(servicePrincipalName=*)(UserAccountControl:1.2.840.113556.1.4.803:=512))(!(UserAccountControl:1.2.840.113556.1.4.803:=2)))" + base_dn = self.domainBase + try: + rawSpnResults = self.do_ldap_query(base_dn, ldap.SCOPE_SUBTREE, objectFilter, attrs) + except LDAPError, e: + print "[!] Error retrieving SPNs" + print "[!] {}".format(e) + sys.exit(1) + return (self.get_search_results(rawSpnResults), attrs) + + def getUnconstrainedUsers(self, attrs=''): + if not attrs: + attrs = ['dn', 'userPrincipalName'] + objectFilter = "(&(&(objectCategory=person)(objectClass=user))(userAccountControl:1.2.840.113556.1.4.803:=524288))" + base_dn = self.domainBase + try: + rawUnconstrainedUsers = self.do_ldap_query(base_dn, ldap.SCOPE_SUBTREE, objectFilter, attrs) + except LDAPError, e: + print "[!] Error retrieving unconstrained users" + print "[!] {}".format(e) + sys.exit(1) + return (self.get_search_results(rawUnconstrainedUsers), attrs) + + def getUnconstrainedComputers(self, attrs=''): + if not attrs: + attrs = ['dn', 'dNSHostName'] + objectFilter = "(&(objectCategory=computer)(objectClass=computer)(userAccountControl:1.2.840.113556.1.4.803:=524288))" + base_dn = self.domainBase + try: + rawUnconstrainedComputers = self.do_ldap_query(base_dn, ldap.SCOPE_SUBTREE, objectFilter, attrs) + except LDAPError, e: + print "[!] Error retrieving unconstrained computers" + print "[!] {}".format(e) + sys.exit(1) + return (self.get_search_results(rawUnconstrainedComputers), attrs) + + def getGPOs(self, attrs=''): + if not attrs: + attrs = ['displayName', 'gPCFileSysPath'] + objectFilter = "objectClass=groupPolicyContainer" + base_dn = self.domainBase + try: + rawGPOs = self.do_ldap_query(base_dn, ldap.SCOPE_SUBTREE, objectFilter, attrs) + except LDAPError, e: + print "[!] Error retrieving GPOs" + print "[!] {}".format(e) + sys.exit(1) + return (self.get_search_results(rawGPOs), attrs) + + def doCustomFilterSearch(self, customFilter, attrs=''): + if not attrs: + attrs = ['dn'] + objectFilter = customFilter + base_dn = self.domainBase + try: + rawResults = self.do_ldap_query(base_dn, ldap.SCOPE_SUBTREE, objectFilter, attrs) + except LDAPError, e: + print "[!] Error retrieving results with custom filter" + print "[!] {}".format(e) + sys.exit(1) + return (self.get_search_results(rawResults), attrs) + + +def prettyPrintResults(results, showDN=False): + for result in results: + if showDN: + print result.dn + result.pretty_print() + print "" + + +def prettyPrintDictionary(results, attrs=None, separator=","): + '''helper function to pretty print a dictionary of dictionaries, like the one returned in getComputerDict''' + keys = set() + common_attrs = ['cn', 'IP', 'dNSHostName', 'userPrincipalName', 'operatingSystem', 'operatingSystemVersion', 'operatingSystemServicePack'] + attrs = [] + + for dn, computer in results.iteritems(): + for key in computer: + keys.add(key) + + for attr in common_attrs: + if attr in keys: + attrs.append(attr) + keys.remove(attr) + for attr in keys: + attrs.append(attr) + print ", ".join(attrs) + + for dn, computer in results.iteritems(): + line = [] + for attr in attrs: + if computer.has_key(attr): + line.append(computer[attr]) + else: + line.append(' ') + print separator.join(line) + +def writeResults(results, attrs, filename): + titleRow = '\t'.join(attrs) + with open(filename, 'w') as fd: + fd.write(titleRow+'\n') + for result in results: + fd.write(result.getCSVLine()+'\n') + print ("[*] {} written").format(filename) + + +def printFunctionalityLevels(levels): + for name, level in levels.items(): + print "[+]\t {}: {}".format(name, FUNCTIONALITYLEVELS[level[0]]) + + +def run(args): + + startTime = datetime.now().strftime("%Y%m%d-%H:%M:%S") + if not args.username: + username = '' + password = '' + print "[+] No username provided. Will try anonymous bind." + else: + username = args.username + + if args.username and not args.password: + password = getpass.getpass("Password for {}: ".format(args.username)) + elif args.password: + password = args.password + + if not args.dc_ip: + print "[+] No DC IP provided. Will try to discover via DNS lookup." + + ldapSession = LDAPSession(dc_ip=args.dc_ip, username=username, password=password, domain=args.domain) + + + print "[+] Using Domain Controller at: {}".format(ldapSession.dc_ip) + + print "[+] Getting defaultNamingContext from Root DSE" + print "[+]\tFound: " + ldapSession.getDefaultNamingContext() + if args.functionality: + levels = ldapSession.getFunctionalityLevel() + print "[+] Functionality Levels:" + printFunctionalityLevels(levels) + + print "[+] Attempting bind" + ldapSession.do_bind() + + if ldapSession.is_binded: + print "[+]\t...success! Binded as: " + print "[+]\t {}".format(ldapSession.whoami()) + + attrs = '' + + if args.full: + attrs = ['*'] + elif args.attrs: + attrs = args.attrs.split(',') + + if args.groups: + print "\n[+] Enumerating all AD groups" + allGroups, searchAttrs = ldapSession.getAllGroups(attrs=attrs) + if not allGroups: + bye(ldapSession) + print "[+]\tFound {} groups: \n".format(len(allGroups)) + prettyPrintResults(allGroups) + if args.output_dir: + filename = "{}/{}-groups.tsv".format(args.output_dir, startTime) + writeResults(allGroups, searchAttrs, filename) + + if args.users: + print "\n[+] Enumerating all AD users" + allUsers, searchAttrs = ldapSession.getAllUsers(attrs=attrs) + if not allUsers: + bye(ldapSession) + print "[+]\tFound {} users: \n".format(len(allUsers)) + prettyPrintResults(allUsers) + if args.output_dir: + filename = "{}/{}-users.tsv".format(args.output_dir, startTime) + writeResults(allUsers, searchAttrs, filename) + + if args.privileged_users: + print "[+] Attempting to enumerate all AD privileged users" + for group in BUILTIN_PRIVILEGED_GROUPS: + daDN = "CN={},CN=Users,{}".format(group,ldapSession.domainBase) + print "[+] Using DN: {}".format(daDN) + domainAdminResults, searchAttrs = ldapSession.getNestedGroupMemberships(daDN, attrs=attrs) + print "[+]\tFound {} nested users for group {}:\n".format(len(domainAdminResults),group) + prettyPrintResults(domainAdminResults) + if args.output_dir: + filename = "{}/{}-{}-users.tsv".format(args.output_dir, startTime, group.replace(" ","_")) + writeResults(domainAdminResults, searchAttrs, filename) + + if args.computers: + print "\n[+] Enumerating all AD computers" + allComputers, searchAttrs = ldapSession.getAllComputers(attrs=attrs) + if not allComputers: + bye(ldapSession) + print "[+]\tFound {} computers: \n".format(len(allComputers)) + if not args.resolve: + prettyPrintResults(allComputers) + else: + allComputersDict = ldapSession.getComputerDict(allComputers, ipLookup=True) + prettyPrintDictionary(allComputersDict, attrs=searchAttrs) + if args.output_dir: + filename = "{}/{}-computers.tsv".format(args.output_dir, startTime) + writeResults(allComputers, searchAttrs, filename) + + if args.group_name: + if not isValidDN(args.group_name): + print "[+] Attempting to enumerate full DN for group: {}".format(args.group_name) + searchResults = ldapSession.doFuzzySearch(args.group_name) + if not searchResults: + print "[!] Couldn't find any DNs matching {}".format(args.group_name) + bye(ldapSession) + elif len(searchResults) == 1: + groupDN = searchResults[0].dn + print "[+]\t Using DN: {}\n".format(groupDN) + elif len(searchResults) > 1: + groupDN = selectResult(searchResults).dn + else: + groupDN = args.group_name + print "[+]\t Using DN: {}\n".format(groupDN) + + groupMembers = ldapSession.queryGroupMembership(groupDN) + if not groupMembers: + print "[!] Found 0 results" + else: + print "[+]\t Found {} members:\n".format(len(groupMembers)) + for member in groupMembers: print member + + if args.da: + print "[+] Attempting to enumerate all Domain Admins" + daDN = "CN=Domain Admins,CN=Users,{}".format(ldapSession.domainBase) + print "[+] Using DN: CN=Domain Admins,CN=Users.{}".format(daDN) + domainAdminResults, searchAttrs = ldapSession.getNestedGroupMemberships(daDN, attrs=attrs) + print "[+]\tFound {} Domain Admins:\n".format(len(domainAdminResults)) + prettyPrintResults(domainAdminResults) + if args.output_dir: + filename = "{}/{}-domainadmins.tsv".format(args.output_dir, startTime) + writeResults(domainAdminResults, searchAttrs, filename) + + if args.admin_objects: + print "[+] Attempting to enumerate all admin (protected) objects" + adminResults, searchAttrs = ldapSession.getAdminObjects(attrs=attrs) + print "[+]\tFound {} Admin Objects:\n".format(len(adminResults)) + prettyPrintResults(adminResults, showDN=True) + if args.output_dir: + filename = "{}/{}-adminobjects.tsv".format(args.output_dir, startTime) + writeResults(adminResults, searchAttrs, filename) + + if args.spns: + print "[+] Attempting to enumerate all User objects with SPNs" + spnResults, searchAttrs = ldapSession.getSPNs(attrs=attrs) + print "[+]\tFound {} Users with SPNs:\n".format(len(spnResults)) + prettyPrintResults(spnResults, showDN=True) + if args.output_dir: + filename = "{}/{}-spns.tsv".format(args.output_dir, startTime) + writeResults(spnResults, searchAttrs, filename) + + if args.unconstrained_users: + print "[+] Attempting to enumerate all user objects with unconstrained delegation" + unconstrainedUserResults, searchAttrs = ldapSession.getUnconstrainedUsers(attrs=attrs) + print "[+]\tFound {} Users with unconstrained delegation:\n".format(len(unconstrainedUserResults)) + prettyPrintResults(unconstrainedUserResults, showDN=True) + if args.output_dir: + filename = "{}/{}-unconstrained-users.tsv".format(args.output_dir, startTime) + writeResults(unconstrainedUserResults, searchAttrs, filename) + + if args.unconstrained_computers: + print "[+] Attempting to enumerate all computer objects with unconstrained delegation" + unconstrainedComputerResults, searchAttrs = ldapSession.getUnconstrainedComputers(attrs=attrs) + print "[+]\tFound {} computers with unconstrained delegation:\n".format(len(unconstrainedComputerResults)) + prettyPrintResults(unconstrainedComputerResults, showDN=True) + if args.output_dir: + filename = "{}/{}-unconstrained-computers.tsv".format(args.output_dir, startTime) + writeResults(unconstrainedComputerResults, searchAttrs, filename) + + if args.gpos: + print "[+] Attempting to enumerate all group policy objects" + gpoResults, searchAttrs = ldapSession.getGPOs(attrs=attrs) + print "[+]\tFound {} GPOs:\n".format(len(gpoResults)) + prettyPrintResults(gpoResults) + if args.output_dir: + filename = "{}/{}-gpos.tsv".format(args.output_dir, startTime) + writeResults(gpoResults, searchAttrs, filename) + + if args.custom_filter: + print "[+] Performing custom lookup with filter: \"{}\"".format(args.custom_filter) + customResults, searchAttrs = ldapSession.doCustomFilterSearch(args.custom_filter, attrs=attrs) + print "[+]\tFound {} results:\n".format(len(customResults)) + prettyPrintResults(customResults, showDN=True) + if args.output_dir: + filename = "{}/{}-custom.tsv".format(args.output_dir, startTime) + writeResults(customResults, searchAttrs, filename) + + if args.search_term: + print "[+] Doing fuzzy search for: \"{}\"".format(args.search_term) + searchResults = ldapSession.doFuzzySearch(args.search_term) + print "[+]\tFound {} results:\n".format(len(searchResults)) + for result in searchResults: + print result.dn + + if args.lookup: + if not isValidDN(args.lookup): + print "[+] Searching for matching DNs for term: \"{}\"".format(args.lookup) + searchResults = ldapSession.doFuzzySearch(args.lookup) + if not searchResults: + print "[!] Couldn't find any DNs matching: \"{}\"".format(args.lookup) + bye(ldapSession) + elif len(searchResults) == 1: + lookupDN = searchResults[0].dn + print "[+]\t Using DN: {}\n".format(lookupDN) + elif len(searchResults) > 1: + lookupDN = selectResult(searchResults).dn + else: + lookupDN = args.lookup + print "[+]\t Using DN: {}\n".format(lookupDN) + if not attrs: + attrs = ['*'] + lookupResults = ldapSession.doCustomSearch(lookupDN, objectFilter="(cn=*)", attrs=attrs) + prettyPrintResults(lookupResults) + if args.output_dir: + filename = "{}/{}-lookup.tsv".format(args.output_dir, startTime) + writeResults(lookupResults, searchAttrs, filename) + + bye(ldapSession) + + + +def isValidDN(testdn): + #super lazy regex way to see if what they entered is a DN + dnRegex = re.compile('(DC=[^,"]+)+') + if dnRegex.search(testdn): + return True + else: + return False + + +def selectResult(results): + print "[+] Found {} results:\n".format(len(results)) + for number, result in enumerate(results): + print "{}: {}".format(number, result.dn) + print "" + response = raw_input("Which DN do you want to use? : ") + return results[int(response)] + + +def bye(ldapSession): + ldapSession.unbind() + print "\n[*] Bye!" + sys.exit(1) + + + + +if __name__ == '__main__': + + parser = argparse.ArgumentParser(add_help = True, description = "Script to perform Windows domain enumeration through LDAP queries to a Domain Controller") + dgroup = parser.add_argument_group("Domain Options") + dgroup.add_argument("-d", "--domain", metavar="DOMAIN", dest='domain', type=str, help="The FQDN of the domain (e.g. 'lab.example.com'). Only needed if DC-IP not provided") + dgroup.add_argument("--dc-ip", metavar="DC_IP", dest='dc_ip', type=str, help="The IP address of a domain controller") + + bgroup = parser.add_argument_group("Bind Options", "Specify bind account. If not specified, anonymous bind will be attempted") + bgroup.add_argument("-u", "--user", metavar="USER", dest="username", type=str, help="The full username with domain to bind with (e.g. 'ropnop@lab.example.com' or 'LAB\\ropnop'") + bgroup.add_argument("-p", "--password", metavar="PASSWORD", dest="password", type=str, help="Password to use. If not specified, will be prompted for") + + egroup = parser.add_argument_group("Enumeration Options", "Data to enumerate from LDAP") + egroup.add_argument("--functionality", action="store_true", help="Enumerate Domain Functionality level. Possible through anonymous bind") + egroup.add_argument("-G", "--groups", action="store_true", help="Enumerate all AD Groups") + egroup.add_argument("-U", "--users", action="store_true", help="Enumerate all AD Users") + egroup.add_argument("-PU", "--privileged-users", dest="privileged_users", action="store_true", help="Enumerate All privileged AD Users. Performs recursive lookups for nested members.") + egroup.add_argument("-C", "--computers", action="store_true", help="Enumerate all AD Computers") + egroup.add_argument("-m", "--members", metavar="GROUP_NAME", dest="group_name", type=str, help="Enumerate all members of a group") + egroup.add_argument("--da", action="store_true", help="Shortcut for enumerate all members of group 'Domain Admins'. Performs recursive lookups for nested members.") + egroup.add_argument("--admin-objects", dest="admin_objects", action="store_true", help="Enumerate all objects with protected ACLs (i.e. admins)") + egroup.add_argument("--user-spns", dest="spns", action="store_true", help="Enumerate all users objects with Service Principal Names (for kerberoasting)") + egroup.add_argument("--unconstrained-users", dest="unconstrained_users", action="store_true", help="Enumerate all user objects with unconstrained delegation") + egroup.add_argument("--unconstrained-computers", dest="unconstrained_computers", action="store_true", help="Enumerate all computer objects with unconstrained delegation") + egroup.add_argument("--gpos", action="store_true", help="Enumerate Group Policy Objects") + egroup.add_argument("-s", "--search", metavar="SEARCH_TERM", dest="search_term", type=str, help="Fuzzy search for all matching LDAP entries") + egroup.add_argument("-l", "--lookup", metavar="DN", dest="lookup", type=str, help="Search through LDAP and lookup entry. Works with fuzzy search. Defaults to printing all attributes, but honors '--attrs'") + egroup.add_argument("--custom", dest="custom_filter", help="Perform a search with a custom object filter. Must be valid LDAP filter syntax") + + ogroup = parser.add_argument_group("Output Options", "Display and output options for results") + ogroup.add_argument("-r", "--resolve", action="store_true", help="Resolve IP addresses for enumerated computer names. Will make DNS queries against system NS") + ogroup.add_argument("--attrs", metavar="ATTRS", dest="attrs", type=str, help="Comma separated custom atrribute names to search for (e.g. 'badPwdCount,lastLogon')") + ogroup.add_argument("--full", action="store_true", help="Dump all atrributes from LDAP.") + ogroup.add_argument("-o", "--output", metavar="output_dir", dest="output_dir", type=str, help="Save results to TSV files in ") + + + + if len(sys.argv) == 1: + parser.print_help() + sys.exit(1) + + args = parser.parse_args() + + if not (args.domain or args.dc_ip): + print "[!] You must specify either a domain or the IP address of a domain controller" + sys.exit(1) + + run(args) + + + + + + + + + + + +