原文始发于CataLpa:一种获取 FortiOS 权限的方法
如何获取 FortiOS shell 权限?
绕过判断逻辑
最近很多朋友来询问我如何获取新版本 FortiOS 的 root 权限,在前面文章中我提到过一篇参考资料,不过这篇资料是针对旧版本 FortiOS 的,Fortinet 曾在 2019 年 11 月 14 日发布了一个影响 FortiOS 的 CVE,在这个漏洞的描述中,官方认为 VM 应用缺少对文件系统的检查,可能导致攻击者向系统中注入恶意程序。为此在启动流程中添加了文件系统校验,按照参考资料的步骤修改系统文件之后可能会导致无法启动或者无限重启。
对于这个问题,我们首先想到的是定位检查逻辑,看看能否根据算法打包出正确的文件或者直接将检查逻辑 patch 掉。先看一下系统启动时的输出信息:

考虑文件系统的校验可能是在内核或者用户态实现,我们首先在文件系统中尝试搜索 System is starting 字符串

在二进制文件 bin/init 中找到了匹配,逆向该文件看看这个字符串是何时打印的:
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 |
t_printf("\nSystem is starting...\n", v11, v8, v12, v5, v6, requested_time.tv_sec);// 打印字符串 fflush(stdout); sub_206FB40(); reboot(0); close(0); close(1); close(2); sub_44DE80(0); chdir("/"); setsid(); v14 = sub_44DDF0("/dev/null"); v15 = v14; if ( v14 >= 0 ) { dup2(v14, 0); dup2(v15, 1); dup2(v15, 2); } if ( sub_290E8D0(1024LL, 1LL) < 0 ) { t_printf("could not setup epoll in init.\n", 1, v16, v17, v18, v19, requested_time.tv_sec); } else if ( sub_452C00() >= 0 ) { sub_450A80(); sub_1F6CD90(16, "%s()-%d: %s: run_initlevel(SYSINIT)\n\n", "main", 2649, "main", v20, requested_time.tv_sec); sub_44E2D0(1LL); sub_451670(); sub_450BC0(); if ( sub_44F240() ) do_halt(); if ( !sub_44F1A0() ) do_halt(); if ( sub_2745D50() ) { sub_2825E00(); if ( sub_44DFC0("/bin/fips_self_test") ) do_halt(); } else { if ( sub_44F1F0() ) do_halt(); sub_2781560(); } // ... } // ... |
我们看到在 init 程序的 main 函数中第 82 行位置打印了此字符串,而向下分析发现有几处判断,其中比较明显的位置引用了 /bin/fips_self_test 字符串。do_halt 函数的内容:
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 |
unsigned __int64 do_halt() { FILE *v0; // rax FILE *v1; // r12 int v2; // eax int v3; // er12 struct timespec requested_time; // [rsp+0h] [rbp-30h] BYREF unsigned __int64 v6; // [rsp+18h] [rbp-18h] v6 = __readfsqword(0x28u); sub_451FA0("do_halt", 1388); v0 = fopen("/etc/shutdown.dat", "w"); if ( v0 ) { v1 = v0; fprintf(v0, "%d", 1LL); fclose(v1); sync(); sleep(1u); } sub_44EF50(); v2 = open("/dev/console", 2305); v3 = v2; if ( v2 >= 0 ) { dprintf(v2, "\r\nThe system is halted.\r\n"); fsync(v3); close(v3); } requested_time.tv_sec = 2LL; requested_time.tv_nsec = 0LL; while ( nanosleep(&requested_time, &requested_time) == -1 && *__errno_location() == 4 ) ; if ( !fork() ) reboot(1126301404); while ( pause() ) ; return v6 - __readfsqword(0x28u); } |
这里会输出信息 The system is halted,表示系统已停止运行。
第一个判断函数内部执行了 ioctl 和 socket 等函数,向内核发送或接收某些信息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
int __fastcall sub_2907930(int a1, __int64 a2) { int v2; // eax int v3; // er13 int v4; // er12 if ( dword_468D350 >= 0 ) return ioctl(dword_468D350, a1, a2); v2 = socket(2, 2, 0); v3 = v2; if ( v2 < 0 ) return -1; v4 = ioctl(v2, a1, a2); close(v3); return v4; } |
第二个函数 fork 出一个子进程,主要处理逻辑:
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 |
_BOOL8 sub_447D40() { char v0; // cl char v1; // dl __int64 i; // rax char v3; // cl char v4; // dl __int64 v5; // rax FILE *v6; // rax FILE *v7; // r12 int v8; // ebx FILE *v9; // r13 _BOOL8 v10; // r12 __int64 v11; // rax __int64 v12; // r15 __int64 v13; // rax __int64 v14; // r14 void *v15; // rsp void *v16; // rsp size_t v17; // rax __int64 v19; // [rsp+0h] [rbp-F0h] BYREF size_t n; // [rsp+8h] [rbp-E8h] void *v21; // [rsp+10h] [rbp-E0h] __int64 *v22; // [rsp+18h] [rbp-D8h] void *v23; // [rsp+20h] [rbp-D0h] BYREF __int64 v24; // [rsp+28h] [rbp-C8h] char v25[32]; // [rsp+30h] [rbp-C0h] BYREF char filename[8]; // [rsp+50h] [rbp-A0h] BYREF __int16 v27; // [rsp+58h] [rbp-98h] __int64 ptr[16]; // [rsp+70h] [rbp-80h] BYREF v0 = 97; v1 = 78; ptr[9] = __readfsqword(0x28u); *filename = 0x42F1C441217474ELL; strcpy(ptr, "aiqu0oZi"); for ( i = 0LL; ; v0 = *(ptr + i) ) { v25[i++] = v0 ^ v1; if ( i == 8 ) break; v1 = filename[i]; } v3 = 97; v4 = 78; v24 = 0x42F1C441217474ELL; strcpy(ptr, "aiqu0oZi"); v5 = 0LL; v25[8] = 0; while ( 1 ) { filename[v5++] = v3 ^ v4; if ( v5 == 8 ) break; v4 = *(&v24 + v5); v3 = *(ptr + v5); } v27 = 50; v6 = fopen(filename, "r"); v7 = v6; if ( v6 && (v8 = fread(ptr, 1uLL, 0x40uLL, v6), fclose(v7), v8 > 0) && (v9 = fopen(v25, "r")) != 0LL ) { LODWORD(v10) = 0; v23 = &unk_3FAC280; v11 = d2i_PUBKEY(0LL, &v23, 294LL); v12 = v11; if ( v11 ) { v13 = EVP_PKEY_get1_RSA(v11); v14 = v13; if ( v13 ) { v22 = &v19; n = RSA_size(v13); v15 = alloca(n); v21 = &v19; v16 = alloca(RSA_size(v14)); v17 = fread(v21, 1uLL, n, v9); if ( RSA_public_decrypt(v17, v21, &v19, v14, 1LL) == v8 ) v10 = memcmp(ptr, &v19, v8) == 0; RSA_free(v14); } EVP_PKEY_free(v12); } fclose(v9); } else { LODWORD(v10) = 0; } return v10; } |
函数开头通过异或运算处理了一个字符串,解密之后得到结果 /.fgtsum

在根目录能找到这个文件,那么显然此函数就是利用该文件实现了某些系统校验。
第三个函数(sub_2745D50)似乎和 FIPS 模式相关,FIPS 简单来说是美国政府针对计算机系统定义的一种标准化信息处理方式,旨在提升信息的安全性。第四个函数(sub_44F1F0)同样 fork 出了子进程,相关逻辑
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
_BOOL8 __fastcall sub_2780BD0(unsigned int a1) { __int64 v1; // rax __int64 v2; // r12 _BOOL8 result; // rax 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_35CFDC0, sizeof(v5)); v6 = 256; v7 = 0; v4 = v5; v1 = d2i_RSAPublicKey(0LL, &v4, 270LL); if ( v1 && (v2 = v1, !sub_2746F20("/data/rootfs.gz", "/data/rootfs.gz.chk", a1, v1)) ) result = sub_2746F20("/data/flatkc", "/data/flatkc.chk", a1, v2) == 0; else result = 0LL; return result; } |
此函数实现对 rootfs.gz 的判断。
到这里可以大体推断文件系统的校验是在用户态程序 /bin/init 中实现的。相关逻辑并不复杂,我们可以深入研究以便于打包一份合适的 rootfs.gz 通过校验,或者采用更简单的方法即 patch 掉校验逻辑。
经过验证,只需要 patch fgtsum 和 rootfs 两个检验即可,将对应跳转指令取反,保存并替换原始文件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
if ( sub_44F240() ) do_halt(); if ( sub_44F1A0(1LL, "%s()-%d: %s: run_initlevel(SYSINIT)\n\n") ) do_halt(); if ( sub_2745D50() ) { sub_2825E00(); if ( sub_44DFC0("/bin/fips_self_test") ) do_halt(); } else { if ( !sub_44F1F0(1LL, "%s()-%d: %s: run_initlevel(SYSINIT)\n\n") ) do_halt(); sub_2781560(); } |
植入后门和启动
后续的操作和参考文章提到的类似,先用 msf 生成一个后门程序,替换 /bin/smartctl 文件,然后将 patch 的 init 程序替换 /bin/init,同时在系统中放置一个 busybox 并将 /bin/sh 指向 busybox 方便后续使用。
根据前面的文章我们知道系统启动时会首先执行 /sbin/init,这个程序的逻辑很简单:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
__int64 __fastcall main(__int64 a1, char **a2, char **a3) { char *argv[4]; // [rsp+0h] [rbp-20h] BYREF argv[3] = (char *)__readfsqword(0x28u); sub_4017D0(a1, a2, a3); unlink("/sbin/init.chk"); if ( (int)sub_401AD0("bin") >= 0 && (int)sub_401AD0("migadmin") >= 0 && (int)sub_401AD0("node-scripts") >= 0 ) sub_401AD0("usr"); argv[0] = "/bin/init"; argv[1] = 0LL; execve("/bin/init", argv, 0LL); return 0LL; } |
它负责解压各个压缩包并唤起 /bin/init 继续执行,没有其他操作。方便起见我们跳过重新打包 xxx.tar.xz 的步骤,在系统启动过程中调试内核并修改内核参数,让它直接去执行 /bin/init 程序。
打包命令
1 2 |
find . | cpio -H newc -o > ../rootfs.raw cat ./rootfs.raw | gzip > rootfs.gz |
将新的 rootfs.gz 替换到磁盘中,挂载到原先系统上,并添加调试桥。
启动时用 gdb 附加,并定位到执行用户空间程序位置:

此时执行的目标程序为 /sbin/init

我们将其修改成 /bin/init,然后继续执行,系统将正常启动,执行命令 diagnose hardware smartctl
即可触发后门程序。由于设备存在防火墙,端口不能随便访问,我们将其自带的 sshd 服务 kill 掉并替换成 telnetd 后门
1
|
killall sshd && busybox telnetd -l /bin/sh -b 0.0.0.0 -p 22
|
这样就可以获取一个完整的 root 权限:

由于有些朋友对操作还存在一定的疑问,我们总结一下具体的操作步骤为:
1 2 3 4 5 6 7 8 |
1. 解包原始的 rootfs.gz 2. 解包全部 xxx.tar.xz,同时保留原始的 xxx.tar.xz 3. patch /bin/init 中的校验逻辑,以及植入后门程序 4. 将文件系统重新打包成 rootfs.gz 替换到虚拟磁盘 5. 为虚拟机添加调试桥 (请参考 https://wzt.ac.cn/2021/05/12/vm_shell/) 6. 启动系统时附加 gdb,在执行 /sbin/init 之前断下,将 /sbin/init 字符串修改为 /bin/init 7. 继续运行系统,进入 CLI 执行命令 diagnose hardware smartctl 8. 在接收端得到 shell |
本文介绍了作者在研究 FortiOS 过程中使用的一种简单获取权限的方法,没有深入涉及 FortiOS 的文件系统检查逻辑,权当抛砖引玉,希望能对有需要的朋友有一些帮助。如果您有更简单的方法,也请分享给安全社区交流学习。