Detection rules › Kusto

UnPAC the hash

Author
FalconForce
Source
github.com/FalconForceTeam/FalconFriday

This query looks for an attack that allows an attacker with a valid TGT token for a certain account, to obtain the NTLM hash for that account. Such an account may either be a user account or a machine account. The TGT can, for example, be obtained by authenticating with a certificate instead of with username and password.

MITRE ATT&CK coverage

References

Event coverage

Rule body kusto

let timeframe = 2*1h;
let RuleId = "0299";
let DedupFields = dynamic(["TimeGenerated"]);
let forwardable = binary_shift_left(1, 30);
let renewable = binary_shift_left(1, 23);
let renewable_ok = binary_shift_left(1, 4);
let enctik = binary_shift_left(1, 3);
let krbflags = binary_or(forwardable, binary_or(renewable, binary_or(renewable_ok, binary_or(enctik, 0))));
SecurityEvent
| where ingestion_time() >= ago(timeframe)
| where EventID == 4769
| extend ticketOptions = toint(extract("<Data Name=\"TicketOptions\">(.*?)</Data>", 1, EventData))
| extend TargetUserName = extract("<Data Name=\"TargetUserName\">(.*?)@.*?</Data>", 1, EventData)
| extend TargetDomainName = extract("<Data Name=\"TargetDomainName\">(.*?)</Data>", 1, EventData)
| extend ServiceName = extract("<Data Name=\"ServiceName\">(.*?)</Data>", 1, EventData)
| where ServiceName =~ TargetUserName // Requirement for getting the NT hash with U2U. This makes the KDC encrypt the NT hash with the key in the TGT.
| where binary_and(ticketOptions, krbflags) == krbflags
| extend HostName=tostring(split(Computer,".")[0]),DnsDomain=iif(Computer contains ".", substring(Computer, indexof(Computer, ".") + 1, strlen(Computer)),"")
// 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*1h;
let RuleId = "0299";
let DedupFields = dynamic(["TimeGenerated"]);
let forwardable = binary_shift_left(1, 30);
let renewable = binary_shift_left(1, 23);
let renewable_ok = binary_shift_left(1, 4);
let enctik = binary_shift_left(1, 3);
let krbflags = binary_or(forwardable, binary_or(renewable, binary_or(renewable_ok, binary_or(enctik, 0))));

Stage 1: source

SecurityEvent

Stage 2: where

| where ingestion_time() >= ago(timeframe)

Stage 3: where

| where EventID == 4769

Stage 4: extend (4 consecutive steps)

| extend ticketOptions = toint(extract("<Data Name=\"TicketOptions\">(.*?)</Data>", 1, EventData))
| extend TargetUserName = extract("<Data Name=\"TargetUserName\">(.*?)@.*?</Data>", 1, EventData)
| extend TargetDomainName = extract("<Data Name=\"TargetDomainName\">(.*?)</Data>", 1, EventData)
| extend ServiceName = extract("<Data Name=\"ServiceName\">(.*?)</Data>", 1, EventData)

Stage 5: where

| where ServiceName =~ TargetUserName

Stage 6: where

| where binary_and(ticketOptions, krbflags) == krbflags

Stage 7: extend

| extend HostName=tostring(split(Computer,".")[0]),DnsDomain=iif(Computer contains ".", substring(Computer, indexof(Computer, ".") + 1, strlen(Computer)),"")

Stage 8: extend

| extend DedupFieldValues=pack_all()

Stage 9: 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 10: extend

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

Stage 11: project-away

| project-away DedupFieldValues, DedupValues

Stage 12: 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

Stage 13: summarize

summarize

Exclusions

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

FieldKindExcluded values
AlertNamematch0299
ProviderNamematchASI

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
EventIDeq
  • 4769 transforms: cased corpus 10 (splunk 6, kusto 4)
ServiceNameeq
  • TargetUserName
ticketOptionseq
  • krbflags transforms: binary_and