Post-AiTM Graph API Reconnaissance Burst

4+ distinct Graph API endpoint categories (role/cross-tenant/mailbox/contact/org) from single UPN within 60 seconds, empty c_DeviceId, from residential ASN 10-20 minutes after AiTM relay session theft

Discovery T1087.004 T1069.003 T1526 Detection difficulty: MEDIUM Prevalence: MEDIUM

Within 10-20 minutes of successful AiTM session token theft, Tycoon 2FA's operator console tier executes an automated burst of 20-30+ Microsoft Graph API calls across five distinct reconnaissance categories (role discovery, cross-tenant recon, mailbox recon, contact harvesting, org and licensing) within 30-60 seconds. Normal Microsoft 365 portal navigation by a human user touches 1-2 endpoint categories in a session; 4+ distinct categories within 60 seconds is mechanically impossible at human speed and indicates automated tooling. The operator automation uses first-party apps with pre-consented broad scopes, an empty c_DeviceId (unmanaged device), and disproportionate /beta/ API usage. A common analyst mistake is filtering on c_sid (Graph Activity Log client session ID) as if it were the user object ID - c_sid is a session/security-context identifier and does NOT equal the user object ID; filtering c_sid == user_object_id returns empty results.

Attack Chokepoints 1 invariant stage

Each stage is an invariant condition the attacker must satisfy, regardless of tool, variant, or threat actor. Detection at any stage breaks the chain.

1 Multi-Category Graph API Burst
  • Kit has successfully obtained session tokens (Tier 1 relay) or device-code-grant tokens
  • Graph Activity Logs must be enabled and ingested (not enabled by default in all tenants)
  • Operator automation must have access tokens with broad pre-consented scopes (RoleManagement.ReadWrite.Directory, MailboxSettings.ReadWrite, etc.)
Input Operator console tier has received stolen session tokens from kit relay; begins automated reconnaissance within 10-20 minutes of Tier 1 completion
Chokepoint The attacker MUST enumerate multiple distinct Graph API endpoint categories to triage the compromised identity's value (roles, cross-tenant trust, mailbox configuration, contacts, licensing). The automation cannot perform all reconnaissance from a single endpoint - each category requires distinct API calls. Covering 4+ categories within 60 seconds is not achievable by human portal navigation (normal = 1-2 categories per session).
Observable Microsoft Graph Activity Logs (MicrosoftGraphActivityLogs in Sentinel): Single UPN makes requests to endpoints spanning 4+ of these categories within a 60-second window: - Role Discovery: transitiveRoleAssignments, memberOf/directoryRole, roleManagement/directory/roleAssignments - Cross-Tenant: tenantRelationships/getResourceTenants (POST) - Mailbox Recon: me/mailboxSettings - Contact Harvest: me/contactFolders/contacts ($top=1000) - Org & Licensing: subscribedSkus, organization, appRoleAssignedResources Additional signals: empty c_DeviceId, $top=999/$count=true parameters, /beta/ API usage, mixed GET/POST methods, mixed 200/400/403 response codes.
Why unavoidable
The operator automation requires breadth to triage the compromised identity's value - checking roles, cross-tenant trusts, mailbox forwarding rules, contact lists, and licensing in one pass. These data are spread across 5 distinct Graph API endpoint categories. Slowing the requests to human-navigation speed (>60s between categories) would add 10+ minutes to the recon phase, increasing detection window and reducing the automation's value. The category diversity is the invariant - the attacker cannot collapse it into a single endpoint.
  • Microsoft Graph Activity Logs (MicrosoftGraphActivityLogs Sentinel table)
  • Entra ID Sign-in Logs (for ASN correlation with Tier 2 residential proxy)
Bypass risk: Graph Activity Logs may not be enabled in all tenants - check ingestion before relying on this detection. c_sid is NOT the user object ID; pivot on source IP + appId cross-referenced with sign-in logs to identify UPN.
View rule →

Variations 1 variant tracked

Tools and methods that exploit this chokepoint. The list grows. The chokepoint doesn't change.

Tycoon 2FA Operator Console - 5-Category Recon Automation 2023-Q3 Active
Operator console tier (Tier 2) executes 20-30+ Graph API calls within 30-60 seconds. Accesses all 5 recon categories. Uses residential-proxy ASN, single fixed browser UA (Firefox on Windows). Appears 10-20 minutes after Tier 1 kit relay success. First-party apps with broad pre-consented scopes: RoleManagement.ReadWrite.Directory, MailboxSettings.ReadWrite, UserAuthenticationMethod.ReadWrite, User.RevokeSessions.All. Empty c_DeviceId (unmanaged, unregistered device from Tier 2 residential proxy). Common mistake: filtering c_sid == user_object_id returns empty results; pivot on source IP + appId against sign-in logs instead.
20-30+ calls within 30-60 seconds. Structured query params ($top=999, $count=true). /beta/ API disproportionate. Mixed 200/400/403 responses (400/403 still indicate recon attempts). Empty c_DeviceId. Residential ASN (Tier 2) appears ~10-20 min after Tier 1 cloud-VPS sign-in.
# Role Discovery
GET https://graph.microsoft.com/beta/me/transitiveRoleAssignments?$select=roleDefinitionId
GET https://graph.microsoft.com/v1.0/me/memberOf/microsoft.graph.directoryRole
GET https://graph.microsoft.com/beta/roleManagement/directory/roleAssignments?$filter=principalId eq '<id>'

# Cross-Tenant Recon
POST https://graph.microsoft.com/beta/tenantRelationships/getResourceTenants

# Mailbox Recon
GET https://graph.microsoft.com/v1.0/me/mailboxSettings

# Contact Harvesting
GET https://graph.microsoft.com/v1.0/me/contactFolders/contacts?$top=1000

# Org & Licensing
GET https://graph.microsoft.com/v1.0/subscribedSkus
GET https://graph.microsoft.com/v1.0/organization
GET https://graph.microsoft.com/beta/me/appRoleAssignedResources?$top=999&$count=true
  • Graph Activity Logs: 4+ endpoint categories from single UPN within 60s
  • Graph Activity Logs: $top=999 / $count=true structured query parameters
  • Graph Activity Logs: empty c_DeviceId on all requests
  • Graph Activity Logs: /beta/ API disproportionate to /v1.0/
  • Entra ID Sign-in Logs: residential ASN sign-in 10-20 min after cloud-VPS sign-in same UPN
Same chokepoint: Tier 1 relay steals session → Tier 2 operator console (residential ASN) receives tokens → automated 5-category Graph recon burst (20-30+ calls / 60s) → operator triages identity value for fraud / BEC / lateral movement
Source: www.elastic.co →

Detection Strategy

Rules organized by the chokepoint stage they detect. Each stage has one or more rules at different maturity levels.

1 Multi-Category Graph API Burst
Log all Graph API requests matching any of the 5 recon categories from Graph ...
Research
Goal
Log all Graph API requests matching any of the 5 recon categories from Graph Activity Logs. Baseline for understanding legitimate admin tooling usage of these endpoints.
Log Sources
  • Microsoft Graph Activity Logs (MicrosoftGraphActivityLogs)
FP Rate
Medium (IT admin tooling, compliance tools, Microsoft first-party portal)
Use Case
Baseline Graph Activity Log coverage. Identify legitimate admin tooling that queries multiple categories. Build c_DeviceId allowlist for managed devices.
requestUri contains_any [transitiveRoleAssignments, directoryRole, roleManagement/directory/roleAssignments, tenantRelationships, mailboxSettings, contactFolders/contacts, subscribedSkus, /organization, appRoleAssignedResources]. No volume/timing filter. Run for one week to identify legitimate admin tooling and build exclusion list.
Sigma Rule - Research Level
title: Microsoft Graph API Request to AiTM Post-Compromise Recon Endpoint (Baseline)
id: e1f2a3b4-c5d6-4e7f-8a9b-0c1d2e3f4a5b
status: experimental
description: >
  Baseline visibility rule: logs all Microsoft Graph Activity Log requests matching
  any endpoint in the five Tycoon 2FA post-compromise reconnaissance categories
  (role discovery, cross-tenant, mailbox, contact harvest, org/licensing). High FP
  rate from IT admin tooling, compliance platforms, and Microsoft first-party portal
  navigation — run for one week to identify legitimate admin tooling and build
  exclusion lists before promoting to hunt tier.
  REQUIRES Microsoft Graph Activity Logs (MicrosoftGraphActivityLogs) to be enabled
  and ingested -- not available by default in all tenants.
references:
  - https://www.elastic.co/security-labs/tycoon-2fa-aitm-detection-engineering
author: '@iimp0ster'
date: 2026-05-27
tags:
  - attack.discovery
  - attack.t1087.004
  - attack.t1069.003
  - attack.t1526
  - detection.maturity.research
logsource:
  product: azure
  service: MicrosoftGraphActivityLogs
detection:
  selection_role_discovery:
    RequestUri|contains:
      - 'transitiveRoleAssignments'
      - 'microsoft.graph.directoryRole'
      - 'roleManagement/directory/roleAssignments'
  selection_cross_tenant:
    RequestUri|contains:
      - 'tenantRelationships'
  selection_mailbox:
    RequestUri|contains:
      - 'mailboxSettings'
  selection_contacts:
    RequestUri|contains:
      - 'contactFolders'
  selection_org_licensing:
    RequestUri|contains:
      - 'subscribedSkus'
      - '/organization'
      - 'appRoleAssignedResources'
  condition: >
    selection_role_discovery or selection_cross_tenant or selection_mailbox
    or selection_contacts or selection_org_licensing
falsepositives:
  - IT admin tooling performing role audits (compliance, PAM platforms)
  - Microsoft 365 admin portal navigation
  - Third-party compliance and governance tools (AvePoint, Varonis, Stealthbits)
  - Internal Graph SDK tooling by identity engineering teams
level: informational
4+ distinct Graph API endpoint categories from a single UPN within 60 seconds...
Hunt
Goal
4+ distinct Graph API endpoint categories from a single UPN within 60 seconds with empty c_DeviceId (unmanaged device). Requires category-tagging enrichment or ES|QL / KQL aggregation pipeline.
Log Sources
  • Microsoft Graph Activity Logs
FP Rate
Low (third-party compliance/audit tools; document and exclude)
Use Case
Active hunting for post-AiTM operator recon. Category diversity at automated speed is the invariant. False positives limited to broad-scope IT tooling.
Tag each request into one of 5 categories based on requestUri pattern. Alert when same UPN has requests spanning 4+ distinct categories within 60s AND c_DeviceId is empty. Exclude known IT admin service accounts and compliance tool service principals. ES|QL or KQL recommended over Sigma for the category-count aggregation. NOTE: c_sid is NOT the user object ID - pivot on source IP + appId against sign-in logs to identify UPN. Do NOT filter c_sid == user_object_id.
Sigma Rule - Hunt Level
title: Post-AiTM Graph API Multi-Category Recon Burst — 4+ Categories in 60 Seconds
id: f2a3b4c5-d6e7-4f8a-9b0c-1d2e3f4a5b6c
status: experimental
description: >
  Detects a single UPN issuing requests spanning 4 or more distinct Graph API endpoint
  categories (role, cross-tenant, mailbox, contacts, org/licensing) within 60 seconds
  with an empty c_DeviceId. This is the Tycoon 2FA operator console automated
  reconnaissance pattern: 20-30+ calls across all 5 categories within 30-60 seconds,
  impossible at human portal-navigation speed. Empty c_DeviceId confirms an unmanaged
  device (operator's residential proxy), not an IT admin managed device.
  NOTE: This rule requires category-tagging aggregation that cannot be expressed in
  standard Sigma. Implement via ES|QL or KQL with STATS aggregation.
  CRITICAL: c_sid in Graph Activity Logs is NOT the user object ID -- pivot on source
  IP + appId against sign-in logs to identify UPN. DO NOT filter c_sid == user_object_id.
  REQUIRES Graph Activity Logs enabled and ingested in the tenant.
  KQL implementation (Sentinel):
    MicrosoftGraphActivityLogs
    | extend category = case(
        RequestUri has_any ("transitiveRoleAssignments","directoryRole","roleManagement/directory/roleAssignments"), "role",
        RequestUri has "tenantRelationships", "cross-tenant",
        RequestUri has "mailboxSettings", "mailbox",
        RequestUri has "contactFolders", "contacts",
        RequestUri has_any ("subscribedSkus","/organization","appRoleAssignedResources"), "org-licensing",
        "other")
    | where category != "other" and isempty(DeviceId)
    | summarize distinct_categories = dcount(category) by UserId, bin(TimeGenerated, 60s)
    | where distinct_categories >= 4
references:
  - https://www.elastic.co/security-labs/tycoon-2fa-aitm-detection-engineering
author: '@iimp0ster'
date: 2026-05-27
tags:
  - attack.discovery
  - attack.t1087.004
  - attack.t1069.003
  - attack.t1526
  - detection.maturity.hunt
logsource:
  product: azure
  service: MicrosoftGraphActivityLogs
detection:
  # Category-count aggregation cannot be expressed in standard Sigma.
  # Use the KQL implementation in the description for production deployment.
  # This stub fires on any recon endpoint with empty DeviceId as a partial signal.
  selection:
    DeviceId: ''
    RequestUri|contains:
      - 'transitiveRoleAssignments'
      - 'tenantRelationships'
      - 'mailboxSettings'
      - 'contactFolders'
      - 'subscribedSkus'
      - 'appRoleAssignedResources'
  filter_managed_devices:
    DeviceId|exists: true
  filter_known_tooling:
    # PENDING: Add known compliance/admin tool service principal app IDs
    # AppId|contains:
    #   - '<varonis-app-id>'
    #   - '<avepoint-app-id>'
    AppId: ''  # placeholder — remove and populate before production
  condition: selection and not filter_managed_devices and not filter_known_tooling
falsepositives:
  - Broad-scope compliance tools querying multiple categories in automated scans
  - IT admin portal performing multi-step identity audit (document and exclude by appId)
  - Identity governance tools with scheduled access reviews
level: high
4+ Graph API categories / 60s with empty c_DeviceId correlated with preceding...
Analyst
Goal
4+ Graph API categories / 60s with empty c_DeviceId correlated with preceding AiTM sign-in from same UPN (Node.js UA or residential ASN transition within 20 minutes).
Log Sources
  • Microsoft Graph Activity Logs
  • Entra ID Sign-in Logs (for AiTM correlation)
FP Rate
Very Low
Use Case
SOC alerting. Immediate IR escalation. Trigger automated response: disable account → enumerate/delete registered devices → revoke sessions.
Graph recon burst (4+ categories / 60s / empty c_DeviceId) AND same UPN has a preceding sign-in with Node.js UA on OfficeHome/Auth Broker within 20 minutes. Correlated with AiTM relay signal = high-confidence post- compromise recon confirmation.
Correlation required: This rule covers one stage only. See the description for SIEM-side join guidance.
Sigma Rule - Analyst Level
title: Post-AiTM Graph API Recon Burst Correlated with Preceding AiTM Session Theft
id: a3b4c5d6-e7f8-4a9b-0c1d-2e3f4a5b6c7d
status: experimental
description: >
  Detects the Tycoon 2FA two-stage post-compromise kill chain: a 4+ category Graph API
  recon burst (20-30+ calls / 60 seconds / empty c_DeviceId) correlated with a
  preceding AiTM relay sign-in (Node.js UA on OfficeHome/Auth Broker) from the same UPN
  within 20 minutes. The two-stage pattern confirms session theft (Tier 1 cloud-VPS
  relay) followed by operator console triage (Tier 2 residential proxy). This is the
  highest-confidence post-AiTM detection signal: the category breadth is invariant to
  kit rotation because it reflects the operator's need to triage identity value.
  NOTE: Requires correlation across Graph Activity Logs and Entra ID sign-in logs.
  Category-count aggregation cannot be expressed as standard Sigma.
  Implement as KQL multi-table join or SIEM correlation rule with two legs.
  CRITICAL: c_sid is NOT the user object ID -- identify UPN via source IP + appId pivot
  against sign-in logs; do NOT filter c_sid == user_object_id (returns empty results).
  REQUIRES Graph Activity Logs enabled and ingested.
  KQL join (Sentinel):
    let AiTMSignIn = SigninLogs
      | where ResultType == "0"
        and UserAgent has_any ("node","axios","undici","node-fetch")
        and AppDisplayName has_any ("OfficeHome","Microsoft Authentication Broker")
      | project TimeGenerated, UserPrincipalName, AiTMTime = TimeGenerated;
    MicrosoftGraphActivityLogs
    | extend category = case(
        RequestUri has_any ("transitiveRoleAssignments","directoryRole"), "role",
        RequestUri has "tenantRelationships", "cross-tenant",
        RequestUri has "mailboxSettings", "mailbox",
        RequestUri has "contactFolders", "contacts",
        RequestUri has_any ("subscribedSkus","/organization","appRoleAssignedResources"), "org-licensing",
        "other")
    | where category != "other" and isempty(DeviceId)
    | summarize distinct_categories = dcount(category), first_recon = min(TimeGenerated) by UserId, bin(TimeGenerated, 60s)
    | where distinct_categories >= 4
    | join kind=inner AiTMSignIn on $left.UserId == $right.UserPrincipalName
    | where first_recon between (AiTMTime .. AiTMTime + 20m)
references:
  - https://www.elastic.co/security-labs/tycoon-2fa-aitm-detection-engineering
author: '@iimp0ster'
date: 2026-05-27
tags:
  - attack.discovery
  - attack.t1087.004
  - attack.t1069.003
  - attack.t1526
  - attack.credential-access
  - attack.t1539
  - attack.t1078.004
  - detection.maturity.analyst
logsource:
  product: azure
  service: MicrosoftGraphActivityLogs
detection:
  # This rule requires correlation across two log tables — use the KQL join above.
  # Sigma stub fires on graph burst partial signal only; full correlation requires SIEM.
  selection_graph_burst:
    DeviceId: ''
    RequestUri|contains:
      - 'transitiveRoleAssignments'
      - 'tenantRelationships'
      - 'mailboxSettings'
  # CORRELATION: same UPN in SigninLogs with Node.js UA within 1200s preceding — KQL required
  condition: selection_graph_burst
falsepositives:
  - Admin tooling + coincident Node.js automation from the same UPN (document per tenant)
level: high

OSINT Pivots

N/A N/A - Graph API recon bursts are internal Microsoft log signals
Graph API recon bursts are not externally observable via OSINT tools (URLScan, Shodan, Hunt.io). Detection relies entirely on Graph Activity Log ingestion within the tenant. Confirm MicrosoftGraphActivityLogs is enabled and flowing to Sentinel before relying on this chokepoint.