13. ECB copier-coller

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"
}