Detection rules › Kusto

Service Principal Name (SPN) Assigned to User Account

Severity
medium
Time window
1h
Group by
AttributeValue, Computer, ObjectDN, SubjectAccount, SubjectDomainName, SubjectUserName, SubjectUserSid
Author
Vasileios Paschalidis
Source
github.com/Azure/Azure-Sentinel

This query identifies whether an Active Directory user object was assigned a service principal name which could indicate that an adversary is preparing for performing Kerberoasting. This query checks for event id 5136, that the Object Class field is "user" and the LDAP Display Name is "servicePrincipalName". Ref: https://thevivi.net/assets/docs/2019/theVIVI-AD-Security-Workshop_AfricaHackon2019.pdf

MITRE ATT&CK coverage

TacticTechniques
Privilege EscalationT1134 Access Token Manipulation

Event coverage

Rule body kusto

id: 875d0eb1-883a-4191-bd0e-dbfdeb95a464
name: Service Principal Name (SPN) Assigned to User Account
description: |
  'This query identifies whether an Active Directory user object was assigned a service principal name which could indicate that an adversary is preparing for performing Kerberoasting. 
  This query checks for event id 5136, that the Object Class field is "user" and the LDAP Display Name is "servicePrincipalName".
  Ref: https://thevivi.net/assets/docs/2019/theVIVI-AD-Security-Workshop_AfricaHackon2019.pdf'
severity: Medium
requiredDataConnectors:
  - connectorId: SecurityEvents
    dataTypes:
      - SecurityEvent
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - PrivilegeEscalation
relevantTechniques:
  - T1134
query: |
    SecurityEvent
    | where EventID == 5136 
    | parse EventData with * 'AttributeLDAPDisplayName">' AttributeLDAPDisplayName "<" *
    | parse EventData with * 'ObjectClass">' ObjectClass "<" *
    | where AttributeLDAPDisplayName == "servicePrincipalName" and  ObjectClass == "user"
    | parse EventData with * 'ObjectDN">' ObjectDN "<" *
    | parse EventData with * 'AttributeValue">' AttributeValue "<" *
    | summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated) by Computer, SubjectAccount, ObjectDN, AttributeValue, SubjectUserName, SubjectDomainName, SubjectUserSid
    | extend HostName = tostring(split(Computer, ".")[0]), DomainIndex = toint(indexof(Computer, '.'))
    | extend HostNameDomain = iff(DomainIndex != -1, substring(Computer, DomainIndex + 1), Computer)
    | project-away DomainIndex
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: SubjectAccount
      - identifier: Name
        columnName: SubjectUserName
      - identifier: NTDomain
        columnName: SubjectDomainName
  - entityType: Account
    fieldMappings:
      - identifier: Sid
        columnName: SubjectUserSid
  - entityType: Host
    fieldMappings:
      - identifier: FullName
        columnName: Computer
      - identifier: HostName
        columnName: HostName
      - identifier: NTDomain
        columnName: HostNameDomain
version: 1.0.4
kind: Scheduled
metadata:
    source:
        kind: Community
    author:
        name: Vasileios Paschalidis
    support:
        tier: Community
    categories:
        domains: [ "Security - Others", "Identity" ]

Stages and Predicates

Stage 1: source

SecurityEvent

Stage 2: where

| where EventID == 5136

Stage 3: parse

| parse EventData with * 'AttributeLDAPDisplayName">' AttributeLDAPDisplayName "<" *

Stage 4: parse

| parse EventData with * 'ObjectClass">' ObjectClass "<" *

Stage 5: where

| where AttributeLDAPDisplayName == "servicePrincipalName" and  ObjectClass == "user"

Stage 6: parse

| parse EventData with * 'ObjectDN">' ObjectDN "<" *

Stage 7: parse

| parse EventData with * 'AttributeValue">' AttributeValue "<" *

Stage 8: summarize

| summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated) by Computer, SubjectAccount, ObjectDN, AttributeValue, SubjectUserName, SubjectDomainName, SubjectUserSid

Stage 9: extend

| extend HostName = tostring(split(Computer, ".")[0]), DomainIndex = toint(indexof(Computer, '.'))

Stage 10: extend

| extend HostNameDomain = iff(DomainIndex != -1, substring(Computer, DomainIndex + 1), Computer)
HostNameDomain =
ifDomainIndex != -1substring(Computer, (DomainIndex + 1))
elseComputer

Stage 11: project-away

| project-away DomainIndex

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
AttributeLDAPDisplayNameeq
  • servicePrincipalName transforms: cased corpus 9 (sigma 4, splunk 3, elastic 1, kusto 1)
EventIDeq
  • 5136 transforms: cased corpus 30 (splunk 24, kusto 5, elastic 1)
ObjectClasseq
  • user transforms: cased corpus 6 (sigma 2, splunk 2, elastic 1, kusto 1)

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
AttributeValuesummarize
Computersummarize
EndTimesummarize
ObjectDNsummarize
StartTimesummarize
SubjectAccountsummarize
SubjectDomainNamesummarize
SubjectUserNamesummarize
SubjectUserSidsummarize
HostNameextend
HostNameDomainextend