Directory.ReadWrite.All Is Not As Powerful As You Might Think

Andy Robbins
Posts By SpecterOps Team Members
9 min readFeb 12, 2024

--

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):

Figure 1 — APIs that Reference Directory.ReadWrite.All

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):

Figure 2 — The v1.0 Version Permissions Table

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):

Figure 3 — The Beta Version Permissions Table

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):

Figure 4 — The Test Environment Design

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).

Figure 5 — Adding a New Credential to SP1

Next, I will use the the client credential flow to acquire an MS Graph-scoped JSON web token (JWT) (Figure 6):

Figure 6 — Acquiring a Token as SP1

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).

Figure 7 — The Decoded JWT

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).

Figure 8 — Executing the First Test

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).

Figure 9 — SP1 Successfully Added as an Owner of SP6

You may have noticed that the POST request was sent to the v1.0 version of the MS Graph API (Figure 10).

Figure 10 — The First Test Targeted the v1.0 Endpoint

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).

Figure 11 — Repeating the Test, Targeting the Beta Endpoint

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).

Figure 12 — The Test Results Table

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).

Figure 13 — The SP2 Test Results

And we’ll update our table (Figure 14).

Figure 14 — The Updated Test Results Table

Figure 15 contains the test results with SP3.

Figure 15 — The SP3 Test Results

Figure 16 contains the test results with SP4.

Figure 16 — The SP4 Test Results

Figure 17 contains our updated test results table.

Figure 17 — The 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).

Figure 18 — The v1.0 Endpoint SP6 Test Result

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)?

Figure 19 — The Beta Endpoint SP6 Test Result

Again, we’re hit with insufficient privileges. Let’s update our test results table again to reflect these results (Figure 20).

Figure 20 — The Updated Test Results Table

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/

--

--