13. ECB copier-coller
ECB cut-and-paste
Write a k=v parsing routine, as if for a structured cookie. The routine should take:
foo=bar&baz=qux&zap=zazzle
... and produce:
{
foo: 'bar',
baz: 'qux',
zap: 'zazzle'
}
(you know, the object; I don't care if you convert it to JSON).
Now write a function that encodes a user profile in that format, given an email address. You should have something like:
profile_for("foo@bar.com")
... and it should produce:
{
email: 'foo@bar.com',
uid: 10,
role: 'user'
}
... encoded as:
email=foo@bar.com&uid=10&role=user
Your "profile_for" function should not allow encoding metacharacters (& and =). Eat them, quote them, whatever you want to do, but don't let people set their email address to "foo@bar.com&role=admin".
Now, two more easy functions. Generate a random AES key, then:
- Encrypt the encoded user profile under the key; "provide" that to the "attacker".
- Decrypt the encoded user profile and parse it.
Using only the user input to profile_for() (as an oracle to generate "valid" ciphertexts) and the ciphertexts themselves, make a role=admin profile.
L'objectif de cet exercice est d'arriver à créer un faux identifiant avec les droits administrateur (symbolisé par role=admin
).
Fonction de parsing
Première étape demandée dans l'énoncé, transformer une chaine de caracètres foo=bar&baz=qux&zap=zazzle
en objet / dictionnaire {'foo': 'bar', 'baz': 'qux', 'zap': 'zazzle'}
:
def parse_uri(uri):
profile = {}
uri = uri.split("&")
for element in uri:
key,value = element.split("=")
profile[key] = value
return profile
print(parse_uri("foo=bar&baz=qux&zap=zazzle"))
On obtient bien le résultat :
{'foo': 'bar', 'baz': 'qux', 'zap': 'zazzle'}
Fonction profile_for
Comme demandé dans l'énonce, voici les fonctions qui transforme une adresse email en un profil puis en une représentation texte chiffrée de celui-ci :
import os
from cryptopals import ecb_encrypt, ecb_decrypt, padding
key = os.urandom(16)
def profile_to_string(profile):
output = []
for property_key in profile:
output.append(property_key.encode()+b"="+profile[property_key])
return ecb_encrypt((b"&".join(output)), key)
def profile_for(email):
return profile_to_string({
"email": email.decode().replace("&", "_").replace("=", "_").encode(),
"uid": b"10",
"role": b'user'
})
Falsification
Sachant qu'un bloc chiffré avec ECB donnera toujours le même résultat si la même clef est utilisée, notre but ici va être de se servir du fait de pouvoir appeler la fonction profile_for
autant de fois que l'on souhaite pour créer une charge de donnée utile.
On peut noter que si l'on appel la fonction profile_for
avec l'email random@mail.com
, le résultat correspondra au 3 blocs suivants chiffrés:
email=random@mai
l.com&uid=10&rol
e=user
Si l'on suit cette logique, en appelant la fonction profile_for
avec comme paramètre 0000000000admin
, on obtient:
email=0000000000
admin&uid=10&rol
e=user
De la même manière, si l'on envoie la valeur rand@mail.com
, on obtient:
email=rand@mail.
com&uid=10&role=
user
Si on assemble les blocs des deux précédentes étapes, on peut obtenir :
email=rand@mail.
com&uid=10&role=
admin&uid=10&rol
user
Le problème de cette solution est qu'en fonction de la fonction de parsing, le payload pourrait ne pas être interprété à cause du fait que uid
soit assigné 2 fois et également à cause de la présence d'une clef rol
sans valeur associé. Notre propre fonction parse_uri
retournerait d'ailleurs une erreur si on lui envoyait ce payload.
La solution "propre" est de se souvenir que si le la longueur du texte n'est pas un multiple de 16, il faut lui rajouter un remplissage (padding).
Si on utilise la valeur 0000000000admin\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b
, on obtient comme second bloc:
admin\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b
Code
On peut coder ces différentes étapes comme ceci :
admin_payload = profile_for(b"0000000000"+padding(b"admin"))[16:32]
email_payload = profile_for(b"rand@mail.com")[0:32]
payload = email_payload + admin_payload
Si l'on déchiffre notre payload :
print(ecb_decrypt(email_payload, key)+ecb_decrypt(admin_payload, key))
On obtient :
email=rand@mail.com&uid=10&role=admin&uid=10&rol
Ce qui, une fois tranformé en objet, donne :
{
"email": "rand@mail.com",
"uid": 10,
"role": "admin"
}