TeamFiltration UNK_CondorFiltration – Microsoft 365 Service Account Password Spraying

Threat Overview

Proofpoint disclosed an active TeamFiltration campaign tracked as UNK_CondorFiltration that targeted more than 5,700 Microsoft 365 accounts across 28 tenants. The campaign used 1,487 unique AWS EC2 source IP addresses. Seven accounts were successfully compromised. All seven were unmanaged functional or service accounts with no prior legitimate login baseline, default or unrotated passwords, and no MFA. Proofpoint observed post-compromise access from a German VPN node, attempted corporate VPN authentication, and access to OfficeHome, Azure Portal, and SharePoint Online.

References

Impacted Systems

Microsoft 365 and Microsoft Entra ID tenants, especially functional, shared, service, or other non-human accounts that retain default or predictable passwords, have no MFA, or lack a normal interactive sign-in baseline. Proofpoint observed AWS EC2 attack infrastructure and later access from a German VPN node.

Why this matters

The campaign demonstrates a common identity gap: service and functional accounts can remain outside normal MFA and lifecycle controls. Successful password spraying can transition directly into Microsoft 365, Azure Portal, SharePoint, and potentially corporate VPN access without endpoint malware.

Exploitation Status

Confirmed active campaign. Proofpoint observed more than 5,700 targeted accounts across 28 tenants and seven successful compromises. The reported waves occurred from late July through August 2026.

What this hunt looks for

Distributed failed sign-ins, successful authentication after repeated failures, first-ever or rare interactive sign-ins, successful sign-ins without MFA, new source IPs, country changes, and access to Azure Portal, OfficeHome, or SharePoint Online.

Required logs

Microsoft Entra SigninLogs and AADNonInteractiveUserSignInLogs. Identity Protection telemetry can strengthen prioritization where available.

First Pass – Password Spray by IP

SigninLogs
| where TimeGenerated >= ago(7d)
| where ResultType != 0
| summarize FailedUsers=dcount(UserPrincipalName),Failures=count(),Users=make_set(UserPrincipalName,50) by IPAddress,bin(TimeGenerated,15m)
| where FailedUsers >= 10 and Failures >= 20
| order by FailedUsers desc,Failures desc

Distributed Spray Against Individual Accounts

SigninLogs
| where TimeGenerated >= ago(7d)
| where ResultType != 0
| summarize SourceIPs=dcount(IPAddress),Failures=count(),IPs=make_set(IPAddress,50) by UserPrincipalName,bin(TimeGenerated,1h)
| where SourceIPs >= 10 and Failures >= 20
| order by SourceIPs desc,Failures desc

Success After Repeated Failures

let Failures=SigninLogs
| where TimeGenerated >= ago(7d)
| where ResultType != 0
| summarize FailedAttempts=count(),FailureIPs=dcount(IPAddress),LastFailure=max(TimeGenerated) by UserPrincipalName;
SigninLogs
| where TimeGenerated >= ago(7d)
| where ResultType == 0
| join kind=inner Failures on UserPrincipalName
| where FailedAttempts >= 10 and TimeGenerated >= LastFailure
| project TimeGenerated,UserPrincipalName,IPAddress,AppDisplayName,ClientAppUsed,AuthenticationRequirement,ConditionalAccessStatus,FailedAttempts,FailureIPs,LastFailure
| order by TimeGenerated desc

Successful Sign-Ins Without MFA

SigninLogs
| where TimeGenerated >= ago(7d)
| where ResultType == 0
| where AuthenticationRequirement !~ "multiFactorAuthentication"
| project TimeGenerated,UserPrincipalName,IPAddress,Location,AppDisplayName,ClientAppUsed,AuthenticationRequirement,ConditionalAccessStatus,UserAgent
| order by TimeGenerated desc

First Seen Interactive Sign-In

let Baseline=SigninLogs
| where TimeGenerated between (ago(30d)..ago(7d))
| where ResultType == 0
| summarize by UserPrincipalName;
SigninLogs
| where TimeGenerated >= ago(7d)
| where ResultType == 0
| join kind=leftanti Baseline on UserPrincipalName
| project TimeGenerated,UserPrincipalName,IPAddress,Location,AppDisplayName,ClientAppUsed,AuthenticationRequirement,ConditionalAccessStatus
| order by TimeGenerated desc

New Source IP for Account

let Baseline=SigninLogs
| where TimeGenerated between (ago(30d)..ago(7d))
| where ResultType == 0
| summarize by UserPrincipalName,IPAddress;
SigninLogs
| where TimeGenerated >= ago(7d)
| where ResultType == 0
| join kind=leftanti Baseline on UserPrincipalName,IPAddress
| project TimeGenerated,UserPrincipalName,IPAddress,Location,AppDisplayName,ClientAppUsed,AuthenticationRequirement
| order by TimeGenerated desc

Azure Portal OfficeHome and SharePoint Access

SigninLogs
| where TimeGenerated >= ago(7d)
| where ResultType == 0
| where AppDisplayName has_any ("Azure Portal","OfficeHome","SharePoint")
| project TimeGenerated,UserPrincipalName,IPAddress,Location,AppDisplayName,ClientAppUsed,AuthenticationRequirement,ConditionalAccessStatus
| order by TimeGenerated desc

Non-Interactive Service Account Failures

AADNonInteractiveUserSignInLogs
| where TimeGenerated >= ago(7d)
| where ResultType != 0
| summarize FailedIPs=dcount(IPAddress),Failures=count(),Apps=make_set(AppDisplayName,20) by UserPrincipalName,bin(TimeGenerated,1h)
| where FailedIPs >= 10 or Failures >= 20
| order by Failures desc

Country Change on Successful Sign-In

SigninLogs
| where TimeGenerated >= ago(14d)
| where ResultType == 0
| extend Country=tostring(LocationDetails.countryOrRegion)
| summarize Countries=make_set(Country,10),CountryCount=dcount(Country),IPs=make_set(IPAddress,20),Apps=make_set(AppDisplayName,20) by UserPrincipalName
| where CountryCount >= 2
| order by CountryCount desc

Detection Notes

The primary signal is behavioral, not a static IOC list. Proofpoint observed 1,487 AWS EC2 source IPs, so IP blocking alone is insufficient. Functional and service accounts may legitimately use non-interactive flows, making account role and historical baseline important. Correlate no-MFA successes with Conditional Access and the account’s intended authentication model.

Leave a comment