如何通过使用服务 SID 运行计划任务来获取 TrustedInstaller 组。由于服务 SID 与您使用虚拟服务帐户时使用的名称相同,因此很明显问题出在此功能的实现方式上,并且可能与创建 LS 或 NS 令牌的方式不同。
Windows 10 中任务调度程序的核心进程创建代码实际上是在统一后台进程管理器 (UBPM) DLL中,而不是在任务调度程序本身中。快速查看该 DLL,我们发现以下代码:
代码语言:javascript复制HANDLE UbpmpTokenGetNonInteractiveToken(PSID PrincipalSid) {
// ...
if (UbpmUtilsIsServiceSid(PrinicpalSid)) {
return UbpmpTokenGetServiceAccountToken(PrinicpalSid);
}
if (EqualSid(PrinicpalSid, kNetworkService)) {
Domain = L"NT AUTHORITY";
User = L"NetworkService";
} else if (EqualSid(PrinicpalSid, kLocalService)) {
Domain = L"NT AUTHORITY";
User = L"LocalService";
}
HANDLE Token;
if (LogonUserExExW(User, Domain, Password,
LOGON32_LOGON_SERVICE,
LOGON32_PROVIDER_DEFAULT, &Token)) {
return Token;
}
// ...
}
这个 UbpmpTokenGetNonInteractiveToken函数从任务注册中获取主体 SID 或传递给RunEx并确定它代表什么以取回令牌。它检查 SID 是否为服务 SID,即 我们在上一篇博文中使用的NT SERVICENAME SID。如果是,则调用单独的函数 UbpmpTokenGetServiceAccountToken来获取服务令牌。
否则,如果 SID 是 NS 或 LS,那么它会为这些 SID 指定众所周知的名称,并使用 LOGON32_LOGON_SERVICE类型调用LogonUserExEx 。UbpmpTokenGetServiceAccountToken函数执行以下操作:
代码语言:javascript复制TOKEN UbpmpTokenGetServiceAccountToken(PSID PrincipalSid) {
LPCWSTR Name = UbpmUtilsGetAccountNamesFromSid(PrincipalSid);
SC_HANDLE scm = OpenSCManager(NULL, NULL, SC_MANAGER_CONNECT);
SC_HANDLE service = OpenService(scm, Name, SERVICE_ALL_ACCESS);
HANDLE Token;
GetServiceProcessToken(g_ScheduleServiceHandle, service, &Token);
return Token;
}
此函数从服务 SID 获取名称,该名称是服务本身的名称,并为所有访问权限 ( SERVICE_ALL_ACCESS ) 打开它。如果成功,则它将服务句柄传递给未记录的 SCM API GetServiceProcessToken,后者返回服务的令牌。查看 SCM 中的实现,这基本上使用了与创建用于启动服务的令牌完全相同的代码。
这就是为什么 LS/NS 和使用 Clément 技术的虚拟服务帐户之间存在区别的原因。如果您使用 LS/NS,则任务调度程序会从 LSA 获取新令牌,而不考虑服务的配置方式。因此,新令牌具有SeImpersonatePrivilege(或其他任何允许的)。但是,对于虚拟服务帐户,服务会向 SCM 询问服务的令牌,因为 SCM 知道存在哪些限制,它尊重特权或 SID 类型等内容。因此,返回的令牌将再次被剥夺SeImpersonatePrivilege,即使它在技术上与当前运行的服务是不同的令牌。
为什么任务调度程序需要一些未记录的函数来获取服务令牌?只有 SCM(从技术上讲是声称它是 SCM 的第一个进程)被允许使用虚拟服务帐户对令牌进行身份验证。如果您问我,这似乎毫无意义,因为您已经需要SeTcbPrivilege来创建服务令牌,但它就是这样。
好的,现在我们知道为什么 Clément 的技术无法让您恢复任何特权。你现在可能会问,那又怎样?一个有趣的行为来自查看任务调度程序如何确定是否允许您将服务 SID 指定为主体。在我关于创建以TrustedInstaller运行的任务的博客文章中,我暗示它需要管理员访问权限,这是真的,也不是。让我们看看任务调度程序使用的函数来确定调用者是否允许将任务作为指定的主体运行。
代码语言:javascript复制BOOL IsPrincipalAllowed(User& principal) {
RpcAutoImpersonate::RpcAutoImpersonate();
User caller;
User::FromImpersonationToken(&caller);
RpcRevertToSelf();
if (tsched::IsUserAdmin(caller) ||
caller.IsLocalSystem(caller)) {
return TRUE;
}
if (principal == caller) {
return TRUE;
}
if (principal.IsServiceSid()) {
LPCWSTR Name = principal.GetAccount();
RpcAutoImpersonate::RpcAutoImpersonate();
SC_HANDLE scm = OpenSCManager(NULL, NULL, SC_MANAGER_CONNECT);
SC_HANDLE service = OpenService(scm, Name, SERVICE_ALL_ACCESS);
RpcRevertToSelf();
if (service) {
return TRUE;
}
}
return FALSE;
}
IsPrincipalAllowed函数首先检查调用者是管理员还是系统。如果是,则允许任何主体(同样不完全正确,但足够好)。接下来,它检查主体的用户 SID 是否与我们设置的匹配。这将允许 NS/LS 或虚拟服务帐户指定作为他们自己的用户帐户运行的任务。
最后,如果主体是服务 SID,则它会在模拟调用者时尝试打开服务以进行完全访问。如果成功,它允许将服务 SID 用作主体。这种行为很有趣,因为它允许以一种偷偷摸摸的方式滥用配置不当的服务。
这是一个众所周知的权限提升检查,您枚举所有本地服务并查看它们是否授予普通用户特权访问权限,主要是SERVICE_CHANGE_CONFIG。这足以劫持服务并让任意代码作为服务帐户运行。一个常见的技巧是更改可执行路径并重新启动服务,但这并不是很好,原因有几个。
- 更改可执行路径很容易被注意到。
- 之后您可能想再次修复路径,这只是一种痛苦。
- 如果服务当前正在运行,您需要停止服务,然后重新启动修改后的服务以执行代码。
但是,只要您的帐户被授予对服务的完全访问权限,即使不是管理员,您也可以使用任务计划程序来让代码以服务的用户帐户(例如 SYSTEM)的身份运行,而无需直接修改服务的配置或停止/启动服务。偷偷摸摸的多了。当然,这确实意味着运行任务的令牌可能会被剥夺特权等,但这很容易处理(只要它不受写限制)。
这是一个很好的教训,告诉我们如何永远不要只看表面上的东西。我只是假设调用者需要管理员权限才能将服务帐户设置为任务的主体。但是,如果您深入研究代码,这似乎并不是必需的。希望有人会发现它有用。
脚注:如果您读到这里,您可能还会问,您是否可以从虚拟服务帐户中取回SeImpersonatePrivilege?由于创建令牌的方式,存储在登录会话中的令牌仍将具有所有分配的权限。您可以通过使用命名管道将令牌提取到您自己的服务,并使用它来创建一个新进程并取回所有丢失的权限。