Mar 6 2025 |
Decrypting the Forest From the Trees
TL;DR: SCCM forest discovery accounts can be decrypted including accounts used for managing untrusted forests. If the site server is a managed client, service account credentials can be decrypted via the Administration Service API.
Introduction
While Duane Michael, Chris Thompson, and I were originally working on the Misconfiguration Manager project, one of the tasks I took on was to create every possible account in the service and see how many of them could be discovered and extracted. Nearly all of those credentials, at least in a standard deployment, can be recovered using the techniques in CRED-5 that Benjamin Delpy and Adam Chester originally shared. There were accounts though that couldn’t be decrypted the same way and were stored in the SC_UserAccount table in a completely different format.
SCCM provides a number of discovery methods for identifying clients including Active Directory (AD) user and system discovery, network discovery, heartbeat, and forest discovery. Unlike the others that identify users and computers that may need to be managed, the forest discovery’s role is to identify locations that can be added as boundaries for client management by querying the local and trusted forests. The default for this discovery method is to use the site server’s machine account as it is already a member of the forest. However, another option is for administrators to manually add a forest and set credentials for a forest discovery account. This can be even more useful when managing untrusted forests.
Over the last few years of researching and attacking SCCM, the most common issue I’ve observed is that the various service accounts are configured with excessive permissions. I suspect the same is true for forest discovery accounts as well and are likely high value; especially when the objective is to move laterally where no direct path exists between forests. Naturally, I wanted to decrypt them.
Decryption
While manually forcing a forest discovery, I checked the modules loaded in the smsexec.exe service, which handles the bulk of processing tasks executed from the management console, and found a pretty descriptive .NET assembly: ActiveDirectoryForestDiscoveryAgent.dll.
Loading the assembly into dnSpy, the RunDiscovery method kicks off the discovery process. The method starts by first ensuring a database connection exists then generates a list of forests to target for discovery from the SCCM database. Once the list is built, ConnectToForest is called to query the database for each forest’s associated discovery account username.
If one exists, that username is passed to the GetCredentialsWrapper utility method, which is a wrapper for the native GetUserCredentials function imported from the ADForestDisc.dll to acquire the account’s password. The verbose logger shows that whatever is returned from GetCredentialsWrapper should be “successfully obtained credentials…” which is a good sign.
Looking at the ADForestDisc.dll in Ghidra, the GetUserCredentials function ensures a username is set then passes the username to GetGlobalUserAccount before returning the account’s password. The error handling logging message that states “failed to get account information from site control file for the discovery user” is a clue to where the credential material is stored.
According to Microsoft, the site control file (SCF) “…defines the settings for a specific site” and is stored in the SCCM site database. Two control files exist at any given time: the “actual” SCF for current site settings and the “delta” SCF for staged changes to update site settings. To perform any modifications to the site programmatically, an administrative user must establish a session handle on the file, commit the changes, then release the handle. This is necessary to prevent multiple users trying to modify site settings and creating conflicts. Admin users have likely experienced this working in the Configuration Manager console when trying to modify settings while another user has the same window open. If not properly handled, duplicate entries or even entire duplicate SCFs can occur, which can brick SCCM (ask me how I know).
Admins can use the Get-CMHierarcySetting PowerShell cmdlet to view the site’s current settings and, by expanding the Props embedded property, find entries for GlobalAccounts; including the account information being queried by the GetGlobalUserAccount function. This is promising as the first eight bytes of the blob stored in Value2 matches how Adam described other credential blobs are stored in the database.
Continuing with the basesvr!GetGlobalUserAccount function is where things get interesting and we start to get some insight into the decryption flow and where the credential is stored.
- The function loads then recovers an encrypted session key blob from the site definition file for the target user with CSCItem_Property::GetCopyFromArray
- Decrypts the session key blob with the CServerAccount::Decrypt function
- Calls CSCItem_UserAccount::GetPassword to retrieve the encrypted password for the user account
- The password is finally decrypted with the CServerAccount:DecrptEx function
Quick observation of the CServerAccount::Decrypt function reveals it’s the equivalent of what has already been shared for decrypting credentials in CRED-5, which makes sense considering the session key’s blob format.
To validate this, we can use the script Chris recently shared to decrypt the blob and recover the session key.
Now, to get the encrypted password, the baseobj.dll!GetPassword function just did not decompile well when imported to Ghidra.
Instead, I attached a debugger to the smsexec.exe process and set a breakpoint on the GetPassword symbol imported from baseobj.dll and kicked off a forest discovery.
Once the breakpoint is hit, the global user account and session key blob are visible in the memory dump. This lines up with what we’ve worked out so far in the decryption flow and are now trying to get the encrypted password value.
Step over the break point a bit and eventually land on a call to CServerAccount:DecryptEX and see the same encrypted password value from the SC_UserAccount table shown at the beginning of the blog along with the session key in the registers. And, after the call to DecryptEx is returned, the password for the account is visible.
Looking into the DecryptEx function, the bulk of it is just prepping the session key and encrypted password data formats for a call to another function to return the decrypted value.
The final function performs multiple steps to finally decrypt the password. It:
- Establishes cryptographic service provider context; it initially tries to reuse an existing key container and, if it doesn’t, it creates one
- Formats the session key for CryptImportKey. This is actually pretty cool and is a nice trick by the devs to reformat the session key. The CreatePrivateExponentOneKey function “encrypts” the session key with a private exponent value of 1 which mathematically checks out but does nothing to encrypt the session key. What it is doing, though, is basically encoding it into SIMPLEBLOB format for session key transport that CryptImportKey expects for session keys
- Imports the formatted key
- Allocates memory space for the decrypted password
- Calls CryptDecrypt to decrypt the password with the session key
I originally set out to rewrite the native code to decrypt the string, but recalled seeing another decryption method (Microsoft.ConfigurationManager.CommonBase.EncryptionUtilities.DecryptWithGeneratedSessionKey) when I was recreating Adam’s work with CRED-5.
This made it pretty easy to create a wrapper around this method to decrypt the forest discovery password.
# Load the DLL Add-Type -Path "C:\Program Files\Microsoft Configuration Manager\bin\X64\microsoft.configurationmanager.commonbase.dll" function Invoke-DecryptEx { [CmdletBinding()] param ( [Parameter(Mandatory = $true, Position = 0)] [string]$sessionKey, [Parameter(Mandatory = $true, Position = 1)] [string]$encryptedPwd ) try { $sessionKeyBytes = [byte[]]::new($sessionKey.Length / 2) $encryptedBytes = [byte[]]::new($encryptedPwd.Length / 2) for($i = 0; $i -lt $sessionKey.Length; $i += 2) { $sessionKeyBytes[$i/2] = [Convert]::ToByte($sessionKey.Substring($i, 2), 16) } for($i = 0; $i -lt $encryptedPwd.Length; $i += 2) { $encryptedBytes[$i/2] = [Convert]::ToByte($encryptedPwd.Substring($i, 2), 16) } $encUtil = [Microsoft.ConfigurationManager.CommonBase.EncryptionUtilities]::Instance $decrypted = $encUtil.DecryptWithGeneratedSessionKey($sessionKeyBytes, $encryptedBytes) if ($decrypted -ne $null) { $length = 0 foreach($byte in $decrypted) { if ($byte -eq 0 -or $byte -lt 32 -or $byte -gt 126) { break } $length++ } $decryptedString = [System.Text.Encoding]::ASCII.GetString($decrypted, 0, $length) return $decryptedString } else { Write-Warning "Decryption returned null" return $null } } catch { Write-Error "Error during decryption: $_" return $null } }
While reading documentation on untrusted forest deployment, I came across an interesting requirement for site system installation that provides another opportunity to recover forest credentials. To support untrusted forest deployment, admins must configure an account in the target domain to use for site installation. The account must have local admin permissions on the host and will be used for all future connections to the site system.
Site system installation accounts are stored the same way most other credentials are in the database and can be decrypted with the various methods from CRED-5.
Finally, while exploring all these various credential blobs, I discovered essentially every encrypted credential in SCCM can be recovered via the Administration Service API. Previously, I summarized the SCCM site control file (SCF) and how administrators can review it with PowerShell. It’s also visible from the API via the /wmi/SMS_SCI_SiteDefintion endpoint.
The SC_UserAccount table from the site database has an equivalent endpoint at /wmi/SMS_SCI_Reserved.
I don’t believe this is an exhaustive list yet but this discovery, in combination with the PowerShell decryption methods, can make credential recovery trivial. If the site server’s host system is a managed client, operators can leverage SCCMHunter’s admin module to recover and decrypt credentials stored in SCCM.

As a demo credential blobs from the SC_UserAccount table can be extracted with the get_creds command.
You can use the decrypt command to decrypt the blobs. The target environment will either need script approval disabled or you’ll need a secondary set of approver credentials since the decrypt function uses scripting under the hood. Again, the site server must be a managed client for this to work.
Defensive Considerations
While more credentials are available for abuse following hierarchy takeover, there really isn’t anything new here that warrants new defensive techniques. The defensive recommendations for CRED-5 are applicable here. In particular, the recommendations in PREVENT-10 to enforce the principle of least privilege for service accounts. Additionally, many of the accounts I’ve seen from previous assessments had an account name of “not configured”.
This happens when an account was being used for an action, in this case forest discovery, and then removed from that service. My conclusion here is admins may incorrectly believe removing the account from the service deletes the account. Organizations should review accounts found in the Administration\Overview\Security\Accounts panel and remove them if they’re no longer in use.
Final Thoughts
I recognize that, if SCCM is managing an untrusted forest, there are likely clients running on devices from that forest and those can be leveraged for the same or greater lateral movement. I personally like to have as many credentials as possible and believe the credentials shown here may be extremely valuable.
Soon after this blog is published, we plan to update Misconfiguration Manager with these techniques to ensure they’re available to attackers and defenders. We have many more updates to make, which we’ll be publishing at a later date.
Come hang out with us in the #sccm channel on the BloodHound Slack. It’s been cool to see other members of the industry develop SCCM tradecraft and we discuss much of that conversation there.
Decrypting the Forest From the Trees was originally published in Posts By SpecterOps Team Members on Medium, where people are continuing the conversation by highlighting and responding to this story.