CVE-2019-1579是orange在2019年7月份公开的Attacking SSL VPN系列文章的第一篇,文章原文在这里:https://blog.orange.tw/2019/07/attacking-ssl-vpn-part-1-preauth-rce-on-palo-alto.html。文章描述了他在GlobalProtect中发现的一个漏洞,这个漏洞是一个未授权的格式化字符串漏洞,文章最后orange也给出了任意命令执行的exp。未授权加上任意命令执行,基本可以做到为所欲为。一时兴起,来调试一下试试。

1. 准备工作

工欲善其事,必先利其器,前期的信息搜集是必不可少的。Google、GitHub上一通搜索,只发现了一个现成的exp,幸运的是作者也给出了文章说明。文章地址为https://www.securifera.com/blog/2019/09/10/preauth-rce-on-palo-alto-globalprotect-part-ii-cve-2019-1579/,代码地址为https://github.com/securifera/CVE-2019-1579。结合这个作者的文章和代码一起阅读,给我启发甚大,让我之后少走了许多弯路,感谢这个作者!

我之后的调试分析就是按着这个作者的思路来的,他的这个思路可以作为这种场景下的通用框架。也就是说,在没有binary文件和调试环境的情况下,用一个格式化字符串漏洞来克服所有限制,具体是怎么做到的呢?

  1. 根据格式化字符串漏洞找出格式化串与输入之间的偏移
  2. dump进程的内存,主要是binary本身的数据
  3. 分析dump出来的内存,找出必要的信息(本次是strlen_GOT和system_PLT)
  4. 完成利用(用system_PLT覆盖strlen_GOT)

下面就根据这四点,对整个调试分析过程做详细点的说明:

2. 确定偏移

确定偏移是所有场景下格式化字符串漏洞利用的第一步工作,没有之一。这个偏移指的是格式化串和输入之间地距离。知道偏移之后,就可以找出我们输入数据的位置,然后就可以可控地做一些事。如何确定偏移?如下图所示,我们的输入是”AAAAAAAA”,格式化串为”%3$llx”,此时格式化串取得值0xffffffff,显然不是我们的输入,因此将3递增,直到找到我们的输入,此时n就是偏移。

对应文件poc_leak.py。需要注意的是,此http post请求有5个参数,每个参数都有自己的偏移,但是一次poc_leak只能确定4个偏移,因为要用一个参数来存储格式化串来泄露信息。所以要想完整地获取五个偏移,得再运行一次poc_leak,但是需要先修改poc_leak.py,让另一个参数来存储格式化串。

3. dump内存

导出sslmgr的core dump文件,能够知道sslmgr的进程内存布局:scp export core-file management-plane from * to root@11.11.11.11:/tmp

有了偏移,我们就可以读写进程的任意内存(前提是可读可写)。输入的是地址值,读用”%s”,写用”%n”。但不幸的是输入是有限制的。因为需要用http请求发送payload,所以不仅要处理’\x00’这个坏字符,还要处理’\x25’,’\x26’这两个坏字符。关于这三个坏字符的处理,大神作者的博客中给出了详细的说明:

需要说明的是,’\x00’和’\x26’这两个坏字符的处理是用格式化串来代替,分别对应’%10$c’、’%170$c’。10,170这两个数字不是随机生成的。10代表的是格式化串下面第10个偏移处,c代表只取最后一个字节。在栈上,’\x00’很容易找,但是’\x26’就没那么容易,一个最稳妥的方式就是自己制造一个’\x26’出来。大神作者就是自己在栈上写入了一个’\x26’,具体做法就是在栈上找到一个指向栈的指针,用’%n’写入’\x26\x26’,这样就可以用”%170$c”这种方式替换掉坏字符了,170是指针指向的内容在栈上与格式化串之间的偏移。

但是,我这个环境是x86_64,而且系统版本是8.0.5,反复调试分析发现,无法在栈上找到一个指向栈的指针。因此就无法用上面的方式替换掉’\x26’这个坏字符,造成的影响就是,只要地址中含有’\x26’这个坏字符,就无法泄露出来它的内容(如果’%s’能够跳过这个地址,是可以泄露它的内容)。

因此,我修改了一下大神作者的脚本,把对’\x26’坏字符的处理部分给去掉了。要不然,dump出来的内存根本不可用。

4. 分析binary

dump出来内存之后,可以通过readelf查看:

可以看到,能够识别elf的头部,以及program header,无法识别出来各个section。这很正常,因为elf此时是执行视图,而我们需要的是elf的链接视图。链接视图和执行视图分别用来反映ELF文件在磁盘中和被加载到内存中的情况。

在执行视图下,section table是可选的,而我们要找的plt,got都是section。经过分析,我们并不需要知道elf所有的section,理论上只需要知道plt的结构就行,可以通过plt来得知对应函数的got地址。但是实际上只知道plt是不行的,因为没法知道各个plt条目对应哪个函数。而函数名称的解析跟symtab、strtab两个section有关。

总的来说,我们只需要知道三个section就可以得到所有函数的plt地址、got地址,这三个section就是symtab、strtab、plt。三个section都在binary的头部,而且特征比较明显,可以通过对比正常的x64 binary,将三个section“抠”出来。

resolve_symbol_table.py用来完成解析三个section,得到各个函数的plt address和got address。其中需要特别说明的是symtab,这个section存储了一个Elf64_Sym数组,说明了这个symbol的所有信息。利用其中的st_info可以过滤掉一些不在plt中的symbol。

struct Elf64_Sym
{
  Elf64_Word    st_name;   /* 4 Symbol name (string tbl index) */
  unsigned char st_info;   /* 1 Symbol type and binding */
  unsigned char st_other;  /* 1 Symbol visibility */
  Elf64_Section st_shndx;  /* 2 Section index */
  Elf64_Addr    st_value;  /* 8 Symbol value */
  Elf64_Xword   st_size;   /* 8 Symbol size */
};

5. 完成利用

通过分析binary可以得到strlen_GOT和system_PLT的值(这里的system不叫system,而叫pan_sys_system)。现在就把strlen_GOT的内容改为system_PLT的值,这样当调用strlen的时候,实际会调用system函数,能够执行任意命令。

具体过程如下:

因为目标进程是小端序,因此先将strlen_GOT的高地址处写入0(strlen_GOT+3)。之后将system_PLT的值,三个字节分为两次写入。完成之后就可以执行任意命令。

可以用sleep命令来验证是否执行了命令:python CVE-2019-1579_8.0.5_x64.py -i 192.168.1.88 -c "sleep 10"

完整的代码:https://github.com/longuan/CVE-2019-1579