Knowing Where the Doors Are (and Fearing They Were All Unlocked)

Jan 7, 2026 min read

The moment it genuinely felt catastrophic

This wasn’t a mild “huh, that’s odd” reaction.

It was a full-body, stomach-dropping, existential one.

While walking two senior security practitioners through OID-See, I demonstrated its default authentication model: a completely standard user authenticating via the Microsoft Azure CLI service principal.

I showed how that user could then enumerate: - Users - Groups - Service principals - App permissions - Assignment requirements - Large chunks of Conditional Access posture

And then one of them asked the question that changed the temperature of the room:

“If Azure CLI is pre-consented for all of that… what’s actually stopping me from just assigning myself Global Admin?”

That wasn’t rhetorical.

For a few long seconds, I didn’t have a satisfying answer.

Because on the surface, the evidence looked damning.


The terrifying implication we couldn’t unsee

Once you really look at the pre-consented delegated scopes on Microsoft Azure CLI, it’s hard not to spiral:

  • Directory.AccessAsUser.All
  • Application.ReadWrite.All
  • Group.ReadWrite.All
  • DelegatedPermissionGrant.ReadWrite.All
  • AppRoleAssignment.ReadWrite.All

Read literally, they appear to say:

“You can read and write basically everything in the directory.”

If that interpretation were true, the implications would be apocalyptic: - Any standard user - In any tenant - Could silently escalate to Global Administrator - Using only Microsoft first-party tooling - With no exploit, no bug, no misconfiguration

That wouldn’t be a vulnerability.

That would be a collapse of the entire trust model.

And for a few minutes, that possibility felt uncomfortably real.


Why this fear was reasonable

This wasn’t paranoia.

It was pattern recognition.

In the last few years, we’ve all watched: - “Safe” defaults turn out not to be safe - First-party apps bypass Conditional Access - Delegated permissions behave in surprising ways - Token semantics drift faster than documentation

So when you see: - Massive delegated scopes - Pre-consented - On a ubiquitous first-party app - Accessible to every user

The correct response is not calm dismissal.

The correct response is: > “Stop everything and test this properly.”

Which is exactly what we did.


Purple teaming the nightmare scenario

The hypothesis was simple:

A standard user can use Azure CLI to grant themselves Global Administrator.

We tested it directly.

And then indirectly.

And then creatively.

Attempts included: - Direct directory role assignment - Adding the user to role-assignable groups - Modifying app role assignments - Granting additional Graph permissions - Abuse via group membership writes - Lateral pivots through application ownership

Every single path failed.

Hard.

Consistently.

Unambiguously.

The directory enforcement boundary held.

That was the first exhale.


The missing mental model: delegated ≠ authoritative

The reason this looked like a catastrophe comes down to a deeply unintuitive truth:

Delegated permissions describe surface area, not authority.

They mean: > “You may attempt these operations as the signed-in user.”

They do not mean: > “You may bypass directory authorization.”

Even with: - Directory.AccessAsUser.All - Group.ReadWrite.All - AppRoleAssignment.ReadWrite.All

The server still enforces: - Directory role checks - Protected object rules - Role-assignable group constraints

Scopes don’t grant power.

They grant reach.

And reach without authority stops at mutation.


What didn’t stop working: discovery

But something else worked flawlessly.

Enumeration.

The same token that couldn’t mutate privileged state could: - Map the entire tenant - Identify every privileged role and group - Enumerate all service principals - See which apps didn’t require assignment - Infer Conditional Access weaknesses

That’s where the fear transformed.

Not: > “This is broken.”

But: > “This collapses attacker discovery cost.”


Enumeration doesn’t give you power — it gives you time

This is the distinction that matters.

Enumeration: - Doesn’t unlock doors - Doesn’t grant admin - Doesn’t bypass controls

What it does is remove uncertainty.

Given full discovery, an attacker can: - Prioritise targets instantly - Ignore dead ends - Focus effort where governance is weakest

OID-See is explicitly built to expose this reality.

It doesn’t ask: > “Can this app escalate you?”

It asks: > “If escalation is possible anywhere, how fast do you find it?”


Active Directory déjà vu (again)

If this all feels familiar, it should.

Classic Active Directory worked the same way: - Any authenticated user could enumerate almost everything - Attacks succeeded because mutation paths were weak - BloodHound didn’t invent risk — it revealed topology

Entra ID didn’t change that model.

It modernised it.

Service principals replaced service accounts. Graph replaced LDAP. Scopes replaced ACL sprawl.

But the underlying truth stayed the same.


Where guest posture re-enters the picture

At this point, the existential fear had passed.

But a quieter, more persistent concern remained.

Guest users don’t live in the same threat model.

For members: - Broad enumeration is effectively mandatory - Security lives in governance and mutation controls

For guests: - Enumeration can be constrained - Cross-tenant access policy matters enormously - Defaults are often dangerously permissive

If guests are treated “like members”: - The discovery surface expands again - First-party apps become external reconnaissance tools - Weak governance collapses faster

This is where earlier guest-focused research kept resurfacing in my head.

Not because enumeration exists — but because who gets to enumerate matters.


Why this belongs in OID-See

OID-See already shows: > “Which service principals have broad reachability?”

The missing dimension was: > “Who else can reach them?”

Guest and external identity posture doesn’t create new exploits.

It amplifies existing risk.

So the design choice was deliberate: - Tenant-level posture only - Opportunistic collection - Conservative interpretation - Transparent amplification

No fear-mongering. No exploit claims. No false certainty.


Opportunistic, because reality demands it

Tenant policy objects sit behind Policy.Read.All.

Most delegated tokens — including Azure CLI — don’t have that.

So OID-See: - Inspects the token - Only attempts policy reads when plausible - Marks posture as unknown otherwise - Explains the limitation clearly

Unknown is not failure.

Unknown is honesty.


The sentence that survived the panic

After the fear subsided, one line stuck:

OID-See doesn’t tell you who can break in.
It tells you who already knows where all the doors are.

The initial terror came from thinking: > “What if all the doors are unlocked?”

The reality is subtler — and more dangerous:

Everyone can see the map.

And maps are power.

Not because they open doors.

But because they tell you which ones are worth kicking.


Closing thoughts

That initial existential fear was useful.

It forced proper testing. It forced better mental models. It forced restraint.

The outcome wasn’t a catastrophic vulnerability.

It was a clearer understanding of how identity risk actually works.

Directories trade secrecy for scale. They always have.

The danger isn’t that users can see.

It’s how fast weak governance collapses once they do.

OID-See exists to make that visible.

Quietly. Accurately. Without panic.

— Graham

comments powered by Disqus