Microsoft Passkey-Themed Vishing – Cloud Identity Persistence and Graph Data Theft

Threat Overview

Microsoft Security Research published on September 9, 2026 that it is tracking active cloud intrusions observed since May in which passkey, MFA, and SSO-themed social engineering leads to compromised Microsoft cloud identities. Attackers impersonate IT through calls, texts, phishing sites, and in some cases trusted Microsoft Teams messages, then use adversary-in-the-middle phishing or device-code authentication to obtain sessions. After access, actors register attacker-controlled MFA methods for persistence, enumerate Microsoft Entra and Microsoft 365 through Microsoft Graph, and collect data from SharePoint, OneDrive, and Exchange.

Microsoft observed automation using Node.js and, in some collection activity, the python-httpx user agent. Collection is often deliberately paced below 1,000 files or emails per hour rather than performed as an obvious burst. Microsoft attributes parts of the ecosystem to Storm-3121, which can lead to ShinyHunters and Falcon extortion, and Storm-3032, associated with the Helix extortion banner. This is not a Microsoft software vulnerability; it is an identity-abuse intrusion pattern that succeeds through social engineering and legitimate cloud authentication/API paths.

References

Impacted Systems

Environment: Microsoft Entra ID and Microsoft 365 tenants, including Microsoft Graph, SharePoint Online, OneDrive for Business, and Exchange Online. No vulnerable software version is required. Prerequisites: a user is socially engineered into an AiTM or device-code authentication flow, or the actor reuses previously compromised credentials/tokens; non-phishing-resistant MFA materially increases risk. Exposure: identity- and SaaS-based rather than tied to an Internet-facing server port. Unmanaged/personal mobile devices can reduce endpoint visibility because the initial phishing interaction may occur outside managed endpoint telemetry. Organizations that allow device-code flows, security-info registration from unmanaged contexts, broad Graph permissions, or unrestricted cloud downloads from unmanaged devices have greater exposure.

Why this matters

The campaign combines human-targeted initial access with persistent cloud identity control and systematic use of Microsoft Graph for reconnaissance and data theft. It is broadly applicable to Microsoft 365 environments because the activity can occur almost entirely in identity and SaaS telemetry and may leave little or no endpoint evidence.

Exploitation Status

Confirmed active threat activity, not exploitation of a software CVE. Microsoft has observed the intrusion sequence since May 2026 across multiple accounts and documented authentication persistence, Graph reconnaissance, SharePoint/OneDrive collection, and Exchange REST-based email access. Infrastructure changes quickly, so Microsoft recommends sequence-based investigation rather than relying on domain or IP matches alone.

What this hunt looks for

Rapid application expansion following sign-in, attacker-controlled MFA registration, broad Microsoft Graph reconnaissance, privilege/authentication-method/application discovery, SharePoint/OneDrive repository enumeration, mailbox/attachment reconnaissance, reconnaissance progressing into content collection, sustained Office 365 file-access patterns, and known passkey/SSO lure domains.

Required logs

  • Microsoft Entra SigninLogs.
  • Microsoft Defender XDR CloudAppEvents for MFA registration hunting.
  • GraphAPIAuditEvents for Microsoft Graph reconnaissance and collection hunting.
  • OfficeActivity for SharePoint/OneDrive access fallbacks.
  • UrlClickEvents where Defender for Office 365 URL-click telemetry is available.

First Pass – Unusual Successful Sign-In Followed by Rapid Application Expansion

SigninLogs
| where TimeGenerated > ago(14d)
| where ResultType == 0
| summarize FirstSeen=min(TimeGenerated), LastSeen=max(TimeGenerated), Apps=dcount(AppDisplayName), AppNames=make_set(AppDisplayName,20), IPs=make_set(IPAddress,20) by UserPrincipalName, bin(TimeGenerated,30m)
| where Apps >= 4
| order by Apps desc, LastSeen desc

Microsoft-Provided Hunt – New MFA Phone or Authenticator Device

CloudAppEvents
| where ActionType == "Update user."
| where tostring(RawEventData.ResultStatus) == "Success"
| where RawEventData has_any ("StrongAuthenticationPhoneAppDetail", "StrongAuthenticationUserDetails")
| extend AccountObjectId = extract(@"User_([a-f0-9\-]+)", 1, tostring(RawEventData.Target))
| where isnotempty(AccountObjectId)
| mvexpand ModifiedProp = RawEventData.ModifiedProperties
| where tostring(ModifiedProp.Name) in ("StrongAuthenticationPhoneAppDetail", "StrongAuthenticationUserDetails")
| extend OldValue = tostring(ModifiedProp.OldValue), NewValue = tostring(ModifiedProp.NewValue)
| extend OldDeviceCount = countof(OldValue, @"""Id"""), NewDeviceCount = countof(NewValue, @"""Id""")
| where NewDeviceCount > OldDeviceCount

Microsoft-Provided Hunt – Broad Graph Reconnaissance in One Session

let Lookback = 24h;
GraphAPIAuditEvents
| where Timestamp > ago(Lookback)
| where toint(ResponseStatusCode) between (200 .. 299)
| extend Uri = tolower(RequestUri), ActorId = coalesce(AccountObjectId, ServicePrincipalId, ApplicationId), Path = tostring(split(tolower(RequestUri), "?")[0])
| extend ReconType = case(Uri has "/organization" or Uri has "/subscribedskus", "Tenant", Uri has "/users" or Uri has "/groups", "Directory", Uri has "/directoryroles" or Uri has "/rolemanagement", "Privilege", Uri has "/applications" or Uri has "/serviceprincipals" or Uri has "/oauth2permissiongrants", "Application", Uri has "/sites" or Uri has "/drive", "Repository", Uri has "/messages" or Uri has "/mailfolders", "Mailbox", "Other")
| where ReconType != "Other" and isnotempty(ActorId)
| summarize Requests=count(), Categories=dcount(ReconType), DistinctPaths=dcount(Path), ReconTypes=make_set(ReconType,10), SampleUris=make_set(RequestUri,10) by ActorId, IpAddress, ApplicationId, bin(Timestamp,30m)
| where Requests >= 10 and Categories >= 3 and DistinctPaths >= 6
| order by Categories desc, Requests desc

Microsoft-Provided Hunt – Privilege, MFA, Application, and Consent Discovery

GraphAPIAuditEvents
| where Timestamp > ago(24h)
| where toint(ResponseStatusCode) between (200 .. 299)
| extend Uri = tolower(RequestUri), ActorId = coalesce(AccountObjectId, ServicePrincipalId, ApplicationId)
| where Uri has_any ("/directoryroles", "/rolemanagement", "/authentication/methods", "/applications", "/serviceprincipals", "/oauth2permissiongrants", "/approleassign")
| summarize Requests=count(), DistinctPaths=dcount(tostring(split(Uri, "?")[0])), ScopesSeen=make_set(Scopes,10), SampleUris=make_set(RequestUri,12) by ActorId, IpAddress, ApplicationId, bin(Timestamp,30m)
| where Requests >= 4 and DistinctPaths >= 2
| order by Requests desc

Microsoft-Provided Hunt – SharePoint and OneDrive Repository Discovery

GraphAPIAuditEvents
| where Timestamp > ago(24h)
| where toint(ResponseStatusCode) between (200 .. 299)
| extend Uri = tolower(RequestUri), ActorId = coalesce(AccountObjectId, ServicePrincipalId, ApplicationId), Path = tostring(split(tolower(RequestUri), "?")[0])
| where Uri has_any ("/sites", "/drives", "/drive/")
| where Uri has_any ("/search", "/children", "/delta", "$skiptoken", "%24skiptoken", "$top", "%24top")
| summarize Requests=count(), DistinctPaths=dcount(Path), SampleUris=make_set(RequestUri,12) by ActorId, IpAddress, ApplicationId, bin(Timestamp,20m)
| where Requests >= 8 and DistinctPaths >= 4
| order by Requests desc

Microsoft-Provided Hunt – Mailbox and Attachment Reconnaissance

GraphAPIAuditEvents
| where Timestamp > ago(24h)
| where toint(ResponseStatusCode) between (200 .. 299)
| extend Uri = tolower(RequestUri), ActorId = coalesce(AccountObjectId, ServicePrincipalId, ApplicationId), Path = tostring(split(tolower(RequestUri), "?")[0])
| where Uri has_any ("/messages", "/mailfolders", "/attachments")
| summarize Requests=count(), DistinctPaths=dcount(Path), MessageRequests=countif(Uri has "/messages"), AttachmentRequests=countif(Uri has "/attachments"), TotalResponseBytes=sum(coalesce(ResponseSize,0)), SampleUris=make_set(RequestUri,12) by ActorId, IpAddress, ApplicationId, bin(Timestamp,30m)
| where (Requests >= 8 and DistinctPaths >= 4) or AttachmentRequests >= 3
| order by AttachmentRequests desc, Requests desc

Microsoft-Provided Hunt – Graph Reconnaissance Progressing to Content Collection

GraphAPIAuditEvents
| where Timestamp > ago(24h)
| where toint(ResponseStatusCode) between (200 .. 299)
| extend Uri = tolower(RequestUri), ActorId = coalesce(AccountObjectId, ServicePrincipalId, ApplicationId)
| extend ActivityType = case(Uri has "/content" or Uri has "/attachments", "ContentCollection", Uri has "/users" or Uri has "/groups", "DirectoryRecon", Uri has "/directoryroles" or Uri has "/rolemanagement" or Uri has "/authentication/methods", "PrivilegeRecon", Uri has "/applications" or Uri has "/serviceprincipals", "ApplicationRecon", Uri has "/sites" or Uri has "/drives" or Uri has "/drive/", "RepositoryRecon", Uri has "/messages" or Uri has "/mailfolders", "MailboxRecon", "Other")
| where ActivityType != "Other"
| summarize DiscoveryFirst=minif(Timestamp, ActivityType != "ContentCollection"), CollectionFirst=minif(Timestamp, ActivityType == "ContentCollection"), DiscoveryCategories=dcountif(ActivityType, ActivityType != "ContentCollection"), ContentRequests=countif(ActivityType == "ContentCollection"), TotalResponseBytes=sum(coalesce(ResponseSize,0)), SampleUris=make_set(RequestUri,15) by ActorId, IpAddress, ApplicationId, bin(Timestamp,1h)
| where isnotnull(DiscoveryFirst) and isnotnull(CollectionFirst)
| where CollectionFirst >= DiscoveryFirst and DiscoveryCategories >= 2 and ContentRequests >= 1
| order by CollectionFirst desc

SharePoint and OneDrive Sustained File Access From OfficeActivity

OfficeActivity
| where TimeGenerated > ago(14d)
| where OfficeWorkload in~ ("SharePoint","OneDrive")
| where Operation in~ ("FileAccessed","FileDownloaded")
| summarize Events=count(), Files=dcount(SourceFileName), Sites=dcount(Site_Url), Operations=make_set(Operation,10) by UserId, ClientIP, bin(TimeGenerated,1h)
| where Events >= 100
| order by Events desc

Known Passkey and SSO Lure Domains in Defender URL Click Telemetry

let LureDomains = dynamic(["passkeyhelpdesk.com","secure-passkey.com","setupmypasskey.com","add-passkey.com","integratedsso.com","oktasession.com","keysyncos.com","oskeysync.com","oskeysetup.com","oskeyregister.com","syncmykey.com","myconnectkey.com","oskeyconnect.com","validationsetupac.com","portalsetuphub.com"]);
UrlClickEvents
| where Timestamp > ago(30d)
| extend Host=tostring(parse_url(Url).Host)
| where Host in~ (LureDomains) or Host endswith_any (LureDomains)
| project Timestamp, AccountUpn, Url, ActionType, ThreatTypes, IsClickedThrough, NetworkMessageId
| order by Timestamp desc

Detection Notes

The Microsoft-provided GraphAPIAuditEvents and CloudAppEvents queries retain their native Timestamp field because those Defender XDR tables use Timestamp rather than TimeGenerated. The Sentinel-native SigninLogs and OfficeActivity fallbacks use TimeGenerated. A single Graph request, python-httpx user agent, or lure-domain match is not sufficient to establish compromise. Highest signal comes from sequence correlation: unusual sign-in or device-code activity, new MFA registration, broad Graph reconnaissance, and subsequent content collection. If GraphAPIAuditEvents, CloudAppEvents, Entra sign-ins, OfficeActivity, or mailbox auditing are not ingested, major phases may not be visible. Initial social engineering on personal devices may leave no managed endpoint artifact.

Leave a comment