Skip to content

MySQL exposed to the internet

Severity: criticalApplies to: Any reachable server

MySQL on a public IP is a brute-force target with no rate limiting and no account lockout. It will answer authentication attempts as fast as they arrive, indefinitely, and internet-wide scanning finds port 3306 within minutes of it becoming routable.

That much is expected. What makes it worse than it sounds is what a successful login reaches.

Three escalation paths, in the order attackers try them:

FILE privilege → read and write files. An account with FILE can read any file the mysql OS user can read, and write files anywhere it can write:

SELECT LOAD_FILE('/etc/passwd');
SELECT '<?php system($_GET[0]); ?>' INTO OUTFILE '/var/www/html/x.php';

That’s a web shell, from a database login. secure_file_priv is what stops it.

UDF → arbitrary code. With FILE and INSERT on mysql, an attacker writes a shared object into the plugin directory and registers it as a user-defined function. The function then executes native code inside the mysqld process, as the mysql user. This is the classic MySQL RCE and it needs no vulnerability — only privileges.

Credentials for everything else. The mysql.user table holds password hashes. On mysql_native_password those are unsalted SHA-1 — cheap to crack offline, and people reuse database passwords.

So “there’s nothing sensitive in that database” is the wrong question, the same way it is for Redis and Postgres. The data is not the prize. The host is.

Roughly in order:

  1. A weak password on an account with '%' as its host — reachable from anywhere, and guessable.
  2. GRANT ALL ON *.*, so the account that fell has FILE and the UDF path is open.
  3. An anonymous account left over from a legacy install — no password required at all.
  4. An EOL server. MySQL 8.0 reached end of life in April 2026 and gets no further security fixes.

Notice that none of these are exotic. They’re the defaults of a setup that was made to work quickly.

  1. Bind to a private interface, or skip-networking if nothing connects over TCP. This alone closes it.
  2. Firewall 3306 — and 33060 if MySQL’s X Protocol is running.
  3. Remove '%' hosts and FILE/SUPER from applications — bounds what a fallen credential reaches.
  4. Set secure_file_priv = NULL — closes both the web-shell and UDF paths even if a grant is wrong.

Steps 1 and 2 take two minutes.