Flippin Bank
4 minutos de lectura
Se nos proporciona el código fuente del servidor en Python:
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()
Análisis del código fuente
El servidor nos permite ingresar un nombre de usuario y una contraseña:
send_msg(s, 'username: ')
user = s.recv(4096).decode().strip()
send_msg(s, user +"'s password: " )
passwd = s.recv(4096).decode().strip()
Entonces, el servidor crea este mensaje msg
y verifica que 'admin&password=g0ld3n_b0y'
no aparece en la cadena:
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
Después de eso, el servidor nos envía el texto cifrado de msg
y nos permite ingresar otro texto cifrado para descifrarlo:
send_msg(s, "Leaked ciphertext: " + encrypt_data(msg)+'\n')
send_msg(s,"enter ciphertext: ")
enc_msg = s.recv(4096).decode().strip()
La función de cifrado usa AES en modo CBC con una clave y un IV desconocidos:
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()
Entonces, si el texto cifrado introducido se descifra a algo que contiene la cadena 'admin&password=g0ld3n_b0y'
, el servidor nos dará la 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()
Ya que decrypt_data
es:
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
Entonces, de alguna manera debemos ingresar un texto cifrado que se descifre a algo que contiene 'admin&password=g0ld3n_b0y'
mediante el uso de un texto cifrado de un mensaje que no contiene dicha cadena.
Solución
La forma de resolver esto explota el comportamiento de AES en modo CBC:
Si dividimos el mensaje de texto claro en bloques, con el nombre de usuario y la contraseña esperados, tenemos:
logged_username=
admin&password=g
0ld3n_b0y
Dado que el servidor solo verifica la existencia de 'admin&password=g0ld3n_b0y'
, podemos usar el primer bloque de texto cifrado para modificar el segundo bloque de texto sin formato:
Entonces, la idea es ingresar un nombre de usuario como bdmin
con contraseña g0ld3n_b0y
, para que pase la primera comprobación. Luego, obtendremos el texto cifrado de logged_username=bdmin@password=g0ld3n_b0y
. Y solo necesitamos ajustar el primer byte del primer bloque de texto cifrado, porque afectará el primer byte del segundo bloque de texto claro. Como resultado, podemos obtener admin@password=g
en el segundo bloque de texto sin formato y pasar la segunda verificación para obtener la flag.
Sea $C$ el primer byte del primer bloque de texto cifrado, y sea $P$ el primer byte de la segunda salida del descifrador AES.
Sabemos que $C \oplus P = \mathtt{b}$. Si queremos $\mathtt{a}$, tendremos $C \oplus P \oplus \mathtt{b} \oplus \mathtt{a} = \mathtt{a}$ y por tanto $(C \oplus \mathtt{b} \oplus \mathtt{a}) \oplus P = \mathtt{a}$.
Implementación
Podemos traducir el procedimiento anterior en Python, que es solo una operación XOR:
#!/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
Si ejecutamos el script, obtendremos la 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