Detection rules › Kusto
Shadow Credentials Added to Account (Alternative)
This query searches for modifications to the 'msDS-KeyCredentialLink' property in Active Directory. There are two different events which contain information to detect such changes: 5136 and 4662. This detection uses the 4662, which is an alternative if 5136 is not available.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Persistence | T1098 Account Manipulation |
| Privilege Escalation | T1098 Account Manipulation, T1484 Domain or Tenant Policy Modification |
References
- https://docs.microsoft.com/en-us/defender-for-identity/configure-windows-event-collection
- https://docs.microsoft.com/en-us/windows/security/threat-protection/auditing/event-4662
- https://posts.specterops.io/shadow-credentials-abusing-key-trust-account-mapping-for-takeover-8ee1a53566ab
- https://github.com/ShutdownRepo/pywhisker
- https://github.com/MichaelGrafnetter/DSInternals
Event coverage
| Provider | Event | Title |
|---|---|---|
| Security-Auditing | Event ID 4662 | An operation was performed on an object. |
Rule body kusto
let timeframe = 2*1h;
let RuleId = "0297";
let DedupFields = dynamic(["TimeGenerated"]);
SecurityEvent
| where ingestion_time() >= ago(timeframe)
| where EventID == 4662
| where Properties has "5b47d60f-6090-40b2-9f37-2a4de88f3063" // msDS-KeyCredentialLink.
| where Properties has "%%7685" or (binary_and(toint(AccessMask), 0x10) == 0x10) // "Write Property" or "Write Extended Attributes": https://gist.github.com/brianreitz/d5b9397a2e8b3d52ceb9359897e07c3f
| extend HostName=tostring(split(Computer,".")[0]),DnsDomain=iif(Computer contains ".", substring(Computer, indexof(Computer, ".") + 1, strlen(Computer)),"")
| 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*1h;
let RuleId = "0297";
let DedupFields = dynamic(["TimeGenerated"]);
Stage 1: source
SecurityEvent
Stage 2: where
| where ingestion_time() >= ago(timeframe)
Stage 3: where
| where EventID == 4662
Stage 4: where
| where Properties has "5b47d60f-6090-40b2-9f37-2a4de88f3063"
Stage 5: where
| where Properties has "%%7685" or (binary_and(toint(AccessMask), 0x10) == 0x10)
Stage 6: extend (3 consecutive steps)
| extend HostName=tostring(split(Computer,".")[0]),DnsDomain=iif(Computer contains ".", substring(Computer, indexof(Computer, ".") + 1, strlen(Computer)),"")
| extend AccountName=iif(Account contains @"\",tostring(split(Account,@"\")[1]),Account),AccountDomain=iif(Account contains @"\",tostring(split(Account,@"\")[0]),"")
| extend DedupFieldValues=pack_all()
Stage 7: 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 8: extend
| extend DedupEntity=strcat_array(DedupValues, "|")
Stage 9: project-away
| project-away DedupFieldValues, DedupValues
Stage 10: 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 11: summarize
summarize
Exclusions
Top-level NOT(...) conjuncts: predicates this rule actively suppresses.
| Field | Kind | Excluded values |
|---|---|---|
AlertName | match | 0297 |
ProviderName | match | ASI |
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.
| Field | Kind | Values |
|---|---|---|
AccessMask | eq |
|
EventID | eq |
|
Properties | match |
|