« Back to home

Inter-Realm Key Roasting (well... within the first 30 days)

Over the last few weeks I've been working on building some Windows AD lab environments, and as you may expect, part of this process included creating trusts between AD forests. During the numerous (numerous!!) rebuilds that took place, I started to look at the different methods available for creating domain trusts, from Powershell to DSC, to .NET, and manually via MMC.

One area that I did find interesting while configuring trusts was the different ways in which keys were requested during configuration, and how they were used to protect Kerberos tickets. Ultimately I wanted to understand if a weak key could be recovered without relying on admin access to a Domain Controller.

So in this blog post we will look at a somewhat familiar, but extremely limited window of opportunity which may come in handy when reviewing a fresh Active Directory forest deployment. I'll try to show just how this works from a low-level perspective which will hopefully help to visualise the use of AD Domain trust keys, and how they are used when passing tickets between domains.

So let's start by picking on 2 methods typically used to create a domain trust. The first and arguably the most useful method for automation is using DSC, with code such as:

ADDomainTrust 'Trust'
{
  Ensure           = 'Present'
  SourceDomainName = $SourceDomain
  TargetDomainName = $TargetDomain
  TargetCredential = $TargetDomainAdminCred
  TrustDirection   = 'Bidirectional'
  TrustType        = 'Forest'
}

The second method is of course using the "Domains and Trusts" dialog that we have all likely seen when setting up a lab manually, and appears like this:

Now for those who haven't reviewed this kind of environment on an engagement, one of the interesting things associated with a trust created between forests is something called the "Inter-Realm Trust Key", sometimes referred to as a "Trust Password". This key is used when transferring Kerberos tickets between two "realms" or in our case, domains or forests. In other posts you may have seen these keys being extracted using the Mimikatz sekurlsa::trust command from a compromised Domain Controller, and being leveraged in a similar way to golden tickets (while dodging SID Filtering).

But before we get too ahead of ourselves, we need to understand just how this key is provisioned when creating a trust.

DSC/.NET Inter-Realm Trust Key Generation

Let's start with how a key is created when provisioning a Domain Controller using something like DSC. In the source code of the DSC module ActiveDirectory, we want to look at the Set-TargetResource function which is used to apply a configuration. This can be found on Github here.

Within this function we need to see just how Powershell is being leveraged to create the trust. With a bit of digging we see that code similar to the following is used:

$trustSource = [System.DirectoryServices.ActiveDirectory.Forest]::GetForest($DirectoryContext)
...
$trustSource.CreateTrustRelationship($trustTarget, $TrustDirection)

So here we see that the DSC module is actually wrapping a call to the .NET method Forest.CreateTrustRelationship. Searching, we can actually see that this is a common pattern used when attempting to create a trust using Powershell.

Let's load this assembly into dnSPY and take a look at the .NET method to see what it actually does behind the scenes:

Here we see that a password is generated for the trust using the method TrustHelper.CreateTrustPassword. Digging into this we see the following:

Hopefully you can see what this is doing, but for those who avoid C#, this is simply taking 15 random bytes (PASSWORD_LENGTH is set to 15) and ensuring that each byte translates to either a uppercase or lowercase character, number, or symbol. This of course means that the initial Inter-Realm Key generated is set to a 15 character random password when a trust is created using Forest.CreateTrustRelationship. Crypto really isn't my thing, so I'll let someone else determine just how effective this code is at creating a strong random password, but if you want to see sample output returned from this method, a trivial way would be something like:

using System;

namespace DomainTrustPasswordGen
{
    class Program
    {
        static void Main(string[] args)
        {
            var ass = System.Reflection.Assembly.GetAssembly(typeof(System.DirectoryServices.ActiveDirectory.Domain));
            var type = ass.GetType("System.DirectoryServices.ActiveDirectory.TrustHelper");
            var method = type.GetMethod("CreateTrustPassword", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static);

            for(int i=0; i < 100; i++)
            {
                Console.WriteLine("{0}", method.Invoke(null, null));
            }
        }
    }
}

Which will give you a sample of random credentials that this method would create:

Anyway, I digress, let's have a look at what happens if we use the dialog box to create a trust.

Dialog Inter-Realm Trust Key Generation

So if we decide to avoid DSC or Powershell automation, what happens when we manually create a domain trust? Well depending on the path taken we can find ourselves at a number of different prompts, but if we follow the defaults, we typically land at a screen such as:

Here we are prompted to provide a password which is used to create the trust. Let's provide a password of "Password2" and repeat the same steps on a second domain controller to establish the trust:

Now what happens if we dump the keys using Mimikatz:

Cool, so we actually see that this password is used directly as the Inter-Realm Trust Key. So now we know how to create the trust, just how is this key used by Kerberos?

How Kerberos uses the Inter-Realm Trust Key

Let's take a look at a simplified (excluding Pre-Auth) Kerberos journey that occurs when a user wants to request a service ticket to a service hosted within another realm:

To expand on each step further:

  1. AS-REQ - A request for a TGT is made to the first.local DC.
  2. AS-REP - A TGT is returned for the authenticated user.
  3. TGS-REQ - A service ticket is requested for a provided SPN within the second.local realm.
  4. TGS-REP - The first.local DC does not hold krbtgt hash for second.local, so instead it returns a TGT encrypted using the Inter-Realm Trust Key.
  5. TGS-REQ - The TGT returned from first.local encrypted with Inter-Realm Trust Key is passed to the second.local DC, requesting a service ticket for SPN.
  6. TGS-REP - The TGT is decrypted using the Inter-Realm Trust Key, validated, and a service ticket returned which can then be passed onto the service.

So here we can see that in order to allow access to the service hosted on the "second.local" domain, a TGT is returned within a TGS-REP signed with the Inter-Realm Trust Key. So what does this mean if we have a weak key such as that provided within the dialog above? Well it means that the Inter-Realm Trust Key may be susceptible to the same type of Kerberoasting attacks we have been leveraging for a while.

Roasting the Inter-Realm Trust Key

Now before we get going, there are a number of caveats. First, this is obviously only feasible if the key has been provided and is weak, so those admins using DSC/Powershell to create the trust are not going to be susceptible as it will generate a random key unlikely to be bruteforced.

Second, and most importantly, after an initial key has been set, the Netlogon service will cycle the key every 30 days. Once this period has passed, a randomly generated key is used. This means that the steps shown below will only be useful if you fall within the first 30 days of a new trust being created. That being said, if you are lucky enough to land on a domain and can use this in practice, let me know ;)

To see if there are any newly created domain trusts within an environment, it is possible to search LDAP for trustedDomain objectClasses and extract the whenCreated property. For example, if we use the ActiveDirectory Powershell module, we can find the created date of a domain trust using:

Get-ADObject -LDAPFilter '(objectClass=trustedDomain)' -Properties whenCreated

As I've done previously in similar posts, let's walk through the roasting process manually from a packet capture so we know just what is happening at the protocol level. First we need to kick off Wireshark and make a request for a cross-realm service. This is easy enough by making a request to something like \\foreign-forest.local\SHARE while Wireshark is capturing traffic.

Within the capture you will see the usual pattern of AS-REQ, AS-REP, TGS-REQ, TGS-REP. Taking the TGS-REP which will contain a ticket for krbtgt/foreign-forest.local, we want to extract the returned encrypted contents within enc-part (note the etype of 23 showing that this is encrypted using RC4):

Now we need to find a way to crack this. It's likely that JohnTheRipper will be fine using the krb5tgs rule, but we should validate that this is the case. First we can look at the source found here and see just how JTR validates a bruteforced key when dealing with a service ticket:

/*
    8 first bytes are nonce, then ASN1 structures
    (DER encoding: type-length-data)
    if length >= 128 bytes:
        length is on 2 bytes and type is
        \x63\x82 (encode_krb5_enc_tkt_part)
        and data is an ASN1 sequence \x30\x82
    else:
        length is on 1 byte and type is \x63\x81
        and data is an ASN1 sequence \x30\x81
    next headers follow the same ASN1 "type-length-data" scheme
*/

So essentially, if a valid key is found, the decrypted contents should reflect the ASN1 structure searched for above.

Looking at Wireshark we can verify that this will still be the case by decrypting the TGS-REP ticket. To do this you'll need to create a keytab which is possible using ktpass on Windows with the following arguments:

ktpass /out test.keytab /princ krbtgt@lab.local /crypto RC4-HMAC-NT /ptype KRB5_NT_PRINCIPAL /password Password2

You can then set the protocol preferences to decrypt KRB5 packets within Wireshark:

And hopefully we can confirm that the decrypted contents will still matched JTR's ASN1 signature:

Now that we know that JTR will bruteforce the Inter-Realm Trust Key the same as it does with service account credentials, we need to convert the encrypted data into the required format. For JTR this will be the following:

$krb5tgs$23$*username$DOMAIN$SPN*$First-16-Bytes$Remaining-Bytes

So if we take a sample TGS-REP, we will need to extract the following:

This will create a hash format of:

$krb5tgs$23$*xpn$SECOND.LAB$krbtgt/LAB.LOCAL*$8d9411d2a0e38ffb698d3f9e06bcfaca$06bf41bb8a31ba5313fa745bd2be3b642911d4fe21b2cd8e86242e68f364dcdedbcb368bc9d58804c47ca2280ee188438879597e835259967c878487c699b36887a063bb53294f3596a866178b12e5f5fed4681ac3cfa39abee06a1fc4b7bc61864abe2900ac3669b0735da24306258acd0042525bb73c99b1a1c8541a15e5a3809f60d6450303551303fb20ee4f16bb53d2f1a331ca80cda2105223ef7aa6878284f7468f8081af042aecccc084413133e1f41dd7e7ec276f5acf149738e37b60a0832938dbbcfd2d4c26380c808c7c7d0abedbf31ddc63a6923b7f7905fb619465589bd5c7132931df53adc8763b495896efe442b25a84259ebbf99f1540e78125d7c98f3662bf4eda09f83e362fdc74dd8caf7745810dc51f2d602240536ef8a90d23aa78e2e0ab2a4af6ed6c8ee60abc405c726214158a33562eefa8a17f2128cd46585478da04617ff9e9908d7619c85a4c29f26740e5c071cfc3305316b72a603cc7240499104f8942d303a9a8d0b9de6721d7d3578a39b7a6c96b505de47467f6294cef22fb02f075200bfac972d92674716d028bdf6539d34950b5fa4ae22e520b5710ac1643270a63fab30521b4b582c3120cd82647a69f54bb82ec83631cd60443814d763a860b36954f1d590ed470f0c6524c9caf987bd7d34d880121b6c5ce7538c60f38c475784e8fb74c76d04e6ed5e9b0dfd357c964a2ced2aad7b55027ba96e84781f74e07abe6921f0ec2af66ffc05f05fffc8c897994f2ab179ebf152d2894a948244dd2d969df33e2fc05fb95b320a35c43f97e3ab8e8de83f5e7cd3893c4d09ecfa0bbe6c390a8ed26d13c7ee59317d28d71236349928949813f96f57defd6d9d3941001cffefd98f4b5225a295f5a773608692e30e8ff56af261213c75a8ac586fdcfd1545c2bb5ba2576e2eaf181c85a646c2d9a1811ef9bd50e82f35384398c8211e1ed738900e1277656d30330c346ca57f743a7cb52c9bb2e0a4fa6d08fb1359010fbf7156852b3dfb618fc8f27fa01aa20f8a8842b860a5178a3f279f145af112cad5d0ee165913176e490d606c620371469737a483ce315fa4cd868366580f2c02a7519d396a5008f726d65d4854af0c2d8dd85c71924ab4ba99c23621fccd38472916c1ec014a165ae1ba530154187f1815d2fbb5955192b549060fa33abbaa146c78d221fbb8677339d6f46155ced7e671cb9041bab4ccea3e88dc490186c2bbc30fbeb2756bba9e971e7cf11ef1b3bf69735511280d86118b863e30309b6d7d8af2f869f7afdc29fbf88854e5421d138e1a766e9bcfc488b25f9de2aa88af2e28a8f0f99beef949f318f6c6ce0164dac904df5fefff5aef4a17adaa14349906bb24040dd77bc6626213fdf2689755432e8bbf47c20d2105b1b9aa083d2266c3e39d58232abb97ee5ae

Once constructed, we should be able to pass this to JTR as you would during Kerberoasting:

./run/john --wordlist=/usr/share/wordlists/kerberos.txt ~/inter-realm.txt

And if the the password is weak, the stars are aligned, your left shoe is off, and you are spinning on your right leg... you may see the retrieval of the key:

It is worth noting that by default, the TGT encrypted with the Inter-Realm Key will use RC4 rather than AES256 due to the following option defaulting to off:

If this option is later selected, we see that tickets are predictably encrypted using AES256:

As for how we can now leverage the recovery of this key, I'll defer to brighter minds who have covered this topic in detail: