原文:https://www.mdsec.co.uk/2018/09/applocker-clm-bypass-via-com/
约束语言模式是一种用来限制PowerShell访问权限的方法,不仅可以用来禁止访问诸如Add-Type之类得功能,还能禁止其访问可用于利用PowerShell运行时来启动post-exploitation工具的各种反射方法。
尽管微软可能会声称,这个特性在很大程度上被用作一种安全控制措施,能够让防御者使“Invoke-Mimikatz”等工具无法运行,因为它们严重依赖于反射型技术。
当将要在强制执行约束语言模式的环境中执行任务时,我会设法寻找绕过该保护措施的方法。为此,我启动了一个Windows 10实例,并通过默认规则集配置了CLM。在这篇文章中,我们将为读者详细展示这项研究的结果,以及作为非管理员用户绕过该保护措施的方法。
出发吧…
在测试环境中,我们需要做的第一件事就是启用AppLocker。就本文来说,我们将使用Windows部署的默认规则来限制脚本的执行。启动Application Identity服务后,可以执行以下命令,以确保CLM已启用:
$ExecutionContext.SessionState.LanguageMode
在这里,我们应该看到返回ConstrainedLanguage的值,表明我们现在处于受限制的环境中。为了进一步确认这一点,我们可以尝试使用PowerShell中的受限命令执行一项简单的任务,看看系统有什么反应:
Add-Type "namespace test { }"
很好,现在CLM已经启用了,接下来,我们要做的事情就是设法绕过它?
AppLocker CLM中的新对象......管用吗?
令人惊讶的是,当我考察CLM的攻击面时,我惊奇地发现,当通过AppLocker启用CLM时,New-Object是可以正常工作的(尽管有一些限制)。这似乎与预期的目标不一致,可实践证明,以下命令确实能够运行:
New-Object -ComObject WScript.Shell
毫无疑问,这为我们提供了一种在PowerShell中操作PowerShell进程的完美方式,因为COM对象完全可以通过DLL加载到调用进程中。那么,我们怎样才能创建一个可以加载的COM对象呢?好吧,如果我们在尝试调用New-Object-ComObject xpntest时查看ProcMon,就会发现针对HKEY_CURRENT_USER的请求有许多:
经过一番折腾后发现,我们可以使用以下脚本在HKCU中创建所需的注册表项:
现在,如果尝试加载COM对象,就会看到我们的自定义的DLL将被加载到PowerShell进程空间中:
太棒了,我们现在可以将任意的DLL加载到PowerShell中,而不用求助于“动静太大”的CreateRemoteThread或WriteProcessMemory调用,并且,所有这些都是在受限的上下文中进行的。但是,如果禁用了约束语言模式,我们如何通过非托管DLL加载方法来实现这一点呢?实际上,我们可以利用.NET CLR,或者确切地说,我们利用非托管DLL来加载.NET CLR,从而帮助调用.NET程序集......
从非托管DLL到托管DLL再到反射
现在,人们认为将CLR加载到非托管进程中是理所当然的,比如,Cobalt Strike就提供了Execute-Assembly等工具来简化这一过程。我之前已经分享过如何在不借助Cobalt Strike的情况下,实现相同技术的相关要点:
当然,我们不会在这里讨论这段代码的内部机制(如果读者有兴趣的话,建议阅读微软的相关文章),但最终结果是,DLL将加载.NET CLR,然后是.NET程序集,并且将执行流程传给指定的方法。
完成上述操作后,现在就可以访问.NET了,更重要的是,还能使用.NET的反射功能。接下来,我们需要弄清楚CLM的开/关“机关”在哪里。
通过对PowerShell、System.Management.Automation.dll的.NET程序集进行反汇编处理,我们发现其中一个用于标识语言模式的地方,即属性System.Management.Automation.Runspaces.RunspaceBase.LanguageMode。由于我们将使用反射技术,所以,我们需要通过一个变量找到对Runspace的引用,当然,这个变量必须是在运行时可以受我们控制的。我发现完成这些任务的最好方式,是借助于Runspaces.Runspace.DefaultRunspace.SessionStateProxy.LanguageMode,例如:
将其编译为.NET程序集后,我们就可以通过反射方法来禁用CLM了。这样一来,剩下的事情就是创建一个PowerShell脚本来处理相应的事项了:
好了,我们来看一段视频:
为什么会这样呢?
那么,为什么COM能够绕过这项保护措施,同时,PowerShell又是如何处理COM加载的呢?
答案可以在SystemPolicy.IsClassInApprovedList方法中找到,该方法用于检查我们是否允许向New-Object提供的CLSID。当我们深入研究这种方法时,会发现大部分工作都是由以下代码完成的:
if (SystemPolicy.WldpNativeMethods.WldpIsClassInApprovedList(ref clsid, ref wldp_HOST_INFORMATION, ref num, 0u) >= 0 && num == 1) { ... }
该调用只是wldp.dll公开的WldpIsClassInApprovedList函数的包装器,它用于根据DeviceGuard策略(或现在已知的Windows Defender Application Control)来检查CLSID。由于该方法不适用于AppLocker,这就意味着任何CLSID都能通过检查。
怪哉??!!
因此,在测试这种技术时,我遇到了一个奇怪的情况:通过以下方法设置CLM时,该技术将失效:
$ExecutionContext.SessionState.LanguageMode = "ConstrainedLanguage"
这个问题困扰了我一段时间,因为我过去曾使用上面的方法测试过有效载荷,那有,区别在哪呢?回到我们的反汇编代码,最终在程序集Microsoft.Powershell.Commands.Utility.dll中找到了答案,准确的说是NewObjectCommand类的BeginProcessing方法:
在这里,我们可以看到实际上有两个代码路径,走哪条路径具体取决于CLM的启用方式。如果SystemPolicy.GetSystemLockdownPolicy返回Enforce,则会采用第一个代码路径,这是启用AppLocker或DeviceGuard时的情况,但是,如果直接设置ExecutionContext.SessionState.LanguageMode属性的话,情况就不同了。如果直接设置该属性,我们就会直接进入if(!flag)...代码块,这时,它会抛出异常。由此得出的结论是,CLM的反应实际上会略有不同,具体取决于它是通过AppLocker、DeviceGuard还是通过LanguageMode属性启用的。
当然,这绝不是绕过CLM的唯一方法,因为即使粗略地看一下PowerShell也会发现,还有许多潜在的路径可以实现类似的效果。需要推荐的是,Oddvar Moe是这方面的专家,读者可以通过他的Derbycon演讲来了解更多的高超技巧!