This weekend I played in KashiCTF 2025 organized by the IIT BHU Cybersec. I played mostly the crypto challenges and had time to solve all but one. Today I’ll write-up the crypto/Random-Inator
and crypto/MMDLX
challenges as it had only 37 and 52 solves respectfully.
Random-Inator - crypto - 476 points
This challenge comes with 1 file called server.py
and the clue which reads:
Dr. Heinz Doofenshmirtz plans to take over the Tri-State
Area. He created this super secret uncrackable encryption
program with the help of his robot buttler Norm. Help
Perry the Platypus decrypt the message and sabotage
his evil plans.
The server.py
code contains the following:
from redacted import PRNG, FLAG
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
def encrypt(key, plaintext, iv):
cipher = AES.new(key, AES.MODE_CBC, iv)
ciphertext = cipher.encrypt(pad(plaintext, 16))
return iv+ciphertext
P = PRNG()
KEY = P.getBytes(16)
IV = P.getBytes(16)
print(f"Doofenshmirtz Evil Incorporated!!!\n")
print(f"All right, I don't like to repeat myself here but it just happens\nAnyhow, here's the encrypted message: {encrypt(KEY, FLAG, IV).hex()}\nOhh, How I love EVIL")
while True:
iv = P.getBytes(16)
try:
pt = input("\nPlaintext >> ")
pt = bytes.fromhex(pt)
except KeyboardInterrupt:
break
except:
print("Invalid input")
continue
ct = encrypt(KEY, pt, iv)
print(ct.hex())
When connecting to the server we get :
Doofenshmirtz Evil Incorporated!!!
All right, I don't like to repeat myself here but it just happens
Anyhow, here's the encrypted message: 5d03b39ad3d949e73d297734a9f84b726a31095f4ac6a8c50e29bed49c66bb41e71b0a7551423093b5bf70f439ca53e1f048a513070cef6d234a9612bee0ae64a716966b736742af8de6f91d1d20c3840a04dc35037d656a2e41cf4e8cf68aec
Ohh, How I love EVIL
Plaintext >>
The first thing that stands out to me in the server.py
code is the PRNG()
call suggesting, combined with the name of the challenge, that there is some weakness there.
The first thing I’d like to do is get some sample plaintexts -> ciphertext encryptions. The thing here is that we can see that the key
and iv
are chosen using the same method. Each ciphertext will contain 1 sample iv
so we can evaluate how random the key
may be
I wrote this python code to collect 100 IVs then count the unique ones:
from pwn import *
pt = b"KashiCTFKashiCTF".hex().encode()
p = remote("kashictf.iitbhucybersec.in", 64583)
p.recvuntil(b'Anyhow, here\'s the encrypted message: ')
ct = p.recvline().decode()
iv = ct[:32]
ct = ct[32:]
log.info(f"got iv: {iv} ct: {ct}")
# Sample the PRNG
samples = []
ivs = []
for i in range(100):
p.sendlineafter(b"Plaintext >> ", pt)
res = p.recvline().decode()
riv = res[:32]
rct = res[32:]
log.info(f"round {i} got iv {riv}")
samples.append([riv, rct])
if riv not in ivs:
ivs.append(riv)
p.close()
log.info(f"got total {len(ivs)} unique IVs from {len(samples)} samples.")
When I run it I see there are only 10 unique IVs in the sample of 100 ciphertexts. This is not very random at all!
$ ./solve.py
[+] Opening connection to kashictf.iitbhucybersec.in on port 64583: Done
[*] got iv: a221a2e491207cf23f755e9cbc052de8 ct: fe023f47926115370e692bbfc7500c7725a336a6e96c438555d3adea2a465b64f03f405ec0e0c24f0e8832377e9d66998c384195e5208f6336e8763f92cf1bb6ec3f06ade5c411ef814cea7940399ddb
[*] round 0 got iv f6a9d2852e5c9c5e249188bd32776866
[*] round 1 got iv 5b757cc3fe4d1d51ece7f972421ca89d
[*] round 2 got iv da4f91417cc40c722bea594809044f3d
[*] round 3 got iv da4f91417cc40c722bea594809044f3d
[*] round 4 got iv 7aba152e75735bc86567d804294acc82
[*] round 5 got iv a221a2e491207cf23f755e9cbc052de8
[*] round 6 got iv 6be0323ac7b2f98edc19907467950a36
[*] round 7 got iv 674d8fb80bce301e336476b1dd4ef695
[*] round 8 got iv b375293656a8b9f32b6789eff95003d1
[*] round 9 got iv 5d03b39ad3d949e73d297734a9f84b72
...
[*] round 90 got iv 6be0323ac7b2f98edc19907467950a36
[*] round 91 got iv 674d8fb80bce301e336476b1dd4ef695
[*] round 92 got iv b375293656a8b9f32b6789eff95003d1
[*] round 93 got iv 5d03b39ad3d949e73d297734a9f84b72
[*] round 94 got iv a221a2e491207cf23f755e9cbc052de8
[*] round 95 got iv b375293656a8b9f32b6789eff95003d1
[*] round 96 got iv 5b757cc3fe4d1d51ece7f972421ca89d
[*] round 97 got iv 963483730151eb82840db14f17159c66
[*] round 98 got iv 963483730151eb82840db14f17159c66
[*] round 99 got iv da4f91417cc40c722bea594809044f3d
[*] Closed connection to kashictf.iitbhucybersec.in port 64583
[*] got total 10 unique IVs from 100 samples.
The chances that 1 of these collected IVs is the same as the key
used to encrypt the flag is high. I modify my script to perform brute force decryption against the initial ciphertext:
from pwn import *
from Crypto.Cipher import AES
def decrypt(key, ciphertext, iv):
cipher = AES.new(key, AES.MODE_CBC, iv)
pt = cipher.decrypt(ciphertext)
return pt
pt = b"4b617368694354464b61736869435446"
p = remote("kashictf.iitbhucybersec.in", 64583)
p.recvuntil(b'Anyhow, here\'s the encrypted message: ')
ct = p.recvline().decode()
iv = ct[:32]
ct = ct[32:]
log.info(f"got iv: {iv} ct: {ct}")
# Sample the PRNG
samples = []
ivs = []
for i in range(100):
p.sendlineafter(b"Plaintext >> ", pt)
res = p.recvline().decode()
riv = res[:32]
rct = res[32:]
log.info(f"round {i} got iv {riv}")
samples.append([riv, rct])
if riv not in ivs:
ivs.append(riv)
p.close()
log.info(f"got total {len(ivs)} from {len(samples)}")
a = unhex(iv)
c = unhex(ct)
for key in ivs:
b = unhex(key)
res = decrypt(b,c,a)
if b"KashiCTF" in res:
log.success(f"flag: {res}")
And when I run it I get the flag on the first attempt:
[+] Opening connection to kashictf.iitbhucybersec.in on port 64583: Done
[*] got iv: a221a2e491207cf23f755e9cbc052de8 ct: fe023f47926115370e692bbfc7500c7725a336a6e96c438555d3adea2a465b64f03f405ec0e0c24f0e8832377e9d66998c384195e5208f6336e8763f92cf1bb6ec3f06ade5c411ef814cea7940399ddb
[*] round 0 got iv f6a9d2852e5c9c5e249188bd32776866
[*] round 1 got iv 5b757cc3fe4d1d51ece7f972421ca89d
[*] round 2 got iv da4f91417cc40c722bea594809044f3d
[*] round 3 got iv da4f91417cc40c722bea594809044f3d
[*] round 4 got iv 7aba152e75735bc86567d804294acc82
[*] round 5 got iv a221a2e491207cf23f755e9cbc052de8
[*] round 6 got iv 6be0323ac7b2f98edc19907467950a36
[*] round 7 got iv 674d8fb80bce301e336476b1dd4ef695
[*] round 8 got iv b375293656a8b9f32b6789eff95003d1
[*] round 9 got iv 5d03b39ad3d949e73d297734a9f84b72
[*] round 10 got iv a221a2e491207cf23f755e9cbc052de8
...
[*] round 96 got iv 5b757cc3fe4d1d51ece7f972421ca89d
[*] round 97 got iv 963483730151eb82840db14f17159c66
[*] round 98 got iv 963483730151eb82840db14f17159c66
[*] round 99 got iv da4f91417cc40c722bea594809044f3d
[*] Closed connection to kashictf.iitbhucybersec.in port 64583
[*] got total 10 from 100
[+] flag: b'KashiCTF{Y0u_brOK3_mY_R4Nd0m_In4t0r_Curse_yOu_Perry_tH3_Pl4TYpus_yYm9vQtg}\x06\x06\x06\x06\x06\x06'
MMDLX - crypto - 454 points
This challenge comes with one file MMDLX.txt and the clue:
Although I know only a fraction of their history, but I think
Romans have done many weird things in life. But this is a
very basic challenge, right?
The file is a very long file containing what looks like Base64 encoded data:
Sj0ta2NvRUiSTDuTS0a4S1VtWAOTJSi3ThOPS01TyAKUx1GQSgXuS2GBQieeJRmRSjmYbCVvPhSRyDelQSStSS
WqzBGiOii5R2qTSTGExD9RSiW3SiWxaDKCPjuPyDt1SQG0S1WUPheeOwiSSjuxJ1WpTjChO05DR214R2GEawCT
SBltSgCxTCKoxDePbjuTSj14VR0uTkKUyRWoRgX1O1RvJQOSJhmFWEmDS1WCy3aTxhWeS0WLzjCExCKiyUeUSj
0uKCiSJEeUyh5VViSxziSnNQCPJSS5QSOPSh1ozBixPEYESgCxOjFwWCaex1mlSgYxQ2KqOheeOh5pVielTCWq
JEeLOjuTQRelTDGoKSiWyCWeV2uTzSCROiKKSiV1SCWPR1WoJUGgOTueR0eZPCWnOjCPyRi6ThWhxDBuzD9Txh
GeSAGLaCGoxDePxwSwTTuly1aDTkOKPDeMRj14S1OSxD9UO0mvQiWpTjGDTjeWJkeUV1WDSSGpQh5TyChuShWx
...
Except decoding the file gives us junk:
The name and clue gives us the next step. Crypto basic challenge with a Roman flavour suggests “caesar” cipher so indeed if you rotate the input with a key of 3 then you see it now decodes to more Base64:
Base64 decoding this again gives us…. More base64 encoded data. In fact you need to base64 decode it 40 times before you get anywhere. The following Recipe on Cyberchef is the solution:
ROT13(true,true,false,3)
From_Base64('A-Za-z0-9+/=',true,false)
From_Base64('A-Za-z0-9+/=',true,false)
From_Base64('A-Za-z0-9+/=',true,false)
From_Base64('A-Za-z0-9+/=',true,false)
From_Base64('A-Za-z0-9+/=',true,false)
From_Base64('A-Za-z0-9+/=',true,false)
From_Base64('A-Za-z0-9+/=',true,false)
From_Base64('A-Za-z0-9+/=',true,false)
From_Base64('A-Za-z0-9+/=',true,false)
From_Base64('A-Za-z0-9+/=',true,false)
From_Base64('A-Za-z0-9+/=',true,false)
From_Base64('A-Za-z0-9+/=',true,false)
From_Base64('A-Za-z0-9+/=',true,false)
From_Base64('A-Za-z0-9+/=',true,false)
From_Base64('A-Za-z0-9+/=',true,false)
From_Base64('A-Za-z0-9+/=',true,false)
From_Base64('A-Za-z0-9+/=',true,false)
From_Base64('A-Za-z0-9+/=',true,false)
From_Base64('A-Za-z0-9+/=',true,false)
From_Base64('A-Za-z0-9+/=',true,false)
From_Base64('A-Za-z0-9+/=',true,false)
From_Base64('A-Za-z0-9+/=',true,false)
From_Base64('A-Za-z0-9+/=',true,false)
From_Base64('A-Za-z0-9+/=',true,false)
From_Base64('A-Za-z0-9+/=',true,false)
From_Base64('A-Za-z0-9+/=',true,false)
From_Base64('A-Za-z0-9+/=',true,false)
From_Base64('A-Za-z0-9+/=',true,false)
From_Base64('A-Za-z0-9+/=',true,false)
From_Base64('A-Za-z0-9+/=',true,false)
From_Base64('A-Za-z0-9+/=',true,false)
From_Base64('A-Za-z0-9+/=',true,false)
From_Base64('A-Za-z0-9+/=',true,false)
From_Base64('A-Za-z0-9+/=',true,false)
From_Base64('A-Za-z0-9+/=',true,false)
From_Base64('A-Za-z0-9+/=',true,false)
From_Base64('A-Za-z0-9+/=',true,false)
From_Base64('A-Za-z0-9+/=',true,false)
From_Base64('A-Za-z0-9+/=',true,false)
From_Base64('A-Za-z0-9+/=',true,false)
The result of all that gives us:
KashiCTF{w31rd_numb3r5_4nd_c1ph3r5}
Basic challenge confounded by many rounds of b64!