FalconFriday —Monitoring for public shares — 0xFF1A

In this blog we will explore the possibilities to use Microsoft Sentinel to monitor a Windows environment for the creation of public SMB shares (i.e., accessible to the entire domain including users, service accounts and computer accounts). The objective is to try preventing accidental information leakage, vulnerabilities or other abuse through inappropriate share permissions.

Introduction

During our red teaming projects we often find sensitive data on shares that are open to a large audience in the organization. For example, they may be shared with the built-in group ‘Everyone’, which includes all ‘Authenticated Users’ (i.e., everyone and everything that is authenticated to the Windows domain); basically exposing the information in the share to the entire organization.

Shares that are accessible to all users should be created with great care, as everyone can use them to retrieve data from and possibly write to them. However, everyone with administrative privileges on a system can create a public share with only a few mouse-clicks, potentially compromising home folders, application folders, backup folders, etc. Also, administrators may not realize that an existing share is shared publicly and may share sensitive data or scripts and leave them for an adversary to discover and abuse. See for example my previous blog. Last, but not least, adversaries may create publicly accessible shares to stage malware, data they want to exfiltrate or even use public shares as internal Command and Control (C2) channels.

By detecting the creation of public shares, the security team can intervene and discuss proper permissions for shares based on the expected contents. Also, they can set up and communicate proper procedures around actual public shares and discuss what is or is not allowed on such shares.


Required Audit Policy

Share creation is not logged by default on Windows operating systems. We will first need to enable the advanced security audit policy ‘Audit File Shares’[1].

This policy will instruct your system to log a number of events, including file interactions on shares (event ID 5140). Depending on share usage, this policy can generate a large number of events.

If this policy is enabled, creating a share generates the following two events:

  • 5142: a network share object was added.
  • 5143: a network share object was modified.

Event 5143 describes the permissions of the created share as a discretionary access control list (DACL) and is also logged when share permissions are modified. This is the event of interest for us [2].


Analyzing event 5143

To gather some data to work with, you can create a share that is shared with ‘Everyone’ and look at the generated event to see how we can use this event to detect the share is publicly accessible.

Creating a share which is accessible to ‘Everyone’ to see how this is reflected in the eventlog.

Having created the share, we can look at the generated events to see how our permissions are reflected and how we might use this information to detect the creation of public shares.

When looking at the generated event 5143 we can see a number of details. For the purpose of this blog we are interested in the field ‘New SD’ which contains the new security descriptor for this share.

New SD contains the following items:

  • O: the SID of the owner of the share (in this case RID 500, the built-in Administrator account).
  • G: the primary group of the owner at the time of share creation (in this case RID 513, i.e., Domain Users).
  • D: configured DACLs, which set the permissions for a share.
  • S: configured system access control lists (SACLs) which tell the system what kind of access should be logged.

For this blog we will further investigate the DACL field.


Dissecting the DACL

In this example we see two DACLs. The DACL contains the following fields (in order). [2]

  • Ace_type (e.g., A for Allow, D for Deny).
  • Inheritance flags.
  • Relevant permission as a hexadecimal string, or a reserved value (e.g., FA for FILE ALL ACCESS).
  • N/A (object_guid).
  • N/A (inherit_object_guid).
  • Account_sid, a specific security principal (SID) or a reserved value like BA (Built-in Administrators group), WD (Everyone).

Note that the way the permissions are reflected may not be as consistent as you would hope between different ways to create shares (e.g., ‘Share’ and ‘Advanced Sharing’). Therefore, we will define any publicly granted Allow (A) permissions as offending.

Looking at the Microsoft documentation [2][3] we can compile a list of interesting built-in principals which would classify as public access, when used to provide access to a share.

  • Anonymous Logon.
  • Authenticated Users.
  • Built-in guests.
  • Built-in users.
  • Domain guests.
  • Domain users.
  • Everyone.
  • Local Guest.
  • Network logon users.

Using this information, we can start building a detection rule.


The detection

When a system is on-boarded in Sentinel, the security event logs are shipped and can be analyzed for the creation of public shares. We can start with an easy query to find the event in Sentinel and figure out how it is represented in Sentinel.

SecurityEvent
| where EventID == 5143
| where Computer == "<your_computer_name>"

Running this query should show the relevant results.

We can see that the data holding the NewSD value is stored in an XML formatted field called ‘EventData’. We first need to parse this XML so we can more easily extract the required data. To this end, add the following line to your query:

| extend EventXML = parse_xml(EventData)

Run the query again to see how the data is parsed and what fields we need.

We can see that Sentinel now better understands our structure and that we can start querying it. For now, we focus on data-field 14, subfield ‘#text’ because it contains the New Security Descriptor, detailing the applied permissions.

Add the following line to your query to extract the NewSD field data:

| extend NewSD = EventXML["EventData"]["Data"][14]["#text"]

We can now search the extracted NewSD field for offending DACLs. For example, by using the following regular expression:

| extend DACLS = extract_all(@"(D:(?:\((?:[\w\-]*;){5}(?:[\w\-]*)\))*)", tostring(NewSD))

This regular expression will search all DACL sets. It also covers the case where the eventlog data includes multiple DACL sets (e.g., D:(DACL)(DACL)S:(SACL)D:(DACL)).

To analyze per DACL which groups were added, we can now expand these into multiple records, so we are guaranteed to have one DACL set per record D:(DACL)(DACL).

| mv-expand DACLS to typeof(string)

Now we need to remove the leading D: so we are left with just the DACL sets per records in (DACL)(DACL) and split the sets into individual DACLs per record:

| extend DACLS = substring(DACLS,2) //strip the leading D:
| extend DACLS = split(DACLS, “)”) //this will strip trailing )
| mv-expand DACLS

We have split the DACLS based on the closing parenthesis ). This means we are left with dangling opening parenthesis (. We do not need these, so strip these and remove empty records or records which do not describe an allow DACL (A;):

| extend DACLS = substring(DACLS, 1) //remove the leading ( 
| where not(isempty(DACLS)) and DACLS startswith "A;"

Now we are left with one allow DACL per record.

Example of the data we have now isolated.

We are interested in the last part, which identifies the group or user that was granted access. This happens to be field index 5, when split by semicolon:

| extend allowed_principal = tostring(split(DACLS,";",5)[0])

This allowed principal can either be a shorthand notation [2] (e.g., AN for Anonymous, BA for built-in Administrators or WD for Everyone) or a SID [3].

The SID can be one of the well-known principals (e.g., S-1–5–7 for Anonymous) or a domain SID (e.g., S-1–5–21-<domain>-513 for domain users).

The SID for anonymous login is always the same, while the SID for domain users is depending on the <domain> part, which uniquely identifies your domain. I have attempted to optimize the maintainability, and extract the last part of domain SIDs (e.g., 513 for domain users) while allowing you to also use the short universal SIDs (e.g., S-1–5–7 of AN for Anonymous) using the following contraption:

| extend allowed_principal = iff(not(allowed_principal startswith "S-" and string_size(allowed_principal) > 15), allowed_principal, split(allowed_principal,"-",countof(allowed_principal,"-"))[0])

Now, by normalizing the data we can create a data-table which holds the groups we want to look out for and define this at the top of our query:

let monitored_principals=datatable(identifier:string, Group_Name:string) // Define a data-table with groups to monitor.
["AN", "Anonymous Logon", // We accept the 'alias' for these well-known SIDS
"AU", "Authenticated Users",
"BG","Built-in guests",
"BU","Built-in users",
"DG","Domain guests",
"DU","Domain users",
"WD","Everyone",
"IU","Interactively Logged-on users",
"LG","Local Guest",
"NU","Network logon users",
"513", "Domain Users", // Support matching on the last part of a SID
"514", "Domain Guests",
"545", "Builtin Users",
"546", "Builtin Guests",
"S-1-5-7", "Anonymous Logon" // For the global SIDS, we accept them as-is
];

Because we have split out all the individual DACLs before, we can now easily filter and enrich them by joining them against this data table. This way, we can present readable names to our analysts, rather than shorthands or SIDs which need to be further decoded:

| join kind=inner monitored_principals on $left.allowed_principal == $right.identifier

Now that we have the data we need, we can start aggregating the DACL records back to a single event record and throw away some of the intermediary data we don’t need anymore:

| project-away allowed_principal, identifier, DACLS
| summarize Authorized_Public_Principals= make_set(Group_Name), take_any(*) by TimeGenerated, SourceComputerId, EventData
| project-away Group_Name

We now have a detection rule that can monitor a set of easily configurable principals and yields human readable results for your SOC.

Our detected records now show a human-readable form of the public groups that are authorized for this share.

You can easily replace the hard coded data-table with a blob or data-file so it can easily be modified without touching the rule. For example, through Sentinel watch lists[4].

If you have been playing along and the correct audit policy is already enabled, you can now start using the rule to see if anybody has been creating public shares. For the complete rule, keep on reading or head straight over to our GitHub.


The tuning

Once you are broadly enabling the ‘Audit File Share’ policy, you will find you are getting a number of false positives. By tuning the rule you can limit these false positives.

For example, when any change to a share is made, a ‘change’ event is triggered. This may be useful when you are not yet fully in control, but can also generate a lot of repeated events. If you decide you only want to receive alerts from shares with changed permissions, you can compare the value of OldSD against NewSD to see if it has changed. For example, by adding the the following:

| extend EventXML = parse_xml(EventData)
| extend OldSD = EventXML["EventData"]["Data"][13]["#text"]
| extend NewSD = EventXML["EventData"]["Data"][14]["#text"]
| project-away EventXML
| where tostring(OldSD) !~ tostring(NewSD)

Furthermore, you will find that a number of system may generate change events for legitimate public shares they mange. For example:

  • Domain controllers managing the SYSVOL and NETLOGON shares.
  • SCCM managing shares for package deployments.
  • DFS Hosts managing DFS shares.
  • Print servers exposing print$ shares for people to download drivers from.

I would suggest to not blindly whitelist such share-names. This would make it easier for adversaries to use, for example, a share named NETLOGON or print$ on an arbitrary system to bypass your detection. Instead, I would recommend you to create a mapping where you link allowed public shares to specific systems. Again, these data-tables could be taken out of the query using, for example, watchlists:

let system_roles = datatable(role:string, system:string)
["DC","dc1.corp.local",
"DC","dc2.coprp.local",
"PRINT","printer.corp.local"];
let share_roles = datatable(role:string, share:string)
["DC", @"\\*\sysvol",
"DC",@"\\*\netlogon",
"PRINT",@"\\*\print$"];

To make it easier to use, we will condense this into a single record per system, which lists all the ‘allowed’ shares for this system. This also allows us to place systems in multiple roles without the need to define overlapping roles. For example, if a system is a DC and print-server at the same time:

let allowed_system_shares = system_roles
| join kind=inner share_roles on role
| extend system = tolower(system), share = tolower(share)
| project-away role
| summarize allowed_shares = make_set(share) by system;

Now, we can join each share-event to this table, and figure out if the configured share is in the list of allowed shares for ‘public’ sharing and remove the temporary data-fields from the query:

| extend system = tolower(Computer), share=tolower(ShareName)
| join kind=leftouter allowed_system_shares on system
| where not(set_has_element(allowed_shares, share))
| project-away system, share, allowed_shares

A completed query, including the discussed tuning options can be found in our GitHub


Closing considerations

This detection will only trigger when permissions are set for new shares or changed on existing shares. Any public shares already in your network will not pop up, unless their permissions are altered. So you probably want to have a look around your network to see if any public shares already exist.

Unfortunately, the detection will not stop your users and administrators from storing sensitive information on existing public shares, even if those public shares are known and allowed. Keep an eye out on what data is stored where (this is easier if you have a clear overview of the public shares in your network) and talk to your users and administrators on what sensible locations are for sensitive data, be it business data or automation scripts.

If you have made it this far, thank you for reading. If you have any questions or comments feel free to reach out on Twitter @0xFFJP.

This rule is also published on our GitHub.


Knowledge center

Other articles

FalconHound, attack path management for blue teams

FalconHound, attack path management for blue teams

[dsm_breadcrumbs show_home_icon="off" separator_icon="K||divi||400" admin_label="Supreme Breadcrumbs" _builder_version="4.18.0" _module_preset="default" items_font="||||||||" items_text_color="rgba(255,255,255,0.6)" custom_css_main_element="color:...

Together. Secure. Today.

Stay in the loop and sign up to our newsletter

FalconForce realizes ambitions by working closely with its customers in a methodical manner, improving their security in the digital domain.

Energieweg 3
3542 DZ Utrecht
The Netherlands

FalconForce B.V.
[email protected]
(+31) 85 044 93 34

KVK 76682307
BTW NL860745314B01