Epic Pentest Fail

Oct 30 2025
Share
By: Forrest Kasler • 8 min read

How to Wreck Entra with a Single Mistyped Character

This is a cautionary tale. Always read the docs when you are about to issue a “DELETE” operation on any resource in your client’s infrastructure! I should have known better…

You’re on a team performing an external penetration test for a large client. To your surprise, you discover a developer who left their private key in an old git commit to a public repository. This quickly leads to the discovery of some internal repositories and more useful keys and secrets. You continue to push and spiral out your access until finally you hit a trove of Entra app secrets!

Attack Path Analysis

One amazing perk of working at SpecterOps is that we get to pull out Bloodhound Enterprise (BHE) on our assessments as needed. We had a dozen or more app credentials, but no clue which (if any) gave us a path to Tier Zero. To solve this problem, we just ran an AzureHound collection with one of the working app secrets, tossed the data in BHE, and within seconds had a clear path from one compromised account to Global Admin!

All we had to do was add a password to another account, and then use that account to add ourselves to the Global Admin (GA) role. This was literally just two commands away from GA, but we weren’t going to do anything drastic without our client’s permission.

Asking Nicely

After two minutes talking with the client about what we found, we hear:

“Why haven’t you done it yet?”

Yay! I really love it when clients see the value in our work, see themselves as part of the pentest team, and trust us with their network!

We ran the commands and, just as our BloodHound results promised, we achieved GA. As a proof-of-concept, and a value add to the client, we re-ran AzureHound collection from the privileged role to get a more comprehensive look at the other attack paths in Entra. So far so good.

All Done Here(?)

It’s Saturday morning. I make some coffee, remember that I should probably go ahead and remove our privileged access now that we have all the data we need, and decide to do some quick cleanup. It’s just two commands. I had already typed them out and had them waiting in my notes. Two easy steps:

1) Remove the extra entry we made to the GA role
2) Remove the extra password we put on an Entra App

I run the first command:

az rest — method DELETE — url “https://graph.microsoft.com/v1.0/directoryRoles/e67ab931-f5eb-4dee-80ee-1e057f5ef555/members/7793192b-3956–494b-9b58-a1b9bd5967f3/\$ref"

Nothing happens.
Nothing! 
No output.

This is a little odd as far as APIs are concerned, but I remember that when I added our service principal to the GA role, that API call didn’t have any output either. I go to check if the change worked by querying the service principal members of the GA role:

az rest — method GET — url “https://graph.microsoft.com/v1.0/directoryRoles/e67ab931-f5eb-4dee-80ee-1e057f5ef555/members/microsoft.graph.servicePrincipal"

Unauthorized({“error”:{“code”:”Authorization_IdentityNotFound”,”message”:”The identity of the calling application could not be established.”)

That’s odd. Now it’s saying I’m unauthorized?

I tried to log out and back in, thinking my session was stale: Authentication Failure! 

Uh Oh!

Petrifying Fear

I look back at my “DELETE” request and it seems like I did it right…but what if I did it wrong? What if the server only parsed up to the directory role, and not the “members” list attribute? Meaning…

What if I just deleted the GA role from my client’s Entra tenant? Is that even possible? If so, is that fixable? Who would have the privileges to fix it if there are no GA? You know, because I deleted it…

Brief Panic

What Happened?

At this point, I log in with another compromised account, run some commands to find out what changed, and discover that the service principal escalated to GA was now deleted!

(Breath a heavy sigh of relief that the Global Admins role is still there)

For a moment, I assume that this must have been an overzealous action by an incident responder who must have seen our new GA member and simply deleted it. Maybe my attempt to remove the GA membership just silently failed. Surely, the /directoryRoles API can’t delete a service principal object! That functionality should only be achievable through the /servicePrincipal API and would be a violation of the Single Responsibly Principle (SRP) in API design. Right?

I double check the docs just to be sure:

SRP Violation

No. Way.

How? Why? When would you ever use that? Why would you just document this mistake and not fix it?

But wait! I did put that “$ref” at the end of my command! What gives?

PowerShell. The way you escape special characters like “$” in PowerShell is with the backtick “`” not the backslash “\”. MS Graph developers decided you need to supply an argument with a special character in it to this API endpoint, and properly escape it, otherwise the endpoint just deletes your account. The worst part? I know this is how to escape characters in PowerShell. I should have known better and still stepped on this landmine.

Watch your step

Restore to the Rescue

Luckily (as noted in the screenshot), MS Graph has the “restore” API for just this type of mistake. We were able to leverage another privileged account to put everything back with a single command:

az rest — method POST — url “https://graph.microsoft.com/v1.0/directory/deletedItems/7793192b-3956–494b-9b58-a1b9bd5967f3/restore"

To my surprise, when I restored the service principal, it was still in the GA role! This meant my botched command not only deleted the service principal, but left a dangling reference in the list of GA members. Yikes! I still needed to properly remove it from GA, but without completely deleting it this time.

Proper Cleanup

In order to restore the deleted principal, we had to temporarily add yet another account to the GA role. We used that account to remove our original addition to the GA, but when we went to back out our own access:

az rest — method DELETE — url “https://graph.microsoft.com/v1.0/directoryRoles/e67ab931-f5eb-4dee-80ee-1e057f5ef555/members/fe6981a3-f359–461e-9e0d-c7846146804e/`$ref" ← Note the fixed ` instead of \

Bad Request: “Unable to remove current authenticated principal from Role membership. User cannot remove self from TenantAdmins role.”

What the? You mean I can delete myself with no complaints, but I can’t remove myself from a group?!? Ahhhhhhhhhhhhhhhhh!

So how do we even clean up access?

It took me a minute, but eventually I realized that my original attack path included promoting a principal to GA, without already being a GA. Maybe that account could also remove a GA member, without itself being a GA. I log out, log back in with our original (now restored) account, and remove our last modifications without issue. Everything was back to how we found it.

Lessons Learned

Always check the docs! — Though, I am not sure knowing about a landmine like this will always save you. I put the right argument on the end of the “DELETE” API call, but just didn’t escape it right. If we had just run the command from a Linux shell, it would have been the correct syntax.

“Pentesting is a contact sport” — Randy Romes

These words ring in my head any time I think about pentesting mishaps. Pentesting carries some inherent risks. Even seasoned practitioners can (and do) make mistakes. The best way forward is always to own the mistake immediately, inform your client, and help fix it if possible. A big shout out and sincere thank you to Randy, who taught me this valuable lesson first hand when I was just a fledgling hacker!

If you write APIs (Any MS developers reading this, please take note)

Adhere to the SRP — Don’t write APIs that can delete a completely unrelated object when the API URL is designed to modify a list property on another object type. If you make this mistake, fix it! Don’t just document the flaw.

Don’t use Special Characters in API URLs — There is a reason why you practically never see special characters like “$,:,#,*,!” in URLs in general, let alone API calls. Doing so could leave your customers in tears and their infrastructure in shambles.

You’ve been warned.