diff --git a/ldapdomaindump/__init__.py b/ldapdomaindump/__init__.py index 45211df..533f057 100644 --- a/ldapdomaindump/__init__.py +++ b/ldapdomaindump/__init__.py @@ -21,18 +21,22 @@ # SOFTWARE. # #################### - +from __future__ import unicode_literals import sys, os, re, codecs, json, argparse, getpass, base64 # import class and constants from datetime import datetime -from urllib import quote_plus - +try: + from urllib.parse import quote_plus +except ImportError: + from urllib import quote_plus import ldap3 from ldap3 import Server, Connection, SIMPLE, SYNC, ALL, SASL, NTLM -from ldap3.core.exceptions import LDAPKeyError, LDAPAttributeError, LDAPCursorError +from ldap3.core.exceptions import LDAPKeyError, LDAPAttributeError, LDAPCursorError, LDAPInvalidDnError from ldap3.abstract import attribute, attrDef from ldap3.utils import dn from ldap3.protocol.formatters.formatters import format_sid +from builtins import str +from future.utils import itervalues, iteritems, native_str # dnspython, for resolving hostnames import dns.resolver @@ -452,7 +456,7 @@ outflags = [] if attr is None: return outflags - for flag, val in flags_def.items(): + for flag, val in iteritems(flags_def): if attr.value & val: outflags.append(flag) return outflags @@ -462,7 +466,7 @@ outflags = [] if attr is None: return outflags - for flag, val in flags_def.items(): + for flag, val in iteritems(flags_def): if attr.value == val: outflags.append(flag) return outflags @@ -473,40 +477,40 @@ #Only if this is the first table it is an actual table, the others are just bodies of the first table #This makes sure that multiple tables have their columns aligned to make it less messy if firstTable: - of.append(u'') + of.append('
') #Table header if header != '': - of.append(u'' % (len(attributes), self.formatId(header), header)) - of.append(u'') + of.append('' % (len(attributes), self.formatId(header), header)) + of.append('') for hdr in attributes: try: #Print alias of this attribute if there is one - of.append(u'' % self.htmlescape(attr_translations[hdr])) + of.append('' % self.htmlescape(attr_translations[hdr])) except KeyError: - of.append(u'' % self.htmlescape(hdr)) - of.append(u'\n') + of.append('' % self.htmlescape(hdr)) + of.append('\n') for li in listable: #Whether we should format group objects separately if specialGroupsFormat and 'group' in li['objectClass'].values: #Give it an extra class and pass it to the function below to make sure the CN is a link liIsGroup = True - of.append(u'') + of.append('') else: liIsGroup = False - of.append(u'') + of.append('') for att in attributes: try: - of.append(u'' % self.formatAttribute(li[att], liIsGroup)) + of.append('' % self.formatAttribute(li[att], liIsGroup)) except (LDAPKeyError, LDAPCursorError): - of.append(u'') - of.append(u'\n') - of.append(u'\n') - return u''.join(of) + of.append('') + of.append('\n') + of.append('\n') + return ''.join(of) #Generate several HTML tables for grouped reports def generateGroupedHtmlTables(self, groups, attributes): first = True - for groupname, members in groups.iteritems(): + for groupname, members in iteritems(groups): yield self.generateHtmlTable(members, attributes, groupname, first, specialGroupsFormat=True) if first: first = False @@ -566,13 +570,14 @@ return value.strftime('%x %X') except ValueError: #Invalid date - return u'0' - if type(value) is unicode: + return '0' + # Make sure it's a unicode string + if type(value) is bytes: + return value.encode('utf8') + if type(value) is str: return value#.encode('utf8') - if type(value) is str: - return unicode(value, errors='replace')#.encode('utf8') if type(value) is int: - return unicode(value) + return str(value) if value is None: return '' #Other type: just return it @@ -619,25 +624,46 @@ def formatCnWithGroupLink(self, cn): - return u'Group: %s' % (self.formatId(cn), self.htmlescape(cn), self.htmlescape(cn)) + return 'Group: %s' % (self.formatId(cn), self.htmlescape(cn), self.htmlescape(cn)) #Convert a CN to a valid HTML id by replacing all non-ascii characters with a _ def formatId(self, cn): return re.sub(r'[^a-zA-Z0-9_\-]+', '_', cn) + + # Fallback function for dirty DN parsing in case ldap3 functions error out + def parseDnFallback(self, dn): + try: + indcn = dn[3:].index(',CN=') + indou = dn[3:].index(',OU=') + if indcn < indou: + cn = dn[3:].split(',CN=')[0] + else: + cn = dn[3:].split(',OU=')[0] + except ValueError: + cn = dn + return cn #Format groups to readable HTML def formatGroupsHtml(self, grouplist): outcache = [] for group in grouplist: - cn = self.unescapecn(dn.parse_dn(group)[0][1]) - outcache.append(u'%s' % (self.config.users_by_group, quote_plus(self.formatId(cn)), self.htmlescape(group), self.htmlescape(cn))) + try: + cn = self.unescapecn(dn.parse_dn(group)[0][1]) + except LDAPInvalidDnError: + # Parsing failed, do it manually + cn = self.unescapecn(self.parseDnFallback(group)) + outcache.append('%s' % (self.config.users_by_group, quote_plus(self.formatId(cn)), self.htmlescape(group), self.htmlescape(cn))) return ', '.join(outcache) #Format groups to readable HTML def formatGroupsGrep(self, grouplist): outcache = [] for group in grouplist: - cn = self.unescapecn(dn.parse_dn(group)[0][1]) + try: + cn = self.unescapecn(dn.parse_dn(group)[0][1]) + except LDAPInvalidDnError: + # Parsing failed, do it manually + cn = self.unescapecn(self.parseDnFallback(group)) outcache.append(cn) return ', '.join(outcache) @@ -707,7 +733,7 @@ #Start of the list yield '[' firstGroup = True - for group in groups.iteritems(): + for group in iteritems(groups): if not firstGroup: #Separate items yield ',' @@ -799,11 +825,11 @@ #Some quick logging helpers def log_warn(text): - print '[!] %s' % text + print('[!] %s' % text) def log_info(text): - print '[*] %s' % text + print('[*] %s' % text) def log_success(text): - print '[+] %s' % text + print('[+] %s' % text) def main(): parser = argparse.ArgumentParser(description='Domain information dumper via LDAP. Dumps users/computers/groups and OS/membership information to HTML/JSON/greppable output.') @@ -813,8 +839,8 @@ #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=str, metavar='USERNAME', help="DOMAIN\\username for authentication, leave empty for anonymous authentication") - parser.add_argument("-p", "--password", type=str, metavar='PASSWORD', help="Password or LM:NTLM hash, will prompt if not specified") + 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)") #Output parameters @@ -879,6 +905,7 @@ # define the server and the connection s = Server(args.host, get_info=ALL) 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 diff --git a/ldapdomaindump/convert.py b/ldapdomaindump/convert.py index 776124b..14ebac1 100644 --- a/ldapdomaindump/convert.py +++ b/ldapdomaindump/convert.py @@ -1,3 +1,4 @@ +from __future__ import unicode_literals import argparse import os import logging @@ -5,6 +6,8 @@ import codecs import re from ldapdomaindump import trust_flags, trust_directions +from builtins import str +from future.utils import itervalues, iteritems logging.basicConfig() logger = logging.getLogger('ldd2bloodhound') @@ -84,7 +87,7 @@ # Read group mapping - write to csv # file is already created here, we just append with codecs.open('group_membership.csv', 'a', 'utf-8') as outfile: - for group in self.groups_by_dn.itervalues(): + for group in itervalues(self.groups_by_dn): for membergroup in group['memberOf']: try: outfile.write('%s,%s,%s\n' % (self.groups_by_dn[membergroup]['principal'], group['principal'], 'group')) diff --git a/setup.py b/setup.py index 6d2e804..0d16462 100644 --- a/setup.py +++ b/setup.py @@ -1,12 +1,12 @@ from setuptools import setup setup(name='ldapdomaindump', - version='0.8.7', + version='0.9.1', description='Active Directory information dumper via LDAP', author='Dirk-jan Mollema', author_email='dirkjan@sanoweb.nl', url='https://github.com/dirkjanm/ldapdomaindump/', packages=['ldapdomaindump'], - install_requires=['dnspython','ldap3>=2.0'], + install_requires=['dnspython', 'ldap3==2.5.1', 'future'], package_data={'ldapdomaindump': ['style.css']}, include_package_data=True, scripts=['bin/ldapdomaindump', 'bin/ldd2bloodhound']
%s
%s
%s%s%s
%s
%s%s