Rubeus – Now With More Kekeo

Rubeus, my C# port of some of features from @gentilkiwi‘s Kekeo toolset, already has a few new updates in its 1.1.0 release, and another new feature in its 1.2.0 release. This post will cover the main new features as well as any miscellaneous changes, and will dive a bit into the coolest new features- fake delegation TGTs and Kerberos based password changes.

As before, I want to stress that @gentilkiwi is the originator of these techniques, and this is project is only a reimplementation of his work. If it wasn’t for Kekeo, I would never have been able to figure out these attack components. I’ve found that I don’t truly understand something unless I implement it, hence my continuing march of re-implementing parts of Kekeo. I’ll try my best to explain what’s going on under the hood for the tgt::deleg/tgtdeleg and the misc::changepw/changepw functions (Kekeo and Rubeus) so everyone understands what Benjamin implemented.

https://twitter.com/gentilkiwi/status/998219775485661184
https://twitter.com/gentilkiwi/status/554806023786336257

But first, a bit of background that helped clarify a few things for me.

From TGTs to .kirbis

As most of us have traditionally understood it, in the Kerberos exchange you use a hash (ntlm/rc4_hmac, aes128_cts_hmac, aes256_cts_hmac. .etc) to get a ticket-granting-ticket (TGT) from the domain controller, also known as the KDC (Key Distribution Center.) The exchange, in Kerberos language, involves a AS-REQ (the authentication service request) to authenticate to the KDC/DC, which (if successful) results in an AS-REP (authentication service reply) which contains the TGT. However, this is not all all that it contains.

Big (possibly not self-obvious note): TGTs are useless on their own. TGTs are opaque blobs encrypted/signed with the Kerberos service’s (krbtgt) hash, so we can’t decode them as a regular user. So how are TGTs actually used?

On successful authentication for a user (AS-REQ), TGT is not the only blob of data returned in the AS-REP. There is also an “enc-part” which is a tagged EncKDCRepPart structure that is encrypted with the user’s hash. The hash format used (rc4_hmac, aes256_cts_hmac_sha1, etc.) is negotiated during the initial exchange. When this blob is decrypted, it includes a set of metadata including things like starttime, endtime, and renew-till for the ticket, but most importantly also includes a session key that’s also present in the opaque TGT blob (which is again encrypted with the krbtgt hash.)

So how does a user/machine “use” a TGT? It presents the TGT along with an Authenticator encrypted with the session key- this proves the client knows the session key returned in the initial authentication exchange (and therefore also contained in the TGT.) This is needed for TGT renewals, service ticket requests, S4U requests, etc.

Overall this makes sense! ;)

All of this data is contained within a KRB-CRED structure. This is a .kirbi file in Mimikatz language and represents the encoded structure of the full Kerberos credential that’s submittable though the established LSA APIs. So when we talk about “TGTs”, we actually mean usable TGT .kirbi files (that contain the plaintext session key), NOT just the TGT blob. This is an important distinction that we’ll cover in more depth in a bit.

Also, I want to quickly cover the differences in extracting Kerberos tickets from elevated (high-integrity) and non-elevated contexts. The Rubeus.exe dump command will take the appropriate approach depending on the integrity level Rubeus is executing under.

If you are elevated, the general approach is:

  1. Elevate to SYSTEM privileges.
  2. Register a fake logon process using LsaRegisterLogonProcess() (why we need SYSTEM privileges). This returns a privileged handle to the LSA server.
  3. Enumerate current logon sessions with LsaEnumerateLogonSessions().
  4. For each logon session, build a KERB_QUERY_TKT_CACHE_REQUEST structure specifying the logon session ID of the logon session and a message type of KerbQueryTicketCacheMessage. This returns information about all of the cached Kerberos tickets for the specified user logon session.
  5. Call LsaCallAuthenticationPackage() with the KERB_QUERY_TKT_CACHE_REQUEST and parse the resulting ticket cache information.
  6. For each bit of ticket information from the cache, build a KERB_RETRIEVE_TKT_REQUEST structure with a message type of KerbRetrieveEncodedTicketMessage, the logon session ID we’re currently iterating over, and the target server (i.e. SPN) of the ticket from the cache we’re iterating over. This indicates that we want an encoded KRB-CRED (.kirbi) blob for the specific ticket from the cache. PS- this was a bit annoying to figure out in C# ;)
  7. Call LsaCallAuthenticationPackage() with the KERB_RETRIEVE_TKT_REQUEST and parse the resulting ticket .kirbi information.

This will return complete .kirbi blobs for all TGT and service tickets for all users currently on the system, without opening up a read handle to LSASS. Alternatively you can use Mimikatz’ sekurlsa::tickets /export command to export all Kerberos tickets directly from LSASS’ memory, but remember that this isn’t the only method :)

https://twitter.com/gentilkiwi/status/1032270189444911104

If you are in a non-elevated context, the approach is a bit different:

  1. Open up an untrusted connection to LSA with LsaConnectUntrusted().
  2. Build a KERB_QUERY_TKT_CACHE_REQUEST with a message type of KerbQueryTicketCacheMessage. This returns information about all of the cached Kerberos tickets for the current user’s logon session.
  3. Call LsaCallAuthenticationPackage() with the KERB_QUERY_TKT_CACHE_REQUEST and parse the resulting ticket cache information.
  4. For each bit of ticket information from the cache, build a KERB_RETRIEVE_TKT_REQUEST structure with a message type of KerbRetrieveEncodedTicketMessage and the target server (i.e. SPN) of the ticket from the cache we’re iterating over. This indicates that we want an encoded KRB-CRED (.kirbi) blob for the specific ticket from the cache.
  5. Call LsaCallAuthenticationPackage() with the KERB_RETRIEVE_TKT_REQUEST and parse the resulting ticket .kirbi information.

When you are not elevated, you can logically only query for tickets from your current logon session. Also, in Win7+, Windows restricts the retrieval of TGT session keys when querying from userland, so you’ll get something like this when dumping TGTs:

This means that without elevation, you can not extract usable TGT .kirbis for the current user, as the required session key is nulled. Also, as mentioned in the Mimikatz output above, Microsoft introduced a registry key (allowtgtsessionkey) that allows TGT session keys to be returned. However, this key is not enabled by default, and requires elevation to change.

The tgtdeleg section below explains Benjamin’s trick in getting around this restriction.

However, session keys ARE returned for service tickets. This will be important later.

asktgs

The first “big” new feature is generic service ticket requests:

Rubeus.exe asktgs </ticket:BASE64 | /ticket:FILE.KIRBI> </service:SPN1,SPN2,...> [/dc:DOMAIN_CONTROLLER] [/ptt]

The asktgs action accepts the same /dc:X /ptt parameters as the asktgt. The /ticket:X accepts the same base64 encoding of a .kirbi file or the path to a .kirbi file on disk. This ticket needs to be a .kirbi representation of a TGT (complete with session key, as described earlier) so we can properly request a service ticket in a TGS-REQ/TGS-REP exchange.

The /service:SPN parameter is required, and specifies what service principal name (SPN) that you’re requesting a service ticket for. This parameter accepts one or more common separated SPNs, so the following will work:

C:\Temp\tickets>Rubeus.exe asktgt /user:harmj0y /rc4:2b576acbe6bcfda7294d6bd18041b8fe

   ______        _
  (_____ \      | |
   _____) )_   _| |__  _____ _   _  ___
  |  __  /| | | |  _ \| ___ | | | |/___)
  | |  \ \| |_| | |_) ) ____| |_| |___ |
  |_|   |_|____/|____/|_____)____/(___/

  v1.0.0

[*] Action: Ask TGT

[*] Using rc4_hmac hash: 2b576acbe6bcfda7294d6bd18041b8fe
[*] Using domain controller: PRIMARY.testlab.local (192.168.52.100)
[*] Building AS-REQ (w/ preauth) for: 'testlab.local\harmj0y'
[*] Connecting to 192.168.52.100:88
[*] Sent 232 bytes
[*] Received 1405 bytes
[+] TGT request successful!
[*] base64(ticket.kirbi):

      doIFFjCCBRKgAwIBBa...(snip)...

C:\Temp\tickets>Rubeus.exe asktgs /ticket:doIFFjCCBRKgAwIBBa...(snip...)== /service:LDAP/primary.testlab.local,cifs/primary.testlab.local /ptt

   ______        _
  (_____ \      | |
   _____) )_   _| |__  _____ _   _  ___
  |  __  /| | | |  _ \| ___ | | | |/___)
  | |  \ \| |_| | |_) ) ____| |_| |___ |
  |_|   |_|____/|____/|_____)____/(___/

  v1.0.0

[*] Action: Ask TGS

[*] Using domain controller: PRIMARY.testlab.local (192.168.52.100)
[*] Building TGS-REQ request for: 'LDAP/primary.testlab.local'
[*] Connecting to 192.168.52.100:88
[*] Sent 1384 bytes
[*] Received 1430 bytes
[+] TGS request successful!
[*] base64(ticket.kirbi):

      doIFSjCCBUagAwIBBaEDA...(snip)...

[*] Action: Import Ticket
[+] Ticket successfully imported!

[*] Action: Ask TGS

[*] Using domain controller: PRIMARY.testlab.local (192.168.52.100)
[*] Building TGS-REQ request for: 'cifs/primary.testlab.local'
[*] Connecting to 192.168.52.100:88
[*] Sent 1384 bytes
[*] Received 1430 bytes
[+] TGS request successful!
[*] base64(ticket.kirbi):

      doIFSjCCBUagAwIBBaEDAgE...(snip)...

[*] Action: Import Ticket
[+] Ticket successfully imported!


C:\Temp\tickets>C:\Windows\System32\klist.exe tickets

Current LogonId is 0:0x570ba

Cached Tickets: (2)

#0>     Client: harmj0y @ TESTLAB.LOCAL
        Server: cifs/primary.testlab.local @ TESTLAB.LOCAL
        KerbTicket Encryption Type: AES-256-CTS-HMAC-SHA1-96
        Ticket Flags 0x40a50000 -> forwardable renewable pre_authent ok_as_delegate name_canonicalize
        Start Time: 9/30/2018 18:17:55 (local)
        End Time:   9/30/2018 23:17:01 (local)
        Renew Time: 10/7/2018 18:17:01 (local)
        Session Key Type: AES-128-CTS-HMAC-SHA1-96
        Cache Flags: 0
        Kdc Called:

#1>     Client: harmj0y @ TESTLAB.LOCAL
        Server: LDAP/primary.testlab.local @ TESTLAB.LOCAL
        KerbTicket Encryption Type: AES-256-CTS-HMAC-SHA1-96
        Ticket Flags 0x40a50000 -> forwardable renewable pre_authent ok_as_delegate name_canonicalize
        Start Time: 9/30/2018 18:17:55 (local)
        End Time:   9/30/2018 23:17:01 (local)
        Renew Time: 10/7/2018 18:17:01 (local)
        Session Key Type: AES-128-CTS-HMAC-SHA1-96
        Cache Flags: 0
        Kdc Called:

Operationally, if you are not elevated and don’t want to stomp on your existing logon session’s TGT by applying a new TGT as described last week, you can request a TGT for a given account and use this blob with asktgs to request/apply just the service tickets needed.

For more information on service ticket takeover primitives, see the “Service to Silver Ticket Reference” of Sean Metcalf‘s “How Attackers Use Kerberos Silver Tickets to Exploit Systems” post.

tgtdeleg

The tgtdeleg feature is a re-coded version of Kekeo’s tgt::deleg function, and allows you to extract a usable TGT .kirbi from the current user without elevation on the system. This is a very cool trick discovered by Benjamin that I will attempt to explain in detail, covering operational use cases at the end.

There’s something called the Generic Security Service Application Program Interface (GSS-API) which is a generic API used by applications to interact with security services. While Microsoft does not officially support the GSS-API, it does implement the Kerberos Security Service Provider Interface (SSPI) which is wire-compatible with the Kerberos GSS-API, meaning it should support all the common Kerberos GSS-API structures/approaches. We’ll use RFC4121 as our reference at various points in this post.

Basically, as a tl;dr, you can use Windows APIs to request a delegate TGT that’s intended to be sent to a remote host/SPN by way of SSPI/GSS-API. One of these structures contains a forwarded TGT for the current user in a KRB-CRED (.kirbi) structure that’s encrypted within the AP-REQ intended to be sent to the target server. The session key used to encrypt the Authenticator/KRB-CRED is contained within a service ticket for the target SPN that’s cached in an accessible location. Combining this all together, we can extract this usable TGT for the current user without any elevation!

First, we use AcquireCredentialsHandle() to acquire a handle to the current user’s existing Kerberos credentials. We want to specify SECPKG_CRED_OUTBOUND for the fCredentialUse parameter which will, “Allow a local client credential to prepare an outgoing token.

Then we use InitializeSecurityContext() to initiate a “client side, outbound security context” from the credential handle returned by AcquireCredentialsHandle(). The trick here is to specify the ISC_REQ_DELEGATE and ISC_REQ_MUTUAL_AUTH flags for the fContextReq parameter. This requests a delegate TGT, meaning “The server can use the context to authenticate to other servers as the client.” We also specify a SPN for a server with unconstrained delegation (by default HOST/DC.domain.com) for the pszTargetName. This is the SPN/server we’re pretending to prep a delegation request for.

So what happens when we trigger this API call?

First, a TGS-REQ/TGS-REP exchange occurs to request a service ticket for the SPN we’re pretending to delegate to. This is so a shared session key can be established between the target server and the machine we’re communicating from. This service ticket is stored in the local Kerberos cache, meaning we can later extract the shared session key.

Next, a forwarded TGT is requested for the current user. For more information on forwarded tickets, see the “What are forwarded tickets?” section here. The KDC will return this new TGT with a separate session key from the current TGT. The system then uses this forwarded TGT to build an AP-REQ for the target server, where the Authenticator within the request contains the usable KRB-CRED encoding of the forwarded TGT. This is explained in section “4.1.1. Authenticator Checksum” of RFC4121:

https://tools.ietf.org/html/rfc4121#section-4.1.1

The end result? If everything is successful we get the AP-REQ (including the .kirbi of the new TGT) encoded in an SSPI SecBuffer structure pointed to by the pOutput pointer passed to InitializeSecurityContext(). We can search the output stream for the KerberosV5 OID and extract the AP-REQ from the GSS-API output.

We can then extract the service ticket session key from the cache and use this to decrypt the Authenticator extracted from the AP-REQ. Finally we can pull the encoded KRB-CRED from the Authenticator checksum and output this as our usable TGT .kirbi:

Success! \m/

From an operational standpoint, this is a bit of a niche feature. The main situation I could think of where this would be useful is where you have multiple agents in an environment where there is at least one machine you could not elevate on. From this machine, you could extract the current user’s TGT using Rubeus’ tgtdeleg, and pass this to the Rubeus renew function running on another machine along with the /autorenew flag. This would allow you to extract the user’s credential without elevation and keep it alive on another system for reuse, for up to 7 days (by default.)

Regardless of whether this TTP is incredibly useful, it was a ton of fun to understand and recode :)

changepw

The changepw action (misc::changepw in Kekeo) implements a version of the @Aorato POC that allows an attacker to change a user’s plaintext password (without knowing the previous value) from a TGT .kirbi. Combining this with asktgt and a user’s rc4_hmac/aes128_cts_hmac_sha1/aes256_cts_hmac_sha1 hash this means that an attacker easily force reset a user’s plaintext password from just their hash. Alternatively, if the Rubeus dump command is used (from an elevated context), an attacker can force reset a user’s password solely from ticket extraction through LSA APIs.

The RFC that explains this process is RFC3244 (Microsoft Windows 2000 Kerberos Change Password and Set Password Protocols.) Here’s the diagram of what’s sent to port 464 (kpasswd) on a domain controller:

There are two main required parts here: an AP-REQ and specially constructed KRB-PRIV ASN.1 structure. The AP-REQ message contains the user’s TGT blob, and an Authenticator encrypted with the TGT session key from the TGT .kirbi. The Authenticator must have a randomized sub session key set that’s used to encrypt the following KRB-PRIV structure. The KRB-PRIV contains the new plaintext password, a sequence/nonce, and the sender’s host address (which can be anything.)

If the password set is successful, a KRB-PRIV structure is returned with a result code of 0 (KRB5_KPASSWD_SUCCESS.) Errors are either reflected in KRB-ERROR or other error codes (specified at the end of section 2 in RFC3244.)

Note: I’m not sure why, but ticket retrieved with the tgtdeleg trick can not be used with this changepw approach, returning a KRB5_KPASSWD_MALFORMED error. I tested this with both Rubeus and Kekeo, each with the same result ¯\_(ツ)_/¯

Miscellaneous Changes

Other random changes/fixes:

  • The s4u action now accepts multiple alternate snames (/altservice:X,Y,…)
    • This executes the S4U2self/S4U2proxy process only once, and substitutes the multiple alternate service names into the final resulting service ticket structure(s) for as many snames as specified.
  • Corrected encType extraction for the hash output for the kerberoast action, and properly attributed @machosec for the KerberosRequestorSecurityToken.GetRequest approach.
  • Fixed the salt demarcation line for asreproast hashes and added an eventual Hashcat hash output format.
  • Fixed a bug in the dump action – full ServiceName/TargetName strings and now properly extracted.
  • I also added a Keep a Changelog based CHANGELOG.md to keep track of current and future changes.

Leave a Comment

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.