Codebase list python-ldapdomaindump / 6da1759
Import upstream version 0.9.3+git20210706.1.451ce37 Kali Janitor 2 years ago
17 changed file(s) with 284 addition(s) and 76 deletion(s). Raw diff Collapse all Expand all
+0
-13
.editorconfig less more
0 # http://editorconfig.org
1 root = true
2
3 [*]
4 indent_style = space
5 indent_size = 4
6 end_of_line = crlf
7 charset = utf-8
8 trim_trailing_whitespace = true
9 insert_final_newline = true
10
11 [*.md]
12 trim_trailing_whitespace = false
+0
-5
.gitignore less more
0 build/
1 dist/
2 ldapdomaindump.egg-info
3 domainlookup.py
4 *.pyc
00 The MIT License (MIT)
11
2 Copyright (c) 2016 Dirk-jan
2 Copyright (c) 2020 Dirk-jan Mollema
33
44 Permission is hereby granted, free of charge, to any person obtaining a copy
55 of this software and associated documentation files (the "Software"), to deal
00 include ldapdomaindump/style.css
1 include LICENSE Readme.md
0 Metadata-Version: 1.0
1 Name: ldapdomaindump
2 Version: 0.9.3
3 Summary: Active Directory information dumper via LDAP
4 Home-page: https://github.com/dirkjanm/ldapdomaindump/
5 Author: Dirk-jan Mollema
6 Author-email: [email protected]
7 License: UNKNOWN
8 Description: UNKNOWN
9 Platform: UNKNOWN
2424 - *domain_computers_by_os*: Domain computers sorted by Operating System
2525
2626 ## Dependencies and installation
27 Requires [ldap3](https://github.com/cannatag/ldap3) > 2.0 and [dnspython](https://github.com/rthalley/dnspython)
27 Requires [ldap3](https://github.com/cannatag/ldap3) > 2.0, [dnspython](https://github.com/rthalley/dnspython) and [future](https://python-future.org/). ldapdomaindump runs on both python 2 and 3.
2828
29 Both can be installed with `pip install ldap3 dnspython`
29 Dependencies can be installed manually with `pip install ldap3 dnspython future`, but should in most cases be handled by pip when you install the main package either from git or pypi.
3030
3131 The ldapdomaindump package can be installed with `python setup.py install` from the git source, or for the latest release with `pip install ldapdomaindump`.
3232
9999
100100 ## Visualizing groups with BloodHound
101101 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.
102 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.
102 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*.
103
104 ## Visualizing dump with a pretty output like enum4linux
105 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.
103106
104107 ## License
105108 MIT
0 #!/usr/bin/env python
1 from ldapdomaindump import pretty
2 pretty.main()
2323 from __future__ import unicode_literals
2424 import sys, os, re, codecs, json, argparse, getpass, base64
2525 # import class and constants
26 from datetime import datetime
26 from datetime import datetime, timedelta
2727 try:
2828 from urllib.parse import quote_plus
2929 except ImportError:
4747 'ACCOUNT_LOCKED':0x00000010,
4848 'PASSWD_NOTREQD':0x00000020,
4949 'PASSWD_CANT_CHANGE': 0x00000040,
50 'PASSWORD_STORE_CLEARTEXT': 0x00000080,
5051 'NORMAL_ACCOUNT': 0x00000200,
5152 'WORKSTATION_ACCOUNT':0x00001000,
5253 'SERVER_TRUST_ACCOUNT': 0x00002000,
8081 'TREAT_AS_EXTERNAL':0x00000040,
8182 'USES_RC4_ENCRYPTION':0x00000080,
8283 'CROSS_ORGANIZATION_NO_TGT_DELEGATION':0x00000200,
84 'CROSS_ORGANIZATION_ENABLE_TGT_DELEGATION':0x00000800,
8385 'PIM_TRUST':0x00000400}
8486
8587 # Domain trust direction
111113 'lockoutThreshold':'Lockout Threshold',
112114 'maxPwdAge':'Max password age',
113115 'minPwdAge':'Min password age',
114 'minPwdLength':'Min password length'}
116 'minPwdLength':'Min password length',
117 'pwdHistoryLength':'Password history length',
118 'pwdProperties':'Password properties',
119 'ms-DS-MachineAccountQuota':'Machine Account Quota',
120 'flatName':'NETBIOS Domain name'}
115121
116122 MINIMAL_COMPUTERATTRIBUTES = ['cn', 'sAMAccountName', 'dNSHostName', 'operatingSystem', 'operatingSystemServicePack', 'operatingSystemVersion', 'lastLogon', 'userAccountControl', 'whenCreated', 'objectSid', 'description', 'objectClass']
117123 MINIMAL_USERATTRIBUTES = ['cn', 'name', 'sAMAccountName', 'memberOf', 'primaryGroupId', 'whenCreated', 'whenChanged', 'lastLogon', 'userAccountControl', 'pwdLastSet', 'objectSid', 'description', 'objectClass']
377383 except (LDAPAttributeError, LDAPCursorError):
378384 ugroups = []
379385 #Add the user default group
380 ugroups.append(self.getGroupCnFromDn(self.groups_dnmap[user.primaryGroupId.value]))
386 try:
387 ugroups.append(self.getGroupCnFromDn(self.groups_dnmap[user.primaryGroupId.value]))
388 # Sometimes we can't query this group or it doesn't exist
389 except KeyError:
390 pass
381391 for group in ugroups:
382392 try:
383393 groupsdict[group].append(user)
430440 #In grouped view, don't include the memberOf property to reduce output size
431441 self.userattributes_grouped = ['cn', 'name', 'sAMAccountName', 'whenCreated', 'whenChanged', 'lastLogon', 'userAccountControl', 'pwdLastSet', 'objectSid', 'description']
432442 self.groupattributes = ['cn', 'sAMAccountName', 'memberOf', 'description', 'whenCreated', 'whenChanged', 'objectSid']
433 self.policyattributes = ['cn', 'lockOutObservationWindow', 'lockoutDuration', 'lockoutThreshold', 'maxPwdAge', 'minPwdAge', 'minPwdLength', 'pwdHistoryLength', 'pwdProperties']
443 self.policyattributes = ['distinguishedName', 'lockOutObservationWindow', 'lockoutDuration', 'lockoutThreshold', 'maxPwdAge', 'minPwdAge', 'minPwdLength', 'pwdHistoryLength', 'pwdProperties', 'ms-DS-MachineAccountQuota']
434444 self.trustattributes = ['cn', 'flatName', 'securityIdentifier', 'trustAttributes', 'trustDirection', 'trustType']
435445
436446 #Escape HTML special chars
445455
446456 #Convert password max age (in 100 nanoseconds), to days
447457 def nsToDays(self, length):
448 return abs(length) * .0000001 / 86400
458 # ldap3 >= 2.6 returns timedelta
459 if isinstance(length, timedelta):
460 return length.total_seconds() / 86400
461 else:
462 return abs(length) * .0000001 / 86400
449463
450464 def nsToMinutes(self, length):
451 return abs(length) * .0000001 / 60
465 # ldap3 >= 2.6 returns timedelta
466 if isinstance(length, timedelta):
467 return length.total_seconds() / 60
468 else:
469 return abs(length) * .0000001 / 60
452470
453471 #Parse bitwise flags into a list
454472 def parseFlags(self, attr, flags_def):
473 outflags = []
474 if attr is None or attr.value is None:
475 return outflags
476 for flag, val in iteritems(flags_def):
477 if int(attr.value) & val:
478 outflags.append(flag)
479 return outflags
480
481 #Parse bitwise trust direction - only one flag applies here, 0x03 overlaps
482 def parseSingleFlag(self, attr, flags_def):
455483 outflags = []
456484 if attr is None:
457485 return outflags
458486 for flag, val in iteritems(flags_def):
459 if attr.value & val:
460 outflags.append(flag)
461 return outflags
462
463 #Parse bitwise trust direction - only one flag applies here, 0x03 overlaps
464 def parseTrustDirection(self, attr, flags_def):
465 outflags = []
466 if attr is None:
467 return outflags
468 for flag, val in iteritems(flags_def):
469 if attr.value == val:
487 if int(attr.value) == val:
470488 outflags.append(flag)
471489 return outflags
472490
593611 return self.formatGroupsHtml(att.values)
594612 #Primary group
595613 if aname == 'primarygroupid':
596 return self.formatGroupsHtml([self.dd.groups_dnmap[att.value]])
614 try:
615 return self.formatGroupsHtml([self.dd.groups_dnmap[att.value]])
616 except KeyError:
617 return 'NOT FOUND!'
597618 #Pwd flags
598619 if aname == 'pwdproperties':
599620 return ', '.join(self.parseFlags(att, pwd_flags))
604625 if att.value == 0:
605626 return 'DISABLED'
606627 else:
607 return ', '.join(self.parseTrustDirection(att, trust_directions))
628 return ', '.join(self.parseSingleFlag(att, trust_directions))
608629 if aname == 'trusttype':
609 return ', '.join(self.parseFlags(att, trust_type))
630 return ', '.join(self.parseSingleFlag(att, trust_type))
610631 if aname == 'securityidentifier':
611632 return format_sid(att.raw_values[0])
612633 if aname == 'minpwdage' or aname == 'maxpwdage':
676697 if aname == 'member' or aname == 'memberof' and type(att.values) is list:
677698 return self.formatGroupsGrep(att.values)
678699 if aname == 'primarygroupid':
679 return self.formatGroupsGrep([self.dd.groups_dnmap[att.value]])
700 try:
701 return self.formatGroupsGrep([self.dd.groups_dnmap[att.value]])
702 except KeyError:
703 return 'NOT FOUND!'
680704 #Domain trust flags
681705 if aname == 'trustattributes':
682706 return ', '.join(self.parseFlags(att, trust_flags))
684708 if att.value == 0:
685709 return 'DISABLED'
686710 else:
687 return ', '.join(self.parseTrustDirection(att, trust_directions))
711 return ', '.join(self.parseSingleFlag(att, trust_directions))
688712 if aname == 'trusttype':
689 return ', '.join(self.parseFlags(att, trust_type))
713 return ', '.join(self.parseSingleFlag(att, trust_type))
690714 if aname == 'securityidentifier':
691715 return format_sid(att.raw_values[0])
692716 #Pwd flags
767791 if self.config.outputhtml:
768792 html = self.generateHtmlTable(dd.users, self.userattributes, 'Domain users')
769793 self.writeHtmlFile('%s.html' % self.config.usersfile, html)
794 if self.config.outputgrep:
795 grepout = self.generateGrepList(dd.users, self.userattributes)
796 self.writeGrepFile('%s.grep' % self.config.usersfile, grepout)
770797 if self.config.outputjson:
771798 jsonout = self.generateJsonList(dd.users)
772799 self.writeJsonFile('%s.json' % self.config.usersfile, jsonout)
773 if self.config.outputgrep:
774 grepout = self.generateGrepList(dd.users, self.userattributes)
775 self.writeGrepFile('%s.grep' % self.config.usersfile, grepout)
776800
777801 #Generate report with just a table of all computer accounts
778802 def generateComputersReport(self, dd):
779803 if self.config.outputhtml:
780804 html = self.generateHtmlTable(dd.computers, self.computerattributes, 'Domain computer accounts')
781805 self.writeHtmlFile('%s.html' % self.config.computersfile, html)
806 if self.config.outputgrep:
807 grepout = self.generateGrepList(dd.computers, self.computerattributes)
808 self.writeGrepFile('%s.grep' % self.config.computersfile, grepout)
782809 if self.config.outputjson:
783810 jsonout = self.generateJsonList(dd.computers)
784811 self.writeJsonFile('%s.json' % self.config.computersfile, jsonout)
785 if self.config.outputgrep:
786 grepout = self.generateGrepList(dd.computers, self.computerattributes)
787 self.writeGrepFile('%s.grep' % self.config.computersfile, grepout)
788812
789813 #Generate report with just a table of all computer accounts
790814 def generateGroupsReport(self, dd):
791815 if self.config.outputhtml:
792816 html = self.generateHtmlTable(dd.groups, self.groupattributes, 'Domain groups')
793817 self.writeHtmlFile('%s.html' % self.config.groupsfile, html)
818 if self.config.outputgrep:
819 grepout = self.generateGrepList(dd.groups, self.groupattributes)
820 self.writeGrepFile('%s.grep' % self.config.groupsfile, grepout)
794821 if self.config.outputjson:
795822 jsonout = self.generateJsonList(dd.groups)
796823 self.writeJsonFile('%s.json' % self.config.groupsfile, jsonout)
797 if self.config.outputgrep:
798 grepout = self.generateGrepList(dd.groups, self.groupattributes)
799 self.writeGrepFile('%s.grep' % self.config.groupsfile, grepout)
800824
801825 #Generate policy report
802826 def generatePolicyReport(self, dd):
803827 if self.config.outputhtml:
804828 html = self.generateHtmlTable(dd.policy, self.policyattributes, 'Domain policy')
805829 self.writeHtmlFile('%s.html' % self.config.policyfile, html)
830 if self.config.outputgrep:
831 grepout = self.generateGrepList(dd.policy, self.policyattributes)
832 self.writeGrepFile('%s.grep' % self.config.policyfile, grepout)
806833 if self.config.outputjson:
807834 jsonout = self.generateJsonList(dd.policy)
808835 self.writeJsonFile('%s.json' % self.config.policyfile, jsonout)
809 if self.config.outputgrep:
810 grepout = self.generateGrepList(dd.policy, self.policyattributes)
811 self.writeGrepFile('%s.grep' % self.config.policyfile, grepout)
812836
813837 #Generate policy report
814838 def generateTrustsReport(self, dd):
815839 if self.config.outputhtml:
816840 html = self.generateHtmlTable(dd.trusts, self.trustattributes, 'Domain trusts')
817841 self.writeHtmlFile('%s.html' % self.config.trustsfile, html)
842 if self.config.outputgrep:
843 grepout = self.generateGrepList(dd.trusts, self.trustattributes)
844 self.writeGrepFile('%s.grep' % self.config.trustsfile, grepout)
818845 if self.config.outputjson:
819846 jsonout = self.generateJsonList(dd.trusts)
820847 self.writeJsonFile('%s.json' % self.config.trustsfile, jsonout)
821 if self.config.outputgrep:
822 grepout = self.generateGrepList(dd.trusts, self.trustattributes)
823 self.writeGrepFile('%s.grep' % self.config.trustsfile, grepout)
824848
825849 #Some quick logging helpers
826850 def log_warn(text):
0 from __future__ import print_function
1 from builtins import str
2 import argparse
3 import json
4 import os.path
5 import re
6 from time import strftime, gmtime
7
8 class PrettyOuput(object):
9
10 def d2b(self, a):
11 tbin = []
12 while a:
13 tbin.append(a % 2)
14 a //= 2
15
16 t2bin = tbin[::-1]
17 if len(t2bin) != 8:
18 for x in range(6 - len(t2bin)):
19 t2bin.insert(0, 0)
20 return ''.join([str(g) for g in t2bin])
21
22 def convert(self, time):
23 if isinstance(time, str):
24 return time
25 if time == 0:
26 return "None"
27 if time == -9223372036854775808:
28 return "Not Set"
29 sec = abs(time) // 10000000
30 days = sec // 86400
31 sec -= 86400*days
32 hrs = sec // 3600
33 sec -= 3600*hrs
34 mins = sec // 60
35 sec -= 60*mins
36 result = ""
37 if days > 1:
38 result += "{0} days ".format(days)
39 elif days == 1:
40 result += "{0} day ".format(days)
41 if hrs > 1:
42 result += "{0} hours ".format(hrs)
43 elif hrs == 1:
44 result += "{0} hour ".format(hrs)
45 if mins > 1:
46 result += "{0} minutes ".format(mins)
47 elif mins == 1:
48 result += "{0} minute ".format(mins)
49 return result
50
51 def password_complexity(self, data):
52
53 print('''
54 +-----------------------------------------+
55 | Password Policy Information |
56 +-----------------------------------------+
57 ''')
58
59 print("[+] Password Info for Domain:", data[0]['attributes']['dc'][0].upper())
60 print("\t[+] Minimum password length: ", data[0]['attributes']['instanceType'][0])
61 print("\t[+] Password history length:", data[0]['attributes']['pwdHistoryLength'][0])
62
63 password_properties = self.d2b(data[0]['attributes']['pwdProperties'][0])
64 print("\t[+] Password Complexity Flags:", password_properties)
65 print("")
66 print("\t\t[+] Domain Refuse Password Change:", password_properties[0])
67 print("\t\t[+] Domain Password Store Cleartext:", password_properties[1])
68 print("\t\t[+] Domain Password Lockout Admins:", password_properties[2])
69 print("\t\t[+] Domain Password No Clear Change:", password_properties[3])
70 print("\t\t[+] Domain Password No Anon Change:", password_properties[4])
71 print("\t\t[+] Domain Password Complex:", password_properties[5])
72 print("")
73 print("\t[+] Maximum password age:", self.convert(data[0]['attributes']['maxPwdAge'][0]))
74 print("\t[+] Minimum password age:", self.convert(data[0]['attributes']['minPwdAge'][0]))
75 print("\t[+] Reset Account Lockout Counter:", self.convert(data[0]['attributes']['lockoutDuration'][0]))
76 print("\t[+] Account Lockout Threshold:", data[0]['attributes']['lockoutThreshold'][0])
77 print("\t[+] Forced Log off Time:", self.convert(data[0]['attributes']['forceLogoff'][0]))
78
79 def domain_info(self, data):
80 print('''
81 +--------------------------------------+
82 | Getting Domain Sid For |
83 +--------------------------------------+
84 ''')
85 print('[+] Domain Name:', data[0]['attributes']['dc'][0])
86 print('Domain Sid:', data[0]['attributes']['objectSid'][0])
87 print('')
88 return data[0]['attributes']['dc'][0]
89
90 def user_info(self, users, dc):
91 print('''
92 +------------------------+
93 | Users Infos |
94 +------------------------+
95 ''')
96 for user in users:
97 desc = user['attributes'].get('description')[0] if user['attributes'].get('description') else "(null)"
98 print("Account: " + dc + "\\" + user['attributes']['sAMAccountName'][0] + "\tName: " + user['attributes']['name'][0] + "\tDesc: " + desc)
99
100 print("")
101 for user in users:
102 print("user:[" + user['attributes']['sAMAccountName'][0] + "]")
103 print("")
104
105 def groups_info(self, groups, dc):
106 print('''
107 +------------------------+import os.path
108 | Groups Infos |
109 +------------------------+
110 ''')
111 for group in groups:
112 print("group:[" + group['attributes']['name'][0] + "]")
113
114 for group in groups:
115 if group['attributes'].get('member'):
116 users = re.findall(r"^CN=([\w\s\-\_\{\}\.\$\#]+)", '\n'.join(group['attributes']['member']), re.M)
117 if users:
118 print("\n[+] Getting domain group memberships:")
119 for user in users:
120 if user == "S-1-5-11":
121 user = "NT AUTHORITY\\Authenticated Users"
122 elif user == "S-1-5-4":
123 user = "NT AUTHORITY\\INTERACTIVE"
124 elif user == "S-1-5-17":
125 user = "NT AUTHORITY\\IUSR"
126 print("Group '" + group['attributes']['name'][0] + "' has member: " + dc + "\\" + user)
127
128 def main():
129 parser = argparse.ArgumentParser(description='LDAPDomainDump to pretty output like enum4linux.')
130
131 #Main parameters
132 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")
133 args = parser.parse_args()
134
135 if args.directory:
136 with open(args.directory + '/domain_policy.json') as f:
137 domain = json.load(f)
138 with open(args.directory + '/domain_users.json') as f:
139 users = json.load(f)
140 with open(args.directory + '/domain_groups.json') as f:
141 groups = json.load(f)
142 pretty = PrettyOuput()
143 dc = pretty.domain_info(domain)
144 pretty.password_complexity(domain)
145 pretty.user_info(users, dc.upper())
146 pretty.groups_info(groups, dc.upper())
147 else:
148 print("Missing parameter --directory /output/")
149
150 if __name__ == "__main__":
151 main()
0 Metadata-Version: 1.0
1 Name: ldapdomaindump
2 Version: 0.9.3
3 Summary: Active Directory information dumper via LDAP
4 Home-page: https://github.com/dirkjanm/ldapdomaindump/
5 Author: Dirk-jan Mollema
6 Author-email: [email protected]
7 License: UNKNOWN
8 Description: UNKNOWN
9 Platform: UNKNOWN
0 LICENSE
1 MANIFEST.in
2 Readme.md
3 setup.py
4 bin/ldapdomaindump
5 bin/ldd2bloodhound
6 bin/ldd2pretty
7 ldapdomaindump/__init__.py
8 ldapdomaindump/__main__.py
9 ldapdomaindump/convert.py
10 ldapdomaindump/pretty.py
11 ldapdomaindump/style.css
12 ldapdomaindump.egg-info/PKG-INFO
13 ldapdomaindump.egg-info/SOURCES.txt
14 ldapdomaindump.egg-info/dependency_links.txt
15 ldapdomaindump.egg-info/requires.txt
16 ldapdomaindump.egg-info/top_level.txt
0 dnspython
1 future
2 ldap3!=2.5.0,!=2.5.2,!=2.6,>=2.5
+0
-3
ldapdomaindump.py less more
0 #!/usr/bin/env python
1 import ldapdomaindump
2 ldapdomaindump.main()
0 [egg_info]
1 tag_build =
2 tag_date = 0
3
0 from setuptools import setup
1 setup(name='ldapdomaindump',
2 version='0.9.1',
3 description='Active Directory information dumper via LDAP',
4 author='Dirk-jan Mollema',
5 author_email='[email protected]',
6 url='https://github.com/dirkjanm/ldapdomaindump/',
7 packages=['ldapdomaindump'],
8 install_requires=['dnspython', 'ldap3==2.5.1', 'future'],
9 package_data={'ldapdomaindump': ['style.css']},
10 include_package_data=True,
11 scripts=['bin/ldapdomaindump', 'bin/ldd2bloodhound']
12 )
0 from setuptools import setup
1 setup(name='ldapdomaindump',
2 version='0.9.3',
3 description='Active Directory information dumper via LDAP',
4 author='Dirk-jan Mollema',
5 author_email='[email protected]',
6 url='https://github.com/dirkjanm/ldapdomaindump/',
7 packages=['ldapdomaindump'],
8 install_requires=['dnspython', 'ldap3>=2.5,!=2.5.2,!=2.5.0,!=2.6', 'future'],
9 package_data={'ldapdomaindump': ['style.css']},
10 include_package_data=True,
11 scripts=['bin/ldapdomaindump', 'bin/ldd2bloodhound', 'bin/ldd2pretty']
12 )