Writeup by toby-bro for CocoRiCo

crypto symmetric

April 28, 2025

In this challenge, we see that basically each time we connect to the server and input our name we are given a token, which enables us to login with this username. By looking at the code we see that toto seems to be an admin account. Thus we will try to generate his token.

We see that the token is generated by encrypting the usename and his privileges and their CRC32 checksum with AES in OFB mode. Furthermore the iv is provided to us. So as we can see on this wikipedia image, if we possess one valid token then we only need to XOR it to the new username and checksum to get a new valid token.

For simplicity we took bob as a username so as the json dump of his status had exaclty the same length as the one of toto (True has one letter less than False)

So this is what we did

import json
import socket
from zlib import crc32


def connect_to_server(host: str, port: int) -> socket.socket:
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.connect((host, port))
    return s


def receive_until(s: socket.socket, text: bytes) -> str:
    buffer = b''
    while text not in buffer:
        data = s.recv(1024)
        if not data:
            break
        buffer += data
    return buffer.decode('utf-8')


def forge_admin_token(normal_token_hex: str) -> str:
    normal_token = bytes.fromhex(normal_token_hex)

    normal_plaintext = json.dumps({'name': 'bob', 'admin': False}).encode()
    normal_crc = crc32(normal_plaintext).to_bytes(4, byteorder='big')

    target_plaintext = json.dumps({'name': 'toto', 'admin': True}).encode()
    target_crc = crc32(target_plaintext).to_bytes(4, byteorder='big')

    if len(normal_plaintext) != len(target_plaintext):
        print("Lengths don't match, adjust your payload!")
        exit(1)

    keystream = bytes(c ^ p for c, p in zip(normal_token, normal_plaintext + normal_crc, strict=False))

    forged_token = bytes(p ^ k for p, k in zip(target_plaintext + target_crc, keystream, strict=False))

    return forged_token.hex()


def main() -> None:
    HOST = 'chall.fcsc.fr'
    PORT = 2150

    print(f'[+] Connecting to {HOST}:{PORT}')
    s = connect_to_server(HOST, PORT)

    receive_until(s, b'>>> ')

    print('[+] Selecting login option')
    s.send(b'1\n')
    receive_until(s, b'(y/n) ')

    print("[+] Registering as 'bob'")
    s.send(b'y\n')
    receive_until(s, b'Name: ')
    s.send(b'bob\n')

    response = receive_until(s, b'>>> ')
    token_lines = response.split('\n')
    for i, line in enumerate(token_lines):
        if 'Here is your token:' in line and i + 1 < len(token_lines):
            normal_token_hex = token_lines[i + 1].strip()
            print(f'[+] Received token: {normal_token_hex}')
            break

    forged_token = forge_admin_token(normal_token_hex)
    print(f"[+] Forged token for 'toto' with admin=true: {forged_token}")

    s.send(b'1\n')
    receive_until(s, b'(y/n) ')
    s.send(b'n\n')
    receive_until(s, b'Token: ')
    s.send(f'{forged_token}\n'.encode())

    result = receive_until(s, b'>>> ')
    print('\n=== RESULT ===\n' + result)

    s.close()


if __name__ == '__main__':
    main()