20 | 20 |
# SOFTWARE.
|
21 | 21 |
#
|
22 | 22 |
####################
|
23 | |
|
|
23 |
from __future__ import unicode_literals
|
24 | 24 |
import sys, os, re, codecs, json, argparse, getpass, base64
|
25 | 25 |
# import class and constants
|
26 | 26 |
from datetime import datetime
|
27 | |
from urllib import quote_plus
|
28 | |
|
|
27 |
try:
|
|
28 |
from urllib.parse import quote_plus
|
|
29 |
except ImportError:
|
|
30 |
from urllib import quote_plus
|
29 | 31 |
import ldap3
|
30 | 32 |
from ldap3 import Server, Connection, SIMPLE, SYNC, ALL, SASL, NTLM
|
31 | |
from ldap3.core.exceptions import LDAPKeyError, LDAPAttributeError, LDAPCursorError
|
|
33 |
from ldap3.core.exceptions import LDAPKeyError, LDAPAttributeError, LDAPCursorError, LDAPInvalidDnError
|
32 | 34 |
from ldap3.abstract import attribute, attrDef
|
33 | 35 |
from ldap3.utils import dn
|
34 | 36 |
from ldap3.protocol.formatters.formatters import format_sid
|
|
37 |
from builtins import str
|
|
38 |
from future.utils import itervalues, iteritems, native_str
|
35 | 39 |
|
36 | 40 |
# dnspython, for resolving hostnames
|
37 | 41 |
import dns.resolver
|
|
451 | 455 |
outflags = []
|
452 | 456 |
if attr is None:
|
453 | 457 |
return outflags
|
454 | |
for flag, val in flags_def.items():
|
|
458 |
for flag, val in iteritems(flags_def):
|
455 | 459 |
if attr.value & val:
|
456 | 460 |
outflags.append(flag)
|
457 | 461 |
return outflags
|
|
461 | 465 |
outflags = []
|
462 | 466 |
if attr is None:
|
463 | 467 |
return outflags
|
464 | |
for flag, val in flags_def.items():
|
|
468 |
for flag, val in iteritems(flags_def):
|
465 | 469 |
if attr.value == val:
|
466 | 470 |
outflags.append(flag)
|
467 | 471 |
return outflags
|
|
472 | 476 |
#Only if this is the first table it is an actual table, the others are just bodies of the first table
|
473 | 477 |
#This makes sure that multiple tables have their columns aligned to make it less messy
|
474 | 478 |
if firstTable:
|
475 | |
of.append(u'<table>')
|
|
479 |
of.append('<table>')
|
476 | 480 |
#Table header
|
477 | 481 |
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>')
|
|
482 |
of.append('<thead><tr><td colspan="%d" id="cn_%s">%s</td></tr></thead>' % (len(attributes), self.formatId(header), header))
|
|
483 |
of.append('<tbody><tr>')
|
480 | 484 |
for hdr in attributes:
|
481 | 485 |
try:
|
482 | 486 |
#Print alias of this attribute if there is one
|
483 | |
of.append(u'<th>%s</th>' % self.htmlescape(attr_translations[hdr]))
|
|
487 |
of.append('<th>%s</th>' % self.htmlescape(attr_translations[hdr]))
|
484 | 488 |
except KeyError:
|
485 | |
of.append(u'<th>%s</th>' % self.htmlescape(hdr))
|
486 | |
of.append(u'</tr>\n')
|
|
489 |
of.append('<th>%s</th>' % self.htmlescape(hdr))
|
|
490 |
of.append('</tr>\n')
|
487 | 491 |
for li in listable:
|
488 | 492 |
#Whether we should format group objects separately
|
489 | 493 |
if specialGroupsFormat and 'group' in li['objectClass'].values:
|
490 | 494 |
#Give it an extra class and pass it to the function below to make sure the CN is a link
|
491 | 495 |
liIsGroup = True
|
492 | |
of.append(u'<tr class="group">')
|
|
496 |
of.append('<tr class="group">')
|
493 | 497 |
else:
|
494 | 498 |
liIsGroup = False
|
495 | |
of.append(u'<tr>')
|
|
499 |
of.append('<tr>')
|
496 | 500 |
for att in attributes:
|
497 | 501 |
try:
|
498 | |
of.append(u'<td>%s</td>' % self.formatAttribute(li[att], liIsGroup))
|
|
502 |
of.append('<td>%s</td>' % self.formatAttribute(li[att], liIsGroup))
|
499 | 503 |
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 |
of.append('<td> </td>')
|
|
505 |
of.append('</tr>\n')
|
|
506 |
of.append('</tbody>\n')
|
|
507 |
return ''.join(of)
|
504 | 508 |
|
505 | 509 |
#Generate several HTML tables for grouped reports
|
506 | 510 |
def generateGroupedHtmlTables(self, groups, attributes):
|
507 | 511 |
first = True
|
508 | |
for groupname, members in groups.iteritems():
|
|
512 |
for groupname, members in iteritems(groups):
|
509 | 513 |
yield self.generateHtmlTable(members, attributes, groupname, first, specialGroupsFormat=True)
|
510 | 514 |
if first:
|
511 | 515 |
first = False
|
|
565 | 569 |
return value.strftime('%x %X')
|
566 | 570 |
except ValueError:
|
567 | 571 |
#Invalid date
|
568 | |
return u'0'
|
569 | |
if type(value) is unicode:
|
|
572 |
return '0'
|
|
573 |
# Make sure it's a unicode string
|
|
574 |
if type(value) is bytes:
|
|
575 |
return value.encode('utf8')
|
|
576 |
if type(value) is str:
|
570 | 577 |
return value#.encode('utf8')
|
571 | |
if type(value) is str:
|
572 | |
return unicode(value, errors='replace')#.encode('utf8')
|
573 | 578 |
if type(value) is int:
|
574 | |
return unicode(value)
|
|
579 |
return str(value)
|
575 | 580 |
if value is None:
|
576 | 581 |
return ''
|
577 | 582 |
#Other type: just return it
|
|
618 | 623 |
|
619 | 624 |
|
620 | 625 |
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))
|
|
626 |
return 'Group: <a href="#cn_%s" title="%s">%s</a>' % (self.formatId(cn), self.htmlescape(cn), self.htmlescape(cn))
|
622 | 627 |
|
623 | 628 |
#Convert a CN to a valid HTML id by replacing all non-ascii characters with a _
|
624 | 629 |
def formatId(self, cn):
|
625 | 630 |
return re.sub(r'[^a-zA-Z0-9_\-]+', '_', cn)
|
|
631 |
|
|
632 |
# Fallback function for dirty DN parsing in case ldap3 functions error out
|
|
633 |
def parseDnFallback(self, dn):
|
|
634 |
try:
|
|
635 |
indcn = dn[3:].index(',CN=')
|
|
636 |
indou = dn[3:].index(',OU=')
|
|
637 |
if indcn < indou:
|
|
638 |
cn = dn[3:].split(',CN=')[0]
|
|
639 |
else:
|
|
640 |
cn = dn[3:].split(',OU=')[0]
|
|
641 |
except ValueError:
|
|
642 |
cn = dn
|
|
643 |
return cn
|
626 | 644 |
|
627 | 645 |
#Format groups to readable HTML
|
628 | 646 |
def formatGroupsHtml(self, grouplist):
|
629 | 647 |
outcache = []
|
630 | 648 |
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)))
|
|
649 |
try:
|
|
650 |
cn = self.unescapecn(dn.parse_dn(group)[0][1])
|
|
651 |
except LDAPInvalidDnError:
|
|
652 |
# Parsing failed, do it manually
|
|
653 |
cn = self.unescapecn(self.parseDnFallback(group))
|
|
654 |
outcache.append('<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 | 655 |
return ', '.join(outcache)
|
634 | 656 |
|
635 | 657 |
#Format groups to readable HTML
|
636 | 658 |
def formatGroupsGrep(self, grouplist):
|
637 | 659 |
outcache = []
|
638 | 660 |
for group in grouplist:
|
639 | |
cn = self.unescapecn(dn.parse_dn(group)[0][1])
|
|
661 |
try:
|
|
662 |
cn = self.unescapecn(dn.parse_dn(group)[0][1])
|
|
663 |
except LDAPInvalidDnError:
|
|
664 |
# Parsing failed, do it manually
|
|
665 |
cn = self.unescapecn(self.parseDnFallback(group))
|
640 | 666 |
outcache.append(cn)
|
641 | 667 |
return ', '.join(outcache)
|
642 | 668 |
|
|
706 | 732 |
#Start of the list
|
707 | 733 |
yield '['
|
708 | 734 |
firstGroup = True
|
709 | |
for group in groups.iteritems():
|
|
735 |
for group in iteritems(groups):
|
710 | 736 |
if not firstGroup:
|
711 | 737 |
#Separate items
|
712 | 738 |
yield ','
|
|
798 | 824 |
|
799 | 825 |
#Some quick logging helpers
|
800 | 826 |
def log_warn(text):
|
801 | |
print '[!] %s' % text
|
|
827 |
print('[!] %s' % text)
|
802 | 828 |
def log_info(text):
|
803 | |
print '[*] %s' % text
|
|
829 |
print('[*] %s' % text)
|
804 | 830 |
def log_success(text):
|
805 | |
print '[+] %s' % text
|
|
831 |
print('[+] %s' % text)
|
806 | 832 |
|
807 | 833 |
def main():
|
808 | 834 |
parser = argparse.ArgumentParser(description='Domain information dumper via LDAP. Dumps users/computers/groups and OS/membership information to HTML/JSON/greppable output.')
|
|
812 | 838 |
#Main parameters
|
813 | 839 |
#maingroup = parser.add_argument_group("Main options")
|
814 | 840 |
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")
|
|
841 |
parser.add_argument("-u", "--user", type=native_str, metavar='USERNAME', help="DOMAIN\\username for authentication, leave empty for anonymous authentication")
|
|
842 |
parser.add_argument("-p", "--password", type=native_str, metavar='PASSWORD', help="Password or LM:NTLM hash, will prompt if not specified")
|
817 | 843 |
parser.add_argument("-at", "--authtype", type=str, choices=['NTLM', 'SIMPLE'], default='NTLM', help="Authentication type (NTLM or SIMPLE, default: NTLM)")
|
818 | 844 |
|
819 | 845 |
#Output parameters
|
|
878 | 904 |
# define the server and the connection
|
879 | 905 |
s = Server(args.host, get_info=ALL)
|
880 | 906 |
log_info('Connecting to host...')
|
|
907 |
|
881 | 908 |
c = Connection(s, user=args.user, password=args.password, authentication=authentication)
|
882 | 909 |
log_info('Binding to host')
|
883 | 910 |
# perform the Bind operation
|