Case-insensitive
Authors: Spittfire, Aylmao, dd
Tags: misc, crypto
Points: 305 (8 solves)
I implemented bcrypt-based signing. Can you expose the key?
nc case-insensitive.quals.seccon.jp 8080
Introduction
Last weekend we played SECCON and ended up 2nd overall. It was very fun ! We will present how we solved case-insensitive, a challenge made by kurenaif. This challenge was the least solved misc challenge with only 8 solves.
Challenge structure
We are provided with a single python file named problem.py
. It contains the code that is run remotely. The code simply hashes a provided message appended to the flag using bcrypt and returns the resulting hash. There is also a functionality to verify that a provided hash corresponds to the hash of a provided message appended to the flag. We rapidly concluded that bruteforcing the hash made out of a single message + flag would be impossible as the flag length could easily be more then 32 bytes and that the hashing algorithm used was bcrypt with 5 round salts.
Bcrypt Library code analysis
By inspecting the bcrypt library source code of the used functions we notices that the function hashpw
only hashed the first 72 bytes of the provided password which is our message appended to the flag.
password = password[:72]
This is looks promising as we can use this in our advantage. By providing a long enough message we can compute a hash containing the message we provide appended to only the first few bytes of the hash. In this way we can bruteforce it.
For example, to leak the first byte of the flag we can provide a message containing 71 bytes. Then, the flag would be appended to the end of the message and the hashpw
would get called. We know that only the 72 first bytes are taken which would mean that the resulting hash can be bruteforced by simply computing the hash of every single possible printable character append to our provided message.
Length check bypass
The above presented idea has only one problem. There is a check that bounds the message size to 24 characters. From a challenge of b01lers CTF 2021 , we knew that it was possible to mess with the length of a string by using ligatures in python. By trying out with the ligature fl
. We noticed that we were able to provide a message having length 24 but that would in the end be made of 48 bytes. We then found a ligature made of 3 characters : ffl
to reach 72 bytes with a message of 24 characters. This works because the call to upper messes up the actual length of the message. This is an expected behaviour according to the unicode conventions. Calling upper()
on fl
is actually well defined. In the unicode specification we can see that :
FB02; FB02; 0046 006C; 0046 004C
shows that the character with code FB02
is represented in lower as FB02
and as 0046 006C
in upper case.
Solution script
Using the gathered knowledge we started to write a script that would leak 1 byte of the flag at a time and then find the corresponding character by bruteforcing it over the set of all printable characters.
Here is our solution script :
# Imports
from pwn import * # To interact with the server
import bcrypt
from tqdm import tqdm
import string # To bruteforce on
char_3 = "ffi"
char_2 = "fl"
def make_to_length(l):
nb_of_3 = int(l/3)
nb_of_2 = int((l-nb_of_3*3)/2)
remaining = l - (3*nb_of_3 + 2*nb_of_2)
return char_3*nb_of_3 + char_2*nb_of_2 + remaining*"A"
# Phase 1 : Getting all the hashes
#remote = process('./problem.py')
remote = remote('case-insensitive.quals.seccon.jp',8080)
def sign(conn, msg):
conn.sendline(b"1")
d = conn.recvuntil(b'message: ')
print(d)
conn.sendline(msg.encode())
raw = conn.recvline()
return raw.split(b": ")[1]
# Hashing all the combinations
results = {}
salts = {}
for i in tqdm(range(48, 72)):
results[i] = sign(remote, make_to_length(i)).strip()
salts[i] = results[i][0:29]
flag = ""
for i in range(48, 72)[::-1]:
print("bruteforcing : ", i)
s = salts[i]
r = results[i]
found = None
for c in string.printable:
leading = make_to_length(i).upper()
payload = (leading + flag + c).encode()
attempt = bcrypt.hashpw(payload, s)
if r == attempt:
print("FOUND !", c)
found = c
break
flag += found
if "}" in flag:
break
print(flag)
Flag: SECCON{uPPEr_is_M4g1c}
Conclusion
It was a really nice challenge to remember us how unsafe len()
can be in python ^^.