1、下载飞塔 FortiGate 7.4.1 的 VM 虚拟机镜像

第一步、下载完解压放到 linux 虚拟机里
把 vmdk 这种 "VMware 专有包装格式" 变成 Linux 标准工具能直接读的 "裸盘镜像"
执行命令:qemu-img convert -f vmdk -O raw fortios.vmdk fortios.raw

第二步、查看看分区表
执行命令:fdisk -l fortios.raw

第三步、抠出分区 fortios.raw1 ,起始扇区 2048、长度 524288 扇区正好是 256MB
执行命令:dd if=fortios.raw of=fortios_p1.ext2 bs=512 skip=2048 count=524288

第四步、从分区 fortios.raw1 里 dump 内核和加密的 rootfs
执行命令:
mkdir FORTIOS741
debugfs -R "dump /flatkc FORTIOS741/flatkc" fortios_p1.ext2
debugfs -R "dump /rootfs.gz FORTIOS741/rootfs.gz" fortios_p1.ext2
debugfs -R "dump /flatkc.chk FORTIOS741/flatkc.chk" fortios_p1.ext2
debugfs -R "dump /rootfs.gz.chk FORTIOS741/rootfs.gz.chk" fortios_p1.ext2
第五步、tail 抠出 gzip 流再解压
flatkc 是 bzImage(MZ 引导头 + setup 段 + 内层 gzip 流)
gzip 流不在文件开头,而在偏移 0x41b4
然后 kernel_741.gz 开头才是 1f 8b 方能解压了
把 seed 从内核 ELF 里抠出来(48 字节)
执行命令:
tail -c +$((0x41b4 + 1)) FORTIOS741/flatkc > kernel_741.gz
gzip -d -c kernel_741.gz > kernel_741.elf
dd if=kernel_741.elf of=seed.bin bs=1 skip=$((0x19952e0)) count=48
xxd seed.bin

第六步、/dev/zero 是内核提供的一个 "特殊设备文件"
从它里面读数据,它永远返回 0x00 字节,要多少给多少
再用 chacha_dec.py 去解密生成两个文件 rootfs_741.decrypted(重要)、pub_key_741.der
执行命令:
dd if=/dev/zero of=enc_pub_key.bin bs=1 count=270
python3 chacha_dec.py

第七步、gzip 解出 cpio 归档
执行命令:
gzip -d -c rootfs_741.decrypted > rootfs_741.cpio
xxd rootfs_741.cpio | head -1

第八步、cpio 解包
执行命令:
mkdir rootfs741
cd rootfs741
sudo cpio -idmv < ../rootfs_741.cpio

第九步、解开里面的 tar.xz
严格说普通用户也能解,sudo su 主要是为了,保持文件属主是 root(和固件里一致)
普通用户(iotsec-zone)解包时,
没法把属主设回 root,解出来的文件就变成自己的:
-rwxr-xr-x 1 iotsec-zone iotsec-zone 12345 ... ← 属主变了
root 有权限执行 chown,
所以 tar 能把属主恢复成固件里记录的样子(root : root):
-rwxr-xr-x 1 root root 12345 ... ← 和固件一致
执行命令:
xz -d bin.tar.xz && tar -xvf bin.tar && rm -f bin.tar
xz -d usr.tar.xz && tar -xvf usr.tar && rm -f usr.tar
xz -d node-scripts.tar.xz && tar -xvf node-scripts.tar && rm -f node-scripts.tar
xz -d migadmin.tar.xz && tar -xvf migadmin.tar && rm -f migadmin.tar


打开 VM 界面点击文件 → 打开

接着用把 FortiGate-VM64.hw17.ovf 启动

点击同意,再填入虚拟机名称和存放文件路径,点击导入即可

配置不需要做任何更改

初次登录设置密码

查看分配了什么 IP 手动输入命令:diagnose ip address list
这里可以看到分配了 192.168.2.32

网页输入 192.168.2.32 再输入 admin admin 登录

点击上传(英文网页翻译成中文)

选择 license.lic 文件后,再点击 OK

接着点击确认再系统重启一次(虚拟机会自动重启)

重启名称会变(无关要紧)

然后重新登录就能进入管理页面

补充:有时候启动会报磁盘错误
手动输入命令:
execute disk list(查看 ref: 16)
execute disk scan 16(执行完会重启)

2、格式化字符串漏洞
在 bin/init 程序中的 sub_2B1D9E0() 里面有一个格式化字符串漏洞


这里直接把 v12 填到 snprintf() 把 v12 当数据传
这里漏了 "%s",导致 v12 里的 % 全被当作格式指令解析
正确写法应该是 snprintf(&s[17], 0xC9, "%s", v12)

交叉引用能看到有 4 处地方引用了 sub_2B1D9E0()

先从倒数第一 sub_27E2CC0() 函数看起,这里有一大串字符都在提示 USB 磁盘
这里实际是需要通过管理员从 USB 拷贝才能触发,所以这里触发漏洞点不可靠

倒数第二个函数 sub_27E2B70() 函数需要攻击者先架好恶意 FTP 服务器
攻击者不能直接传内容,
只能让管理员在设备上敲一条 CLI 命令从指定的 FTP 服务器下载到 /tmp/ftkseed 才行

顺数第二 sub_27E2A80() 函数同倒数第二 sub_27E2B70() 函数原理一样
只不过这个开的是 TFTP 服务器

顺数第一 sub_E7A470() 函数才是用户可控的点,再交叉引用找到一段提示词



在 .data:00000000047E2190 有 "user" 、"fortitoken" 、"import-seed" 关键词
用 burp 可以看到有这样的接口,那么就可以知道该 sub_E7A470() 函数的触发接口是
/api/v2/monitor/user/fortitoken/import-seed

接下来就简单讲解一下,如何触发这里漏洞 sub_2B1D9E0(char *filename) 函数
当通过接口上传一个恶意文件到这里,filename 会被打开给 v1 赋值文件指针

接着 v1 文件指针会按照末尾 '\n' 切割出一行内容再赋值 lineptr 指针

这里会把 lineptr 内容按 ' , '(逗号)分别切割赋值给
format(serial 指纹)、v11(密文)、v8(IV)

因为上传的内容是密文,需要对 v11 进行 AES-256-CBC 解密
v12 将会得到解密后的明文

v10 是 64 字节 key


然后这里恶意构造文件内容注入就能通过 %hhn 对内存进行改写

这里再提前了解一下飞塔 FortiGate 的登录获取 Cookie 和 token 两道防线:
① APSCOOKIE 管身份认证,APSCOOKIE → httpsd 校验通过,放行进 API
② ccsrftoken 管CSRF 防护码

CSRF 全称 Cross-Site Request Forgery(跨站请求伪造)
ccsrftoken_10443_2853b1b6 和请求头 X-Csrftoken 做双提交校验
X-Csrftoken == ccsrftoken ,则表示 CSRF 校验通过,允许执行导入
对于写入路由器操作必须带 X-Csrftoken:
第 1 条、后端强制要
POST / PUT / DELETE(会改东西的)→ 必须带,不带就 403
GET(只读)→ 不强制,带不带都行
第 2 条、前端带不带(发不发)
页面 JS 发的 XHR 请求
(有 X-Requested-With: XMLHttpRequest) → 统一会带(前端拦截器顺手加的)
浏览器自己发的资源请求如:
(图片、JS、service-worker,Sec-Fetch-Dest 不是空/cors) → 不带

这里 exp.py 脚本的 IP 地址要对应上飞塔 VM 虚拟机分配的 IP 地址

python exp.py --file /tmp/IOTsec --content 123456789 --sleep 20
用途:
--file 往指定目录下写入文件
--content 往文件里面写入指定的内容
--sleep 等待时间可写可不写

在 VM 模拟的飞塔终端里面输入 fnsysctl ls -l /tmp/IOTsec 的确看到有 9 字节文件
但是输入 fnsysctl cat /tmp/IOTsec 却没有权限查看 "123456789" 内容
也算是成功利用格式化字符串提权对内部做某些操作

补充内容:
① 这里逻辑 httpsd 的 GOT 表被劫持 strchr() 全局指针 被替换成了 setcontext()
当去执行 setcontext() ,它就会由 /bin/httpsd 通过 execve() 替换成 /bin/node 执行
execve() 也就是 "当前这个进程" 的代码和数据整个替换成 node 且 PID 不变
当 /bin/node 跑完后 JS ,那么 PID 消失父进程知道 /bin/httpsd 已经挂掉了
父进程就会立马 fork 一个新的 PID 将 /bin/httpsd 重新拉起
② 这个 fnsysctl 命令是由 /bin/sysctl 程序处理

这里对于一些常用 linux 命令进行重写,还有权限限制不是都能使用的


