Koh: capture of user credential material
Koh
Koh is a C# and Beacon Object File (BOF) toolset that allows for the capture of user credential material via purposeful token/logon session leakage.
Technical Background
When a new logon session is estabslished on a system, a new token for the logon session is created by LSASS using the NtCreateToken() API call and returned by 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 the information described in the Why This Is Possible section, Windows systems will NOT release a logon session if a token handle still exists to it (and therefore the reference count != 0).
So if we can get a handle to a newly created logon session via a token, we can keep that logon session open and later impersonate that token to utilize any cached credentials it contains.
Why This Is Possible
According to this post by a Microsoft engineer:
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 system. If the tokens associated with a given logon session are never released, then the system now also has a permanent logon session leak as well.
MS16-111 was applied back to Windows 7/Server 2008, so this approach should be effective for everything except Server 2003 systems.
Approach
Enumerating logon sessions is easy (from an elevated context) through the use of the LsaEnumerateLogonSessions() Win32 API. What is more difficult is taking a specific logon session identifier (LUID) and somehow getting a usable token linked to that session.
Possible Approaches
We brainstormed a few ways to a) hold open logon sessions and b) abuse this for token impersonation/use of cached credentials.
- The first approach was to 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 which isn’t ideal.
- One possibility was to add SeCreateTokenPrivilege to NT AUTHORITY\SYSTEM via LSA policy modification, but this would need a reboot/new logon session to express the new user rights.
- 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 apparently demonstrated by Ryan.
- Unfortunately this misses newly created local sessions and incoming sessions created from things like PSEXEC.
- 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.
- This requires opening up lots of processes/handles, which looks very suspicious.
- The AcquireCredentialsHandle()/InitializeSecurityContext()/AcceptSecurityContext() approach described below, which is what we went with.
Our Approach
The SSPI AcquireCredentialsHandle() call 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 SeTcbPrivilege, however, this is usually 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 from being released. However, we’re not presented with another problem: given a “leaked”/held open 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 an LUID to a usable token.
However we can use two additional SSPI functions, InitializeSecurityContext() and AcceptSecurityContext() to act as client and server to ourselves, negotiating a new security context that we can then use with QuerySecurityContextToken() to get a usable token. This was documented in KB180548 (mirrored by PKISolutions here) for the purposes of credential validation. This 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 specific groups we want to target.
Advantages/Disadvantages Versus Traditional Credential Extraction
Advantages
- Works for both local 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 legitimately 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, an existing ticket/credential extraction can still be done on the leaked logon session.
- May cause instability if a large number of sessions are leaked (though this can be mitigated with token group SID filtering) and restricting the maximum number of captured tokens (default of 1000 here).
Install & Use
Copyright (c) 2022, Will Schroeder