Azure and M365 Forensics
Azure and Microsoft 365 share an identity plane (Entra ID / Azure AD) — attackers who compromise M365 credentials often pivot to Azure resources and vice versa. This chapter covers the complete Azure/M365 forensic workflow: Entra ID sign-in investigation, Azure VM forensics, Defender for Cloud findings, and M365 identity compromise indicators.
A global admin account was compromised via a phishing attack that bypassed MFA using an adversary-in-the-middle (AiTM) proxy. The attacker used the compromised session to add a persistent OAuth app with mailbox access, read sensitive emails, and then escalated to Azure subscription contributor permissions. The attack chain spans M365 (phishing + OAuth app), Entra ID (privilege escalation), and Azure (resource access). This chapter covers investigating each hop.
Entra ID Sign-In Investigation
# Entra ID forensics via Microsoft Graph PowerShell
# Connect-MgGraph -Scopes "AuditLog.Read.All","User.Read.All","Policy.Read.All"
# Compromised account
$suspect = "admin@corp.com"
# Step 1: Get sign-in history for the account
Get-MgAuditLogSignIn `
-Filter "userPrincipalName eq '$suspect'" `
-Top 500 `
-OrderBy "createdDateTime desc" |
Select-Object CreatedDateTime, IpAddress, Location,
AppDisplayName, ClientAppUsed, ConditionalAccessStatus,
RiskState, RiskLevelAggregated, Status |
Format-Table -AutoSize
# Step 2: Find impossible travel events
Get-MgAuditLogSignIn `
-Filter "userPrincipalName eq '$suspect' and riskEventTypes_v2/any()" `
-Top 100 |
Select-Object CreatedDateTime, IpAddress,
@{N="Country";E={$_.Location.CountryOrRegion}},
RiskEventTypesV2, RiskState |
Format-Table
# Step 3: Find OAuth app consent events (persistent access)
Get-MgAuditLogAudit `
-Filter "activityDisplayName eq 'Consent to application'" `
-Top 100 |
Select-Object ActivityDateTime, InitiatedBy,
@{N="AppName"; E={($_.TargetResources | Select-Object -First 1).DisplayName}},
Result |
Format-Table
# Step 4: Find Global Admin role assignments
Get-MgAuditLogAudit `
-Filter "activityDisplayName eq 'Add member to role'" `
-Top 100 |
Where-Object { $_.TargetResources.ModifiedProperties |
Where-Object { $_.DisplayName -eq "Role.DisplayName" -and
$_.NewValue -match "Global Administrator" }
} |
Select-Object ActivityDateTime, InitiatedBy, TargetResources |
Format-List
Azure Activity Log Investigation
# Azure Activity Log — management plane operations
# Requires Az PowerShell module: Connect-AzAccount
$StartTime = [DateTime]::Parse("2026-09-17T00:00:00Z")
$EndTime = [DateTime]::Parse("2026-09-18T00:00:00Z")
# All activity log events in the window
$activity = Get-AzLog -StartTime $StartTime -EndTime $EndTime `
-WarningAction SilentlyContinue
# Find resource creation (attacker spinning up new VMs / storage)
$activity | Where-Object {
$_.OperationName.Value -match "write" -or
$_.OperationName.Value -match "create"
} |
Select-Object EventTimestamp, Caller, OperationName, ResourceType, ResourceId |
Format-Table -AutoSize
# Find permission changes (role assignments, policy changes)
$activity | Where-Object {
$_.OperationName.Value -match "roleAssignment\|policyAssignment"
} |
Select-Object EventTimestamp, Caller, OperationName, ResourceId |
Format-Table
# Find diagnostic log disabling / deletion (attacker anti-forensics)
$activity | Where-Object {
$_.OperationName.Value -match "diagnosticSettings" -and
$_.Status.Value -eq "Succeeded"
} |
Select-Object EventTimestamp, Caller, OperationName |
Format-Table
AiTM Phishing Investigation
AiTM (Adversary-in-the-Middle) Phishing — Evidence Trail
═══════════════════════════════════════════════════════════════════
Attack flow:
1. User receives phishing email with link to AiTM proxy
2. User authenticates to Microsoft through the proxy
3. Proxy captures the session token and passes auth to real Microsoft
4. User gets legitimate MFA-complete access (doesn't notice)
5. Attacker uses captured session token independently — bypasses MFA
Evidence in Entra ID Sign-in Logs:
├── Two sign-in events for same user at same time:
│ - Legitimate: user's normal IP/device
│ - Attacker: unfamiliar IP (AiTM proxy server)
├── Attacker session: new device (no device compliance state)
├── Risk event: "unfamiliar sign-in properties"
├── Session persists after normal user logout
└── Conditional Access: may show "Success" despite suspicious location
(because the token was legitimately MFA-issued — CA sees valid token)
Entra ID Protection detection signals:
- Risk event: Attacker IP Address
- Risk event: Anonymous IP address
- Risk event: Unfamiliar sign-in properties
→ Check via: Get-MgRiskyUser / Get-MgIdentityRiskDetection
Q & A
Q: An attacker gained access via a stolen OAuth token, not username/password. What does this look like in logs vs. normal auth?
OAuth token theft leaves specific indicators: (1) Sign-in log source type: normal interactive sign-in shows ClientAppUsed: Browser. OAuth token use for non-interactive access shows ClientAppUsed: Other clients or specific app names. If the compromised session suddenly switches from browser access to API/programmatic access, that's the attacker using the stolen token programmatically. (2) TokenIssuedAt vs UsedAt mismatch: the token was issued when the legitimate user authenticated (on their known IP/device), but is being used from a different IP hours later. This is visible in the sign-in logs as a token refresh from an unexpected IP. (3) Service principal activity: if the attacker registers an OAuth app with persistent permissions (mailbox Reader, etc.), that app's service principal appears in the Audit Log (Consent to application event) and then generates its own sign-in events in the non-interactive sign-in log (separate from the user sign-in log — often overlooked). Check Get-MgAuditLogSignIn -Filter "signInEventTypes/any(t:t eq 'servicePrincipal')" for OAuth app activity. (4) Unusual scopes: look at the scopes/permissions of active OAuth app grants via Get-MgServicePrincipalOauth2PermissionGrant. Attacker-added apps often have Mail.ReadWrite, MailboxSettings.ReadWrite, or Files.ReadWrite.All.