diff --git a/LICENSE b/LICENSE index 4b7fbe1..4dbbfcf 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2016 Dirk-jan +Copyright (c) 2020 Dirk-jan Mollema Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/MANIFEST.in b/MANIFEST.in index 854b2a2..0131a8f 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1 +1,2 @@ include ldapdomaindump/style.css +include LICENSE Readme.md diff --git a/Readme.md b/Readme.md index ca8a0cb..2325915 100644 --- a/Readme.md +++ b/Readme.md @@ -1,106 +1,109 @@ -# LDAPDomainDump -Active Directory information dumper via LDAP - -## Introduction -In an Active Directory domain, a lot of interesting information can be retrieved via LDAP by any authenticated user (or machine). -This makes LDAP an interesting protocol for gathering information in the recon phase of a pentest of an internal network. -A problem is that data from LDAP often is not available in an easy to read format. - -ldapdomaindump is a tool which aims to solve this problem, by collecting and parsing information available via LDAP and outputting it in a human readable HTML format, as well as machine readable json and csv/tsv/greppable files. - -The tool was designed with the following goals in mind: -- Easy overview of all users/groups/computers/policies in the domain -- Authentication both via username and password, as with NTLM hashes (requires ldap3 >=1.3.1) -- Possibility to run the tool with an existing authenticated connection to an LDAP service, allowing for integration with relaying tools such as impackets ntlmrelayx - -The tool outputs several files containing an overview of objects in the domain: -- *domain_groups*: List of groups in the domain -- *domain_users*: List of users in the domain -- *domain_computers*: List of computer accounts in the domain -- *domain_policy*: Domain policy such as password requirements and lockout policy -- *domain_trusts*: Incoming and outgoing domain trusts, and their properties - -As well as two grouped files: -- *domain_users_by_group*: Domain users per group they are member of -- *domain_computers_by_os*: Domain computers sorted by Operating System - -## Dependencies and installation -Requires [ldap3](https://github.com/cannatag/ldap3) > 2.0 and [dnspython](https://github.com/rthalley/dnspython) - -Both can be installed with `pip install ldap3 dnspython` - -The ldapdomaindump package can be installed with `python setup.py install` from the git source, or for the latest release with `pip install ldapdomaindump`. - -## Usage -There are 3 ways to use the tool: -- With just the source, run `python ldapdomaindump.py` -- After installing, by running `python -m ldapdomaindump` -- After installing, by running `ldapdomaindump` - -Help can be obtained with the -h switch: -``` -usage: ldapdomaindump.py [-h] [-u USERNAME] [-p PASSWORD] [-at {NTLM,SIMPLE}] - [-o DIRECTORY] [--no-html] [--no-json] [--no-grep] - [--grouped-json] [-d DELIMITER] [-r] [-n DNS_SERVER] - [-m] - HOSTNAME - -Domain information dumper via LDAP. Dumps users/computers/groups and -OS/membership information to HTML/JSON/greppable output. - -Required options: - HOSTNAME Hostname/ip or ldap://host:port connection string to - connect to (use ldaps:// to use SSL) - -Main options: - -h, --help show this help message and exit - -u USERNAME, --user USERNAME - DOMAIN\username for authentication, leave empty for - anonymous authentication - -p PASSWORD, --password PASSWORD - Password or LM:NTLM hash, will prompt if not specified - -at {NTLM,SIMPLE}, --authtype {NTLM,SIMPLE} - Authentication type (NTLM or SIMPLE, default: NTLM) - -Output options: - -o DIRECTORY, --outdir DIRECTORY - Directory in which the dump will be saved (default: - current) - --no-html Disable HTML output - --no-json Disable JSON output - --no-grep Disable Greppable output - --grouped-json Also write json files for grouped files (default: - disabled) - -d DELIMITER, --delimiter DELIMITER - Field delimiter for greppable output (default: tab) - -Misc options: - -r, --resolve Resolve computer hostnames (might take a while and - cause high traffic on large networks) - -n DNS_SERVER, --dns-server DNS_SERVER - Use custom DNS resolver instead of system DNS (try a - domain controller IP) - -m, --minimal Only query minimal set of attributes to limit memmory - usage -``` - -## Options -### Authentication -Most AD servers support NTLM authentication. In the rare case that it does not, use --authtype SIMPLE. - -### Output formats -By default the tool outputs all files in HTML, JSON and tab delimited output (greppable). There are also two grouped files (users_by_group and computers_by_os) for convenience. These do not have a greppable output. JSON output for grouped files is disabled by default since it creates very large files without any data that isn't present in the other files already. - -### DNS resolving -An important option is the *-r* option, which decides if a computers DNSHostName attribute should be resolved to an IPv4 address. -While this can be very useful, the DNSHostName attribute is not automatically updated. When the AD Domain uses subdomains for computer hostnames, the DNSHostName will often be incorrect and will not resolve. Also keep in mind that resolving every hostname in the domain might cause a high load on the domain controller. - -### Minimizing network and memory usage -By default ldapdomaindump will try to dump every single attribute it can read to disk in the .json files. In large networks, this uses a lot of memory (since group relationships are currently calculated in memory before being written to disk). To dump only the minimal required attributes (the ones shown by default in the .html and .grep files), use the `--minimal` switch. - -## Visualizing groups with BloodHound -LDAPDomainDump includes a utility that can be used to convert ldapdomaindumps `.json` files to CSV files suitable for BloodHound. The utility is called `ldd2bloodhound` and is added to your path upon installation. Alternatively you can run it with `python -m ldapdomaindump.convert` or with `python ldapdomaindump/convert.py` if you are running it from the source. -The conversion tool will take the users/groups/computers/trusts `.json` file and convert those to `group_membership.csv` and `trust.csv` which you can add to BloodHound. - -## License -MIT +# LDAPDomainDump +Active Directory information dumper via LDAP + +## Introduction +In an Active Directory domain, a lot of interesting information can be retrieved via LDAP by any authenticated user (or machine). +This makes LDAP an interesting protocol for gathering information in the recon phase of a pentest of an internal network. +A problem is that data from LDAP often is not available in an easy to read format. + +ldapdomaindump is a tool which aims to solve this problem, by collecting and parsing information available via LDAP and outputting it in a human readable HTML format, as well as machine readable json and csv/tsv/greppable files. + +The tool was designed with the following goals in mind: +- Easy overview of all users/groups/computers/policies in the domain +- Authentication both via username and password, as with NTLM hashes (requires ldap3 >=1.3.1) +- Possibility to run the tool with an existing authenticated connection to an LDAP service, allowing for integration with relaying tools such as impackets ntlmrelayx + +The tool outputs several files containing an overview of objects in the domain: +- *domain_groups*: List of groups in the domain +- *domain_users*: List of users in the domain +- *domain_computers*: List of computer accounts in the domain +- *domain_policy*: Domain policy such as password requirements and lockout policy +- *domain_trusts*: Incoming and outgoing domain trusts, and their properties + +As well as two grouped files: +- *domain_users_by_group*: Domain users per group they are member of +- *domain_computers_by_os*: Domain computers sorted by Operating System + +## Dependencies and installation +Requires [ldap3](https://github.com/cannatag/ldap3) > 2.0 and [dnspython](https://github.com/rthalley/dnspython) + +Both can be installed with `pip install ldap3 dnspython` + +The ldapdomaindump package can be installed with `python setup.py install` from the git source, or for the latest release with `pip install ldapdomaindump`. + +## Usage +There are 3 ways to use the tool: +- With just the source, run `python ldapdomaindump.py` +- After installing, by running `python -m ldapdomaindump` +- After installing, by running `ldapdomaindump` + +Help can be obtained with the -h switch: +``` +usage: ldapdomaindump.py [-h] [-u USERNAME] [-p PASSWORD] [-at {NTLM,SIMPLE}] + [-o DIRECTORY] [--no-html] [--no-json] [--no-grep] + [--grouped-json] [-d DELIMITER] [-r] [-n DNS_SERVER] + [-m] + HOSTNAME + +Domain information dumper via LDAP. Dumps users/computers/groups and +OS/membership information to HTML/JSON/greppable output. + +Required options: + HOSTNAME Hostname/ip or ldap://host:port connection string to + connect to (use ldaps:// to use SSL) + +Main options: + -h, --help show this help message and exit + -u USERNAME, --user USERNAME + DOMAIN\username for authentication, leave empty for + anonymous authentication + -p PASSWORD, --password PASSWORD + Password or LM:NTLM hash, will prompt if not specified + -at {NTLM,SIMPLE}, --authtype {NTLM,SIMPLE} + Authentication type (NTLM or SIMPLE, default: NTLM) + +Output options: + -o DIRECTORY, --outdir DIRECTORY + Directory in which the dump will be saved (default: + current) + --no-html Disable HTML output + --no-json Disable JSON output + --no-grep Disable Greppable output + --grouped-json Also write json files for grouped files (default: + disabled) + -d DELIMITER, --delimiter DELIMITER + Field delimiter for greppable output (default: tab) + +Misc options: + -r, --resolve Resolve computer hostnames (might take a while and + cause high traffic on large networks) + -n DNS_SERVER, --dns-server DNS_SERVER + Use custom DNS resolver instead of system DNS (try a + domain controller IP) + -m, --minimal Only query minimal set of attributes to limit memmory + usage +``` + +## Options +### Authentication +Most AD servers support NTLM authentication. In the rare case that it does not, use --authtype SIMPLE. + +### Output formats +By default the tool outputs all files in HTML, JSON and tab delimited output (greppable). There are also two grouped files (users_by_group and computers_by_os) for convenience. These do not have a greppable output. JSON output for grouped files is disabled by default since it creates very large files without any data that isn't present in the other files already. + +### DNS resolving +An important option is the *-r* option, which decides if a computers DNSHostName attribute should be resolved to an IPv4 address. +While this can be very useful, the DNSHostName attribute is not automatically updated. When the AD Domain uses subdomains for computer hostnames, the DNSHostName will often be incorrect and will not resolve. Also keep in mind that resolving every hostname in the domain might cause a high load on the domain controller. + +### Minimizing network and memory usage +By default ldapdomaindump will try to dump every single attribute it can read to disk in the .json files. In large networks, this uses a lot of memory (since group relationships are currently calculated in memory before being written to disk). To dump only the minimal required attributes (the ones shown by default in the .html and .grep files), use the `--minimal` switch. + +## Visualizing groups with BloodHound +LDAPDomainDump includes a utility that can be used to convert ldapdomaindumps `.json` files to CSV files suitable for BloodHound. The utility is called `ldd2bloodhound` and is added to your path upon installation. Alternatively you can run it with `python -m ldapdomaindump.convert` or with `python ldapdomaindump/convert.py` if you are running it from the source. +The conversion tool will take the users/groups/computers/trusts `.json` file and convert those to `group_membership.csv` and `trust.csv` which you can add to BloodHound. *Note that these files are only compatible with **BloodHound 1.x** which is quite old. There are no plans to support the latest version as the [BloodHound.py project](https://github.com/fox-it/BloodHound.py) was made for this. With the DCOnly collection method this tool will also only talk to LDAP and collect more information than ldapdomaindump would*. + +## Visualizing dump with a pretty output like enum4linux +LDAPDomainDump includes a utility that can be used to output ldapdomaindumps `.json` files to an enum4linux like output. The utility is called `ldd2pretty` and is added to your path upon installation. Alternatively you can run it with `python -m ldapdomaindump.pretty` or with `python ldapdomaindump/pretty.py` if you are running it from the source. + +## License +MIT diff --git a/bin/ldd2pretty b/bin/ldd2pretty new file mode 100644 index 0000000..82a9136 --- /dev/null +++ b/bin/ldd2pretty @@ -0,0 +1,3 @@ +#!/usr/bin/env python +from ldapdomaindump import pretty +pretty.main() diff --git a/ldapdomaindump/__init__.py b/ldapdomaindump/__init__.py index 533f057..3f3b953 100644 --- a/ldapdomaindump/__init__.py +++ b/ldapdomaindump/__init__.py @@ -24,7 +24,7 @@ from __future__ import unicode_literals import sys, os, re, codecs, json, argparse, getpass, base64 # import class and constants -from datetime import datetime +from datetime import datetime, timedelta try: from urllib.parse import quote_plus except ImportError: @@ -112,7 +112,11 @@ 'lockoutThreshold':'Lockout Threshold', 'maxPwdAge':'Max password age', 'minPwdAge':'Min password age', - 'minPwdLength':'Min password length'} + 'minPwdLength':'Min password length', + 'pwdHistoryLength':'Password history length', + 'pwdProperties':'Password properties', + 'ms-DS-MachineAccountQuota':'Machine Account Quota', + 'flatName':'NETBIOS Domain name'} MINIMAL_COMPUTERATTRIBUTES = ['cn', 'sAMAccountName', 'dNSHostName', 'operatingSystem', 'operatingSystemServicePack', 'operatingSystemVersion', 'lastLogon', 'userAccountControl', 'whenCreated', 'objectSid', 'description', 'objectClass'] MINIMAL_USERATTRIBUTES = ['cn', 'name', 'sAMAccountName', 'memberOf', 'primaryGroupId', 'whenCreated', 'whenChanged', 'lastLogon', 'userAccountControl', 'pwdLastSet', 'objectSid', 'description', 'objectClass'] @@ -378,7 +382,11 @@ except (LDAPAttributeError, LDAPCursorError): ugroups = [] #Add the user default group - ugroups.append(self.getGroupCnFromDn(self.groups_dnmap[user.primaryGroupId.value])) + try: + ugroups.append(self.getGroupCnFromDn(self.groups_dnmap[user.primaryGroupId.value])) + # Sometimes we can't query this group or it doesn't exist + except KeyError: + pass for group in ugroups: try: groupsdict[group].append(user) @@ -431,7 +439,7 @@ #In grouped view, don't include the memberOf property to reduce output size self.userattributes_grouped = ['cn', 'name', 'sAMAccountName', 'whenCreated', 'whenChanged', 'lastLogon', 'userAccountControl', 'pwdLastSet', 'objectSid', 'description'] self.groupattributes = ['cn', 'sAMAccountName', 'memberOf', 'description', 'whenCreated', 'whenChanged', 'objectSid'] - self.policyattributes = ['cn', 'lockOutObservationWindow', 'lockoutDuration', 'lockoutThreshold', 'maxPwdAge', 'minPwdAge', 'minPwdLength', 'pwdHistoryLength', 'pwdProperties'] + self.policyattributes = ['distinguishedName', 'lockOutObservationWindow', 'lockoutDuration', 'lockoutThreshold', 'maxPwdAge', 'minPwdAge', 'minPwdLength', 'pwdHistoryLength', 'pwdProperties', 'ms-DS-MachineAccountQuota'] self.trustattributes = ['cn', 'flatName', 'securityIdentifier', 'trustAttributes', 'trustDirection', 'trustType'] #Escape HTML special chars @@ -446,15 +454,23 @@ #Convert password max age (in 100 nanoseconds), to days def nsToDays(self, length): - return abs(length) * .0000001 / 86400 + # ldap3 >= 2.6 returns timedelta + if isinstance(length, timedelta): + return length.total_seconds() / 86400 + else: + return abs(length) * .0000001 / 86400 def nsToMinutes(self, length): - return abs(length) * .0000001 / 60 + # ldap3 >= 2.6 returns timedelta + if isinstance(length, timedelta): + return length.total_seconds() / 60 + else: + return abs(length) * .0000001 / 60 #Parse bitwise flags into a list def parseFlags(self, attr, flags_def): outflags = [] - if attr is None: + if attr is None or attr.value is None: return outflags for flag, val in iteritems(flags_def): if attr.value & val: @@ -594,7 +610,10 @@ return self.formatGroupsHtml(att.values) #Primary group if aname == 'primarygroupid': - return self.formatGroupsHtml([self.dd.groups_dnmap[att.value]]) + try: + return self.formatGroupsHtml([self.dd.groups_dnmap[att.value]]) + except KeyError: + return 'NOT FOUND!' #Pwd flags if aname == 'pwdproperties': return ', '.join(self.parseFlags(att, pwd_flags)) @@ -677,7 +696,10 @@ if aname == 'member' or aname == 'memberof' and type(att.values) is list: return self.formatGroupsGrep(att.values) if aname == 'primarygroupid': - return self.formatGroupsGrep([self.dd.groups_dnmap[att.value]]) + try: + return self.formatGroupsGrep([self.dd.groups_dnmap[att.value]]) + except KeyError: + return 'NOT FOUND!' #Domain trust flags if aname == 'trustattributes': return ', '.join(self.parseFlags(att, trust_flags)) diff --git a/ldapdomaindump/pretty.py b/ldapdomaindump/pretty.py new file mode 100644 index 0000000..285f05a --- /dev/null +++ b/ldapdomaindump/pretty.py @@ -0,0 +1,152 @@ +from __future__ import print_function +from builtins import str +import argparse +import json +import os.path +import re +from time import strftime, gmtime + +class PrettyOuput(object): + + def d2b(self, a): + tbin = [] + while a: + tbin.append(a % 2) + a //= 2 + + t2bin = tbin[::-1] + if len(t2bin) != 8: + for x in range(6 - len(t2bin)): + t2bin.insert(0, 0) + return ''.join([str(g) for g in t2bin]) + + def convert(self, time): + if isinstance(time, str): + return time + if time == 0: + return "None" + if time == -9223372036854775808: + return "Not Set" + sec = abs(time) // 10000000 + days = sec // 86400 + sec -= 86400*days + hrs = sec // 3600 + sec -= 3600*hrs + mins = sec // 60 + sec -= 60*mins + result = "" + if days > 1: + result += "{0} days ".format(days) + elif days == 1: + result += "{0} day ".format(days) + if hrs > 1: + result += "{0} hours ".format(hrs) + elif hrs == 1: + result += "{0} hour ".format(hrs) + if mins > 1: + result += "{0} minutes ".format(mins) + elif mins == 1: + result += "{0} minute ".format(mins) + return result + + def password_complexity(self, data): + + print(''' + +-----------------------------------------+ + | Password Policy Information | + +-----------------------------------------+ + ''') + + print("[+] Password Info for Domain:", data[0]['attributes']['dc'][0].upper()) + print("\t[+] Minimum password length: ", data[0]['attributes']['instanceType'][0]) + print("\t[+] Password history length:", data[0]['attributes']['pwdHistoryLength'][0]) + + password_properties = self.d2b(data[0]['attributes']['pwdProperties'][0]) + print("\t[+] Password Complexity Flags:", password_properties) + print("") + print("\t\t[+] Domain Refuse Password Change:", password_properties[0]) + print("\t\t[+] Domain Password Store Cleartext:", password_properties[1]) + print("\t\t[+] Domain Password Lockout Admins:", password_properties[2]) + print("\t\t[+] Domain Password No Clear Change:", password_properties[3]) + print("\t\t[+] Domain Password No Anon Change:", password_properties[4]) + print("\t\t[+] Domain Password Complex:", password_properties[5]) + print("") + print("\t[+] Maximum password age:", self.convert(data[0]['attributes']['maxPwdAge'][0])) + print("\t[+] Minimum password age:", self.convert(data[0]['attributes']['minPwdAge'][0])) + print("\t[+] Reset Account Lockout Counter:", self.convert(data[0]['attributes']['lockoutDuration'][0])) + print("\t[+] Account Lockout Threshold:", data[0]['attributes']['lockoutThreshold'][0]) + print("\t[+] Forced Log off Time:", self.convert(data[0]['attributes']['forceLogoff'][0])) + + def domain_info(self, data): + print(''' + +--------------------------------------+ + | Getting Domain Sid For | + +--------------------------------------+ + ''') + print('[+] Domain Name:', data[0]['attributes']['dc'][0]) + print('Domain Sid:', data[0]['attributes']['objectSid'][0]) + print('') + return data[0]['attributes']['dc'][0] + + def user_info(self, users, dc): + print(''' + +------------------------+ + | Users Infos | + +------------------------+ + ''') + for user in users: + desc = user['attributes'].get('description')[0] if user['attributes'].get('description') else "(null)" + print("Account: " + dc + "\\" + user['attributes']['sAMAccountName'][0] + "\tName: " + user['attributes']['name'][0] + "\tDesc: " + desc) + + print("") + for user in users: + print("user:[" + user['attributes']['sAMAccountName'][0] + "]") + print("") + + def groups_info(self, groups, dc): + print(''' + +------------------------+import os.path + | Groups Infos | + +------------------------+ + ''') + for group in groups: + print("group:[" + group['attributes']['name'][0] + "]") + + for group in groups: + if group['attributes'].get('member'): + users = re.findall(r"^CN=([\w\s\-\_\{\}\.\$\#]+)", '\n'.join(group['attributes']['member']), re.M) + if users: + print("\n[+] Getting domain group memberships:") + for user in users: + if user == "S-1-5-11": + user = "NT AUTHORITY\\Authenticated Users" + elif user == "S-1-5-4": + user = "NT AUTHORITY\\INTERACTIVE" + elif user == "S-1-5-17": + user = "NT AUTHORITY\\IUSR" + print("Group '" + group['attributes']['name'][0] + "' has member: " + dc + "\\" + user) + +def main(): + parser = argparse.ArgumentParser(description='LDAPDomainDump to pretty output like enum4linux.') + + #Main parameters + parser.add_argument("-d", "--directory", help="The ldapdomaindump directory where the json files are saved. Required files: domain_users.json, domain_groups.json and domain_policy.json") + args = parser.parse_args() + + if args.directory: + with open(args.directory + '/domain_policy.json') as f: + domain = json.load(f) + with open(args.directory + '/domain_users.json') as f: + users = json.load(f) + with open(args.directory + '/domain_groups.json') as f: + groups = json.load(f) + pretty = PrettyOuput() + dc = pretty.domain_info(domain) + pretty.password_complexity(domain) + pretty.user_info(users, dc.upper()) + pretty.groups_info(groups, dc.upper()) + else: + print("Missing parameter --directory /output/") + +if __name__ == "__main__": + main() diff --git a/setup.py b/setup.py index 0d16462..8a1b0e7 100644 --- a/setup.py +++ b/setup.py @@ -1,13 +1,13 @@ -from setuptools import setup -setup(name='ldapdomaindump', - 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.5.1', 'future'], - package_data={'ldapdomaindump': ['style.css']}, - include_package_data=True, - scripts=['bin/ldapdomaindump', 'bin/ldd2bloodhound'] - ) +from setuptools import setup +setup(name='ldapdomaindump', + version='0.9.3', + 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.5,!=2.5.2,!=2.5.0,!=2.6', 'future'], + package_data={'ldapdomaindump': ['style.css']}, + include_package_data=True, + scripts=['bin/ldapdomaindump', 'bin/ldd2bloodhound', 'bin/ldd2pretty'] + )