标题 简介 类型 公开时间
关联规则 关联知识 关联工具 关联文档 关联抓包
参考1(官网)
参考2
参考3
详情
[SAFE-ID: JIWO-2024-3145]   作者: 大猪 发表于: [2022-07-22]

本文共 [608] 位读者顶过

Koh: The Token Stealer

Edit 07/13/22: After an awesome back and forth with 

 and @SteveSyfuhs on Twitter on the effects of “TokenLeakDetectDelaySecs” and “Protected Users” for mitigating token theft, I’ve updated the Koh README to reflect these mitigations.

Years ago I was chatting with a few experienced red teamers and one was lamenting token abuse. Specifically, they wanted to be able to automatically “harvest” tokens on a host as people connected, keeping the tokens usable for operators even after the associated account logged off. I knew very little about Windows authentication at the time, so when the other red teamer investigated the idea and told us it wasn’t possible, I left it at that. Before September 2016 they were (probably? maybe?) correct. Since then, however, it’s a different story.

In this post I will introduce a toolkit called Koh that can indefinitely (at least I think? someone smarter please let me know if this isn’t the case) harvest and reuse tokens for accounts that connect to a machine you have administrative rights on. I’ll go over the motivation for this approach, the technical background of why it’s possible and what changed in 2016, and briefly show what Koh can do.

Motivation

LSASS, oh St. LSASS, the eternal bestower of credentials. Attackers want access. This naturally leads to the desire to pillage credentials from a host for lateral movement, persistence, and other reasons, and LSASS has been a great source of plaintext credentials and hashes for years. The most notorious of the credential dumpers is the (in)famous Mimikatz, built by the venerable Benjamin Delpy (to teach himself C, yes, the legends are true). When vendors state “We stop Mimikatz” more often than not they’re referring to protections against LSASS credential extraction.

(Not) surprisingly, the advertising isn’t always 100% accurate:

Point being that the cat and mouse game for LSASS abuse is certainly afoot, and while various offensive workarounds have publicly surfaced over the past several years (MiniDumpWriteDumpSyscall methodsPssCaptureSnapShotreusing existing LSASS handlesmemory shenanigans, offensive drivers, yes-I-know-I’m-missing-other-methods-and-this-list-isn’t-complete) as well as private methods I’m sure exist on internal teams, it’s hard to deny that defensive EDRs have increasingly pushed the offensive industry into more of a corner in this regard. Since credential dumping, in particular LSASS abuse, is such a common component of many attack chains it makes reasonable sense for the defensive industry to focus on this as a choke point.

However I would caution any defenders against believing that an EDR will save you in every case, despite whatever the marketing tells you. There are a lot of ways to get credentials off a host beyond just LSASS dumping. We’ve personally been big fans of offensive DPAPI abuse as well as Kerberos ticket extraction, two approaches we’ve been very public about and have built multiple open source tools to facilitate (and of course it’s always been possible in Mimikatz ; )

Due to this defensive pressure, attackers are always on the lookout for new ways to abuse access on a host. Traditionally this has involved various methods to retrieve plaintext passwords, hashes, or Kerberos keys/tickets. More historically, “back in the day” (think 2008) this was tokens, with Luke Jennings’ original release of Incognito being a game changer.

A while ago I was thinking about this cat-and-mouse-game of credential extraction and wanted to revisit this idea of token harvesting. A tool that allows the long-running harvest and reuse of tokens on a host could prove really useful on many engagements, with the obvious caveat that a host rebooting will reset what’s been gathered.

Remember: as offensive operators what we really care about is access. Credentials are just a means to that end, but there are other ways to get there.

Technical Background

Note: I’m going to do my best to explain why I believe this works, with the big caveat that I’m not a great Windows internals person. If I have any mistakes here please let me know and I’ll update the post!

First, a bit of background on tokens, logon sessions, authentication packages, and credentials. Understanding how these all work together is important in order to understand why this attack technique is possible. I shall explain this in slide form:

If this is still a bit confusing, check out Joseph Bialek’s OG blog on tokens/auth packages, Elastic’s excellent “Introduction to Windows tokens for security practitioners” post, the Incognito whitepaper, or join us at our Adversary Tactics: Red Team Operations course (where these slides are from) that we will be teaching in person at Black Hat USA this year.

Tokens act as our Windows-approved bridge to utilize impersonated credentials. However, for many years we offensive operators have had a problem: when we stole/cloned a newly created token for an account that logged onto a host we control, the logon sessions linked to the stolen token would sometimes be destroyed at some point after the user logged off the system. This isn’t ideal offensively, and was one of the many reasons (in my opinion at least) many operators deprioritized token abuse in favor of LSASS credential extraction in the early 2010s.

Note: I didn’t confirm this exact cleanup behavior as I didn’t want to go through the process of setting up an old enough Windows VM, and the old behavior doesn’t really matter after 2016 anyway (which I’ll explain shortly). I also wasn’t able to track down exactly how long logon sessions would originally stick around after a user logged off but a linked token was still leaked — if anyone knows what the old process was or if I’m misunderstanding something please let me know.

This all changed with MS16–111.

Ryan Ries published an amazing post in 2017 titled “Using Debugging Tools to Find Token and Session Leaks”. In it he describes one of the effects of MS16–111 (emphasize mine):

This update is significant because it changes how the relationship between tokens and logon sessions is treated across all supported versions of Windows going forward. Applications and services that erroneously leak tokens have always been with us, but the penalty paid for leaking tokens is now greater than before. After MS16–111, when security tokens are leaked, the logon sessions associated with those security tokens also remain on the system until all associated tokens are closed… even after the user has logged off the systemIf the tokens associated with a given logon session are never released, then the system now also has a permanent logon session leak as well.

This means that if an application doesn’t properly release a token with CloseHandle(), the associated logon session (and therefore any associated cached credentials) should remain on the host indefinitely. Specifically, when a new logon session is established on a system, a new token for the logon session is created by LSASS using the NtCreateToken() API call and returned to the caller of LsaLogonUser(). This increases the ReferenceCount field of the logon session kernel structure. When this ReferenceCount reaches 0, the logon session is destroyed. Because of MS16–111 as described above, Windows now will NOT release a logon session if a token handle is still linked to it and therefore the reference count != 0. Again, this is as I understand it, but I can’t guarantee this explanation is correct.

Offensively, instead of relying upon sloppy code leaking tokens/logon sessions, why don’t we intentionally cause these leaks for the purpose of offensive credential preservation? If we can open a token handle linked to a newly created logon session, we can keep that logon session open and later impersonate that token to utilize any cached credentials it contains. And as the MS16–111 patch was applied to Windows 7/10 and Server 2008/2012, this technique should apply for pretty much all modern Windows systems we encounter.

Approaches

Our goal is to open a token handle linked to a logon session for a user account we want to preserve access for. Previously, the approach for token abuse was often as follows, credit to Joseph Bialek’s OG “PowerShell and Token Impersonation” post for the exact description:

  1. Enumerate all processes
  2. Call OpenProcess to get a handle to each process
  3. Call OpenProcessToken to get the token of the process
  4. Enumerate all threads
  5. Call OpenThread to get a handle to each thread
  6. Call OpenThreadToken to get the token of the thread (if it has one)
  7. Call GetTokenInformation to retrieve information about the token such as if it is elevated, who it belongs to, and what privileges it has

There are two big downsides I see to this approach. First, we’re calling OpenProcess() against every process on the host. This is something that can trigger some EDRs. Second, there can be tokens linked to existing LogonSessions that aren’t actively used by any specific threads, which this approach would miss. That is, an application can have a handle to a token linked to a live logon session, but not be actively using it for impersonation.

Instead, we can easily enumerate all current logon sessions on a host from an elevated context through the use of the LsaEnumerateLogonSessions() Win32 API. Then the tricky part is taking a specific logon session identifier (LUID) and somehow getting a usable token linked to that session.

My coworker Lee and I brainstormed a number of possible approaches for this (if there are other approaches, let me know!):

  1. On a new logon session, open up a handle to every reachable process and enumerate all existing handles, cloning the token linked to the new logon session.[出自:jiwo.org]
    - This requires opening up lots of processes/handles, which may look very suspicious.
  2. You can use NtCreateToken() which allows you to specify a logon session ID (LUID) to create a new token.
    - Unfortunately, you need SeCreateTokenPrivilege which is traditionally only held by LSASS, meaning you need to steal LSASS’ token. Running OpenProcess() to LSASS isn’t ideal. One workaround is to add SeCreateTokenPrivilege to our rights manually but this does require a reboot, which obviously clears out existing tokens.
  3. You can also focus on just RemoteInteractive logon sessions by using WTSQueryUserToken() to get tokens for new desktop sessions to clone.
    -This is the approach I think Ryan used in his post.
    -Unfortunately this misses newly created local sessions and incoming sessions created from things like PSEXEC in some situations.
  4. The Security Support Provider Interface (SSPI) approach described in the next section, which is what we went with.

Our Approach

The SSPI AcquireCredentialsHandle() function has a pvLogonID field which states:

A pointer to a locally unique identifier (LUID) that identifies the user. This parameter is provided for file-system processes such as network redirectors.

Note: In order to utilize a logon session LUID with AcquireCredentialsHandle() you need SE_TCB_NAME (SeTcbPrivilege, e.g., running as SYSTEM) , however this is generally easier to get than SeCreateTokenPrivilege.

Using this call while specifying a logon session ID (LUID) appears to increase the ReferenceCount for the logon session structure, preventing it (and linked credentials) from being released. However, we’re not presented with another problem: given the LUID of a purposely “leaked” logon session, how do we get a usable token from it? WTSQueryUserToken() only works with desktop sessions, and there’s no userland API that we could find that lets you map a LUID to a usable token.

However we can pass the acquired credential handle to two additional SSPI functions, InitializeSecurityContext() and AcceptSecurityContext(), in order to act as client and server to ourselves. This lets us negotiate a new security context that we can supply to the QuerySecurityContextToken() function to get a usable token. This was documented in KB180548 (mirrored by PKISolutions here) for the purposes of credential validation and is a similar approach to Internal-Monologue, except we are completing the entire handshake process, producing a token, and then holding that for later use.

Filtering can then be done on the token itself, via CheckTokenMembership() or GetTokenInformation(). For example, we could release any tokens except for ones belonging to Domain Admins, or other specific groups we want to target.

Advantages

  • Works for both local, NewCredentials (runas /netonly), and inbound (non-network) logons.
  • Works for inbound sessions created via Kerberos and NTLM.
  • Doesn’t require opening up a handle to multiple processes.
  • Doesn’t create a new logon event or logon session.
  • Doesn’t create additional event logs on the DC outside of normal system ticket renewal behavior (I don’t think?).
  • No default lifetime on the tokens (I don’t think?) so access should work as long as the captured account’s credentials don’t change and the system doesn’t reboot.
  • Reuses legitimate captured auth on a system, so should “blend with the noise” reasonably well.

Disadvantages

  • Access is only usable as long as the system doesn’t reboot.
  • Doesn’t let you reuse access on other systems. However, existing ticket/credential extraction should still be possible on the leaked logon session. I.e., you can do Rubeus.exe dump /luid:<LUID> to get a TGT of a Kerberos-authed logon session captured by this approach. I need to confirm this but it appears that tickets are auto-renewed, and our guess is that this is due to user GPO refreshes.
  • May cause instability if a large number of sessions are leaked (though this can be mitigated with token group SID filtering). We currently limit the number of captured tokens to 1000, but we need to test this more thoroughly to see if this is a good setting.

Koh: The Token Stealer

Our tool that implements this, Koh, has two parts: a capture server to negotiate captured tokens for new logon sessions, and a client to utilize the captured tokens. The capture server is currently written in C# and the client is a Cobalt Strike BOF (big surprise, I know) but this approach would be relatively simple to implement in pretty much any language.

Here we can see interacting with Koh from the published BOF, listing the current logon sessions. Note that THESHIRE\localadmin has no processes running as it was previously captured:

Listing captured tokens/logon sessions for a host.

And now, assuming SeImpersonatePrivilege (which we do have in our SYSTEM context), we can impersonate a captured token for a privileged user and reuse the access:

Impersonating a captured user logon session.

Conclusion

As offensive operators, what we usually care about is access. Credentials are just a means to that end, but there are many ways to get there. By getting back to our token abusing roots, we can generalize token theft to its logical next step and harvest access for anyone who connects. We sure as hell didn’t invent token abuse, and most of these pieces existed publicly in various states, however we feel like this approach is an elegant amalgamation of various parts. We hope you find it useful!

We need to experiment more with this in the field to see its effectiveness and operational concerns, so if you have any feedback or problems please let us know in the repo issues.

Note: there is one BIG remaining issue with running the the Koh.exe assembly inline using something like InlineExecute-Assembly or Inject-Assembly. For details check out the README details on the “The Inline Shenanigans Bug”. Please help us figure this out!

References

评论

暂无
发表评论
 返回顶部 
热度(608)
 关注微信