// create a whitelist that consists of your AD domain names. ex: ("contoso.com","contoso.local","contoso.dmz")
let _whitelist_ServiceName = dynamic (["contoso.com","contoso.local","contoso.dmz"]);
let _fromDate = ago(8d);
let _thruDate = now();
let _min_dcount_threshold = 6;
let _period =2h; // this value should also be used as the rule frequency.
// If an account requested more than <_min_dcount_threshold> service tickets during the last <_period> hour,
// we need to check if it is suspicious or not by looking at historical data for the same user.
// STEP 1: Create a list of accounts to be checked based on their requests during the last <_period> hour.
// This approach also increases the rule performance as only the suspicious accounts are checked historically.
let _accounts_to_be_checked =
SecurityEvent
| where TimeGenerated > ago(_period)
| where EventID == 4769
| parse EventData with * 'Status">' Status "<" *
| where Status == '0x0'
| parse EventData with * 'ServiceName">' ServiceName "<" *
| where ServiceName !contains "$" and ServiceName !contains "krbtgt"
| where ServiceName !in~ (_whitelist_ServiceName )
| parse EventData with * 'TargetUserName">' TargetUserName "<" *
| where TargetUserName !contains ServiceName
| parse EventData with * 'TicketEncryptionType">' TicketEncryptionType "<" *
| where TicketEncryptionType in~ ('0x17','0x18') //these are the weak encryption types.
| parse EventData with * 'TicketOptions">' TicketOptions "<" *
| parse EventData with * 'IpAddress">::ffff:' ClientIPAddress "<" *
| summarize dcount(ServiceName) by TargetUserName
| where dcount_ServiceName > _min_dcount_threshold
| summarize make_list(TargetUserName);
// STEP 2: Create a time-series statistics per day for each account in the list, for the last 8 days.
let baseData = materialize(
SecurityEvent
| where TimeGenerated > _fromDate
| where EventID == 4769
| parse EventData with * 'Status">' Status "<" *
| where Status == '0x0'
| parse EventData with * 'ServiceName">' ServiceName "<" *
| where ServiceName !contains "$" and ServiceName !contains "krbtgt"
| where ServiceName !in~ (_whitelist_ServiceName )
| parse EventData with * 'TargetUserName">' TargetUserName "<" *
| where TargetUserName in~ (_accounts_to_be_checked) // only the accounts in the list will be analyzed.
| parse EventData with * 'TicketEncryptionType">' TicketEncryptionType "<" *
| where TicketEncryptionType in~ ('0x17','0x18')
| parse EventData with * 'TicketOptions">' TicketOptions "<" *
| parse EventData with * 'IpAddress">::ffff:' ClientIPAddress "<" *
//creating the time-series stats
| make-series dcount(ServiceName) default=0 on TimeGenerated in range(_fromDate, _thruDate, 1d) by TargetUserName, ClientIPAddress
| extend avg = todouble(series_stats_dynamic(dcount_ServiceName).avg )
);
// STEP 3: Find outliers(anomalies) in the time-series data and make a list of suspicious accounts.
let AnomTargetUserNames = (
baseData
| extend outliers = series_outliers(dcount_ServiceName)
| mvexpand dcount_ServiceName, TimeGenerated, outliers to typeof(double)
// dcount must be at least greater than the threshold. otherwise small numbers can also be an outlier.
// ex: 1,1,1,1,5 -> 5 is an outlier but probably not suspicious in big environment
| where dcount_ServiceName > _min_dcount_threshold
// time-series function rounds the time information and displays the time info as the startofday like '2020-08-23T00:00:00.0000000Z'.
// check if the anomlay happened on the current day.
| where TimeGenerated >= startofday(now())
// we are looking for positive outliers, meaning increase in the volume.
// this threshold can be adjusted based on the number of false positives or false negatives
| where outliers > 1.5
// we are looking for big outliers. Top 25 outliers should be more than enough.
| top 25 by outliers desc
| summarize make_list(TargetUserName)
);
// STEP 4: Anomaly is found but it's not clear when it happened exactly and there is not enough information for incident response.
// Get last <_period> of data and dcount of service names, set of service names that have been requested for each account in the anomalous account list.
// Join with the baseData as it has both avg values and other details of the anomaly.
// Avg value will also be used for preventing the rule from triggering the same alert again.
// We already found the anomalous users but we need to check if the anomaly happened in the last <_period> to prevent duplicate incidents.
SecurityEvent
| where TimeGenerated >= ago(_period)
| where EventID == 4769
| parse EventData with * 'Status">' Status "<" *
| where Status == '0x0'
| parse EventData with * 'ServiceName">' ServiceName "<" *
| where ServiceName !contains "$" and ServiceName !contains "krbtgt"
| where ServiceName !in~ (_whitelist_ServiceName )
| parse EventData with * 'TargetUserName">' TargetUserName "<" *
| where TargetUserName in (AnomTargetUserNames) // checking if the user is in the list of anomolous users
| parse EventData with * 'TicketEncryptionType">' TicketEncryptionType "<" *
| where TicketEncryptionType in~ ('0x17','0x18')
| parse EventData with * 'TicketOptions">' TicketOptions "<" *
| parse EventData with * 'IpAddress">::ffff:' ClientIPAddress "<" *
// Calculate the dcount of service tickets, create the set of service tickets requested
| summarize start_Time=min(TimeGenerated), end_Time=max(TimeGenerated), ServiceNameCount_LastPeriod = dcount(ServiceName), ServiceNameSet_LastPeriod = makeset(ServiceName) by TargetUserName
// Now join this with the baseData to display the details of the anomaly.
| join kind=leftouter baseData on TargetUserName
| where ServiceNameCount_LastPeriod > avg + 2 // this is a simple check to see if the anomaly happened in the last period.
// Display the columns we need
| project start_Time, end_Time, ClientIPAddress, TargetUserName, ServiceNameCount_Avg = avg, ServiceNameCount_LastPeriod, dcount_ServiceName, ServiceNameSet_LastPeriod
-
-
Save Cyb3r-Monk/818eb8b965a4cd4ff67028ba0f81fdd8 to your computer and use it in GitHub Desktop.
Kerberoasting Detection with KQL
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment