Visualização normal

Antes de ontemStream principal
  • ✇Posts By SpecterOps Team Members - Medium
  • Decrypting the Forest From the Trees Garrett Foster
    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.IntroductionWhile 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
     

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.

  1. The function loads then recovers an encrypted session key blob from the site definition file for the target user with CSCItem_Property::GetCopyFromArray
  2. Decrypts the session key blob with the CServerAccount::Decrypt function
  3. Calls CSCItem_UserAccount::GetPassword to retrieve the encrypted password for the user account
  4. 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:

  1. Establishes cryptographic service provider context; it initially tries to reuse an existing key container and, if it doesn’t, it creates one
  2. 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
  3. Imports the formatted key
  4. Allocates memory space for the decrypted password
  5. 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.

  • ✇Posts By SpecterOps Team Members - Medium
  • Further Adventures With CMPivot — Client Coercion Diego lomellini
    Further Adventures With CMPivot — Client CoercionPerfectly Generated AI Depiction based on TitleTL:DRCMPivot queries can be used to coerce SMB authentication from SCCM client hostsIntroductionCMPivot is a component part of the Configuration Manager framework. With the rise in popularity for ConfigMgr as a target in red team operations, this post looks to cover a way other than using CMPivot (CMP) data gathering capabilities for taking over a computer object in Active Directory environments. If i
     

Further Adventures With CMPivot — Client Coercion

Further Adventures With CMPivot — Client Coercion

Perfectly Generated AI Depiction based on Title

TL:DR

CMPivot queries can be used to coerce SMB authentication from SCCM client hosts

Introduction

CMPivot is a component part of the Configuration Manager framework. With the rise in popularity for ConfigMgr as a target in red team operations, this post looks to cover a way other than using CMPivot (CMP) data gathering capabilities for taking over a computer object in Active Directory environments. If interested in leveraging CMP for data enumeration, see the previous post “Lateral Movement without Lateral Movement

Why

After learning about CMPivot’s potential for offensive operations, how much information it can pull from a client host (almost as if you’d be operating within the target itself) and more importantly for this post, how it achieves this, I always itched to go a little further than just using the queries for how they are meant to be used (i.e., data collection). This post will showcase a simple but effective way to use CMPivot to coerce authentication from an SCCM/ConfigMgr client host.

CMPivot for offensive operators

From the point of view of an offensive operator, CMPivot’s “intended” use flow of remote data querying does have the potential to reveal information that could indirectly aid in taking over an SCCM client host. For example, if we use the query that allows us to read file contents and happen to find an SSH key on a client host, we could then leverage those keys for lateral movement.

Now, even though the ability to enumerate almost any data from a host is a big plus for us as offensive operators, that is where the intended capabilities of CMPivot’s end: with data enumeration.

Rather than allowing any type of remote command execution, CMP was designed with the sole purpose of gathering information from client hosts.

Note: SCCM/ConfigMgr does have built in ways for a user to achieve execution on clients when working under the right context and privileges. These privileges are not always attainable and probably more closely tracked nowadays.

CMPivot Relay Background

The way CMPivot gathers data from clients is by taking any queries we make on the CMPivot GUI or via SharpSCCM (adminservice command) and sends those to the CCMExec client engine living on a client host. After this the client software runs a PowerShell script with our CMP query as one of the parameters. The CMPivot client PowerShell script on the target will then filter our queries most of the time for use with WMI methods to enumerate the specific information we asked for. The results of those queries will be collected and sent back to the ConfigMgr/SCCM site Management Point. Ultimately, that data will be presented to the querying user’s GUI.

In most cases, whatever input we provide as part of these CMPivot queries is correlated to a WMI class and method executed on the client host. For example, let’s look at the CMPivot client side script located at C:\Windows\CCM\ScriptStore on client machines. When calling the “Users” CMPivot query from the GUI, the client side powershell script will leverage the Win32_LoggedOnuser WMI class as seen in the code block below.


elseif( $wmiquery -eq 'Users' )
{
$users = New-Object System.Collections.Generic.List[String]

foreach( $user in (get-WmiObject -class Win32_LoggedOnuser -ErrorAction Stop | Select Antecedent))
{
$parts = $user.Antecedent.Split("""")

# If this is not a built-in account
if(( $parts[1] -ne "Window Manager" ) -and (($parts[1] -ne $env:COMPUTERNAME) -or (($parts[3] -notlike "UMFD-*")) -and ($parts[3] -notlike "DWM-*")))
{
# add to list
$users.Add($parts[1] + "\\" + $parts[3])
}
}

# Create unique set of users
$users | sort-object -Unique | foreach-object { $results.Add(@{ UserName = $_ }) }
}

My assumption was that this was the modus operandi for all queries that we can make with CMPivot. Further inspecting the client-side PowerShell script shows this section referring to some “one-off” cases that are easier to deal with using straight PowerShell versus Windows Management Instrumentation (WMI) classes:


#Create the result set
$results = New-Object System.Collections.Generic.List[Object]

**#deal with one-offs that don't work well over WMI**
if( $wmiquery -eq 'SMBConfig' )
{
# Get Smb Config
$smbConfig = Get-SmbServerConfiguration -ErrorAction Stop| Select-object -Property $propertyFilter
.. SNIP SNIP ..

elseif ($wmiquery.StartsWith("FileContent(") )

Included in that section of the PowerShell script, we have the logic that deals with the “File” and “FileContent” CMPivot queries among others. Those two let us check if an arbitrary file exists on the target and to retrieve its contents depending on the type of data.

These two queries fall into what the CMPivot client script classifies as “one-off” calls. When calling either of those queries, our input is taken all the way to the command being executed , which can be seen here:

elseif ($wmiquery.StartsWith("FileContent(") )
{
$first = $wmiquery.IndexOf("'")+1
$last = $wmiquery.LastIndexOf("'")
$filepath = [System.Environment]::ExpandEnvironmentVariables( $wmiquery.Substring($first, $last-$first) )
#verify if the file exists
if( [System.IO.File]::Exists($filepath) )
{
$lines = (get-content -path $filepath -ErrorAction Stop)
# our code handles lines as list of object
# get-content return list of lines if multiple lines are present
# in case of single line a string is returned which is casted to list
if ($lines -is [string]) {
$lines = @($lines)
}
for ($index = 0; $index -lt $lines.Length; $index++)
{
$line = $lines[$index]
$hash = @{
Line = $index+1
Content = $line
}
$results.Add($hash)
}
}
}

Above, we can see that in the case of the “FileContent” query this is just ran as a simple PowerShell Get-Content cmdlet in the background.

CMPivot Reading File Content Remotely

Actual CMPivot Relay Demo

Where it gets more interesting is that the check for file existence [System.IO.File]::Exists($filepath) and the Get-Content cmdlet also allow for UNC paths and, as mentioned in my previous CMPivot blogpost, the actions taken to gather the data on client host are performed as NT AUTHORITY/SYSTEM. Something as simple as pointing our query to a UNC path under our control lets us coerce SMB authentication from any SCCM client host that we can run CMPivot queries against. Let’s look at a relay example.

For this example, I used PortBender to take over port 445/TCP on a previously compromised host where I had full control. If you are not familiar with PortBender, you can read about it here. There is also a great post by Nick Powers on taking over 445/TCP, which I highly recommend called Relay Your Heart Away: A Conscious Approach to 445 Takeover.

We start our relay server and point it to a certificate authority (CA) in order to take advantage of the incoming SMB authentication.

Relay Server Setup

Next, we set up our required proxies:

Cobalt Strike Socks Proxy

…and our port forwards.

Cobalt Strike 445 Port Forward Setup

The relay server is set to point our victim to the domain’s CA and request for a certificate which can later be used to request a kerberos ticket for the machine account of our target.

We can use tools like SharpSCCM to execute a CMPivot query against the target SCCM client. The admin-service command is our ally in this case. For more CMPivot and SharpSCCM admin-service usage details, see the previous post Lateral Movement without Lateral Movement.

SharpSCCM admin-service File Contents Read Query

If we have access to the CMPivot GUI, we can use the standard FileContent query.

CMPivot GUI File Contents Read Query

And we end up getting a hit on our relay server and the resulting base64 blob for our desired certificate.

Relay Server Hit

Outro

Hopefully, this adds one more tool to the arsenal when it comes to getting control of a machine object without executing foreign tradecraft on a target and helps build tradecraft depth when operating within ConfigMgr/SCCM environments. If a low privilege account has been assigned the right security roles, that user could potentially escalate their privileges with this technique.

Requirements

These are some of the permissions and security roles that allow CMPivot querying:

Permissions:

  • Run CMPivot permission on the Collection
  • Read permission on Inventory Reports
  • Read permissions on Devices and Collections.

Security Roles:

  • Read-Only Analyst (Built-in role) — Can run CMPivot queries but cannot take action.
  • Full Administrator — Has all permissions, including CMPivot.
  • Custom Role (if creating a new one) — Needs specific permissions (see below).

When dealing with a custom security role, it needs at at least the following permissions:

CMPivot Execution Permissions

Scope:

  • The user must have access to the collections where CMPivot is being executed.

Connectivity:

For the scenario presented in the example:

  • ConfigMgr clients should be able to talk to the Management Point
  • Clients need to be able to reach the relay or proxy server
  • A Certificate authority should be reachable

Abuse Vector:

  • A vulnerable template must be available

Resources


Further Adventures With CMPivot — Client Coercion was originally published in Posts By SpecterOps Team Members on Medium, where people are continuing the conversation by highlighting and responding to this story.

❌
❌