Detecting Azure API Permissions Abuse
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:
- The two permissions that can be abused are: RoleManagement.ReadWrite.Directory & AppRoleAssignment.ReadWrite.All. Detailed information can be found in Microsoft documentation — HERE
- These permissions allow an object (whether its a Service Principal, Application, or IAM user) to basically take over a tenant (Global Admin)
- 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
- You’re forwarding your M365/Azure logs to your SIEM
- Your SIEM is Splunk
- 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).