<!-- Copyright (c) 2026 defconxt. All rights reserved. -->
<!-- Licensed under AGPL-3.0 — see LICENSE file for details. -->
---
name: detecting-shadow-admin-accounts
description: >-
  Detect shadow administrator accounts that have effective admin privileges
  through indirect paths — nested group memberships, application consent
  grants, custom role assignments, and Azure AD service principal permissions
  that bypass standard admin role visibility.
domain: cybersecurity
subdomain: identity-access-management
tags:
  - shadow-admin
  - privilege-escalation
  - azure-ad
  - active-directory
  - role-assignments
  - hidden-privileges
  - okta
version: "1.0"
author: defconxt
license: AGPL-3.0
metadata:
  mitre-attack: ["T1098"]
---

# Detecting Shadow Admin Accounts

## Overview

Shadow admins are accounts with effective administrative control that do not
appear in standard admin role listings. They acquire privileges through nested
group memberships, OAuth application consent grants, service principal
permissions, custom RBAC roles, or resource-level admin rights. These accounts
are invisible to access reviews, PIM, and standard admin inventories — making
them ideal persistence mechanisms for attackers and governance blind spots.

Mode: `[MODE: BLUE]` for shadow admin detection; `[MODE: RED]` for privilege escalation paths.

MITRE ATT&CK: T1098 (Account Manipulation), T1078 (Valid Accounts), T1098.003 (Additional Cloud Roles)

## Prerequisites

- Azure AD Global Reader or Security Reader
- Microsoft Graph API with Directory.Read.All
- Active Directory with RSAT tools (on-premises)
- BloodHound / AzureHound for path analysis
- Python 3.10+ for agent tooling

## Key Concepts

### Shadow Admin Vectors

| Vector | Description | Visibility |
|--------|-------------|------------|
| Nested group admin | User in group that is member of admin group | Low |
| App consent grant | App with Directory.ReadWrite.All acting as user | Very Low |
| Service principal roles | SP with Global Admin equivalent permissions | Low |
| Custom RBAC roles | Custom role with admin-equivalent actions | Medium |
| Resource admin | Key Vault/Storage admin with data plane access | Low |
| AD delegation | AdminSDHolder-protected objects with hidden ACLs | Very Low |
| Conditional Access exclusion | Account excluded from all CA policies | Medium |

### Azure AD Shadow Admin Detection

```powershell
# Find all users with admin roles (including transitive group membership)
Connect-MgGraph -Scopes "Directory.Read.All", "RoleManagement.Read.Directory"

# Direct admin role assignments
$directAdmins = Get-MgDirectoryRole -ExpandProperty Members |
  ForEach-Object {
    $role = $_
    $_.Members | ForEach-Object {
      [PSCustomObject]@{
        Role = $role.DisplayName
        RoleId = $role.RoleTemplateId
        MemberType = $_.'@odata.type'
        MemberId = $_.Id
        DisplayName = $_.AdditionalProperties.displayName
      }
    }
  }

# Service principals with high-privilege app roles
$dangerousPerms = @(
    "Directory.ReadWrite.All",
    "RoleManagement.ReadWrite.Directory",
    "Application.ReadWrite.All",
    "AppRoleAssignment.ReadWrite.All"
)

Get-MgServicePrincipal -All | ForEach-Object {
    $sp = $_
    Get-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $sp.Id |
      Where-Object { $_.AppRoleId -ne "00000000-0000-0000-0000-000000000000" } |
      ForEach-Object {
        [PSCustomObject]@{
          ServicePrincipal = $sp.DisplayName
          AppRoleId = $_.AppRoleId
          ResourceDisplayName = $_.ResourceDisplayName
        }
      }
}

# Users excluded from all Conditional Access policies
$caPolicies = Get-MgIdentityConditionalAccessPolicy
$excludedUsers = @{}
foreach ($policy in $caPolicies) {
    foreach ($userId in $policy.Conditions.Users.ExcludeUsers) {
        if (-not $excludedUsers[$userId]) { $excludedUsers[$userId] = 0 }
        $excludedUsers[$userId]++
    }
}
$excludedUsers.GetEnumerator() | Where-Object { $_.Value -eq $caPolicies.Count } |
  ForEach-Object { Get-MgUser -UserId $_.Key | Select-Object UserPrincipalName }
```

### Active Directory Shadow Admin Detection

```powershell
# Find accounts with AdminSDHolder protection (hidden admin equivalence)
Get-ADUser -Filter {AdminCount -eq 1} -Properties AdminCount, MemberOf |
  Select-Object SamAccountName, AdminCount,
    @{N="Groups"; E={$_.MemberOf | ForEach-Object {($_ -split ',')[0] -replace 'CN=',''}}}

# Find nested admin group memberships
function Get-NestedGroupMember {
    param([string]$GroupName, [int]$Depth = 0, [int]$MaxDepth = 5)
    if ($Depth -ge $MaxDepth) { return }
    Get-ADGroupMember -Identity $GroupName | ForEach-Object {
        [PSCustomObject]@{
            Name = $_.Name
            Type = $_.objectClass
            Depth = $Depth + 1
            Path = $GroupName
        }
        if ($_.objectClass -eq 'group') {
            Get-NestedGroupMember -GroupName $_.Name -Depth ($Depth + 1) -MaxDepth $MaxDepth
        }
    }
}
Get-NestedGroupMember -GroupName "Domain Admins"

# Find users with DCSync rights (DS-Replication-Get-Changes-All)
$domainDN = (Get-ADDomain).DistinguishedName
$acl = Get-ACL "AD:\$domainDN"
$acl.Access | Where-Object {
    $_.ObjectType -eq "1131f6ad-9c07-11d1-f79f-00c04fc2dcd2"  # DS-Replication-Get-Changes-All
} | Select-Object IdentityReference, AccessControlType
```

### AzureHound / BloodHound Analysis

```bash
# Collect Azure AD data with AzureHound
azurehound list -t ${ACCESS_TOKEN} --output azurehound.json

# Import to BloodHound and query for shadow admin paths
# Cypher: Find shortest path to Global Admin
# MATCH p=shortestPath((u:AZUser)-[*1..]->(r:AZRole {displayname:'Global Administrator'}))
# WHERE NOT u.displayname CONTAINS 'admin'
# RETURN p
```

## Workflow

1. **Enumerate** — List all direct admin role members across Azure AD and AD
2. **Expand** — Resolve nested group memberships to find transitive admins
3. **Analyze** — Map service principal permissions and app consent grants
4. **Compare** — Diff effective admins vs. documented/expected admin list
5. **Investigate** — Research each shadow admin for legitimacy
6. **Remediate** — Remove unnecessary privileges, convert to PIM eligible
7. **Monitor** — Deploy continuous detection for new shadow admin creation

## Detection

```yaml
title: Shadow Admin Accounts Detection
id: 3d85d3dd-33e0-4192-8c7f-30f824dab3b0
status: experimental
description: Detects suspicious activity related to detecting shadow admin accounts techniques in identity access management context
logsource:
  category: authentication
  product: windows
detection:
  selection:
    EventType: authentication
    Status: failure
  condition: selection
level: medium
tags:
  - attack.t1098
  - attack.credential_access
falsepositives:
  - Automated password rotation by privileged access management tools
```

## Verification

- [ ] All effective admin accounts inventoried (direct + transitive)
- [ ] Service principal permissions reviewed — no unnecessary Directory.ReadWrite.All
- [ ] Nested group memberships resolved for all admin-equivalent groups
- [ ] AdminSDHolder-protected accounts reviewed in Active Directory
- [ ] Conditional Access exclusion lists audited — no blanket exclusions
- [ ] OAuth application consent grants reviewed for admin-equivalent permissions
- [ ] BloodHound/AzureHound attack path analysis completed
- [ ] Continuous monitoring deployed for new admin role assignments

## References

- [Microsoft — Identify Shadow Admins](https://learn.microsoft.com/en-us/defender-for-identity/security-assessment-unconstrained-kerberos)
- [BloodHound Documentation](https://bloodhound.readthedocs.io/)
- [CIS Microsoft 365 Benchmark](https://www.cisecurity.org/benchmark/microsoft_365)
- [MITRE ATT&CK T1098.003](https://attack.mitre.org/techniques/T1098/003/)
