Feb 12 2024 |
Directory.ReadWrite.All Is Not As Powerful As You Might Think
Directory.ReadWrite.All is an MS Graph permission that is frequently cited as granting high amounts of privilege, even being equated to the Global Admin Entra ID role.
Why it matters
- Azure admins and security professionals may put undue focus on this permission at the expense of more impactful permissions
- Those more impactful permissions may go ignored, leaving potentially dangerous configurations in place
Yes, but…
- Directory.ReadWrite.All does grant some privileges, and those privileges can lead to dangerous attack paths depending on other configurations and user behaviors
- Admins and security professionals should understand and give Directory.ReadWrite.All the attention it deserves
- Since MS Graph changes all the time, any app role (including Directory.ReadWrite.All) could become more powerful in the future
Why Directory.ReadWrite.All Gets So Much (Undue) Attention
Misleading or incorrect documentation create most of the misconceptions regarding this permission. The Microsoft documentation for Directory.ReadWrite.All says:
“Directory.ReadWrite.All grants access that is broadly equivalent to a global tenant admin. Apps that are granted Directory.ReadWrite.All can manage the full range of directory resources, and they can manage authorization for other apps and users to access resources across the organization. This includes directory resources like users, groups, applications, and devices, and non-directory resources in Exchange, SharePoint, Teams, and other services.”
To understand what this really means, we need to go deeper into the documentation and identify all MS Graph endpoints that reference the Directory.ReadWrite.All permission as being required. The official documentation does not provide that list, but Merill Fernando has done the work for us on his excellent Graph Permissions site. This page lists all endpoints that cite Directory.ReadWrite.All on their respective pages. It’s on that page where we can start to see specific MS Graph URIs that cite Directory.ReadWrite.All (Figure 1):
For example, we see this HTTP verb and URI on that page:
POST /servicePrincipals/{id}/owners/$ref
Clicking the link takes us to this page, which has information about the permissions required to call this API and examples of how to do so. With the permissions table on this page, you may notice that Directory.ReadWrite.All isn’t cited at all (Figure 2):
This is because we are looking at the v1.0 version of this endpoint. In the upper left, select “Microsoft Graph REST API Beta” to see information about the beta version of the endpoint. It’s here where we will see Directory.ReadWrite.All mentioned in the permissions table (Figure 3):
In the “Application” permission type row, the table says, “Application.ReadWrite.OwnedBy and Directory.ReadWrite.All” and, “Application.ReadWrite.All and Directory.ReadWrite.All”. This seems to indicate to the reader that both permissions are needed to make a successful POST to this endpoint; however, as the saying goes, “Documentation is a lie waiting to happen.”
Proving (and Disproving) the Documentation
The documentation seems to be saying that a service principal must have both of these MS Graph app roles in order to add a new owner to an existing service principal:
- Application.ReadWrite.All
- Directory.ReadWrite.All
Or, if modifying a service principal that the service principal itself owns, these two app roles:
- Application.ReadWrite.OwnedBy
- Directory.ReadWrite.All
We can pretty easily test this ourselves to determine what the truth is. For this test, we will create the following lab environment (Figure 4):
SP1 through SP6 are Entra ID service principals:
- SP1 has been granted the Directory.ReadWrite.All and Application.ReadWrite.All MS Graph app roles
- SP2 has been granted the Directory.ReadWrite.All and Application.ReadWrite.OwnedBy MS Graph app roles and is an an owner of SP6
- SP3 has been granted the Application.ReadWrite.All MS Graph app role
- SP4 has been granted the Application.ReadWrite.OwnedBy MS Graph app role and has been added as an owner of SP6
- SP5 has been granted the Directory.ReadWrite.All MS Graph app role
First, we will test whether SP1 is able to add a new owner to SP6. I’ll use my GLOBAL ADMIN account to add a new credential to SP1’s app registration (Figure 5).
Next, I will use the the client credential flow to acquire an MS Graph-scoped JSON web token (JWT) (Figure 6):
When we decode the token, we can verify that it has the Directory.ReadWrite.All and Application.ReadWrite.All app roles. I like to use jwt.ms for decoding (Figure 7).
Now we are ready to make our POST request to the /servicePrincipals/{id}/owners/$ref endpoint. In the body, we will specify the object ID of “SP1”. In the request URI, we will specify the object ID of “SP6”. In other words, we are attempting to add SP1 as a new owner of SP6 (Figure 8).
The HTTP response status code is 204 (i.e., “no content”). If we look at the Azure portal GUI, we will see that the SP1 service principal was able to add itself as an owner to SP6 (Figure 9).
You may have noticed that the POST request was sent to the v1.0 version of the MS Graph API (Figure 10).
Let’s try against the beta version this time. First, we will use our GLOBAL ADMIN user to remove SP1 as an owner of SP6, and then repeat the test, but targeting the beta version of MS Graph this time (Figure 11).
Again, SP1 was able to add itself as an owner to SP6.
Let’s start building a table to keep track of our test results, noting the granted MS Graph app roles, whether the test SP owns the target SP, API version we hit, and the result of the test (Figure 12).
I know it isn’t pretty, but it doesn’t need to be.
We will continue the testing with SP2. SP2 is already an owner of the target SP, so if we attempt to add it as an owner again, that will fail. Instead, we will create another SP (i.e., SP7), and attempt to have SP2 add SP7 as an owner to SP6 using both the v1.0 and beta versions (Figure 13).
And we’ll update our table (Figure 14).
Figure 15 contains the test results with SP3.
Figure 16 contains the test results with SP4.
Figure 17 contains our updated test results table.
Now we are ready to test SP5’s ability to add itself as an owner over SP6. Recall from our earlier diagram that SP5 has only been granted the Directory.ReadWrite.All MS Graph app role. We will first test against the v1.0 version of the API (Figure 18).
You can see that the API returned an error stating we have, “Insufficient privileges to complete the operation.” What if we try against the beta version of the API (Figure 19)?
Again, we’re hit with insufficient privileges. Let’s update our test results table again to reflect these results (Figure 20).
As you can see, the Directory.ReadWrite.All MS Graph app role is not enough, on its own, to add new owners to service principals.
Just How Powerful Is Directory.ReadWrite.All, Anyway?
Not very. In fact, Directory.ReadWrite.All is more equivalent in power to the built-in Entra ID role GROUPS ADMINISTRATOR, not the GLOBAL ADMINISTRATOR role as the current documentation states. I have run tests similar to the above for each MS Graph API endpoint that an adversary may use in the course of an attack path.
Here’s what a service principal with only the Directory.ReadWrite.All app role can’t do:
- Promote itself to GLOBAL ADMIN
- Grant itself an app role
- Reset any user’s password
- Add a new credential to an app
- Add a new credential to a service principal
- Add a new owner to an app
- Add a new owner to a service principal
- Add a new member to a role-assignable security group
- Add a new owner to a role-assignable security group
And here is what a service principal with only the Directory.ReadWrite.All app role can do:
- Add a new member to a non-role-assignable security group
- Add a new owner to a non-role-assignable security group
- Add new users to the tenant
Now, this does not mean that Directory.ReadWrite.All doesn’t matter or that it doesn’t present any risk. While non-role-assignable security groups can’t have Entra ID role assignments, they can have AzureRM role assignments. We have seen attack paths traverse through non-role-assignable groups to AzureRM resources that can result in escalation to GLOBAL ADMIN.
Admins and security professionals should not outright ignore the Directory.ReadWrite.All app role; rather, they should give it the appropriate attention it deserves based on these facts and on the particular configurations of each Azure environment.
What if You’re Wrong?
I’ve been wrong before, I’ll be wrong again, and I could be wrong now. I wouldn’t blame you, reader, for taking at face value Microsoft’s statement of this role being equivalent to GLOBAL ADMIN; however, from my understanding and testing, this doesn’t appear to be the case and it is likely distracting you from roles that actually are equivalent to GLOBAL ADMIN.
If you have a POC that shows how to escalate to GLOBAL ADMIN with only the Directory.ReadWrite.All app role, please let me know and I will correct this post.
There are other privileged APIs an adversary may be interested in abusing for persistence or privilege escalation purposes. I have tested the ones I know of, but there are new APIs coming to MS Graph all the time; as such, the information in this blog post will eventually be out of date.
In my 2022 Ekoparty presentation, “Azure Backdoors: How to Hide Them, How to Find Them”, I demonstrated several other attractive API endpoints and the privileges actually required to access them. Directory.ReadWrite.All alone is not enough to access any of them.
What App Roles Should We Focus On?
There are two MS Graph app roles that guarantee escalation to GLOBAL ADMIN:
- RoleManagement.ReadWrite.Directory
- AppRoleAssignment.ReadWrite.All
The first, RoleManagement.ReadWrite.Directory, permits a service principal with that app role to promote itself or any other principal to any Entra ID role, including GLOBAL ADMINISTRATOR.
The second, AppRoleAssignment.ReadWrite.All, permits a service principal with that app role to grant itself or any other service principal any MS Graph app role, including RoleManagement.ReadWrite.Directory, with the added bonus of uniquely having the ability to bypass the admin consent process.
You can read more about those two particular roles here: https://posts.specterops.io/azure-privilege-escalation-via-azure-api-permissions-abuse-74aee1006f48
Admins and security professionals should also pay attention to the following MS Graph app roles:
- Application.ReadWrite.All — Enables adding credentials and owners to all existing apps and service principals
- Group.ReadWrite.All — Enables adding owners and members to all non-role-assignable groups
- GroupMember.ReadWrite.All — Enables adding members to all non-role-assignable groups
- ServicePrincipalEndpoint.ReadWrite.All — Enables adding credentials to all existing service principals
Conclusion
These systems are not just confusing and opaque; they’re also dynamic. What’s true today may be false tomorrow. As such, it is important for administrators and security professionals to understand the true impact of any given permission and, unfortunately, that often means we cannot trust documentation and must verify our environments for ourselves.
We work hard to do that work for our users. BloodHound CE is free and open-source software (OSS) you can use to easily audit all of the abusable MS Graph app roles in a tenant. See Stephen Hinck’s recent blog post for a great example: https://posts.specterops.io/microsoft-breach-how-can-i-see-this-in-bloodhound-33c92dca4c65
Not using BloodHound yet? Get BloodHound CE here: https://github.com/SpecterOps/BloodHound
We also have a commercial version of BloodHound called BloodHound Enterprise (BHE), which you can learn about at https://bloodhoundenterprise.io/
Directory.ReadWrite.All Is Not As Powerful As You Might Think was originally published in Posts By SpecterOps Team Members on Medium, where people are continuing the conversation by highlighting and responding to this story.