Flippin Bank
4 minutes to read
We are given the Python source code of the server:
import socketserver
import socket, os
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad,unpad
from Crypto.Random import get_random_bytes
from binascii import unhexlify
from secret import FLAG
wlcm_msg ='########################################################################\n'+\
'# Welcome to the Bank of the World #\n'+\
'# All connections are monitored and recorded #\n'+\
'# Disconnect IMMEDIATELY if you are not an authorized user! #\n'+\
'########################################################################\n'
key = get_random_bytes(16)
iv = get_random_bytes(16)
def encrypt_data(data):
padded = pad(data.encode(),16,style='pkcs7')
cipher = AES.new(key, AES.MODE_CBC,iv)
enc = cipher.encrypt(padded)
return enc.hex()
def decrypt_data(encryptedParams):
cipher = AES.new(key, AES.MODE_CBC,iv)
paddedParams = cipher.decrypt( unhexlify(encryptedParams))
print(paddedParams)
if b'admin&password=g0ld3n_b0y' in unpad(paddedParams,16,style='pkcs7'):
return 1
else:
return 0
def send_msg(s, msg):
enc = msg.encode()
s.send(enc)
def main(s):
send_msg(s, 'username: ')
user = s.recv(4096).decode().strip()
send_msg(s, user +"'s password: " )
passwd = s.recv(4096).decode().strip()
send_msg(s, wlcm_msg)
msg = 'logged_username=' + user +'&password=' + passwd
try:
assert('admin&password=g0ld3n_b0y' not in msg)
except AssertionError:
send_msg(s, 'You cannot login as an admin from an external IP.\nYour activity has been logged. Goodbye!\n')
raise
msg = 'logged_username=' + user +'&password=' + passwd
send_msg(s, "Leaked ciphertext: " + encrypt_data(msg)+'\n')
send_msg(s,"enter ciphertext: ")
enc_msg = s.recv(4096).decode().strip()
try:
check = decrypt_data(enc_msg)
except Exception as e:
send_msg(s, str(e) + '\n')
s.close()
if check:
send_msg(s, 'Logged in successfully!\nYour flag is: '+ FLAG)
s.close()
else:
send_msg(s, 'Please try again.')
s.close()
class TaskHandler(socketserver.BaseRequestHandler):
def handle(self):
main(self.request)
if __name__ == '__main__':
socketserver.ThreadingTCPServer.allow_reuse_address = True
server = socketserver.ThreadingTCPServer(('0.0.0.0', 1337), TaskHandler)
server.serve_forever()
Source code analysis
The server allows us to enter a username and a password:
send_msg(s, 'username: ')
user = s.recv(4096).decode().strip()
send_msg(s, user +"'s password: " )
passwd = s.recv(4096).decode().strip()
Then, the server creates this message msg
and verifies that 'admin&password=g0ld3n_b0y'
does not appear in the string:
msg = 'logged_username=' + user +'&password=' + passwd
try:
assert('admin&password=g0ld3n_b0y' not in msg)
except AssertionError:
send_msg(s, 'You cannot login as an admin from an external IP.\nYour activity has been logged. Goodbye!\n')
raise
After that, the server sends us the ciphertext of msg
and allows us to enter another ciphertext to decrypt it:
send_msg(s, "Leaked ciphertext: " + encrypt_data(msg)+'\n')
send_msg(s,"enter ciphertext: ")
enc_msg = s.recv(4096).decode().strip()
The encryption function uses AES in CBC mode with an unknown key and IV:
def encrypt_data(data):
padded = pad(data.encode(),16,style='pkcs7')
cipher = AES.new(key, AES.MODE_CBC,iv)
enc = cipher.encrypt(padded)
return enc.hex()
Then, if the input ciphertext decrypts to something that contains the string 'admin&password=g0ld3n_b0y'
, the server will give us the flag:
try:
check = decrypt_data(enc_msg)
except Exception as e:
send_msg(s, str(e) + '\n')
s.close()
if check:
send_msg(s, 'Logged in successfully!\nYour flag is: '+ FLAG)
s.close()
else:
send_msg(s, 'Please try again.')
s.close()
Because decrypt_data
is:
def decrypt_data(encryptedParams):
cipher = AES.new(key, AES.MODE_CBC,iv)
paddedParams = cipher.decrypt( unhexlify(encryptedParams))
print(paddedParams)
if b'admin&password=g0ld3n_b0y' in unpad(paddedParams,16,style='pkcs7'):
return 1
else:
return 0
So, we must somehow enter a ciphertext that decrypts to something that contains 'admin&password=g0ld3n_b0y'
by using a ciphertext of a message that does not contain such string.
Solution
The way to solve this exploits the behavior of AES in CBC mode:
If we divide the plaintext message in blocks, with the expected username and password, we have:
logged_username=
admin&password=g
0ld3n_b0y
Since the server only checks the existence of 'admin&password=g0ld3n_b0y'
, we can use the first ciphertext block to modify the second plaintext block:
So, the idea is to enter a username like bdmin
with password g0ld3n_b0y
, so that the first check passes. Then, we will get the ciphertext of logged_username=bdmin@password=g0ld3n_b0y
. And only need to tweak the first byte of the first ciphertext block, because it will affect the first byte of the second plaintext block. As a result, we are able to obtain admin@password=g
in the second plaintext block and pass the second check to get the flag.
Let’s say $C$ is the first byte of the first ciphertext block, and $P$ is the first byte of the second AES decryptor output.
We know that $C \oplus P = \mathtt{b}$. If we want $\mathtt{a}$, we have $C \oplus P \oplus \mathtt{b} \oplus \mathtt{a} = \mathtt{a}$ and therefore $(C \oplus \mathtt{b} \oplus \mathtt{a}) \oplus P = \mathtt{a}$.
Implementation
We can translate the above procedure in Python, which is just one XOR operation:
#!/usr/bin/env python3
from pwn import remote, sys
host, port = sys.argv[1].split(':')
io = remote(host, port)
io.sendlineafter(b'username: ', b'bdmin')
io.sendlineafter(b'password: ', b'g0ld3n_b0y')
io.recvuntil(b'Leaked ciphertext: ')
ct = bytearray.fromhex(io.recvline().decode())
ct[0] ^= ord('b') ^ ord('a')
io.sendlineafter(b'enter ciphertext: ', ct.hex().encode())
io.recvline()
io.success(io.recv().decode())
Flag
If we run the script, we will get the flag:
$ python3 solve.py 206.189.28.180:30033
[+] Opening connection to 206.189.28.180 on port 30033: Done
[+] Your flag is: HTB{b1t_fl1pp1ng_1s_c00l}
[*] Closed connection to 206.189.28.180 port 30033