Everything about Service Principals, Applications, and API Permissions

Service Principals are identities used by created applications, services, and automation tools to access specific resources. It only needs to do specific things, which can be controlled by assigning the required API permissions. The majority of organizations that work a lot with Azure AD, have service principals as well. Every time when an application has been registered. It will automatically create an application object, and a service principal in a tenant.

An service principal can authenticate via two different options. The first one is via password-based authentication, whereby an client secret has been created for an application.

The second option is to use certificate-based authentication. This form of authentication is done via an certificate, which can be self-signed for instance.

API Permissions

As discussed previously, we know that applications have certain API permissions assigned to it. This allows an application being able to access the specific resources that it needs.

We can see for example that this application has ‘Directory.ReadWrite.All’ permission assigned to it, which is considered an sensitive permission. This allows a Service Principal to grant admin consent through automation for example. Having this permission assigned to an application is not immediately the end of the world, because there are legit use-cases that require it. One of the use-case to grant an application ‘Directory.ReadWrite.All’ is, to allow an application to make backups of Azure AD.

SolarWinds Campaign

During the SolarWinds campaign, we have seen adversaries targeting Service Principals, in order to access Microsoft 365 mailboxes of users.

Source: https://www.darkreading.com/attacks-breaches/solarwinds-linked-attackers-target-microsoft-365-mailboxes/d/d-id/1340467

Let’s now take a step back. At the beginning of the blog post, we discussed that once an application has been registered. It will automatically create an application object and a service principal.

Once the application has been registered. We are able to assign API permissions to the application to allow it perform specific operations. This could include the following ‘Mail.Read’ permissions, which allows a Service Principal to read all the mailboxes of users.

Organizations have tons of different applications, but how often are these permissions reviewed? – I can tell from experience it rarely happens. An other important thing to highlight is, that organizations don’t monitor Service Principals in the first place nor understand what applications are considered ”critical”.

What applications are considered critical?

As we (may) know, organizations have tons of different applications. Every application contains certain permissions, but not every application is immediately critical. I believe there should be some form of ‘baseline’ in terms of what API permissions are considered ”critical”.

Great thing is that Microsoft has shared this with us, and they have marked the following API permissions as sensitive:

  1. Mail.* (including Mail.Send*, but not Mail.ReadBasic*)
  2. Contacts. *
  3. MailboxSettings.*
  4. People.*
  5. Files.*
  6. Notes.*
  7. Directory.AccessAsUser.All
  8. User_Impersonation

Delegated and AppOnly versions of the following permissions:

  • Application.ReadWrite.All
  • Directory.ReadWrite.All
  • Domain.ReadWrite.All*
  • EduRoster.ReadWrite.All*
  • Group.ReadWrite.All
  • Member.Read.Hidden*
  • RoleManagement.ReadWrite.Directory
  • User.ReadWrite.All*
  • User.ManageCreds.All
  • All other AppOnly permissions that allow write access

We can use the AzureADIR PowerShell module of Microsoft to make an export of all the permissions that have been assigned to applications.

In order to install this module, we have to run the following command:

Install-Module -Name AzureADIncidentResponse

Once the PowerShell module has been installed, we have to run the following command:

Import-Module AzureADIncidentResponse

Now we have to connect to our Azure AD tenant.

Connect-AzureADIR <YourTenantId>

We are now connected with our tenant. The final step is to run the following command, which will export all the application permissions in a CSV file.

Get-AzureADIRPermission -TenantId <YourTenantId> -CsvOutput

Application Permissions Types

There are two types of permissions, which are known as ‘Application‘ and ‘Delegated‘ permission types. Besides that, there is a term known as effective permissions, which are the permissions that your application has, when it makes a request to the target resource.

Application permissions are used by apps that run without a signed-in user present, for example, apps that run as background services.

For application permissions, the effective permissions of your app are the full level of privileges . For example, an app that has the Mail.Read permission can read the mailboxes of every user in the organization.

Delegated permissions are used by apps that have a signed-in user present. For these apps, either the user or an administrator consents to the permissions that the app requests. The app is delegated with the permission to act as a signed-in user when it makes calls to the target resource.

For delegated permissions, the effective permissions of your app are the least-privileged intersection of the delegated permissions the app has been granted (by consent) and the privileges of the currently signed-in user. Your app can never have more privileges than the signed-in user.

IMPORTANT

Let’s assume your app has been granted the User.ReadWrite.All delegated permission. This permission nominally grants your app permission to read and update the profile of every user in an organization. If the signed-in user is a global administrator or equivalent, your app can update the profile of every user in the organization. However, if the signed-in user doesn’t have an administrator role, your app can update only the profile of the signed-in user. It can NOT update the profiles of other users in the organization, because the user that it has permission to act on behalf of, doesn’t have the rights.

Analyzing Permissions

We have made an export of all the permissions that are associated with the applications. The second step is now to analyze the permissions. As discussed previously, Microsoft has some kind of baseline when it comes down to permissions, that are classified as ‘sensitive’. This means that not every application is considered critical if it doesn’t have a sensitive permission.

During this example, we will be using Azure Data Explorer. This allows us to ingest the CSV files and query it with Kusto. However, it does not mean that ADX is required. Feel free to use something else that can help in analyzing datasets.

Application Permission

Application permissions are used by apps that run without a signed-in user present, for example, apps that run as background services.

  1. Here we are running a simple query to display the permissions of each application.

Query

Application
| project PermissionType, ClientDisplayName, ClientAppId, ResourceDisplayName, Permission

Result

At the sample result, we can see all the applications with the associated permission(s). I have left the display name of the application out of the results due to privacy reasons.

2. The second step is to look for applications with sensitive permissions. Microsoft has shared an list of API permissions to look at, which we will be using in our query.

Query

Application
| where Permission has_any ("Mail.Read","Mail.ReadWrite","Mail.Send","Contacts.Read","Contacts.ReadWrite","MailboxSettings.Read","MailboxSettings.ReadWrite","People.Read.All","Files.Read.All","Files.ReadWrite.All","Notes.Read.All","Notes.ReadWrite.All","Application.ReadWrite.All","Directory.ReadWrite.All","Domain.ReadWrite.All","EduRoster.ReadWrite.All","Group.ReadWrite.All","Member.Read.Hidden","RoleManagement.ReadWrite.Directory","User.ReadWrite.All","User.ManageCreds.All")
| summarize by PermissionType, ClientAppId, Permission
| sort by ClientAppId asc

Result

At the sample result, we can see all the application with the specified permissions.

3. Last part will now only look at the applications that are considered ”critical”, because they may have one of the sensitive permissions.

Query

Application
| where Permission has_any ("Mail.Read","Mail.ReadWrite","Mail.Send","Contacts.Read","Contacts.ReadWrite","MailboxSettings.Read","MailboxSettings.ReadWrite","People.Read.All","Files.Read.All","Files.ReadWrite.All","Notes.Read.All","Notes.ReadWrite.All","Application.ReadWrite.All","Directory.ReadWrite.All","Domain.ReadWrite.All","EduRoster.ReadWrite.All","Group.ReadWrite.All","Member.Read.Hidden","RoleManagement.ReadWrite.Directory","User.ReadWrite.All","User.ManageCreds.All")
| summarize by ClientAppId

Result

At the sample result, we can see 3 applications. These are the applications that have sensitive permissions and need to be monitored properly.

Delegated Permission

An reminder, but delegated permissions are used by apps that have a signed-in user present. For these apps, either the user or an administrator consents to the permissions that the app requests. The app is delegated with the permission to act as a signed-in user when it makes calls to the target resource.

This section is basically the exact same thing as the previous one, but with the focus on the ‘Delegated’ permission types.

  1. We are now looking at the delegated permission type. This will be a simple query to display all the delegated permission types that are associated with each application.

Query

Delegated
| project PermissionType, ClientDisplayName, ClientAppId, ResourceDisplayName, Permission

Result

At the sample result, we can see a couple of applications.

2. The second step is to look for applications with sensitive permissions. Microsoft has shared an list of API permissions to look at, which we will be using in our query.

Query

Delegated
| where PermissionType == "Delegated"
| where Permission has_any ("Application.Read","Mail.Read","Mail.ReadWrite","Mail.ReadWrite.All","Mail.Read.All","Mail.Read.Shared","Mail.Send","Mail.Send.All","Mail.Send.Shared","MailboxSettings.Read","MailboxSettings.ReadWrite","Contacts.Read","Contacts.Read.Shared","Contacts.ReadWrite","Contacts.ReadWrite.Shared","People.Read","People.Read.All","Files.Read","Files.Read.All","Files.ReadWrite","Files.ReadWrite.All","Notes.Read","Notes.ReadWrite","Notes.Read.All","Notes.ReadWrite.All","Directory.AccessAsUser.All","Application.Read.All","Application.ReadWrite.All","AppRoleAssignment.ReadWrite.All","Directory.ReadWrite.All","EduRoster.ReadWrite","Group.ReadWrite.All","Member.Read.Hidden","RoleManagement.ReadWrite.Directory","RoleManagementPolicy.ReadWrite.Directory","User.ReadWrite","User.ReadWrite.All")
| project PermissionType, ClientDisplayName, ClientAppId, ResourceDisplayName, Permission, ConsentType, PrincipalObjectId, PrincipalDisplayName
| sort by ClientAppId

Result

At the sample result, we can see different application that have at least one of the specified permission(s).

3. Last part will now look at the applications that are considered ”critical”, because they may have one of the sensitive permissions.

Query

Delegated
| where PermissionType == "Delegated"
| where Permission has_any ("Application.Read","Mail.Read","Mail.ReadWrite","Mail.ReadWrite.All","Mail.Read.All","Mail.Read.Shared","Mail.Send","Mail.Send.All","Mail.Send.Shared","MailboxSettings.Read","MailboxSettings.ReadWrite","Contacts.Read","Contacts.Read.Shared","Contacts.ReadWrite","Contacts.ReadWrite.Shared","People.Read","People.Read.All","Files.Read","Files.Read.All","Files.ReadWrite","Files.ReadWrite.All","Notes.Read","Notes.ReadWrite","Notes.Read.All","Notes.ReadWrite.All","Directory.AccessAsUser.All","Application.Read.All","Application.ReadWrite.All","AppRoleAssignment.ReadWrite.All","Directory.ReadWrite.All","EduRoster.ReadWrite","Group.ReadWrite.All","Member.Read.Hidden","RoleManagement.ReadWrite.Directory","RoleManagementPolicy.ReadWrite.Directory","User.ReadWrite","User.ReadWrite.All")
| summarize by ClientAppId

Result

At the sample result, we can see a couple of application that have one of the specified permission(s).

How to get visibility in Service Principals and Applications?

We can use Log Analytics to configure these data sources. It is very important to do this, because Service Principal Sign-Ins logs, as well as Non-Interactive User sign-in logs, are NOT stored in Unified Audit Logs. This means that, when we are looking at it from a forensics perspective. We won’t be able to determine where a SP has logged in or what kind of Non-Interactive logins were happening, and so on.

There are some interesting use-cases that we will cover later in this blog post, which will include detection as well.

In order to create a Log Analytics workspace:

  • Go to the Azure Portal
  • Search for “Log Analytics workspace” in the search bar and press enter
  • Click on “Create”
  • Fill the rest of information in and finish

Once we have finished that:

  • Go to the Azure Portal
  • Click on “Azure Active Directory”
  • Click on “Diagnostic settings”
  • Click on “Add diagnostic setting”

Select the following data sources:

  • NonInteractiveUserSignInLogs
  • ServicePrincipalSignInLogs

Service Principal Sign-Ins from untrusted location

This section will cover an interesting use-case with Log Analytics. The plan is to create an detection rule once a Service Principal has signed-in from a untrusted location.

Once we have Service Principal Sign-In logs enabled. We can start querying the logs by calling the ‘AADServicePrincipalSignInLogs’ table.

Before we are doing that, we will first look at the ‘Application‘ permission type, and list all the Applications that we considered critical. There were 3 results in this example.

This image has an empty alt attribute; its file name is image-116.png

The second step is to understand from which IP range(s) or addresses the Service Principal usually sign-ins from. If you already collecting ‘AADServicePrincipalSignInLogs’ in Log Analytics, you can run the following query as an example.

Query

let timeframe = 90d;
AADServicePrincipalSignInLogs
| where TimeGenerated >= ago(timeframe)
| where AppId == '<YourAppId>'
| summarize arg_max(ServicePrincipalName, *) by IPAddress
| project TimeGenerated, AppId, ServicePrincipalName, IPAddress, Location

Result

At the sample result, we can see that the Service Principal has signed-in from 7 different IP addresses. The important thing now is to verify if it’s legitimate or not.

Let’s say that we could verify all of them and the IP addresses are legit. We can start writing a KQL query, whereby we will exclude a list of IP addresses or ranges. In order to do this, we can use the ipv4_is_match() function in KQL.

Query

AADServicePrincipalSignInLogs
| where AppId == 'e7c01394-3a7f-4f99-ba4c-5f2e23946701'
| where ipv4_is_match(IPAddress, '3.212.149.113') == false
| where ipv4_is_match(IPAddress, '3.216.35.196') == false
| where ipv4_is_match(IPAddress, '3.220.9.82') == false
| where ipv4_is_match(IPAddress, '52.188.84.13') == false
| where ipv4_is_match(IPAddress, '52.252.175.114') == false
| where ipv4_is_match(IPAddress, '18.233.140.207') == false
| where ipv4_is_match(IPAddress, '52.153.233.135') == false

Result

At the sample result, we can now see there are 0 results. This means that once a Service Principal would sign-in from an IP address that was not listed in our query. We can alert on it!

We still have two applications left that have sensitive permissions, so we have to do it for those applications as well. Once everything is finished. We can start finalizing our KQL query, which may look like the following:

Query

(union isfuzzy=true
(AADServicePrincipalSignInLogs
// Exclusion list for Service Principal 'A'
| where AppId == 'e7c01394-3a7f-4f99-ba4c-5f2e23946701'
| where ipv4_is_match(IPAddress, '3.212.149.113') == false
| where ipv4_is_match(IPAddress, '3.216.35.196') == false
| where ipv4_is_match(IPAddress, '3.220.9.82') == false
| where ipv4_is_match(IPAddress, '52.188.84.13') == false
| where ipv4_is_match(IPAddress, '52.252.175.114') == false
| where ipv4_is_match(IPAddress, '18.233.140.207') == false
| where ipv4_is_match(IPAddress, '52.153.233.135') == false
),
(AADServicePrincipalSignInLogs
// Exclusion list for Service Principal 'B'
| where AppId == '363afdf9-6e3d-4e69-8114-8b2f86a08440'
| where ipv4_is_match(IPAddress, '186.47.183.0/24') == false
),
(AADServicePrincipalSignInLogs
// Exclusion list for Service Principal 'C'
| where AppId == '6804077e-6455-4aec-ab2b-461f2846b14f'
| where ipv4_is_match(IPAddress, '146.223.80.0/24') == false
| where ipv4_is_match(IPAddress, '40.113.160.138') == false
)
)

Result

There are no results now. However, when one of the specified Service Principals sign-ins from a different IP address, that is not listed in the query. We can start alerting on it.

Here is an example of creating an alert based of a KQL query.

Non-Interactive User Sign-In Logs

Non-interactive user sign-ins are sign-ins that were performed by a client app or an OS component on behalf of a user. Like interactive user sign-ins, these sign-ins are done on behalf of a user. The device or client app uses a token or code to authenticate or access a resource on behalf of a user.

Read this one more time:

Let’s assume your app has been granted the User.ReadWrite.All delegated permission. This permission nominally grants your app permission to read and update the profile of every user in an organization. If the signed-in user is a global administrator or equivalent, your app can update the profile of every user in the organization. However, if the signed-in user doesn’t have an administrator role, your app can update only the profile of the signed-in user. It can NOT update the profiles of other users in the organization, because the user that it has permission to act on behalf of, doesn’t have the rights.

Ok, we are now going to explain this in steps. We can see an application with ‘User.ReadWrite.All’ and the permission type is set as ‘Delegated’

If we now run the following query:

let timeframe = 90d;
AADNonInteractiveUserSignInLogs
| where TimeGenerated >= ago(timeframe)
| where AppId == '<YourAppId>'
| summarize arg_max(AppDisplayName, *) by Identity
| project TimeGenerated, AppId, AppDisplayName, IPAddress, Identity

Result

We can see which identities have signed-in to the application. The App has ‘User.ReadWrite.All’ permission as we discussed before. However, it is important to look at which identities have signed-in to the application that are an Global Admin or equivalent.

As we discussed previously, delegated permissions are used by apps that have a signed-in user present. In order for an application to update the profile of every user in the organization. The signed-in user needs to be a Global Admin or equivalent in the tenant. When this is not the case, the application can update only the profile of the signed-in user.

Last thing, which is important to highlight. There is a huge blind spot for investigators, because API calls that have been made via MS Graph are not logged.

Summary

We started with explaining what Service Principals are and followed-up with a use-case on how they have been abused in real cases. Once we have done that, we started explaining what API permissions are considered ‘sensitive’ by Microsoft, and how we can audit those permissions with the AzureADIR PowerShell module.

After we made an export of all the application permissions. We have learned the difference between ‘Application’ and ‘Delegated’ permission types. It can be confusing sometimes, so it’s good to understand the difference.

After that, we covered some use-cases with Log Analytics. This helps us to get visibility in our Service Principals and Applications.

Reference

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s