As Red Canary continues to observe OAuth application attacks in the wild, our Threat Research team is pivoting off real-world tradecraft to anticipate new innovations in attack techniques.
The following research breaks down a hypothetical OAuth attack in Entra ID that leverages ChatGPT to ultimately gain access to a user’s email account. Using the framework we apply when analyzing data sources for detection, we’ll investigate detection and remediation strategies that can be applied more generally to OAuth consent attacks.
What could happen
In this scenario, the following event occurs:
At 2025-12-02T20:22:16, within Entra ID tenant ID
747930ee-9a33-43c0-9d5d-470b3fb855e7,TestUser@ContosoCorp.onmicrosoft.com(ID:1daac687-c3b3-4aad-8111-4bac9568a064) added a new third-party service principal, ChatGPT (App ID:e0476654-c1d5-430b-ab80-70cbd947616a) and consented as a non-admin to the following Microsoft Graph (App ID:00000003-0000-0000-c000-000000000000) OAuth permissions:Mail.Read offline_access profile openid. This action was performed from the following IP address:3.89.177.26.
This ChatGPT application is indeed the legitimate OpenAI application that was investigated due to its use of one or more OAuth permissions that are frequently abused, in this case, Mail.Read. In the end, this investigation resulted in a benign classification but the investigation steps followed a similar sequence to an incident we observed in the wild.
Summarized at a higher level, this means that the ChatGPT app has access to read the emails of TestUser@ContosoCorp.onmicrosoft.com. The following questions may arise in the course of an investigation:
- Did the legitimate user behind the
TestUser@ContosoCorp.onmicrosoft.comidentity actually mean to use this application? Was the user somehow coerced into this consent? - Is it authorized for this app to read this user’s email?
- Is the ChatGPT app actually from OpenAI or is it merely posing as a legitimate looking application?
- Is this app actually sanctioned within this tenant?
In order to answer these questions, we need data, specifically, the following Log Analytics AuditLogs events are required and are correlated via the CorrelationId property:
OperationName: Consent to applicationOperationName: Add service principal
Here is the actual (albeit, obfuscated) data that was generated for this case study:
{
"TenantId": "52672484-b4e1-402d-934c-a8e2fd9b05d1",
"SourceSystem": "Azure AD",
"TimeGenerated": "2025-12-02T20:22:16.1185371Z",
"ResourceId": "/tenants/747930ee-9a33-43c0-9d5d-470b3fb855e7/providers/Microsoft.aadiam",
"OperationName": "Add service principal",
"OperationVersion": "1.0",
"Category": "ApplicationManagement",
"ResultType": "",
"ResultSignature": "None",
"ResultDescription": "",
"DurationMs": "0",
"CorrelationId": "f540cbd8-9ec4-4d0e-855c-86e8916c3a1b",
"Resource": "Microsoft.aadiam",
"ResourceGroup": "Microsoft.aadiam",
"ResourceProvider": "",
"Identity": "Azure ESTS Service",
"Level": "4",
"Location": "",
"AdditionalDetails": [
{
"key": "User-Agent",
"value": "EvoSTS"
},
{
"key": "AppId",
"value": "e0476654-c1d5-430b-ab80-70cbd947616a"
},
{
"key": "AppOwnerOrganizationId",
"value": "a48cca56-e6da-484e-a814-9c849652bcb3"
}
],
"Id": "Directory_f540cbd8-9ec4-4d0e-855c-86e8916c3a1b_XC60C_97530478",
"InitiatedBy": {
"user": {
"displayName": "Azure ESTS Service",
"id": "1daac687-c3b3-4aad-8111-4bac9568a064",
"userPrincipalName": "TestUser@ContosoCorp.onmicrosoft.com",
"ipAddress": "3.89.177.26",
"roles": []
}
},
"LoggedByService": "Core Directory",
"Result": "success",
"ResultReason": "",
"TargetResources": {
"id": "07ec4c16-2cc4-4cd7-b6e3-95a9ba007a21",
"displayName": "ChatGPT",
"type": "ServicePrincipal",
"modifiedProperties": [
{
"displayName": "AccountEnabled",
"oldValue": [],
"newValue": [true]
},
{
"displayName": "AppAddress",
"oldValue": [],
"newValue": [
{
"AddressType": 0,
"Address": "http://localhost:5000/hermes/connectors/oauth",
"ReplyAddressClientType": 1,
"ReplyAddressIndex": null,
"IsReplyAddressDefault": false
},
{
"AddressType": 0,
"Address": "https://platform.api.openai.org/hermes/connectors/oauth",
"ReplyAddressClientType": 1,
"ReplyAddressIndex": null,
"IsReplyAddressDefault": false
},
{
"AddressType": 0,
"Address": "https://platform.openai.com/hermes/connectors/oauth",
"ReplyAddressClientType": 1,
"ReplyAddressIndex": null,
"IsReplyAddressDefault": false
},
{
"AddressType": 0,
"Address": "https://tailor.openai.com/api/v1/oauth/callback",
"ReplyAddressClientType": 1,
"ReplyAddressIndex": null,
"IsReplyAddressDefault": false
},
{
"AddressType": 0,
"Address": "https://connectors.api.openai.com/connector/oauth_callback/ios_relay",
"ReplyAddressClientType": 1,
"ReplyAddressIndex": null,
"IsReplyAddressDefault": false
},
{
"AddressType": 0,
"Address": "https://chatgpt.com/ccc/o365connector-business-dac4c231-bc0f-4d07-8b4c-3e1f3ee122ae/oauth/callback",
"ReplyAddressClientType": 1,
"ReplyAddressIndex": null,
"IsReplyAddressDefault": false
},
{
"AddressType": 0,
"Address": "https://chatgpt.com/ccc/o365connector-personal-b9ce8873-ed1f-405d-97b4-51ca6b2a4f3f/oauth/callback",
"ReplyAddressClientType": 1,
"ReplyAddressIndex": null,
"IsReplyAddressDefault": false
},
{
"AddressType": 0,
"Address": "https://chatgpt.com/connector_platform_oauth_redirect",
"ReplyAddressClientType": 1,
"ReplyAddressIndex": null,
"IsReplyAddressDefault": false
}
]
},
{
"displayName": "AppPrincipalId",
"oldValue": [],
"newValue": ["e0476654-c1d5-430b-ab80-70cbd947616a"]
},
{
"displayName": "DisplayName",
"oldValue": [],
"newValue": ["ChatGPT"]
},
{
"displayName": "ServicePrincipalName",
"oldValue": [],
"newValue": ["e0476654-c1d5-430b-ab80-70cbd947616a","api://e0476654-c1d5-430b-ab80-70cbd947616a"]
},
{
"displayName": "Credential",
"oldValue": [],
"newValue": [{
"CredentialType": 2,
"KeyStoreId": "291154f0-a9f5-45bb-87be-9c8ee5b6d62c",
"KeyGroupId": "291154f0-a9f5-45bb-87be-9c8ee5b6d62c"
}]
},
{
"displayName": "ServicePrincipalTag",
"oldValue": [],
"newValue": ["WindowsAzureActiveDirectoryIntegratedApp","apiConsumer","webApp"]
},
{
"displayName": "Included Updated Properties",
"oldValue": null,
"newValue": "AccountEnabled, AppAddress, AppPrincipalId, DisplayName, ServicePrincipalName, Credential, ServicePrincipalTag"
},
{
"displayName": "TargetId.ServicePrincipalNames",
"oldValue": null,
"newValue": "e0476654-c1d5-430b-ab80-70cbd947616a;api://e0476654-c1d5-430b-ab80-70cbd947616a"
}
],
"administrativeUnits": []
},
"AADTenantId": "747930ee-9a33-43c0-9d5d-470b3fb855e7",
"ActivityDisplayName": "Add service principal",
"ActivityDateTime": "2025-12-02T20:22:16.1185371Z",
"AADOperationType": "Add",
"Type": "AuditLogs"
},
{
"TenantId": "52672484-b4e1-402d-934c-a8e2fd9b05d1",
"SourceSystem": "Azure AD",
"TimeGenerated": "2025-12-02T20:22:16.2365366Z",
"ResourceId": "/tenants/747930ee-9a33-43c0-9d5d-470b3fb855e7/providers/Microsoft.aadiam",
"OperationName": "Consent to application",
"OperationVersion": "1.0",
"Category": "ApplicationManagement",
"ResultType": "",
"ResultSignature": "None",
"ResultDescription": "",
"DurationMs": "0",
"CorrelationId": "f540cbd8-9ec4-4d0e-855c-86e8916c3a1b",
"Resource": "Microsoft.aadiam",
"ResourceGroup": "Microsoft.aadiam",
"ResourceProvider": "",
"Identity": "Azure ESTS Service",
"Level": "4",
"Location": "",
"AdditionalDetails": [
{
"key": "User-Agent",
"value": "EvoSTS"
},
{
"key": "AppId",
"value": "e0476654-c1d5-430b-ab80-70cbd947616a"
},
{
"key": "AppOwnerOrganizationId",
"value": "a48cca56-e6da-484e-a814-9c849652bcb3"
}
],
"Id": "Directory_f540cbd8-9ec4-4d0e-855c-86e8916c3a1b_XC60C_97530533",
"InitiatedBy": {
"user": {
"displayName": "Azure ESTS Service",
"id": "1daac687-c3b3-4aad-8111-4bac9568a064",
"userPrincipalName": "TestUser@ContosoCorp.onmicrosoft.com",
"ipAddress": "3.89.177.26",
"roles": []
}
},
"LoggedByService": "Core Directory",
"Result": "success",
"ResultReason": "",
"TargetResources": {
"id": "07ec4c16-2cc4-4cd7-b6e3-95a9ba007a21",
"displayName": "ChatGPT",
"type": "ServicePrincipal",
"modifiedProperties": [
{
"displayName": "ConsentContext.IsAdminConsent",
"oldValue": null,
"newValue": "False"
},
{
"displayName": "ConsentContext.IsAppOnly",
"oldValue": null,
"newValue": "False"
},
{
"displayName": "ConsentContext.OnBehalfOfAll",
"oldValue": null,
"newValue": "False"
},
{
"displayName": "ConsentContext.Tags",
"oldValue": null,
"newValue": "WindowsAzureActiveDirectoryIntegratedApp"
},
{
"displayName": "ConsentAction.Permissions",
"oldValue": null,
"newValue": "[] => [[Id: FkzsB8Qs10y245WpugB6IUdjEXl8YehProtQM5deXYiHxqods8OtSoERS6yVaKBk, ClientId: 07ec4c16-2cc4-4cd7-b6e3-95a9ba007a21, PrincipalId: 1daac687-c3b3-4aad-8111-4bac9568a064, ResourceId: 79116347-617c-4fe8-ae8b-5033975e5d88, ConsentType: Principal, Scope: Mail.Read offline_access profile openid, CreatedDateTime: , LastModifiedDateTime ]]; "
},
{
"displayName": "TargetId.ServicePrincipalNames",
"oldValue": null,
"newValue": "e0476654-c1d5-430b-ab80-70cbd947616a;api://e0476654-c1d5-430b-ab80-70cbd947616a"
}
],
"administrativeUnits": []
},
"AADTenantId": "747930ee-9a33-43c0-9d5d-470b3fb855e7",
"ActivityDisplayName": "Consent to application",
"ActivityDateTime": "2025-12-02T20:22:16.2365366Z",
"AADOperationType": "Assign",
"Type": "AuditLogs"
}
Who
This corresponds to the actor that performed the action.
TestUser@ContosoCorp.onmicrosoft.com (ID: 1daac687-c3b3-4aad-8111-4bac9568a064)
Field derivation
| Operation | Field | Value | Description |
Consent to application | InitiatedBy.user.userPrincipalName | TestUser@ContosoCorp.onmicrosoft.com | The human-readable name for the identity |
Consent to application | InitiatedBy.user.id | 1daac687-c3b3-4aad-8111-4bac9568a064 | The object ID of the identity which is the unique identifier for the identity |
What
This corresponds to the resource that was affected and the specifics of the action that was performed on it.
“successfully added a new third-party service principal, ChatGPT (App ID: e0476654-c1d5-430b-ab80-70cbd947616a) and consented as a non-admin to the following OAuth permissions: Mail.Read offline_access profile openid.”
Field derivation
| Operation | Field | Value | Description |
Add service principal | Result | “successfully” | When Result is “success”, it indicates that the service principalwas successfully added to the tenant. |
Add service principal | OperationName | “added a new … service principal” | This indicates that the service principal was newly introduced to the tenant. |
Add service principal | AdditionalDetails['AppOwnerOrganizationId'] and AADTenantId | “third-party” | It is not an internally developed app because thenAdditionalDetails['AppOwnerOrganizationId']would be the same as AADTenantId. It is not a first-party Microsoft app because AdditionalDetails['AppOwnerOrganizationId'] is neitherf8cdef31-a31e-4b4a-93e4-5f571e91255anor 72f988bf-86f1-41af-91ab-2d7cd011db47.Since it is neither internal nor first-party, you can conclude that it is a third-party application. |
Consent to application | TargetResources[0].displayName | ChatGPT | The name of the service principal |
Consent to application | AdditionalDetails['AppId'] | e0476654-c1d5-430b-ab80-70cbd947616a | The globally unique identifier of the application |
Consent to application | OperationName | “consented … to … OAuth permissions” | Indicates that OAuth permissions were consented to |
Consent to application | TargetResources[0].modifiedProperties | “non-admin” | A non-admin consent is performed when TargetResources[0].modifiedPropertiesis “ False”. |
Consent to application | TargetResources[0].modifiedProperties | “Mail.Read offline_access profile openid” | This property represents an oAuth2PermissionGrant instance which indicates the specific OAuth permissions that were consented to. |
Interpreting ConsentAction.Permissions values
Consider the following example from above:
[] => [[Id: FkzsB8Qs10y245WpugB6IUdjEXl8YehProtQM5deXYiHxqods8OtSoERS6yVaKBk, ClientId: 07ec4c16-2cc4-4cd7-b6e3-95a9ba007a21, PrincipalId: 1daac687-c3b3-4aad-8111-4bac9568a064, ResourceId: 79116347-617c-4fe8-ae8b-5033975e5d88, ConsentType: Principal, Scope: Mail.Read offline_access profile openid, CreatedDateTime: , LastModifiedDateTime ]]; The structure of this data is not documented by Microsoft but it is clear that it comprises an array of one or more oAuth2PermissionGrant instances. In its most basic form, it has the following structure:
[[PREVIOUS_OAUTH2PERMISSIONGRANT_1],[PREVIOUS_OAUTH2PERMISSIONGRANT_2]] => [[NEW_OAUTH2PERMISSIONGRANT_1],[NEW_OAUTH2PERMISSIONGRANT_2]];The oAuth2PermissionGrant instance above is broken down as follows:
Id:FkzsB8Qs10y245WpugB6IUdjEXl8YehProtQM5deXYiHxqods8OtSoERS6yVaKBk- This corresponds to the unique identifier for the OAuth permission grant. This value is necessary for performing targeted remediation. This value is composed of the combined
ClientId,ResourceId, andPrincipalIdvalues (in that order), which is then Base-64 encoded. See the Remediation section below for more details.
- This corresponds to the unique identifier for the OAuth permission grant. This value is necessary for performing targeted remediation. This value is composed of the combined
ClientId:07ec4c16-2cc4-4cd7-b6e3-95a9ba007a21- This is the object ID (not the application ID) of the service principal instance that the permission grant is applied to.
PrincipalId:1daac687-c3b3-4aad-8111-4bac9568a064- This is the object ID of the identity to which the permission grant applies. When
ConsentTypeisAllPrincipals, this value is not populated.
- This is the object ID of the identity to which the permission grant applies. When
ResourceId:79116347-617c-4fe8-ae8b-5033975e5d88- This is the object ID (not the application ID) of the resource app that implements the requested scope. In most cases, this will correspond to the Microsoft Graph resource app.
ConsentType:Principal- When this value is
Principal, it means that the granted permissions apply only to the identity specified byPrincipalId. When this value isAllPrincipals, it means that the scopes were granted to all users.
- When this value is
Scope:Mail.Read offline_access profile openid- This is a space-delimited list of the granted OAuth permissions.
CreatedDateTime:- This has never been observed to be populated.
LastModifiedDateTime- This has never been observed to be populated.
In the example above, the permission string begins with [] => , meaning that it is a new OAuth permission grant.
Note: The permission string can contain multiple oAuth2PermissionGrant instances. In those cases, the ResourceId value will be unique, as it will contain a set of Scope values that are implemented in that particular resource app.
When
This corresponds to the time in which the action occurred.
“At 2025-12-02T20:22:16”
Field derivation
| Operation | Field | Value | Description |
Consent to application | ActivityDateTime | 2025-12-02T20:22:16.2365366Z | The date and time that this event occurred. |
Where
This corresponds to the environment in which the action occurred.
“within Entra ID tenant ID 747930ee-9a33-43c0-9d5d-470b3fb855e7”
Field derivation
| Operation | Field | Value | Description |
Consent to application | AADTenantId | 747930ee-9a33-43c0-9d5d-470b3fb855e7 | The tenant in which the action took place. |
Whence
From where did the action originate?
“This action was performed from the following IP address: 3.89.177.26.”
Field derivation
| Operation | Field | Value | Description |
Consent to application | InitiatedBy.user.ipAddress | 3.89.177.26 | The IP address from which the actor performed the action. |
How
This corresponds to the means by which the action occurred.
Field derivation
| Operation | Field | Value | Description |
Consent to application | AdditionalDetails['User-Agent'] | EvoSTS | The user agent that performed the action.EvoSTS indicates that the consentwas performed via the typical graphical UI consent prompt. |
Detection
The detection strategy you may consider employing will depend on how you scope the technique and what your particular detection objectives are.
Technique scope
- Are you concerned about non-admin users being phished and consenting to unsanctioned applications that either violate policy or are malicious?
- Are you concerned about admins being targeted and granting privileges for privileged OAuth scopes?
- Are you concerned about threat actors who have already compromised an identity creating an internal application that is then used in a phishing campaign?
Detection objective
- What is the detection of malicious applications versus unsanctioned applications?
- What is your detection volume tolerance?
- What is your tolerance for false positives?
- Do you have the ability to perform application enrichment and assess prevalence?
Each combination of scope and objective will dictate the detection strategy. As a good starting point, we recommend the following detection strategy that minimizes volume.
Detect a non-admin permission grant for a new, third-party application that was granted one or more commonly abused permissions.
Such a strategy can be broken down as follows:
- We are assuming that non-admin users are more likely to be targeted and being less tech-savvy are more likely to fail to recognize a suspicious consent request.
- The application doesn’t already exist in the tenant. An application that already exists in the tenant is more likely to be sanctioned.
- A malicious, adversary-controlled application will not be a first-party, Microsoft application. By excluding internal and 1st-party applications, we can reduce detection volume drastically.
- As of this writing, Microsoft’s default app consent policy blocks the following permissions for non-admins that are known to be abused:
Calendars.Read,Calendars.Read.Shared,Calendars.ReadBasic,Calendars.ReadWrite,Calendars.ReadWrite.Shared,Chat.Read,Chat.ReadWrite,Files.Read.All,Files.ReadWrite.All,Mail.Read,Mail.Read.Shared,Mail.ReadBasic,Mail.ReadBasic.Shared,Mail.ReadWrite,Mail.ReadWrite.Shared,MailboxFolder.Read,MailboxFolder.ReadWrite,MailboxItem.Read,MailboxSettings.Read,MailboxSettings.ReadWrite,OnlineMeetings.Read,OnlineMeetings.ReadWrite,Sites.Read.All,Sites.ReadWrite.All
This detection strategy can be implemented with the following pseudo-logic:
| Operation | Field | Condition | Value | Description |
Consent to application | TargetResources[0].modifiedProperties['ConsentAction.Permissions'].newValue | Starts with | [] => | It is a new consent for the user. |
Consent to application | TargetResources[0].modifiedProperties['ConsentAction.Permissions'].newValue | Contains one or more of the following | See the list of risky scopes above, e.g. Mail.Read | One or more of the known abused, non-privileged permissions |
Consent to application | TargetResources[0].modifiedProperties['ConsentContext.IsAdminConsent'].newValue | Equals | False | A non-admin consent occurred. |
Consent to applicationAND Add service principal | CorrelationId | Equals | The same CorrelationIdvalue for both events | The user consented to an application that wasn’t already present in the tenant. |
Add service principal | AdditionalDetailsAND AADTenantId | Does not equal | NEITHER AADTenantIdNOR f8cdef31-a31e-4b4a-93e4-5f571e91255aNOR 72f988bf-86f1-41af-91ab-2d7cd011db47 | The service principal added is neither an internal app nor a first-party application, therefore, it is a third-party application. |
Remediation
If it has been determined that an OAuth consent grant was malicious, the following immediate actions can be performed:
1. Remove the consent grant.
Using the permission grant ID value from the TargetResources[0].modifiedProperties['ConsentAction.Permissions'].newValue field in the Consent to application event, the following Microsoft Graph command can be executed:
Remove-MgBetaOAuth2PermissionGrant -OAuth2PermissionGrantId FkzsB8Qs10y245WpugB6IUdjEXl8YehProtQM5deXYiHxqods8OtSoERS6yVaKBk2. Remove the service principal that was added to the tenant.
If the app added to the tenant has been deemed malicious or unsanctioned, it can be removed using the service principal ID in the TargetResources[0].id field in the Consent to application event. In the example above, this value was 07ec4c16-2cc4-4cd7-b6e3-95a9ba007a21. Here is a sample PowerShell Graph command that would remove the service principal:
Remove-MgServicePrincipal -ServicePrincipalId 07ec4c16-2cc4-4cd7-b6e3-95a9ba007a21Mitigations
Fortunately, Microsoft supplies ample opportunities to mitigate against this technique. As discussed this technique relies upon a victim to have permission to perform the following two actions:
- Add a service principal to the tenant.
- Consent to OAuth permissions that the identity is permitted to consent to.
As Microsoft states, “by default, all users are allowed to consent to applications for permissions that don’t require administrator consent. For example, by default, a user can consent to allow an app to access their mailbox but can’t consent to allow an app unfettered access to read and write to all files in your organization.”
Microsoft supplies three options to mitigate against non-admin users introducing unvetted applications and over-provisioning of OAuth permissions:
- Safest by default but incurs an administrative burden: “Do not allow user consent.” This option requires an administrator (i.e. Global Administrator or Privileged Role Administrator) to approve all consent requests.
- Balances security while easing administrative burden and grants users the ability to consent to pre-approved OAuth permissions for “higher-trusted” applications: “Allow user consent for apps from verified publishers, for selected permissions. All users can consent for permissions classified as “low impact”, for apps from verified publishers or apps registered in this organization.”
- Microsoft’s recommended option for balancing security and flexibility: “Let Microsoft manage your consent settings. Automatically update your organization to Microsoft’s current user consent guidelines.”
References
- Configure how users consent to applications
- Manage app consent policies
- Detect and Remediate Illicit Consent Grants
- Compromised and malicious applications investigation