Basic Logging and Attack Detection in Microsoft Graph
Introduction
Microsoft Graph has simplified access to data and actions across Microsoft's various cloud products. This is a useful single point for both legitimate use cases and for attackers seeking to turn stolen credential material into tangible impact. With Continuous Access Evaluation currently only targeting direct interaction with Exchange, Teams, and SharePoint Online, attackers can use tokens for Microsoft Graph outside of an organisation's network, ignoring Conditional Access Policies for the lifetime of the token.
The introduction of Microsoft Graph Activity Logs begins to level the playing field for defenders, granting them visibility to attacker actions. This blog post aims to give the reader a baseline understanding of how these logs, along with the existing sign-in logs in Entra ID, can give indicators of a basic attack chain involving the Microsoft Graph API. The advice outlined will have to be tuned to your environment, with deviations from the norm likely to be more useful than any simple signatures, but this will provide a base to begin that exercise.
Enabling the Logs
All the log types discussed in this post can be enabled and forwarded to a Log Analytics Workspace, Event Hub, and/or Storage Account through "Diagnostic Settings" on the relevant Entra ID tenant.
Structure of a Log Entry
The full list of fields recorded in a Microsoft Graph Activity log entry are in the documentation. The most relevant fields for attack detection are:
Field | Use for Defenders |
AadTenantId | Shows the tenant targeted by the Graph API request. Not to be confused with the "TenantId" field that gives the ID of the Log Analytics Workspace. |
RequestId | Unique ID of a request, useful for referencing. |
RequestMethod | The HTTP method used for the API request. Allows differentiation between enumeration (broadly GET) and state-changing (broadly POST) actions. |
ResponseStatusCode | The HTTP status code returned from the API request. Vital for identifying whether the attacker action succeeded. |
IPAddress | The originating IP of the request. Useful for identifying Microsoft Graph queries from outside of an organisation's network. |
UserAgent | The UserAgent supplied in the HTTP request, useful for identifying manual queries through curl etc., or otherwise anomalous User Agents for the expected client application. |
RequestUri | Shows the actual API endpoint queried, and therefore the action attempted. |
ResponseSizeBytes | Untested, but theoretically could be useful for identifying abnormally large downloads/exfiltrations via Microsoft Graph. |
TokenIssuedAt | Allows correlation with sign-in events. |
AppId | Shows the Client ID used to request the token presented to Microsoft Graph. Different applications can request different permissions to Microsoft Graph but additionally may have different "normal" behaviour to baseline against. |
UserId | The user identity making the request to Microsoft Graph. |
Scopes | The permissions that the presented token has to the Microsoft Graph API. Each client application has a different set of scopes it can request to Microsoft Graph, those used by an attacker manually querying may be different to those normally requested by a client application. |
ClientAuthMethod | Shows if and how any authentication steps were enforced by the client application. |
Wids | Shows tenant-wide roles possessed by the calling user, useful for quickly filtering to actions performed by privileged user accounts. |
While Microsoft Graph Activity logs show actions taken by authenticated users, they cannot show the actual authentication events. These take place in a different part of the Microsoft Cloud ecosystem and Microsoft Graph simply takes in an uses the tickets. To get the full picture of attacker actions in Microsoft Graph, it is therefore necessary to also ingest SignInLogs and NonInteractiveSignInLogs. The matching Log Analytics Workspace tables take the names "SigninLogs" and "AADNonInteractiveUserSignInLogs".
Field | Use for Defenders |
TimeGenerated | Shows the time that the log event was created. |
CreatedDateTime | Shows time of the actual authentication event. |
AppDisplayName, AppId | Both show the client application the authentication request claims to originate from. Useful for spotting anomalous client behaviour. |
AuthenticationDetails | Contains information about whether and when MFA requirements were satisfied, for instance indicating they were previously satisfied when a refresh token was created. |
AuthenticationProcessingDetails | Details scopes requested, allowing anomalous and potentially privileged scope requests to be identified. |
AuthenticationRequirement | Shows whether the request requires multi-factor authentication (including prior multi-factor authentication), allowing quick identification of exploitation of a gap in MFA enforcement. |
ConditionalAccessPolicies, ConditionalAccessStatus | Shows Conditional Access Policies applied to an authentication attempt, allowing identification of gaps being exploited. |
CrossTenantAccessType | Allows identification of cross-tenant access. |
DeviceDetail | Displays device information provided in the headers of the authentication request including the User-Agent. |
HomeTenantId | Home tenant ID of the principal authenticating, allowing both identification of cross-tenant activity and of the tenant affected by an attack. |
IPAddress | Allows quick identification of authentication attempts from outside of an organisation's network. |
LocationDetails | Checks location data of IP address, allows quick identification of known-bad locations (if an attacker's VPN drops at an inopportune moment) |
ResourceDisplayName, ResourceIdentity | Identifies the target resource for the authentication request, in our case Microsoft Graph or 00000003-0000-0000-c000-000000000000 |
RiskDetail, RiskEventTypes_V2, RiskLevelAggregated, RiskLevelDuringSignIn, RiskState | Allows ingesting of sign-in risk as calculated by Entra. |
UserAgent | UserAgent in authentication request, useful for identifying use of common tools or otherwise manual/anomalous method of authentication. |
UserId, UserPrincipalName, UserDisplayName, UserType | Identify the user account being authenticated to. |
SignIn Logs have all these fields, with one additional of interest:
Field | Use for Defenders |
AuthenticationProtocol | Tells us if authentication was performed through a process such as device code grants. |
Example Log Entries
To demonstrate the utility of these three log types in detecting an attack, we can look at an example path in which an attacker performs device code phishing, requests a new access token as a different client ID, and uses this to read emails. This will not go into significant depth on the attack steps to keep the focus defensive, the relevant research that does show these is linked along the way.
To begin with, we can initiate device code grant for a specific client ID within the "family of client IDs" (https://github.com/secureworks/family-of-client-ids-research/tree/main) , targeting one that matches a reasonable phishing pretext as described in the Family of Client IDs research repository (https://github.com/secureworks/family-of-client-ids-research/tree/main#device-code-phishing). For the example, we can use the tool roadtx (https://github.com/dirkjanm/ROADtools) to initiate this as follows for Microsoft Authenticator App:
roadtx auth --device-code -c 4813382a-8fa7-425e-ab75-3b753aab3abb
If the attacker successfully convinces the target to input the device code to an authenticated session, they will receive access and refresh tokens and generate an interactive sign-in log entry.
Whilst mimicking a client such as the Microsoft Authenticator App may assist in the phishing pretext for device code phishing, the fact that this client never normally uses the device code flow should make the sign-in event stick out. The key features of this log entry would be:
Field | Value |
AuthenticationProtocol | deviceCode |
AppDisplayName | Microsoft Authenticator App (or other client application that rarely/never uses device code flow) |
AppId | 4813382a-8fa7-425e-ab75-3b753aab3abb (or other client application ID that rarely/never uses device code flow) |
ResourceDisplayName | Windows Azure Active Directory (legacy Azure AD Graph, commonly missed by Conditional Access Policies and default target for ROADtools |
ResourceIdentity | 00000002-0000-0000-c000-000000000000 (legacy Azure AD Graph, commonly missed by Conditional Access Policies and default target for ROADtools) |
IPAddress | Shows the public IP address associated with the phished user's browser/device. |
Broadly, this entry covers the authentication that the user performs in the browser as they type in the device code. Another event is created in the AADNonInteractiveUserSignInLogs table for the other participating device in the authentication. When device code phishing is performed this way, a few points of interest appear in the logs:
Field | Value |
AppDisplayName | Microsoft Authenticator App (or other client application that rarely/never uses device code flow) |
AppId | 4813382a-8fa7-425e-ab75-3b753aab3abb (or other client application ID that rarely/never uses device code flow) |
ResourceDisplayName | Windows Azure Active Directory (legacy Azure AD Graph, commonly missed by Conditional Access Policies and default target for ROADtools) |
ResourceIdentity | 00000002-0000-0000-c000-000000000000 (legacy Azure AD Graph, commonly missed by Conditional Access Policies and default target for ROADtools) |
IPAddress | Shows the public IP address associated with the attackers. Is subject to Conditional Access checks so indicates a Conditional Access bypass if not inside an organisation. |
UserAgent | python-urlib3/2.2.0 (or similar depending on version. User Agent can be changed in roadtx but if not specified, defaults to python. Should be compared with claimed client application -- a custom internal application may use a python User-Agent, Microsoft Authenticator App should not) |
Once an attacker has a refresh token for a client in the family of client IDs, they may attempt to use this to request a token for another client that has the permissions they want. The act of using a refresh token to request an access token lands in the AADNonInteractiveUserSignInLogs table. Such a request in roadtx, targeting the Microsoft Office client id, may look like:
roadtx gettoken -c d3590ed6-52b3-4102-aeff-aad2292ab01c -r https://graph.microsoft.com --refresh-token $refresh
Under the hood, this is simply performing the refresh token flow detailed in Microsoft documentation, which can be performed manually. However, if performed in this manner using this tool, a few IOCs are left in the logs:
Field | Value |
AppDisplayName | Microsoft Office (or other client application in the family of client IDs) |
AppId | d3d76c7c-de54-4573-93e1-777570124800 (or other client application ID in the family of client IDs) |
ResourceDisplayName | Microsoft Graph |
ResourceIdentity | 00000003-0000-0000-c000-000000000000 (Microsoft Graph) |
IPAddress | Shows the public IP address associated with the attacker's origin -- may be inside the organisation network if they have an implant on a device |
UserAgent | python-requests/2.31.0 (version number may change, default used by roadtx if no other is specified. Should be compared with supposed client application, an internal app may use this but Microsoft Office should not) |
AuthenticationProcessingDetails | {"key":"Oauth Scope Info","value":"["AuditLog.Read.All","Calendar.ReadWrite","Calendars.Read.Shared","Calendars.ReadWrite"... |
The set of OAuth scopes requested has been trimmed, and can be seen in full below:
{"key":"Oauth Scope Info","value":"[\"AuditLog.Read.All\",\"Calendar.ReadWrite\",\"Calendars.Read.Shared\",\"Calendars.ReadWrite\",\"Contacts.ReadWrite\"\"DataLossPreventionPolicy.Evaluate\",\"Directory.AccessAsUser.All\",\"Directory.Read.All\",\"Files.Read\",\"Files.Read.All\",\"Files.ReadWrite.All\",\"Group.Read.All\",\"Group.ReadWrite.All\",\"InformationProtectionPolicy.Read\",\"Mail.ReadWrite\",\"Mail.Send\",\"Notes.Create\",\"Organization.Read.All\",\"People.Read\",\"People.Read.All\",\"PrintJob.ReadWriteBasic\",\"SensitiveInfoType.Detect\",\"SensitiveInfoType.Read.All\",\"SensitivityLabel.Evaluate\",\"Tasks.ReadWrite\",\"TeamMember.ReadWrite.All\",\"TeamsTab.ReadWriteForChat\",\"User.Read.All\",\"User.ReadBasic.All\",\"User.ReadWrite\",\"Users.Read\",\"Printer.Read.All\"]"}
roadtx allows specifying scopes to request, but will default to requesting a ".default", which equates in this instance to a large number of scopes. This should be compared with the behaviour of legitimate Microsoft clients in an organisation over a long period of time.
Once an attacker has a token with delegated privileges to Microsoft Graph, they can begin to make requests of the API. From this point, an attacker is no longer bound by Conditional Access Policies -- until their current token expires and they need to re-refresh. An attacker can read a user's emails through Microsoft Graph roughly as follows (omitting additional parameters for filtering and specifying number of messages for simplicity):
curl -H "Authorization: Bearer $accesstoken" https://graph.microsoft.com/v1.0/me/messages
This will leave the following in the MicrosoftGraphActivityLogs table:
Field | Value |
RequestMethod | GET |
ResponseStatusCode | 200 |
IPAddress | The originating IP of the request. More likely to be attacker-controlled IP address as requests no longer have to obey or bypass Conditional Access Policies, but a more careful attacker may still proxy requests through an implant on an organisation's device. |
UserAgent | curl/8.5.0 (once again, can be just about anything but can be compared to legitimate behaviour of the claimed client ID when calling Microsoft Graph). |
RequestUri | https://graph.microsoft.com/v1.0/me/messages |
ResponseSizeBytes | Untested, but theoretically could be useful for identifying abnormally large downloads/exfiltrations via Microsoft Graph |
TokenIssuedAt | (highlighting again for usefulness in backtracing authentication event) |
AppId | d3590ed6-52b3-4102-aeff-aad2292ab01c (in this case, Microsoft Office -- should allow identifying whether this is expected behaviour for the claimed client ID) |
Scopes | AuditLog.Read.All Calendar.ReadWrite... (the same long list from before) |
Whilst this example has followed the chronology of the attack, this may not be the order that a defender receives this information. For example, if strong Conditional Access Policies are in place then an attacker would have to operate from within an organisation's network while acquiring their tokens for Microsoft Graph. If they do not believe the defenders are ingesting or alerting on Microsoft Graph Activity logs, they may then proceed to act on the Microsoft Graph API from an arbitrary IP address, potentially triggering an alert based on this IP address appearing in the final request of the chain. From there, a defender would have to work backwards through the other log sources to identify the initial authentication events.
Mapping Microsoft Graph to MITRE ATT&CK
Now we’ve seen one example of the use of Microsoft Graph logging in combination with sign-in logs, we can look at identifying which Microsoft Graph API calls could be used by attackers. To that end, I have compared Microsoft Graph API reference documentation with the MITRE ATT&CK framework and compiled this into a “layer” file for the MITRE ATT&CK® Navigator (mitre-attack.github.io). This JSON file assigns Graph v1.0 endpoints for each relevant technique. This is intended to act as a starting point for detection engineering, threat hunting, and incident response based on these actions. Microsoft obviously do not include inherently malicious Graph API endpoints, they only become malicious if used for ill. Therefore, any detections based on this logging must be based on the specific behaviour seen in your organisation.