Access=0000

This 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.

Here is a depiction of CBC mode encryption.

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