Using EDR telemetry for offensive research - part 2 - service SDDLs

This article continues on the subject of using EDR telemetry to search for potential security vulnerabilities, the basics of which I have covered in the first part: https://hackingiscool.pl/using-edr-telemetry-for-offensive-research/.  In that part I mentioned that having base events carrying information on file (or any other securable object) permissions would be incredibly useful in finding simple permission-based LPEs. Since that time, I have been occasionally examining telemetry offered by different EDR solutions, looking for base events meeting such criteria, and I have eventually found some in Falcon CrowdStrike. This is by no means an advertisement or an endorsement, I just simply have not found telemetry providing this information in any other product so far, even though I have checked a lot.

In addition to providing another example of practical application of the title’s premise, this article also delves into SDDL (Security Descriptor Definition Language) and its nuances surrounding Windows service permissions.


The CreateService base event

Several months ago, I stumbled upon a new base event collected by Falcon CrowdStrike sensors – CreateService. The event caught my attention because of the interestingly named property – ServiceSecurity (in the screenshot below most irrelevant base event properties were skipped for brevity):

It is best to search for these events by either using the following query in the Advanced Event Search:

#event_simpleName=CreateService ServiceSecurity not empty

or by using the same criteria in the regular event search:

From the start I suspected and expected that property to contain information about service permissions. Or to be more exact and use proper terminology – I hoped it would contain the ACL (Access Control List) set on the service upon its creation.

While this feature is not documented, after some manual analysis and additional digging I was able to eventually confirm that the ascii hex string provided by this property represents the service security descriptor in the format of binary SDDL (Microsoft’s Security Descriptor Definition Language), and can be converted to an SDDL string by utilizing the BinarySDToSDDL helper function.

Decoding and interpreting ServiceSecurity

The basic PowerShell script that does the conversion can be found here: https://github.com/ewilded/Service_SDDLs/blob/main/print_SDDL.ps1.

Converting the example from the screenshot (01000480b0000000bc000000000000001400000002009c0007000000000014008d010200010100000000000504000000000014008d01020001010000000000050600000000001400ff010f0001010000000000051200000000001800ff010f00010200000000000520000000200200000000180014000000010200000000000f02000000010000000000140014000000010100000000000504000000000014001400000001010000000000050b000000010100000000000512000000010100000000000512000000) gives us the following result:

O:SYG:SYD:(A;;CCLCSWLOCRRC;;;IU)(A;;CCLCSWLOCRRC;;;SU)(A;;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;SY)(A;;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;BA)(A;;LCRP;;;AC)(A;;LCRP;;;IU)(A;;LCRP;;;AU)

Let’s break this SDDL down for better understanding:

Owner and group

The first part, marked blue, prefixed with “O:”, defines the owner of the object (thus the “O” prefix), which in this case is NT AUTHORITY\SYSTEM (referred to by “SY”, which is one of the known SDDL-defined aliases for standard users and groups). The second part, marked orange, is very similar as it defines the primary group of the object (thus the “G” prefix), which also happens to be NT AUTHORITY\SYSTEM (“SY”).

DACL

The third part, prefixed with “D:”, marked green, is the DACL (Discretionary Access Control List), and in this case contains 5 ACEs (ACE stands for Access Control Entry), each one in brackets.

Security descriptors can also contain auditing rules. If present in the descriptor, in the SDDL representation their part is prefixed with “S:” and is located following the DACL part. While auditing rules have impact on the forensic footprint, they do not influence the process of access control enforcement, so we’re not interested in them here. Instead, what we want to be examining here are the ACEs from the DACL part. I am only mentioning auditing rules for the sake of thoroughness.

Now, finally, we break down the first ACE from the list in our example: “A;;CCLCSWLOCRRC;;;IU.

The first letter in the SDDL representation of an ACE is either “A” (for Allow) or “D” (for Deny). As the latter are rarely used and rarely needed, while building search criteria we want to focus on ACEs starting with “A”.
The string of letters in the middle – in this case CCLCSWLOCRRC – is the list of permissions defined by the ACE, to which the type of access decision (Allow or Deny) applies. This permission list also consists of predefined two-letter aliases, so here it breaks down into: CC, LC, SW, LO, CR and RC. We will get into the meaning of these in just a second.

Finally, the last part is the trustee (who the ACE applies to). In this particular case we also have two letters (IU), which is another pre-defined SDDL alias for one of standard Windows groups – Interactive Users.  This field can explicitly contain a SID, but in most SDDLs we will see pre-defined aliases of standard well-known groups such as IU, AU (Authenticated Users), BU (Builtin Users), BG (Bultin Guest), WD (World SID, which means Everyone) and so on (https://learn.microsoft.com/en-us/windows/win32/secauthz/sid-strings).

Service permissions

Now back to permissions. The list of pre-defined SDDL permission aliases can be found in Sddl.h (which is also available  here or here), as well as on this MSDN webpage:  https://learn.microsoft.com/en-us/windows/win32/secauthz/ace-strings. It does not explicitly mention how exactly those permissions map to Service Manager access rights, although this subject is to some extent covered here https://learn.microsoft.com/en-us/windows/win32/services/service-security-and-access-rights, as it provides a table with default access rights granted to different trustees upon new service creation. The table translates generic access rights such as GENERIC_WRITE to Service Manager access rights such as STANDARD_RIGHTS_WRITE and SERVICE_CHANGE_CONFIG:

Still, it does not fully cover all the mappings from standard access rights to the service-specific ones.

For example, it does not say that the “WP” (Write Property) permission alias in the context of Service Manager translates to SERVICE_STOP, not to SERVICE_CONFIG_CHANGE. Same goes for “CC” (Create Child), which in the context of Service Manager translates to SERVICE_QUERY_CONFIG. Although there are several searchable resources this information can be found at (e.g. here), I had a hard time finding it on any official Microsoft websites.

Therefore, for the purpose of testing these I created a simple C++ program allowing to specify an arbitrary SDDL while creating a new service for testing purposes. It can be found here: https://github.com/ewilded/Service_SDDLs/blob/main/create_service.cpp.

After creation we can manually confirm the service’s SDDL by running:

sd.exe sdshow TEST

Whereas “TEST” is the service name:

By recreating the service with a very limited ACE for the “IU” group (one permission at a time), and then manually attempting the relevant action from a regular interactive user, I was able to confirm the information found in the unofficial sources.

For example, this is how I confirmed that while “CC” (SERVICE_QUERY_CONFIG) does not allow querying the current service status, “LC” (SERVICE_QUERY_STATUS) is a sufficient permission to do exactly that:

By doing another test, in which I deliberately specified an SDDL explicitly granting Authenticated Users the “LCGW” permission (SERVICE_QUERY_STATUS + GENERIC_WRITE), not only I confirmed that it allowed Authenticated Users to open the service with the SERVICE_CONFIG_CHANGE access right (C++ test POC here: https://github.com/ewilded/Service_SDDLs/blob/main/change_service_config.cpp), but also discovered that an SDDL created this way for a service mapped the requested “LCGW” permission to “KW”:

The “KW” part makes sense as services are represented by registry structures in HKLM\SYSTEM\CurrentControlSet\Services, while “KW” is the corresponding SDDL alias for the KEY_WRITE permission (as per https://learn.microsoft.com/en-us/windows/win32/secauthz/ace-strings):

It was however quite surprising to see the “LC” part from the originally requested ACE disappear. Presumably that takes place due to redundance of both those permissions together.
Either way this proved that “KW” is definitely one of the permissions we want to look for when searching for escalation vectors.

Even more interestingly, by requesting “GW” alone, I ended up with “DCRC” in the final SDDL:

While “DC” in the context of “directory object permissions” translates to “Delete Children”, in the Service Manager context it also translates to SERVICE_CONFIG_CHANGE, including when specified alone:

Therefore “DC” next to “KW” is another string worth looking for in service SDDLs!

Now finally, back to our example from the beginning. The ACE is A;;CCLCSWLOCRRC;;;IU, which means it allows Interactive Users to do CCLCSWLOCRRC. Once we break these permissions down, we get:

CC - SERVICE_QUERY_CONFIG,

LC - SERVICE_QUERY_STATUS,

SW – SERVICE_ENUMERATE_DEPENDENTS,

LO – SERVICE_INTERROGATE,

CR – SERVICE_USER_DEFINED_CONTROL,

RC – READ_CONTROL.

This is a standard set of service permissions granted to regular users and none of them creates a direct attack path such as “GW” (Generic Write effectively giving SERVICE_CONFIG_CHANGE). Although the “CR” (SERVICE_USER_DEFINED_CONTROL) permission does look interesting (https://learn.microsoft.com/en-us/windows/win32/api/winsvc/nf-winsvc-controlservice).

Approach to searching

Since in CrowdStrike telemetry service SDDLs are stored in the form of ascii hex binary SDDL, it’s not possible to directly use string SDDLs or their parts in search criteria. Although individual parts which we are interested in referring to (e.g. known group aliases such as Builtin Users) have their binary representation which we could try to identify in their ascii hex binary SDDL representation and then directly use in criteria, instead of simplifying the process it would add a layer of complexity to it.

Personally, I prefer the following search algorithm:

1.        Extract the full list of all the unique values of the ServiceSecurity property that have been observed in the telemetry we have access to within maximum retention period. The command I use in Advanced Search is: #event_simpleName=CreateService ServiceSecurity not empty | groupBy([ServiceSecurity]).

2.        Export the list and process every single entry individually, using a script. The script inspects every single ACE entry from the entire DACL from every single unique SDDL and reports interesting results – such as regular user groups holding permissions that are normally considered administrative and could potentially be used for escalation.

3.        For any weak SDDLs found in step 2, take their original ascii hex binary SDDL value and use it again to search for individual service creation events to follow up on case-by-case basis.

In step 2 I use the following PowerShell script:

https://github.com/ewilded/Service_SDDLs/blob/main/check_SDDLs.ps1.

It takes a text file with ascii hex binary SDDLs exported from CrowdStrike, converts them into string SDDL representation, parses out all their ACEs and checks each one of them for permissions such as “GW” (Generic Write) granted to known unprivileged groups such as Builtin Users.

Demo:

More results

When analyzing SDDLs, the simple approach is to search for potentially dangerous (administrative) permissions given to specific known non-administrative users/groups.

To get more results we might want to reverse the search policy and instead of explicitly searching for well-known dangerous entries, we can only reject what is explicitly known as normal, typical and safe configurations, and then report on everything else. So instead of searching for the known bad scenarios, we’ll reject the known good ones and inspect everything else.

More groups

Instead of searching for explicit write permissions given to groups such as “IU”, “BU” or “AU”, we might want to focus on dismissing any ACEs with trustees such as “SY” (SYSTEM) and “BA” (Builtin Administrators), and reporting on any permissive ACEs with trustees other than these two.

More permissions                                                           

Likewise with groups, instead of searching for ACEs granting known dangerous permissions such as “WO” (Write Owner), we can dismiss the ones granting known safe permissions such as CC, LC, CR etc. and only report on the ones outside this list. This will not only allow us to find known dangerous permissions, but also any other we did not expect and might want to learn more about.

Checking owner

Although this is an unlikely scenario to encounter, we could even encounter a service with the owner property pointing to an identity other than NT AUTHORITY\SYSTEM. In such case that different authority would have the right to change the security descriptor (WriteDacl) and therefore add themselves a permission to reconfigure the service if not present already. So, if we are processing SDDLs for weak spots, it is also worth to add this extra check and report service SDDLs with owner other than NT AUTHORITY\SYSTEM.

More events

As it turns out, CreateService is not the only base event provided by CrowdStrike which has the ServiceSecurity property populated with the service’s SDDL. Another equally good is ModifyServiceBinary, so it is best to use base events collected by both when obtaining the full list of unique SDDLs observed in our environment.

To inspect the full list of base event names that carry a specific property – in this case ServiceSecurity – we can run a search such as: #event_simpleName=* ServiceSecurity not empty | groupBy([#event_simpleName]).

It is also worthwhile to review the full list of all the base events that are currently available, to then manually inspect any new ones for interesting properties. Which can be done with a similar query: #event_simpleName=*| groupBy([#event_simpleName]).

Conclusion

This article provides another viable example of utilizing EDR telemetry to discover security weaknesses and demonstrates that from time-to-time it is worthwhile to check what information is currently covered and provided by those solutions at our disposal. It would be great to see similar base events for other securable objects and in more EDR solutions.
Happy hunting!

No one really cares about cookies and neither do I