Script to review and remove service principal credentials

Last week, we explored Entra ID’s app instance property lock feature. As part of the process, we examined one possible way that bad actors could take advantage of the convoluted nature of working with multi-tenant applications within Microsoft 365 and their in-tenant representation, the service principal. As the app instance property lock feature does not address scenarios where the service principal object already has credentials generated, we promised to provide a PowerShell script that can help remediate this. So here we go, with a proof of concept script to review and remove service principal credentials.

If you have doubts that the tasks of reviewing service principal credentials is an important one, perhaps this recent article on the Microsoft security blog will come handy. In true Microsoft fashion, most of the mitigation steps outlined in the article are of the good old “buy this additional product” type, or hunting queries that you can run post-compromise. In contrast, actual prevention effort would benefit from a way to proactively review any and all service principal objects within your tenant.

The script we will explore in this article is based on “raw” Graph API requests, but you can easily “translate” it to a version that leverages the Graph SDK for PowerShell, if that’s what you prefer. In a nutshell, the script will enumerate all service principal objects within the organization, gather additional details on each of them and offer to remediate any SP objects for which password or key credentials are found. The remediation part if optional, and if you run the script without it, it will simply generate a detailed report of all the SP objects, similar to the the one we covered in this article.

The script borrows most of the logic of the app_Permissions_inventory_GraphAPI.ps1 script, which honesty might be an overkill. The idea is to present you with as much information about the service principal objects as possible, so that you can then decide whether remediation makes sense. Thus, additional calls are made to gather the set of permissions granted to each service principal (consents), the set of owners, information about the publisher and so on. Some additional parameters have been introduced as well, but we will cover those in a minute.

For authentication, the script uses application permissions, which in turn means you need to have a application registration with the appropriate permissions, as follows:

  • Application.Read.All (to read the service principals)
  • User.Read.All (for “resolving” user IDs to UPNs)
  • Application.ReadWrite.All (required for remediation)

Everything the script does can also be executed via delegate permissions, so if that’s your preference, you can make the necessary changes to the connectivity part (lines 143-165). Keep in mind that the script allows for fully automated execution, so switching to delegate permissions will be at the cost of automation. And lastly, here’s the mandatory reminder to never store any client secrets or any other login details in scripts. Treat this as sample code, and not something that is ready for production use!

Moving on, the script supports the following set of parameters:

  • IncludeBuiltin – switch parameter, used to indicate whether you want to include any first-party (Microsoft) service principal objects. More on this a bit latter. Default value of this parameter is $false.
  • IncludeConsents – switch parameter, used to indicate whether you want information about any permissions (consents) granted to the service principal to be included in the script’s output and as part of the decision-making process. Default value is $false, as the process involves additional calls for each service principal.
  • Remediate – switch parameter, used to indicate whether you want the script to perform the remediation action. Remediation means removing any password and/or key credentials registered on the service principal’s object as well as null-in the value of the tokenEncryptionKeyId property. Default value is $false, meaning the script will not perform any remediation, but only generate a report of all the service principal objects.
  • Force – switch parameter, used to override the confirmation prompts in scenarios where you want to execute the remediation action. Default value is $false.

The script also supports the “default” –Verbose, –WhatIf and –Confirm switch parameters. Use the first one to generate more detailed output as the script progresses. The –Verbose switch can be especially useful when doing a remediation, as it will spill out the full set of properties of the service principal object when prompted to make a decision. Unfortunately, the Invoke-WebRequest cmdlet we use for the Graph API calls does not allow us to do an actual evaluation of the changes before committing them, so the –WhatIf switch doesn’t really work. Lastly, the –Confirm parameter can be used when you want to make sure that each change is reviewed before executing. Setting it to $false should have the same effect as using the –Force switch.

Before moving to specific examples, a note is due. If you opt to include “built-in” service principal objects, i.e. the ones NOT tagged with “WindowsAzureActiveDirectoryIntegratedApp”, you can expect to see 300-500+ additional entries in the output, depending on the set of Microsoft 365 services you are using. The script can process those just fine, though this still does NOT give you the full picture. There are plenty other (let’s call them “system”) service principals that are “hidden”, and will not be returned in the script’s output. For example, most Microsoft 365 tenants have the “Microsoft PowerQuery For Excel” service principal (with appID a672d62c-fc7b-4e81-a576-e60dc46e951d) enabled, but no Graph API query will ever return it.

While the script will run OK for first-party service principals it can “see”, and you can technically use the methods outlined in our previous article to generate credentials for such SPs, this does not necessarily mean you will be able to generate an access token later on. For example, the corresponding application might not even support the client credentials flow we exploited in the previous article. In addition, the majority of the “built-in” service principals should have zero consents. In effect, the lack of permissions means that any token you can potentially obtain is “benign” in nature. There is however the occasional exception to this rule, for example the “SharePoint Online Client Extensibility Web Application Principal Helper” service principal objects (with appID of 92d350ef-0974-4aa5-ace6-fd374300f5d7) has the “Directory.AccessAsUser.All” scope consented to, for all users within the tenant.

Whether you should be processing such service principals is debatable, but the current version of the script does allow coverage. In fact, few “built-in” service principals that actually have a set of credentials generated can be found in most tenants. One such example is the “Azure Multi-Factor Auth Client” one (with GUID 981f26a1-7f43-403b-a875-f8b09b8cd720), which is set up as part of the integration process between AD FS and Azure MFA. Some of the FastTrack apps also feature a set of credentials. And of course, they likely need said credentials to function as expected.

Thus, there exists the possibility that running a remediation action against built-in SPs can have some very undesired consequences, so only use said switches if you are absolutely certain you know what you are doing. This is also one of the reasons why the script’s logic implements a robust review/confirmation process, so make sure to double- and triple-check things before committing to the remediation. A good test would be to check the existence of the same SP in a different tenant, and compare the properties – if no creds are found therein, it’s likely that they were generated with malicious intent in mind.

Anyway, here are few examples of how to run the script. The first example will simply generate a report of any SPs that might need remediation. The second one adds the remediation steps as well. The third example shows how you can automate the script to remediate all SPs without user interaction.

#Running the script without any parameter will generate a report of all SPs
C:\> .\SP_creds_remediate.ps1

#Use the -Remediate switch to trigger the remediation flow
C:\> .\SP_creds_remediate.ps1 -Remediate

#Combine it with the -Force switch to automate the process
C:\> .\SP_creds_remediate.ps1 -Remediate -Force

To get additional information as the script progresses, use the –Verbose switch. When used together with the –Remediate switch, this will allow you to review all the relevant properties of the SP object before making a decision. To also include information about any consents granted to the SP, use the –IncludeConsents switch. Lastly, to cover built-in SP objects as well, use the –IncludeBuiltIn switch.

#Use verbose output
C:\> .\SP_creds_remediate.ps1 -Verbose

#Verbose output is especially useful when remediating
C:\> .\SP_creds_remediate.ps1 -Remediate -Verbose

#To also include consent info
C:\> .\SP_creds_remediate.ps1 -Remediate -Verbose -IncludeConsents

#To cover built-in service principals
C:\> .\SP_creds_remediate.ps1 -IncludeBuiltin

The screenshot below illustrates the remediation process. When a service principal object with credentials is found, and the –Remediate switch is specified, the script will prompt you for confirmation before removing the credentials (separate prompts for each type of creds). By default, the confirmation prompt only shows the SP name and its objectID. In most cases, this information will likely be insufficient, so you can either consult the generated CSV report file, or use the –Verbose switch to get additional details (all the lines in yellow).

SPCredsRemediateSpeaking of the CSV file, here’s how a sample report will look like. In this case, we have included all the service principals and also fetched their corresponding consents. The first view shows a filter based on the presence of SP credentials, whereas the second one is the output you can expect to see when the –IncludeConsents switch is used.


As always, feel free to modify the script to better suit your needs. And as an added bonus, here’s also a script that goes over your application objects and toggles the app instance property lock feature, if needed. To run it, use the following examples:

#Running the script without any parameter will simply generate a report
C:\> .\appInstancePropLock.ps1 

#Use the -Remediate switch to trigger the remediation flow 
C:\> .\appInstancePropLock.ps1 -Remediate 

#By default, only applications with signInAudience of AzureADMultipleOrgs are included
#Use the the -IncludeAllApps switch to cover all applications.
C:\> .\appInstancePropLock.ps1 -Verbose -IncludeAllApps -Remediate

Tony’s version of the script is based on the Graph SDK for PowerShell and can be found here.

1 thought on “Script to review and remove service principal credentials

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.