Blog: Vulnerability Disclosure
A broken marriage. Abusing mixed vendor Kerberos stacks
My first DEF CON talk was nerve-racking but something I would definitely put myself through again. In hindsight I should have submitted a 45-minute talk as there were some elements missing from what I presented, based on additional research since submitting the CFP.
With that in mind, and for those that weren’t able to attend, here’s a follow-up blog post.
- MIT or Heimdal Kerberos stacks allow *nix based servers and services to be joined to Active Directory networks
- A by design issue exists with the userPrincipalName attribute belonging to user and computer accounts within Active Directory. Accounts are susceptible to user spoofing when providing Kerberos tickets to *nix based services joined to an Active Directory realm.
- A spoofed Kerberos ticket can be presented to GSSAPI based authentication stacks resulting in privilege escalation on the target host or service.
- Whilst some degree of privilege is required over a single user or computer account within Active Directory to abuse this feature, any user can be spoofed against any service hosted over GSSAPI.
*nix based servers and services can be joined to Active Directory networks in the same way as their Windows counterparts. This is usually facilitated through the MIT or Heimdal Kerberos stacks.
Kerberos is designed as an authentication-based protocol therefore authorisation decisions are implemented independently to the Kerberos protocol itself. Due to this, different vendor stacks behave differently on how authorisation decisions are made.
Windows via SSPI
Windows based servers heavily rely on the Privileged Attribute Certificate (PAC) inside service tickets (ST) for authorising a user to a particular service. This is commonly done centrally via LSASS through the SSPI interface along with the Kerberos pluggable authentication scheme. Without the PAC present within a ST, Windows based services will not authenticate the presented ticket regardless of the validity of the ticket itself.
Figure 1. KERB_VALIDATION_INFO contains the PAC
Among other things, the PAC contains some key elements that allows a Windows service to identify the user within Active Directory. Information such as the user SID, groups, and AD claims belonging to the account at the time of authentication.
- EffectiveName: The samAccountName of the user
- UserId: The RID portion of the users SID
- PrimaryGroupId: The primary group id of the user (typically Domain Users)
- LogonDomainId: The SID of the domain
- GroupIds: The groups within the domain the user belongs to
There are other intricacies when cross domain tickets are generated, but in essence, the attributes above allow Windows services to accurately identify the correct user belonging to a specific ticket.
MIT / Heimdal
Now when it comes to Unix and Linux operating systems, MIT or Heimdal based stacks are typically used, but these stacks are not limited to *nix type machines. Certain cross-platform server software will also use these Kerberos stacks to facilitate authentication. Typically, Kerberos authentication is implemented via GSSAPI within these environments.
These stacks will rely on the server implementor to perform any authorisation decisions once authentication has succeeded. These authorisation decisions are typically a secondary step independent to GSSAPI. This is usually performed by inspecting the Kerberos cname attribute within the Kerberos ticket to determine the authenticated principal name and then performing checks against an LDAP database or a local user database to determine correct authorisation decisions. The GSSPI layer does not perform these steps unlike SSPI on Windows, therefore the implementation of authorisation is left to the vendor of the service.
This is where decisions can be disastrous within Active Directory environments.
The Kerberos PrincipalName type is an ASN.1 structure that defines the name of a principal within a Kerberos realm.
Figure 2. Kerberos PrincipalName type
The PrincipalName type is used for both the cname (client name) and sname (service name) attributes within a Kerberos ticket. The type is comprised of a name string, a string identifying a unique user within the realm and a name type hint, which is merely a hint to the format of the name string provided and is not designed to partition the namespace within the realm.
Figure 3. name-types documented within the Kerberos RFC
Active Directory Principal Search
Active Directory uses a somewhat complex search algorithm to find which user will be used for authentication purposes when searching for principals within the realm. The algorithm can be found here.
I have tried to simply the algorithm in the form of a flow diagram below.
Figure 4. MS-KILE client name search algorithm
You will notice that the search algorithm will inspect both the samAccountName attribute and the userPrincipalName attribute within AD. The other key thing to notice with the search algorithm is the order that the attributes are searched. Depending on the name-type hint will determine which path is taken first. If NT_PRINCIPAL type is used as the hint, samAccountName is processed first. If NT_ENTERPRISE is used as the hint, userPrincipalName is searched first.
The samAccountName attribute is searched twice. Once, using the name-string as provided then a second time with a $ appended to facilitate searching for machine accounts. As you can see, there is already scope for confusion here since Active Directory can contain two separate accounts in AD, one called USER and another called USER$. This is exactly what happened during CVE-2021-42287 and CVE-2021-42278. The issue was discovered by Andrew Bartlett, a lead Samba developer. Charlie Clark did an excellent write-up of the technical details behind it since there was nothing public at the time.
This blog was then later weaponised into a single python script and dubbed Sam the Admin. Later, Andrew released his own blog too, already hinting at some issues with *nix based Kerberos services within Active Directory realms.
Spoofing Active Directory users within GSSAPI
Since samAccountName had been hardened considerably since the fixes of CVE-2021-42287 et al, I decided to look at the userPrincipalName attribute within AD.
Figure 5. User principal name described by Microsoft
A valid user principal name consists of a user account name followed by a @ character and the UPN suffix. Essentially the same format as an email address. The suffix of course is designed to partition the namespace of users similar to how an email address does.
But AD does not allow duplicate userPrincipalName attributes to be set within the database, so how can we spoof other users.
Well as it turns out, providing you have write permission on the Public Information attribute set or Generic Write on any user or computer account, you can set this value to anything and it does not need to conform to a valid UPN. Therefore, we can set this to the value of the samAccountName attribute of another AD account.
Figure 6. Setting UPN to other accounts samAccountName
Paired with the ability to change the search order of the attributes via the name-type hint I described earlier, I wondered if we could authenticate using the username Administrator but the password for john.doe, the account used in the image above.
To facilitate this, a new argument was added to Rubeus’s asktgt command, principaltype. This now enables you to specify the name-type hint during authentication.
Figure 7. Spoofing Administrator using john.doe
And to my surprise, I got a TGT back where Administrator was set as the tickets cname value. So, what could we do with this type of ticket? As it turns out, on Windows services authenticated via SSPI, not a lot. Kerberos authentication via SSPI always inspects the PAC, and since the PAC contains both the samAccountName and unique SID of the user along with all the groups the user is a member of during authentication, no form of privilege escalation was possible. I also tried via requesting a PACless service ticket, which can be done using asktgt and specifically requesting the service required vs the default krbtgt service that would otherwise be provided. The end result was anonymous authentication due to the PACless nature of the service ticket.
But as I have alluded to earlier, Kerberos authentication via GSSAPI does things differently. I joined a CentOS machine to AD using SSSD. SSSD will determine group membership by performing a secondary search within LDAP against the samAccountName attribute. When logging in via SSH and entering domain credentials via the username and password fields, everything was fine. I had to use the domain administrator password when using the [email protected] username. This meant that NT-PRINCIPAL was being used as the name type hint during Kerberos authentication. SSH also supports Kerberos SSO authentication via GSSAPI. This is enabled by default on CentOS and RHES. When presenting a service ticket to the CentOS host via the -K argument using a TGT that was obtained using Rubeus using the NT_ENTERPRISE name-type hint, I was able to login and impersonate the domain and enterprise administrator account.
Figure 8. SSH logging via Kerberos SSO and spoofed account
Now when it came to Ubuntu, things didn’t go to plan. Ubuntu and other Debian based hosts do not have GSSAPI enabled over SSH by default. I decided to go through the same process, add a Ubuntu host to AD but also enable GSSAPI to determine if the same issue existed.
Figure 9. Enabling GSSAPI on Ubuntu sshd_config
And I was still presented with the password input box when attempting Kerberos authentication after making this change, indicating that Kerberos was failing. After some further digging, I discovered a whole heap of mess that can get in the way of Kerberos authentication. Some solvable by an unprivileged user remotely and others only solvable by making changes on the target host, obviously eliminating the feasibility of attacking.
Let’s start with the unsolvable first. Kerberos will perform the equivalent of a hostname -a command when authenticating via SSH to determine the fully qualified host name. The sname inside the ticket, which contains the target SPN seems to be ignored when searching through the hosts keytab. Therefore, if hostname -a does not resolve to a matching key within the host keytab, Kerberos authentication fails. This behaviour can be changed, but they’re the default settings.
If we check the default SPN’s registered for a host called ubuntu joined via realm join, this is what we get.
Figure 10. keytab for computer called ubuntu joined to AD
We can see that the FQDN version is lowercase, but the simple hostname is uppercase. I’m not sure if this is a bug in realm join or if the developers are sticking to Windows based nomenclature when adding computer accounts to AD, but this breaks SSH’s search when hostname -a returns a lowercase simple hostname during SSH connectivity.
ssh -K [email protected]@ubuntu
The fix for simple hostname based connectivity is to insert a lowercase version of SPN inside the keytab, but of course that will stop you in your tracks if the goal is to attack from an unprivileged user perspective remotely.
Now onto the FQDN resolution. By default, both RH and Debian based OS’s use /etc/hosts followed by DNS to resolve host names. RH and derived hosts seem to create the following /etc/hosts by default during installation.
127.0.0.1 localhost localhost.localdomain localhost4 localhost4.locadomain4 ::1 localhost localhost.localdomain localhost6 localhost6.locadomain6
Notice the absence of any hostname other than localhost. Therefore, when SSH performs the hostname lookup, since there is no match in /etc/hosts, a reverse PTR DNS query is performed against the IP address used for SSH connectivity. If a hostname is returned, this is used as the basis of searching the host keytab to match the SPN.
Now on Debian based hosts, you’ll get something that looks like this for /etc/hosts.
127.0.0.1 localhost 127.0.1.1 ubuntu # The following lines are desirable for IPv6 capable hosts ::1 ip6-localhost ip6-loopback fe00::0 ip6-localnet ff00::0 ip6-mcastprefix ff02::1 ip6-allnodes ff02::2 ip6-allrouters
Details can be found here.
Because the simple hostname is present within the hosts file, resolution over DNS is not performed. Therefore, you will always get ubuntu back for the hostname -f command. Now if you are lucky enough that during setup that the correct modifications were made to fully qualify the hostname and domain, you would likely have something like this.
127.0.0.1 localhost 127.0.1.1 ubuntu.ad.ginge.com ubuntu
In this scenario, hostname -a will return ubuntu.ad.ginge.com and the correct principal will be resolved inside the host keytab. So, for authenticating with Kerberos over SSH on Debian based hosts, success will vary depending on how /etc/hosts is populated.
Now for hosts that do not have the corresponding hostname entry inside /etc/hosts, hostname resolution will fall back to DNS. If this is the case, both forward and reverse lookups should resolve to the fully qualified HOST/ SPN record for the computer account in AD, e.g. HOST/ubuntu.ad.ginge.com.
Secure DNS updates
If either the forward or reverse lookup information is missing within DNS, we can solve this via secure DNS updates as an unprivileged user within AD.
To support dynamic DNS updates, default settings will permit any authenticated user to create new records within existing zones inside Active Directory DNS.
Dirk-jan Mollema released this capability as part of the excellent krbrelax tool. dnstool.py performs DNS updates via LDAP. Active Directory integrated zones store DNS information inside LDAP and supports two-way synchronisation. Therefore, if you update DNS records inside LDAP, within 15 minutes or so the record will propagate to DNS.
I didn’t fancy waiting 15 minutes, so started looking at DNS TSIG, which directly performs instant secure updates to DNS records over port 53. Kevin Robertson had already implemented this as part of Powermad, but since I had already started tooling for GSSAPI abuse using python, I was looking for a python solution. As it turns out, dnspython supports DNS TSIG updates in the latest version. So as part of the tooling released for this vector, I have included a dns mode that allows dynamic updates of DNS records within AD DNS.
GSSAPI abuse tool
To help identify potential *nix hosts that can be abused through the research presented here, I have created a tool to speed things along.
gssapi-abuse has two modes. An enumeration mode for identifying *nix based hosts within AD that could potentially be abusable over SSH and of course the dns update mode as described above. You can find the tool here.
SSH is not the only service that can be abused. I also confirmed that Postgres can be confused in the same way if GSSAPI based authentication is enabled. Additionally, I have found the following services all support Kerberos authentication via GSSAPI. Whilst I am yet to check them all, there is a strong chance they are all abusable in the same way providing the service principals belong to an Active Directory based realm.
Figure 11. Other services supporting Kerberos auth via GSSAPI
I reported the issue to MSRC and Microsoft claim that Active Directory is behaving as designed. Reading through the Kerberos RFC, it seems to be clear that principal names should identify the same principal whenever the principal name only differs by the name-type hint. The name-type hint does not partition the principal namespace. Therefore, it seems Microsoft does not currently conform to what the RFC states.
Figure 12. Kerberos RFC excerpt regarding principal names
First and foremost, the best solution is to have good Active Directory hygiene to ensure unprivileged users cannot make changes to the userPrincipalName attribute of any account. Bloodhound is your friend in this scenario for reviewing permissions.
The simplest mitigation if you do not require Kerberos SSO over SSH is to disable GSSAPI within sshd_config. By setting KerberosAuthentication and GSSAPIAuthentication options to no, this will ensure that SSH will not allow authentication via Kerberos service tickets. You will still be able to login with valid domain credentials over SSH, but SSO will not be possible.
For reactive based monitoring, check for changes against the userPrincipalName attribute that do not conform to a valid format. In other words, any update to a userPrincipalName that doesn’t have the @ character and / or domain suffix should be investigated.