Detection rules › Kusto

Excessive share permissions

Status
available
Severity
medium
Time window
1h
Group by
EventData, SourceComputerId, TimeGenerated, allowed_principal, identifier
Source
github.com/Azure/Azure-Sentinel

The query searches for event 5143, which is triggered when a share is created or changed and includes de share permissions. First it checks to see if this is a whitelisted share for the system (e.g. domaincontroller netlogon, printserver print$ etc.). The share permissions are then checked against 'allow' rule (A) for a number of well known overly permissive groups, like all users, guests, authenticated users etc. If these are found, an alert is raised so the share creation may be audited. Note: this rule only checks for changed permissions, to prevent repeat alerts if for example a comment is changed, but the permissions are not altered.

MITRE ATT&CK coverage

Event coverage

Rule body kusto

id: aba0b08c-aace-40c5-a21d-39153023dcaa
name: Excessive share permissions
description: |
  The query searches for event 5143, which is triggered when a share is created or changed and includes de share permissions.
  First it checks to see if this is a whitelisted share for the system (e.g. domaincontroller netlogon, printserver print$ etc.).
  The share permissions are then checked against 'allow' rule (A) for a number of well known overly permissive groups, like all users, guests, authenticated users etc.
  If these are found, an alert is raised so the share creation may be audited.
  Note: this rule only checks for changed permissions, to prevent repeat alerts if for example a comment is changed, but the permissions are not altered.
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: SecurityEvents
    dataTypes:
      - SecurityEvent
  - connectorId: WindowsSecurityEvents
    dataTypes:
      - SecurityEvent
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Collection
  - Discovery
relevantTechniques:
  - T1039
  - T1135
query: |
  let timeframe=1h;
  let system_roles = datatable(role:string, system:string)                  // Link roles to systems.
    ["DC","dc1.corp.local",
    "DC","dc2.corp.local",
    "PRINT","printer.corp.local"
    ];
  let share_roles = datatable(role:string, share:string)                    // Link roles to shares.
    ["DC", @"\\*\sysvol",
    "DC",@"\\*\netlogon",
    "PRINT",@"\\*\print$"];
  let allowed_system_shares = system_roles                                  // Link systems to shares.
    | 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;
  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.
    ];
  SecurityEvent
  | where TimeGenerated >= ago(timeframe)
  | where EventID == 5143
  | extend EventXML = parse_xml(EventData)
  | extend OldSD = tostring(EventXML["EventData"]["Data"][13]["#text"])     // Grab the previous Security Descriptor.
  | extend NewSD = tostring(EventXML["EventData"]["Data"][14]["#text"])     // Grab the new Security Descriptor.
  | project-away EventXML
  | where tostring(OldSD) !~ tostring(NewSD)                                // Don't bother with unchanged permissions.
  | extend system = tolower(Computer), share=tolower(ShareName)             // Normalize system and share name for matching with whitelist.
  | join kind=leftouter allowed_system_shares on system                     // Retrieve the allowed shares per system.
  | where not(set_has_element(allowed_shares, share))                       // Check if the current share is an allowed share.
  | project-away system, share, allowed_shares                              // Get rid of temporary fields.
  | extend DACLS = extract_all(@"(D:(?:\((?:[\w\-]*;){5}(?:[\w\-]*)\))*)", tostring(NewSD)) // Grab all instances of D:(DACL), in case there are multiple sets.
  | project-away OldSD, NewSD                                               // Get rid of data we no longer need.
  | mv-expand DACLS to typeof(string)                                       // In case there are any duplicate/subsequent D: entries (e.g., D:<dacls>S:<sacls>D:<dacls>) split them out to individual D: sets.
  | extend DACLS = substring(DACLS,2)                                       // Strip the leading D:.
  | extend DACLS = split(DACLS, ")")                                        // Split the sets of DACLS ()() to an array of individual DACLS (). This removes the trailing ) character.
  | mv-expand DACLS to typeof(string)                                       // Duplicate the records in such a way that only 1 DACL per record exist. We will aggregate them back later.
  | extend DACLS = substring(DACLS, 1)                                      // Also remove the leading ( character.
  | where not(isempty(DACLS)) and DACLS startswith "A;"                     // Remove any empty or non-allow DACLs.
  | extend allowed_principal = tostring(split(DACLS,";",5)[0])              // Grab the SID what is affected by this DACL.
  | extend allowed_principal = iff(not(allowed_principal startswith "S-" and string_size(allowed_principal) > 15), allowed_principal, split(allowed_principal,"-",countof(allowed_principal,"-"))[0]) // This line takes only the last part (e.g., 513) of a long SID, so you can refer to groups/users without needing to supply the full SID above.
  | join kind=inner monitored_principals on $left.allowed_principal == $right.identifier // Join the found groups to the table of groups to be monitored above. Adds the more readable 'group_name).
  | project-away allowed_principal, identifier, DACLS
  | summarize Authorized_Public_Principals = make_set(Group_Name), take_any(*) by TimeGenerated, SourceComputerId, EventData // Summarize the fields back, making a set of the various group_name values for this record.
  | project-away Group_Name
  // Begin client-specific filter.
  // End client-specific filter.
entityMappings:
  - entityType: Host
    fieldMappings:
      - identifier: FullName
        columnName: Computer
version: 1.0.1
kind: Scheduled

Stages and Predicates

Parameters

let timeframe = 1h;

Let binding: system_roles

let system_roles = datatable(role:string, system:string)
  ["DC","dc1.corp.local",
  "DC","dc2.corp.local",
  "PRINT","printer.corp.local"
  ];

Let binding: share_roles

let share_roles = datatable(role:string, share:string)
  ["DC", @"\\*\sysvol",
  "DC",@"\\*\netlogon",
  "PRINT",@"\\*\print$"];

Let binding: allowed_system_shares

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;

Derived from system_roles, share_roles.

Let binding: monitored_principals

let monitored_principals = datatable(identifier:string, Group_Name:string)
  ["AN", "Anonymous Logon",
  "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",
  "514", "Domain Guests",
  "545", "Builtin Users",
  "546", "Builtin Guests",
  "S-1-5-7", "Anonymous Logon"
  ];

Stage 1: source

SecurityEvent

Stage 2: where

| where TimeGenerated >= ago(timeframe)

Stage 3: where

| where EventID == 5143

Stage 4: extend (3 consecutive steps)

| extend EventXML = parse_xml(EventData)
| extend OldSD = tostring(EventXML["EventData"]["Data"][13]["#text"])
| extend NewSD = tostring(EventXML["EventData"]["Data"][14]["#text"])

Stage 5: project-away

| project-away EventXML

Stage 6: where

| where tostring(OldSD) !~ tostring(NewSD)

Stage 7: extend

| extend system = tolower(Computer), share=tolower(ShareName)

Stage 8: join

| join kind=leftouter allowed_system_shares on system

Stage 9: where

| where not(set_has_element(allowed_shares, share))

Stage 10: project-away

| project-away system, share, allowed_shares

Stage 11: extend

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

Stage 12: project-away

| project-away OldSD, NewSD

Stage 13: mv-expand

| mv-expand DACLS to typeof(string)

Stage 14: extend

| extend DACLS = substring(DACLS,2)

Stage 15: extend

| extend DACLS = split(DACLS, ")")

Stage 16: mv-expand

| mv-expand DACLS to typeof(string)

Stage 17: extend

| extend DACLS = substring(DACLS, 1)

Stage 18: where

| where not(isempty(DACLS)) and DACLS startswith "A;"

Stage 19: extend

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

Stage 20: extend

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

Stage 21: join

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

Stage 22: project-away

| project-away allowed_principal, identifier, DACLS

Stage 23: summarize

| summarize Authorized_Public_Principals = make_set(Group_Name), take_any(*) by TimeGenerated, SourceComputerId, EventData

Stage 24: project-away

| project-away Group_Name

Exclusions

Top-level NOT(...) conjuncts: predicates this rule actively suppresses.

FieldKindExcluded values
shareeqallowed_shares
DACLSis_null(no value, null check)

Indicators

Each row is a field, operator, and value that the rule matches. The corpus column counts how many other rules in the catalog look for the same combination: high numbers point to widely-used, community-vetted indicators. Blank or 1 shows that the indicator is specific to this rule.

FieldKindValues
DACLSstarts_with
  • A;
EventIDeq
  • 5143 transforms: cased
OldSDne
  • NewSD transforms: tostring

Output fields

Fields the rule emits when it matches. Chronicle authors list these in the outcome block; they appear on the detection and $risk_score drives alerting. Sentinel / Defender XDR rules build them up through project / summarize / extend stages. Sentinel maps these into alert fields via entityMappings and customDetails; Defender XDR custom detections surface them as alert fields directly.

FieldSource
Authorized_Public_Principalssummarize
EventDatasummarize
SourceComputerIdsummarize
TimeGeneratedsummarize