Fortinet semi-recently had a security advisory for a heap-based buffer overflow in SSLVPNd. This blog post will cover the initial steps we’ve taken with setting up an exploitation environment for FortiOS, without debugger access.

 

Static Analysis

 

FortiOS proved to be a simple target when diffing because there is only one main binary, init. Running file on the binary provides some info:

init: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically
linked, interpreter /fortidev/lib64/ld-linux-x86-64.so.2,
BuildID[sha1]=4a163ca34973e1c8e8c49fbcffff3b99b01c9058, for GNU/Linux 3.2.0,
stripped

A pull from IDA’s Lumina server later and some cross-references, alongside reading a blog helps us find the faulting code, referenced in Lumina as fsv_malloc . We use Gepetto  to enhance the code and get it more readable and we find that just like in the blog, in our diff a boundary check has been added.

 

Above is the unpatched fsv_malloc. The patched fsv_malloc checks if  size is greater than the value 0x40000000 and returns if so, just like in the blogpost. We first check cross-references to fsv_malloc to see anything that stands out — there’s a few initial references to sslvpnd but nothing too interesting so we carry on.

However, we can write a fuzzer to quickly validate this:

fuzzer code

The header file – We use jemalloc as IDA specifically signatures the malloc function as one belonging to jemalloc.



The fuzzer – We use libFuzzer due to ease-of-use.

 

Running our fuzzer produces this crash:

This validates the crash (as seen below) even though we are testing on ARM64 with this fuzzer. (Note: the majority of FortiOS targets are ARMv7 or ARM64 w/o PAC)

We’re ready to go for dynamic analysis.

 

Setup

 

First of all, we need to grab a vulnerable image. I grabbed a vulnerable image of version 7.2.2 which can be found by Google. Upon configuring a VM, you’ll notice you’re limited to 1 vCPU and 2 GB of RAM. This isn’t entirely pertinent but is a pain. I used an M1 Mac throughout all this, with both an emulated Debian x64 machine and an emulated FortiOS image, through UTM, passing the -S flag to FortiOS and disabling UEFI boot, and enabling bridged networking for the FortiOS appliance due to how it operates. With this we can reach the WebUI easily and as FortiOS is a slim Linux system (unlike, persay, F5-BIGIP) we can emulate it readily on an M1 Max MacBook Pro.

We also painstakingly setup a headless Debian x64 system through QEMU for debugging. We use a custom gdbinit  for assistance in debugging QEMU.

Setting up SSLVPNd

 

Our first “problem” is setting up SSLVPNd. After registering for a license, we can configure it as so, with split-tunnel disabled. Most of this is detailed in the Chinese blog mentioned above.

We also need to configure an SSLVPN policy. This is configured as so:

Awesome! Now we should be able to run diagnose sys top to get the PID of sslvpnd and debug from there.

So far so good. All we need to do is attach to a process now

Well, that’s not a good sign. Unfortunately, this also crashes QEMU. What to do here?

Thankfully, FortiOS is kind enough to include some debug utilities of its own. After reading some documentation and fiddling with the command line, I’m able to get this output when running my minimal PoC against the target. We generate a PoC payload as so:

`➜ ~ python3 -c ‘from pwn import *; print(cyclic(100000))’ > payload

and the “PoC” is as such:

And from the handy debug logs they provide, we can see ourselves in the stack trace and $rax and $rip . The payload was generated with pwntool’s cyclic generator, so we can find the offset of $rip/$rax

Awesome! We now have offsets to work with. Note that the offset for rax and rip are the same.

But our payload is too big. We need to reduce the size, so we’ll reduce it by setting it to 5000.

“➜ ~ python3 -c ‘from pwn import *; print(cyclic(5000))’ > payload

We rerun our payload.

Register dump:
RAX: 00007fcbb8eee768   RBX: 61736a6161726a61
RCX: 00007fcbb8eeea80   RDX: 00007fcbb72061d0 
R08: 0000000000000050   R09: 0000000000000021
R10: 0000000000002800   R11: 0000000000000063 
R12: 00007fcbb735ac18   R13: 0000000000000000 
R14: 0000000000000001   R15: 0000000100300404 
RSI: 00007fcbbcc81240   RDI: 61736a6161726a61 
RBP: 00007fffa8040480   RSP: 00007fffa8040470 
RIP: 0000000001652cfb   EFLAGS: 0000000000000202

CS: 0033   FS: 0000   GS: 0000
Trap: 000000000000000d   Error: 0000000000000000 
OldMask: 0000000000000000
CR2: 0000000000000000
stack: 0x7fffa80404f8 - 0x7fffa8043350 
Backtrace:
[0x0178551e] => /bin/sslvpnd [0x01786cdc] => /bin/sslvpnd 
[0x01788062] => /bin/sslvpnd [0x00448dcf] => /bin/sslvpnd 
[0x00451eaa] => /bin/sslvpnd [0x0044ea0c] => /bin/sslvpnd 
[0x00451118] => /bin/sslvpnd [0x00451a41] => /bin/sslvpnd
[0x7fcbbc599deb] => /usr/lib/x86_64-linux-gnu/libc.so.6 (      libc_start_main+0x000000eb) liboffset 00023deb 
[0x00443c6a] => /bin/sslvpnd
183: 2023-06-01 08:05:04 <04827> fortidev 6.0.1.0005
184: 2023-06-01 08:05:04 the killed daemon is /bin/sslvpnd: status=0xb 
Crash log interval is 3600 seconds
sslvpnd crashed 1 times. The last crash was at 2023-06-01 08:05:04

A different result! We now control $rbx and $rdi . We can likely make a ROP chain out of this. We can use ROPGadget to dump all the gadgets from the init binary and then put a chain together that way. One oddity with FortiOS is that it uses a Busybox-like structure for shells and such.     sh   is a symlink to a binary called sysctl , so we’ll want to invoke that with our ROP chain. (Or rather, invoke execve in such a way that it calls /bin/sh instead) — we can see this in the SCRT PoC.

 

Takeaways

 

A key ROP gadget for the exploit might look like the following:

Putting together the rest of the ROP chain is trivial assuming $rsp dereferences to the data stored in $rdi .

There’s still obstacles such as ASLR to defeat on FortiOS not running Linux 2.4 (ver. 7.2.2, the latest vulnerable, runs Linux 3.2.6), but SSLVPNd restarts so rapidly that bruteforcing the address may not end up being a problem. DoS is fortunately a non-lethal scenario in this case, though you’d litter the system logs with evidence of exploitation attempts. It’s noisy, but good for adversary emulation.

Given this information, one should easily be able to put together an exploit for SSLVPNd, even without debugging the device directly. This is intended as an exercise left to the reader, as public PoCs (such as SCRT’s) already exist.

In the end, given some sort of debugging mechanism on the device, a debugger may not be necessarily required with enough knowledge of the source code. This is one such example.

 

References:

A More Complete Exploit for Fortinet CVE-2022-42475

Fortinet Adivsory

SCRT Writeup

SCRT PoC

Brandon previously played with MacOS here, he also runs our vulnerability research team.