深入探索任务计划程序的服务账户使用机制与权限提升技巧

本文详细分析了Windows任务计划程序中服务账户的令牌生成机制,揭示了虚拟服务账户与LOCAL/NETWORK SERVICE在权限获取上的差异,并探讨了如何通过服务配置漏洞实现非管理员权限的SYSTEM代码执行。

任务计划程序服务账户使用的进一步探讨

最近我在研究一个以完整虚拟服务账户(而非LOCAL SERVICE或NETWORK SERVICE)运行的服务,但该服务已被移除SeImpersonatePrivilege权限。在寻找解决方案时,我回忆起Andrea Pierini曾发表过关于使用虚拟服务账户的博客,于是决定从中寻找灵感。有趣的是,他提到Clément Labro发现的滥用任务计划程序的技术(对LS或NS有效)在使用虚拟服务账户时却无效。出于好奇,我决定进一步研究,并在此过程中发现了一个可用于其他目的的隐蔽技术。

我之前已经写过关于任务计划程序使用服务账户的博客。特别是在之前的博文中,我讨论了如何通过使用服务SID运行计划任务来获取TrustedInstaller组权限。由于服务SID与使用虚拟服务账户时使用的名称相同,很明显问题在于该功能的实现方式,并且可能与LS或NS令牌的创建方式不同。

Windows 10中任务计划程序的核心进程创建代码实际上位于统一后台进程管理器(UBPM)DLL中,而不是在任务计划程序本身。快速查看该DLL,我们找到以下代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
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函数从任务注册或传递给RunEx的主体SID中确定其代表的内容以获取令牌。它检查SID是否为服务SID,即我们在上一篇博文中使用的NT SERVICE\NAME SID。如果是,则调用单独的函数UbpmpTokenGetServiceAccountToken来获取服务令牌。

否则,如果SID是NS或LS,则指定这些SID的已知名称,并使用LOGON32_LOGON_SERVICE类型调用LogonUserExExUbpmpTokenGetServiceAccountToken函数执行以下操作:

1
2
3
4
5
6
7
8
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 APIGetServiceProcessToken,该API返回服务的令牌。查看SCM中的实现,这基本上使用与创建启动服务令牌时完全相同的代码。

这就是为什么使用Clément的技术时,LS/NS和虚拟服务账户之间存在区别。如果使用LS/NS,任务计划程序会从LSA获取一个新令牌,而不考虑服务的配置方式。因此,新令牌具有SeImpersonatePrivilege(或其他允许的权限)。然而,对于虚拟服务账户,服务会向SCM请求服务令牌,由于SCM知道存在的限制,它会遵守诸如权限或SID类型之类的事项。因此,返回的令牌将再次被剥离SeImpersonatePrivilege,尽管在技术上它与当前运行的服务令牌不同。

为什么任务计划程序需要一些未记录的函数来获取服务令牌?正如我在之前关于虚拟账户的博文中提到的,只有SCM(严格来说,是第一个声称自己是SCM的进程)被允许使用虚拟服务账户验证令牌。如果你问我,这似乎有点毫无意义,因为你已经需要SeTcbPrivilege来创建服务令牌,但事实就是如此。

好了,现在我们知道为什么Clément的技术无法让你恢复任何权限。你现在可能会问,那又怎样?嗯,一个有趣的行为来自查看任务计划程序如何确定是否允许你将服务SID指定为主体。在我创建以TrustedInstaller身份运行的任务的博文中,我暗示需要管理员访问权限,这在一定程度上是对的,但也并非完全如此。让我们看看任务计划程序用于确定调用者是否被允许以指定主体身份运行任务的函数。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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函数首先检查调用者是否是管理员或SYSTEM。如果是,则允许任何主体(同样不完全正确,但足够好)。接下来,它检查主体的用户SID是否与我们设置的SID匹配。这将允许NS/LS或虚拟服务账户指定以自己用户账户身份运行的任务。

最后,如果主体是服务SID,则它会在模拟调用者的同时尝试完全打开服务。如果成功,则允许使用服务SID作为主体。这种行为很有趣,因为它提供了一种隐蔽的方式来滥用配置不当的服务。

权限提升的一个众所周知的方法是枚举所有本地服务,并查看是否有任何服务授予普通用户特权访问权限,主要是SERVICE_CHANGE_CONFIG。这足以劫持服务并以服务账户身份运行任意代码。一个常见的技巧是更改可执行路径并重新启动服务,但由于几个不同的原因,这并不理想。

  1. 更改可执行路径很容易被注意到。
  2. 你可能希望之后修复路径,但这很麻烦。
  3. 如果服务当前正在运行,你需要停止服务,然后重新启动修改后的服务以获得代码执行。

然而,只要你的账户被授予对服务的完全访问权限,你就可以使用任务计划程序,即使不是管理员,也能以服务用户账户(如SYSTEM)身份运行代码,而无需直接修改服务的配置或停止/启动服务。这更加隐蔽。当然,这确实意味着任务运行的令牌可能被剥离权限等,但这是很容易处理的事情(只要它不是写限制的)。

这是一个很好的教训,告诉我们不要只看表面价值。我只是假设调用者需要管理员权限才能将服务账户设置为任务的主体。但如果你深入研究代码,似乎实际上并不需要。希望有人会发现这有用。

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计