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.
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:
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.
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
$rip . The payload was generated with pwntool’s cyclic generator, so we can find the offset of
Awesome! We now have offsets to work with. Note that the offset for
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
$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.
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
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.