The Three-Eyed Oracle
4 minutes to read
We are given this Python source code:
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
import random
import signal
import subprocess
import socketserver
FLAG = b'HTB{--REDACTED--}'
prefix = random.randbytes(12)
key = random.randbytes(16)
def encrypt(key, msg):
msg = bytes.fromhex(msg)
crypto = AES.new(key, AES.MODE_ECB)
padded = pad(prefix + msg + FLAG, 16)
return crypto.encrypt(padded).hex()
def challenge(req):
req.sendall(b'Welcome to Klaus\'s crypto lab.\n' +
b'It seems like there is a prefix appended to the real firmware\n' +
b'Can you somehow extract the firmware and fix the chip?\n')
while True:
req.sendall(b'> ')
try:
msg = req.recv(4096).decode()
ct = encrypt(key, msg)
req.sendall(ct.encode() + b'\n')
except Exception as e:
print(e)
req.sendall(b'An error occurred! Please try again!')
class incoming(socketserver.BaseRequestHandler):
def handle(self):
signal.alarm(1500)
req = self.request
challenge(req)
class ReusableTCPServer(socketserver.ForkingMixIn, socketserver.TCPServer):
pass
def main():
socketserver.TCPServer.allow_reuse_address = True
server = ReusableTCPServer(("0.0.0.0", 1337), incoming)
server.serve_forever()
if __name__ == "__main__":
main()
Basically, we have the opportunity to send a message that will be encrypted using AES ECB in a particular way:
FLAG = b'HTB{--REDACTED--}'
prefix = random.randbytes(12)
key = random.randbytes(16)
def encrypt(key, msg):
msg = bytes.fromhex(msg)
crypto = AES.new(key, AES.MODE_ECB)
padded = pad(prefix + msg + FLAG, 16)
return crypto.encrypt(padded).hex()
Our message is surrounded by a 12-bytes random prefix (prefix
) and the flag (FLAG
) before encryption. The problem here is that it uses AES ECB:
Therefore, the plaintext is divided in blocks of 16 bytes and then the blocks are encrypted one by one. In order to get the flag, we have an oracle. For instance, we can enter 15 characters A
, then a test character and then another 15 characters A
. When the output of the two encrypted blocks is the same, we will know that the test character is the first character of the flag.
Let’s show this graphically:
Our input: "BBBBAAAAAAAAAAAAAAAxAAAAAAAAAAAAAAA"
Plaintext Blocks:
rrrrrrrrrrrrBBBB AAAAAAAAAAAAAAAx AAAAAAAAAAAAAAA? ????????????????
Ciphertext Blocks:
R X Y Z
We will iterate over all characters and test them where the x
is placed above. We watch the output ciphertext blocks until X = Y
; at that point, we will know that the two plaintext blocks are the same, so the tested character is correct.
Then we must continue as follows:
Our input: "BBBBAAAAAAAAAAAAAAHxAAAAAAAAAAAAAA"
Plaintext Blocks:
rrrrrrrrrrrrBBBB AAAAAAAAAAAAAAHx AAAAAAAAAAAAAAH? ????????????????
Ciphertext Blocks:
R X Y Z
Our input: "BBBBAAAAAAAAAAAAAHTxAAAAAAAAAAAAA"
Plaintext Blocks:
rrrrrrrrrrrrBBBB AAAAAAAAAAAAAHTx AAAAAAAAAAAAAHT? ????????????????
Ciphertext Blocks:
R X Y Z
Our input: "BBBBAAAAAAAAAAAAHTBxAAAAAAAAAAAA"
Plaintext Blocks:
rrrrrrrrrrrrBBBB AAAAAAAAAAAAHTBx AAAAAAAAAAAAHTB? ????????????????
Ciphertext Blocks:
R X Y Z
Our input: "BBBBAAAAAAAAAAAHTB{xAAAAAAAAAAA"
Plaintext Blocks:
rrrrrrrrrrrrBBBB AAAAAAAAAAAHTB{x AAAAAAAAAAAHTB{? ????????????????
Ciphertext Blocks:
R X Y Z
There will be a situation where we cannot enter more junk A
characters. Let’s assume that the flag is HTB{f4k3_fl4g_f0r_t3st1ng}
. This will happen after this iteration:
Our input: "BBBBHTB{f4k3_f0r_t3x"
Plaintext Blocks:
rrrrrrrrrrrrBBBB HTB{f4k3_f0r_t3x HTB{f4k3_f0r_t3? ????????????????
Ciphertext Blocks:
R X Y Z
But it is easy to overcome, we just need to continue shifting the payload:
Our input: "BBBBTB{f4k3_f0r_t3sxAAAAAAAAAAAAAAA"
Plaintext Blocks:
rrrrrrrrrrrrBBBB TB{f4k3_f0r_t3sx AAAAAAAAAAAAAAAH TB{f4k3_f0r_t3s? ????????????????
Ciphertext Blocks:
R X Y Z W
So we will check the output blocks X
and Z
. And we proceed as follows:
Our input: "BBBBB{f4k3_f0r_t3stxAAAAAAAAAAAAAA"
Plaintext Blocks:
rrrrrrrrrrrrBBBB B{f4k3_f0r_t3stx AAAAAAAAAAAAAAHT B{f4k3_f0r_t3st? ????????????????
Ciphertext Blocks:
R X Y Z W
Our input: "BBBB{f4k3_f0r_t3st1xAAAAAAAAAAAAA"
Plaintext Blocks:
rrrrrrrrrrrrBBBB {f4k3_f0r_t3st1x AAAAAAAAAAAAAHTB {f4k3_f0r_t3st1? ????????????????
Ciphertext Blocks:
R X Y Z W
Our input: "BBBBf4k3_f0r_t3st1nxAAAAAAAAAAAA"
Plaintext Blocks:
rrrrrrrrrrrrBBBB f4k3_f0r_t3st1nx AAAAAAAAAAAAHTB{ f4k3_f0r_t3st1n? ????????????????
Ciphertext Blocks:
R X Y Z W
Our input: "BBBB4k3_f0r_t3st1ngxAAAAAAAAAAA"
Plaintext Blocks:
rrrrrrrrrrrrBBBB 4k3_f0r_t3st1ngx AAAAAAAAAAAHTB{f 4k3_f0r_t3st1ng? ????????????????
Ciphertext Blocks:
R X Y Z W
And the process will end here, because the correct character is }
, which terminates the flag. Now, we need to automate this process in Python and interact with the server so that we get the correct characters with the AES ECB oracle and craft the flag:
$ python3 solve.py 64.227.37.154:30799
[+] Opening connection to 64.227.37.154 on port 30799: Done
[◓] Flag: HTB{7h3_br0k3n_0r@c1e!!!}
[*] Closed connection to 64.227.37.154 port 30799
The full script can be found in here: solve.py
.