Protein Cookies 2
5 minutos de lectura
Este reto utiliza la misma base de código que Protein Cookies, Entonces asumiremos muchas cosas de infraestructura.
Objetivo
El objetivo del reto es falsificar una cookie de sesión para autenticarnos para descargar el archivo PDF secreto que contiene la flag:
@web.route('/program')
@verify_login
def program():
return send_file('flag.pdf')
La cookie (llamada login_info
) se crea y se verifica en util.py
:
def create_cookie(username, is_logged_in=False):
data = f'user_id={username}&isLoggedIn={is_logged_in}'
signature = lj12_hash(SECRET + data.encode())
return data + '.' + signature
def verify_cookie(cookie_data):
data, signature = cookie_data.split(".")
if lj12_hash(SECRET + data.encode()) == signature:
return {
k: v[-1] for k, v in parse_qs(data).items()
}.get('isLoggedIn', '') == 'True'
return False
Formato de la cookie
Esta cookie está diseñada con el siguiente código (cryptoutil.py
):
from Crypto.Cipher import AES
from Crypto.Random import get_random_bytes
BLOCK_LEN = 32
SECRET = get_random_bytes(50)
iv = b"@\xab\x97\xca\x18\x1d\xac<\x1e\xc3xC\x9b\x1c\xc5\x1f\x8aD=\xec*\x16G\xe7\x89'\x80\xe4\xe6\xfc5l"
def pad(data):
if len(data) % BLOCK_LEN == 0:
return data
pad_byte = bytes([len(data) % 256])
pad_len = BLOCK_LEN - (len(data) % BLOCK_LEN)
data += pad_byte * pad_len
return data
def compression_function(data, key):
if len(data) != BLOCK_LEN or len(key) != BLOCK_LEN:
raise ValueError(f"Input for compression function is not {BLOCK_LEN} bytes long!")
# AES is a safe compression function, right? Why not just use that?
cipher = AES.new(key, AES.MODE_ECB)
enc = cipher.encrypt(data)
# let's confuse it up a bit more, don't want to make it too easy!
enc = enc[::-1]
enc = enc[::2] + enc[1::2]
enc = enc[::3] + enc[2::3] + enc[1::3]
return enc
def lj12_hash(data):
data = pad(data)
blocks = [data[x:x + BLOCK_LEN] for x in range(0, len(data), BLOCK_LEN)]
enc_block = iv
for i in range(len(blocks)):
enc_block = compression_function(blocks[i], enc_block)
return enc_block.hex()
Utiliza AES como una función de hash, que es un poco raro.
En resumen, la cookie contiene datos y una firma (separados por un punto), de modo que los datos no son fácilmente modificables:
Comprendiendo la firma
Vamos a probar estas funciones en el REPL de Python:
$ python3 -q
>>> from Crypto.Cipher import AES
>>> from Crypto.Random import get_random_bytes
>>>
>>> BLOCK_LEN = 32
>>> SECRET = get_random_bytes(50)
>>>
>>> iv = b"@\xab\x97\xca\x18\x1d\xac<\x1e\xc3xC\x9b\x1c\xc5\x1f\x8aD=\xec*\x16G\xe7\x89'\x80\xe4\xe6\xfc5l"
>>>
>>>
>>> def pad(data):
... if len(data) % BLOCK_LEN == 0:
... return data
... pad_byte = bytes([len(data) % 256])
... pad_len = BLOCK_LEN - (len(data) % BLOCK_LEN)
... data += pad_byte * pad_len
... return data
...
>>>
>>> def compression_function(data, key):
... print(f'compression_function({data}, {key.hex()})')
... if len(data) != BLOCK_LEN or len(key) != BLOCK_LEN:
... raise ValueError(f"Input for compression function is not {BLOCK_LEN} bytes long!")
... # AES is a safe compression function, right? Why not just use that?
... cipher = AES.new(key, AES.MODE_ECB)
... enc = cipher.encrypt(data)
... print(f'AES({key.hex()}) ({data}) = {enc.hex()}')
... # let's confuse it up a bit more, don't want to make it too easy!
... enc = enc[::-1]
... enc = enc[::2] + enc[1::2]
... enc = enc[::3] + enc[2::3] + enc[1::3]
... print(f'{enc.hex() = }\n')
... return enc
...
>>>
>>> def lj12_hash(data):
... data = pad(data)
... blocks = [data[x:x + BLOCK_LEN] for x in range(0, len(data), BLOCK_LEN)]
... enc_block = iv
... for i in range(len(blocks)):
... enc_block = compression_function(blocks[i], enc_block)
... return enc_block.hex()
...
>>>
Especialmente, estamos interesados en compression_function
, así que obsérvese cómo agregamos algunas instrucciones print
para ver los resultados intermedios. Ahora, construyamos una cookie:
>>> def create_cookie(username, is_logged_in=False):
... data = f'user_id={username}&isLoggedIn={is_logged_in}'
... signature = lj12_hash(SECRET + data.encode())
... return data + '.' + signature
...
>>> create_cookie('guest')
compression_function(b'\xe6oe\x8b\x1b\x01\x9d\xa2\xfd\x82n\x19#\x1e=\xbdT\xcdfV/\xfc\xbeh0\xe2\xeb\x00\xc6.A6', 40ab97ca181dac3c1ec378439b1cc51f8a443dec2a1647e7892780e4e6fc356c)
AES(40ab97ca181dac3c1ec378439b1cc51f8a443dec2a1647e7892780e4e6fc356c) (b'\xe6oe\x8b\x1b\x01\x9d\xa2\xfd\x82n\x19#\x1e=\xbdT\xcdfV/\xfc\xbeh0\xe2\xeb\x00\xc6.A6') = 0775dee00d03383ffe727ac4f525fd87c6218042edfe65003887974b669251c8
enc.hex() = 'c88742253f7597edfdfede4bfe8772e06665c67a0d920021c403513880f53807'
compression_function(b'l\x8bk\xf4\xaa|\xbdL\xef\x87\x0e\x1d[\xe8\xb7\xa3gVuser_id=guest&', c88742253f7597edfdfede4bfe8772e06665c67a0d920021c403513880f53807)
AES(c88742253f7597edfdfede4bfe8772e06665c67a0d920021c403513880f53807) (b'l\x8bk\xf4\xaa|\xbdL\xef\x87\x0e\x1d[\xe8\xb7\xa3gVuser_id=guest&') = 5e5244d093d9bbe4da813e1c769a99c92825e7b9498672f940dbd73bab6d97f3
enc.hex() = 'f3dbb99ae452d74999da443b86c981d0ab72283e936df9251cd99740e776bb5e'
compression_function(b'isLoggedIn=FalsePPPPPPPPPPPPPPPP', f3dbb99ae452d74999da443b86c981d0ab72283e936df9251cd99740e776bb5e)
AES(f3dbb99ae452d74999da443b86c981d0ab72283e936df9251cd99740e776bb5e) (b'isLoggedIn=FalsePPPPPPPPPPPPPPPP') = c2da45ccd5382bac94a79eed57a6151d571efe71fe0669e4c53ba8228865ed82
enc.hex() = '823b71a6acdaa8fe15944522061da7cc8869579ed565e41eed38edc5fe572bc2'
'user_id=guest&isLoggedIn=False.823b71a6acdaa8fe15944522061da7cc8869579ed565e41eed38edc5fe572bc2'
¿Parece claro, no? compression_function
coge data
y key
, crea un cifrador AES con key
y cifra data
con AES ECB. Luego, el texto cifrado se mezcla y se devuelve el resultado. La próxima llamada a compression_function
cogerá el siguiente bloque de datos como data
y el resultado anterior como key
. Nótese cómo los datos de entrada están rellenados con caracteres P
.
Ataque de hash length extension
Al igual que en Protein Cookies, este reto está relacionado con el ataque de hash length extension. Mientras leía este artículo, comprendí exactamente lo que había que hacer aquí.
El objetivo es agregar más información a los datos ya firmados y crear una nueva firma que contenga la nueva información más la anterior. La clave es comprender que las funciones hash dividen la información en bloques de longitud fija (y usan relleno si es necesario). Calculan el hash para un bloque y tienen ese resultado en cuenta para el siguiente bloque.
Entonces, para agregar información, necesitamos llenar los datos ya firmados con relleno y agregar nuevos datos. Luego, debemos tomar la firma actual de los datos firmados y usarla para generar la nueva firma utilizando el siguiente bloque (la información agregada). El resultado será una firma válida.
Implementación
Para este reto, nos gustaría agregar isLoggedIn=True
. Debido a cómo funcionan las queries de consulta y Python, simplemente podemos agregar &isLoggedIn=True
a la información actual:
>>> from urllib.parse import parse_qs
>>>
>>> {k: v[-1] for k, v in parse_qs('user_id=guest&isLoggedIn=False').items()}
{'user_id': 'guest', 'isLoggedIn': 'False'}
>>> {k: v[-1] for k, v in parse_qs('user_id=guest&isLoggedIn=FalsePPPPPPPPPPPPPPPP&isLoggedIn=True').items()}
{'user_id': 'guest', 'isLoggedIn': 'True'}
Entonces, la implementación es simple:
>>> lj12_hash(SECRET + b'user_id=guest&isLoggedIn=FalsePPPPPPPPPPPPPPPP&isLoggedIn=True')
compression_function(b'\xe6oe\x8b\x1b\x01\x9d\xa2\xfd\x82n\x19#\x1e=\xbdT\xcdfV/\xfc\xbeh0\xe2\xeb\x00\xc6.A6', 40ab97ca181dac3c1ec378439b1cc51f8a443dec2a1647e7892780e4e6fc356c)
AES(40ab97ca181dac3c1ec378439b1cc51f8a443dec2a1647e7892780e4e6fc356c) (b'\xe6oe\x8b\x1b\x01\x9d\xa2\xfd\x82n\x19#\x1e=\xbdT\xcdfV/\xfc\xbeh0\xe2\xeb\x00\xc6.A6') = 0775dee00d03383ffe727ac4f525fd87c6218042edfe65003887974b669251c8
enc.hex() = 'c88742253f7597edfdfede4bfe8772e06665c67a0d920021c403513880f53807'
compression_function(b'l\x8bk\xf4\xaa|\xbdL\xef\x87\x0e\x1d[\xe8\xb7\xa3gVuser_id=guest&', c88742253f7597edfdfede4bfe8772e06665c67a0d920021c403513880f53807)
AES(c88742253f7597edfdfede4bfe8772e06665c67a0d920021c403513880f53807) (b'l\x8bk\xf4\xaa|\xbdL\xef\x87\x0e\x1d[\xe8\xb7\xa3gVuser_id=guest&') = 5e5244d093d9bbe4da813e1c769a99c92825e7b9498672f940dbd73bab6d97f3
enc.hex() = 'f3dbb99ae452d74999da443b86c981d0ab72283e936df9251cd99740e776bb5e'
compression_function(b'isLoggedIn=FalsePPPPPPPPPPPPPPPP', f3dbb99ae452d74999da443b86c981d0ab72283e936df9251cd99740e776bb5e)
AES(f3dbb99ae452d74999da443b86c981d0ab72283e936df9251cd99740e776bb5e) (b'isLoggedIn=FalsePPPPPPPPPPPPPPPP') = c2da45ccd5382bac94a79eed57a6151d571efe71fe0669e4c53ba8228865ed82
enc.hex() = '823b71a6acdaa8fe15944522061da7cc8869579ed565e41eed38edc5fe572bc2'
compression_function(b'&isLoggedIn=Truepppppppppppppppp', 823b71a6acdaa8fe15944522061da7cc8869579ed565e41eed38edc5fe572bc2)
AES(823b71a6acdaa8fe15944522061da7cc8869579ed565e41eed38edc5fe572bc2) (b'&isLoggedIn=Truepppppppppppppppp') = df5243aa39123c49d7bc5e60f5c56871e6db406f1cef75b306cbdfb4ca295384
enc.hex() = '84cb6fc54952df1c68d743b4ef71bcaaca75e65e3929b3db6012530640f53cdf'
'84cb6fc54952df1c68d743b4ef71bcaaca75e65e3929b3db6012530640f53cdf'
Como se puede ver, solo necesitamos tomar la firma actual (823b71...572bc2
) y ponerla en compression_function
usando &isLoggedIn=True
rellenado con caracteres p
. El hash de salida será una firma válida.
Hagamos esto para la instancia remota del reto. La firma actual es 2ab7bd...4372de
. Vamos a ejecutar compression_function
:
>>> compression_function(b'&isLoggedIn=Truepppppppppppppppp', bytes.fromhex('2ab7bd190e7d915b8b284fd41d9bf8a9363d81c0da1bccae8159a7a09d4372de'))
compression_function(b'&isLoggedIn=Truepppppppppppppppp', 2ab7bd190e7d915b8b284fd41d9bf8a9363d81c0da1bccae8159a7a09d4372de)
AES(2ab7bd190e7d915b8b284fd41d9bf8a9363d81c0da1bccae8159a7a09d4372de) (b'&isLoggedIn=Truepppppppppppppppp') = 9ce82a3638c784bb73234dd5cd05bc749e20f2eace2c6d3956b712a65ab1c268
enc.hex() = '68b7ea05bbe812cebc732aa62c7423365a6d9e4d38b13920d5c7c256f2cd849c'
b'h\xb7\xea\x05\xbb\xe8\x12\xce\xbcs*\xa6,t#6Zm\x9eM8\xb19 \xd5\xc7\xc2V\xf2\xcd\x84\x9c'
Ahora agregamos la siguiente cookie al navegador:
user_id=guest&isLoggedIn=FalsePPPPPPPPPPPPPPPP&isLoggedIn=True.68b7ea05bbe812cebc732aa62c7423365a6d9e4d38b13920d5c7c256f2cd849c
Flag
Si vamos a /program
, veremos la flag:
HTB{b3aT1nG_tH3_cUsT0m_h4sH_??!}