|
0 |
####################
|
|
1 |
#
|
|
2 |
# Copyright (c) 2017 Dirk-jan Mollema
|
|
3 |
#
|
|
4 |
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
5 |
# of this software and associated documentation files (the "Software"), to deal
|
|
6 |
# in the Software without restriction, including without limitation the rights
|
|
7 |
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
8 |
# copies of the Software, and to permit persons to whom the Software is
|
|
9 |
# furnished to do so, subject to the following conditions:
|
|
10 |
#
|
|
11 |
# The above copyright notice and this permission notice shall be included in all
|
|
12 |
# copies or substantial portions of the Software.
|
|
13 |
#
|
|
14 |
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
15 |
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
16 |
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
17 |
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
18 |
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
19 |
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
20 |
# SOFTWARE.
|
|
21 |
#
|
|
22 |
####################
|
|
23 |
|
|
24 |
import sys, os, re, codecs, json, argparse, getpass, base64
|
|
25 |
# import class and constants
|
|
26 |
from datetime import datetime
|
|
27 |
from urllib import quote_plus
|
|
28 |
|
|
29 |
import ldap3
|
|
30 |
from ldap3 import Server, Connection, SIMPLE, SYNC, ALL, SASL, NTLM
|
|
31 |
from ldap3.core.exceptions import LDAPKeyError, LDAPAttributeError, LDAPCursorError
|
|
32 |
from ldap3.abstract import attribute, attrDef
|
|
33 |
from ldap3.utils import dn
|
|
34 |
from ldap3.protocol.formatters.formatters import format_sid
|
|
35 |
|
|
36 |
# dnspython, for resolving hostnames
|
|
37 |
import dns.resolver
|
|
38 |
|
|
39 |
|
|
40 |
# User account control flags
|
|
41 |
# From: https://blogs.technet.microsoft.com/askpfeplat/2014/01/15/understanding-the-useraccountcontrol-attribute-in-active-directory/
|
|
42 |
uac_flags = {'ACCOUNT_DISABLED':0x00000002,
|
|
43 |
'ACCOUNT_LOCKED':0x00000010,
|
|
44 |
'PASSWD_NOTREQD':0x00000020,
|
|
45 |
'PASSWD_CANT_CHANGE': 0x00000040,
|
|
46 |
'NORMAL_ACCOUNT': 0x00000200,
|
|
47 |
'WORKSTATION_ACCOUNT':0x00001000,
|
|
48 |
'SERVER_TRUST_ACCOUNT': 0x00002000,
|
|
49 |
'DONT_EXPIRE_PASSWD': 0x00010000,
|
|
50 |
'SMARTCARD_REQUIRED': 0x00040000,
|
|
51 |
'TRUSTED_FOR_DELEGATION': 0x00080000,
|
|
52 |
'NOT_DELEGATED': 0x00100000,
|
|
53 |
'USE_DES_KEY_ONLY': 0x00200000,
|
|
54 |
'DONT_REQ_PREAUTH': 0x00400000,
|
|
55 |
'PASSWORD_EXPIRED': 0x00800000,
|
|
56 |
'TRUSTED_TO_AUTH_FOR_DELEGATION': 0x01000000,
|
|
57 |
'PARTIAL_SECRETS_ACCOUNT': 0x04000000
|
|
58 |
}
|
|
59 |
|
|
60 |
# Password policy flags
|
|
61 |
pwd_flags = {'PASSWORD_COMPLEX':0x01,
|
|
62 |
'PASSWORD_NO_ANON_CHANGE': 0x02,
|
|
63 |
'PASSWORD_NO_CLEAR_CHANGE': 0x04,
|
|
64 |
'LOCKOUT_ADMINS': 0x08,
|
|
65 |
'PASSWORD_STORE_CLEARTEXT': 0x10,
|
|
66 |
'REFUSE_PASSWORD_CHANGE': 0x20}
|
|
67 |
|
|
68 |
# Domain trust flags
|
|
69 |
# From: https://msdn.microsoft.com/en-us/library/cc223779.aspx
|
|
70 |
trust_flags = {'NON_TRANSITIVE':0x00000001,
|
|
71 |
'UPLEVEL_ONLY':0x00000002,
|
|
72 |
'QUARANTINED_DOMAIN':0x00000004,
|
|
73 |
'FOREST_TRANSITIVE':0x00000008,
|
|
74 |
'CROSS_ORGANIZATION':0x00000010,
|
|
75 |
'WITHIN_FOREST':0x00000020,
|
|
76 |
'TREAT_AS_EXTERNAL':0x00000040,
|
|
77 |
'USES_RC4_ENCRYPTION':0x00000080,
|
|
78 |
'CROSS_ORGANIZATION_NO_TGT_DELEGATION':0x00000200,
|
|
79 |
'PIM_TRUST':0x00000400}
|
|
80 |
|
|
81 |
# Domain trust direction
|
|
82 |
# From: https://msdn.microsoft.com/en-us/library/cc223768.aspx
|
|
83 |
trust_directions = {'INBOUND':0x01,
|
|
84 |
'OUTBOUND':0x02,
|
|
85 |
'BIDIRECTIONAL':0x03}
|
|
86 |
# Domain trust types
|
|
87 |
trust_type = {'DOWNLEVEL':0x01,
|
|
88 |
'UPLEVEL':0x02,
|
|
89 |
'MIT':0x03}
|
|
90 |
|
|
91 |
# Common attribute pretty translations
|
|
92 |
attr_translations = {'sAMAccountName':'SAM Name',
|
|
93 |
'cn':'CN',
|
|
94 |
'operatingSystem':'Operating System',
|
|
95 |
'operatingSystemServicePack':'Service Pack',
|
|
96 |
'operatingSystemVersion':'OS Version',
|
|
97 |
'userAccountControl':'Flags',
|
|
98 |
'objectSid':'SID',
|
|
99 |
'memberOf':'Member of groups',
|
|
100 |
'primaryGroupId':'Primary group',
|
|
101 |
'dNSHostName':'DNS Hostname',
|
|
102 |
'whenCreated':'Created on',
|
|
103 |
'whenChanged':'Changed on',
|
|
104 |
'IPv4':'IPv4 Address',
|
|
105 |
'lockOutObservationWindow':'Lockout time window',
|
|
106 |
'lockoutDuration':'Lockout Duration',
|
|
107 |
'lockoutThreshold':'Lockout Threshold',
|
|
108 |
'maxPwdAge':'Max password age',
|
|
109 |
'minPwdAge':'Min password age',
|
|
110 |
'minPwdLength':'Min password length'}
|
|
111 |
|
|
112 |
MINIMAL_COMPUTERATTRIBUTES = ['cn', 'sAMAccountName', 'dNSHostName', 'operatingSystem', 'operatingSystemServicePack', 'operatingSystemVersion', 'lastLogon', 'userAccountControl', 'whenCreated', 'objectSid', 'description', 'objectClass']
|
|
113 |
MINIMAL_USERATTRIBUTES = ['cn', 'name', 'sAMAccountName', 'memberOf', 'primaryGroupId', 'whenCreated', 'whenChanged', 'lastLogon', 'userAccountControl', 'pwdLastSet', 'objectSid', 'description', 'objectClass']
|
|
114 |
MINIMAL_GROUPATTRIBUTES = ['cn', 'name', 'sAMAccountName', 'memberOf', 'description', 'whenCreated', 'whenChanged', 'objectSid', 'distinguishedName', 'objectClass']
|
|
115 |
|
|
116 |
#Class containing the default config
|
|
117 |
class domainDumpConfig(object):
|
|
118 |
def __init__(self):
|
|
119 |
#Base path
|
|
120 |
self.basepath = '.'
|
|
121 |
|
|
122 |
#Output files basenames
|
|
123 |
self.groupsfile = 'domain_groups' #Groups
|
|
124 |
self.usersfile = 'domain_users' #User accounts
|
|
125 |
self.computersfile = 'domain_computers' #Computer accounts
|
|
126 |
self.policyfile = 'domain_policy' #General domain attributes
|
|
127 |
self.trustsfile = 'domain_trusts' #Domain trusts attributes
|
|
128 |
|
|
129 |
#Combined files basenames
|
|
130 |
self.users_by_group = 'domain_users_by_group' #Users sorted by group
|
|
131 |
self.computers_by_os = 'domain_computers_by_os' #Computers sorted by OS
|
|
132 |
|
|
133 |
#Output formats
|
|
134 |
self.outputhtml = True
|
|
135 |
self.outputjson = True
|
|
136 |
self.outputgrep = True
|
|
137 |
|
|
138 |
#Output json for groups
|
|
139 |
self.groupedjson = False
|
|
140 |
|
|
141 |
#Default field delimiter for greppable format is a tab
|
|
142 |
self.grepsplitchar = '\t'
|
|
143 |
|
|
144 |
#Other settings
|
|
145 |
self.lookuphostnames = False #Look up hostnames of computers to get their IP address
|
|
146 |
self.dnsserver = '' #Addres of the DNS server to use, if not specified default DNS will be used
|
|
147 |
self.minimal = False #Only query minimal list of attributes
|
|
148 |
|
|
149 |
#Domaindumper main class
|
|
150 |
class domainDumper(object):
|
|
151 |
def __init__(self, server, connection, config, root=None):
|
|
152 |
self.server = server
|
|
153 |
self.connection = connection
|
|
154 |
self.config = config
|
|
155 |
#Unless the root is specified we get it from the server
|
|
156 |
if root is None:
|
|
157 |
self.root = self.getRoot()
|
|
158 |
else:
|
|
159 |
self.root = root
|
|
160 |
self.users = None #Domain users
|
|
161 |
self.groups = None #Domain groups
|
|
162 |
self.computers = None #Domain computers
|
|
163 |
self.policy = None #Domain policy
|
|
164 |
self.groups_dnmap = None #CN map for group IDs to CN
|
|
165 |
self.groups_dict = None #Dictionary of groups by CN
|
|
166 |
self.trusts = None #Domain trusts
|
|
167 |
|
|
168 |
#Get the server root from the default naming context
|
|
169 |
def getRoot(self):
|
|
170 |
return self.server.info.other['defaultNamingContext'][0]
|
|
171 |
|
|
172 |
#Query the groups of the current user
|
|
173 |
def getCurrentUserGroups(self, username, domainsid=None):
|
|
174 |
self.connection.search(self.root, '(&(objectCategory=person)(objectClass=user)(sAMAccountName=%s))' % username, attributes=['cn', 'memberOf', 'primaryGroupId'])
|
|
175 |
try:
|
|
176 |
groups = self.connection.entries[0]['memberOf'].values
|
|
177 |
if domainsid is not None:
|
|
178 |
groups.append(self.getGroupDNfromID(domainsid, self.connection.entries[0]['primaryGroupId'].value))
|
|
179 |
return groups
|
|
180 |
except LDAPKeyError:
|
|
181 |
#No groups, probably just member of the primary group
|
|
182 |
if domainsid is not None:
|
|
183 |
primarygroup = self.getGroupDNfromID(domainsid, self.connection.entries[0]['primaryGroupId'].value)
|
|
184 |
return [primarygroup]
|
|
185 |
else:
|
|
186 |
return []
|
|
187 |
except IndexError:
|
|
188 |
#The username does not exist (might be a computer account)
|
|
189 |
return []
|
|
190 |
|
|
191 |
#Check if the user is part of the Domain Admins or Enterprise Admins group, or any of their subgroups
|
|
192 |
def isDomainAdmin(self, username):
|
|
193 |
domainsid = self.getRootSid()
|
|
194 |
groups = self.getCurrentUserGroups(username, domainsid)
|
|
195 |
#Get DA and EA group DNs
|
|
196 |
dagroupdn = self.getDAGroupDN(domainsid)
|
|
197 |
eagroupdn = self.getEAGroupDN(domainsid)
|
|
198 |
#First, simple checks
|
|
199 |
for group in groups:
|
|
200 |
if 'CN=Administrators' in group or 'CN=Domain Admins' in group or dagroupdn == group:
|
|
201 |
return True
|
|
202 |
#Also for enterprise admins if applicable
|
|
203 |
if 'CN=Enterprise Admins' in group or (eagroupdn is not False and eagroupdn == group):
|
|
204 |
return True
|
|
205 |
#Now, just do a recursive check in both groups and their subgroups using LDAP_MATCHING_RULE_IN_CHAIN
|
|
206 |
self.connection.search(self.root, '(&(objectCategory=person)(objectClass=user)(sAMAccountName=%s)(memberOf:1.2.840.113556.1.4.1941:=%s))' % (username, dagroupdn), attributes=['cn', 'sAMAccountName'])
|
|
207 |
if len(self.connection.entries) > 0:
|
|
208 |
return True
|
|
209 |
self.connection.search(self.root, '(&(objectCategory=person)(objectClass=user)(sAMAccountName=%s)(memberOf:1.2.840.113556.1.4.1941:=%s))' % (username, eagroupdn), attributes=['cn', 'sAMAccountName'])
|
|
210 |
if len(self.connection.entries) > 0:
|
|
211 |
return True
|
|
212 |
#At last, check the users primary group ID
|
|
213 |
return False
|
|
214 |
|
|
215 |
#Get all users
|
|
216 |
def getAllUsers(self):
|
|
217 |
if self.config.minimal:
|
|
218 |
self.connection.extend.standard.paged_search('%s' % (self.root), '(&(objectCategory=person)(objectClass=user))', attributes=MINIMAL_USERATTRIBUTES, paged_size=500, generator=False)
|
|
219 |
else:
|
|
220 |
self.connection.extend.standard.paged_search('%s' % (self.root), '(&(objectCategory=person)(objectClass=user))', attributes=ldap3.ALL_ATTRIBUTES, paged_size=500, generator=False)
|
|
221 |
return self.connection.entries
|
|
222 |
|
|
223 |
#Get all computers in the domain
|
|
224 |
def getAllComputers(self):
|
|
225 |
if self.config.minimal:
|
|
226 |
self.connection.extend.standard.paged_search('%s' % (self.root), '(&(objectClass=computer)(objectClass=user))', attributes=MINIMAL_COMPUTERATTRIBUTES, paged_size=500, generator=False)
|
|
227 |
else:
|
|
228 |
self.connection.extend.standard.paged_search('%s' % (self.root), '(&(objectClass=computer)(objectClass=user))', attributes=ldap3.ALL_ATTRIBUTES, paged_size=500, generator=False)
|
|
229 |
return self.connection.entries
|
|
230 |
|
|
231 |
#Get all user SPNs
|
|
232 |
def getAllUserSpns(self):
|
|
233 |
if self.config.minimal:
|
|
234 |
self.connection.extend.standard.paged_search('%s' % (self.root), '(&(objectCategory=person)(objectClass=user)(servicePrincipalName=*))', attributes=MINIMAL_USERATTRIBUTES, paged_size=500, generator=False)
|
|
235 |
else:
|
|
236 |
self.connection.extend.standard.paged_search('%s' % (self.root), '(&(objectCategory=person)(objectClass=user)(servicePrincipalName=*))', attributes=ldap3.ALL_ATTRIBUTES, paged_size=500, generator=False)
|
|
237 |
return self.connection.entries
|
|
238 |
|
|
239 |
#Get all defined groups
|
|
240 |
def getAllGroups(self):
|
|
241 |
if self.config.minimal:
|
|
242 |
self.connection.extend.standard.paged_search(self.root, '(objectClass=group)', attributes=MINIMAL_GROUPATTRIBUTES, paged_size=500, generator=False)
|
|
243 |
else:
|
|
244 |
self.connection.extend.standard.paged_search(self.root, '(objectClass=group)', attributes=ldap3.ALL_ATTRIBUTES, paged_size=500, generator=False)
|
|
245 |
return self.connection.entries
|
|
246 |
|
|
247 |
#Get the domain policies (such as lockout policy)
|
|
248 |
def getDomainPolicy(self):
|
|
249 |
self.connection.search(self.root, '(objectClass=domain)', attributes=ldap3.ALL_ATTRIBUTES)
|
|
250 |
return self.connection.entries
|
|
251 |
|
|
252 |
#Get domain trusts
|
|
253 |
def getTrusts(self):
|
|
254 |
self.connection.search(self.root, '(objectClass=trustedDomain)', attributes=ldap3.ALL_ATTRIBUTES)
|
|
255 |
return self.connection.entries
|
|
256 |
|
|
257 |
#Get all defined security groups
|
|
258 |
#Syntax from:
|
|
259 |
#https://ldapwiki.willeke.com/wiki/Active%20Directory%20Group%20Related%20Searches
|
|
260 |
def getAllSecurityGroups(self):
|
|
261 |
self.connection.search(self.root, '(groupType:1.2.840.113556.1.4.803:=2147483648)', attributes=ldap3.ALL_ATTRIBUTES)
|
|
262 |
return self.connection.entries
|
|
263 |
|
|
264 |
#Get the SID of the root object
|
|
265 |
def getRootSid(self):
|
|
266 |
self.connection.search(self.root, '(objectClass=domain)', attributes=['objectSid'])
|
|
267 |
try:
|
|
268 |
sid = self.connection.entries[0].objectSid
|
|
269 |
except (LDAPAttributeError, LDAPCursorError, IndexError):
|
|
270 |
return False
|
|
271 |
return sid
|
|
272 |
|
|
273 |
#Get group members recursively using LDAP_MATCHING_RULE_IN_CHAIN (1.2.840.113556.1.4.1941)
|
|
274 |
def getRecursiveGroupmembers(self, groupdn):
|
|
275 |
self.connection.extend.standard.paged_search(self.root, '(&(objectCategory=person)(objectClass=user)(memberOf:1.2.840.113556.1.4.1941:=%s))' % groupdn, attributes=MINIMAL_USERATTRIBUTES, paged_size=500, generator=False)
|
|
276 |
return self.connection.entries
|
|
277 |
|
|
278 |
#Resolve group ID to DN
|
|
279 |
def getGroupDNfromID(self, domainsid, gid):
|
|
280 |
self.connection.search(self.root, '(objectSid=%s-%d)' % (domainsid, gid), attributes=['distinguishedName'])
|
|
281 |
return self.connection.entries[0]['distinguishedName'].value
|
|
282 |
|
|
283 |
#Get Domain Admins group DN
|
|
284 |
def getDAGroupDN(self, domainsid):
|
|
285 |
return self.getGroupDNfromID(domainsid, 512)
|
|
286 |
|
|
287 |
#Get Enterprise Admins group DN
|
|
288 |
def getEAGroupDN(self, domainsid):
|
|
289 |
try:
|
|
290 |
return self.getGroupDNfromID(domainsid, 519)
|
|
291 |
except (LDAPAttributeError, LDAPCursorError, IndexError):
|
|
292 |
#This does not exist, could be in a parent domain
|
|
293 |
return False
|
|
294 |
|
|
295 |
|
|
296 |
#Lookup all computer DNS names to get their IP
|
|
297 |
def lookupComputerDnsNames(self):
|
|
298 |
dnsresolver = dns.resolver.Resolver()
|
|
299 |
dnsresolver.lifetime = 2
|
|
300 |
ipdef = attrDef.AttrDef('ipv4')
|
|
301 |
if self.config.dnsserver != '':
|
|
302 |
dnsresolver.nameservers = [self.config.dnsserver]
|
|
303 |
for computer in self.computers:
|
|
304 |
try:
|
|
305 |
answers = dnsresolver.query(computer.dNSHostName.values[0], 'A')
|
|
306 |
ip = str(answers.response.answer[0][0])
|
|
307 |
except dns.resolver.NXDOMAIN:
|
|
308 |
ip = 'error.NXDOMAIN'
|
|
309 |
except dns.resolver.Timeout:
|
|
310 |
ip = 'error.TIMEOUT'
|
|
311 |
except (LDAPAttributeError, LDAPCursorError):
|
|
312 |
ip = 'error.NOHOSTNAME'
|
|
313 |
#Construct a custom attribute as workaround
|
|
314 |
ipatt = attribute.Attribute(ipdef, computer, None)
|
|
315 |
ipatt.__dict__['_response'] = ip
|
|
316 |
ipatt.__dict__['raw_values'] = [ip]
|
|
317 |
ipatt.__dict__['values'] = [ip]
|
|
318 |
#Add the attribute to the entry's dictionary
|
|
319 |
computer._state.attributes['IPv4'] = ipatt
|
|
320 |
|
|
321 |
#Create a dictionary of all operating systems with the computer accounts that are associated
|
|
322 |
def sortComputersByOS(self, items):
|
|
323 |
osdict = {}
|
|
324 |
for computer in items:
|
|
325 |
try:
|
|
326 |
cos = computer.operatingSystem.value or 'Unknown'
|
|
327 |
except (LDAPAttributeError, LDAPCursorError):
|
|
328 |
cos = 'Unknown'
|
|
329 |
try:
|
|
330 |
osdict[cos].append(computer)
|
|
331 |
except KeyError:
|
|
332 |
#New OS
|
|
333 |
osdict[cos] = [computer]
|
|
334 |
return osdict
|
|
335 |
|
|
336 |
#Map all groups on their ID (taken from their SID) to CNs
|
|
337 |
#This is used for getting the primary group of a user
|
|
338 |
def mapGroupsIdsToDns(self):
|
|
339 |
dnmap = {}
|
|
340 |
for group in self.groups:
|
|
341 |
gid = int(group.objectSid.value.split('-')[-1])
|
|
342 |
dnmap[gid] = group.distinguishedName.values[0]
|
|
343 |
self.groups_dnmap = dnmap
|
|
344 |
return dnmap
|
|
345 |
|
|
346 |
#Create a dictionary where a groups CN returns the full object
|
|
347 |
def createGroupsDictByCn(self):
|
|
348 |
gdict = {grp.cn.values[0]:grp for grp in self.groups}
|
|
349 |
self.groups_dict = gdict
|
|
350 |
return gdict
|
|
351 |
|
|
352 |
#Get CN from DN
|
|
353 |
def getGroupCnFromDn(self, dnin):
|
|
354 |
cn = self.unescapecn(dn.parse_dn(dnin)[0][1])
|
|
355 |
return cn
|
|
356 |
|
|
357 |
#Unescape special DN characters from a CN (only needed if it comes from a DN)
|
|
358 |
def unescapecn(self, cn):
|
|
359 |
for c in ' "#+,;<=>\\\00':
|
|
360 |
cn = cn.replace('\\'+c, c)
|
|
361 |
return cn
|
|
362 |
|
|
363 |
#Sort users by group they belong to
|
|
364 |
def sortUsersByGroup(self, items):
|
|
365 |
groupsdict = {}
|
|
366 |
#Make sure the group CN mapping already exists
|
|
367 |
if self.groups_dnmap is None:
|
|
368 |
self.mapGroupsIdsToDns()
|
|
369 |
for user in items:
|
|
370 |
try:
|
|
371 |
ugroups = [self.getGroupCnFromDn(group) for group in user.memberOf.values]
|
|
372 |
#If the user is only in the default group, its memberOf property wont exist
|
|
373 |
except (LDAPAttributeError, LDAPCursorError):
|
|
374 |
ugroups = []
|
|
375 |
#Add the user default group
|
|
376 |
ugroups.append(self.getGroupCnFromDn(self.groups_dnmap[user.primaryGroupId.value]))
|
|
377 |
for group in ugroups:
|
|
378 |
try:
|
|
379 |
groupsdict[group].append(user)
|
|
380 |
except KeyError:
|
|
381 |
#Group is not yet in dict
|
|
382 |
groupsdict[group] = [user]
|
|
383 |
|
|
384 |
#Append any groups that are members of groups
|
|
385 |
for group in self.groups:
|
|
386 |
try:
|
|
387 |
for parentgroup in group.memberOf.values:
|
|
388 |
try:
|
|
389 |
groupsdict[self.getGroupCnFromDn(parentgroup)].append(group)
|
|
390 |
except KeyError:
|
|
391 |
#Group is not yet in dict
|
|
392 |
groupsdict[self.getGroupCnFromDn(parentgroup)] = [group]
|
|
393 |
#Without subgroups this attribute does not exist
|
|
394 |
except (LDAPAttributeError, LDAPCursorError):
|
|
395 |
pass
|
|
396 |
|
|
397 |
return groupsdict
|
|
398 |
|
|
399 |
#Main function
|
|
400 |
def domainDump(self):
|
|
401 |
self.users = self.getAllUsers()
|
|
402 |
self.computers = self.getAllComputers()
|
|
403 |
self.groups = self.getAllGroups()
|
|
404 |
if self.config.lookuphostnames:
|
|
405 |
self.lookupComputerDnsNames()
|
|
406 |
self.policy = self.getDomainPolicy()
|
|
407 |
self.trusts = self.getTrusts()
|
|
408 |
rw = reportWriter(self.config)
|
|
409 |
rw.generateUsersReport(self)
|
|
410 |
rw.generateGroupsReport(self)
|
|
411 |
rw.generateComputersReport(self)
|
|
412 |
rw.generatePolicyReport(self)
|
|
413 |
rw.generateTrustsReport(self)
|
|
414 |
rw.generateComputersByOsReport(self)
|
|
415 |
rw.generateUsersByGroupReport(self)
|
|
416 |
|
|
417 |
class reportWriter(object):
|
|
418 |
def __init__(self, config):
|
|
419 |
self.config = config
|
|
420 |
self.dd = None
|
|
421 |
if self.config.lookuphostnames:
|
|
422 |
self.computerattributes = ['cn', 'sAMAccountName', 'dNSHostName', 'IPv4', 'operatingSystem', 'operatingSystemServicePack', 'operatingSystemVersion', 'lastLogon', 'userAccountControl', 'whenCreated', 'objectSid', 'description']
|
|
423 |
else:
|
|
424 |
self.computerattributes = ['cn', 'sAMAccountName', 'dNSHostName', 'operatingSystem', 'operatingSystemServicePack', 'operatingSystemVersion', 'lastLogon', 'userAccountControl', 'whenCreated', 'objectSid', 'description']
|
|
425 |
self.userattributes = ['cn', 'name', 'sAMAccountName', 'memberOf', 'primaryGroupId', 'whenCreated', 'whenChanged', 'lastLogon', 'userAccountControl', 'pwdLastSet', 'objectSid', 'description']
|
|
426 |
#In grouped view, don't include the memberOf property to reduce output size
|
|
427 |
self.userattributes_grouped = ['cn', 'name', 'sAMAccountName', 'whenCreated', 'whenChanged', 'lastLogon', 'userAccountControl', 'pwdLastSet', 'objectSid', 'description']
|
|
428 |
self.groupattributes = ['cn', 'sAMAccountName', 'memberOf', 'description', 'whenCreated', 'whenChanged', 'objectSid']
|
|
429 |
self.policyattributes = ['cn', 'lockOutObservationWindow', 'lockoutDuration', 'lockoutThreshold', 'maxPwdAge', 'minPwdAge', 'minPwdLength', 'pwdHistoryLength', 'pwdProperties']
|
|
430 |
self.trustattributes = ['cn', 'flatName', 'securityIdentifier', 'trustAttributes', 'trustDirection', 'trustType']
|
|
431 |
|
|
432 |
#Escape HTML special chars
|
|
433 |
def htmlescape(self, html):
|
|
434 |
return (html.replace("&", "&").replace("<", "<").replace(">", ">").replace("'", "'").replace('"', """))
|
|
435 |
|
|
436 |
#Unescape special DN characters from a CN (only needed if it comes from a DN)
|
|
437 |
def unescapecn(self, cn):
|
|
438 |
for c in ' "#+,;<=>\\\00':
|
|
439 |
cn = cn.replace('\\'+c, c)
|
|
440 |
return cn
|
|
441 |
|
|
442 |
#Convert password max age (in 100 nanoseconds), to days
|
|
443 |
def nsToDays(self, length):
|
|
444 |
return abs(length) * .0000001 / 86400
|
|
445 |
|
|
446 |
def nsToMinutes(self, length):
|
|
447 |
return abs(length) * .0000001 / 60
|
|
448 |
|
|
449 |
#Parse bitwise flags into a list
|
|
450 |
def parseFlags(self, attr, flags_def):
|
|
451 |
outflags = []
|
|
452 |
if attr is None:
|
|
453 |
return outflags
|
|
454 |
for flag, val in flags_def.items():
|
|
455 |
if attr.value & val:
|
|
456 |
outflags.append(flag)
|
|
457 |
return outflags
|
|
458 |
|
|
459 |
#Parse bitwise trust direction - only one flag applies here, 0x03 overlaps
|
|
460 |
def parseTrustDirection(self, attr, flags_def):
|
|
461 |
outflags = []
|
|
462 |
if attr is None:
|
|
463 |
return outflags
|
|
464 |
for flag, val in flags_def.items():
|
|
465 |
if attr.value == val:
|
|
466 |
outflags.append(flag)
|
|
467 |
return outflags
|
|
468 |
|
|
469 |
#Generate a HTML table from a list of entries, with the specified attributes as column
|
|
470 |
def generateHtmlTable(self, listable, attributes, header='', firstTable=True, specialGroupsFormat=False):
|
|
471 |
of = []
|
|
472 |
#Only if this is the first table it is an actual table, the others are just bodies of the first table
|
|
473 |
#This makes sure that multiple tables have their columns aligned to make it less messy
|
|
474 |
if firstTable:
|
|
475 |
of.append(u'<table>')
|
|
476 |
#Table header
|
|
477 |
if header != '':
|
|
478 |
of.append(u'<thead><tr><td colspan="%d" id="cn_%s">%s</td></tr></thead>' % (len(attributes), self.formatId(header), header))
|
|
479 |
of.append(u'<tbody><tr>')
|
|
480 |
for hdr in attributes:
|
|
481 |
try:
|
|
482 |
#Print alias of this attribute if there is one
|
|
483 |
of.append(u'<th>%s</th>' % self.htmlescape(attr_translations[hdr]))
|
|
484 |
except KeyError:
|
|
485 |
of.append(u'<th>%s</th>' % self.htmlescape(hdr))
|
|
486 |
of.append(u'</tr>\n')
|
|
487 |
for li in listable:
|
|
488 |
#Whether we should format group objects separately
|
|
489 |
if specialGroupsFormat and 'group' in li['objectClass'].values:
|
|
490 |
#Give it an extra class and pass it to the function below to make sure the CN is a link
|
|
491 |
liIsGroup = True
|
|
492 |
of.append(u'<tr class="group">')
|
|
493 |
else:
|
|
494 |
liIsGroup = False
|
|
495 |
of.append(u'<tr>')
|
|
496 |
for att in attributes:
|
|
497 |
try:
|
|
498 |
of.append(u'<td>%s</td>' % self.formatAttribute(li[att], liIsGroup))
|
|
499 |
except (LDAPKeyError, LDAPCursorError):
|
|
500 |
of.append(u'<td> </td>')
|
|
501 |
of.append(u'</tr>\n')
|
|
502 |
of.append(u'</tbody>\n')
|
|
503 |
return u''.join(of)
|
|
504 |
|
|
505 |
#Generate several HTML tables for grouped reports
|
|
506 |
def generateGroupedHtmlTables(self, groups, attributes):
|
|
507 |
first = True
|
|
508 |
for groupname, members in groups.iteritems():
|
|
509 |
yield self.generateHtmlTable(members, attributes, groupname, first, specialGroupsFormat=True)
|
|
510 |
if first:
|
|
511 |
first = False
|
|
512 |
|
|
513 |
#Write generated HTML to file
|
|
514 |
def writeHtmlFile(self, rel_outfile, body, genfunc=None, genargs=None, closeTable=True):
|
|
515 |
if not os.path.exists(self.config.basepath):
|
|
516 |
os.makedirs(self.config.basepath)
|
|
517 |
outfile = os.path.join(self.config.basepath, rel_outfile)
|
|
518 |
with codecs.open(outfile, 'w', 'utf8') as of:
|
|
519 |
of.write('<!DOCTYPE html>\n<html>\n<head><meta charset="UTF-8">')
|
|
520 |
#Include the style
|
|
521 |
try:
|
|
522 |
with open(os.path.join(os.path.dirname(__file__), 'style.css'), 'r') as sf:
|
|
523 |
of.write('<style type="text/css">')
|
|
524 |
of.write(sf.read())
|
|
525 |
of.write('</style>')
|
|
526 |
except IOError:
|
|
527 |
log_warn('style.css not found in package directory, styling will be skipped')
|
|
528 |
of.write('</head><body>')
|
|
529 |
#If the generator is not specified, we should write the HTML blob directly
|
|
530 |
if genfunc is None:
|
|
531 |
of.write(body)
|
|
532 |
else:
|
|
533 |
for tpart in genfunc(*genargs):
|
|
534 |
of.write(tpart)
|
|
535 |
#Does the body contain an open table?
|
|
536 |
if closeTable:
|
|
537 |
of.write('</table>')
|
|
538 |
of.write('</body></html>')
|
|
539 |
|
|
540 |
#Write generated JSON to file
|
|
541 |
def writeJsonFile(self, rel_outfile, jsondata, genfunc=None, genargs=None):
|
|
542 |
if not os.path.exists(self.config.basepath):
|
|
543 |
os.makedirs(self.config.basepath)
|
|
544 |
outfile = os.path.join(self.config.basepath, rel_outfile)
|
|
545 |
with codecs.open(outfile, 'w', 'utf8') as of:
|
|
546 |
#If the generator is not specified, we should write the JSON blob directly
|
|
547 |
if genfunc is None:
|
|
548 |
of.write(jsondata)
|
|
549 |
else:
|
|
550 |
for jpart in genfunc(*genargs):
|
|
551 |
of.write(jpart)
|
|
552 |
|
|
553 |
#Write generated Greppable stuff to file
|
|
554 |
def writeGrepFile(self, rel_outfile, body):
|
|
555 |
if not os.path.exists(self.config.basepath):
|
|
556 |
os.makedirs(self.config.basepath)
|
|
557 |
outfile = os.path.join(self.config.basepath, rel_outfile)
|
|
558 |
with codecs.open(outfile, 'w', 'utf8') as of:
|
|
559 |
of.write(body)
|
|
560 |
|
|
561 |
#Format a value for HTML
|
|
562 |
def formatString(self, value):
|
|
563 |
if type(value) is datetime:
|
|
564 |
try:
|
|
565 |
return value.strftime('%x %X')
|
|
566 |
except ValueError:
|
|
567 |
#Invalid date
|
|
568 |
return u'0'
|
|
569 |
if type(value) is unicode:
|
|
570 |
return value#.encode('utf8')
|
|
571 |
if type(value) is str:
|
|
572 |
return unicode(value, errors='replace')#.encode('utf8')
|
|
573 |
if type(value) is int:
|
|
574 |
return unicode(value)
|
|
575 |
if value is None:
|
|
576 |
return ''
|
|
577 |
#Other type: just return it
|
|
578 |
return value
|
|
579 |
|
|
580 |
#Format an attribute to a human readable format
|
|
581 |
def formatAttribute(self, att, formatCnAsGroup=False):
|
|
582 |
aname = att.key.lower()
|
|
583 |
#User flags
|
|
584 |
if aname == 'useraccountcontrol':
|
|
585 |
return ', '.join(self.parseFlags(att, uac_flags))
|
|
586 |
#List of groups
|
|
587 |
if aname == 'member' or aname == 'memberof' and type(att.values) is list:
|
|
588 |
return self.formatGroupsHtml(att.values)
|
|
589 |
#Primary group
|
|
590 |
if aname == 'primarygroupid':
|
|
591 |
return self.formatGroupsHtml([self.dd.groups_dnmap[att.value]])
|
|
592 |
#Pwd flags
|
|
593 |
if aname == 'pwdproperties':
|
|
594 |
return ', '.join(self.parseFlags(att, pwd_flags))
|
|
595 |
#Domain trust flags
|
|
596 |
if aname == 'trustattributes':
|
|
597 |
return ', '.join(self.parseFlags(att, trust_flags))
|
|
598 |
if aname == 'trustdirection':
|
|
599 |
if att.value == 0:
|
|
600 |
return 'DISABLED'
|
|
601 |
else:
|
|
602 |
return ', '.join(self.parseTrustDirection(att, trust_directions))
|
|
603 |
if aname == 'trusttype':
|
|
604 |
return ', '.join(self.parseFlags(att, trust_type))
|
|
605 |
if aname == 'securityidentifier':
|
|
606 |
return format_sid(att.raw_values[0])
|
|
607 |
if aname == 'minpwdage' or aname == 'maxpwdage':
|
|
608 |
return '%.2f days' % self.nsToDays(att.value)
|
|
609 |
if aname == 'lockoutobservationwindow' or aname == 'lockoutduration':
|
|
610 |
return '%.1f minutes' % self.nsToMinutes(att.value)
|
|
611 |
if aname == 'objectsid':
|
|
612 |
return '<abbr title="%s">%s</abbr>' % (att.value, att.value.split('-')[-1])
|
|
613 |
#Special case where the attribute is a CN and it should be made clear its a group
|
|
614 |
if aname == 'cn' and formatCnAsGroup:
|
|
615 |
return self.formatCnWithGroupLink(att.value)
|
|
616 |
#Other
|
|
617 |
return self.htmlescape(self.formatString(att.value))
|
|
618 |
|
|
619 |
|
|
620 |
def formatCnWithGroupLink(self, cn):
|
|
621 |
return u'Group: <a href="#cn_%s" title="%s">%s</a>' % (self.formatId(cn), self.htmlescape(cn), self.htmlescape(cn))
|
|
622 |
|
|
623 |
#Convert a CN to a valid HTML id by replacing all non-ascii characters with a _
|
|
624 |
def formatId(self, cn):
|
|
625 |
return re.sub(r'[^a-zA-Z0-9_\-]+', '_', cn)
|
|
626 |
|
|
627 |
#Format groups to readable HTML
|
|
628 |
def formatGroupsHtml(self, grouplist):
|
|
629 |
outcache = []
|
|
630 |
for group in grouplist:
|
|
631 |
cn = self.unescapecn(dn.parse_dn(group)[0][1])
|
|
632 |
outcache.append(u'<a href="%s.html#cn_%s" title="%s">%s</a>' % (self.config.users_by_group, quote_plus(self.formatId(cn)), self.htmlescape(group), self.htmlescape(cn)))
|
|
633 |
return ', '.join(outcache)
|
|
634 |
|
|
635 |
#Format groups to readable HTML
|
|
636 |
def formatGroupsGrep(self, grouplist):
|
|
637 |
outcache = []
|
|
638 |
for group in grouplist:
|
|
639 |
cn = self.unescapecn(dn.parse_dn(group)[0][1])
|
|
640 |
outcache.append(cn)
|
|
641 |
return ', '.join(outcache)
|
|
642 |
|
|
643 |
#Format attribute for grepping
|
|
644 |
def formatGrepAttribute(self, att):
|
|
645 |
aname = att.key.lower()
|
|
646 |
#User flags
|
|
647 |
if aname == 'useraccountcontrol':
|
|
648 |
return ', '.join(self.parseFlags(att, uac_flags))
|
|
649 |
#List of groups
|
|
650 |
if aname == 'member' or aname == 'memberof' and type(att.values) is list:
|
|
651 |
return self.formatGroupsGrep(att.values)
|
|
652 |
if aname == 'primarygroupid':
|
|
653 |
return self.formatGroupsGrep([self.dd.groups_dnmap[att.value]])
|
|
654 |
#Domain trust flags
|
|
655 |
if aname == 'trustattributes':
|
|
656 |
return ', '.join(self.parseFlags(att, trust_flags))
|
|
657 |
if aname == 'trustdirection':
|
|
658 |
if att.value == 0:
|
|
659 |
return 'DISABLED'
|
|
660 |
else:
|
|
661 |
return ', '.join(self.parseTrustDirection(att, trust_directions))
|
|
662 |
if aname == 'trusttype':
|
|
663 |
return ', '.join(self.parseFlags(att, trust_type))
|
|
664 |
if aname == 'securityidentifier':
|
|
665 |
return format_sid(att.raw_values[0])
|
|
666 |
#Pwd flags
|
|
667 |
if aname == 'pwdproperties':
|
|
668 |
return ', '.join(self.parseFlags(att, pwd_flags))
|
|
669 |
if aname == 'minpwdage' or aname == 'maxpwdage':
|
|
670 |
return '%.2f days' % self.nsToDays(att.value)
|
|
671 |
if aname == 'lockoutobservationwindow' or aname == 'lockoutduration':
|
|
672 |
return '%.1f minutes' % self.nsToMinutes(att.value)
|
|
673 |
return self.formatString(att.value)
|
|
674 |
|
|
675 |
#Generate grep/awk/cut-able output
|
|
676 |
def generateGrepList(self, entrylist, attributes):
|
|
677 |
hdr = self.config.grepsplitchar.join(attributes)
|
|
678 |
out = [hdr]
|
|
679 |
for entry in entrylist:
|
|
680 |
eo = []
|
|
681 |
for attr in attributes:
|
|
682 |
try:
|
|
683 |
eo.append(self.formatGrepAttribute(entry[attr]) or '')
|
|
684 |
except (LDAPKeyError, LDAPCursorError):
|
|
685 |
eo.append('')
|
|
686 |
out.append(self.config.grepsplitchar.join(eo))
|
|
687 |
return '\n'.join(out)
|
|
688 |
|
|
689 |
#Convert a list of entities to a JSON string
|
|
690 |
#String concatenation is used here since the entities have their own json generate
|
|
691 |
#method and converting the string back to json just to process it would be inefficient
|
|
692 |
def generateJsonList(self, entrylist):
|
|
693 |
out = '[' + ','.join([entry.entry_to_json() for entry in entrylist]) + ']'
|
|
694 |
return out
|
|
695 |
|
|
696 |
#Convert a group key/value pair to json
|
|
697 |
#Same methods as previous function are used
|
|
698 |
def generateJsonGroup(self, group):
|
|
699 |
out = '{%s:%s}' % (json.dumps(group[0]), self.generateJsonList(group[1]))
|
|
700 |
return out
|
|
701 |
|
|
702 |
#Convert a list of group dicts with entry lists to JSON string
|
|
703 |
#Same methods as previous functions are used, except that text is returned
|
|
704 |
#from a generator rather than allocating everything in memory
|
|
705 |
def generateJsonGroupedList(self, groups):
|
|
706 |
#Start of the list
|
|
707 |
yield '['
|
|
708 |
firstGroup = True
|
|
709 |
for group in groups.iteritems():
|
|
710 |
if not firstGroup:
|
|
711 |
#Separate items
|
|
712 |
yield ','
|
|
713 |
else:
|
|
714 |
firstGroup = False
|
|
715 |
yield self.generateJsonGroup(group)
|
|
716 |
yield ']'
|
|
717 |
|
|
718 |
#Generate report of all computers grouped by OS family
|
|
719 |
def generateComputersByOsReport(self, dd):
|
|
720 |
grouped = dd.sortComputersByOS(dd.computers)
|
|
721 |
if self.config.outputhtml:
|
|
722 |
#Use the generator approach to save memory
|
|
723 |
self.writeHtmlFile('%s.html' % self.config.computers_by_os, None, genfunc=self.generateGroupedHtmlTables, genargs=(grouped, self.computerattributes))
|
|
724 |
if self.config.outputjson and self.config.groupedjson:
|
|
725 |
self.writeJsonFile('%s.json' % self.config.computers_by_os, None, genfunc=self.generateJsonGroupedList, genargs=(grouped, ))
|
|
726 |
|
|
727 |
#Generate report of all groups and detailled user info
|
|
728 |
def generateUsersByGroupReport(self, dd):
|
|
729 |
grouped = dd.sortUsersByGroup(dd.users)
|
|
730 |
if self.config.outputhtml:
|
|
731 |
#Use the generator approach to save memory
|
|
732 |
self.writeHtmlFile('%s.html' % self.config.users_by_group, None, genfunc=self.generateGroupedHtmlTables, genargs=(grouped, self.userattributes_grouped))
|
|
733 |
if self.config.outputjson and self.config.groupedjson:
|
|
734 |
self.writeJsonFile('%s.json' % self.config.users_by_group, None, genfunc=self.generateJsonGroupedList, genargs=(grouped, ))
|
|
735 |
|
|
736 |
#Generate report with just a table of all users
|
|
737 |
def generateUsersReport(self, dd):
|
|
738 |
#Copy dd to this object, to be able to reference it
|
|
739 |
self.dd = dd
|
|
740 |
dd.mapGroupsIdsToDns()
|
|
741 |
if self.config.outputhtml:
|
|
742 |
html = self.generateHtmlTable(dd.users, self.userattributes, 'Domain users')
|
|
743 |
self.writeHtmlFile('%s.html' % self.config.usersfile, html)
|
|
744 |
if self.config.outputjson:
|
|
745 |
jsonout = self.generateJsonList(dd.users)
|
|
746 |
self.writeJsonFile('%s.json' % self.config.usersfile, jsonout)
|
|
747 |
if self.config.outputgrep:
|
|
748 |
grepout = self.generateGrepList(dd.users, self.userattributes)
|
|
749 |
self.writeGrepFile('%s.grep' % self.config.usersfile, grepout)
|
|
750 |
|
|
751 |
#Generate report with just a table of all computer accounts
|
|
752 |
def generateComputersReport(self, dd):
|
|
753 |
if self.config.outputhtml:
|
|
754 |
html = self.generateHtmlTable(dd.computers, self.computerattributes, 'Domain computer accounts')
|
|
755 |
self.writeHtmlFile('%s.html' % self.config.computersfile, html)
|
|
756 |
if self.config.outputjson:
|
|
757 |
jsonout = self.generateJsonList(dd.computers)
|
|
758 |
self.writeJsonFile('%s.json' % self.config.computersfile, jsonout)
|
|
759 |
if self.config.outputgrep:
|
|
760 |
grepout = self.generateGrepList(dd.computers, self.computerattributes)
|
|
761 |
self.writeGrepFile('%s.grep' % self.config.computersfile, grepout)
|
|
762 |
|
|
763 |
#Generate report with just a table of all computer accounts
|
|
764 |
def generateGroupsReport(self, dd):
|
|
765 |
if self.config.outputhtml:
|
|
766 |
html = self.generateHtmlTable(dd.groups, self.groupattributes, 'Domain groups')
|
|
767 |
self.writeHtmlFile('%s.html' % self.config.groupsfile, html)
|
|
768 |
if self.config.outputjson:
|
|
769 |
jsonout = self.generateJsonList(dd.groups)
|
|
770 |
self.writeJsonFile('%s.json' % self.config.groupsfile, jsonout)
|
|
771 |
if self.config.outputgrep:
|
|
772 |
grepout = self.generateGrepList(dd.groups, self.groupattributes)
|
|
773 |
self.writeGrepFile('%s.grep' % self.config.groupsfile, grepout)
|
|
774 |
|
|
775 |
#Generate policy report
|
|
776 |
def generatePolicyReport(self, dd):
|
|
777 |
if self.config.outputhtml:
|
|
778 |
html = self.generateHtmlTable(dd.policy, self.policyattributes, 'Domain policy')
|
|
779 |
self.writeHtmlFile('%s.html' % self.config.policyfile, html)
|
|
780 |
if self.config.outputjson:
|
|
781 |
jsonout = self.generateJsonList(dd.policy)
|
|
782 |
self.writeJsonFile('%s.json' % self.config.policyfile, jsonout)
|
|
783 |
if self.config.outputgrep:
|
|
784 |
grepout = self.generateGrepList(dd.policy, self.policyattributes)
|
|
785 |
self.writeGrepFile('%s.grep' % self.config.policyfile, grepout)
|
|
786 |
|
|
787 |
#Generate policy report
|
|
788 |
def generateTrustsReport(self, dd):
|
|
789 |
if self.config.outputhtml:
|
|
790 |
html = self.generateHtmlTable(dd.trusts, self.trustattributes, 'Domain trusts')
|
|
791 |
self.writeHtmlFile('%s.html' % self.config.trustsfile, html)
|
|
792 |
if self.config.outputjson:
|
|
793 |
jsonout = self.generateJsonList(dd.trusts)
|
|
794 |
self.writeJsonFile('%s.json' % self.config.trustsfile, jsonout)
|
|
795 |
if self.config.outputgrep:
|
|
796 |
grepout = self.generateGrepList(dd.trusts, self.trustattributes)
|
|
797 |
self.writeGrepFile('%s.grep' % self.config.trustsfile, grepout)
|
|
798 |
|
|
799 |
#Some quick logging helpers
|
|
800 |
def log_warn(text):
|
|
801 |
print '[!] %s' % text
|
|
802 |
def log_info(text):
|
|
803 |
print '[*] %s' % text
|
|
804 |
def log_success(text):
|
|
805 |
print '[+] %s' % text
|
|
806 |
|
|
807 |
def main():
|
|
808 |
parser = argparse.ArgumentParser(description='Domain information dumper via LDAP. Dumps users/computers/groups and OS/membership information to HTML/JSON/greppable output.')
|
|
809 |
parser._optionals.title = "Main options"
|
|
810 |
parser._positionals.title = "Required options"
|
|
811 |
|
|
812 |
#Main parameters
|
|
813 |
#maingroup = parser.add_argument_group("Main options")
|
|
814 |
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)")
|
|
815 |
parser.add_argument("-u", "--user", type=str, metavar='USERNAME', help="DOMAIN\\username for authentication, leave empty for anonymous authentication")
|
|
816 |
parser.add_argument("-p", "--password", type=str, metavar='PASSWORD', help="Password or LM:NTLM hash, will prompt if not specified")
|
|
817 |
parser.add_argument("-at", "--authtype", type=str, choices=['NTLM', 'SIMPLE'], default='NTLM', help="Authentication type (NTLM or SIMPLE, default: NTLM)")
|
|
818 |
|
|
819 |
#Output parameters
|
|
820 |
outputgroup = parser.add_argument_group("Output options")
|
|
821 |
outputgroup.add_argument("-o", "--outdir", type=str, metavar='DIRECTORY', help="Directory in which the dump will be saved (default: current)")
|
|
822 |
outputgroup.add_argument("--no-html", action='store_true', help="Disable HTML output")
|
|
823 |
outputgroup.add_argument("--no-json", action='store_true', help="Disable JSON output")
|
|
824 |
outputgroup.add_argument("--no-grep", action='store_true', help="Disable Greppable output")
|
|
825 |
outputgroup.add_argument("--grouped-json", action='store_true', default=False, help="Also write json files for grouped files (default: disabled)")
|
|
826 |
outputgroup.add_argument("-d", "--delimiter", help="Field delimiter for greppable output (default: tab)")
|
|
827 |
|
|
828 |
#Additional options
|
|
829 |
miscgroup = parser.add_argument_group("Misc options")
|
|
830 |
miscgroup.add_argument("-r", "--resolve", action='store_true', help="Resolve computer hostnames (might take a while and cause high traffic on large networks)")
|
|
831 |
miscgroup.add_argument("-n", "--dns-server", help="Use custom DNS resolver instead of system DNS (try a domain controller IP)")
|
|
832 |
miscgroup.add_argument("-m", "--minimal", action='store_true', default=False, help="Only query minimal set of attributes to limit memmory usage")
|
|
833 |
|
|
834 |
args = parser.parse_args()
|
|
835 |
#Create default config
|
|
836 |
cnf = domainDumpConfig()
|
|
837 |
#Dns lookups?
|
|
838 |
if args.resolve:
|
|
839 |
cnf.lookuphostnames = True
|
|
840 |
#Custom dns server?
|
|
841 |
if args.dns_server is not None:
|
|
842 |
cnf.dnsserver = args.dns_server
|
|
843 |
#Minimal attributes?
|
|
844 |
if args.minimal:
|
|
845 |
cnf.minimal = True
|
|
846 |
#Custom separator?
|
|
847 |
if args.delimiter is not None:
|
|
848 |
cnf.grepsplitchar = args.delimiter
|
|
849 |
#Disable html?
|
|
850 |
if args.no_html:
|
|
851 |
cnf.outputhtml = False
|
|
852 |
#Disable json?
|
|
853 |
if args.no_json:
|
|
854 |
cnf.outputjson = False
|
|
855 |
#Disable grep?
|
|
856 |
if args.no_grep:
|
|
857 |
cnf.outputgrep = False
|
|
858 |
#Custom outdir?
|
|
859 |
if args.outdir is not None:
|
|
860 |
cnf.basepath = args.outdir
|
|
861 |
#Do we really need grouped json files?
|
|
862 |
cnf.groupedjson = args.grouped_json
|
|
863 |
|
|
864 |
#Prompt for password if not set
|
|
865 |
authentication = None
|
|
866 |
if args.user is not None:
|
|
867 |
if args.authtype == 'SIMPLE':
|
|
868 |
authentication = 'SIMPLE'
|
|
869 |
else:
|
|
870 |
authentication = NTLM
|
|
871 |
if not '\\' in args.user:
|
|
872 |
log_warn('Username must include a domain, use: DOMAIN\\username')
|
|
873 |
sys.exit(1)
|
|
874 |
if args.password is None:
|
|
875 |
args.password = getpass.getpass()
|
|
876 |
else:
|
|
877 |
log_info('Connecting as anonymous user, dumping will probably fail. Consider specifying a username/password to login with')
|
|
878 |
# define the server and the connection
|
|
879 |
s = Server(args.host, get_info=ALL)
|
|
880 |
log_info('Connecting to host...')
|
|
881 |
c = Connection(s, user=args.user, password=args.password, authentication=authentication)
|
|
882 |
log_info('Binding to host')
|
|
883 |
# perform the Bind operation
|
|
884 |
if not c.bind():
|
|
885 |
log_warn('Could not bind with specified credentials')
|
|
886 |
log_warn(c.result)
|
|
887 |
sys.exit(1)
|
|
888 |
log_success('Bind OK')
|
|
889 |
log_info('Starting domain dump')
|
|
890 |
#Create domaindumper object
|
|
891 |
dd = domainDumper(s, c, cnf)
|
|
892 |
|
|
893 |
#Do the actual dumping
|
|
894 |
dd.domainDump()
|
|
895 |
log_success('Domain dump finished')
|
|
896 |
|
|
897 |
if __name__ == '__main__':
|
|
898 |
main()
|