From fa87370bfb22c46fd27e7dd4177710f25d17e6f5 Mon Sep 17 00:00:00 2001 From: "ellen.freeman" Date: Tue, 18 Nov 2025 10:58:30 +0530 Subject: Added Kerberos Support and ported from LDAP3 to Impacket's LDAP --- Resurrect/README.md | 17 +- Resurrect/requirements.txt | 1 + Resurrect/resurrect.py | 403 ++++++++++++++++++++++++++------------------- 3 files changed, 249 insertions(+), 172 deletions(-) diff --git a/Resurrect/README.md b/Resurrect/README.md index a616b77..a4c88a1 100644 --- a/Resurrect/README.md +++ b/Resurrect/README.md @@ -14,7 +14,7 @@ Resurrect is a Python-based tool designed to interact with Active Directory's De - **LDAPS Support** - Secure LDAP over SSL/TLS (port 636) - **Pass-The-Hash** - Authenticate using NTLM hashes without plaintext passwords - +- **KERBEROS SUPPORT** - Authentication with Kerberos ## 🔍 Object Discovery @@ -67,6 +67,11 @@ python3 resurrect.py find --domain example.com --username admin \ python3 resurrect.py find --domain example.com --username admin \ --hash '8846f7eaee8fb117ad06bdd830b7586c' \ --target 10.10.11.72 --ldaps + + +python3 resurrect.py find --domain example.com --username admin \ +-k --dc-host dc.example.com --ldaps + ``` @@ -83,6 +88,14 @@ python3 resurrect.py restore --domain example.com --username admin \ --hash '149e0ed1f84c8fd4ecb11a9c2ab7af2b' --target 10.10.11.72 --ldaps \ --guid "f88369c8-86a2-4a7f-a56c-9c15edd7d1e3" \ --ou "OU=IT,OU=Users,DC=example,DC=com" + + + python3 resurrect.py restore --domain example.com --username admin \ + -k --dc-host dc.example.com --ldaps \ + --guid "f88369c8-86a2-4a7f-a56c-9c15edd7d1e3" \ + --ou "OU=IT,OU=Users,DC=example,DC=com" + + ``` @@ -136,7 +149,7 @@ python3 resurrect.py restore --domain example.com --username admin \ ## TODO -- [ ] **Kerberos Support** - Add Kerberos authentication as an alternative to NTLM +- [x] **Kerberos Support** - Add Kerberos authentication as an alternative to NTLM - [ ] Export deleted objects to JSON/CSV - [ ] Find objects by their sAMAccountName - [ ] Automatic Restoration to OUs diff --git a/Resurrect/requirements.txt b/Resurrect/requirements.txt index 94fd5dd..c61b991 100644 --- a/Resurrect/requirements.txt +++ b/Resurrect/requirements.txt @@ -1,2 +1,3 @@ tabulate ldap3 +impacket diff --git a/Resurrect/resurrect.py b/Resurrect/resurrect.py index d06fae2..9126640 100644 --- a/Resurrect/resurrect.py +++ b/Resurrect/resurrect.py @@ -4,84 +4,142 @@ #!/usr/bin/env python3 -from ldap3 import Server, Connection, ALL, NTLM, BASE, MODIFY_DELETE, MODIFY_REPLACE import argparse import sys from tabulate import tabulate -from ldap3.core.exceptions import ( - LDAPInvalidCredentialsResult, - LDAPBindError, - LDAPSocketOpenError, - LDAPException -) - +import os +from impacket.ldap import ldap, ldapasn1 +from impacket.ldap.ldapasn1 import ModifyRequest, Control +from pyasn1.type.univ import SequenceOf,Sequence, OctetString, Integer, SetOf + +def create_ldap_connection(args): + dom = args.domain.split('.')[0] + tld = args.domain.split('.')[1] + + if args.target: + server_host = args.target + print(f"[*] Connecting to {args.target}") + elif args.dc_host: + server_host = args.dc_host + print(f"[*] Connecting to {args.dc_host}") + else: + print("[!] Please specify either a DC or a target (--dc-host/--target)") + return None + + + if args.k: + if not args.dc_host: + print("[!] Kerberos Authentication Selected but --dc-host not specified") + sys.exit() -def find_deleted_objects(args): - try: - if args.target: - print(f"[*] Connecting to {args.target}") - elif args.dc: - print(f"[*] Connecting to {args.dc}") - dom = args.domain.split('.')[0] - tld = args.domain.split('.')[1] - if args.target: - if args.ldaps: - s = Server(host=args.target, port=636,use_ssl=True, get_info='ALL') - else: - s = Server(host=args.target, port=389, use_ssl=False, get_info='ALL') - elif args.dc: + print("[*] Attempting Kerberos authentication") + + if os.environ.get('KRB5CCNAME'): + print(f"[*] Using Kerberos ccache from KRB5CCNAME: {os.environ.get('KRB5CCNAME')}") + else: + print("[*] Using default Kerberos ccache location") + + try: if args.ldaps: - s = Server(host=args.dc, port=636,use_ssl=True, get_info='ALL') + conn = ldap.LDAPConnection(f'ldaps://{args.dc_host}') + conn.kerberosLogin(f'{args.username}', '', f'{args.domain}', '', '') else: - s = Server(host=args.dc, port=389,use_ssl=False, get_info='ALL') - else: - print("[!] Please Specify either a DC or a target") - sys.exit() + conn = ldap.LDAPConnection(f'ldap://{args.dc_host}') + conn.kerberosLogin(f'{args.username}', '', f'{args.domain}', '', '') + + print("[*] Kerberos Authentication successful") + return conn + + except Exception as e: + print(f"[!] Kerberos authentication error: {e}") + return None - if args.hash: - if len(args.hash) == 32: - try: - conn = Connection(s, user=f"{dom}\\{args.username}", password=f"aad3b435b51404eeaad3b435b51404ee:{args.hash}", auto_bind=True, authentication=NTLM, version=3, check_names=True, raise_exceptions=True) - except LDAPInvalidCredentialsResult as e: - print("[!] Authentication failed: ", e) - sys.exit() - - except LDAPSocketOpenError as e: - print("[!] Unable to connect to the target: ",e) - sys.exit() - elif len(args.hash) != 32: - print("[!] Hash Length mismatch") - sys.exit() - elif args.password: + elif args.hash: + if len(args.hash) == 32: try: - conn = Connection(s, user=f"{dom}\\{args.username}", password=args.password, auto_bind=True, authentication=NTLM, version=3, check_names=True, raise_exceptions=True) - except LDAPInvalidCredentialsResult as e: - print("[!] Authentication failed: ", e) - sys.exit() - except LDAPSocketOpenError as e: - print("[!] Unable to connect to the target: ", e) - sys.exit() - else: - print("[!] Please Specify either a hash or a password") - sys.exit() + if args.ldaps: + conn = ldap.LDAPConnection(f'ldaps://{server_host}') + else: + conn = ldap.LDAPConnection(f'ldap://{server_host}') - if not conn.bind(): - print("[!] Could not connect to the server") - sys.exit() + lmhash = 'aad3b435b51404eeaad3b435b51404ee' + nthash = args.hash + + conn.login( + user=args.username, + password='', + domain=args.domain, + lmhash=lmhash, + nthash=nthash + + ) + print("[*] Authentication successful") + return conn + except Exception as e: + print(f"[!] Authentication Error: {e}") else: + print("[!] Hash length mismatch - should be 32 characters") + return None + + elif args.password: + try: + if args.ldaps: + conn = ldap.LDAPConnection(f'ldaps://{server_host}') + else: + conn = ldap.LDAPConnection(f'ldap://{server_host}') + + conn.login( + user=args.username, + password=args.password, + domain=args.domain, + ) print("[*] Authentication successful") + return conn + except Exception as e: + print(f"[!] Authentication Error: {e}") + else: + print("[!] Please specify an authentication method (password, hash, or Kerberos)") + return None - entry_list = conn.extend.standard.paged_search( - search_base = f'CN=Deleted Objects,DC={args.domain.split(".")[0]},DC={args.domain.split(".")[1]}', - search_filter = '(&(|(objectClass=User)(objectCategory=Computer))(isDeleted=TRUE))', - search_scope = 'SUBTREE', - attributes = ['cn', 'sAMAccountName', 'objectClass', 'lastKnownParent'], - controls= [ - ('1.2.840.113556.1.4.417', True, None) - ], - paged_size = args.page_size if args.page_size else 10, - generator=False - ) + + +def find_deleted_objects(args): + try: + conn = create_ldap_connection(args) + if not conn: + sys.exit(1) + + search_base = f'CN=Deleted Objects,DC={args.domain.split(".")[0]},DC={args.domain.split(".")[1]}' + search_filter = '(&(|(objectClass=User)(objectCategory=Computer))(isDeleted=TRUE))' + attributes = ['cn', 'sAMAccountName', 'objectClass', 'lastKnownParent'] + show_deleted_control = ldapasn1.Control() + show_deleted_control['controlType'] = ldapasn1.LDAPOID('1.2.840.113556.1.4.417') + show_deleted_control['criticality'] = True + + entry_list = [] + page_size = args.page_size if args.page_size else 10 + cookie = b'' + + while True: + paging_control = ldapasn1.SimplePagedResultsControl(criticality=False, size=page_size, cookie=cookie) + try: + resp = conn.search( + searchBase = search_base, + searchFilter = search_filter, + scope=ldapasn1.Scope('wholeSubtree'), + attributes=attributes, + searchControls=[show_deleted_control, paging_control] + ) + + except Exception as e: + print("[!] Search Error: ", e) + break + + for item in resp: + if isinstance(item, ldapasn1.SearchResultEntry): + entry_list.append(item) + if not cookie: + break if not entry_list: print("[*] No deleted users found or your current user doesn't have the permissions to view them") sys.exit() @@ -89,14 +147,22 @@ def find_deleted_objects(args): print("[*] Deleted user(s) found\r\t") data = [] for entry in entry_list: - attrs = entry.get('attributes') + attrs = {} + for attr in entry['attributes']: + attr_name = str(attr['type']) + attr_values = [str(val) for val in attr['vals']] + attrs[attr_name] = attr_values + if not attrs: continue - guid = attrs.get('cn').split('\n')[1].split(':')[1] - ou = attrs.get('lastKnownParent') - sam = attrs.get('sAMAccountName') + + cn = attrs.get('cn', [''])[0] + guid = cn.split('\n')[1].split(':')[1] + ou = attrs.get('lastKnownParent', [''])[0] + sam = attrs.get('sAMAccountName', [''])[0] objectclass = attrs.get('objectClass')[3] data.append([sam, guid, ou, objectclass]) + headers = ['username', 'GUID', 'OU', 'objectClass'] print(tabulate(data, headers=headers, tablefmt='grid')) except Exception as e: @@ -104,118 +170,110 @@ def find_deleted_objects(args): def restore_deleted_objects(args): - + import uuid try: + conn = create_ldap_connection(args) + if not conn: + sys.exit(1) - if args.target: - print(f"[*] Connecting to {args.target}") - elif args.dc: - print(f"[*] Connecting to {args.dc}") - dom = args.domain.split('.')[0] - tld = args.domain.split('.')[1] - if args.target: - if args.ldaps: - s = Server(host=args.target, port=636,use_ssl=True, get_info='ALL') - else: - s = Server(host=args.target, port=389, use_ssl=False, get_info='ALL') - elif args.dc: - if args.ldaps: - s = Server(host=args.dc, port=636,use_ssl=True, get_info='ALL') - else: - s = Server(host=args.dc, port=389,use_ssl=False, get_info='ALL') - else: - print("[!] Please Specify either a DC or a target") - sys.exit() - - if args.hash: - if len(args.hash) == 32: - try: - conn = Connection(s, user=f"{dom}\\{args.username}", password=f"aad3b435b51404eeaad3b435b51404ee:{args.hash}", auto_bind=True, authentication=NTLM, version=3, check_names=True, raise_exceptions=True) - except LDAPInvalidCredentialsResult as e: - print("[!] Authentication failed: ", e) - sys.exit() - - except LDAPSocketOpenError as e: - print("[!] Unable to connect to the target: ",e) - sys.exit() - elif len(args.hash) != 32: - print("[-] Hash Length mismatch") - sys.exit() - elif args.password: - try: - conn = Connection(s, user=f"{dom}\\{args.username}", password=args.password, auto_bind=True, authentication=NTLM, version=3, check_names=True, raise_exceptions=True) - except LDAPInvalidCredentialsResult as e: - print("[!] Authentication failed: ", e) - sys.exit() - except LDAPSocketOpenError as e: - print("[!] Unable to connect to the target: ", e) - sys.exit() - else: - print("[!] Please Specify either a hash or a password") - sys.exit() + if args.guid: - if not conn.bind(): - print("[-] Could not connect to the server") - sys.exit() - else: - print("[*] Authentication successful") + + def guid_to_ldap_filter(g): + u = uuid.UUID(g) + return ''.join('\\%02X' % b for b in u.bytes_le) + + guid_filter = guid_to_ldap_filter(args.guid) + + + search_base = f'CN=Deleted Objects,DC={args.domain.split(".")[0]},DC={args.domain.split(".")[1]}' + search_filter = f'(&(objectGuid={guid_filter})(isDeleted=TRUE))' + attributes = ['distinguishedName'] + show_deleted_control = ldapasn1.Control() + show_deleted_control['controlType'] = ldapasn1.LDAPOID('1.2.840.113556.1.4.417') + show_deleted_control['criticality'] = True + + entry_list = [] + page_size = 10 + cookie = b'' + + try: + resp = conn.search( + searchBase = search_base, + searchFilter = search_filter, + scope=ldapasn1.Scope('wholeSubtree'), + attributes=attributes, + searchControls=[show_deleted_control] + ) - if args.guid: - entry_list = conn.extend.standard.paged_search( - search_base = f'CN=Deleted Objects,DC={args.domain.split(".")[0]},DC={args.domain.split(".")[1]}', - search_filter = f'(&(objectGuid={args.guid})(isDeleted=TRUE))', - search_scope = 'SUBTREE', - attributes = ['distinguishedName'], - controls= [ - ('1.2.840.113556.1.4.417', True, None) - ] - ) + except Exception as e: + print("[!] Search Error: ", e) + + for item in resp: + if isinstance(item, ldapasn1.SearchResultEntry): + entry_list.append(item) if not entry_list: - print("[*] Could not find an object with the supplied GUID") + print("[!] Could not find any object with the supplied GUID") sys.exit() else: + data = [] for entry in entry_list: - attrs = entry.get('attributes') - if attrs: - dn = attrs.get('distinguishedName') - print(f"[*] Found Object : {dn}") - cn = dn.split(':')[0].split('\\')[0] - - else: - print("[!] Could not extract DN for the target object") - - new_dn = f"{cn},{args.ou}" - + attrs = {} + for attr in entry['attributes']: + attr_name = str(attr['type']) + attr_values = [str(val) for val in attr['vals']] + attrs[attr_name] = attr_values + + if not attrs: + continue + + dn= attrs.get('distinguishedName', [''])[0] + print(f"[*] Object Found: {dn}") + cn = dn.split(':')[0].split('\\')[0] + new_dn = f"{cn},{args.ou}" + + + + + + changes = SequenceOf() + change1 = Sequence() + change1.setComponentByPosition(0, Integer(1)) + mod1 = Sequence() + mod1.setComponentByPosition(0, OctetString("isDeleted")) + mod1.setComponentByPosition(1, SetOf()) + change1.setComponentByPosition(1, mod1) + changes.append(change1) + change2 = Sequence() + change2.setComponentByPosition(0, Integer(2)) + mod2 = Sequence() + mod2.setComponentByPosition(0, OctetString("distinguishedName")) + vals2 = SetOf() + vals2.append(OctetString(new_dn)) + mod2.setComponentByPosition(1, vals2) + change2.setComponentByPosition(1, mod2) + changes.append(change2) + req = ModifyRequest() + req.setComponentByName('object', OctetString(dn)) + req.setComponentByName('changes', changes) + ctrl = Control() + ctrl['controlType'] = '1.2.840.113556.1.4.417' + ctrl['criticality'] = True + ctrl['controlValue'] = b'' + print("[*] Restoring Object with GUID: ", args.guid) try: - changes = { - 'isDeleted': [(MODIFY_DELETE, [])], - 'distinguishedName': [(MODIFY_REPLACE, [new_dn])] - - } - - print("[*] Attempting to restore object") - results = conn.modify( - dn, - changes, - controls=[ - ('1.2.840.113556.1.4.417', True, None) - ] - ) - if results: - print("[*] Object restored successfully") - print(f"[*] New DN: {new_dn}") - else: - print(f"[!] Could not restore object: {conn.result} ") - print(f"[!] Error: {conn.last_error}") + resp = conn.send(req, controls=[ctrl]) + print("[+] Restore successful!") + print(f"[+] New DN = {new_dn}") except Exception as e: - print("[!] Error during restore operation: ", e) - - except Exception as e: - print("[-] An error has occured", e) + print("[!] Restore failed:", e) + + except Exception as e: + print("[!] Error during restore operation: ", e) def main(): @@ -232,7 +290,11 @@ python3 {sys.argv[0]} find --domain example.com --username admin --password pass python3 {sys.argv[0]} restore --domain example.com --username admin --password password123 --target 10.10.11.70 --guid f80369c8-96a2-4a7f-a56c-9c15edd7d1e3 --ou "OU=Staff,DC=evilcorp,DC=com" # Pass-The-Hash Support -python3 resurrect.py find --domain example.com --username admin --ldaps --target 10.10.11.70 --hash 149e0ed1f84c8fd4ecb11a9c2ab7af2 +python3 {sys.argv[0]} find --domain example.com --username admin --ldaps --target 10.10.11.70 --hash 149e0ed1f84c8fd4ecb11a9c2ab7af2 + +# Kerberos Support +python3 {sys.argv[0]} find --domain example.com --username admin --ldaps --dc-host dc.example.com -k +python3 {sys.argv[0]} restore --domain example.com --username admin -k --dc-host dc.example.com --guid f80369c8-96a2-4a7f-a56c-9c15edd7d1e3 --ou "OU=Staff,DC=evilcorp,DC=com" """ ) @@ -248,10 +310,11 @@ python3 resurrect.py find --domain example.com --username admin --ldaps --target subparsers.add_argument("--domain", help="Target Domain", required=True) subparsers.add_argument("--username", help="Username", required=True) subparsers.add_argument("--password", help="Password", required=False) - subparsers.add_argument("--dc", help="Domain Controller", required=False) + subparsers.add_argument("--dc-host", help="Domain Controller FQDN", required=False) subparsers.add_argument("--target", help="Target", required=False) subparsers.add_argument("--ldaps", help="Force LDAP to authenticate over SSL", action="store_true", required=False) subparsers.add_argument("--hash", help="LM:NTLM hash", required=False) + subparsers.add_argument("-k", help="Use Kerberos for Authentication",action="store_true",required=False) find_parser = subparsers.add_parser( 'find', -- cgit v1.2.3