From b8af6b96f629e874027fa07c402211cb0b9c70bb Mon Sep 17 00:00:00 2001 From: Casper Nielsen Date: Fri, 18 Sep 2020 23:38:48 +0200 Subject: [PATCH 01/18] Init of extracting and enumerating server names --- ade/__init__.py | 18 ++++++++++++++---- ade/enumerate/__init__.py | 0 ade/enumerate/enumerate.py | 27 +++++++++++++++++++++++++++ 3 files changed, 41 insertions(+), 4 deletions(-) create mode 100644 ade/enumerate/__init__.py create mode 100644 ade/enumerate/enumerate.py diff --git a/ade/__init__.py b/ade/__init__.py index 4d772cc..57571fb 100755 --- a/ade/__init__.py +++ b/ade/__init__.py @@ -23,6 +23,7 @@ from pyasn1.type.univ import noValue from binascii import hexlify import datetime, random +from .enumerate import enumerate # Thanks SecureAuthCorp for GetUserSPNs.py # For SPN enum @@ -80,7 +81,7 @@ def __init__(self, domainController, ldaps, output, enumsmb, bhout, kpre, spnEnu self.runWithCreds() else: self.runWithoutCreds() - + def runWithCreds(self): self.CREDS = True @@ -91,6 +92,8 @@ def runWithCreds(self): if self.output: self.write_file() + + self.enumerate_names() self.checkForPW() self.checkOS() @@ -129,6 +132,8 @@ def runWithoutCreds(self): self.bind() self.search() + self.enumerate_names() + if self.output: self.write_file() @@ -140,7 +145,6 @@ def runWithoutCreds(self): print('[ ' + colored('WARN', 'yellow') +' ] Didn\'t find useable info as anonymous user, please gather credentials and run again') exit(0) - @contextlib.contextmanager def suppressOutput(self): with open(os.devnull, 'w') as devnull: @@ -237,7 +241,13 @@ def search(self): if len(self.deletedUsers) > 0: print('[ ' + colored('INFO', 'green') +' ] Searching for juicy info in deleted users') self.enumForCreds(self.deletedUsers) - + + + def enumerate_names(self): + enum = enumerate.Enumerate(self.computers) + enum.enumerate_server_names() + print(enum.results) + ''' Since it sometimes is real that the property 'userPassword:' is set @@ -252,7 +262,7 @@ def checkForPW(self): if user['attributes'].get('userPassword') is not None: passwords[user['attributes']['name'][0]] = user['attributes'].get('userPassword') if len(passwords.keys()) > 0: - with open('{0}-clearpw'.format(self.server), 'w') as f: + with open(f'{self.server}-clearpw', 'w') as f: json.dump(passwords, f, sort_keys=False) if len(passwords.keys()) == 1: diff --git a/ade/enumerate/__init__.py b/ade/enumerate/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ade/enumerate/enumerate.py b/ade/enumerate/enumerate.py new file mode 100644 index 0000000..1d914a9 --- /dev/null +++ b/ade/enumerate/enumerate.py @@ -0,0 +1,27 @@ +import json + +class Enumerate(): + + def __init__(self, computerobjects): + self.wordlist = { + "mssql": ["mssql", "sqlserver"], + "ftp": ["ftp"], + "smtp": ["exchange", "smtp"], + "ad": ["dc", "domaincontroller", "msol", "domain controller"] + } + self.results = {} + self.computerobjects = computerobjects + + + def enumerate_server_names(self): + for key, value in self.wordlist.items(): + for fingerprint in value: + for obj in self.computerobjects: + if fingerprint in str(obj["name"]).lower(): + self.results[str(obj["dNSHostName"])] = key + elif fingerprint in str(obj["dNSHostName"]).lower(): + self.results[str(obj["dNSHostName"])] = key + elif fingerprint in str(obj["distinguishedName"]).lower(): + self.results[str(obj["dNSHostName"])] = key + elif fingerprint in str(obj["dNSHostName"]).lower(): + self.results[str(obj["dNSHostName"])] = key \ No newline at end of file From 263acaaae2d8fbdfd080ddec7df12911756d4b45 Mon Sep 17 00:00:00 2001 From: Casper Guldbech Nielsen Date: Sat, 19 Sep 2020 10:59:14 +0200 Subject: [PATCH 02/18] Renaming Signed-off-by: Casper Guldbech Nielsen --- ade/{enumerate => modEnumerator}/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename ade/{enumerate => modEnumerator}/__init__.py (100%) diff --git a/ade/enumerate/__init__.py b/ade/modEnumerator/__init__.py similarity index 100% rename from ade/enumerate/__init__.py rename to ade/modEnumerator/__init__.py From 7acaac7aefd4f13aa731c285a7f39f7684ec2fa2 Mon Sep 17 00:00:00 2001 From: Casper Guldbech Nielsen Date: Sat, 19 Sep 2020 10:59:28 +0200 Subject: [PATCH 03/18] Renaming Signed-off-by: Casper Guldbech Nielsen --- .../modEnumerator.py} | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) rename ade/{enumerate/enumerate.py => modEnumerator/modEnumerator.py} (76%) diff --git a/ade/enumerate/enumerate.py b/ade/modEnumerator/modEnumerator.py similarity index 76% rename from ade/enumerate/enumerate.py rename to ade/modEnumerator/modEnumerator.py index 1d914a9..a0666cc 100644 --- a/ade/enumerate/enumerate.py +++ b/ade/modEnumerator/modEnumerator.py @@ -1,19 +1,27 @@ +# -*- coding: utf-8 -*- + import json +import ldap3 + +class ModEnumerator(): -class Enumerate(): + def __init__(self, ): + pass - def __init__(self, computerobjects): + + def enumerate_server_names(self, computerobjects: ldap3.Entry) -> dict: + '''Return a dict of key(dNSHostName) and value(fingerprinted servertype) + + ''' self.wordlist = { "mssql": ["mssql", "sqlserver"], "ftp": ["ftp"], "smtp": ["exchange", "smtp"], "ad": ["dc", "domaincontroller", "msol", "domain controller"] } - self.results = {} self.computerobjects = computerobjects + self.results = {} - - def enumerate_server_names(self): for key, value in self.wordlist.items(): for fingerprint in value: for obj in self.computerobjects: @@ -24,4 +32,6 @@ def enumerate_server_names(self): elif fingerprint in str(obj["distinguishedName"]).lower(): self.results[str(obj["dNSHostName"])] = key elif fingerprint in str(obj["dNSHostName"]).lower(): - self.results[str(obj["dNSHostName"])] = key \ No newline at end of file + self.results[str(obj["dNSHostName"])] = key + + return self.results \ No newline at end of file From 7df298f23931681d2791253a550d05020f9aa9f3 Mon Sep 17 00:00:00 2001 From: Casper Guldbech Nielsen Date: Sat, 19 Sep 2020 11:00:00 +0200 Subject: [PATCH 04/18] Restructuring the usage of the module Signed-off-by: Casper Guldbech Nielsen --- ade/__init__.py | 73 +++++++++++++++++++++++-------------------------- 1 file changed, 34 insertions(+), 39 deletions(-) diff --git a/ade/__init__.py b/ade/__init__.py index 57571fb..85352a0 100755 --- a/ade/__init__.py +++ b/ade/__init__.py @@ -22,8 +22,9 @@ from pyasn1.codec.der import decoder, encoder from pyasn1.type.univ import noValue from binascii import hexlify -import datetime, random -from .enumerate import enumerate +import datetime +import random +from .modEnumerator.modEnumerator import ModEnumerator # Thanks SecureAuthCorp for GetUserSPNs.py # For SPN enum @@ -59,6 +60,9 @@ def __init__(self, domainController, ldaps, output, enumsmb, bhout, kpre, spnEnu # At the moment we just want everything self.ldapProps = ["*"] + # Initialize modules + self.enumerator = ModEnumerator() + # Setting lists containing elements we want from the domain controller self.computers = [] @@ -71,6 +75,9 @@ def __init__(self, domainController, ldaps, output, enumsmb, bhout, kpre, spnEnu self.ous = [] self.deletedUsers = [] self.passwd = False + self.passwords = {} + # Holds the values of servers that has been fingerprinted to a particular service + self.namedServers = {} # TODO: Figure a good way to go through the code dryrun if dryrun: @@ -81,6 +88,14 @@ def __init__(self, domainController, ldaps, output, enumsmb, bhout, kpre, spnEnu self.runWithCreds() else: self.runWithoutCreds() + + self.enumerate_names() + self.checkForPW() + self.checkOS() + self.write_file() + + # Unbind the connection to release the handle + self.conn.unbind() def runWithCreds(self): @@ -90,13 +105,6 @@ def runWithCreds(self): self.bind() self.search() - if self.output: - self.write_file() - - self.enumerate_names() - - self.checkForPW() - self.checkOS() if self.searchSysvol: self.checkSYSVOL() @@ -107,9 +115,7 @@ def runWithCreds(self): self.enumKerbPre() if self.spnEnum: - self.enumSPNUsers() - - self.conn.unbind() + self.enumSPNUsers() if self.enumsmb: # Setting variables for further testing and analysis @@ -121,6 +127,8 @@ def runWithCreds(self): # Lets clear variable now self.passwd = None + return + def runWithoutCreds(self): self.CREDS = False @@ -131,19 +139,10 @@ def runWithoutCreds(self): self.bind() self.search() - - self.enumerate_names() - - if self.output: - self.write_file() - self.checkForPW() - self.checkOS() - self.enumForCreds(self.people) - print('[ ' + colored('WARN', 'yellow') +' ] Didn\'t find useable info as anonymous user, please gather credentials and run again') - exit(0) + return @contextlib.contextmanager def suppressOutput(self): @@ -244,9 +243,7 @@ def search(self): def enumerate_names(self): - enum = enumerate.Enumerate(self.computers) - enum.enumerate_server_names() - print(enum.results) + self.namedServers = self.enumerator.enumerate_server_names(self.computers) ''' @@ -254,23 +251,22 @@ def enumerate_names(self): we test for it and dump the passwords ''' def checkForPW(self): - passwords = {} idx = 0 for _ in self.people: user = json.loads(self.people[idx].entry_to_json()) idx += 1 if user['attributes'].get('userPassword') is not None: - passwords[user['attributes']['name'][0]] = user['attributes'].get('userPassword') - if len(passwords.keys()) > 0: + self.passwords[user['attributes']['name'][0]] = user['attributes'].get('userPassword') + if len(self.passwords.keys()) > 0: with open(f'{self.server}-clearpw', 'w') as f: - json.dump(passwords, f, sort_keys=False) + json.dump(self.passwords, f, sort_keys=False) - if len(passwords.keys()) == 1: - print('[ ' + colored('WARN', 'yellow') +' ] Found {0} clear text password'.format(len(passwords.keys()))) - elif len(passwords.keys()) == 0: - print('[ ' + colored('OK', 'green') +' ] Found {0} clear text password'.format(len(passwords.keys()))) + if len(self.passwords.keys()) == 1: + print('[ ' + colored('WARN', 'yellow') +' ] Found {0} clear text password'.format(len(self.passwords.keys()))) + elif len(self.passwords.keys()) == 0: + print('[ ' + colored('OK', 'green') +' ] Found {0} clear text password'.format(len(self.passwords.keys()))) else: - print('[ ' + colored('OK', 'green') +' ] Found {0} clear text passwords'.format(len(passwords.keys()))) + print('[ ' + colored('OK', 'green') +' ] Found {0} clear text passwords'.format(len(self.passwords.keys()))) ''' @@ -713,8 +709,9 @@ def enumForCreds(self, ldapdump): if not self.CREDS: self.domuser = usr self.passwd = passwd + self.passwords[usr] = passwd self.runWithCreds() - exit(0) + return def entroPass(self, user, password): @@ -781,7 +778,7 @@ def main(args): ''')) parser.add_argument('dc', type=str, help='Hostname of the Domain Controller') - parser.add_argument('-o', '--out-file', type=str, help='Path to output file. If no path, CWD is assumed (default: None)') + parser.add_argument('-o', '--out-file', type=str, help='Name prefix of output files (default: the name of the dc)') parser.add_argument('-u', '--user', type=str, help='Username of the domain user to query with. The username has to be domain name as `user@domain.org`') parser.add_argument('-s', '--secure', help='Try to estalish connection through LDAPS', action='store_true') parser.add_argument('-smb', '--smb', help='Force enumeration of SMB shares on all computer objects fetched', action='store_true') @@ -824,9 +821,7 @@ def main(args): # Boolean flow control flags - file_to_write = None - if args.out_file: - file_to_write = args.out_file + file_to_write = args.out_file if args.out_file else f'{args.dc}' enumAD = EnumAD(args.dc, args.secure, file_to_write, args.smb, args.bloodhound, args.kerberos_preauth, args.spn, args.sysvol, args.dry_run, args.user) From 0f0ee329274307c299526d7222d7e30d10cb9e6e Mon Sep 17 00:00:00 2001 From: Casper Guldbech Nielsen Date: Sat, 19 Sep 2020 11:07:02 +0200 Subject: [PATCH 05/18] Update ignore list Signed-off-by: Casper Guldbech Nielsen --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 15d6af0..8f16f84 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ *.json *-jtr-hashes pyvenv.cfg +__pycache__/* ActiveDirectoryEnum.egg-info/* build/* dist/* From 6876628f39ce63b16b0f6586ccafac313c410406 Mon Sep 17 00:00:00 2001 From: Casper Guldbech Nielsen Date: Sat, 19 Sep 2020 11:07:16 +0200 Subject: [PATCH 06/18] Update manifest Signed-off-by: Casper Guldbech Nielsen --- MANIFEST.in | 2 ++ 1 file changed, 2 insertions(+) diff --git a/MANIFEST.in b/MANIFEST.in index 363df3e..38d0d4b 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,4 +1,6 @@ recursive-include ade/external/* +recursive-include ade/modEnumerator/* +recursive-include ade/connectors/* include requirements.txt exclude .github/* exclude .gitignore \ No newline at end of file From 1bc3eb76fafc73209d2d8d395e41da72562412c5 Mon Sep 17 00:00:00 2001 From: Casper Guldbech Nielsen Date: Sat, 19 Sep 2020 11:37:34 +0200 Subject: [PATCH 07/18] Adding more modularity by including connectors -> first is LDAP Signed-off-by: Casper Guldbech Nielsen --- ade/connectors/__init__.py | 0 ade/connectors/connectors.py | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+) create mode 100644 ade/connectors/__init__.py create mode 100644 ade/connectors/connectors.py diff --git a/ade/connectors/__init__.py b/ade/connectors/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ade/connectors/connectors.py b/ade/connectors/connectors.py new file mode 100644 index 0000000..26be08b --- /dev/null +++ b/ade/connectors/connectors.py @@ -0,0 +1,36 @@ +import ldap3 +from ldap3.core.exceptions import LDAPBindError +import sys +from termcolor import colored + +class Connectors(): + + + def __init__(self): + pass + + + def ldap_connector(self, server: str, ldaps: bool, domuser: str, passwd: str, level='ALL') -> ldap3.Connection: + '''Returns an ldap3.Connection object that is bound to the supplied domain controller + + Raise ldap3.core.exceptions.LDAPBindError on bind() errors. + + ''' + if ldaps: + dc_conn = ldap3.Server(server, port=636, use_ssl=True, get_info=level) + conn = ldap3.Connection(dc_conn, user=domuser, password=passwd) + conn.bind() + conn.start_tls() + # Validate the login (bind) request + if int(conn.result['result']) != 0: + raise LDAPBindError + else: + dc_conn = ldap3.Server(server, get_info=level) + conn = ldap3.Connection(dc_conn, user=domuser, password=passwd) + conn.bind() + # Validate the login (bind) request + if int(conn.result['result']) != 0: + print('\033[1A\r[ ' + colored('ERROR', 'red') +' ] Failed to bind to LDAP server: {0}'.format(conn.result['description'])) + raise LDAPBindError + + return conn \ No newline at end of file From a6117fe0242e2c70c387ccc65649cb2789b5d854 Mon Sep 17 00:00:00 2001 From: Casper Guldbech Nielsen Date: Sat, 19 Sep 2020 11:37:52 +0200 Subject: [PATCH 08/18] Utilising newly added connector module for ldap connections Signed-off-by: Casper Guldbech Nielsen --- ade/__init__.py | 40 +++++++--------------- ade/modEnumerator/modEnumerator.py | 54 ++++++++++++++++++++++++------ 2 files changed, 55 insertions(+), 39 deletions(-) diff --git a/ade/__init__.py b/ade/__init__.py index 85352a0..5c85ee3 100755 --- a/ade/__init__.py +++ b/ade/__init__.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 from ldap3 import Server, Connection, ALL, ALL_ATTRIBUTES, LEVEL, SUBTREE, ALL_OPERATIONAL_ATTRIBUTES from progressbar import Bar, Percentage, ProgressBar, ETA -from ldap3.core.exceptions import LDAPKeyError +from ldap3.core.exceptions import LDAPKeyError, LDAPBindError, LDAPSocketOpenError from impacket.smbconnection import SessionError from impacket.nmb import NetBIOSTimeout, NetBIOSError from getpass import getpass @@ -25,6 +25,7 @@ import datetime import random from .modEnumerator.modEnumerator import ModEnumerator +from .connectors.connectors import Connectors # Thanks SecureAuthCorp for GetUserSPNs.py # For SPN enum @@ -61,6 +62,7 @@ def __init__(self, domainController, ldaps, output, enumsmb, bhout, kpre, spnEnu self.ldapProps = ["*"] # Initialize modules + self.connectors = Connectors() self.enumerator = ModEnumerator() @@ -154,28 +156,13 @@ def suppressOutput(self): def bind(self): try: if self.ldaps: - self.dc_conn = Server(self.server, port=636, use_ssl=True, get_info='ALL') - self.conn = Connection(self.dc_conn, user=self.domuser, password=self.passwd) - self.conn.bind() - self.conn.start_tls() - # Validate the login (bind) request - if int(self.conn.result['result']) != 0: - print('\033[1A\r[ ' + colored('ERROR', 'red') +' ] Failed to bind to LDAPS server: {0}'.format(self.conn.result['description'])) - sys.exit(1) - else: - print('\033[1A\r[ ' + colored('OK', 'green') +' ] Bound to LDAPS server: {0}'.format(self.server)) + self.conn = self.connectors.ldap_connector(self.server, True, self.domuser, self.passwd) + print('\033[1A\r[ ' + colored('OK', 'green') +' ] Bound to LDAPS server: {0}'.format(self.server)) else: - self.dc_conn = Server(self.server, get_info=ALL) - self.conn = Connection(self.dc_conn, user=self.domuser, password=self.passwd) - self.conn.bind() - # Validate the login (bind) request - if int(self.conn.result['result']) != 0: - print('\033[1A\r[ ' + colored('ERROR', 'red') +' ] Failed to bind to LDAP server: {0}'.format(self.conn.result['description'])) - sys.exit(1) - else: - print('\033[1A\r[ ' + colored('OK', 'green') +' ] Bound to LDAP server: {0}'.format(self.server)) + self.conn = self.connectors.ldap_connector(self.server, False, self.domuser, self.passwd) + print('\033[1A\r[ ' + colored('OK', 'green') +' ] Bound to LDAP server: {0}'.format(self.server)) # TODO: Catch individual exceptions instead - except Exception: + except (LDAPBindError, LDAPSocketOpenError): if self.ldaps: print('\033[1A\r[ ' + colored('ERROR', 'red') +' ] Failed to bind to LDAPS server: {0}'.format(self.server)) else: @@ -251,15 +238,12 @@ def enumerate_names(self): we test for it and dump the passwords ''' def checkForPW(self): - idx = 0 - for _ in self.people: - user = json.loads(self.people[idx].entry_to_json()) - idx += 1 - if user['attributes'].get('userPassword') is not None: - self.passwords[user['attributes']['name'][0]] = user['attributes'].get('userPassword') + passwords = self.enumerator.enumerate_for_cleartext_passwords(self.people, self.server) + self.passwords = { **passwords, **self.passwords } + if len(self.passwords.keys()) > 0: with open(f'{self.server}-clearpw', 'w') as f: - json.dump(self.passwords, f, sort_keys=False) + json.dump(self.passwords, f, sort_keys=False) if len(self.passwords.keys()) == 1: print('[ ' + colored('WARN', 'yellow') +' ] Found {0} clear text password'.format(len(self.passwords.keys()))) diff --git a/ade/modEnumerator/modEnumerator.py b/ade/modEnumerator/modEnumerator.py index a0666cc..6aa94be 100644 --- a/ade/modEnumerator/modEnumerator.py +++ b/ade/modEnumerator/modEnumerator.py @@ -2,10 +2,13 @@ import json import ldap3 +from ldap3.core.exceptions import LDAPBindError + +from . .connectors.connectors import Connectors class ModEnumerator(): - def __init__(self, ): + def __init__(self): pass @@ -13,25 +16,54 @@ def enumerate_server_names(self, computerobjects: ldap3.Entry) -> dict: '''Return a dict of key(dNSHostName) and value(fingerprinted servertype) ''' - self.wordlist = { + wordlist = { "mssql": ["mssql", "sqlserver"], "ftp": ["ftp"], "smtp": ["exchange", "smtp"], "ad": ["dc", "domaincontroller", "msol", "domain controller"] } - self.computerobjects = computerobjects - self.results = {} + results = {} - for key, value in self.wordlist.items(): + for key, value in wordlist.items(): for fingerprint in value: - for obj in self.computerobjects: + for obj in computerobjects: if fingerprint in str(obj["name"]).lower(): - self.results[str(obj["dNSHostName"])] = key + results[str(obj["dNSHostName"])] = key elif fingerprint in str(obj["dNSHostName"]).lower(): - self.results[str(obj["dNSHostName"])] = key + results[str(obj["dNSHostName"])] = key elif fingerprint in str(obj["distinguishedName"]).lower(): - self.results[str(obj["dNSHostName"])] = key + results[str(obj["dNSHostName"])] = key elif fingerprint in str(obj["dNSHostName"]).lower(): - self.results[str(obj["dNSHostName"])] = key + results[str(obj["dNSHostName"])] = key + + return results + + + def enumerate_for_cleartext_passwords(self, peopleobjects: ldap3.Entry, server: str) -> dict: + '''Return a dict of key(username) and value(password) + + ''' + passwords = {} + + idx = 0 + for _ in peopleobjects: + user = json.loads(peopleobjects[idx].entry_to_json()) + idx += 1 + if user['attributes'].get('userPassword') is not None: + # Attempt login + try: + # First we try encrypted + conn = Connectors().ldap_connector(server=server, ldaps=True, domuser=user['attributes']['name'][0], passwd=user['attributes'].get('userPassword')) + except LDAPBindError: + # Then default to non-encrypted + try: + conn = Connectors().ldap_connector(server=server, ldaps=False, domuser=user['attributes']['name'][0], passwd=user['attributes'].get('userPassword')) + except LDAPBindError: + # No luck + continue + finally: + if int(conn.result['result']) == 0: + # We had a valid login + passwords[user['attributes']['name'][0]] = user['attributes'].get('userPassword') - return self.results \ No newline at end of file + return passwords \ No newline at end of file From 9c7502fdb061076d02ff6b23551440014fc5d00c Mon Sep 17 00:00:00 2001 From: Casper Guldbech Nielsen Date: Sat, 19 Sep 2020 12:00:00 +0200 Subject: [PATCH 09/18] Append os enumerator Signed-off-by: Casper Guldbech Nielsen --- ade/modEnumerator/modEnumerator.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/ade/modEnumerator/modEnumerator.py b/ade/modEnumerator/modEnumerator.py index 6aa94be..e115281 100644 --- a/ade/modEnumerator/modEnumerator.py +++ b/ade/modEnumerator/modEnumerator.py @@ -38,6 +38,36 @@ def enumerate_server_names(self, computerobjects: ldap3.Entry) -> dict: return results + def enumerate_os_version(self, computerobjects: ldap3.Entry) -> dict: + '''Return a dict of key(os_version) and value(computers with said os) + + ''' + os_json = { + # Should perhaps include older version + "Windows XP": [], + "Windows Server 2008": [], + "Windows 7": [], + "Windows Server 2012": [], + "Windows 10": [], + "Windows Server 2016": [], + "Windows Server 2019": [] + } + idx = 0 + for _ in computerobjects: + computer = json.loads(computerobjects[idx].entry_to_json()) + idx += 1 + + for os_version in os_json.keys(): + try: + if os_version in computer['attributes'].get('operatingSystem')[0]: + if computer['attributes']['dNSHostName'][0] not in os_json[os_version]: + os_json[os_version].append(computer['attributes']['dNSHostName'][0]) + except TypeError: + # computer['attributes'].get('operatingSystem') is of NoneType, just continue + continue + + return os_json + def enumerate_for_cleartext_passwords(self, peopleobjects: ldap3.Entry, server: str) -> dict: '''Return a dict of key(username) and value(password) From 967a0d43527623f664ee792e0e7347ab172a83d6 Mon Sep 17 00:00:00 2001 From: Casper Guldbech Nielsen Date: Sat, 19 Sep 2020 12:00:23 +0200 Subject: [PATCH 10/18] Utilize os enumerator Signed-off-by: Casper Guldbech Nielsen --- ade/__init__.py | 28 +++------------------------- 1 file changed, 3 insertions(+), 25 deletions(-) diff --git a/ade/__init__.py b/ade/__init__.py index 5c85ee3..95c0fa3 100755 --- a/ade/__init__.py +++ b/ade/__init__.py @@ -260,39 +260,17 @@ def checkForPW(self): enumeration afterwards ''' def checkOS(self): - - os_json = { - # Should perhaps include older version - "Windows XP": [], - "Windows Server 2008": [], - "Windows 7": [], - "Windows Server 2012": [], - "Windows 10": [], - "Windows Server 2016": [], - "Windows Server 2019": [] - } - idx = 0 - for _ in self.computers: - computer = json.loads(self.computers[idx].entry_to_json()) - idx += 1 - - for os_version in os_json.keys(): - try: - if os_version in computer['attributes'].get('operatingSystem'): - os_json[os_version].append(computer['attributes']['dNSHostName']) - except TypeError: - # computer['attributes'].get('operatingSystem') is of NoneType, just continue - continue + os_json = self.enumerator.enumerate_os_version(self.computers) for key, value in os_json.items(): if len(value) == 0: continue - with open('{0}-oldest-OS'.format(self.server), 'w') as f: + with open(f'{self.output}-oldest-OS', 'w') as f: for item in value: f.write('{0}: {1}\n'.format(key, item)) break - print('[ ' + colored('OK', 'green') + ' ] Wrote hosts with oldest OS to {0}-oldest-OS'.format(self.server)) + print('[ ' + colored('OK', 'green') + f' ] Wrote hosts with oldest OS to {self.output}-oldest-OS') def checkSYSVOL(self): From 8c18d5e0d30600a479bd50827b4c47daec903d20 Mon Sep 17 00:00:00 2001 From: Casper Guldbech Nielsen Date: Sat, 19 Sep 2020 12:01:31 +0200 Subject: [PATCH 11/18] Correcting filename on write Signed-off-by: Casper Guldbech Nielsen --- ade/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ade/__init__.py b/ade/__init__.py index 95c0fa3..7f7a289 100755 --- a/ade/__init__.py +++ b/ade/__init__.py @@ -242,7 +242,7 @@ def checkForPW(self): self.passwords = { **passwords, **self.passwords } if len(self.passwords.keys()) > 0: - with open(f'{self.server}-clearpw', 'w') as f: + with open(f'{self.output}-clearpw', 'w') as f: json.dump(self.passwords, f, sort_keys=False) if len(self.passwords.keys()) == 1: From 4f04ffb403b870899b5a0aefb6b3194438f1c26f Mon Sep 17 00:00:00 2001 From: Casper Guldbech Nielsen Date: Sat, 19 Sep 2020 12:01:54 +0200 Subject: [PATCH 12/18] Add ignore filename for testing (run -o testing-testing) Signed-off-by: Casper Guldbech Nielsen --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 8f16f84..7895fc5 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ *.pyc *.json *-jtr-hashes +testing-testing* pyvenv.cfg __pycache__/* ActiveDirectoryEnum.egg-info/* From 4d3a565ded72dac9f38345a78e162b36b92c6cd6 Mon Sep 17 00:00:00 2001 From: Casper Guldbech Nielsen Date: Sat, 19 Sep 2020 12:04:22 +0200 Subject: [PATCH 13/18] Preparing connector types required Signed-off-by: Casper Guldbech Nielsen --- ade/connectors/connectors.py | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/ade/connectors/connectors.py b/ade/connectors/connectors.py index 26be08b..86f859f 100644 --- a/ade/connectors/connectors.py +++ b/ade/connectors/connectors.py @@ -33,4 +33,24 @@ def ldap_connector(self, server: str, ldaps: bool, domuser: str, passwd: str, le print('\033[1A\r[ ' + colored('ERROR', 'red') +' ] Failed to bind to LDAP server: {0}'.format(conn.result['description'])) raise LDAPBindError - return conn \ No newline at end of file + return conn + + + def winrm_connector(self): + pass + + + def rpc_connector(self): + pass + + + def smb_connector(self): + pass + + + def ftp_connector(self): + pass + + + def smtp_connector(self): + pass \ No newline at end of file From 7f5dde155372a18391195b9631e3af888b262959 Mon Sep 17 00:00:00 2001 From: Casper Guldbech Nielsen Date: Sat, 19 Sep 2020 12:06:14 +0200 Subject: [PATCH 14/18] Removing redundant info Signed-off-by: Casper Guldbech Nielsen --- ade/connectors/connectors.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/ade/connectors/connectors.py b/ade/connectors/connectors.py index 86f859f..aadde2e 100644 --- a/ade/connectors/connectors.py +++ b/ade/connectors/connectors.py @@ -13,7 +13,7 @@ def __init__(self): def ldap_connector(self, server: str, ldaps: bool, domuser: str, passwd: str, level='ALL') -> ldap3.Connection: '''Returns an ldap3.Connection object that is bound to the supplied domain controller - Raise ldap3.core.exceptions.LDAPBindError on bind() errors. + Raise LDAPBindError on bind() errors. ''' if ldaps: @@ -30,7 +30,6 @@ def ldap_connector(self, server: str, ldaps: bool, domuser: str, passwd: str, le conn.bind() # Validate the login (bind) request if int(conn.result['result']) != 0: - print('\033[1A\r[ ' + colored('ERROR', 'red') +' ] Failed to bind to LDAP server: {0}'.format(conn.result['description'])) raise LDAPBindError return conn From 1365b1a36cf83787c6ff80150105c542885b7a29 Mon Sep 17 00:00:00 2001 From: Casper Guldbech Nielsen Date: Thu, 24 Sep 2020 17:39:57 +0200 Subject: [PATCH 15/18] Add smb connector Signed-off-by: Casper Guldbech Nielsen --- ade/connectors/connectors.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/ade/connectors/connectors.py b/ade/connectors/connectors.py index aadde2e..70733a7 100644 --- a/ade/connectors/connectors.py +++ b/ade/connectors/connectors.py @@ -1,5 +1,8 @@ import ldap3 from ldap3.core.exceptions import LDAPBindError +from impacket import smbconnection +from impacket.smbconnection import SessionError +from impacket.nmb import NetBIOSTimeout, NetBIOSError import sys from termcolor import colored @@ -43,8 +46,14 @@ def rpc_connector(self): pass - def smb_connector(self): - pass + def smb_connector(self, server: str, domuser: str, passwd: str) -> smbconnection: + try: + smbconn = smbconnection.SMBConnection(f'\\\\{server}\\', server, timeout=5) + smbconn.login(domuser, passwd) + except (SessionError, UnicodeEncodeError, NetBIOSError): + smbconn.close() + return False + return smbconn def ftp_connector(self): @@ -52,4 +61,5 @@ def ftp_connector(self): def smtp_connector(self): - pass \ No newline at end of file + pass + \ No newline at end of file From bc8155d0c971375636dc184502ae7c24e30e58e7 Mon Sep 17 00:00:00 2001 From: Casper Guldbech Nielsen Date: Thu, 24 Sep 2020 17:40:12 +0200 Subject: [PATCH 16/18] Change usage to smb connector Signed-off-by: Casper Guldbech Nielsen --- ade/__init__.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/ade/__init__.py b/ade/__init__.py index c2935dd..aa9a275 100755 --- a/ade/__init__.py +++ b/ade/__init__.py @@ -301,8 +301,7 @@ def checkSYSVOL(self): print('[ .. ] Searching SYSVOL for cpasswords\r') cpasswords = {} try: - smbconn = smbconnection.SMBConnection('\\\\{0}\\'.format(self.server), self.server, timeout=5) - smbconn.login(self.domuser, self.passwd) + smbconn = self.connectors.smb_connector(self.server, self.domuser, self.passwd) dirs = smbconn.listShares() for share in dirs: if str(share['shi1_netname']).rstrip('\0').lower() == 'sysvol': @@ -423,8 +422,7 @@ def enumSMB(self): try: # Changing default timeout as shares should respond withing 5 seconds if there is a share # and ACLs make it available to self.user with self.passwd - smbconn = smbconnection.SMBConnection('\\\\' + str(dnsname), str(dnsname), timeout=5) - smbconn.login(self.domuser, self.passwd) + smbconn = self.connectors.smb_connector(self.server, self.domuser, self.passwd) dirs = smbconn.listShares() self.smbBrowseable[str(dnsname)] = {} for share in dirs: From 8d5d65836419792eadd89db1c7d14059d3302820 Mon Sep 17 00:00:00 2001 From: Casper Guldbech Nielsen Date: Thu, 24 Sep 2020 17:59:51 +0200 Subject: [PATCH 17/18] Print correction Signed-off-by: Casper Guldbech Nielsen --- ade/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ade/__init__.py b/ade/__init__.py index aa9a275..7291dc1 100755 --- a/ade/__init__.py +++ b/ade/__init__.py @@ -458,9 +458,9 @@ def enumSMB(self): availDirs.append(key) if len(self.smbShareCandidates) == 1: - print('[ ' + colored('OK', 'green') + ' ] Searched {0} share and {1} with {2} subdirectories/files is browseable by {3}'.format(len(self.smbShareCandidates), len(self.smbBrowseable.keys()), len(availDirs), self.domuser)) + print('[ ' + colored('OK', 'green') + ' ] Searched {0} share and {1} share with {2} subdirectories/files is browseable by {3}'.format(len(self.smbShareCandidates), len(self.smbBrowseable.keys()), len(availDirs), self.domuser)) else: - print('[ ' + colored('OK', 'green') + ' ] Searched {0} shares and {1} with {2} subdirectories/file sare browseable by {3}'.format(len(self.smbShareCandidates), len(self.smbBrowseable.keys()), len(availDirs), self.domuser)) + print('[ ' + colored('OK', 'green') + ' ] Searched {0} shares and {1} shares with {2} subdirectories/file sare browseable by {3}'.format(len(self.smbShareCandidates), len(self.smbBrowseable.keys()), len(availDirs), self.domuser)) if len(self.smbBrowseable.keys()) > 0: with open('{0}-open-smb.json'.format(self.server), 'w') as f: json.dump(self.smbBrowseable, f, indent=4, sort_keys=False) From 08cc778696e36a65890a0d0d3fdcb214129b80ef Mon Sep 17 00:00:00 2001 From: Casper Guldbech Nielsen Date: Thu, 24 Sep 2020 19:41:56 +0200 Subject: [PATCH 18/18] Adding rpc connector Signed-off-by: Casper Guldbech Nielsen --- ade/connectors/connectors.py | 38 ++++++++++++++++++++++++++++++++++-- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/ade/connectors/connectors.py b/ade/connectors/connectors.py index 70733a7..0099a3c 100644 --- a/ade/connectors/connectors.py +++ b/ade/connectors/connectors.py @@ -1,8 +1,16 @@ +# LDAP connection import ldap3 from ldap3.core.exceptions import LDAPBindError +# SMB connection from impacket import smbconnection from impacket.smbconnection import SessionError from impacket.nmb import NetBIOSTimeout, NetBIOSError +# RPC connection +from impacket.dcerpc.v5 import transport +# For when impacket releases the feature as a release +#from impacket.http import AUTH_NTLM +from socket import gaierror +# Generic imports import sys from termcolor import colored @@ -42,8 +50,34 @@ def winrm_connector(self): pass - def rpc_connector(self): - pass + def rpc_connector(self, server: str, domuser: str, passwd: str): + rpc_protocols = { + 135: 'ncacn_ip_tcp:{}[135]', + 139: 'ncacn_np:{}[\pipe\epmapper]', + 443: 'ncacn_http:[593,RpcProxy={}:443]', + #445: 'ncacn_np:{}[\pipe\epmapper]', + } + dce = False + + for port, protocol in rpc_protocols.items(): + transporter = transport.DCERPCTransportFactory(protocol.format(server)) + + transporter.set_credentials(domuser, passwd, server, '', '') + + if port == 139 or port == 445: + transporter.setRemoteHost(server) + transporter.set_dport(port) + #elif [443] in protocol: + # transporter.set_auth_type(AUTH_NTLM) + try: + dce = transporter.get_dce_rpc() + dce.connect() + # We can break to return since the connection did not raise an error + break + except (NetBIOSError, gaierror): + # Something was off + continue + return dce def smb_connector(self, server: str, domuser: str, passwd: str) -> smbconnection: