Here is the server code provided in the prompt:
#!/usr/local/bin/python
from Crypto.Cipher import ChaCha20
from Crypto.Random import get_random_bytes
from secrets import FLAG
key = get_random_bytes(32)
nonce = get_random_bytes(8)
cipher = ChaCha20.new(key=key, nonce=nonce)
print(bytes.hex(cipher.encrypt(b'Slide to the left')))
print(bytes.hex(cipher.encrypt(b'Slide to the right')))
try:
user_in = input().rstrip('\n')
cipher = ChaCha20.new(key=key, nonce=nonce)
decrypted = cipher.decrypt(bytes.fromhex(user_in))
if decrypted == b'Criss cross, criss cross':
print("Cha cha real smooth")
print(FLAG)
else:
print("Those aren't the words!")
except Exception as e:
print("Those aren't the words!")
We notice that the server reuses the same key and nonce for every encryption. To recover the keystream, we simply compute:
ks_block1 = c1 ⊕ b"Slide to the left"
ks_block2 = c2 ⊕ b"Slide to the right"
ks = ks_block1 ∥ ks_block2
Then we can encrypt the target plaintext b"Criss cross, criss cross"
using that keystream. Here is a Python script to perform the exploit (the first two ciphertexts must be entered manually):
c1 = bytes.fromhex("06bd1ca0cad3e290b74cfe07550c7de8f9")
c2 = bytes.fromhex("8cc9674d669d1b4d2f25536efbf60bc61d93")
pt1 = b"Slide to the left"
pt2 = b"Slide to the right"
# Recover keystream
ks = bytearray()
ks.extend(x ^ y for x, y in zip(c1, pt1))
ks.extend(x ^ y for x, y in zip(c2, pt2))
# Encrypt target
target = b"Criss cross, criss cross"
ct_target = bytes(ks[i] ^ target[i] for i in range(len(target)))
print(ct_target.hex())
Using this approach against the challenge server:
$ nc smooth.chal.cyberjousting.com 1350
06bd1ca0cad3e290b74cfe07550c7de8f9
8cc9674d669d1b4d2f25536efbf60bc61d93
16a31cb7dcd3f58df84be54e55036ae7feac856d5b6cce1c
Cha cha real smooth
byuctf{ch4ch4_sl1d3?...n0,ch4ch4_b1tfl1p}