Access=0000
17 Jul 2020This is a writeup for a crypto challenge in RACTF 2020, where we placed 6th.
Challenge Description:
Challenge instance ready at 95.216.233.106:57735
We found a strange service, it looks like you can generate an access token for the network service, but you shouldn't be able to read the flag... We think.
Solving :
We are given access.py. Lets take a look the server file to see what the program does.
From the top, we see that get_flag
:
def get_flag(token, iv):
token = bytes.fromhex(token)
iv = bytes.fromhex(iv)
try:
cipher = AES.new(KEY, AES.MODE_CBC, iv)
decrypted = cipher.decrypt(token)
unpadded = unpad(decrypted, 16)
except ValueError as e:
return {"error": str(e)}
if b"access=0000" in unpadded:
return {"flag": FLAG}
else:
return {"error": "not authorized to read flag"}
1) converts token
and iv
to their representation as “bytes”
2) tries to decrypt token
with the given key
and iv
3) if the string access=0000
is in the message, then it will return the flag
4) if the string access=0000
is not in the message, else print not authorized to read flag
Below that is generate_token
:
def generate_token():
expires_at = (datetime.today() + timedelta(days=1)).strftime("%s")
token = f"access=9999;expiry={expires_at}".encode()
iv = get_random_bytes(16)
padded = pad(token, 16)
cipher = AES.new(KEY, AES.MODE_CBC, iv)
encrypted = cipher.encrypt(padded)
ciphertext = iv.hex() + encrypted.hex()
return {"token": ciphertext}
1) creates an expiration date
2) generates a token string with access=9999
(defined with guest + token expire time)
3) generates a random iv
4) pads token
to the AES block size
5) creates a new AES cipher instance
6) encrypts the padded token
7) generates the cipher text as a result of iv.hex()
and encrypted.hex()
8) returns the ciphertext
As we can see, the goal of this challenge is to retrieve the flag by generating a guest token = access=9999
and forge an admin token = access=0000
. As we can see from generate_token()
, guest tokens are generatated with an AES-CBC mode encryption of access=9999;expiry={expires_at}
. In the AES-CBC mode, the stream is split into 16-byte blocks, with each block encrypted with AES. Then the result is sent to output and XORed with the next block before it gets encrypted.
The ciphertext (the result of the encryption) is = c[i] = AES_Enc(c[i-1] XOR m[i])
with c[0]
being iv
. If we want to decrypt the ciphertext, we can use the following formula = m[i] = AES_Dec(c[i]) XOR c[i-1]
Solve Script :
token = '3913er43j134h6d4fk81df97h4812df315dfd978g62fuehf38173g7eba445g9833kj1371637h26hfa3khuy3j5vx948d0'
token = bytes.fromhex(token)
iv = token[:16]
ct = token[16:]
for i in range(7,11):
iv = iv[:i] + bytes([(iv[i] ^ ord('0') ^ ord('9'))]) + iv[i+1:]
print(iv.hex())
print(ct.hex())
This script replaces iv
with iv XOR D
, so the first block of the plaintext will be decoded as m[0] XOR D
. We can XOR our iv
to change the guest token (access=9999
) to the admin token (access=0000
).
Flag: ractf{cbc_b17_fl1pp1n6_F7W!}