Detecting Azure API Permissions Abuse

redhead0ntherun
3 min readDec 4, 2021

This is a follow-up article based on SpecterOps recent article that walked through a few attack paths using Azure API permissions to elevate all the way up to Global Admin. I encourage everyone to read the article which can be found HERE.

Basic information about the attack path identified in the SpecterOps post:

  1. The two permissions that can be abused are: RoleManagement.ReadWrite.Directory & AppRoleAssignment.ReadWrite.All. Detailed information can be found in Microsoft documentation — HERE
  2. These permissions allow an object (whether its a Service Principal, Application, or IAM user) to basically take over a tenant (Global Admin)
  3. The AppRoleAssignment.ReadWrite.All permission will enable the object to grant itself the RoleManagement.ReadWrite.Directory permission which allows the object to become a Global Admin.

Detection

SpecterOps provided instructions on how to configure auditing which will enable detection for this attack path in the above referenced blog post. I will focus on detecting the activity within your SIEM environment.

Assumptions

  1. You’re forwarding your M365/Azure logs to your SIEM
  2. Your SIEM is Splunk
  3. You know which index/sourcetype these audit logs reside in

Key Field’s and their values:

  • Workload=AzureActiveDirectory
  • Operation=”Consent to Application” OR “Add delegated permission grant”
  • ModifiedProperties{}.Name = “DelegatedPermissionGrant.Scope” OR “ConsentAction.Permissions”
  • ModifiedProperties{}.NewValue = “RoleManagement.ReadWrite.Directory” OR “AppRoleAssignment.ReadWrite.All”

Now, these logs in Splunk come in the form of JSON. JSON in Splunk is nice, but, nested JSON is kind of a mess when you’re looking for specific values. You can get around this with wildcards (“*”) at the beginning and end of your strings. Another way to deal with JSON is to use the SPATH command in Splunk. Using a combination of SPATH rename, and eval we can map the JSON field names from the nested JSON fields to corresponding values.

| spath “ModifiedProperties{}.Name” | spath “ModifiedProperties{}.NewValue” | rename “ModifiedProperties{}.Name” as “ModifiedProperties_Name”, “ModifiedProperties{}.NewValue” as “ModifiedProperties_NewValue” | eval parameters = mvzip(ModifiedProperties_Name,ModifiedProperties_NewValue,”::DELIM::”) | mvexpand parameters | eval parameters=split(parameters,”::DELIM::”) | eval parameters_name=mvindex(parameters,0) | eval parameters_value=mvindex(parameters,1) | eval {parameters_name}=parameters_value

{parameters_name}=parameters_value will create the following fields:

  • ConsentAction.Permissions
  • DelegatedPermissionGrant.Scope

The basic search is very straightforward:

index=azure sourcetype=azure:audit Workload=”AzureActiveDirectory” Operation IN (“Consent to application.”,”Add delegated permission grant.”) AND (“approleassignment.readwrite.all” OR “rolemanagement.readwrite.directory”) AND ModifiedProperties{}.NewValue IN (“*approleassignment.readwrite.all*”,”*rolemanagement.readwrite.directory*”)

Above will bring back the basic raw results. To turn it into a simple alert we want to transform the data into a more user friendly version. The rest of the search is filtering out all the extra information not necessarily needed for a SIEM alarm and assigning a severity to the alarm based on the permission assigned.

| spath “ModifiedProperties{}.Name” | spath “ModifiedProperties{}.NewValue” | rename “ModifiedProperties{}.Name” as “ModifiedProperties_Name”, “ModifiedProperties{}.NewValue” as “ModifiedProperties_NewValue” | eval parameters = mvzip(ModifiedProperties_Name,ModifiedProperties_NewValue,”::DELIM::”) | mvexpand parameters | eval parameters=split(parameters,”::DELIM::”) | eval parameters_name=mvindex(parameters,0) | eval parameters_value=mvindex(parameters,1) | eval {parameters_name}=parameters_value
| table _time Workload Operation “ConsentAction.Permissions” “DelegatedPermissionGrant.Scope”
| makemv delim=”, “ “ConsentAction.Permissions”
| eval ConsentAction.Permissions = mvindex(‘ConsentAction.Permissions’,5)
| makemv delim=” “ ConsentAction.Permissions
| makemv delim=” “ “DelegatedPermissionGrant.Scope”
| where isnotnull(‘ConsentAction.Permissions’) OR isnotnull(‘DelegatedPermissionGrant.Scope’)
| eval bad_permission1 = mvfilter(match(‘ConsentAction.Permissions’,”(?i)approleassignment\.readwrite\.all|rolemanagement\.readwrite\.directory”))
| eval bad_permission2 = mvfilter(match(‘DelegatedPermissionGrant.Scope’,”(?i)approleassignment\.readwrite\.all|rolemanagement\.readwrite\.directory”))
| eval bad_permissions = trim(mvdedup(mvappend(bad_permission1,bad_permission2)),”]”)
| eval severity = case(match(bad_permissions,”(?i)rolemanagement\.readwrite\.directory”),”Critical”,match(bad_permissions,”(?i)approleassignment\.readwrite\.all”),”High”)
| fields _time Workload Operation bad_permissions severity
| where isnotnull(bad_permissions)

Some additional fields you might want to include are IP addresses and UserId’s (I’ve left them out on purpose for privacy reasons).

--

--

redhead0ntherun

Cyber Security enthusiast, detection developer and engineer, researcher, consultant.