Detection rules › Kusto

Large number of AD objects accessed by user

Group by
Account, DedupEntity
Author
FalconForce
Source
github.com/FalconForceTeam/FalconFriday

This query detects a user accessing a large number of Group and User objects from Active Directory which is outside the baseline of normal behavior for that particular user.

Known false positives

  • There are legitimate use-cases for downloading large number of AD objects. For example, an administrator might run a script to extract all users as part of certain migration activities.

MITRE ATT&CK coverage

References

Telemetry coverage

Rule body

let timeframe = 2*1d;
let RuleId = "0108";
let DedupFields = dynamic(["TimeGenerated"]);
let lookback_days=14d; // Look back this many days to calculate baseline for maximum number of objects accessed per day.
let min_suspicious_count = 20000; // Only consider users looking up at least this many objects during the timeframe.
let suspicious_factor = 50; // Consider as suspicious if requesting more than suspicious_factor*daily maximum in lookback period.
let ADObjectAccess=(
    SecurityEvent
    | where EventID == 4662
    | where not(Account endswith "$")
    | where ObjectType in~ (
        "%{bf967aba-0de6-11d0-a285-00aa003049e2}", // User.
        "%{bf967a9c-0de6-11d0-a285-00aa003049e2}" //  Group.
    )
);
let AccessBaseline=(
    ADObjectAccess
    | where TimeGenerated <= ago(timeframe)
    | where TimeGenerated >= ago(timeframe + lookback_days)
    | summarize BaselineObjectCount=count() by Account, bin(TimeGenerated,1d)
    | summarize PreviousMaxObjectPerDay=max(BaselineObjectCount) by Account
);
ADObjectAccess
| where ingestion_time() >= ago(timeframe)
| summarize ObjectCount=count(),TimeGenerated=min(TimeGenerated) by Account
| where ObjectCount > min_suspicious_count
| join kind=leftouter AccessBaseline on Account
| extend PreviousMaxObjectPerDay=coalesce(PreviousMaxObjectPerDay,0)
// Calculate what is considered a suspicious number for the given user.
| extend SuspiciousThreshold=max_of(PreviousMaxObjectPerDay*suspicious_factor,min_suspicious_count)
| where ObjectCount > SuspiciousThreshold
| project TimeGenerated, Account, ObjectCount, PreviousMaxObjectPerDay, SuspiciousThreshold
| extend AccountName=iif(Account contains @"\",tostring(split(Account,@"\")[1]),Account),AccountDomain=iif(Account contains @"\",tostring(split(Account,@"\")[0]),"")
// Begin environment-specific filter.
// End environment-specific filter.
// Begin de-duplication logic.
| extend DedupFieldValues=pack_all()
| mv-apply e=DedupFields to typeof(string) on (
    extend DedupValue=DedupFieldValues[tostring(e)]
    | order by e // Sorting is required to ensure make_list is deterministic.
    | summarize DedupValues=make_list(DedupValue)
)
| extend DedupEntity=strcat_array(DedupValues, "|")
| project-away DedupFieldValues, DedupValues
| join kind=leftanti (
    SecurityAlert
    | where AlertName has RuleId and ProviderName has "ASI"
    | where TimeGenerated >= ago(timeframe)
    | extend DedupEntity = tostring(parse_json(tostring(parse_json(ExtendedProperties)["Custom Details"])).DedupEntity[0])
    | project DedupEntity
) on DedupEntity
// End de-duplication logic.

Stages and Predicates

Parameters

let timeframe = 2*1d;
let RuleId = "0108";
let DedupFields = dynamic(["TimeGenerated"]);
let lookback_days = 14d;
let min_suspicious_count = 20000;
let suspicious_factor = 50;

let ADObjectAccess is inlined into the numbered stages below.

Let binding: AccessBaseline used in Stages 2, 10

let AccessBaseline = (
    ADObjectAccess
    | where TimeGenerated <= ago(timeframe)
    | where TimeGenerated >= ago(timeframe + lookback_days)
    | summarize BaselineObjectCount=count() by Account, bin(TimeGenerated,1d)
    | summarize PreviousMaxObjectPerDay=max(BaselineObjectCount) by Account
);

Stages 1 to 6 define let ADObjectAccess (the rule's main pipeline source); stages 7 to 20 run on it.

Stage 1: source

let ADObjectAccess

Stage 2: source

let AccessBaseline

Stage 3: source

SecurityEvent

Stage 4: where

| where EventID == 4662

Stage 5: where

| where not(Account endswith "$")

Stage 6: where

| where ObjectType in~ (
        "%{bf967aba-0de6-11d0-a285-00aa003049e2}",
        "%{bf967a9c-0de6-11d0-a285-00aa003049e2}"
    )

Stage 7: where

ADObjectAccess
| where ingestion_time() >= ago(timeframe)

Stage 8: summarize

| summarize ObjectCount=count(),TimeGenerated=min(TimeGenerated) by Account

Stage 9: where

| where ObjectCount > min_suspicious_count

Stage 10: join

| join kind=leftouter AccessBaseline on Account

Stage 11: extend

| extend PreviousMaxObjectPerDay=coalesce(PreviousMaxObjectPerDay,0)

Stage 12: extend

| extend SuspiciousThreshold=max_of(PreviousMaxObjectPerDay*suspicious_factor,min_suspicious_count)

Stage 13: where

| where ObjectCount > SuspiciousThreshold

Stage 14: project

| project TimeGenerated, Account, ObjectCount, PreviousMaxObjectPerDay, SuspiciousThreshold

Stage 15: extend

| extend AccountName=iif(Account contains @"\",tostring(split(Account,@"\")[1]),Account),AccountDomain=iif(Account contains @"\",tostring(split(Account,@"\")[0]),"")

Stage 16: extend

| extend DedupFieldValues=pack_all()

Stage 17: kusto:mv-apply

| mv-apply e=DedupFields to typeof(string) on (
    extend DedupValue=DedupFieldValues[tostring(e)]
    | order by e
    | summarize DedupValues=make_list(DedupValue)
)

Stage 18: extend

| extend DedupEntity=strcat_array(DedupValues, "|")

Stage 19: project-away

| project-away DedupFieldValues, DedupValues

Stage 20: join (negated)

| join kind=leftanti (
    SecurityAlert
    | where AlertName has RuleId and ProviderName has "ASI"
    | where TimeGenerated >= ago(timeframe)
    | extend DedupEntity = tostring(parse_json(tostring(parse_json(ExtendedProperties)["Custom Details"])).DedupEntity[0])
    | project DedupEntity
) on DedupEntity

Exclusions

The rule actively suppresses these predicates.

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
EventIDeq
  • 4662 corpus 15 (splunk 7, kusto 4, elastic 3, chronicle 1)
field:"EventID" kind:eq value:"4662"
ObjectCountcross_field_compare
  • SuspiciousThreshold transforms: op:gt
field:"ObjectCount" kind:cross_field_compare value:"SuspiciousThreshold"
ObjectCountgt
  • 20000
field:"ObjectCount" kind:gt value:"20000"
ObjectTypein
  • %{bf967a9c-0de6-11d0-a285-00aa003049e2}
  • %{bf967aba-0de6-11d0-a285-00aa003049e2}
field:"ObjectType" kind:in

Output fields

These fields are emitted when the rule matches.

FieldSource
Accountproject
ObjectCountproject
PreviousMaxObjectPerDayproject
SuspiciousThresholdproject
TimeGeneratedproject
AccountDomainextend
AccountNameextend
DedupEntityextend