Protect your multi-tenant applications from being hijacked by admins in the customer tenant

In the beginning of 2023, Microsoft introduced the app instance property lock feature, as a response to some of the “app hijacking” techniques used by prominent attacks over the past few years. The feature reached GA status in September 2023 and will be enabled by default for all new multi-tenant applications, regardless of how they were created, starting March 2024. Which all sounds good, but there are some limitations in the implementation that you need to be aware of. In addition, most of the articles I could find online fail to illustrate why this feature is important, as in specific scenarios it helps prevent, which is important part of the puzzle. So here we go again, another “educational” article 🙂

Setting the stage – a refresher on service principals

The first thing you need to understand is that this feature is intended to address specific use scenarios around multi-tenant application registrations. In other words, Entra ID integrated applications developed by other organizations (including Microsoft) and “consumed” within your tenant. Such apps are represented by service principal objects, and generally require consent before being added (more info for example here). In turn, the permissions you consented to and the set of admin roles assigned to the SP object (if any) determine which operations a given app will be able to execute against objects within your organization.

Because the service principal object is the local representation of the app, some of it property values are inherited and can only be managed by the organization that owns the parent application. This is regardless of the fact that your own organization “owns” the service principal object, technically speaking. Thus, you can decide to disable the service principal object, allow access to specific users only, or configure the set of custom security attributes assigned to it. On the other hand, you cannot change the display name, homepage, appId and so on – those are all properties managed by the organization that owns the app. The same applies to properties related to authentication, such as the set of OAuth flows supported, the set of replyURIs and, most importantly, the set of client secrets and certificate credentials.

It of course makes sense – after all you cannot expect to be able to dictate what authentication methods the application will be using, or the permissions it will require. If every organization using a given app had a say in the process, conflicts would be common, and in most cases, unresolvable. There is however a BUT here, and it’s an important one. While most of the aforementioned properties are out of reach for organizations “consuming” the app, Entra ID will still allow them to create and use credentials to authenticate as the service principal object. And this is where issues start arising, and what the app instance property lock feature aims to address.

I suppose another note is due here. Yes, service principal objects can have credentials on their own. Why? Because they are a local representation of the app, and the way the platform was designed simply allows the service principal object to have its own credentials, independent of the parent app. This is illustrated by the screenshot below, where a subset of the properties of a service principal object corresponding to a multi-tenant app are visible. Note the two highlighted ones, namely keyCredentials (certificates) and passwordCredentials (client secrets). By default, you can expect to see null values for said properties across all your multi-tenant app representations. Yet the apps themselves will work just fine, as they leverage another set of credentials, the ones configured on the parent application object.

AppPropLock

If “local” credentials exist for the service principal object, anyone in possession of the certificate or client secret will be able to authenticate against the current tenant and “impersonate” the application, effectively gaining access to all the objects and operations the app has been granted permissions to. So, after thing lengthy introduction, let’s finally describe what the app instance property lock feature is all about: it allows app developers to restrict the creation of service principal credentials. And few other things, but going over them would open yet another can of worms…

Demonstrating the use of service principal credentials

Let us do a quick demonstration. We have the service principal above, corresponding to a multi-tenant application. We will generate a new password credential for said principal, then use the credential to obtain an access token. If everything goes as planned, the token will then be used to perform any of the operations allowed by the permissions granted/assigned to the service principal in the current directory. Here are the corresponding PowerShell cmdlets:

Get-MgServicePrincipal -Filter {appId eq 'ae23ad05-xxxx-xxxx-xxxx-78b7a0757f5f'} | fl id,appDisplayName,appid,appOwnerOrganizationId,signInAudience,keyCredentials,passwordCredentials

Add-MgServicePrincipalPassword -ServicePrincipalId 669c02d9-xxxx-xxxx-xxxx-b70c01f82322

AppPropLock1At this point, we can check again the properties of the service principal object and compare it with the “original” values showcased on the Graph explorer screenshot above. Let’s actually use the Graph explorer again, as the Graph SDK for PowerShell continues to be the most disappointing module ever and uses an output formatting that can easily mislead you. So, fetching the properties of the same service principal object via the Graph explorer, we now see the presence of a passwordCredentials property:

AppPropLock2

Next, we proceed to obtain an access token via said credential. You can use a direct request, the MSAL library, or even leverage the Graph SDK for PowerShell to authenticate via the client secret. In all cases, the end result will be that you now have a valid access token, which allows you to perform any and all of the operations the original app is authorized for in your tenant. The screenshot below illustrates one potential method of leveraging the credentials we generated for the service principal:

$cred = Get-Credential ae23ad05-xxxx-xxxx-xxxx-78b7a0757f5f

Connect-MgGraph -TenantId michevdev3.onmicrosoft.com -ClientSecretCredential $cred

AppPropLock3

Effectively, we were able to perform some operations by leveraging an access token issued for the service principal. In fact, a token obtained via this method is virtually indistinguishable from a token obtained via the parent app, in the context of the current tenant that is. If you decode both tokens, you will observe the exact same set of claims, including the roles and wids claims, which define the permissions and admin roles granted. The only difference between the two tokens will be the presence of the “xms_spcu”:”true” claim in the case of the SP key token. Said claim is unfortunately undocumented, so we can only guess whether the label can be reliably used to differentiate such tokens (spcu = “service principal credential usage”?).

AppPropLock6

This in turn poses a problem, as any operation performed by the means described above will result in audit log trail that “implicates” the parent operation. For example, if we were to disable a user account, the corresponding audit log will feature the service principal Id and the parent’s application name as the only information about the “actor”. This is of course the expected behavior. The problem is that if you perform the same operation directly via the parent app, the audit record generated will be undistinguishable. In other words, anyone investigating the audit log trail will not be able to tell whether the operation was performed by the third-party app, or by someone that has “hijacked” its representation in the current tenant. And that’s a problem.

Update-MgUser -UserId 7d7e348e-cbeb-4358-8522-d592f980cfd1 -AccountEnabled:$false 
Update-MgUser -UserId 7d7e348e-cbeb-4358-8522-d592f980cfd1 -AccountEnabled:$true

AppPropLock4

How to mitigate the issue

Hopefully, the above wall of text lets you understand what Microsoft is trying to address by introducing the app instance property lock feature. The feature allows the application developer/owner to “lock” properties such as passwordCredentials from being modified on any “child” service principal object, representing the app across customer’s tenants. Unfortunately, it does not help in scenarios where such modifications have already been made, but we’ll talk about this in a minute. For now, let’s see how we can toggle the feature.

The easy method, and what’s detailed in the official documentation, is to open the Entra ID portal/Azure AD blade, navigate to App registrations, select the app, select Authentication and press the Configure link under App instance property lock. Therein, make sure the Enable property lock checkbox is selected and include all properties from the dropdown, then confirm the changes.

Of course doing things the UI way is no fun, so here’s how to query and set the app instance property lock feature via the Graph SDK for PowerShell (the corresponding Graph API requests can be found for example here):

Update-MgApplication -ApplicationId (Get-MgApplication -Filter "appId eq 'ae23ad05-xxxx-xxxx-xxxx-78b7a0757f5f'").Id -ServicePrincipalLockConfiguration @{"isEnabled"=$true;"AllProperties"=$true}

Get-MgApplication -Filter "appId eq 'ae23ad05-xxxx-xxxx-xxxx-78b7a0757f5f'" | select -ExpandProperty ServicePrincipalLockConfiguration

AllProperties CredentialsWithUsageSign CredentialsWithUsageVerify IsEnabled TokenEncryptionKeyId
------------- ------------------------ -------------------------- --------- --------------------
True                                                              True

Once you toggle the feature, any service principal linked to the parent app has its (selected) properties locked. Any attempt to generate new set of passwordCredentials will result in an error, such as the one shown below. Unfortunately, this does not affect any previously made modifications, such as the password credential we created in the example above, which we can continue using. In turn, this means that the feature addresses only part of the issue, and additional steps are needed in order to ensure that the method has not already been used. But that will be a subject for our next article 🙂

AppPropLock5

For now, let’s wrap things up by mentioning that Microsoft is planning to enforce this behavior going forward. Here’s the relevant quote:

Starting March 2024, new applications created using Microsoft Graph application API will have “App instance lock” enabled by default. The capability called App instance lock for workload identities was launched in September 2023. This feature allows app developers to protect their multi-tenant apps from attackers tampering with critical properties. Applications created using Entra ID portal already have the setting enabled by default, and going forward, it will be enabled for other app creation surface areas such as MS Graph, PowerShell, and SDKs. For more information, see How to configure app instance property lock in your applications | Microsoft Learn.

Summary

With the app instance property lock feature, Microsoft is addressing a gap in the process of securing application access, which allowed bad actors to “hijack” service principal objects corresponding to third-party applications. While this is not a elevation of privileges exploit, we have already seen real-life attacks that take advantage of its obscure nature. But I want to be clear – this method can only be used if the credentials of an admin, or user that has been set as the owner of a given service principal have already been compromised. And, it only allows access to the current tenant, and only to the operations authorized by the set of permissions and admin roles granted to the service principal.

Microsoft is taking the correct approach by toggling this feature by default next year. Still, for ISVs and anyone creating multi-tenant applications, it is important to toggle this feature as soon as possible. While adding service principal credentials can only ever give you access to the current tenant and cannot be compared to compromise of the credentials configured on the application object, the lack of detail in the audit record can potentially lead to situations where you have to prove that your app’s credentials weren’t leaked or used in any unwanted manner. Which of course you should already be keeping track of, right?

As the issue can be used to the same effect for any single-tenant apps, I’d argue that your own applications should be protected in the same manner as well. We have all the means necessary to report on such apps and changes made to the SP objects (Tony included an example of an audit log entry here), it’s simply yet another convoluted thing many admins have probably not even heard about. So, in a follow up article, we will explore what methods can be used to enumerate all service principal credentials and the available remediation options. And yes, even first-party “system” Microsoft apps can be abused in this way. Stay tuned!

2 thoughts on “Protect your multi-tenant applications from being hijacked by admins in the customer tenant

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.