Tell me something: # With empty input I receive the same ciphertext 23ace1ee2679fa7c81ccf66a865e98c5e80c9208a206b78776a8dd16f5ea1457f44c8a9882d7f440fc2b4aad213b65436510556e7c2f57422a959ea2b1b0c8ba
Tell me something: a # The ciphertext of a is different from the ciphertext of b cd49af33294afc9b843bb713517f12bd89d92e5b0c9244d5d71d33815a4e8cf7adc474c0f5e77546a43732488e5ce6afdfeca90bd8711ba453f4bedd051902da
Tell me something: b b769ee2ef66a76351a62a2e38dc38f1989d92e5b0c9244d5d71d33815a4e8cf7adc474c0f5e77546a43732488e5ce6afdfeca90bd8711ba453f4bedd051902da
Tell me something: aaa 38ae0e0c0f19f15a55eee646ba888c4e73dd7e606cc6755f22e3e5d9e5b22bede4eca07d36503b4d12f3a2228df1d40063217add6089a5357875c46ff0457291
Tell me something: bbb 282e6c3ac5cf6f1b367b37056332b781921ee57568816ccdf6a1889c736030c7e4eca07d36503b4d12f3a2228df1d40063217add6089a5357875c46ff0457291
Tell me something: aaaaaaaaa # The first block of the enc(aaa) is equal to the first block of enc(aaaaaaaaa) where enc is the encryption 38ae0e0c0f19f15a55eee646ba888c4ed87f273b983ee8b13f1d5f48bcd7197e333191ae17102fdfe97131dd0e9b82bccc2be3d21f1a8fa27c19af26aa77f9f59eaf3341c8216c7e42ae21e7fc665881
Tell me something: bbbbbbbbb # From this block we can deduce that the blocksize of the ciphertext is 16 bytes = 32 hex digits 282e6c3ac5cf6f1b367b37056332b781e034117ceda52b3bf2ed14b57f088803333191ae17102fdfe97131dd0e9b82bccc2be3d21f1a8fa27c19af26aa77f9f59eaf3341c8216c7e42ae21e7fc665881
Tell me something: aaaaaaabbbb # As expected the first block of enc(aaaaaaabbbb) = enc(aaaaaaaxxxx) 38ae0e0c0f19f15a55eee646ba888c4e1bc7b1e266a3ece24d184a2080e66271557f58d5358319774b16842b0fd34189a6dedbe100d30fe9a356f73cfa61439699b14bfc188dd46879f8e53c851e82b6
Tell me something: aaaaaaaxxxx 38ae0e0c0f19f15a55eee646ba888c4e9fbbac9bf0b2a35bd1d46db6e809d964557f58d5358319774b16842b0fd34189a6dedbe100d30fe9a356f73cfa61439699b14bfc188dd46879f8e53c851e82b6
# Let's see if the ciphertext is using ECB as mode of operation Tell me something: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 38ae0e0c0f19f15a55eee646ba888c4eed4605b6ad00c4151179392e6436f97eed4605b6ad00c4151179392e6436f97eed4605b6ad00c4151179392e6436f97eed4605b6ad00c4151179392e6436f97eed4605b6ad00c4151179392e6436f97ef6886e8837caefd9dc554fd9a6cb78d014381d2cdad87676ef62fe39851139e43d51a965425508ed39911610b213351b5ad5764d547113d60305dfbbcf5db01d
From the the output above we see that the server it’s encrypting every plaintext taken in input, so we can craft a Chosen Plaintext Attack (CPA).
“Fuzzing” the server with some requests we can deduce:
It’s using ECB as encryption scheme because there are repetitive blocks from a ciphertext derived from a repetitive plaintext.
It’s add random byte at the start because enc(aaaaaaabbbb)[:First_Block] = enc(aaaaaaaxxxx)[:First_Block].
The blocksize of the PRF/PRP is 16 bytes = 32 hex digits.
The last blocks are always the same, so probably it’s appending the encryption of (flag || pad).
To confirm all this deduction I started writing an exploit :
defcompute_bsize(self): plaintext = b'' # aes-ecb(key, junk || unknown-string = flag || pad) initial_len = len(self.receive_enc_pt(plaintext)) final_len = initial_len # aes-ecb(key, junk || plaintext || unknown-string = flag || pad) while final_len == initial_len: plaintext += 'a' final_len = len(self.receive_enc_pt(plaintext)) return (final_len - initial_len) defcompute_junk(self, blocksize): ciphertexts = [] ciphertexts.append(self.receive_enc_pt('')[:blocksize]) # Incrementing the plaintext of one 'a' at every iteration # There will be 2 consecutive first block of the ciphertexts # equal. From there I can deduce the size of the junk for i in range(1, 17): ciphertexts.append(self.receive_enc_pt('a' * i)[:blocksize]) if ciphertexts[i] == ciphertexts[i - 1]: return17 - i # 16 or 0 is indifferent! return0
defidentify_ecb(ct, blocksize): blocks = [ct[i: i + blocksize] for i in range(0, len(ct), blocksize)] for i in range(0, len(blocks)): for j in range(i+1, len(blocks)-1): if blocks[i] == blocks[j]: return1 returnNone
defget_secret(self, blocksize, part_secret, junk): # Length of plaintext must be between 0 and 15, so % blocksize = 16 length_plaintext = (blocksize - junk - (1 + len(part_secret))) % blocksize # Length to crack is always length_plaintext + len(secret that I know) + 1 # After the first block has been decrypted, we need to reconstruct the length_plaintext # to 15, and add to it during the verification (if) the part_secret length_to_crack = length_plaintext + junk + len(part_secret) + 1 # Create plaintext plaintext = 'a' * length_plaintext # Ciphertext to compare ciphertext = self.receive_enc_pt(plaintext) # Let's create a charset of all possible ascii values (readable) charset = "qwertyuiopasdfghjklzxcvbnm" charset += charset.upper() charset += "_{}0123456789@!#$%^&*()_-+=/?.><,|"
# Time to bruteforce -\_('_')_/- for x in charset: ct = self.receive_enc_pt(plaintext + part_secret + x) if ciphertext[:length_to_crack] == ct[:length_to_crack]: return x return''
defcompute_bsize(self): plaintext = b'' # aes-ecb(key, junk || unknown-string = flag || pad) initial_len = len(self.receive_enc_pt(plaintext)) final_len = initial_len # aes-ecb(key, junk || plaintext || unknown-string = flag || pad) while final_len == initial_len: plaintext += 'a' final_len = len(self.receive_enc_pt(plaintext)) return (final_len - initial_len) defcompute_junk(self, blocksize): ciphertexts = [] ciphertexts.append(self.receive_enc_pt('')[:blocksize]) # Incrementing the plaintext of one 'a' at every iteration # There will be 2 consecutive first block of the ciphertexts # equal. From there I can deduce the size of the junk for i in range(1, 17): ciphertexts.append(self.receive_enc_pt('a' * i)[:blocksize]) if ciphertexts[i] == ciphertexts[i - 1]: return17 - i # 16 or 0 is indifferent! return0
defget_secret(self, blocksize, part_secret, junk): # Length of plaintext must be between 0 and 15, so % blocksize = 16 length_plaintext = (blocksize - junk - (1 + len(part_secret))) % blocksize # Length to crack is always length_plaintext + len(secret that I know) + 1 # After the first block has been decrypted, we need to reconstruct the length_plaintext # to 15, and add to it during the verification (if) the part_secret length_to_crack = length_plaintext + junk + len(part_secret) + 1 # Create plaintext plaintext = 'a' * length_plaintext # Ciphertext to compare ciphertext = self.receive_enc_pt(plaintext) # Let's create a charset of all possible ascii values (readable) charset = "qwertyuiopasdfghjklzxcvbnm" charset += charset.upper() charset += "_{}0123456789@!#$%^&*()_-+=/?.><,|"
# Time to bruteforce -\_('_')_/- for x in charset: ct = self.receive_enc_pt(plaintext + part_secret + x) if ciphertext[:length_to_crack] == ct[:length_to_crack]: return x return''
defidentify_ecb(ct, blocksize): blocks = [ct[i: i + blocksize] for i in range(0, len(ct), blocksize)] for i in range(0, len(blocks)): for j in range(i+1, len(blocks)-1): if blocks[i] == blocks[j]: return1 returnNone