Classic nonce reuse vulnerability in ChaCha20. The source code shows both the known message and the flag are encrypted with the same key and nonce.
$ cat source.py
from Crypto.Cipher import ChaCha20
from secret import FLAG
import os
def encryptMessage(message, key, nonce):
cipher = ChaCha20.new(key=key, nonce=iv)
ciphertext = cipher.encrypt(message)
return ciphertext
def writeData(data):
with open("out.txt", "w") as f:
f.write(data)
if __name__ == "__main__":
message = b"Our counter agencies have intercepted your messages and a lot "
message += b"of your agent's identities have been exposed. In a matter of "
message += b"days all of them will be captured"
key, iv = os.urandom(32), os.urandom(12)
encrypted_message = encryptMessage(message, key, iv)
encrypted_flag = encryptMessage(FLAG, key, iv)
data = iv.hex() + "\n" + encrypted_message.hex() + "\n" + encrypted_flag.hex()
writeData(data)
$ cat out.txt
c4a66edfe80227b4fa24d431
7aa34395a258f5893e3db1822139b8c1f04cfab9d757b9b9cca57e1df33d093f07c7f06e06bb6293676f9060a838ea138b6bc9f20b08afeb73120506e2ce7b9b9dcd9e4a421584cfaba2481132dfbdf4216e98e3facec9ba199ca3a97641e9ca9782868d0222a1d7c0d3119b867edaf2e72e2a6f7d344df39a14edc39cb6f960944ddac2aaef324827c36cba67dcb76b22119b43881a3f1262752990
7d8273ceb459e4d4386df4e32e1aecc1aa7aaafda50cb982f6c62623cf6b29693d86b15457aa76ac7e2eef6cf814ae3a8d39c7%
Stream ciphers like ChaCha20 work by XORing the plaintext with a keystream. If you reuse the same key + nonce, you get the same keystream. This means:
ciphertext1 = plaintext1 ⊕ keystream
ciphertext2 = plaintext2 ⊕ keystream
If we know plaintext1, we can recover the keystream:
keystream = ciphertext1 ⊕ plaintext1
Then use it to decrypt ciphertext2:
plaintext2 = ciphertext2 ⊕ keystream
$ cat solve.py
#!/usr/bin/python3
import binascii
# Known plaintext
known_message = (
b"Our counter agencies have intercepted your messages and a lot "
b"of your agent's identities have been exposed. In a matter of "
b"days all of them will be captured"
)
# Read data from the provided output file
with open("out.txt", "r") as f:
lines = f.read().splitlines()
# Extract nonce, encrypted message, and encrypted flag
nonce = binascii.unhexlify(lines[0])
encrypted_message = binascii.unhexlify(lines[1])
encrypted_flag = binascii.unhexlify(lines[2])
# Derive the keystream from the known plaintext
keystream = bytes(m ^ c for m, c in zip(known_message, encrypted_message))
# Decrypt the flag using the keystream
flag = bytes(c ^ k for c, k in zip(encrypted_flag, keystream))
print(f"FLAG: {flag.decode()}")
$ ./solve.py
FLAG: HTB{und3r57AnD1n9_57R3aM_C1PH3R5_15_51mPl3_a5_7Ha7}
Flag: HTB{und3r57AnD1n9_57R3aM_C1PH3R5_15_51mPl3_a5_7Ha7}