TFCCTF is a CTF event hosted by The Few Chosen, a Hungarian-based CTF team. I participated as an individual team and solved all crypto challenges. The highest-point for crypto challenge was Admin Panel but Harder and Fixed, and here’s the write-up.
Admin Panel but Harder and Fixed
Challenge
This challenge service ran on 01.linux.challenges.ctf.thefewchosen.com 60711
.
import os
import random
from Crypto.Cipher import AES
KEY = os.urandom(16)
PASSWORD = os.urandom(16)
FLAG = os.getenv('FLAG')
menu = """========================
1. Access Flag
2. Change Password
========================"""
def xor(bytes_first, bytes_second):
d = b''
for i in range(len(bytes_second)):
d += bytes([bytes_first[i] ^ bytes_second[i]])
return d
def decrypt(ciphertext):
iv = ciphertext[:16]
ct = ciphertext[16:]
cipher = AES.new(KEY, AES.MODE_ECB)
pt = b''
state = iv
for i in range(len(ct)):
b = cipher.encrypt(state)[0]
c = b ^ ct[i]
pt += bytes([c])
state = state[1:] + bytes([ct[i]])
return pt
if __name__ == "__main__":
while True:
print(menu)
option = int(input("> "))
if option == 1:
password = bytes.fromhex(input("Password > "))
if password == PASSWORD:
print(FLAG)
exit(0)
else:
print("Wrong password!")
continue
elif option == 2:
token = input("Token > ").strip()
if len(token) != 64:
print("Wrong length!")
continue
hex_token = bytes.fromhex(token)
r_bytes = random.randbytes(32)
print(f"XORing with: {r_bytes.hex()}")
xorred = xor(r_bytes, hex_token)
PASSWORD = decrypt(xorred)
Solution
The program offers 2 options:
- Guess the password
- Reset the password using a token
In option 2, the decrypt
function processes the XOR of random.randbytes
and the input. These random bytes come from Python’s built-in module, which uses the Mersenne Twister pseudorandom number generator.
$ python3
Python 3.10.4 (main, Jun 29 2022, 12:14:53) [GCC 11.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import random
>>> help(random)
...
General notes on the underlying Mersenne Twister core generator:
* The period is 2**19937-1.
* It is one of the most extensively tested generators in existence.
* The random() method is implemented in C, executes in a single Python step,
and is, therefore, threadsafe.
Here’s the source code of random.randbytes
from its repository.
## -------------------- bytes methods ---------------------
def randbytes(self, n):
"""Generate n random bytes."""
return self.getrandbits(n * 8).to_bytes(n, 'little')
The random.randbytes
function calls random.getrandbits
, which we can predict using mt19937predictor. We need to collect at least 19937 bits of random state (in this case, 19968 bits) to predict the next random state. We can collect this state using option 2, which prints the random.randbytes
value as r_bytes
in hex.
After predicting the next random state, we can exploit the AES-CFB8 bug. This means the generated password will be one of 256 possible byte values. This AES-CFB8 bug works similarly to the Zerologon vulnerability or CVE-2020-1472.
Implementation
#!/usr/bin/env python3
from mt19937predictor import MT19937Predictor
from pwn import *
io = remote('01.linux.challenges.ctf.thefewchosen.com', 60711)
def access_flag(password):
io.sendlineafter(b'> ', b'1')
io.sendlineafter(b'Password > ', password.encode())
return io.recvline(0).decode()
def change_password(token):
assert len(token) == 64
io.sendlineafter(b'> ', b'2')
io.sendlineafter(b'Token > ', token.encode())
io.recvuntil(b'XORing with: ')
xoring_with = io.recvline(0).decode()
return bytes.fromhex(xoring_with)
predictor = MT19937Predictor()
for i in range(624 // 8):
xw = change_password('0' * 64)
state = int.from_bytes(xw, 'little')
print(f'state {i+1}: {hex(state)}')
predictor.setrandbits(state, 256)
state = predictor.getrandbits(256)
token = state.to_bytes(256 // 8, 'little')
xw = change_password(token.hex())
assert token.hex() == xw.hex()
for i in range(256):
pload = bytes([i]) * 16
flag = access_flag(pload.hex())
if flag.startswith('TFCCTF'):
print(flag)
break
$ python3 solve.py
[+] Opening connection to 01.linux.challenges.ctf.thefewchosen.com on port 60711: Done
state 1: 0x29195298f5c49b2576615d2d32efed14d7a01be2e4127c98226caf051a2f9fbd
state 2: 0xfd6fe17dd278b40868ce89b89669c5ddfe6d331dab6f5a53e334212a3be375d4
state 3: 0x72ca0ac9ead9de9ec56bc1d4c33fb87ca3499642c13ad68ae8a2fad2bc9a0b1b
...
state 76: 0xc48747adf5517da734307d38f09e46cbdcdbb0c6fc18180e494b647a6e628a55
state 77: 0x38b0b0411084870b4b7953fa078ae1d6b9b089443ea2b024ae14e45871db8fa8
state 78: 0xa9bb1664193d964d655904fda332fbc0a0f6e5971c7ac2a18fda1c1297643cb1
TFCCTF{4pp4r3ntly_sp4ces_br34ks_th3_0ld_0ne}
Flag
TFCCTF{4pp4r3ntly_sp4ces_br34ks_th3_0ld_0ne}