The biggest risk to a secure building isn’t the burglar with a crowbar, it’s the locksmith who quietly makes an extra copy of the master key. In Microsoft 365, client secrets are those keys. They are confidential credentials issued to applications that allow those apps to authenticate as themselves via OAuth (an open-standard authorization protocol) and request access tokens. This method of authentication unlocks powerful application and service principal access, and when mishandled, it can silently grant attackers the same privileges you give to your most trusted employees and systems.
This post will build upon research conducted by Proofpoint, which provides insight into malicious OAuth apps and how they get abused.
We’ll explore a technical approach to identifying and investigating malicious client secrets in Microsoft 365 environments. This guide will walk you through a step-by-step methodology, utilized by GuidePoint Security’s DFIR (Digital Forensics and Incident Response) team in real-world incidents.
We provide working PowerShell scripts and Kusto Query Language (KQL) queries that will aid the investigation process; however, the query logic can be easily understood and adapted to other security solutions or languages available in your environment.
Our methodology consists of three main phases:
The first step in hunting for malicious activity is knowing what client secrets exist in your environment. You can’t protect what you can’t see, so comprehensive visibility is essential.
Before you begin the discovery process, you’ll need to prepare your PowerShell environment with the appropriate tools. Microsoft Graph SDK for PowerShell provides the necessary cmdlets to interact with your Microsoft 365 tenant programmatically. The following commands will ensure that the Microsoft Graph PowerShell module is installed for you, and the appropriate permissions needed to enumerate application and service principal configurations are assigned to the Microsoft Graph connection.
Install-Module Microsoft.Graph -Scope CurrentUser -Force
Connect-MgGraph -Scopes "Application.Read.All"
Service principals represent applications within your specific Entra ID tenant. They define what an application can do, who can access it, and what resources it can access. Each service principal can have multiple credentials associated with it.
$allSpCreds = Get-MgServicePrincipal -All | ForEach-Object {
    $sp = $_
    ForEach ($cred in $sp.PasswordCredentials) {
        [PSCustomObject]@{
            SPname = $sp.DisplayName
            AppId = $sp.AppId
            SPObjectId = $sp.Id
            KeyId = $cred.KeyId
            Hint = $cred.Hint
            DisplayName = $cred.DisplayName
            StartDateTime = $cred.StartDateTime
            EndDateTime = $cred.EndDateTime
        }
    }
}
$allSpCreds | Export-Csv -Path "ServicePrincipalCredentials.csv" -NoTypeInformationNOTE: All the queries provided in this post may need additional fine-tuning for each unique environment.
This script retrieves every service principal in your tenant, then loops through each one’s credentials. For each item found, it creates a structured record containing key information:
The results are exported to a CSV file for further analysis.
While service principals represent applications within your tenant, application objects are the global representation of applications across all tenants where they’re used. It’s important to check both, as threat actors might create credentials at either level.
$allAppCreds = Get-MgApplication -All | ForEach-Object {
    $app = $_
    ForEach ($cred in $app.PasswordCredentials) {
        [PSCustomObject]@{
            AppName = $app.DisplayName
            AppId = $app.AppId
            AppObjectId = $app.Id
            KeyId = $cred.KeyId
            Hint = $cred.Hint
            DisplayName = $cred.DisplayName
            StartDateTime = $cred.StartDateTime
            EndDateTime = $cred.EndDateTime
        }
    }
}
$allAppCreds | Export-Csv -Path "ApplicationCredentials.csv" -NoTypeInformationNOTE: All the queries provided in this post may need additional fine-tuning for each unique environment.
This script follows the same pattern as the service principal discovery but focuses on application objects instead. By running both scripts, you create a comprehensive inventory of all credentials that could potentially be used for authentication.
Applications and service principals serve different purposes in the identity architecture. An application object is the template, while service principals are the local instances. Attackers might create malicious credentials at either level, depending on their objectives and the permissions they’ve obtained.
Once you’ve identified potential malicious client secrets, the next step is understanding what they’ve been used for. In many real-world incidents, threat actors leverage compromised or malicious client secrets to access sensitive data, particularly email messages, which often contain valuable intellectual property, financial information, and confidential communications.
Microsoft Graph API provides a powerful interface for accessing Microsoft 365 resources programmatically. While this is invaluable for legitimate automation and integration scenarios, it’s also a prime target for attackers. When threat actors gain access through malicious client secrets, they often use Graph API to export email messages from multiple accounts. This is why monitoring Graph API usage is crucial for security operations.
The table GraphAPIAuditEvents captures detailed telemetry for API requests made via Microsoft Graph API for resources in your Entra ID tenant. These events provide visibility into which applications accessed what resources, when, and from where.
GraphAPIAuditEvents
| where AccountObjectId in ("AppID 1", "AppID 2")NOTE: All the queries provided in this post may need additional fine-tuning for each unique environment.
This KQL statement filters the GraphAPIAuditEvents table to show only those activities performed by specific applications. To utilize this query, replace the placeholder AppIDs with the actual application identifiers you discovered in Phase 1 that appear suspicious.
When this query returns results, pay close attention to the RequestUri field. For mail access operations, you’ll see URLs following this pattern:
https://graph.microsoft.com/v1.0/users/<userId>/messages/<messageId>
Two important things to note in this URL pattern is the <userId> value, identifying the account whose mailbox was accessed, and the <messageId> value, identifying the specific email message that was retrieved.
The <messageId> value can later be utilized to correlate the identified accessed messages with Message Trace logs in Microsoft 365 to obtain additional details about the accessed email messages in order to identify patterns of interest.
Each of these URIs represents a single email message that was accessed by the application. If you see hundreds or thousands of these events, it’s a strong indicator of large-scale data exfiltration.
To understand the scope of the compromise, you need to identify all user accounts whose mailboxes were accessed. This enhanced query extracts the email addresses from the request URIs and provides a timeline of access:
GraphAPIAuditEvents
| where AccountObjectId in ("AppID 1", "AppID 2")
| extend EmailAddress = extract(@"/users/([^/]+)/", 1, RequestUri)
| summarize FirstSeen = min(Timestamp), LastSeen = max(Timestamp) by EmailAddress, IPAddress
| project FirstSeen, LastSeen, IPAddress, EmailAddressNOTE: All the queries provided in this post may need additional fine-tuning for each unique environment.
This information is crucial for your incident response efforts, enabling you to notify affected users, assess the sensitivity of potentially compromised data, and take appropriate containment actions.
Another option is to analyze Microsoft 365 OfficeActivity logs in order to identify potential cases where the same access token (client secret) is being used to access multiple users, a behavior that can indicate a potential compromise or misuse of the application identity.
OfficeActivity
| where isnotempty(AppAccessContext)
| extend UniqueTokenId = tostring(AppAccessContext.UniqueTokenId)
| extend AppId = tostring(AppAccessContext.ClientAppId)
| where isnotempty(UniqueTokenId) and isnotempty(AppId)
| where Operation == "MailItemsAccessed"
| summarize UniqueUsers = dcount(UserId), Users = make_set(UserId) by UniqueTokenId, AppId | where UniqueUsers > 1
| project TimeGenerated, AppId, UniqueTokenId, UniqueUsers, UsersNOTE: All the queries provided in this post may need additional fine-tuning for each unique environment.
The above query focuses solely on MailItemsAccess operation and focuses on tokens used to access more than one user account.
Further fine-tuning of this query can be made by excluding cases where users legitimately access multiple mailboxes in the environment, for instance, their personal account and a shared mailbox.
Identifying when and where new credentials were added is crucial during the investigation process. While that can be answered by triaging credentials for all Service Principals and Applications in the environment through the scripts provided in this article, you can also hunt and should regularly monitor for the creation of new service principal credentials.
This data exists in two separate tables, AuditLogs and CloudAppEvents. The relevant KQL queries are shown below:
CloudAppEvents
| where ActionType == "Add service principal credentials."
AuditLogs
| where OperationName == "Add service principal credentials"
| where Identity != "Managed Service Identity"NOTE: All the queries provided in this post may need additional fine-tuning for each unique environment.
In the case of the AuditLogs we have excluded newly created credentials for service principals that are automatically managed by the system.
Armed with the data from your investigation, you can now assess the full impact of the incident and formulate an appropriate response strategy.
During your assessment, focus on these critical questions:
Scope of Compromise:
Timeline Analysis:
Threat Actor Indicators:
Based on your findings, your response should include:
Prevention is always better than cure. Here are key practices to minimize your exposure to malicious client secret attacks:
Perform Regular Credential Audits:
Implement Conditional Access: Use Entra ID Conditional Access policies to restrict application access based on risk factors.
Enable Privileged Identity Management (PIM): For applications requiring high-privilege permissions, use PIM to ensure just-in-time access rather than standing privileges.
Monitor Graph API Activity: Don’t wait for an incident. Establish baseline behavior for Graph API usage in your environment and set up alerts for anomalies.
Principle of Least Privilege: When granting permissions to applications, provide only the minimum necessary scopes.
The methodology outlined in this post provides a structured approach to hunting for malicious client secrets in your Microsoft 365 environment. By combining proactive discovery with detailed investigation techniques, you can identify threats that might otherwise remain hidden in the noise of normal application activity.
Remember, security is not a one-time effort but an ongoing process. Threat actors continually evolve their techniques, and your defensive measures must evolve as well. The scripts and queries provided here are starting points, adapt them to your specific environment, integrate them into your security operations workflow, and continuously refine your detection capabilities based on the threats you observe.
Learn more about how GuidePoint Security can help you with threat hunting and incident response.
Stay vigilant, stay informed, and happy hunting!