|
| 1 | +[metadata] |
| 2 | +creation_date = "2025/05/01" |
| 3 | +integration = ["o365"] |
| 4 | +maturity = "production" |
| 5 | +updated_date = "2025/05/01" |
| 6 | +min_stack_version = "8.17.0" |
| 7 | +min_stack_comments = "Elastic ES|QL values aggregation is more performant in 8.16.5 and above." |
| 8 | + |
| 9 | +[rule] |
| 10 | +author = ["Elastic"] |
| 11 | +description = """ |
| 12 | +Identifies sign-ins on behalf of a principal user to the Microsoft Graph API from multiple IPs using the Microsoft |
| 13 | +Authentication Broker or Visual Studio Code application. This behavior may indicate an adversary using a phished OAuth |
| 14 | +refresh token. |
| 15 | +""" |
| 16 | +from = "now-1h" |
| 17 | +language = "esql" |
| 18 | +license = "Elastic License v2" |
| 19 | +name = "Suspicious Microsoft 365 UserLoggedIn via OAuth Code" |
| 20 | +note = """## Triage and analysis |
| 21 | +
|
| 22 | +### Investigating Suspicious Microsoft 365 UserLoggedIn via OAuth Code |
| 23 | +
|
| 24 | +### Possible Investigation Steps: |
| 25 | +
|
| 26 | +- `o365.audit.UserId`: The identity value the application is acting on behalf of principal user. |
| 27 | +- `unique_ips`: Analyze the list of unique IP addresses used within the 30-minute window. Determine whether these originate from different geographic regions, cloud providers, or anonymizing infrastructure (e.g., Tor or VPNs). |
| 28 | +- `target_time_window`: Use the truncated time window to pivot into raw events to reconstruct the full sequence of resource access events, including exact timestamps and service targets. |
| 29 | +- `azure.auditlogs` to check for device join or registration events around the same timeframe. |
| 30 | +- `azure.identityprotection` to identify correlated risk detections, such as anonymized IP access or token replay. |
| 31 | +- Any additional sign-ins from the `ips` involved, even outside the broker, to determine if tokens have been reused elsewhere. |
| 32 | +
|
| 33 | +### False Positive Analysis |
| 34 | +
|
| 35 | +- Developers or IT administrators working across environments may also produce similar behavior. |
| 36 | +
|
| 37 | +### Response and Remediation |
| 38 | +
|
| 39 | +- If confirmed unauthorized, revoke all refresh tokens for the affected user and remove any devices registered during this session. |
| 40 | +- Notify the user and determine whether the device join or authentication activity was expected. |
| 41 | +- Audit Conditional Access and broker permissions (`29d9ed98-a469-4536-ade2-f981bc1d605e`) to ensure policies enforce strict access controls. |
| 42 | +- Consider blocking token-based reauthentication to Microsoft Graph and DRS from suspicious locations or user agents. |
| 43 | +- Continue monitoring for follow-on activity like lateral movement or privilege escalation. |
| 44 | +""" |
| 45 | +references = [ |
| 46 | + "https://www.volexity.com/blog/2025/04/22/phishing-for-codes-russian-threat-actors-target-microsoft-365-oauth-workflows/", |
| 47 | + "https://github.com/dirkjanm/ROADtools", |
| 48 | + "https://dirkjanm.io/phishing-for-microsoft-entra-primary-refresh-tokens/", |
| 49 | +] |
| 50 | +risk_score = 73 |
| 51 | +rule_id = "36188365-f88f-4f70-8c1d-0b9554186b9c" |
| 52 | +setup = """## Setup |
| 53 | +
|
| 54 | +The Office 365 Logs Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule. |
| 55 | +""" |
| 56 | +severity = "high" |
| 57 | +tags = [ |
| 58 | + "Domain: Cloud", |
| 59 | + "Data Source: Microsoft 365", |
| 60 | + "Use Case: Identity and Access Audit", |
| 61 | + "Use Case: Threat Detection", |
| 62 | + "Resources: Investigation Guide", |
| 63 | + "Tactic: Defense Evasion", |
| 64 | +] |
| 65 | +timestamp_override = "event.ingested" |
| 66 | +type = "esql" |
| 67 | + |
| 68 | +query = ''' |
| 69 | +from logs-o365.audit-default* |
| 70 | +| WHERE event.dataset == "o365.audit" and event.action == "UserLoggedIn" and |
| 71 | + source.ip is not null and o365.audit.UserId is not null and o365.audit.ApplicationId is not null and o365.audit.UserType in ("0", "2", "3", "10") and |
| 72 | +
|
| 73 | + // filter for successful logon to Microsoft Graph and from the Microsoft Authentication Broker or Visual Studio Code |
| 74 | + o365.audit.ApplicationId in ("aebc6443-996d-45c2-90f0-388ff96faa56", "29d9ed98-a469-4536-ade2-f981bc1d605e") and |
| 75 | + o365.audit.ObjectId in ("00000003-0000-0000-c000-000000000000") |
| 76 | +
|
| 77 | +// keep relevant fields only |
| 78 | +| keep @timestamp, o365.audit.UserId, source.ip, o365.audit.ApplicationId, o365.audit.ObjectId, o365.audit.ExtendedProperties.RequestType, source.as.organization.name, o365.audit.ExtendedProperties.ResultStatusDetail |
| 79 | +
|
| 80 | +// case statements to track which are OAuth2 authorization request via redirect and which are related to OAuth2 code to token conversion |
| 81 | +| eval oauth_authorize = case(o365.audit.ExtendedProperties.RequestType == "OAuth2:Authorize" and o365.audit.ExtendedProperties.ResultStatusDetail == "Redirect", o365.audit.UserId, null), oauth_token = case(o365.audit.ExtendedProperties.RequestType == "OAuth2:Token", o365.audit.UserId, null) |
| 82 | +
|
| 83 | +// split time to 30 minutes intervals |
| 84 | +| eval target_time_window = DATE_TRUNC(30 minutes, @timestamp) |
| 85 | +
|
| 86 | +// aggregate by principal, applicationId, objectId and time window |
| 87 | +| stats unique_ips = COUNT_DISTINCT(source.ip), source_ips = VALUES(source.ip), appIds = VALUES(o365.audit.ApplicationId), asn = values(`source.as.organization.name`), is_oauth_token = COUNT_DISTINCT(oauth_token), is_oauth_authorize = COUNT_DISTINCT(oauth_authorize) by o365.audit.UserId, target_time_window, o365.audit.ApplicationId, o365.audit.ObjectId |
| 88 | +
|
| 89 | +// filter for cases where the same appId is used by the same principal user to access the same object and from multiple addresses via OAuth2 token |
| 90 | +| where unique_ips >= 2 and is_oauth_authorize > 0 and is_oauth_token > 0 |
| 91 | +''' |
| 92 | + |
| 93 | + |
| 94 | +[[rule.threat]] |
| 95 | +framework = "MITRE ATT&CK" |
| 96 | +[[rule.threat.technique]] |
| 97 | +id = "T1550" |
| 98 | +name = "Use Alternate Authentication Material" |
| 99 | +reference = "https://attack.mitre.org/techniques/T1550/" |
| 100 | +[[rule.threat.technique.subtechnique]] |
| 101 | +id = "T1550.001" |
| 102 | +name = "Application Access Token" |
| 103 | +reference = "https://attack.mitre.org/techniques/T1550/001/" |
| 104 | + |
| 105 | + |
| 106 | + |
| 107 | +[rule.threat.tactic] |
| 108 | +id = "TA0005" |
| 109 | +name = "Defense Evasion" |
| 110 | +reference = "https://attack.mitre.org/tactics/TA0005/" |
0 commit comments