TAMUctf 2020 : 652AHS
Adam the admin spent awhile writing a really nifty Python server to do admin things.
He protected it with state of the art cryptography.
nc challenges.tamuctf.com 7393
Hint: The reviewer for this challenge was successfully able to do this challenge in just 274 requests. Can you do any better?
The challenge was intended to be a timing side-channel attack but due to a bad implementation I was able to make the service crash when I guess an answer correctly, which allowed me to recover all the expected answers.
When connecting to the service, we are given 3 choices :
Welcome admin. Select an option: 1. Enter password 2. Reset password 3. Exit
The first one just asks for a password that we don’t know and the second asks 20 yes/no questions that we must answer correctly in order to change the password :
-------------------------- Password Reset -------------------------- Please answer the following yes/no security questions to prove your identity. Type either "Yes" or "No" for each (without quotation marks). Does pineapple belong on pizza?
The sources are not given.
Where’s the crypto you might wonder ?
Let the guessing begin
The title of the challenge is “652AHS”, which is “SHA256” in reverse order. Does that mean that we will have to reverse SHA256 ? Maybe SHA256 is used to store the password ? It turns out that the title is completely unrelated to the aim of the challenge and is completely useless and misleading.
What about the description ? Protected by “state of the art cryptography”, is that a reference to SHA256 or to the security questions ? Once again, useless. The only interesting info is that it’s a Python server.
What about the hint ? The challenge is solvable in “just 274 requests”. I don’t really see how this number is related to the fact that there are 20 questions. Should we guess the answers ? There are 2^20 possibilities, which is quite small in cryptographie. This would be bruteforceable but the server responds way too slowly for that. Additionally, bruteforce is never the way to go.
At this point, the only thing I could think of was a timing attack. It requires multiple requests to be more accurate but makes no sense in this use case. That’s why I decided that this idea was a dead end and I started to think of ways to gather more information about the logic behind the service.
In the main menu, entering a number above 3 made the service crash. Same when entering a letter instead of a number.
The traceback showed that the function
input is used. The code is written in Python3.
Python3 makes the distinction between
str, when converting a printable string to bytes, the method
.encode() is used.
But this method crashes when trying to convert non-printable characters like
I used this to try to make the service crash on the answer check :
$ echo -e '2\n\xFF' | nc challenges.tamuctf.com 7393 Welcome admin. Select an option: 1. Enter password 2. Reset password 3. Exit -------------------------- Password Reset -------------------------- Please answer the following yes/no security questions to prove your identity. Type either "Yes" or "No" for each (without quotation marks). Does pineapple belong on pizza? Traceback (most recent call last): File "/crypto/server.py", line 92, in <module> run_server() File "/crypto/server.py", line 88, in run_server options[option-1]() # sneak File "/crypto/server.py", line 67, in reset good = good and check(answer, answer_hash) File "/crypto/server.py", line 19, in check good = good and (encrypt(plaintext) == ciphertext) File "/crypto/server.py", line 9, in encrypt return hashlib.sha256(plaintext.encode()).hexdigest() UnicodeEncodeError: 'utf-8' codec can't encode character '\udcff' in position 0: surrogates not allowed
Thanks to the traceback we see that SHA256 is indeed used by the function
encrypt to hash our answer but it’s not the interesting part.
The following line is more interesting :
good = good and (encrypt(plaintext) == ciphertext)
The expression after the
and is evaluated only if good is
I assume that
good == True before the first answer check.
This means that if I answer
Yes to the first answer and
\xFF to the second,
the service will crash only if the first answer was correct.
I can use this weakness to guess the 19 first answers and I just have to guess the last one manually.
I was able to solve the challenge in exactly 20 requests. Which is far less then the 274 mentioned in the hint.
Full exploit script
from pwn import * def dump(reps): conn = remote("challenges.tamuctf.com", 7393) conn.recvlines(5) conn.sendline("2") conn.recvlines(5) for e in reps: conn.sendline(e) conn.recvline() conn.sendline("Yes") conn.recvline() conn.sendline(b'\xFF') data = conn.recvline() answer = b'No' # will only crash on the right answer because the check is: good = good and (encrypt(plaintext) == ciphertext) # if good is already false, the right part is not executed if b'Traceback' in data: answer = b'Yes' conn.close() return answer #resp = [b'No', b'No', b'Yes', b'Yes', b'No', b'Yes', b'No', b'Yes', b'No', b'No', b'Yes', b'No', b'Yes', b'No', b'Yes', b'Yes', b'Yes', b'No', b'Yes', b'Yes'] resp =  for i in range(19): resp.append(dump(resp)) print(resp) #last one guess it. resp.append(b'Yes') conn = remote("challenges.tamuctf.com", 7393) conn.recvlines(5) conn.sendline("2") conn.recvlines(5) for e in resp: conn.sendline(e) conn.recvline() conn.sendline(b'ENOENT') print(conn.recvline()) conn.close()
I don’t think that this way of solving was intended, but it was actually more fun and less guessing than the timing attack. During testing phase, don’t forget to test for edge cases, there were too many ways of making the service crash.