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!}