Quantcast
Channel: Hacker News
Viewing all articles
Browse latest Browse all 25817

One Line of Code That Compromises Your Server

$
0
0

A session secret is key used for encrypting cookies. Application developers often set it to a weak key during development, and don't fix it during production. This article explains how such a weak key can be cracked, and how that cracked key can be used to gain control of the server that hosts the application. We can prevent this by using strong keys and careful key management. Library authors should encourage this with tools and documentation.

I was recently taking a quick look at a small Ruby web application built on Sinatra. As I scanned through the configuration code I came across this line:

set :session_secret, 'super secret'

Uh oh. Chances are the string 'super secret' isn't actually that much of a secret.

Even though it is quite obvious that this is a mistake when I pull that line out alone in a post about the importance of secrets, it's an extremely common type of mistake to make. It's easy to do. After all, it's just one line of code among many, and once it was written there was likely little reason to revisit that part of the code again.

What's more, it's a mistake that has no immediate impact for either users or developers. The app still works fine, sessions still hold state, deployments continue without a hitch.

An attacker, however, could likely use this flaw to log in as any user in the system, and even gain shell access to the server it’s running on.

Let’s explore how that is possible, tracing through the steps an attacker could take.

But first, what exactly is this session secret?


What is a session secret?

The session secret is a key used for signing and/or encrypting cookies set by the application to maintain session state.

In practice, this is often what prevents users from pretending to be someone they’re not -- ensuring that random person on the internet cannot access your application as an administrator.

Cookies are the most common way for web applications to persist state (like the currently logged in user) across distinct HTTP requests. To achieve this, web browsers will hang on to pieces of information that a web server wants to remember, dutifully sending it back with each subsequent request to remind the server that, for example, we are still logged in -- and possibly also that we are or are not an admin.

But because these cookies are stored by the web browser (the client), the web server doesn't actually know that the cookies it receives from the client are legitimate. This guarantee is not provided by the cookie spec, which states:

A malicious client could alter the Cookie header before transmission, with unpredictable results

Well that sounds bad. Later on, the spec gives us some advice:

Servers SHOULD encrypt and sign the contents of cookies (using whatever format the server desires) when transmitting them to the user agent

This advice wasn’t exactly followed, and web frameworks are just now starting to encrypt cookies by default. Sinatra (and the lower level framework, Rack) do, however, sign cookies by default. This means that while clients can read the contents of a cookie, they shouldn't be able to change the value in any way.

Many other frameworks offer functionality to do the same thing. For example, Node/Express has a secret parameter, Python/Django has a SECRET_KEY parameter, and Java/Play has acrypto.secret parameter. While they may use slightly different algorithms under the hood, the basic functionality is the same and they are prone to the same attacks I'm about to describe in the context of Ruby/Sinatra.

Looking at the Rack code around cookie management we see:

class Rack::Session::Cookie

  def write_session(req, session_id, session, options)
    session = session.merge("session_id" => session_id)
    session_data = coder.encode(session)
    if @secrets.first      session_data << "--#{generate_hmac(session_data, @secrets.first)}"
    end
    # …

source

  def generate_hmac(data, secret)    OpenSSL::HMAC.hexdigest(@hmac.new, secret, data)
  end

source

  def initialize(app, options={})
    @secrets = options.values_at(:secret, :old_secret).compact    @hmac = options.fetch(:hmac, OpenSSL::Digest::SHA1)
    # …

source

Rack will first encode the session data in some way, then (in its default configuration) use OpenSSL to generate the HMAC-SHA1 of the session secret and the session data, and append that HMAC to the encoded session data separated by '--'.

In math-y terms, the application returns a cookie value of (data, hmac) where hmac = hmac-sha1(secret, data)

By making a request to our application, we can see the result:

$ curl -v http://192.168.50.50:9494/
(...)< Set-Cookie:
rack.session=BAh7CEkiD3Nlc3Npb25faWQGOgZFVEkiRTdhYTliNGY5ZjVmOTE4MjIxYTU5%0AMGM4OGI1YTdjMzA3Y2QxNTYyYmJjZGQwYTEyNjJmOThhNmVlNmQzM2ExMTEG%0AOwBGSSIJY3NyZgY7AEZJIiU2M2ZjZTFkZGIxNTc1ZmU4YzM0Y2YyZjc2M2Vl%0AMGMwYQY7AEZJIg10cmFja2luZwY7AEZ7B0kiFEhUVFBfVVNFUl9BR0VOVAY7%0AAFRJIi1lZjE4YWVkMjg0YWI3NWU3MGEwMWIyMmUzMWI5MGU3YmE0NDcwYzc2%0ABjsARkkiGUhUVFBfQUNDRVBUX0xBTkdVQUdFBjsAVEkiLWRhMzlhM2VlNWU2%0AYjRiMGQzMjU1YmZlZjk1NjAxODkwYWZkODA3MDkGOwBG%0A--b64eac9e0a5fb41a12b58a7ffe97c51b73fbf1a6;
path=/; HttpOnly

So if we know that:

data = BAh...%0A

and:

hmac = b64...1a6

Then in order to tamper with the session data, we need to find a secret where

hmac-sha1(secret, BAh...%0A) = b64...1a6

By design, there is no way to mathematically calculate secret in this equation. In order to find it, we'll just have to keep guessing until we find the right value...


How to crack a weak session secret

So “super secret” isn't cryptographically secure random data... but would an attacker really be able to take advantage of this without access to the source code?

While SHA1 isn't reversible, it is, unfortunately in this case, extremely fast (as a general purpose hash function, it was designed to be). This isn't a problem if the secret is suitably long cryptographically secure random data, but “super secret” definitely isn't. Let's see how long it would take an attacker to guess it.

Instead of making completely random guesses resulting in a brute force attack, we can try our luck at a dictionary attack. The dictionary attack gets it's name from trying every word in a dictionary, but in reality the dictionary is only the start. Taylor Hornby writes this about his CrackStation list:

The list contains every wordlist, dictionary, and password database leak that I could find on the internet (and I spent a LOT of time looking). It also contains every word in the Wikipedia databases (pages-articles, retrieved 2010, all languages) as well as lots of books from Project Gutenberg. It also includes the passwords from some low-profile database breaches that were being sold in the underground years ago.

-- Taylor Hornby

Wow, that sounds like a lot of data. The full CrackStation list contains almost 1.5 billion entries in a single 15 gigabyte file.

SHA1 is fast, but with that much data, let's make sure we're calculating those hashes as fast as possible. Hashcat is a program to do exactly that. Written in highly optimized C, and taking advantage of both CPUs and GPUs, Hashcat will fly through SHA1. The GPU support is key as GPU's can compute hashes much faster than CPU's can. My laptop doesn't have a GPU, but it would be a shame not to take advantage of this support...

At the end of 2013 Amazon launched GPU instances as part of it's EC2 offering. For just $2.60 an hour, we can rent a g2.8xlarge instance with:

  • 4 GPUs
  • 32 vCPUs
  • 60G of memory

With the CrackStation wordlist, Hashcat, and our giant EC2 instance, we have a fairly respectable hashing setup for very little effort and astonishingly little cost.

The dictionary attack

Let's try this out with some sample data:

gen-cookie.rb…

  require 'base64'
  require 'openssl'
  key = 'super secret'
  cookie_data = 'test'
  cookie = Base64.strict_encode64(Marshal.dump(cookie_data)).chomp
  digest = OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('SHA1'), key, cookie)  puts("#{cookie}--#{digest}")
$ ruby gen-cookie.rb 
BAhJIgl0ZXN0BjoGRVQ=--8c5ae09ed57f1e933cc466f5b99ea636d1fc31a2

Hashcat is mainly designed for cracking password hashes, which often include a password and a salt instead of data and key. But as people sometimes use HMAC-SHA1 in password storage schemes, it is supported by the program. Pretending that our session data is a password salt, we convert our cookie value into the "hash:salt" format that Hashcat expects:

$ echo '8c5ae09ed57f1e933cc466f5b99ea636d1fc31a2:BAhJIgl0ZXN0BjoGRVQ=' > hashes 

And then run Hashcat with our new one-line hashes file, the crackstation wordlist, and the '-m150' option, telling it to use HMAC-SHA1 (the full list of supported algorithms can be seen by typing 'hashcat -h'):

$ hashcat -m150 hashes ~/wordlists/crackstation.txt
(...)8c5ae09ed57f1e933cc466f5b99ea636d1fc31a2:BAhJIgl0ZXN0BjoGRVQ=:super secret
Session.Name...: hashcat
Status.........: Cracked
Input.Mode.....: File (/home/ec2-user/wordlists/crackstation.txt)
Hash.Target....: 8c5ae09ed57f1e933cc466f5b99ea636d1fc31a2:...
Hash.Type......: HMAC-SHA1 (key = $pass)
Time.Started...: Wed Aug 17 21:45:08 2016 (43 secs)
Speed.Dev.#1...: 6019.4 kH/s (12.95ms)
Speed.Dev.#2...: 5714.5 kH/s (13.04ms)
Speed.Dev.#3...: 5626.1 kH/s (13.20ms)
Speed.Dev.#4...: 6096.9 kH/s (13.24ms)
Speed.Dev.#*...: 23456.9 kH/s
Recovered......: 1/1 (100.00%) Digests, 1/1 (100.00%) Salts
Progress.......: 1021407839/1196843344 (85.34%)
Rejected.......: 6826591/1021407839 (0.67%)
Restore.Point..: 1017123528/1196843344 (84.98%)
Started: Wed Aug 17 21:45:08 2016
Stopped: Wed Aug 17 21:46:04 2016

Wow! In just 43 seconds we blasted through over a billion hashes and, 85.34% of the way through the list, correctly guessed 'super secret'.

Caveats

There is unfortunately (or fortunately?) a caveat with using Hashcat in this way: as it's really designed for use with passwords, and password salts tend to be quite short, it doesn't accept "salts" longer than 55 characters, which rack session data will usually surpass.

However, this doesn't mean though that other programs, or even custom software, won't be able to handle longer payloads.

Impact

This experiment clearly shows that dictionary attacks against Rack session secrets are well within the realm of possibility. A session secret that is not sufficiently cryptographically random can be guessed with fairly little time, effort and resources.

This attack is not limited to Rack secrets, and many web frameworks require a session secret in their default configuration in order to operate securely. These all work very similarly to our :session_secret, and can also be guessed in a similar ways.

Next, let’s explore the harm an attacker could cause after guessing this secret.

Getting hold a session secret doesn't do much damage on its own. The problem lies in what I can do with it once I have it. In my next installment I'll show you how I can use it to take over the application on the way to compromising the server itself.

To find out when we publish the next installment, follow the site's RSS feed,my twitter feed, or Martin's twitter feed



Viewing all articles
Browse latest Browse all 25817

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>