├── README.md ├── Defender For Endpoint ├── HuntPublicDevicesWithoutTag.md ├── DetectTokenStealingWithWdac.md ├── HuntDevicesDoingFirstRdpSession.md ├── DetectCveExploitForVulnerableDevice.md ├── DetectDumpGuardNtlmChallenge.md ├── HuntPublicDevicesWithTag.md ├── HuntPublicDevicesOverTime.md ├── HuntDevicesSupportingMdeContainment.md ├── HuntDeviceDiscoverySubnetRanges.md ├── DetectServiceAccLoginOnNewDevice.md ├── HuntDevicesDoingRdpToNonTpmDevice.md ├── HuntADWSRequestsFromUnknownDevice.md └── HuntOrganizeDevicesBySubnet.md ├── DetectionTemplate.md ├── Entra ID ├── DetectEntraTokenRequestViaBofIoC.md ├── HuntMSOLAzureADConnectOrEntraSyncServers.md ├── DetectSuspiciousNcryptUsageByCliToolOrUnknownProcess.md ├── DetectUserRequestTokenForAdminApp.md ├── DetectSuspiciousNcryptUsageByCliToolOrUnknownProcessWithNonce.md ├── HuntDomainsWithSeamlessSsoEnabled.md ├── DetectChangesToConnectSyncApplication.md ├── DetectCredAddToConnectSyncApplication.md ├── DetectSuspiciousFociTokenLogins.md ├── DetectMultipleWhfbPrtTokensUsedSimultaneouslyForOneDevice.md ├── DetectSuspiciousFociTokenLoginsV2.md ├── DetectSuspiciousNcryptUsageWithSuspiciousRdpSession.md ├── DetectSuspiciousNcryptUsageWithSuspiciousAdminRdpSession.md └── DetectSuspiciousCaChanges.md ├── Azure ├── DetectFirstTimeAzureCustomScriptOrRunCommand.md ├── DetectExecutableDropViaAzure.md ├── DetectAzureScriptOrRunCommandByRiskyUser.md └── DetectProcessDropViaAzureLateralMovement.md ├── Defender for Identity ├── HuntMdiNotInstalled.md ├── DetectSuspiciousSpnLogonFromWorkstation.md └── HuntNnrHealthIssues.md ├── Exposure Management ├── HuntCriticalCredentialsOnDevicesWithNonCriticalAccounts.md ├── HuntPrivilegeEscalationPathsWithHighACLs.md ├── HuntCriticalCredentialsOnNonCredGuardDevices.md ├── HuntCriticalCredentialsOnNonTpmDevices.md └── HuntPublicRemotlyExploitableDevicesWithHighEPSS.md ├── Threat & Vulnerability Management └── HuntCompromisedBrowserExtensions.md ├── Global Secure Access └── HuntMdeWithGsaEvents.md ├── Defender for Office365 └── DetectDirectSendPhishingEmails.md └── Normalization queries └── CefToCommonSecurityLog.md /README.md: -------------------------------------------------------------------------------- 1 | # KQL Sentinel & Defender queries 2 | 3 | # KQL for Defender XDR, Microsoft Sentinel & other Microsoft Solutions 4 | 5 | The purpose of this repository is to share KQL queries that can be used by anyone and are understandable. These queries are intended to increase detection coverage through the logs of Microsoft Security products. Not all suspicious activities generate an alert by default, but many of those activities can be made detectable through the logs. These queries include Detection Rules, Hunting Queries, Security misconfigurations and Visualisations. Anyone is free to use the queries. 6 | 7 | **Presenting this material as your own is illegal and forbidden. A reference to Twitter [@RobbeVdDaele](https://x.com/RobbeVdDaele) or Github [RobbeVandenDaele](https://github.com/RobbeVandenDaele) is much appriciated when sharing or using the content.** 8 | 9 | # Credits 10 | 11 | [@BertJanCyber](https://twitter.com/BertJanCyber) - The content structure of this repository was adopted from [Bert-Jan's KQL repository](https://github.com/Bert-JanP/Hunting-Queries-Detection-Rules) -------------------------------------------------------------------------------- /Defender For Endpoint/HuntPublicDevicesWithoutTag.md: -------------------------------------------------------------------------------- 1 | # *Hunt for public facing devices via DeviceNetworkEvents* 2 | 3 | ## Query Information 4 | 5 | #### MITRE ATT&CK Technique(s) 6 | 7 | | Technique ID | Title | Link | 8 | | --- | --- | --- | 9 | | T1190 | Exploit Public-Facing Application | https://attack.mitre.org/techniques/T1190/ | 10 | 11 | #### Description 12 | Find public facing devices via the DeviceNetworkEvents table. 13 | 14 | #### Risk 15 | When a proxy solution is in front of the public facing device, the devices will not be included in this query. 16 | 17 | #### Author 18 | - **Name:** Robbe Van den Daele 19 | - **Github:** https://github.com/RobbeVandenDaele 20 | - **Twitter:** https://x.com/RobbeVdDaele 21 | - **LinkedIn:** https://www.linkedin.com/in/robbe-van-den-daele-677986190/ 22 | - **Website:** https://hybridbrothers.com/ 23 | 24 | #### References 25 | - https://hybridbrothers.com/analyzing-mde-network-inspections/ 26 | 27 | ## Defender XDR 28 | ```KQL 29 | DeviceNetworkEvents 30 | | where ActionType contains "InboundConnection" 31 | | where RemoteIPType == "Public" 32 | | distinct DeviceName 33 | ``` 34 | 35 | ## Sentinel 36 | ```KQL 37 | N/A 38 | ``` -------------------------------------------------------------------------------- /DetectionTemplate.md: -------------------------------------------------------------------------------- 1 | # *Detection Title* 2 | 3 | ## Query Information 4 | 5 | #### MITRE ATT&CK Technique(s) 6 | 7 | | Technique ID | Title | Link | 8 | | --- | --- | --- | 9 | | T1134.002 | Access Token Manipulation: Create Process with Token | https://attack.mitre.org/techniques/T1134/002/ | 10 | 11 | #### Description 12 | Description of the detection rule. 13 | 14 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. 15 | 16 | #### Risk 17 | Explain what risk this detection tries to cover 18 | 19 | #### Author 20 | - **Name:** 21 | - **Github:** 22 | - **Twitter:** 23 | - **LinkedIn:** 24 | - **Website:** 25 | 26 | #### References 27 | - https://kqlquery.com/ 28 | - https://github.com/Bert-JanP/Hunting-Queries-Detection-Rules 29 | - example link 3 30 | 31 | ## Defender XDR 32 | ```KQL 33 | // Paste your query here 34 | DeviceProcessEvents 35 | | where FileName == "Example.File" 36 | ``` 37 | 38 | ## Sentinel 39 | ```KQL 40 | // Paste your query here 41 | DeviceProcessEvents 42 | | where FileName == "Example.File" 43 | ``` -------------------------------------------------------------------------------- /Entra ID/DetectEntraTokenRequestViaBofIoC.md: -------------------------------------------------------------------------------- 1 | # *Detect entra token request via specific BOF (IOC based)* 2 | 3 | ## Query Information 4 | 5 | #### MITRE ATT&CK Technique(s) 6 | 7 | | Technique ID | Title | Link | 8 | | --- | --- | --- | 9 | | T1651 | Cloud Administration Command | https://attack.mitre.org/techniques/T1651/ | 10 | | T1606 | Forge Web Credentials | https://attack.mitre.org/techniques/T1606/ | 11 | 12 | #### Description 13 | This might be one of the silliest detections I have created. But since there is a Beacon Object File out there which can be used to directly request Entra ID access tokens from an active beacon on a device using a specific User Agent, we can easily detect this beacon file by flagging the funny user agent and / or scope identifier that is used. 14 | 15 | #### Risk 16 | Detect token request via a specific BOF file. 17 | 18 | #### Author 19 | - **Name:** Robbe Van den Daele 20 | - **Github:** https://github.com/RobbeVandenDaele 21 | - **Twitter:** https://x.com/RobbeVdDaele 22 | - **LinkedIn:** https://www.linkedin.com/in/robbe-van-den-daele-677986190/ 23 | - **Website:** https://hybridbrothers.com/ 24 | 25 | #### References 26 | - https://github.com/trustedsec/CS-Remote-OPs-BOF/blob/12850f1d9306ccdec21f2b4e9dd16f78b0b949a9/src/Remote/get_azure_token/entry.c#L260 27 | 28 | ## Sentinel 29 | ```KQL 30 | AADNonInteractiveUserSignInLogs 31 | | where TimeGenerated > ago(5m) 32 | | where UserAgent contains "ur mum" 33 | | where ResourceIdentity == "797f4846-ba00-4fd7-ba43-dac1f8f63013" 34 | ``` -------------------------------------------------------------------------------- /Azure/DetectFirstTimeAzureCustomScriptOrRunCommand.md: -------------------------------------------------------------------------------- 1 | # *Detect first time Azure Custom Script or Run Command deployment* 2 | 3 | ## Query Information 4 | 5 | #### MITRE ATT&CK Technique(s) 6 | 7 | | Technique ID | Title | Link | 8 | | --- | --- | --- | 9 | | T1021.008 | Remote Services: Direct Cloud VM Connections | https://attack.mitre.org/techniques/T1021/008/ | 10 | | T1651 | Cloud Administration Command | https://attack.mitre.org/techniques/T1651/ | 11 | 12 | 13 | #### Description 14 | This detection rule flags using UEBA of Defender XDR and Microsoft Sentinel if it is the first time that an account is deploying Custom Scripts or Run Commands on Azure and Azure Arc machines. Since UEBA uses a baseline of 180 days, it might indicate that an account is being abused to compormise Azure or Azure Arc machines. 15 | 16 | #### Risk 17 | This rule tries to mitigate the risk of cloud admin accounts being abused to compromised Azure or Azure Arc machines while Custom Scripts or Run Commands are not really used in the environment. 18 | 19 | #### Author 20 | - **Name:** Robbe Van den Daele 21 | - **Github:** https://github.com/RobbeVandenDaele 22 | - **Twitter:** https://x.com/RobbeVdDaele 23 | - **LinkedIn:** https://www.linkedin.com/in/robbe-van-den-daele-677986190/ 24 | - **Website:** https://hybridbrothers.com/ 25 | 26 | #### References 27 | - https://learn.microsoft.com/en-us/azure/sentinel/ueba-reference?tabs=log-analytics#action-performed 28 | - https://thecollective.eu/ 29 | 30 | ## Defender XDR 31 | ```kql 32 | BehaviorAnalytics 33 | | where TimeGenerated > ago(1h) 34 | | extend ActivityInsights = parse_json(ActivityInsights) 35 | | where ActivityInsights.EventMessage has_any ('runCommand/action', 'extensions/write') 36 | | where ActivityInsights.FirstTimeUserPerformedAction == "True" 37 | ``` -------------------------------------------------------------------------------- /Defender For Endpoint/DetectTokenStealingWithWdac.md: -------------------------------------------------------------------------------- 1 | # *Detect device token stealing with WDAC* 2 | 3 | ## Query Information 4 | 5 | #### MITRE ATT&CK Technique(s) 6 | 7 | | Technique ID | Title | Link | 8 | | --- | --- | --- | 9 | | T1212 | Exploitation for Credential Access | https://attack.mitre.org/techniques/T1212/ | 10 | | T1606.001 | Forge Web Credentials: Web Cookies | https://attack.mitre.org/techniques/T1606/001/ | 11 | | T1528 | Steal Application Access Token | https://attack.mitre.org/techniques/T1528/ | 12 | | T1539 | Steal Web Session Cookie | https://attack.mitre.org/techniques/T1539/ | 13 | 14 | #### Description 15 | This rule uses a WDAC audit policy to ingest missing Microsoft Defender for Endpoint events. By doing this, we can detect PRT token stealing on a device when exploiting the MicrosoftAccountTokenProvider.dll. For more detailed information on the WDAC audit policy, see the blogpost added in the references. 16 | 17 | #### Risk 18 | Exploitation of the MicrosoftAccountTokenProvider.dll is something Defender for Endpoint does not detect by default. This makes this detection rule so important, since it fills a very important blind spot. 19 | 20 | #### Author 21 | - **Name:** Robbe Van den Daele 22 | - **Github:** https://github.com/RobbeVandenDaele 23 | - **Twitter:** https://x.com/RobbeVdDaele 24 | - **LinkedIn:** https://www.linkedin.com/in/robbe-van-den-daele-677986190/ 25 | - **Website:** https://hybridbrothers.com/ 26 | 27 | #### References 28 | - https://hybridbrothers.com/using-wdac-to-ingest-missing-mde-events/ 29 | 30 | ## Defender XDR 31 | ```KQL 32 | DeviceEvents 33 | | where ActionType startswith "AppControl" 34 | | where FileName =~ "MicrosoftAccountTokenProvider.dll" 35 | | invoke FileProfile(InitiatingProcessSHA1, 1000) 36 | | where GlobalPrevalence < 250 37 | ``` 38 | 39 | ## Sentinel 40 | ```KQL 41 | N/A 42 | ``` -------------------------------------------------------------------------------- /Entra ID/HuntMSOLAzureADConnectOrEntraSyncServers.md: -------------------------------------------------------------------------------- 1 | # *Hunt MSOL Azure AD Connect / Entra Sync servers* 2 | 3 | ## Query Information 4 | 5 | #### MITRE ATT&CK Technique(s) 6 | 7 | N/A 8 | 9 | #### Description 10 | Microsoft announced that starting from April 30 2025, Microsoft Entra Connect will need to have the minimal version of 2.4.18.0. If you want to identitify if you still have an AD Connect or Entra Sync server with a lower version, you can use below KQL query. 11 | 12 | #### Risk 13 | See reference for impacted scenario's. 14 | 15 | #### Author 16 | - **Name:** Robbe Van den Daele 17 | - **Github:** https://github.com/RobbeVandenDaele 18 | - **Twitter:** https://x.com/RobbeVdDaele 19 | - **LinkedIn:** https://www.linkedin.com/in/robbe-van-den-daele-677986190/ 20 | - **Website:** https://hybridbrothers.com/ 21 | 22 | #### References 23 | - https://learn.microsoft.com/en-us/entra/identity/hybrid/connect/harden-update-ad-fs-pingfederate 24 | - https://learn.microsoft.com/en-us/entra/identity/hybrid/connect/how-to-upgrade-previous-version 25 | 26 | ## Defender XDR 27 | ```KQL 28 | DeviceTvmSoftwareInventory 29 | | where SoftwareVendor == "microsoft" 30 | | where SoftwareName in ("microsoft_entra_connect_sync", "microsoft_azure_ad_connect") 31 | | distinct DeviceName, SoftwareName, SoftwareVendor, SoftwareVersion 32 | | extend MSOnlineDepricationSafe = iff( 33 | parse_version(SoftwareVersion) < parse_version("2.4.18.0"), 34 | "No", 35 | "Yes" 36 | ) 37 | ``` 38 | 39 | ## Sentinel 40 | ```KQL 41 | DeviceTvmSoftwareInventory 42 | | where SoftwareVendor == "microsoft" 43 | | where SoftwareName in ("microsoft_entra_connect_sync", "microsoft_azure_ad_connect") 44 | | distinct DeviceName, SoftwareName, SoftwareVendor, SoftwareVersion 45 | | extend MSOnlineDepricationSafe = iff( 46 | parse_version(SoftwareVersion) < parse_version("2.4.18.0"), 47 | "No", 48 | "Yes" 49 | ) 50 | ``` -------------------------------------------------------------------------------- /Defender For Endpoint/HuntDevicesDoingFirstRdpSession.md: -------------------------------------------------------------------------------- 1 | # *Hunt for devices doing first RDP session* 2 | 3 | ## Query Information 4 | 5 | #### MITRE ATT&CK Technique(s) 6 | 7 | | Technique ID | Title | Link | 8 | | --- | --- | --- | 9 | | T1021.001 | Remote Services: Remote Desktop Protocol | https://attack.mitre.org/techniques/T1021/001/ | 10 | 11 | #### Description 12 | This hunting query can help you find devices doing an RDP connection for the first time in 30 days. While this can be normal behavior, it might be interesting to look at why this device is suddenly doing an RDP connection. 13 | 14 | #### Risk 15 | By investigating these devices, you might find an attacker performing lateral movement over RDP from an end-user device. 16 | 17 | #### Author 18 | - **Name:** Robbe Van den Daele 19 | - **Github:** https://github.com/RobbeVandenDaele 20 | - **Twitter:** https://x.com/RobbeVdDaele 21 | - **LinkedIn:** https://www.linkedin.com/in/robbe-van-den-daele-677986190/ 22 | - **Website:** https://hybridbrothers.com/ 23 | 24 | #### References 25 | - https://hybridbrothers.com/detecting-non-privileged-windows-hello-abuse/ 26 | 27 | ## Defender XDR 28 | ```KQL 29 | let historic_rdp_devices = toscalar( 30 | DeviceNetworkEvents 31 | | where Timestamp > ago (30d) 32 | | where ActionType == "ConnectionSuccess" 33 | | where RemotePort == 3389 34 | | summarize make_set(DeviceId) 35 | ); 36 | DeviceNetworkEvents 37 | | where Timestamp > ago(1h) 38 | | where ActionType == "ConnectionSuccess" 39 | | where RemotePort == 3389 40 | | where DeviceId !in (historic_rdp_devices) 41 | ``` 42 | 43 | ## Sentinel 44 | ```KQL 45 | let historic_rdp_devices = toscalar( 46 | DeviceNetworkEvents 47 | | where TimeGenerated > ago (30d) 48 | | where ActionType == "ConnectionSuccess" 49 | | where RemotePort == 3389 50 | | summarize make_set(DeviceId) 51 | ); 52 | DeviceNetworkEvents 53 | | where TimeGenerated > ago(1h) 54 | | where ActionType == "ConnectionSuccess" 55 | | where RemotePort == 3389 56 | | where DeviceId !in (historic_rdp_devices) 57 | ``` -------------------------------------------------------------------------------- /Azure/DetectExecutableDropViaAzure.md: -------------------------------------------------------------------------------- 1 | # *Detect executable drops via Azure custom script extension* 2 | 3 | ## Query Information 4 | 5 | #### MITRE ATT&CK Technique(s) 6 | 7 | | Technique ID | Title | Link | 8 | | --- | --- | --- | 9 | | T1021.008 | Remote Services: Direct Cloud VM Connections | https://attack.mitre.org/techniques/T1021/008/ | 10 | | T1651 | Cloud Administration Command | https://attack.mitre.org/techniques/T1651/ | 11 | 12 | 13 | #### Description 14 | This detection rule flags when the Custom Script extension service on a machine is dropping executable files. This might indicate that an actor is trying to drop malware or beacons via a compromised cloud admin account. In the most legitimate cases administrators are pushing only PowerShell or Shell scripts, although these can also contain malicious content. Be aware of this gap in the below detection rule. 15 | 16 | #### Risk 17 | This rule triest to mitigate the risk of malicious actors trying to deploy malware or beacons via Azure or Azure Arc Custom Script extensions. 18 | 19 | #### Author 20 | - **Name:** Robbe Van den Daele 21 | - **Github:** https://github.com/RobbeVandenDaele 22 | - **Twitter:** https://x.com/RobbeVdDaele 23 | - **LinkedIn:** https://www.linkedin.com/in/robbe-van-den-daele-677986190/ 24 | - **Website:** https://hybridbrothers.com/ 25 | 26 | #### References 27 | - https://thecollective.eu/ 28 | 29 | ## Defender XDR 30 | ```kql 31 | // Executable extensions we want to flag (you can also add .ps1 and .sh) 32 | let win_executable_extensions = dynamic([".dll", ".exe", ".msi", ".bat", ".cmd", ".com", ".vbs", ".wsf", ".scr", ".cpl"]); 33 | DeviceFileEvents 34 | | where TimeGenerated > ago(1h) 35 | // Search for file created events by Arc Custom Script Handler 36 | | where ActionType == "FileCreated" 37 | | where InitiatingProcessFileName =~ "customscripthandler.exe" 38 | // Get the file type 39 | | extend FileType = tostring(parse_json(AdditionalFields).FileType) 40 | // Flag on extension or executable file type 41 | | where FileName has_any (win_executable_extensions) or 42 | FileType contains "Executable" 43 | ``` -------------------------------------------------------------------------------- /Azure/DetectAzureScriptOrRunCommandByRiskyUser.md: -------------------------------------------------------------------------------- 1 | # *Detect Custom Script or Run Command deployment by risky user* 2 | 3 | ## Query Information 4 | 5 | #### MITRE ATT&CK Technique(s) 6 | 7 | | Technique ID | Title | Link | 8 | | --- | --- | --- | 9 | | T1021.008 | Remote Services: Direct Cloud VM Connections | https://attack.mitre.org/techniques/T1021/008/ | 10 | | T1651 | Cloud Administration Command | https://attack.mitre.org/techniques/T1651/ | 11 | 12 | 13 | #### Description 14 | This detection rule flags when a user with risk events in Entra ID Identity Protection is deploying Custom Scripts or Run Commands on Azure or Azure Arc machines. This may indicate a compromised cloud user that is now performaring lateral movement from the Azure control plane to Virtual Machines in other environments. 15 | 16 | #### Risk 17 | This rule tries to mitigate the risk of compromised cloud admin accounts performing lateral movement via Azure or Azure Arc Custom Script or Run Command deployments. 18 | 19 | #### Author 20 | - **Name:** Robbe Van den Daele 21 | - **Github:** https://github.com/RobbeVandenDaele 22 | - **Twitter:** https://x.com/RobbeVdDaele 23 | - **LinkedIn:** https://www.linkedin.com/in/robbe-van-den-daele-677986190/ 24 | - **Website:** https://hybridbrothers.com/ 25 | 26 | #### References 27 | - https://thecollective.eu/ 28 | 29 | ## Defender XDR 30 | ```kql 31 | AzureActivity 32 | | where TimeGenerated > ago(1h) 33 | | where CategoryValue == "Administrative" 34 | | where OperationNameValue =~ "Microsoft.Compute/virtualMachines/runCommand/action" 35 | or OperationNameValue =~ "MICROSOFT.COMPUTE/VIRTUALMACHINES/EXTENSIONS/WRITE" 36 | | extend VMName = tostring(todynamic(Properties).resource) 37 | | summarize make_list(ActivityStatusValue), TimeGenerated = max(TimeGenerated) by CorrelationId, CallerIpAddress, Caller, ResourceGroup, VMName 38 | | join kind=inner (AADUserRiskEvents | where TimeGenerated > ago(14d) ) on $left.Caller == $right.UserPrincipalName 39 | ``` 40 | 41 | > [!NOTE] 42 | > You can also use the `CloudAppEvents` table with `Applicationid` `12260` and `ActionType` `Write Extensions` 43 | -------------------------------------------------------------------------------- /Defender for Identity/HuntMdiNotInstalled.md: -------------------------------------------------------------------------------- 1 | # *Hunt for Defender for Identity not installed but eligible* 2 | 3 | ## Query Information 4 | 5 | #### MITRE ATT&CK Technique(s) 6 | 7 | N/A 8 | 9 | #### Description 10 | This query shows you which servers are eligible for Defender for identity but does not have the Defender for Identity agent installed. The query seach the eligible servers via Defender for Endpoint (requirement for this query to work), and is based on the server roles that MDE recongnizes. 11 | 12 | #### Risk 13 | If not alle eligible servers are onboarded in Defender for Identity, you have a detection gap. 14 | 15 | 16 | #### Author 17 | - **Name:** Robbe Van den Daele 18 | - **Github:** https://github.com/RobbeVandenDaele 19 | - **Twitter:** https://x.com/RobbeVdDaele 20 | - **LinkedIn:** https://www.linkedin.com/in/robbe-van-den-daele-677986190/ 21 | - **Website:** https://hybridbrothers.com/ 22 | 23 | #### References 24 | - N/A 25 | 26 | 27 | ## Defender XDR 28 | ```KQL 29 | let device_roles = dynamic(["EntraConnectServer", "AzureADConnectServer", "ActiveDirectoryCertificateServicesServer", "DomainController", "ADFS"]); 30 | let mdi_servers = ( 31 | DeviceTvmSoftwareInventory 32 | | where SoftwareName == "azure_advanced_threat_protection_sensor" 33 | | distinct MdiDeviceName=tolower(DeviceName) 34 | ); 35 | let mdi_eligible_servers = ( 36 | ExposureGraphNodes 37 | | extend DeviceRoles= parse_json(NodeProperties)["rawData"]["deviceRole"] 38 | | extend CriticalityRuleNames = parse_json(NodeProperties)["rawData"]["criticalityLevel"]["ruleNames"] 39 | | where DeviceRoles has_any (device_roles) or 40 | CriticalityRuleNames has_any (device_roles) 41 | | distinct NodeName=tolower(NodeName), tostring(DeviceRoles), tostring(CriticalityRuleNames) 42 | ); 43 | mdi_servers 44 | | join kind=rightouter mdi_eligible_servers on $left.MdiDeviceName == $right.NodeName 45 | | extend Issue = iff(isempty(MdiDeviceName), "This server is eligible for MDI but does not have MDI installed", "None") 46 | | where Issue != "None" 47 | ``` 48 | 49 | ## Sentinel 50 | ```KQL 51 | N/A 52 | ``` -------------------------------------------------------------------------------- /Azure/DetectProcessDropViaAzureLateralMovement.md: -------------------------------------------------------------------------------- 1 | # *Detect process drops via Azure Custom Script Extension performing lateral movement* 2 | 3 | ## Query Information 4 | 5 | #### MITRE ATT&CK Technique(s) 6 | 7 | | Technique ID | Title | Link | 8 | | --- | --- | --- | 9 | | T1021.008 | Remote Services: Direct Cloud VM Connections | https://attack.mitre.org/techniques/T1021/008/ | 10 | | T1651 | Cloud Administration Command | https://attack.mitre.org/techniques/T1651/ | 11 | | T1021 | Remote Services | https://attack.mitre.org/techniques/T1021/ | 12 | 13 | 14 | #### Description 15 | This detection rule spots processes that where dropped via Azure Custom Script Extension on a machine and are now performing lateral movement. A common procedures for attackers when they compromised one machine is to move laterally to other machines via common protocols such as RDP, SSH, VNC, WMI, RPC, etc. It is not very common in an environment that Custom Script Extensions is being used for this. 16 | 17 | #### Risk 18 | This detection rule tries to mitigate the risk of Azure and Azure Arc being used to compromise servers and move laterally through the environment. 19 | 20 | #### Author 21 | - **Name:** Robbe Van den Daele 22 | - **Github:** https://github.com/RobbeVandenDaele 23 | - **Twitter:** https://x.com/RobbeVdDaele 24 | - **LinkedIn:** https://www.linkedin.com/in/robbe-van-den-daele-677986190/ 25 | - **Website:** https://hybridbrothers.com/ 26 | 27 | #### References 28 | - https://thecollective.eu/ 29 | 30 | ## Defender XDR 31 | ```kql 32 | let process_drop_via_arc = ( 33 | DeviceFileEvents 34 | | where TimeGenerated > ago(7d) 35 | // Search for file created events by Arc Custom Script Handler 36 | | where ActionType == "FileCreated" 37 | | where InitiatingProcessFileName =~ "customscripthandler.exe" 38 | | where isnotempty(SHA256) 39 | | distinct SHA256 40 | ); 41 | DeviceNetworkEvents 42 | | where TimeGenerated > ago(1h) 43 | | join kind=inner process_drop_via_arc on $left.InitiatingProcessSHA256 == $right.SHA256 44 | | where RemotePort in ("5985", "5986", "445", "3389", "22", "5900", "135") 45 | | where ActionType in~ ("ConnectionSuccess", "ConnectionAttempt", 46 | "ConnectionFailed", "ConnectionRequest") 47 | ``` -------------------------------------------------------------------------------- /Defender For Endpoint/DetectCveExploitForVulnerableDevice.md: -------------------------------------------------------------------------------- 1 | # *Detect CVE exploits on network for which a device is vulnerable* 2 | 3 | ## Query Information 4 | 5 | #### MITRE ATT&CK Technique(s) 6 | 7 | | Technique ID | Title | Link | 8 | | --- | --- | --- | 9 | | T1203 | Exploitation for Client Execution | https://attack.mitre.org/techniques/T1203/ | 10 | | T1068 | Exploitation for Privilege Escalation | https://attack.mitre.org/techniques/T1068/ | 11 | | T1210 | Exploitation of Remote Services | https://attack.mitre.org/techniques/T1210/ | 12 | 13 | #### Description 14 | This detection query can be used to find specific CVE exploits passing on the wire for which the device is vulnerable. This query should have a very high TP rate, and can be considered as a 'High severity' query. 15 | 16 | #### Risk 17 | Detection of CVE exploits depends on the CVE detections Microsoft included in the Zeek engine of MDE. 18 | 19 | #### Author 20 | - **Name:** Robbe Van den Daele 21 | - **Github:** https://github.com/RobbeVandenDaele 22 | - **Twitter:** https://x.com/RobbeVdDaele 23 | - **LinkedIn:** https://www.linkedin.com/in/robbe-van-den-daele-677986190/ 24 | - **Website:** https://hybridbrothers.com/ 25 | 26 | #### References 27 | - https://hybridbrothers.com/analyzing-mde-network-inspections/ 28 | ## Defender XDR 29 | ```KQL 30 | // Get all the TVM data 31 | let tvm_data = DeviceTvmSoftwareVulnerabilities 32 | | distinct DeviceName, SoftwareName, SoftwareVendor, SoftwareVersion, CveId, VulnerabilitySeverityLevel; 33 | // Get CVE signatures on the network 34 | DeviceNetworkEvents 35 | | where ActionType contains "NetworkSignatureInspected" 36 | | extend AdditionalFields = todynamic(AdditionalFields) 37 | | extend SignatureName = tostring(AdditionalFields.SignatureName), 38 | SignatureMatchedContent = tostring(AdditionalFields.SignatureMatchedContent), 39 | SamplePacketContent = tostring(AdditionalFields.SamplePacketContent) 40 | | where SignatureName contains "CVE" 41 | // Join the TVM data of the related device 42 | | join kind=inner tvm_data on DeviceName 43 | // Check if the server is vulnerable to the detected CVE in network traffic 44 | | where SignatureName == CveId 45 | | project-away DeviceName1 46 | ``` 47 | 48 | ## Sentinel 49 | ```KQL 50 | N/A 51 | ``` -------------------------------------------------------------------------------- /Defender For Endpoint/DetectDumpGuardNtlmChallenge.md: -------------------------------------------------------------------------------- 1 | # *DumpGuard NTLM challenge detected* 2 | 3 | ## Query Information 4 | 5 | #### MITRE ATT&CK Technique(s) 6 | 7 | | Technique ID | Title | Link | 8 | | --- | --- | --- | 9 | | T1003.004 | OS Credential Dumping: LSA Secrets | https://attack.mitre.org/techniques/T1003/004/ | 10 | | T1003 | OS Credential Dumping | https://attack.mitre.org/techniques/T1003/ | 11 | 12 | #### Description 13 | With the DumpGuard tool, attackers are able to dump credetials via Remote Credential Guard on devices that have Credential Guard enabled. The creator of the DumpGuard tool purposely used a hard-coded NTLMv1 challenge into the tool, for easy detection. 14 | 15 | > [!WARNING] 16 | > Since the detection relies on a static IOC that can easily be changed in the source code, this detection has a low confidence score since it can be easily bypassed. However, if the detection hits it is almost 100% certain the alert will be TP. 17 | > Also take into account that the `NetworkSignatureInspected` ActionType in MDE is sampled, which means not very event will be logged. 18 | 19 | #### Risk 20 | This detection tries to mitigate the risk of attackers bypassing Credential Guard on devices by using the DumpGuard tool. 21 | 22 | #### Author 23 | - **Name:** Robbe Van den Daele 24 | - **Github:** https://github.com/RobbeVandenDaele 25 | - **Twitter:** https://x.com/RobbeVdDaele 26 | - **LinkedIn:** https://www.linkedin.com/in/robbe-van-den-daele-677986190/ 27 | - **Website:** https://hybridbrothers.com/ 28 | 29 | #### References 30 | - https://specterops.io/blog/2025/10/23/catching-credential-guard-off-guard/ 31 | 32 | ## Defender XDR 33 | ```KQL 34 | DeviceNetworkEvents 35 | // Get NTLM Challenges 36 | | where ActionType == "NetworkSignatureInspected" 37 | | where tostring(todynamic(AdditionalFields).SignatureName) =~ "NTLM-Challenge" 38 | // Extract the NTLM Sample Packet 39 | | extend SamplePacketContent = extract('\\["(.+)"\\]', 1, tostring(todynamic(AdditionalFields).SamplePacketContent)) 40 | // Remove % values, since the '1122334455667788' is easy to find without conversions 41 | | extend NewSamplePacketContent = strcat_array(split(SamplePacketContent, "%"), "") 42 | | where NewSamplePacketContent contains "1122334455667788" 43 | ``` 44 | -------------------------------------------------------------------------------- /Entra ID/DetectSuspiciousNcryptUsageByCliToolOrUnknownProcess.md: -------------------------------------------------------------------------------- 1 | # *Detect Suspicious ncrypt.dll usage by CLI tool or unknown process* 2 | 3 | ## Query Information 4 | 5 | #### MITRE ATT&CK Technique(s) 6 | 7 | | Technique ID | Title | Link | 8 | | --- | --- | --- | 9 | | T1555.004 | Credentials from Password Stores: Windows Credential Manager | https://attack.mitre.org/techniques/T1555/004/ | 10 | | T1606 | Forge Web Credentials | https://attack.mitre.org/techniques/T1606/ | 11 | 12 | #### Description 13 | This detection rule uses a WDAC audit policy to ingest missing DeviceImageLoad events in MDE, and check for suspicious processes using the ncrypt.dll. More information on the attack scenario this is detection is applicable for can be found in the references. 14 | 15 | #### Risk 16 | By using this detections, we can try to detect an attacker using the hellopoc.ps1 script in RoadTools to generate an assertion. 17 | 18 | #### Author 19 | - **Name:** Robbe Van den Daele 20 | - **Github:** https://github.com/RobbeVandenDaele 21 | - **Twitter:** https://x.com/RobbeVdDaele 22 | - **LinkedIn:** https://www.linkedin.com/in/robbe-van-den-daele-677986190/ 23 | - **Website:** https://hybridbrothers.com/ 24 | 25 | #### References 26 | - https://hybridbrothers.com/detecting-non-privileged-windows-hello-abuse/ 27 | - https://github.com/dirkjanm/ROADtools/blob/master/winhello_assertion/hellopoc.ps1 28 | 29 | ## Defender XDR 30 | ```KQL 31 | let cli_tools = dynamic(["powershell", "python"]); 32 | // Get suspicious ncrypt.dll usage via WDAC audit policy 33 | DeviceEvents 34 | | where ActionType startswith "AppControl" and FileName =~ "ncrypt.dll" 35 | | invoke FileProfile(InitiatingProcessSHA1, 1000) 36 | | where ( 37 | // Flag CLI tools 38 | InitiatingProcessFileName has_any (cli_tools) or 39 | // Flag unknown processes 40 | GlobalPrevalence < 250 41 | ) 42 | | sort by TimeGenerated desc 43 | ``` 44 | 45 | ## Sentinel 46 | ```KQL 47 | let cli_tools = dynamic(["powershell", "python"]); 48 | // Get suspicious ncrypt.dll usage via WDAC audit policy 49 | DeviceEvents 50 | | where ActionType startswith "AppControl" and FileName =~ "ncrypt.dll" 51 | | invoke FileProfile(InitiatingProcessSHA1, 1000) 52 | | where ( 53 | // Flag CLI tools 54 | InitiatingProcessFileName has_any (cli_tools) or 55 | // Flag unknown processes 56 | GlobalPrevalence < 250 57 | ) 58 | | sort by TimeGenerated desc 59 | ``` -------------------------------------------------------------------------------- /Defender For Endpoint/HuntPublicDevicesWithTag.md: -------------------------------------------------------------------------------- 1 | # *Hunt for public facing devices via public tag* 2 | 3 | ## Query Information 4 | 5 | #### MITRE ATT&CK Technique(s) 6 | 7 | | Technique ID | Title | Link | 8 | | --- | --- | --- | 9 | | T1190 | Exploit Public-Facing Application | https://attack.mitre.org/techniques/T1190/ | 10 | 11 | #### Description 12 | Find public facing devices via the public device tag in the DeviceInfo table. The internet facing reason is also included in this query. 13 | 14 | #### Risk 15 | Public facing identification is only supported for Windows operating systems with specific versions. For more details about the nuances, see the blogpost added in the references. 16 | 17 | #### Author 18 | - **Name:** Robbe Van den Daele 19 | - **Github:** https://github.com/RobbeVandenDaele 20 | - **Twitter:** https://x.com/RobbeVdDaele 21 | - **LinkedIn:** https://www.linkedin.com/in/robbe-van-den-daele-677986190/ 22 | - **Website:** https://hybridbrothers.com/ 23 | 24 | #### References 25 | - https://hybridbrothers.com/analyzing-mde-network-inspections/ 26 | 27 | ## Defender XDR 28 | ```KQL 29 | DeviceInfo 30 | | where Timestamp > ago(7d) 31 | | extend AdditionalFields = todynamic(AdditionalFields) 32 | | where todatetime(AdditionalFields.InternetFacingLastSeen) > ago(7d) 33 | | extend InternetFacingLastSeen = tostring(AdditionalFields.InternetFacingLastSeen) 34 | , InternetFacingReason = tostring(AdditionalFields.InternetFacingReason) 35 | , InternetFacingLocalIp = tostring(AdditionalFields.InternetFacingLocalIp) 36 | , InternetFacingPublicScannedIp = tostring(AdditionalFields.InternetFacingPublicScannedIp) 37 | , InternetFacingLocalPort = tostring(AdditionalFields.InternetFacingLocalPort) 38 | , InternetFacingPublicScannedPort = tostring(AdditionalFields.InternetFacingPublicScannedPort) 39 | , InternetFacingTransportProtocol = tostring(AdditionalFields.InternetFacingTransportProtocol) 40 | | summarize arg_max(InternetFacingLastSeen, *) by DeviceName, InternetFacingLocalIp, InternetFacingLocalPort, InternetFacingPublicScannedIp, InternetFacingPublicScannedPort, InternetFacingTransportProtocol, InternetFacingReason 41 | | project InternetFacingLastSeen, DeviceName, InternetFacingLocalIp, InternetFacingLocalPort, InternetFacingPublicScannedIp, InternetFacingPublicScannedPort, InternetFacingTransportProtocol, InternetFacingReason 42 | ``` 43 | 44 | ## Sentinel 45 | ```KQL 46 | N/A 47 | ``` -------------------------------------------------------------------------------- /Entra ID/DetectUserRequestTokenForAdminApp.md: -------------------------------------------------------------------------------- 1 | # *Detect non-admin requesting token for admin applications* 2 | 3 | ## Query Information 4 | 5 | #### MITRE ATT&CK Technique(s) 6 | 7 | | Technique ID | Title | Link | 8 | | --- | --- | --- | 9 | | T1651 | Cloud Administration Command | https://attack.mitre.org/techniques/T1651/ | 10 | 11 | #### Description 12 | This rule detects sign-in attempts from non-admin users to admin applications in Entra ID. 13 | 14 | #### Risk 15 | When for example RoadTx is used without modifications, it will request tokens for Azure AD PowerShell. This can easily be detected when done on a non-admin account. 16 | 17 | #### Author 18 | - **Name:** Robbe Van den Daele 19 | - **Github:** https://github.com/RobbeVandenDaele 20 | - **Twitter:** https://x.com/RobbeVdDaele 21 | - **LinkedIn:** https://www.linkedin.com/in/robbe-van-den-daele-677986190/ 22 | - **Website:** https://hybridbrothers.com/ 23 | 24 | #### References 25 | - https://hybridbrothers.com/device-to-entraid/ 26 | 27 | ## Defender XDR 28 | ```KQL 29 | N/A 30 | ``` 31 | 32 | ## Sentinel 33 | ```KQL 34 | let ITAccounts=(_GetWatchlist('ITAccounts') | summarize make_set(ITAccounts)); 35 | // Materialize Dataset 36 | let DataSetMat= materialize (SigninLogs 37 | | where TimeGenerated > ago(1h) 38 | | where AppDisplayName has_any ("PowerShell", "CLI", "Command Line", "Management Shell") 39 | // Get successful and failed due to no assignment logins 40 | | where ResultType in ("0", "50105") 41 | | summarize max(TimeGenerated) by UserPrincipalName, AppDisplayName, IPAddress, UserId, ResultType 42 | // join IdentityInfo to get more information 43 | | join kind=leftouter (IdentityInfo | where TimeGenerated > ago(14d) | summarize arg_max(TimeGenerated, *) by AccountObjectId ) on $left.UserId == $right.AccountObjectId 44 | // exclude Accounts with Assigned Roles 45 | | where array_length(AssignedRoles) == 0 46 | // exclude known IT personnel Departments 47 | | where Department !has "it" and Department !has "ict" and Department !has "operations" 48 | // exclude service accounts 49 | | where JobTitle != "Service Account"); 50 | // exclude IT accounts 51 | let FIL= (DataSetMat 52 | | extend ITAccounts= toscalar(ITAccounts) 53 | | mv-expand ITAccounts 54 | | where AccountUPN contains ITAccounts or AccountDisplayName contains ITAccounts); 55 | DataSetMat 56 | // exclude service accounts 57 | | join kind=leftanti FIL on AccountUPN 58 | | distinct max_TimeGenerated, UserPrincipalName, AppDisplayName, IPAddress, JobTitle, Department, UserId, ResultType 59 | ``` -------------------------------------------------------------------------------- /Defender For Endpoint/HuntPublicDevicesOverTime.md: -------------------------------------------------------------------------------- 1 | # *Hunt for public facing devices and exposed ports over time* 2 | 3 | ## Query Information 4 | 5 | #### MITRE ATT&CK Technique(s) 6 | 7 | | Technique ID | Title | Link | 8 | | --- | --- | --- | 9 | | T1190 | Exploit Public-Facing Application | https://attack.mitre.org/techniques/T1190/ | 10 | 11 | #### Description 12 | Find public facing devices over time via the public device tag in the DeviceInfo table. 13 | 14 | #### Risk 15 | Public facing identification is only supported for Windows operating systems with specific versions. For more details about the nuances, see the blogpost added in the references. 16 | 17 | #### Author 18 | - **Name:** Robbe Van den Daele 19 | - **Github:** https://github.com/RobbeVandenDaele 20 | - **Twitter:** https://x.com/RobbeVdDaele 21 | - **LinkedIn:** https://www.linkedin.com/in/robbe-van-den-daele-677986190/ 22 | - **Website:** https://hybridbrothers.com/ 23 | 24 | #### References 25 | - https://hybridbrothers.com/analyzing-mde-network-inspections/ 26 | 27 | ## Defender XDR 28 | ```KQL 29 | // Create a base function 30 | let base = (){ 31 | DeviceInfo 32 | | where Timestamp > ago(30d) 33 | | extend AdditionalFields = todynamic(AdditionalFields) 34 | | extend InternetFacingLastSeen = todatetime(AdditionalFields.InternetFacingLastSeen) 35 | , InternetFacingReason = tostring(AdditionalFields.InternetFacingReason) 36 | , InternetFacingLocalIp = tostring(AdditionalFields.InternetFacingLocalIp) 37 | , InternetFacingPublicScannedIp = tostring(AdditionalFields.InternetFacingPublicScannedIp) 38 | , InternetFacingLocalPort = tostring(AdditionalFields.InternetFacingLocalPort) 39 | , InternetFacingPublicScannedPort = tostring(AdditionalFields.InternetFacingPublicScannedPort) 40 | , InternetFacingTransportProtocol = tostring(AdditionalFields.InternetFacingTransportProtocol) 41 | }; 42 | base() 43 | // Get the latest resport 44 | | summarize arg_max(InternetFacingLastSeen, *) by DeviceName, InternetFacingLocalIp, InternetFacingLocalPort, InternetFacingTransportProtocol 45 | // Join with the earliest report 46 | | join kind=inner ( base() 47 | | summarize arg_min(InternetFacingLastSeen, *) by DeviceName, InternetFacingLocalIp, InternetFacingLocalPort, InternetFacingTransportProtocol 48 | ) on DeviceName, InternetFacingLocalIp, InternetFacingLocalPort, InternetFacingTransportProtocol 49 | // Make a data point for each day between earliest and latest report 50 | | extend Range = range(bin(InternetFacingLastSeen1, 1d), bin(InternetFacingLastSeen, 1d), 1d) 51 | // Now expand all datapoints for dates the ports have been active 52 | | mv-expand Range 53 | | where Range != "" 54 | | summarize count() by InternetFacingLocalPort, bin(todatetime(Range), 1d) 55 | | render linechart 56 | ``` 57 | 58 | ## Sentinel 59 | ```KQL 60 | N/A 61 | ``` -------------------------------------------------------------------------------- /Defender For Endpoint/HuntDevicesSupportingMdeContainment.md: -------------------------------------------------------------------------------- 1 | # *Hunt devices supporting MDE Containment* 2 | 3 | ## Query Information 4 | 5 | #### MITRE ATT&CK Technique(s) 6 | 7 | N/A 8 | 9 | #### Description 10 | This hunting query can help you finding which Defender for Endpoint enrolled devices support device containment. This is being done by looking at the client version and estimated time the version was available. 11 | 12 | #### Risk 13 | The calculation to check if device containment is supported is done by checking the date of when an update is available. Microsoft documentation is not always clear about which versions contain which features, which makes this query an estimation query. More information can be found in below references. 14 | 15 | #### Author 16 | - **Name:** Robbe Van den Daele 17 | - **Github:** https://github.com/RobbeVandenDaele 18 | - **Twitter:** https://x.com/RobbeVdDaele 19 | - **LinkedIn:** https://www.linkedin.com/in/robbe-van-den-daele-677986190/ 20 | - **Website:** https://hybridbrothers.com/ 21 | 22 | #### References 23 | - https://learn.microsoft.com/en-us/defender-endpoint/windows-whatsnew 24 | - https://hybridbrothers.com/device-isolation-and-containment-strategies/ 25 | 26 | ## Defender XDR 27 | ```KQL 28 | // Paste your query here 29 | // Gets the onboarded windows devices and checks containment support nuances 30 | let onboardedWindows = DeviceInfo 31 | | where OnboardingStatus == "Onboarded" and OSPlatform contains "Windows" 32 | | distinct DeviceId, DeviceName, ClientVersion, OSPlatform 33 | | parse ClientVersion with Major:int "." Minor:int "." Build:int "." Revision:int 34 | // Reference: https://learn.microsoft.com/en-us/defender-endpoint/windows-whatsnew 35 | | extend Date = case( 36 | Minor >= 8760, "July-2024", 37 | Minor >= 8750, "May-2024", 38 | Minor >= 8735, "Feb-2024", 39 | Minor >= 8672, "Dec-2023", 40 | Minor >= 8560, "Sept-2023", 41 | Minor > 8295, "May-2023", 42 | Minor == 8295 and Revision >= 1023, "May-2023", 43 | Minor == 8295 and Revision between (1019 .. 1023), "Jan/Feb-2023", 44 | Minor > 8210, "Dec-2022", 45 | Minor == 8210 and Build >= 22621 and Revision >= 1016, "Dec-2022", 46 | Minor == 8210 and not(Build >= 22621 and Revision >= 1016), "Aug-2022", 47 | "< Aug-2022" 48 | ) 49 | // Containment without AH Audit supported from Nov-2022 50 | // Containment with AH Audit supported from Mar-2023 51 | | extend Containment = case( 52 | Minor >= 8295, "Supported with AH Audit", 53 | (Minor == 8210 and Build >= 22621 and Revision >= 1016) or Minor > 8210, "Supported without AH Audit", 54 | "Unsupported" 55 | ); 56 | // Gets onboarded non-windows devices, since containment is not supported here 57 | let onboardedNonWindows = DeviceInfo 58 | | where OnboardingStatus == "Onboarded" and OSPlatform !contains "Windows" 59 | | distinct DeviceId, DeviceName, ClientVersion, OSPlatform 60 | | extend Containment = "Unsupported"; 61 | // Get not-onboarded Servers 62 | let notOnboardedServers = DeviceInfo 63 | | where OnboardingStatus != "Onboarded" and DeviceType == "Server" 64 | | distinct DeviceId, DeviceName, ClientVersion, OSPlatform 65 | | extend Containment = "Unsupported"; 66 | // Union all and show diagram 67 | union onboardedNonWindows, onboardedWindows, notOnboardedServers 68 | | summarize count() by Containment 69 | | render piechart 70 | ``` 71 | 72 | ## Sentinel 73 | ```KQL 74 | N/A 75 | ``` -------------------------------------------------------------------------------- /Exposure Management/HuntCriticalCredentialsOnDevicesWithNonCriticalAccounts.md: -------------------------------------------------------------------------------- 1 | # *Hunt for critical credentials on devices with non-critical accounts* 2 | 3 | ## Query Information 4 | 5 | #### MITRE ATT&CK Technique(s) 6 | 7 | | Technique ID | Title | Link | 8 | | --- | --- | --- | 9 | | T1078 | Valid Accounts | https://attack.mitre.org/techniques/T1078/ | 10 | 11 | #### Description 12 | In most organizations normal user accounts or accounts with low risk permissions have less security controls enabled. This because there are less security controls needed in order to minize the risk vectors that come with these accounts. If these accounts are used on devices where critical account credentials are also present, the critical user account can be compromised more easily when the device is accessed by an adversary via the non-critical user account. 13 | 14 | Because of this, a Privileged Access Workstation should be used which serves as a dedicated workstation for the critical accounts. By doing this, the critical user account cannot be compromised via a unhardened device. 15 | 16 | #### Risk 17 | When you know which devices are exposing critical credentials via access from non-critical accounts, you know which devices have the most risk to allow for privilege escalation. 18 | 19 | #### Author 20 | - **Name:** Robbe Van den Daele 21 | - **Github:** https://github.com/RobbeVandenDaele 22 | - **Twitter:** https://x.com/RobbeVdDaele 23 | - **LinkedIn:** https://www.linkedin.com/in/robbe-van-den-daele-677986190/ 24 | - **Website:** https://hybridbrothers.com/ 25 | 26 | #### References 27 | 28 | ## Defender XDR 29 | ```KQL 30 | // Search for all users and save their criticality level 31 | let xspm_users = materialize( 32 | ExposureGraphNodes 33 | | where NodeLabel == "user" 34 | | extend CriticalityLevel = todynamic(NodeProperties).rawData.criticalityLevel.criticalityLevel 35 | | extend RuleNames = todynamic(NodeProperties).rawData.criticalityLevel.ruleNames 36 | | distinct NodeName, NodeId, tostring(CriticalityLevel), tostring(RuleNames) 37 | ); 38 | // Make a list of all critical users 39 | let critical_users = toscalar( 40 | xspm_users 41 | | where CriticalityLevel == 0 42 | | summarize make_set(NodeName) 43 | ); 44 | // Make a list of all non critical users 45 | let non_critical_users = toscalar( 46 | xspm_users 47 | | where CriticalityLevel != 0 48 | | summarize make_set(NodeName) 49 | ); 50 | // Make graph for max of 3 edges, where we start from a device and end with an user 51 | ExposureGraphEdges 52 | | make-graph SourceNodeId --> TargetNodeId with ExposureGraphNodes on NodeId 53 | | graph-match (SourceNode)-[anyEdge*1..3]->(TargetNode) 54 | where SourceNode.NodeLabel in ("device", "microsoft.compute/virtualmachines") and TargetNode.NodeLabel == "user" 55 | project SourceNodeName = SourceNode.NodeName, 56 | Edges = anyEdge.EdgeLabel, 57 | TargetNodeName = TargetNode.NodeName, 58 | TargetNodeLabel = TargetNode.NodeLabel 59 | // Make a list of all users a device has credentials for 60 | | summarize UserList = make_set(TargetNodeName) by SourceNodeName 61 | // Only return devices with more than one credential 62 | | where array_length(UserList) > 1 63 | // Make new lists saving the critical users and non critical users per device 64 | | extend CriticalUserList = set_intersect(UserList, critical_users), 65 | NonCriticalUserList = set_intersect(UserList, non_critical_users) 66 | // Flag when a device has both critical and non critical users 67 | | where array_length(CriticalUserList) > 0 and array_length(NonCriticalUserList) > 0 68 | ``` -------------------------------------------------------------------------------- /Threat & Vulnerability Management/HuntCompromisedBrowserExtensions.md: -------------------------------------------------------------------------------- 1 | # *Hunt for compromised browser extensions* 2 | 3 | ## Query Information 4 | 5 | #### MITRE ATT&CK Technique(s) 6 | 7 | | Technique ID | Title | Link | 8 | | --- | --- | --- | 9 | | T1176 | Browser Extensions | https://attack.mitre.org/techniques/T1176/ | 10 | 11 | #### Description 12 | This hunting rule can be used to find devices using known compromised browser extensions. It is created based on the threat researched linked in the reference section. 13 | 14 | #### Risk 15 | These browser extensions have been found to steal sensitive user information and sign-in coockies due to a supply-chain compromise attack. 16 | 17 | #### Author 18 | - **Name:** Robbe Van den Daele 19 | - **Github:** https://github.com/RobbeVandenDaele 20 | - **Twitter:** https://x.com/RobbeVdDaele 21 | - **LinkedIn:** https://www.linkedin.com/in/robbe-van-den-daele-677986190/ 22 | - **Website:** https://hybridbrothers.com/ 23 | 24 | #### References 25 | - https://arstechnica.com/security/2025/01/dozens-of-backdoored-chrome-extensions-discovered-on-2-6-million-devices/ 26 | 27 | ## Defender XDR 28 | ```KQL 29 | let extensions = datatable (Id:string, VulnVersion:string) [ 30 | "nnpnnpemnckcfdebeekibpiijlicmpom", "2.0.1", 31 | "kkodiihpgodmdankclfibbiphjkfdenh", "1.16.2", 32 | "oaikpkmjciadfpddlpjjdapglcihgdle", "1.0.12", 33 | "dpggmcodlahmljkhlmpgpdcffdaoccni", "1.1.1", 34 | "acmfnomgphggonodopogfbmkneepfgnh", "4.00", 35 | "mnhffkhmpnefgklngfmlndmkimimbphc", "4.40", 36 | "cedgndijpacnfbdggppddacngjfdkaca", "0.0.11", 37 | "bbdnohkpnbkdkmnkddobeafboooinpla", "1.0.1", 38 | "egmennebgadmncfjafcemlecimkepcle", "2.2.7", 39 | "bibjgkidgpfbblifamdlkdlhgihmfohh", "0.1.3", 40 | "befflofjcniongenjmbkgkoljhgliihe", "2.13.0", 41 | "pkgciiiancapdlpcbppfkmeaieppikkk", "1.3.7", 42 | "llimhhconnjiflfimocjggfjdlmlhblm", "1.5.7", 43 | "oeiomhmbaapihbilkfkhmlajkeegnjhe", "3.18.0", 44 | "pajkjnmeojmbapicmbpliphjmcekeaac", "24.10.4", 45 | "ndlbedplllcgconngcnfmkadhokfaaln", "2.22.6", 46 | "epdjhgbipjpbbhoccdeipghoihibnfja", "1.4", 47 | "cplhlgabfijoiabgkigdafklbhhdkahj", "1.0.161", 48 | "jiofmdifioeejeilfkpegipdjiopiekl", "1.1.61", 49 | "hihblcmlaaademjlakdpicchbjnnnkbo", "3.0.2", 50 | "llimhhconnjiflfimocjggfjdlmlhblm", "1.5.7", 51 | "ekpkdmohpdnebfedjjfklhpefgpgaaji", "1.3", 52 | "epikoohpebngmakjinphfiagogjcnddm", "2.7.3", 53 | "miglaibdlgminlepgeifekifakochlka", "1.4.5", 54 | "eanofdhdfbcalhflpbdipkjjkoimeeod", "1.4.9", 55 | "ogbhbgkiojdollpjbhbamafmedkeockb", "1.8.1", 56 | "bgejafhieobnfpjlpcjjggoboebonfcg", "1.1.1", 57 | "igbodamhgjohafcenbcljfegbipdfjpk", "2.3", 58 | "mbindhfolmpijhodmgkloeeppmkhpmhc", "1.44", 59 | "hodiladlefdpcbemnbbcpclbmknkiaem", "3.1.3", 60 | "lbneaaedflankmgmfbmaplggbmjjmbae", "1.3.8", 61 | "eaijffijbobmnonfhilihbejadplhddo", "2.4", 62 | "hmiaoahjllhfgebflooeeefeiafpkfde", "1.0.0" 63 | ]; 64 | DeviceTvmBrowserExtensions 65 | // Find devices using vulnerable extensions 66 | | join kind=inner extensions on $left.ExtensionId == $right.Id 67 | | extend IntVersion = parse_version(ExtensionVersion), IntVulnVursion = parse_version(VulnVersion) 68 | | where IntVersion <= IntVulnVursion and IsActivated == "true" 69 | // Join for more device info 70 | | join kind=inner ( 71 | DeviceInfo 72 | | where Timestamp > ago(7d) 73 | ) on DeviceId 74 | | distinct DeviceName, DeviceId, BrowserName, ExtensionName, ExtensionDescription, ExtensionVersion, ExtensionRisk, VulnVersion 75 | ``` 76 | 77 | ## Sentinel 78 | ```KQL 79 | N/A 80 | ``` -------------------------------------------------------------------------------- /Entra ID/DetectSuspiciousNcryptUsageByCliToolOrUnknownProcessWithNonce.md: -------------------------------------------------------------------------------- 1 | # *Detect Suspicious ncrypt.dll usage by process requesting Entra ID Nonce* 2 | 3 | ## Query Information 4 | 5 | #### MITRE ATT&CK Technique(s) 6 | 7 | | Technique ID | Title | Link | 8 | | --- | --- | --- | 9 | | T1555.004 | Credentials from Password Stores: Windows Credential Manager | https://attack.mitre.org/techniques/T1555/004/ | 10 | | T1606 | Forge Web Credentials | https://attack.mitre.org/techniques/T1606/ | 11 | 12 | #### Description 13 | This detection rule uses a WDAC audit policy to ingest missing DeviceImageLoad events in MDE, and check for suspicious processes using the ncrypt.dll and requesting an Entra ID Nonce. More information on the attack scenario this is detection is applicable for can be found in the references. 14 | 15 | #### Risk 16 | By using this detections, we can try to detect an attacker using the hellopoc.ps1 script in RoadTools to generate an assertion. 17 | 18 | #### Author 19 | - **Name:** Robbe Van den Daele 20 | - **Github:** https://github.com/RobbeVandenDaele 21 | - **Twitter:** https://x.com/RobbeVdDaele 22 | - **LinkedIn:** https://www.linkedin.com/in/robbe-van-den-daele-677986190/ 23 | - **Website:** https://hybridbrothers.com/ 24 | 25 | #### References 26 | - https://hybridbrothers.com/detecting-non-privileged-windows-hello-abuse/ 27 | - https://github.com/dirkjanm/ROADtools/blob/master/winhello_assertion/hellopoc.ps1 28 | 29 | ## Defender XDR 30 | ```KQL 31 | let cli_tools = dynamic(["powershell", "python"]); 32 | // Get all possible nonce requests 33 | let nonce_requests = ( 34 | DeviceNetworkEvents 35 | | where Timestamp > ago(1h) 36 | | where ActionType startswith "ConnectionSuccess" 37 | | where RemoteUrl =~ "login.microsoftonline.com" 38 | | project-rename NonceTimestamp = Timestamp 39 | ); 40 | // Get suspicious ncrypt.dll usage via WDAC audit policy 41 | DeviceEvents 42 | | where Timestamp > ago(1h) 43 | | where ActionType startswith "AppControl" and FileName =~ "ncrypt.dll" 44 | // Check if the same initiating process is doing a nonce request 45 | | join kind=inner nonce_requests on InitiatingProcessId, DeviceId 46 | // Only flag when nonce was request 10min before of after ncrypt usage 47 | | where Timestamp between (todatetime(NonceTimestamp - 10m) .. todatetime(NonceTimestamp + 10m)) 48 | | invoke FileProfile(InitiatingProcessSHA1, 1000) 49 | | where ( 50 | // Flag CLI tools 51 | InitiatingProcessFileName has_any (cli_tools) or 52 | // Flag unknown processes 53 | GlobalPrevalence < 250 54 | ) 55 | ``` 56 | 57 | ## Sentinel 58 | ```KQL 59 | let cli_tools = dynamic(["powershell", "python"]); 60 | // Get all possible nonce requests 61 | let nonce_requests = ( 62 | DeviceNetworkEvents 63 | | where TimeGenerated > ago(1h) 64 | | where ActionType startswith "ConnectionSuccess" 65 | | where RemoteUrl =~ "login.microsoftonline.com" 66 | | project-rename NonceTimestamp = TimeGenerated 67 | ); 68 | // Get suspicious ncrypt.dll usage via WDAC audit policy 69 | DeviceEvents 70 | | where TimeGenerated > ago(1h) 71 | | where ActionType startswith "AppControl" and FileName =~ "ncrypt.dll" 72 | // Check if the same initiating process is doing a nonce request 73 | | join kind=inner nonce_requests on InitiatingProcessId, DeviceId 74 | // Only flag when nonce was request 10min before of after ncrypt usage 75 | | where TimeGenerated between (todatetime(NonceTimestamp - 10m) .. todatetime(NonceTimestamp + 10m)) 76 | | invoke FileProfile(InitiatingProcessSHA1, 1000) 77 | | where ( 78 | // Flag CLI tools 79 | InitiatingProcessFileName has_any (cli_tools) or 80 | // Flag unknown processes 81 | GlobalPrevalence < 250 82 | ) 83 | ``` -------------------------------------------------------------------------------- /Defender for Identity/DetectSuspiciousSpnLogonFromWorkstation.md: -------------------------------------------------------------------------------- 1 | # *Suspicious SPN logon from workstation (DumpGuard)* 2 | 3 | ## Query Information 4 | 5 | #### MITRE ATT&CK Technique(s) 6 | 7 | | Technique ID | Title | Link | 8 | | --- | --- | --- | 9 | | T1003.004 | OS Credential Dumping: LSA Secrets | https://attack.mitre.org/techniques/T1003/004/ | 10 | | T1003 | OS Credential Dumping | https://attack.mitre.org/techniques/T1003/ | 11 | 12 | #### Description 13 | With the DumpGuard tool, attackers are able to dump credetials via Remote Credential Guard on devices that have Credential Guard enabled. 14 | Since the DumpGuard tool needs to use an SPN enabled account (in the POC they use a machine account) for two exploitation scenario's, it is interesting to look for TGT requests happening from client devices for SPN enabled accounts. 15 | 16 | #### Risk 17 | This detection tries to mitigate the risk of attackers bypassing Credential Guard on devices by using the DumpGuard tool. 18 | 19 | #### Author 20 | - **Name:** Robbe Van den Daele 21 | - **Github:** https://github.com/RobbeVandenDaele 22 | - **Twitter:** https://x.com/RobbeVdDaele 23 | - **LinkedIn:** https://www.linkedin.com/in/robbe-van-den-daele-677986190/ 24 | - **Website:** https://hybridbrothers.com/ 25 | 26 | #### References 27 | - https://specterops.io/blog/2025/10/23/catching-credential-guard-off-guard/ 28 | - [Inspired by a BlueRaven query](https://www.linkedin.com/posts/bluraven_kql-threathunting-detectionengineering-activity-7387496098510319616-dGhE?utm_source=share&utm_medium=member_desktop&rcm=ACoAACz-oDsBI8pyHV8fT38Q6oiZQcBRxBPyw0I) 29 | 30 | ## Defender XDR 31 | ```KQL 32 | let spn_accounts = toscalar( 33 | // Search for all SPNs we can find in historic logs 34 | IdentityLogonEvents 35 | | where TimeGenerated > ago(14d) 36 | | where Application == "Active Directory" 37 | | where isnotempty(AdditionalFields.Spns) 38 | | extend Spns = split(AdditionalFields.Spns, ",") 39 | | summarize make_set(Spns) 40 | ); 41 | let workstation_subnets = toscalar( 42 | DeviceNetworkInfo 43 | | where TimeGenerated > ago(14d) 44 | // Filter out empty device names 45 | | where isnotempty(DeviceName) 46 | // Expand IP Addresses 47 | | mv-expand todynamic(IPAddresses) 48 | // Focus on device name and IP Address info 49 | | distinct DeviceName, tostring(IPAddresses) 50 | // Filter out IPv6 addresses, /32 addresses, and APIPA addresses 51 | | where todynamic(IPAddresses).IPAddress !contains ":" 52 | | where todynamic(IPAddresses).SubnetPrefix != "32" 53 | | where todynamic(IPAddresses).IPAddress !startswith "169.254" 54 | // Find Device Type of the device 55 | | join kind=inner ( 56 | DeviceInfo 57 | | where TimeGenerated > ago(30d) 58 | | distinct DeviceName, DeviceType 59 | ) on DeviceName 60 | // Only focus on workstations 61 | | where DeviceType == "Workstation" 62 | // Create Network Address based on the host IP Address and create a distinct list 63 | | extend NetworkAddress = format_ipv4_mask(tostring(todynamic(IPAddresses).IPAddress), tolong(todynamic(IPAddresses).SubnetPrefix)) 64 | | summarize make_set(NetworkAddress) 65 | ); 66 | IdentityLogonEvents 67 | | where TimeGenerated > ago(1h) 68 | // Get AD TGT requests by looking for Kerberos requests to KRBTGT account 69 | | where Application == "Active Directory" 70 | | where Protocol == "Kerberos" 71 | | where AdditionalFields.Spns contains "krbtgt" 72 | // Check for requests to account names with SPNs 73 | | where AccountName in (spn_accounts) 74 | // Check if IP Address is from a client range 75 | | where ipv4_is_in_any_range(IPAddress, workstation_subnets) 76 | // Optional - Ignore failed logins 77 | | where ActionType != "LogonFailed" 78 | ``` 79 | -------------------------------------------------------------------------------- /Exposure Management/HuntPrivilegeEscalationPathsWithHighACLs.md: -------------------------------------------------------------------------------- 1 | # *Hunt for privilege escalation paths with high ACLs* 2 | 3 | ## Query Information 4 | 5 | #### MITRE ATT&CK Technique(s) 6 | 7 | | Technique ID | Title | Link | 8 | | --- | --- | --- | 9 | | T1078 | Valid Accounts | https://attack.mitre.org/techniques/T1078/ | 10 | 11 | #### Description 12 | When an adversary establishes data collection of an Active Directory domain, they regularly search for interesting accounts with privilege escalation paths using the genericWrite and genericAll ACL permissions on objects. When using BloodHound, it is very easy to get a visual overview of these paths in an Active Directory domain. This query tries to establish the same using Defender XDR Exposure Management. 13 | 14 | #### Risk 15 | By knowing these paths you can effectivly remediate and lower the risk of privilege escalation paths in AD DS. 16 | 17 | #### Author 18 | - **Name:** Robbe Van den Daele 19 | - **Github:** https://github.com/RobbeVandenDaele 20 | - **Twitter:** https://x.com/RobbeVdDaele 21 | - **LinkedIn:** https://www.linkedin.com/in/robbe-van-den-daele-677986190/ 22 | - **Website:** https://hybridbrothers.com/ 23 | 24 | #### References 25 | - https://www.hackingarticles.in/genericwrite-active-directory-abuse/ 26 | - https://bloodhound.specterops.io/resources/edges/generic-write 27 | - https://bloodhound.specterops.io/resources/edges/generic-all 28 | 29 | ## Defender XDR 30 | ```KQL 31 | let high_permissions = dynamic(["genericWrite", "genericAll"]); 32 | let edge_labels = dynamic(["member of", "has permissions to", "can authenticate to", "can authenticate as", "has credentials of", "can impersonate as"]); 33 | // Get users and groups with high ACL permissions on other objects 34 | let HighPermissionLinks = (ExposureGraphEdges 35 | // Get edges related to roles 36 | | where EdgeLabel == "has role on" 37 | // Get edges containing high permission ACLs 38 | | extend Permissions = todynamic(EdgeProperties).rawData.acl.controlTypes 39 | | where Permissions has_any (high_permissions) 40 | // Exclude Domain and Enterprise Administrators as source node 41 | | where not(SourceNodeLabel == "group" and SourceNodeName in ("Domain Admins", "Enterprise Admins")) 42 | // Exclude Built-in administrator account 43 | | where not(SourceNodeLabel == "user" and SourceNodeName == "Administrator") 44 | | summarize TargetNodes = make_set(TargetNodeName), TargetNodeCount = count() by SourceNodeName, SourceNodeLabel, tostring(Permissions), TargetNodeLabel, SourceNodeId 45 | ); 46 | let HighPermissionNodes = toscalar( 47 | HighPermissionLinks 48 | | summarize SourceNodes = make_set(SourceNodeName) 49 | ); 50 | // Get edges for links to the high ACL permissions 51 | ExposureGraphEdges 52 | | where TargetNodeName in (HighPermissionNodes) 53 | | make-graph SourceNodeId --> TargetNodeId with ExposureGraphNodes on NodeId 54 | // Get between one and three relations 55 | | graph-match (SourceNode)-[anyEdge*1..3]->(TargetNode) 56 | project IncomingNodeName = SourceNode.NodeName, 57 | IncomingNodeLabel = SourceNode.NodeLabel, 58 | Edges = anyEdge.EdgeLabel, 59 | OutgoingNodeName = TargetNode.NodeName, 60 | OutgoingNodeId = TargetNode.NodeId 61 | // Filter for interesting edges 62 | | where Edges has_any (edge_labels) 63 | // Join the high permission ACLs 64 | | join kind=inner HighPermissionLinks on $left.OutgoingNodeId == $right.SourceNodeId 65 | // Exclude Domain and Enterprise Administrators as source node 66 | | where not(IncomingNodeLabel == "group" and IncomingNodeName in ("Domain Admins", "Enterprise Admins")) 67 | // Exclude Built-in administrator account 68 | | where not(IncomingNodeLabel == "user" and IncomingNodeName == "Administrator") 69 | | distinct IncomingNodeName, IncomingNodeLabel, tostring(Edges), OutgoingNodeName, OutgoingNodeLabel = SourceNodeLabel, tostring(Permissions), TargetNodeLabel, tostring(TargetNodes), TargetNodeCount 70 | ``` -------------------------------------------------------------------------------- /Entra ID/HuntDomainsWithSeamlessSsoEnabled.md: -------------------------------------------------------------------------------- 1 | # *Hunt domains with Seamless SSO enabled in Entra ID Connect* 2 | 3 | ## Query Information 4 | 5 | #### MITRE ATT&CK Technique(s) 6 | 7 | N/A 8 | 9 | #### Description 10 | With below KQL query you can search through the IdentityLogon events of Microsoft Defender for Identity to find users and devices still using Seamless SSO in Entra ID Connect. This feature has been marked by the community multiple times as a security risk, and should be disabled if not in use. The KQL query returns the domains where Seamless SSO is enabled, allong with the related users and devices. On top of that, devices get enriched to find their OS distribution, version, and join type and tells you if Seamless SSO is expected to be used for the related device or not. If there are no results or if all results are showing 'No' for the 'Seamless SSO Expected' column, it should be save to disable the feature in Entra ID connect. 11 | 12 | !**Important**: This query relies on the Domain Controller EventID 4769 and Defender for Identity. Make sure the EventID is being logged and Defender for Identity is healthy. For more information see references! 13 | 14 | #### Risk 15 | See reference for impacted scenario's. 16 | 17 | #### Author 18 | - **Name:** Robbe Van den Daele 19 | - **Github:** https://github.com/RobbeVandenDaele 20 | - **Twitter:** https://x.com/RobbeVdDaele 21 | - **LinkedIn:** https://www.linkedin.com/in/robbe-van-den-daele-677986190/ 22 | - **Website:** https://hybridbrothers.com/ 23 | 24 | #### References 25 | - https://nathanmcnulty.com/blog/2025/08/finding-seamless-sso-usage/# 26 | - https://ourcloudnetwork.com/why-you-should-disable-seamless-sso-in-microsoft-entra-connect/ 27 | - https://learn.microsoft.com/en-us/entra/identity/hybrid/connect/how-to-connect-sso-faq#how-can-i-disable-seamless-sso- 28 | - https://learn.microsoft.com/en-us/entra/identity/hybrid/connect/how-to-connect-sso 29 | 30 | ## Defender XDR 31 | ```KQL 32 | // Get all device info we can find 33 | let devices = ( 34 | DeviceInfo 35 | // Search for 14 days 36 | | where TimeGenerated > ago(14d) 37 | // Normalize DeviceName 38 | // --> if it is an IP Address we keep it 39 | // --> If it is not an IP Address we only use the hostname for correlation 40 | | extend DeviceName = iff(ipv4_is_private(DeviceName), DeviceName, tolower(split(DeviceName, ".")[0])) 41 | // Only get interesting data 42 | | distinct DeviceName, OSPlatform, OSVersion, DeviceId, OnboardingStatus, Model, JoinType 43 | ); 44 | IdentityLogonEvents 45 | // Get the last 30 days of logon events on Domain Controllers 46 | | where TimeGenerated > ago(30d) 47 | // Search for Seamless SSO events 48 | | where Application == "Active Directory" and Protocol == "Kerberos" 49 | | where TargetDeviceName == "AZUREADSSOACC" 50 | // Save the domain name of the Domain Controller 51 | | extend OnPremisesDomainName = strcat(split(DestinationDeviceName, ".")[-2], ".", split(DestinationDeviceName, ".")[-1]) 52 | // Normalize DeviceName 53 | // --> if it is an IP Address we keep it 54 | // --> If it is not an IP Address we only use the hostname for correlation 55 | | extend DeviceName = iff(ipv4_is_private(DeviceName), DeviceName, tolower(split(DeviceName, ".")[0])) 56 | // Only use interesting data and find more info regarding the source device 57 | | distinct AccountUpn, OnPremisesDomainName, DeviceName 58 | | join kind=leftouter devices on DeviceName 59 | | project-away DeviceName1 60 | // Check if Seamless SSO usage is expected 61 | | extend ['Seamless SSO Expected'] = case( 62 | // Cases where we do not expect Seamless SSO to be used 63 | JoinType == "Hybrid Azure AD Join" or 64 | JoinType == "AAD Joined" or 65 | JoinType == "AAD Registered", "No", 66 | // Cases where we do expect Seamless SSO to be used 67 | JoinType == "Domain Joined" or 68 | (OSPlatform startswith "Windows" and toreal(OSVersion) < 10.0) , "Yes", 69 | // Cases that need to be verified 70 | "Unknown (to verify)" 71 | ) 72 | ``` 73 | -------------------------------------------------------------------------------- /Exposure Management/HuntCriticalCredentialsOnNonCredGuardDevices.md: -------------------------------------------------------------------------------- 1 | # *Hunt for critical credentials on non Credential Guard enabled devices* 2 | 3 | ## Query Information 4 | 5 | #### MITRE ATT&CK Technique(s) 6 | 7 | | Technique ID | Title | Link | 8 | | --- | --- | --- | 9 | | TA0006 | Credential Access | https://attack.mitre.org/tactics/TA0006/ | 10 | 11 | 12 | #### Description 13 | This query searches for devices that does not have Credential Guard enabled but contains critical credentials. The output shows for how many users each non Credential Guard device has credentials, together with the list of users being exposed. 14 | 15 | 16 | #### Risk 17 | When critical credentials are stored on devices without Credential Guard enabled, it is more easy for adversaries to steal those credentials when the device is compromised. This is because without Credential Guard Kerberos, NTLM, and Credential Manager secrets are stored in the Local Security Authority (LSA) process called `lsass.exe`, which can be dumped with various tools like MimiKatz. With Credential Guard enabled, these secrets are protected and isolated using Virtualization-based security (VBS). 18 | 19 | 20 | #### Author 21 | - **Name:** Robbe Van den Daele 22 | - **Github:** https://github.com/RobbeVandenDaele 23 | - **Twitter:** https://x.com/RobbeVdDaele 24 | - **LinkedIn:** https://www.linkedin.com/in/robbe-van-den-daele-677986190/ 25 | - **Website:** https://hybridbrothers.com/ 26 | 27 | #### References 28 | 29 | ## Defender XDR 30 | ```KQL 31 | 32 | let no_credguard_devices = ( 33 | ExposureGraphNodes 34 | // Get devices with credential guard misconfiguration 35 | | where array_length(NodeProperties.rawData.hasGuardMisconfigurations) > 0 36 | // Get interesting data 37 | | extend DeviceName = tostring(parse_json(NodeProperties)["rawData"]["deviceName"]), 38 | DeviceId = tostring(EntityIds.id) 39 | | extend DeviceName = iff(isempty(DeviceName), NodeName, DeviceName) 40 | // Search for distinct devices 41 | | distinct NodeId, DeviceName 42 | ); 43 | let critical_users = toscalar( 44 | // Search for critical users 45 | ExposureGraphNodes 46 | | where NodeLabel == "user" 47 | | extend CriticalityLevel = todynamic(NodeProperties).rawData.criticalityLevel.criticalityLevel 48 | | extend RuleNames = todynamic(NodeProperties).rawData.criticalityLevel.ruleNames 49 | | where CriticalityLevel == 0 50 | | distinct NodeName, NodeId, tostring(CriticalityLevel), tostring(RuleNames) 51 | | summarize make_set(NodeName) 52 | ); 53 | // Make graph for max of 3 edges, where we start from a device and end with an user 54 | ExposureGraphEdges 55 | | make-graph SourceNodeId --> TargetNodeId with ExposureGraphNodes on NodeId 56 | | graph-match (SourceNode)-[anyEdge*1..3]->(TargetNode) 57 | where SourceNode.NodeLabel in ("device", "microsoft.compute/virtualmachines") and TargetNode.NodeLabel == "user" and TargetNode.NodeName in ( critical_users ) 58 | project SourceNodeName = SourceNode.NodeName, 59 | SourceNodeId = SourceNode.NodeId, 60 | Edges = anyEdge.EdgeLabel, 61 | TargetNodeId = TargetNode.NodeId, 62 | TargetNodeName = TargetNode.NodeName, 63 | TargetNodeLabel = TargetNode.NodeLabel, 64 | TargetCriticalityLevel = TargetNode.NodeProperties.rawData.criticalityLevel.criticalityLevel, 65 | TargetRuleNames = TargetNode.NodeProperties.rawData.criticalityLevel.ruleNames 66 | | distinct SourceNodeId, SourceNodeName, TargetNodeId, TargetNodeName, tostring(TargetCriticalityLevel), tostring(TargetRuleNames) 67 | // Only return devices that does not have a TPM fully enabled 68 | | join kind=inner no_credguard_devices on $left.SourceNodeId == $right.NodeId 69 | // Make list of users per device 70 | | summarize UserList = make_list(TargetNodeName) by DeviceName 71 | // Count amount of exposed users per device 72 | | extend UserCount = array_length(UserList) 73 | | sort by UserCount desc 74 | ``` 75 | -------------------------------------------------------------------------------- /Defender For Endpoint/HuntDeviceDiscoverySubnetRanges.md: -------------------------------------------------------------------------------- 1 | # *Hunt Device Discovery Subnet Ranges* 2 | 3 | ## Query Information 4 | 5 | #### MITRE ATT&CK Technique(s) 6 | 7 | N/A 8 | 9 | #### Description 10 | This KQL query helps you identify which subnet ranges are behind the Microsoft Defender for Endpoint Device Discovery 'Monitored Networks' page. By using this query you can investigate if all of your corporate networks are being monitored and change monitored states effectivly. More information can be found in the references. 11 | 12 | #### Risk 13 | This query helps mitigating the risk that you do not perform Device Discovery on all monitored networks, which would result into an incomplete asset inventory list in Defender XDR. 14 | 15 | #### Author 16 | - **Name:** Robbe Van den Daele 17 | - **Github:** https://github.com/RobbeVandenDaele 18 | - **Twitter:** https://x.com/RobbeVdDaele 19 | - **LinkedIn:** https://www.linkedin.com/in/robbe-van-den-daele-677986190/ 20 | - **Website:** https://hybridbrothers.com/ 21 | 22 | #### References 23 | - https://hybridbrothers.com/mde-device-discovery-improving-the-monitored-network-page/ 24 | 25 | ## Defender XDR 26 | ```KQL 27 | // OPTIONAL - Device cap used to ignore network with less then X devices in them 28 | let device_cap = 0; 29 | DeviceNetworkInfo 30 | | where Timestamp > ago(7d) 31 | // Ignore empty networks 32 | | where ConnectedNetworks != "" 33 | // Get networks data 34 | | extend ConnectedNetworksExp = parse_json(ConnectedNetworks) 35 | | mv-expand bagexpansion = array ConnectedNetworks=ConnectedNetworksExp 36 | | extend NetworkName = tostring(ConnectedNetworks ["Name"]), NetworkDescription = tostring(ConnectedNetworks ["Description"]), NetworkCategory = tostring(ConnectedNetworks ["Category"]) 37 | // Get subnet data for IPv4 Addresses 38 | | extend IPAddressesExp = parse_json(IPAddresses) 39 | | mv-expand bagexpansion = array IPAddresses=IPAddressesExp 40 | | extend IPAddress = tostring(IPAddresses ["IPAddress"]), SubnetPrefix = tolong(IPAddresses ["SubnetPrefix"]) 41 | | extend NetworkAddress = format_ipv4(IPAddress, SubnetPrefix) 42 | | extend SubnetRange = strcat(NetworkAddress, "/", SubnetPrefix) 43 | // Exclude IPv6 and APIPPA 44 | | where SubnetPrefix <= 32 45 | | where IPAddress !startswith "169.254" 46 | // Ignore unidentified networks 47 | | where not(NetworkName has_any ("Unidentified", "Identifying...")) 48 | // Provide list 49 | | distinct DeviceId, NetworkName, IPv4Dhcp, SubnetRange 50 | | summarize Devices = count(), SubnetRanges = make_set(SubnetRange) by NetworkName, IPv4Dhcp 51 | // Ignore network with very low device count 52 | | where Devices >= device_cap 53 | | sort by Devices desc 54 | ``` 55 | 56 | ## Sentinel 57 | ```KQL 58 | // OPTIONAL - Device cap used to ignore network with less then X devices in them 59 | let device_cap = 0; 60 | DeviceNetworkInfo 61 | | where TimeGenerated > ago(7d) 62 | // Ignore empty networks 63 | | where ConnectedNetworks != "" 64 | // Get networks data 65 | | extend ConnectedNetworksExp = parse_json(ConnectedNetworks) 66 | | mv-expand bagexpansion = array ConnectedNetworks=ConnectedNetworksExp 67 | | extend NetworkName = tostring(ConnectedNetworks ["Name"]), NetworkDescription = tostring(ConnectedNetworks ["Description"]), NetworkCategory = tostring(ConnectedNetworks ["Category"]) 68 | // Get subnet data for IPv4 Addresses 69 | | extend IPAddressesExp = parse_json(IPAddresses) 70 | | mv-expand bagexpansion = array IPAddresses=IPAddressesExp 71 | | extend IPAddress = tostring(IPAddresses ["IPAddress"]), SubnetPrefix = tolong(IPAddresses ["SubnetPrefix"]) 72 | | extend NetworkAddress = format_ipv4(IPAddress, SubnetPrefix) 73 | | extend SubnetRange = strcat(NetworkAddress, "/", SubnetPrefix) 74 | // Exclude IPv6 and APIPPA 75 | | where SubnetPrefix <= 32 76 | | where IPAddress !startswith "169.254" 77 | // Ignore unidentified networks 78 | | where not(NetworkName has_any ("Unidentified", "Identifying...")) 79 | // Provide list 80 | | distinct DeviceId, NetworkName, IPv4Dhcp, SubnetRange 81 | | summarize Devices = count(), SubnetRanges = make_set(SubnetRange) by NetworkName, IPv4Dhcp 82 | // Ignore network with very low device count 83 | | where Devices >= device_cap 84 | | sort by Devices desc 85 | ``` -------------------------------------------------------------------------------- /Global Secure Access/HuntMdeWithGsaEvents.md: -------------------------------------------------------------------------------- 1 | # *Hunt MDE with GSA events* 2 | 3 | ## Query Information 4 | 5 | #### MITRE ATT&CK Technique(s) 6 | 7 | N/A 8 | 9 | #### Description 10 | This rule correlates the Microsoft Defender for Endpoint DeviceNetworkEvents table with the Global Secure Access NetworkAccessTraffic table. By doing this, you can enrich the MDE events which contains detailed process information with the GSA events that contains detailed HTTP header information and more. 11 | 12 | #### Risk 13 | With this query you can reduce FP rates of existing detections, and try to create more accurate new detections by combining MDE and GSA logs. 14 | 15 | #### Author 16 | - **Name:** Robbe Van den Daele 17 | - **Github:** https://github.com/RobbeVandenDaele 18 | - **Twitter:** https://x.com/RobbeVdDaele 19 | - **LinkedIn:** https://www.linkedin.com/in/robbe-van-den-daele-677986190/ 20 | - **Website:** https://hybridbrothers.com/ 21 | 22 | #### References 23 | - https://hybridbrothers.com/correlating-defender-for-endpoint-and-global-secure-access-logs/ 24 | 25 | ## Defender XDR 26 | ```KQL 27 | let gsa_events = NetworkAccessTraffic 28 | // Join DeviceInfo to get MDE DeviceID 29 | | join kind=inner ( 30 | DeviceInfo 31 | | distinct DeviceId, AadDeviceId 32 | ) on $left.DeviceId == $right.AadDeviceId 33 | // Remove Entra Device ID from GSA logs 34 | | project-away DeviceId 35 | // Rename MDE Device ID to DeviceId column 36 | | project-rename DeviceId = DeviceId1; 37 | // Get all MDE network events 38 | DeviceNetworkEvents 39 | // Get HTTP details if HTTP connection is logged 40 | | extend HttpStatus = toint(todynamic(AdditionalFields).status_code), 41 | BytesIn = toint(todynamic(AdditionalFields).response_body_len), 42 | BytesOut = toint(todynamic(AdditionalFields).request_body_len), 43 | HttpMethod = tostring(todynamic(AdditionalFields).method), 44 | UrlHostname = tostring(todynamic(AdditionalFields).host), 45 | UrlPath = tostring(todynamic(AdditionalFields).uri), 46 | UserAgent = tostring(todynamic(AdditionalFields).user_agent), 47 | HttpVersion = tostring(todynamic(AdditionalFields).version) 48 | // Join GSA logs 49 | | join kind=inner gsa_events on 50 | DeviceId, 51 | $left.RemoteUrl == $right.DestinationFqdn, 52 | $left.RemotePort == $right.DestinationPort, 53 | $left.Protocol == $right.TransportProtocol, 54 | $left.InitiatingProcessFileName == $right.InitiatingProcessName 55 | | project-rename TimeGeneratedGsa = TimeGenerated1, TimestampMde = Timestamp 56 | | project-away Type, TenantId, TimeGenerated, TenantId1, Type1, DeviceId1, AadDeviceId 57 | ``` 58 | 59 | ## Sentinel 60 | ```KQL 61 | let gsa_events = NetworkAccessTraffic 62 | // Join DeviceInfo to get MDE DeviceID 63 | | join kind=inner ( 64 | DeviceInfo 65 | | distinct DeviceId, AadDeviceId 66 | ) on $left.DeviceId == $right.AadDeviceId 67 | // Remove Entra Device ID from GSA logs 68 | | project-away DeviceId 69 | // Rename MDE Device ID to DeviceId column 70 | | project-rename DeviceId = DeviceId1; 71 | // Get all MDE network events 72 | DeviceNetworkEvents 73 | // Get HTTP details if HTTP connection is logged 74 | | extend HttpStatus = toint(todynamic(AdditionalFields).status_code), 75 | BytesIn = toint(todynamic(AdditionalFields).response_body_len), 76 | BytesOut = toint(todynamic(AdditionalFields).request_body_len), 77 | HttpMethod = tostring(todynamic(AdditionalFields).method), 78 | UrlHostname = tostring(todynamic(AdditionalFields).host), 79 | UrlPath = tostring(todynamic(AdditionalFields).uri), 80 | UserAgent = tostring(todynamic(AdditionalFields).user_agent), 81 | HttpVersion = tostring(todynamic(AdditionalFields).version) 82 | // Join GSA logs 83 | | join kind=inner gsa_events on 84 | DeviceId, 85 | $left.RemoteUrl == $right.DestinationFqdn, 86 | $left.RemotePort == $right.DestinationPort, 87 | $left.Protocol == $right.TransportProtocol, 88 | $left.InitiatingProcessFileName == $right.InitiatingProcessName 89 | | project-rename TimeGeneratedGsa = TimeGenerated2, TimestampMde = TimeGenerated 90 | | project-away Type, TenantId, TimeGenerated, TenantId1, Type1, DeviceId1, AadDeviceId 91 | ``` -------------------------------------------------------------------------------- /Exposure Management/HuntCriticalCredentialsOnNonTpmDevices.md: -------------------------------------------------------------------------------- 1 | # *Hunt for critical credentials on non-TPM enabled devices* 2 | 3 | ## Query Information 4 | 5 | #### MITRE ATT&CK Technique(s) 6 | 7 | | Technique ID | Title | Link | 8 | | --- | --- | --- | 9 | | TA0006 | Credential Access | https://attack.mitre.org/tactics/TA0006/ | 10 | 11 | 12 | #### Description 13 | This query searches for devices that does not have a TPM (Trusted Platform Module) enabled but contains critical credentials. The output shows for how many users each non-TPM device has credentials, together with the rules why each user is considered a critical user. 14 | 15 | 16 | #### Risk 17 | When critical credentials are stored on devices without a TPM enabled, it is more easy for adversaries to steal those credentials when the device is compromised. 18 | 19 | 20 | #### Author 21 | - **Name:** Robbe Van den Daele 22 | - **Github:** https://github.com/RobbeVandenDaele 23 | - **Twitter:** https://x.com/RobbeVdDaele 24 | - **LinkedIn:** https://www.linkedin.com/in/robbe-van-den-daele-677986190/ 25 | - **Website:** https://hybridbrothers.com/ 26 | 27 | #### References 28 | 29 | ## Defender XDR 30 | ```KQL 31 | let no_tpm_devices = ( 32 | ExposureGraphNodes 33 | // Get device nodes with their inventory ID 34 | | mv-expand EntityIds 35 | | where EntityIds.type == "DeviceInventoryId" 36 | // Get interesting properties 37 | | extend OnboardingStatus = tostring(parse_json(NodeProperties)["rawData"]["onboardingStatus"]), 38 | TpmSupported = tostring(parse_json(NodeProperties)["rawData"]["tpmData"]["supported"]), 39 | TpmEnabled = tostring(parse_json(NodeProperties)["rawData"]["tpmData"]["enabled"]), 40 | TpmActivated = tostring(parse_json(NodeProperties)["rawData"]["tpmData"]["activated"]), 41 | DeviceName = tostring(parse_json(NodeProperties)["rawData"]["deviceName"]), 42 | DeviceId = tostring(EntityIds.id) 43 | // Search for distinct devices 44 | | distinct NodeId, DeviceName, OnboardingStatus, TpmSupported, TpmEnabled, TpmActivated 45 | // Get device with no TPM enabled 46 | | where TpmSupported != "true" and TpmActivated != "true" and TpmEnabled != "true" 47 | | extend TpmSupported = iff(TpmSupported == "", "unknown", TpmSupported), 48 | TpmActivated = iff(TpmActivated == "", "unknown", TpmActivated), 49 | TpmEnabled = iff(TpmEnabled == "", "unknown", TpmEnabled) 50 | ); 51 | let critical_users = toscalar( 52 | // Search for critical users 53 | ExposureGraphNodes 54 | | where NodeLabel == "user" 55 | | extend CriticalityLevel = todynamic(NodeProperties).rawData.criticalityLevel.criticalityLevel 56 | | extend RuleNames = todynamic(NodeProperties).rawData.criticalityLevel.ruleNames 57 | | where CriticalityLevel == 0 58 | | distinct NodeName, NodeId, tostring(CriticalityLevel), tostring(RuleNames) 59 | | summarize make_set(NodeName) 60 | ); 61 | // Make graph for max of 3 edges, where we start from a device and end with an user 62 | ExposureGraphEdges 63 | | make-graph SourceNodeId --> TargetNodeId with ExposureGraphNodes on NodeId 64 | | graph-match (SourceNode)-[anyEdge*1..3]->(TargetNode) 65 | where SourceNode.NodeLabel in ("device", "microsoft.compute/virtualmachines") and TargetNode.NodeLabel == "user" and TargetNode.NodeName in ( critical_users ) 66 | project SourceNodeName = SourceNode.NodeName, 67 | SourceNodeId = SourceNode.NodeId, 68 | Edges = anyEdge.EdgeLabel, 69 | TargetNodeId = TargetNode.NodeId, 70 | TargetNodeName = TargetNode.NodeName, 71 | TargetNodeLabel = TargetNode.NodeLabel, 72 | TargetCriticalityLevel = TargetNode.NodeProperties.rawData.criticalityLevel.criticalityLevel, 73 | TargetRuleNames = TargetNode.NodeProperties.rawData.criticalityLevel.ruleNames 74 | | distinct SourceNodeId, SourceNodeName, TargetNodeId, TargetNodeName, tostring(TargetCriticalityLevel), tostring(TargetRuleNames) 75 | // Only return devices that does not have a TPM fully enabled 76 | | join kind=inner no_tpm_devices on $left.SourceNodeId == $right.NodeId 77 | // Make JSON of tpm data 78 | | extend TpmState = tostring(bag_pack( 79 | 'TpmSupported', TpmSupported, 80 | 'TpmEnabled', TpmEnabled, 81 | 'TpmActivated', TpmActivated 82 | )) 83 | // Make JSON of users data 84 | | extend Json = bag_pack( 85 | 'User', TargetNodeName, 86 | 'UserCriticalityLevel', TargetCriticalityLevel, 87 | 'UserRuleNames', TargetRuleNames 88 | ) 89 | // Make list of users per device 90 | | summarize UserList = make_list(Json) by DeviceName, OnboardingStatus, TpmState 91 | // Count amount of exposed users per device 92 | | extend UserCount = array_length(UserList) 93 | | sort by UserCount desc 94 | ``` -------------------------------------------------------------------------------- /Defender for Identity/HuntNnrHealthIssues.md: -------------------------------------------------------------------------------- 1 | # *Hunt for Defender for Identity NNR issues* 2 | 3 | ## Query Information 4 | 5 | #### MITRE ATT&CK Technique(s) 6 | 7 | N/A 8 | 9 | #### Description 10 | This query can help you in finding Network Name Resolution health issues of Microsoft Defender for Identity. NNR is a critical component which is used to get more information on IP addresses seen by MDI. Without NNR proparly working, MDI can throw a lot of False Positive alerts. 11 | 12 | #### Risk 13 | High False Negative detections by MDI. 14 | 15 | 16 | #### Author 17 | - **Name:** Robbe Van den Daele 18 | - **Github:** https://github.com/RobbeVandenDaele 19 | - **Twitter:** https://x.com/RobbeVdDaele 20 | - **LinkedIn:** https://www.linkedin.com/in/robbe-van-den-daele-677986190/ 21 | - **Website:** https://hybridbrothers.com/ 22 | 23 | #### References 24 | - https://hybridbrothers.com/mdi-nnr-health/ 25 | 26 | 27 | ## Defender XDR 28 | ```KQL 29 | let networks = DeviceNetworkInfo 30 | // Expand all IPs 31 | | mv-expand todynamic(IPAddresses) 32 | // Get the network address related to the IP 33 | | extend NetworkAddress = format_ipv4(tostring(IPAddresses.IPAddress), tolong(IPAddresses.SubnetPrefix)) 34 | // Build the IP with the CIDR notation 35 | | extend IPAddress = strcat(tostring(IPAddresses.IPAddress), "/", tolong(IPAddresses.SubnetPrefix)) 36 | // Save the Prefix as an extra property 37 | | extend Prefix = tostring(IPAddresses.SubnetPrefix) 38 | // Make a set of all the IP's belonging to the same subnet 39 | | summarize make_set(IPAddress) by NetworkAddress, Prefix 40 | // Count how many IPs there are in one subnet 41 | | extend CountIPs = array_length(set_IPAddress) 42 | | extend Joiner = 1; 43 | // Network Information 44 | let network_info = DeviceNetworkInfo 45 | | mv-expand todynamic(IPAddresses) 46 | | extend IPAddress = tostring(IPAddresses.IPAddress); 47 | // Ports used in NNR 48 | let nnr_ports = dynamic(["3389", "135", "137"]); 49 | let mdi_servers = dynamic([]); 50 | // Query network connections 51 | DeviceNetworkEvents 52 | // Get events from Defender for Identity sensors - fill in mdi-servers variable for more complete results 53 | | where InitiatingProcessFileName == "Microsoft.Tri.Sensor.exe" or DeviceName has_any (mdi_servers) 54 | // Check traffic for NNR ports 55 | | where RemotePort in (nnr_ports) 56 | // Join the network info for more destination context 57 | | join kind=inner network_info on $left.RemoteIP == $right.IPAddress 58 | // Get distinct values 59 | | project-rename RemoteDeviceName = DeviceName1 60 | | distinct DeviceName, ActionType, RemoteIP, RemotePort, RemoteDeviceName 61 | // Join all network addresses 62 | | extend Joiner = 1 63 | | join kind=inner networks on Joiner 64 | // Check if remote ip is in a certain network address 65 | | extend NetworkAddrPrefix = strcat(NetworkAddress, "/", Prefix) 66 | | where ipv4_is_in_range(RemoteIP, NetworkAddrPrefix) 67 | // Create Object to reuse later 68 | | extend Obj = pack( 69 | "DeviceName", DeviceName, 70 | "NetworkAddrPrefix", NetworkAddrPrefix, 71 | "RemotePort", RemotePort, 72 | "RemoteIP", RemoteIP 73 | ) 74 | // Count amount of failed and succeeded logins 75 | | summarize FailedConnections = countif(ActionType == "ConnectionFailed"), 76 | SucceededConnections = countif(ActionType == "ConnectionSuccess") by tostring(Obj) 77 | // Extract the columns from the object again 78 | | extend Obj = todynamic(Obj) 79 | // Save the properties for later use 80 | | extend DeviceName = tostring(Obj.DeviceName), 81 | NetworkAddrPrefix = tostring(Obj.NetworkAddrPrefix), 82 | RemotePort = tostring(Obj.RemotePort), 83 | RemoteIP = tostring(Obj.RemoteIP) 84 | // Create a new object to save the amount of failed and succeeded attempts per IP 85 | | extend Obj = pack( 86 | "RemoteIP", RemoteIP, 87 | "SucceededConnections", SucceededConnections, 88 | "FailedConnections", FailedConnections 89 | ) 90 | // Create a list of the remote ips and their connections by MDI sensor, destination subnet and RemoteIP 91 | // Subnets with only fails on both ports will fail in NNR 92 | | summarize ConnectionDetails = make_set(Obj), 93 | TotalSucceededConnections = sum(SucceededConnections), 94 | TotalFailedConnections = sum(FailedConnections) by DeviceName, NetworkAddrPrefix, RemotePort 95 | // Filter out /32 addresses 96 | | where NetworkAddrPrefix !contains "/32" 97 | // Sorting 98 | | sort by TotalFailedConnections desc 99 | // Reorder 100 | | project-reorder DeviceName, NetworkAddrPrefix, RemotePort, TotalSucceededConnections, TotalFailedConnections, ConnectionDetails 101 | ``` 102 | 103 | ## Sentinel 104 | ```KQL 105 | N/A 106 | ``` -------------------------------------------------------------------------------- /Defender For Endpoint/DetectServiceAccLoginOnNewDevice.md: -------------------------------------------------------------------------------- 1 | # *Detect service account login on new device* 2 | 3 | ## Query Information 4 | 5 | #### MITRE ATT&CK Technique(s) 6 | 7 | | Technique ID | Title | Link | 8 | | --- | --- | --- | 9 | | T1021.001 | Remote Services: Remote Desktop Protocol | https://attack.mitre.org/techniques/T1021/001/ | 10 | | T1021.002 | Remote Services: SMB/Windows Admin Shares | https://attack.mitre.org/techniques/T1021/002/ | 11 | | T1021.003 | Remote Services: Distributed Component Object Model | https://attack.mitre.org/techniques/T1021/003/ | 12 | | T1021.006 | Remote Services: Windows Remote Management | https://attack.mitre.org/techniques/T1021/006/ | 13 | 14 | #### Description 15 | This detection rule tries to flag suspicious logins on devices from service accounts, for which these service accounts did not login into those devices for the last 14 days. This might indicate that the service account is compromised and is being used for lateral movement into the environment. 16 | 17 | Most service accounts have a fairly static set of devices they authenticate to. Because of this, it is easier to flag deviations for service accounts compared to user accounts. However, some service accounts are known to dynamically log into devices based on observed events (susch as the MDI service accounts). Because of this some environment specific finetuning might be needed to reduce BP detections. 18 | 19 | #### Risk 20 | This detections tries to cover the risk of service account compromise being used for lateral movement. 21 | 22 | #### Author 23 | - **Name:** Robbe Van den Daele 24 | - **Github:** https://github.com/RobbeVandenDaele 25 | - **Twitter:** https://x.com/RobbeVdDaele 26 | - **LinkedIn:** https://www.linkedin.com/in/robbe-van-den-daele-677986190/ 27 | - **Website:** https://hybridbrothers.com/ 28 | 29 | #### References 30 | 31 | ## Defender XDR 32 | ```KQL 33 | // Get all enabled service accounts 34 | let service_acc = ( 35 | IdentityInfo 36 | | where Timestamp > ago(7d) 37 | | where Type == "ServiceAccount" and IsAccountEnabled == 1 38 | | distinct AccountName = tolower(AccountName) 39 | ); 40 | // Get the history service account logins 41 | let historic_events = ( 42 | DeviceLogonEvents 43 | | where Timestamp between (ago(14d) .. ago(1h)) 44 | | where ActionType == "LogonSuccess" 45 | | extend AccountName = tolower(AccountName) 46 | | join kind=inner service_acc on AccountName 47 | | summarize HistoricLogins = make_set(DeviceName) by AccountName 48 | ); 49 | // Get the account logins done over Network 50 | DeviceLogonEvents 51 | | where Timestamp > ago(1h) 52 | | where LogonType == "Network" 53 | | where ActionType == "LogonSuccess" 54 | | extend AccountName = tolower(AccountName) 55 | // Join inner to only get known service account logins 56 | | join kind=inner service_acc on AccountName 57 | // Join inner to get a list of the historic device logins for the service accounts 58 | | join kind=inner historic_events on AccountName 59 | // Only get sign-ins where Device is not in the history logins 60 | | extend HistoricLogins = tostring(HistoricLogins) 61 | | where HistoricLogins !contains DeviceName 62 | // Make output better 63 | | project-away AccountName1, AccountName2 64 | // Exclude MDI Service Account - CHANGE IF DIFFERENT FOR YOUR ORG 65 | | where AccountName != "gsma_mdi$" 66 | // Environment specific finetuning - begin 67 | // Environment specific finetuning - end 68 | ``` 69 | 70 | ## Sentinel 71 | ```KQL 72 | // Get all enabled service accounts 73 | let service_acc = ( 74 | IdentityInfo 75 | | where TimeGenerated > ago(7d) 76 | | where Type == "ServiceAccount" and IsAccountEnabled == 1 77 | | distinct AccountName = tolower(AccountName) 78 | ); 79 | // Get the history service account logins 80 | let historic_events = ( 81 | DeviceLogonEvents 82 | | where TimeGenerated between (ago(14d) .. ago(1h)) 83 | | where ActionType == "LogonSuccess" 84 | | extend AccountName = tolower(AccountName) 85 | | join kind=inner service_acc on AccountName 86 | | summarize HistoricLogins = make_set(DeviceName) by AccountName 87 | ); 88 | // Get the account logins done over Network 89 | DeviceLogonEvents 90 | | where TimeGenerated > ago(1h) 91 | | where LogonType == "Network" 92 | | where ActionType == "LogonSuccess" 93 | | extend AccountName = tolower(AccountName) 94 | // Join inner to only get known service account logins 95 | | join kind=inner service_acc on AccountName 96 | // Join inner to get a list of the historic device logins for the service accounts 97 | | join kind=inner historic_events on AccountName 98 | // Only get sign-ins where Device is not in the history logins 99 | | extend HistoricLogins = tostring(HistoricLogins) 100 | | where HistoricLogins !contains DeviceName 101 | // Make output better 102 | | project-away AccountName1, AccountName2 103 | // Exclude MDI Service Account - CHANGE IF DIFFERENT FOR YOUR ORG 104 | | where AccountName != "gsma_mdi$" 105 | // Environment specific finetuning - begin 106 | // Environment specific finetuning - end 107 | ``` -------------------------------------------------------------------------------- /Defender for Office365/DetectDirectSendPhishingEmails.md: -------------------------------------------------------------------------------- 1 | # *Detect Direct Send phishing emails* 2 | 3 | ## Query Information 4 | 5 | #### MITRE ATT&CK Technique(s) 6 | 7 | | Technique ID | Title | Link | 8 | | --- | --- | --- | 9 | | T1566 | Phishing | https://attack.mitre.org/techniques/T1566/ | 10 | 11 | #### Description 12 | In May 2025, a campaign started using the Microsoft Exchange Direct Send feature to send phishing mails to organizations. This feature is designed for use by printers, scanners, cloud services and other devices that need to send messages on behalf of the company. This detection rule tries to detect the malicious mails being send via Direct Send using a couple of indicators: 13 | 14 | - Sender email is the same as Recipient email 15 | - SPF and DMARC both failed 16 | - Mail is not comming in via an Exchange connector 17 | - The sender IP address country is not a country that the user is known to login from 18 | 19 | FYI, if you remove the country check (last line of the query), you can use the query to identify if there are legitimate mails as well. If not, you can disable the Direct Send feature in Exchange Online. 20 | 21 | 22 | #### Risk 23 | This detection tries to detect the Direct Send phsihing mails when you cannot disable Direct Send, especially when they are not being blocked by MDO. This allows you to ZAP the mails manually after delivery. 24 | 25 | #### Author 26 | - **Name:** Robbe Van den Daele 27 | - **Github:** https://github.com/RobbeVandenDaele 28 | - **Twitter:** https://x.com/RobbeVdDaele 29 | - **LinkedIn:** https://www.linkedin.com/in/robbe-van-den-daele-677986190/ 30 | - **Website:** https://hybridbrothers.com/ 31 | 32 | #### References 33 | - https://www.bleepingcomputer.com/news/security/microsoft-365-direct-send-abused-to-send-phishing-as-internal-users/ 34 | - https://www.jumpsec.com/guides/microsoft-direct-send-phishing-abuse-primitive/ 35 | - https://techcommunity.microsoft.com/blog/exchange/introducing-more-control-over-direct-send-in-exchange-online/4408790?WT.mc_id=M365-MVP-9501 36 | 37 | ## Defender XDR 38 | ```KQL 39 | let country_land_codes = ( 40 | externaldata (Country:string,Alpha2:string,Alpha3:string,CountryCode:string,Iso3166:string,Region:string,SubRegion:string,IntermediateRegion:string,RegionCode:string,SubRegionCode:string,IntermediateRegionCode:string) 41 | ['https://raw.githubusercontent.com/lukes/ISO-3166-Countries-with-Regional-Codes/refs/heads/master/all/all.csv'] with (format='csv', ignoreFirstRecord=true) 42 | | distinct Country, Alpha2 43 | ); 44 | let login_locations = ( 45 | SigninLogs 46 | | where TimeGenerated > ago(7d) 47 | | where ResultSignature == 0 48 | | distinct UserId, UserPrincipalName, LandCode = tostring(LocationDetails.countryOrRegion) 49 | | where isnotempty(LandCode) 50 | | join kind=inner country_land_codes on $left.LandCode == $right.Alpha2 51 | | summarize UsageCountries = make_set(Country) by UserId, UserPrincipalName 52 | ); 53 | EmailEvents 54 | | where TimeGenerated > ago(30d) 55 | // Find Direct Send mails 56 | | where SenderMailFromAddress == RecipientEmailAddress 57 | | extend AuthenticationDetails = parse_json(AuthenticationDetails) 58 | | where AuthenticationDetails.SPF == "fail" and AuthenticationDetails.DMARC == "fail" 59 | // Only flag mails not comming in via an exchange connector (these are legit mails) 60 | | where isempty(Connectors) 61 | // Exclude if detected as phish (since it is already remediated then) 62 | | where DeliveryAction !in ("Junked", "Blocked") 63 | | extend Location = parse_json(geo_info_from_ip_address(SenderIPv4)) 64 | | extend MailFromCountry = tostring(Location.country) 65 | // Find the usage countries of the users 66 | | join kind=leftouter login_locations on $left.SenderObjectId == $right.UserId 67 | // Flag when sender country is not a usage country of the user 68 | | where tostring(UsageCountries) !contains MailFromCountry 69 | ``` 70 | 71 | ## Sentinel 72 | ```KQL 73 | let country_land_codes = ( 74 | externaldata (Country:string,Alpha2:string,Alpha3:string,CountryCode:string,Iso3166:string,Region:string,SubRegion:string,IntermediateRegion:string,RegionCode:string,SubRegionCode:string,IntermediateRegionCode:string) 75 | ['https://raw.githubusercontent.com/lukes/ISO-3166-Countries-with-Regional-Codes/refs/heads/master/all/all.csv'] with (format='csv', ignoreFirstRecord=true) 76 | | distinct Country, Alpha2 77 | ); 78 | let login_locations = ( 79 | SigninLogs 80 | | where TimeGenerated > ago(7d) 81 | | where ResultSignature == 0 82 | | distinct UserId, UserPrincipalName, LandCode = tostring(LocationDetails.countryOrRegion) 83 | | where isnotempty(LandCode) 84 | | join kind=inner country_land_codes on $left.LandCode == $right.Alpha2 85 | | summarize UsageCountries = make_set(Country) by UserId, UserPrincipalName 86 | ); 87 | EmailEvents 88 | | where TimeGenerated > ago(30d) 89 | // Find Direct Send mails 90 | | where SenderMailFromAddress == RecipientEmailAddress 91 | | extend AuthenticationDetails = parse_json(AuthenticationDetails) 92 | | where AuthenticationDetails.SPF == "fail" and AuthenticationDetails.DMARC == "fail" 93 | // Only flag mails not comming in via an exchange connector (these are legit mails) 94 | | where isempty(Connectors) 95 | // Exclude if detected as phish (since it is already remediated then) 96 | | where DeliveryAction !in ("Junked", "Blocked") 97 | | extend Location = parse_json(geo_info_from_ip_address(SenderIPv4)) 98 | | extend MailFromCountry = tostring(Location.country) 99 | // Find the usage countries of the users 100 | | join kind=leftouter login_locations on $left.SenderObjectId == $right.UserId 101 | // Flag when sender country is not a usage country of the user 102 | | where tostring(UsageCountries) !contains MailFromCountry 103 | ``` -------------------------------------------------------------------------------- /Entra ID/DetectChangesToConnectSyncApplication.md: -------------------------------------------------------------------------------- 1 | # *Detect changes to Connect Sync Application* 2 | 3 | ## Query Information 4 | 5 | #### MITRE ATT&CK Technique(s) 6 | 7 | | Technique ID | Title | Link | 8 | | --- | --- | --- | 9 | | T1098 | Account Manipulation | https://attack.mitre.org/techniques/T1098/ | 10 | | T1078.004 | Valid Accounts: Cloud Accounts | https://attack.mitre.org/techniques/T1078/004/ | 11 | | T1556.007 | Modify Authentication Process: Hybrid Identity | https://attack.mitre.org/techniques/T1556/007/ | 12 | 13 | #### Description 14 | This detection flags any changes happening on the Connect Sync Application in Entra ID. Since this is a very interesting account for attackers to abuse when moving laterally between AD DS and Entra ID, any change to the account should be investigated. We try to exclude legitimate certificate renewal processes, and a new account onboarding in the detection rule. 15 | 16 | #### Risk 17 | This detection tries to cover the risk of an attacker trying to manipulate the Connect Sync Application. 18 | 19 | #### Author 20 | - **Name:** Robbe Van den Daele 21 | - **Github:** https://github.com/RobbeVandenDaele 22 | - **Twitter:** https://x.com/RobbeVdDaele 23 | - **LinkedIn:** https://www.linkedin.com/in/robbe-van-den-daele-677986190/ 24 | - **Website:** https://hybridbrothers.com/ 25 | 26 | #### References 27 | - https://specterops.io/blog/2025/06/09/update-dumping-entra-connect-sync-credentials/ 28 | 29 | ## Defender XDR 30 | ```KQL 31 | // Flag everything except a renewal process and onboarding 32 | let base = materialize ( 33 | AuditLogs 34 | // Search events happening on the Sync Account 35 | | extend AppName = tostring(TargetResources[0].displayName) 36 | | where AppName startswith "ConnectSyncProvisioning_" 37 | // Only get cretificate or secret changes 38 | | where OperationName contains "Update application – Certificates and secrets management" 39 | // Expand the target resources and modified properties, and only use events ralted to KeyDescription 40 | | mv-expand TargetResources 41 | | extend ModifiedProperties = TargetResources.modifiedProperties 42 | | mv-expand ModifiedProperties 43 | | where ModifiedProperties.displayName == "KeyDescription" 44 | // Save the old and new values of the secrets on the application 45 | | extend OldValue = tostring(ModifiedProperties.oldValue), 46 | NewValue = tostring(ModifiedProperties.newValue) 47 | // Save the old and new credential names in an array 48 | | extend OldCredentialNames = extract_all(@"DisplayName=([^\]]+)", dynamic([1]), OldValue), 49 | NewCredentialNames = extract_all(@"DisplayName=([^\]]+)", dynamic([1]), NewValue) 50 | ); 51 | let newCredsAdd = ( 52 | base 53 | // Flag when there are more new credentials than old ones 54 | | where array_length(OldCredentialNames) < array_length(NewCredentialNames) 55 | | project-rename NewCredAddTimeGenerated = TimeGenerated 56 | ); 57 | let credRemove = ( 58 | base 59 | // Get events where credentials are being removed 60 | | where array_length(OldCredentialNames) > array_length(NewCredentialNames) 61 | | project-rename CredRemoveTimeGenerated = TimeGenerated 62 | ); 63 | let credRenewal = ( 64 | // Find legitimate credential renewals 65 | newCredsAdd 66 | | join kind=leftouter credRemove on AppName 67 | | where CredRemoveTimeGenerated - NewCredAddTimeGenerated <= 1m 68 | ); 69 | AuditLogs 70 | | extend AppName = tostring(TargetResources[0].displayName) 71 | | where AppName startswith "ConnectSyncProvisioning_" 72 | | join kind=leftanti credRenewal on CorrelationId 73 | // Exclude cred removes (duplicate / not interesting) 74 | | join kind=leftanti credRemove on CorrelationId 75 | // Exclude new deployment 76 | | where OperationName !in ("Add service principal", "Add application") 77 | ``` 78 | 79 | ## Sentinel 80 | ```KQL 81 | // Flag everything except a renewal process and onboarding 82 | let base = materialize ( 83 | AuditLogs 84 | // Search events happening on the Sync Account 85 | | extend AppName = tostring(TargetResources[0].displayName) 86 | | where AppName startswith "ConnectSyncProvisioning_" 87 | // Only get cretificate or secret changes 88 | | where OperationName contains "Update application – Certificates and secrets management" 89 | // Expand the target resources and modified properties, and only use events ralted to KeyDescription 90 | | mv-expand TargetResources 91 | | extend ModifiedProperties = TargetResources.modifiedProperties 92 | | mv-expand ModifiedProperties 93 | | where ModifiedProperties.displayName == "KeyDescription" 94 | // Save the old and new values of the secrets on the application 95 | | extend OldValue = tostring(ModifiedProperties.oldValue), 96 | NewValue = tostring(ModifiedProperties.newValue) 97 | // Save the old and new credential names in an array 98 | | extend OldCredentialNames = extract_all(@"DisplayName=([^\]]+)", dynamic([1]), OldValue), 99 | NewCredentialNames = extract_all(@"DisplayName=([^\]]+)", dynamic([1]), NewValue) 100 | ); 101 | let newCredsAdd = ( 102 | base 103 | // Flag when there are more new credentials than old ones 104 | | where array_length(OldCredentialNames) < array_length(NewCredentialNames) 105 | | project-rename NewCredAddTimeGenerated = TimeGenerated 106 | ); 107 | let credRemove = ( 108 | base 109 | // Get events where credentials are being removed 110 | | where array_length(OldCredentialNames) > array_length(NewCredentialNames) 111 | | project-rename CredRemoveTimeGenerated = TimeGenerated 112 | ); 113 | let credRenewal = ( 114 | // Find legitimate credential renewals 115 | newCredsAdd 116 | | join kind=leftouter credRemove on AppName 117 | | where CredRemoveTimeGenerated - NewCredAddTimeGenerated <= 1m 118 | ); 119 | AuditLogs 120 | | extend AppName = tostring(TargetResources[0].displayName) 121 | | where AppName startswith "ConnectSyncProvisioning_" 122 | | join kind=leftanti credRenewal on CorrelationId 123 | // Exclude cred removes (duplicate / not interesting) 124 | | join kind=leftanti credRemove on CorrelationId 125 | // Exclude new deployment 126 | | where OperationName !in ("Add service principal", "Add application") 127 | ``` -------------------------------------------------------------------------------- /Entra ID/DetectCredAddToConnectSyncApplication.md: -------------------------------------------------------------------------------- 1 | # *Detect credential add to Connect Sync Application* 2 | 3 | ## Query Information 4 | 5 | #### MITRE ATT&CK Technique(s) 6 | 7 | | Technique ID | Title | Link | 8 | | --- | --- | --- | 9 | | T1098 | Account Manipulation | https://attack.mitre.org/techniques/T1098/ | 10 | | T1098.001 | Account Manipulation: Additional Cloud Credentials | https://attack.mitre.org/techniques/T1098/001/ | 11 | | T1078.004 | Valid Accounts: Cloud Accounts | https://attack.mitre.org/techniques/T1078/004/ | 12 | | T1556.007 | Modify Authentication Process: Hybrid Identity | https://attack.mitre.org/techniques/T1556/007/ | 13 | 14 | #### Description 15 | This detection specifically flags credentials being added to the Connect Sync Application in Entra ID, a technique known to have persistance from On-premise AD DS to Entra ID. It tries to look at both certificate, client secret, and federated credentials being added, and tries to remove legitimate renewal processes. Since the legitimate renewal process first adds a new certificate only te remove the old one short after, we by default allow for a maximum of 1 minute between the certificate create and delete events. 16 | 17 | #### Risk 18 | This detection tries to cover the risk of an attacker trying to persist access to Entra ID via a compromised Entra ID Connector account, as exlplained in the SpecterOps blogpost which can be found in the references. 19 | 20 | 21 | #### Author 22 | - **Name:** Robbe Van den Daele 23 | - **Github:** https://github.com/RobbeVandenDaele 24 | - **Twitter:** https://x.com/RobbeVdDaele 25 | - **LinkedIn:** https://www.linkedin.com/in/robbe-van-den-daele-677986190/ 26 | - **Website:** https://hybridbrothers.com/ 27 | 28 | #### References 29 | - https://specterops.io/blog/2025/06/09/update-dumping-entra-connect-sync-credentials/ 30 | 31 | ## Defender XDR 32 | ```KQL 33 | // Flag adding credential that does not look like a renewal 34 | let base = materialize ( 35 | AuditLogs 36 | // Search events happening on the Sync Account 37 | | extend AppName = tostring(TargetResources[0].displayName) 38 | | where AppName startswith "ConnectSyncProvisioning_" 39 | // Only get cretificate or secret changes 40 | | where OperationName has_any ("Add service principal", "Certificates and secrets management", "Update application") 41 | // Expand the target resources and modified properties, and only use events ralted to KeyDescription 42 | | mv-expand TargetResources 43 | | extend ModifiedProperties = TargetResources.modifiedProperties 44 | | mv-expand ModifiedProperties 45 | | where ModifiedProperties.displayName == "KeyDescription" 46 | // Save the old and new values of the secrets on the application 47 | | extend OldValue = tostring(ModifiedProperties.oldValue), 48 | NewValue = tostring(ModifiedProperties.newValue) 49 | // Save the old and new credential names in an array 50 | | extend OldCredentialNames = extract_all(@"DisplayName=([^\]]+)", dynamic([1]), OldValue), 51 | NewCredentialNames = extract_all(@"DisplayName=([^\]]+)", dynamic([1]), NewValue) 52 | ); 53 | let newCredsAdd = ( 54 | base 55 | // Flag when there are more new credentials than old ones 56 | | where array_length(OldCredentialNames) < array_length(NewCredentialNames) 57 | | project-rename NewCredAddTimeGenerated = TimeGenerated 58 | ); 59 | let credRemove = ( 60 | base 61 | // Get events where credentials are being removed 62 | | where array_length(OldCredentialNames) > array_length(NewCredentialNames) 63 | | project-rename CredRemoveTimeGenerated = TimeGenerated 64 | ); 65 | // Only flag when credentials are added without another being removed for the same application within a small time window 66 | // This excludes a normal renewal process 67 | newCredsAdd 68 | | join kind=leftouter credRemove on AppName 69 | | where CredRemoveTimeGenerated - NewCredAddTimeGenerated > 1m 70 | | project NewCredAddTimeGenerated, CredRemoveTimeGenerated, OperationName, AdditionalDetails, InitiatedBy, AppName, ModifiedProperties, OldCredentialNames, NewCredentialNames 71 | ``` 72 | 73 | ## Sentinel 74 | ```KQL 75 | // Flag adding credential that does not look like a renewal 76 | let base = materialize ( 77 | AuditLogs 78 | // Search events happening on the Sync Account 79 | | extend AppName = tostring(TargetResources[0].displayName) 80 | | where AppName startswith "ConnectSyncProvisioning_" 81 | // Only get cretificate or secret changes 82 | | where OperationName has_any ("Add service principal", "Certificates and secrets management", "Update application") 83 | // Expand the target resources and modified properties, and only use events ralted to KeyDescription 84 | | mv-expand TargetResources 85 | | extend ModifiedProperties = TargetResources.modifiedProperties 86 | | mv-expand ModifiedProperties 87 | | where ModifiedProperties.displayName == "KeyDescription" 88 | // Save the old and new values of the secrets on the application 89 | | extend OldValue = tostring(ModifiedProperties.oldValue), 90 | NewValue = tostring(ModifiedProperties.newValue) 91 | // Save the old and new credential names in an array 92 | | extend OldCredentialNames = extract_all(@"DisplayName=([^\]]+)", dynamic([1]), OldValue), 93 | NewCredentialNames = extract_all(@"DisplayName=([^\]]+)", dynamic([1]), NewValue) 94 | ); 95 | let newCredsAdd = ( 96 | base 97 | // Flag when there are more new credentials than old ones 98 | | where array_length(OldCredentialNames) < array_length(NewCredentialNames) 99 | | project-rename NewCredAddTimeGenerated = TimeGenerated 100 | ); 101 | let credRemove = ( 102 | base 103 | // Get events where credentials are being removed 104 | | where array_length(OldCredentialNames) > array_length(NewCredentialNames) 105 | | project-rename CredRemoveTimeGenerated = TimeGenerated 106 | ); 107 | // Only flag when credentials are added without another being removed for the same application within a small time window 108 | // This excludes a normal renewal process 109 | newCredsAdd 110 | | join kind=leftouter credRemove on AppName 111 | | where CredRemoveTimeGenerated - NewCredAddTimeGenerated > 1m 112 | | project NewCredAddTimeGenerated, CredRemoveTimeGenerated, OperationName, AdditionalDetails, InitiatedBy, AppName, ModifiedProperties, OldCredentialNames, NewCredentialNames 113 | ``` 114 | -------------------------------------------------------------------------------- /Defender For Endpoint/HuntDevicesDoingRdpToNonTpmDevice.md: -------------------------------------------------------------------------------- 1 | # *Hunt for RDP sessions to unmanaged and non TPM devices* 2 | 3 | ## Query Information 4 | 5 | #### MITRE ATT&CK Technique(s) 6 | 7 | | Technique ID | Title | Link | 8 | | --- | --- | --- | 9 | | T1021.001 | Remote Services: Remote Desktop Protocol | https://attack.mitre.org/techniques/T1021/001/ | 10 | 11 | #### Description 12 | This query can help you find devices performing RDP sessions to unmanaged or non-TPM protected devices. 13 | 14 | #### Risk 15 | This can be a risk to expose Windows Hello for Business credentials when Windows Hello for Business is being used as authentication method for the RDP session, since the keys will not be protected on the destination device by a TPM. 16 | 17 | #### Author 18 | - **Name:** Robbe Van den Daele 19 | - **Github:** https://github.com/RobbeVandenDaele 20 | - **Twitter:** https://x.com/RobbeVdDaele 21 | - **LinkedIn:** https://www.linkedin.com/in/robbe-van-den-daele-677986190/ 22 | - **Website:** https://hybridbrothers.com/ 23 | 24 | #### References 25 | - https://hybridbrothers.com/detecting-non-privileged-windows-hello-abuse/ 26 | 27 | ## Defender XDR 28 | ```KQL 29 | let no_tpm_devices = ( 30 | ExposureGraphNodes 31 | // Get device nodes with their inventory ID 32 | | where NodeLabel == "device" 33 | | mv-expand EntityIds 34 | | where EntityIds.type == "DeviceInventoryId" 35 | // Get interesting properties 36 | | extend OnboardingStatus = tostring(parse_json(NodeProperties)["rawData"]["onboardingStatus"]), 37 | TpmSupported = tostring(parse_json(NodeProperties)["rawData"]["tpmData"]["supported"]), 38 | TpmEnabled = tostring(parse_json(NodeProperties)["rawData"]["tpmData"]["enabled"]), 39 | TpmActivated = tostring(parse_json(NodeProperties)["rawData"]["tpmData"]["activated"]), 40 | DeviceName = tostring(parse_json(NodeProperties)["rawData"]["deviceName"]), 41 | DeviceId = tostring(EntityIds.id) 42 | // Search for distinct devices 43 | | distinct DeviceId, DeviceName, OnboardingStatus, TpmSupported, TpmEnabled, TpmActivated 44 | // Get Unmanaged devices and device not supporting a TPM 45 | | where OnboardingStatus != "Onboarded" or (TpmSupported != "true" and TpmActivated != "true" and TpmEnabled != "true") 46 | | extend TpmSupported = iff(TpmSupported == "", "unknown", TpmSupported), 47 | TpmActivated = iff(TpmActivated == "", "unknown", TpmActivated), 48 | TpmEnabled = iff(TpmEnabled == "", "unknown", TpmEnabled) 49 | ); 50 | let no_tpm_device_info = ( 51 | DeviceNetworkInfo 52 | | where Timestamp > ago(7d) 53 | // Get latest network info for each device ID 54 | | summarize arg_max(Timestamp, *) by DeviceId 55 | | mv-expand todynamic(IPAddresses) 56 | | extend IPAddress = tostring(IPAddresses.IPAddress) 57 | // Find no TPM devices and join with their network information 58 | | join kind=inner no_tpm_devices on DeviceId 59 | | project DeviceId, DeviceName, MacAddress, IPAddress, OnboardingStatus, TpmActivated, TpmEnabled, TpmSupported 60 | ); 61 | DeviceNetworkEvents 62 | // Search for RDP connections to non-tpm devices 63 | | where Timestamp > ago(1h) 64 | | where ActionType == "ConnectionSuccess" 65 | | where RemotePort == 3389 66 | // Exclude MDI RDP Connections (known for NNR) 67 | | where InitiatingProcessFileName !~ "microsoft.tri.sensor.exe" 68 | | join kind=inner no_tpm_device_info on $left.RemoteIP == $right.IPAddress 69 | | project-rename RemoteDeviceId = DeviceId1, RemoteDeviceName = DeviceName1, RemoteMacAddress = MacAddress, RemoteDeviceOnboardingStatus = OnboardingStatus, RemoteDeviceTpmActivated = TpmActivated, RemoteDeviceTpmEnabled = TpmEnabled, RemoteDeviceTpmSupported = TpmSupported 70 | | project-away IPAddress 71 | ``` 72 | 73 | ## Sentinel 74 | ```KQL 75 | let no_tpm_devices = ( 76 | ExposureGraphNodes 77 | // Get device nodes with their inventory ID 78 | | where NodeLabel == "device" 79 | | mv-expand EntityIds 80 | | where EntityIds.type == "DeviceInventoryId" 81 | // Get interesting properties 82 | | extend OnboardingStatus = tostring(parse_json(NodeProperties)["rawData"]["onboardingStatus"]), 83 | TpmSupported = tostring(parse_json(NodeProperties)["rawData"]["tpmData"]["supported"]), 84 | TpmEnabled = tostring(parse_json(NodeProperties)["rawData"]["tpmData"]["enabled"]), 85 | TpmActivated = tostring(parse_json(NodeProperties)["rawData"]["tpmData"]["activated"]), 86 | DeviceName = tostring(parse_json(NodeProperties)["rawData"]["deviceName"]), 87 | DeviceId = tostring(EntityIds.id) 88 | // Search for distinct devices 89 | | distinct DeviceId, DeviceName, OnboardingStatus, TpmSupported, TpmEnabled, TpmActivated 90 | // Get Unmanaged devices and device not supporting a TPM 91 | | where OnboardingStatus != "Onboarded" or (TpmSupported != "true" and TpmActivated != "true" and TpmEnabled != "true") 92 | | extend TpmSupported = iff(TpmSupported == "", "unknown", TpmSupported), 93 | TpmActivated = iff(TpmActivated == "", "unknown", TpmActivated), 94 | TpmEnabled = iff(TpmEnabled == "", "unknown", TpmEnabled) 95 | ); 96 | let no_tpm_device_info = ( 97 | DeviceNetworkInfo 98 | | where TimeGenerated > ago(7d) 99 | // Get latest network info for each device ID 100 | | summarize arg_max(TimeGenerated, *) by DeviceId 101 | | mv-expand todynamic(IPAddresses) 102 | | extend IPAddress = tostring(IPAddresses.IPAddress) 103 | // Find no TPM devices and join with their network information 104 | | join kind=inner no_tpm_devices on DeviceId 105 | | project DeviceId, DeviceName, MacAddress, IPAddress, OnboardingStatus, TpmActivated, TpmEnabled, TpmSupported 106 | ); 107 | DeviceNetworkEvents 108 | // Search for RDP connections to non-tpm devices 109 | | where TimeGenerated > ago(1h) 110 | | where ActionType == "ConnectionSuccess" 111 | | where RemotePort == 3389 112 | // Exclude MDI RDP Connections (known for NNR) 113 | | where InitiatingProcessFileName !~ "microsoft.tri.sensor.exe" 114 | | join kind=inner no_tpm_device_info on $left.RemoteIP == $right.IPAddress 115 | | project-rename RemoteDeviceId = DeviceId1, RemoteDeviceName = DeviceName1, RemoteMacAddress = MacAddress, RemoteDeviceOnboardingStatus = OnboardingStatus, RemoteDeviceTpmActivated = TpmActivated, RemoteDeviceTpmEnabled = TpmEnabled, RemoteDeviceTpmSupported = TpmSupported 116 | | project-away IPAddress 117 | ``` -------------------------------------------------------------------------------- /Exposure Management/HuntPublicRemotlyExploitableDevicesWithHighEPSS.md: -------------------------------------------------------------------------------- 1 | # *Hunt for public remotly exploitable devices (with high EPSS)* 2 | 3 | ## Query Information 4 | 5 | #### MITRE ATT&CK Technique(s) 6 | 7 | | Technique ID | Title | Link | 8 | | --- | --- | --- | 9 | | T1190 | Exploit Public-Facing Application | https://attack.mitre.org/techniques/T1190 | 10 | 11 | 12 | #### Description 13 | This query searches for devices that comply with the following criteria: 14 | - Incomming connections from public IP addresses in last 7 days (internet exposed) 15 | - High or Critical severity CVE's 16 | - CVE's must have known exploits 17 | - CVE's are remotely exploitable over the network 18 | - No user interaction is required to exploit the CVE's 19 | - EPSS score of CVE must by above 10% (likelihood of exploitation) 20 | 21 | > If devices are placed behind a proxy, they will not be returned in this query by default 22 | 23 | 24 | #### Risk 25 | Devices that return from these results are possible high-risk devices that could be exploited successfully any time. 26 | 27 | 28 | #### Author 29 | - **Name:** Robbe Van den Daele 30 | - **Github:** https://github.com/RobbeVandenDaele 31 | - **Twitter:** https://x.com/RobbeVdDaele 32 | - **LinkedIn:** https://www.linkedin.com/in/robbe-van-den-daele-677986190/ 33 | - **Website:** https://hybridbrothers.com/ 34 | 35 | #### References 36 | 37 | ## Defender XDR 38 | ```KQL 39 | // Flag remotly exploitable, no user interaction, CVE's with a EPSS score above a certain threshold (likelyhood of exploitation) 40 | // For devices with incomming public connections 41 | // See efficiency research on https://www.first.org/epss/model 42 | let epss_threshold = 0.1; 43 | let exploit_statusses = dynamic(["ExploitIsPublic","ExploitIsInKit","ExploitIsVerified"]); 44 | // Xspm base query we materialize since we need these results multiple times 45 | let xspm_base = materialize ( 46 | ExposureGraphNodes 47 | // Get device nodes with their inventory ID 48 | | mv-expand EntityIds 49 | | where EntityIds.type == "DeviceInventoryId" 50 | // Get first important properties 51 | | extend DeviceId = tostring(parse_json(EntityIds)["id"]), 52 | ExposureScore = tostring(parse_json(NodeProperties)["rawData"]["exposureScore"]), 53 | HasHighOrCriticalCve = tostring(parse_json(NodeProperties)["rawData"]["highRiskVulnerabilityInsights"]["hasHighOrCritical"]) 54 | // Focus on devices with high exposure 55 | | where ExposureScore == "High" 56 | // Get vulnerability exploit information 57 | | extend RceExploitLevels = parse_json(NodeProperties)["rawData"]["highRiskVulnerabilityInsights"]["vulnerableToRemoteCodeExecution"]["explotabilityLevels"] 58 | | extend PrivEscExploitLevels = parse_json(NodeProperties)["rawData"]["highRiskVulnerabilityInsights"]["vulnerableToPrivilegeEscalation"]["explotabilityLevels"] 59 | // Focus on devices where cve has known epxloits 60 | | where RceExploitLevels has_any (exploit_statusses) or PrivEscExploitLevels has_any (exploit_statusses) 61 | // Focus on devices that are public exposed 62 | | join kind=inner ( 63 | DeviceNetworkEvents 64 | | where TimeGenerated > ago(7d) 65 | | where ActionType contains "InboundConnection" 66 | | where RemoteIPType == "Public" 67 | // Exclude MacOS Rapportd and ControlCenter 68 | | where InitiatingProcessFileName != "rapportd" and InitiatingProcessFileName != "controlcenter" 69 | | distinct DeviceName, DeviceId, LocalPort, InitiatingProcessFolderPath, InitiatingProcessVersionInfoProductName, InitiatingProcessFileName 70 | ) on $left.DeviceId == $right.DeviceId 71 | // Save all the open ports and their process in a JSON 72 | | extend OpenPortJson = bag_pack_columns(LocalPort, InitiatingProcessFolderPath, InitiatingProcessFileName) 73 | // Save open ports by Device ID 74 | | summarize PublicOpenPortList = make_set(OpenPortJson) by DeviceId 75 | ); 76 | // Save flagged device IDs in list to limit results of CVE's we need to search later 77 | let flagged_devices = toscalar( 78 | xspm_base 79 | | summarize make_set(DeviceId) 80 | ); 81 | // CVE base query we materialize since we need these results multiple times 82 | let cve_base = materialize ( 83 | DeviceTvmSoftwareVulnerabilities 84 | | where VulnerabilitySeverityLevel in ("High", "Critical") 85 | | where DeviceId in ( flagged_devices ) 86 | ); 87 | // Save flagged CVE IDs in list to limit results of CVE database we need to search later 88 | let flagged_cves = toscalar( 89 | cve_base 90 | | summarize make_set(CveId) 91 | ); 92 | // Query the CVE's of the flagged devices 93 | cve_base 94 | // Enrich the CVE data with their EPSS and CVSS Score 95 | | join kind=inner ( 96 | DeviceTvmSoftwareVulnerabilitiesKB 97 | // Focus on flagged CVE's 98 | | where CveId in ( flagged_cves ) 99 | // Focus on CVE's tagged with Attack Vector being over the Network 100 | // 'Vulnerabilities with this rating are remotely exploitable, from one or more hops away, up to and including remote exploitation over the Internet.' 101 | // 'Does not require user interaction' 102 | | where CvssVector contains "/AV:N" and CvssVector contains "/UI:N" 103 | // Focus on CVE's where an exploit is available 104 | | where IsExploitAvailable != 0 105 | | distinct CveId, EpssScore, CvssScore, CvssVector, IsExploitAvailable, AffectedSoftwareList=tostring(AffectedSoftware) 106 | ) on CveId 107 | // Continue with only relevant data 108 | | project DeviceId, DeviceName, OSPlatform, OSVersion, OSArchitecture, SoftwareName, SoftwareVendor, SoftwareVersion, CveId, VulnerabilitySeverityLevel, EpssScore, CvssScore, CvssVector, IsExploitAvailable, AffectedSoftwareList 109 | // Now flag CVE's with a EPSS score above a certain threshold 110 | // See efficiency research on https://www.first.org/epss/model 111 | | where EpssScore >= epss_threshold 112 | // Save all the CVE data in a JSON column 113 | | extend CveJson = bag_pack_columns(SoftwareName, SoftwareVendor, SoftwareVersion, CveId, EpssScore, CvssScore, CvssVector, IsExploitAvailable, AffectedSoftwareList) 114 | // Group the CVE data for each device per device 115 | | summarize CveList = make_list(CveJson) by DeviceId, DeviceName 116 | // Add xspm data again 117 | | join kind=inner xspm_base on DeviceId 118 | | project-away DeviceId1 119 | // Sort by CVE amount 120 | | extend CveCount = array_length(CveList) 121 | | sort by CveCount desc 122 | ``` -------------------------------------------------------------------------------- /Defender For Endpoint/HuntADWSRequestsFromUnknownDevice.md: -------------------------------------------------------------------------------- 1 | # *Hunt for ADWS requests from unknown devices* 2 | 3 | ## Query Information 4 | 5 | #### MITRE ATT&CK Technique(s) 6 | 7 | | Technique ID | Title | Link | 8 | | --- | --- | --- | 9 | | T1119 | Automated Collection | https://attack.mitre.org/techniques/T1119/ | 10 | | T1087.002 | Account Discovery - Domain Account | https://attack.mitre.org/techniques/T1087/002/ | 11 | | T1069.002 | Permission Groups Discovery - Domain Groups | https://attack.mitre.org/techniques/T1069/002/ | 12 | | T1201 | Password Policy Discovery | https://attack.mitre.org/techniques/T1201/ | 13 | | T1482 | Domain Trust Discovery | https://attack.mitre.org/techniques/T1482/ | 14 | | T1021.002 | Remote Services - SMB/Windows Admin Shares | https://attack.mitre.org/techniques/T1021/002/ | 15 | | T1018 | Remote System Discovery | https://attack.mitre.org/techniques/T1018/ | 16 | | T1135 | Network Share Discovery | https://attack.mitre.org/techniques/T1135/ | 17 | 18 | #### Description 19 | This hunting rule searches for incomming ADWS connections on Domain Controllers (DC's need to be onboarded in Defender for Endpoint) from IP Addresses that cannot be linked to MDE onboarded devices. 20 | 21 | #### Risk 22 | Adversary tools used to enumerate Active Directory over ADWS instead of using LDAP are becoming more popular, since they often stay under the radar of most monitoring tools. In the references you can find two detections provided by FalconForce on how you can detect ADWS connections from an unexpected binary, when the source device is onboarded in Microsoft Defender for Endpoint. But if somehow an unmanaged device was able to to connect via ADWS to domain controllers, we are not able to use these detections. Because of this, you should hunt for unknown devices performing ADWS connections and treat them as suspicious. 23 | 24 | #### Author 25 | - **Name:** Robbe Van den Daele 26 | - **Github:** https://github.com/RobbeVandenDaele 27 | - **Twitter:** https://x.com/RobbeVdDaele 28 | - **LinkedIn:** https://www.linkedin.com/in/robbe-van-den-daele-677986190/ 29 | - **Website:** https://hybridbrothers.com/ 30 | 31 | #### References 32 | - https://falconforce.nl/soaphound-tool-to-collect-active-directory-data-via-adws/ 33 | - https://github.com/FalconForceTeam/FalconFriday/blob/master/Discovery/ADWS_Connection_from_Unexpected_Binary-Win.md 34 | - https://github.com/FalconForceTeam/FalconFriday/blob/master/Discovery/ADWS_Connection_from_Process_Injection_Target-Win.md 35 | - https://cyberlandsec.com/soapy-the-ultimate-stealthy-active-directory-enumeration-tool-via-adws/ 36 | - https://github.com/FalconForceTeam/SOAPHound 37 | 38 | ## Defender XDR 39 | ```KQL 40 | let device_info = ( 41 | // Get device network info from last 7 days 42 | DeviceNetworkInfo 43 | | where Timestamp > ago(7d) 44 | // Expand the IP Addresses of the devices 45 | | mv-expand todynamic(IPAddresses) 46 | | extend IPAddress = tostring(IPAddresses.IPAddress) 47 | // Distinct IP address for each device 48 | | distinct DeviceName, DeviceId, IPAddress 49 | // Search for each device if it is onboarded or not 50 | | join kind=inner ( 51 | DeviceInfo 52 | | where Timestamp > ago(7d) 53 | | distinct DeviceName, DeviceId, OnboardingStatus 54 | // Get the first timestamp the device was seen 55 | | join kind=inner ( 56 | DeviceInfo 57 | | where Timestamp > ago(30d) 58 | | summarize FirstSeen = arg_min(Timestamp, DeviceId) by DeviceId 59 | ) on DeviceId 60 | | project-away DeviceId1, DeviceId2 61 | ) on DeviceId, DeviceName 62 | | project-away DeviceName1, DeviceId1 63 | ); 64 | // Get incomming traffic on ADWS port and save unique remote IP addresses 65 | DeviceNetworkEvents 66 | | where Timestamp > ago(30d) 67 | | where ActionType != "ListeningConnectionCreated" 68 | | where InitiatingProcessFolderPath == @"c:\windows\adws\microsoft.activedirectory.webservices.exe" 69 | | where LocalPort == "9389" 70 | | summarize ConnectionTimes=make_list(Timestamp) by RemoteIP, DeviceName 71 | // Get device information of remote IP addresses, results for IP we do not find information for are allowed 72 | | join kind=leftouter device_info on $left.RemoteIP == $right.IPAddress 73 | | project-away IPAddress 74 | // Check if the remote IPs are onboarded devices or not 75 | | where OnboardingStatus != "Onboarded" 76 | // Make output better 77 | | project DeviceName, ConnectionTimes, RemoteIP, RemoteDeviceName = DeviceName1, RemoteDeviceId = DeviceId, RemoteOnboardingStatus = OnboardingStatus, RemoteDeviceFirstSeen = FirstSeen 78 | ``` 79 | 80 | ## Sentinel 81 | ```KQL 82 | let device_info = ( 83 | // Get device network info from last 7 days 84 | DeviceNetworkInfo 85 | | where TimeGenerated > ago(7d) 86 | // Expand the IP Addresses of the devices 87 | | mv-expand todynamic(IPAddresses) 88 | | extend IPAddress = tostring(IPAddresses.IPAddress) 89 | // Distinct IP address for each device 90 | | distinct DeviceName, DeviceId, IPAddress 91 | // Search for each device if it is onboarded or not 92 | | join kind=inner ( 93 | DeviceInfo 94 | | where TimeGenerated > ago(7d) 95 | | distinct DeviceName, DeviceId, OnboardingStatus 96 | // Get the first timestamp the device was seen 97 | | join kind=inner ( 98 | DeviceInfo 99 | | where TimeGenerated > ago(30d) 100 | | summarize FirstSeen = arg_min(TimeGenerated, DeviceId) by DeviceId 101 | ) on DeviceId 102 | | project-away DeviceId1, DeviceId2 103 | ) on DeviceId, DeviceName 104 | | project-away DeviceName1, DeviceId1 105 | ); 106 | // Get incomming traffic on ADWS port and save unique remote IP addresses 107 | DeviceNetworkEvents 108 | | where TimeGenerated > ago(30d) 109 | | where ActionType != "ListeningConnectionCreated" 110 | | where InitiatingProcessFolderPath == @"c:\windows\adws\microsoft.activedirectory.webservices.exe" 111 | | where LocalPort == "9389" 112 | | summarize ConnectionTimes=make_list(TimeGenerated) by RemoteIP, DeviceName 113 | // Get device information of remote IP addresses, results for IP we do not find information for are allowed 114 | | join kind=leftouter device_info on $left.RemoteIP == $right.IPAddress 115 | | project-away IPAddress 116 | // Check if the remote IPs are onboarded devices or not 117 | | where OnboardingStatus != "Onboarded" 118 | // Make output better 119 | | project DeviceName, ConnectionTimes, RemoteIP, RemoteDeviceName = DeviceName1, RemoteDeviceId = DeviceId, RemoteOnboardingStatus = OnboardingStatus, RemoteDeviceFirstSeen = FirstSeen 120 | ``` -------------------------------------------------------------------------------- /Entra ID/DetectSuspiciousFociTokenLogins.md: -------------------------------------------------------------------------------- 1 | # *Detect suspicious foci token logins* 2 | 3 | ## Query Information 4 | 5 | #### MITRE ATT&CK Technique(s) 6 | 7 | | Technique ID | Title | Link | 8 | | --- | --- | --- | 9 | | T1651 | Cloud Administration Command | https://attack.mitre.org/techniques/T1651/ | 10 | | T1606 | Forge Web Credentials | https://attack.mitre.org/techniques/T1606/ | 11 | 12 | #### Description 13 | FOCI tokens (Family of Client IDs tokens) are special refresh tokens that allow multiple applications within the same "family" to share authentication tokens. This means that once a user authenticates with one application, they can access other applications in the same family without needing to re-authenticate. For adversaries, these are very interesting tokens to abuse since they can access a normal application (Microsoft Teams for example), and reuse that refresh token to access another application (like Azure CLI). 14 | 15 | To detect a suspicious foci token combination, we look for all the logins using foci tokens and group them by Session ID (since these belong to the same session). Then we take the first login where no refresh token was provided, and look at the logins that used refresh tokens as incomming token types within that same session. If the second login application is one that is typically abused by adversaries and the application for the first login is a 'normal' application, we flag the event. 16 | 17 | We added a second version for this query in this repo, to also flag when an adversary is using the same application to get new access tokens but with another scope. The v2 version focusses more on RoadTool detection tho, while this detection is more broad. 18 | 19 | Some organizations have a high BP hit count on Microsoft Azure CLI. To limit those hits, you have three finetune options to enable in the query: 20 | - Only alert when first and second login has X time between each other (default 90 minutes if enabled) 21 | - Only alert on Microsoft Azure CLI when Global Administrator scope is used in token 22 | - Only alert on Microsoft Azure CLI when Global Administrator scope is used in token and request came from a non-compliant device 23 | 24 | #### Risk 25 | With this detection rule we try to detect suspicious foci token usage. 26 | 27 | #### Author 28 | - **Name:** Robbe Van den Daele 29 | - **Github:** https://github.com/RobbeVandenDaele 30 | - **Twitter:** https://x.com/RobbeVdDaele 31 | - **LinkedIn:** https://www.linkedin.com/in/robbe-van-den-daele-677986190/ 32 | - **Website:** https://hybridbrothers.com/ 33 | 34 | #### References 35 | - https://swisskyrepo.github.io/InternalAllTheThings/cloud/azure/azure-access-and-token/#foci-refresh-token 36 | - https://github.com/secureworks/family-of-client-ids-research/tree/main 37 | 38 | ## Sentinel 39 | ```KQL 40 | // TimeDiff threshold in minutes. Needed for some environments with a lot of BP hits on long time frames. Used in scenario where you expect adversary to quickly request new tokens after first token request. 41 | let maxTimeDiff = 90; 42 | // External lookup to get list of FOCI applications 43 | let FociClientApplications = toscalar(externaldata(client_id: string) 44 | [@"https://raw.githubusercontent.com/secureworks/family-of-client-ids-research/refs/heads/main/known-foci-clients.csv"] with (format="csv", ignoreFirstRecord=true) 45 | //| project-rename FociClientId = client_id 46 | | summarize FociClientId = make_list(client_id) 47 | ); 48 | // Get all token requests for Foci clients 49 | let FociTokenRequest = materialize ( 50 | AADNonInteractiveUserSignInLogs 51 | | where TimeGenerated > ago(6h) 52 | // Filter for sign-ins to home tenant only 53 | | where HomeTenantId == ResourceTenantId 54 | // Lookup for FOCI client 55 | | where AppId in (FociClientApplications) 56 | ); 57 | FociTokenRequest 58 | // First get all initial logins without refresh tokens as incomming token type 59 | | where IncomingTokenType == "none" 60 | // Then get logins with refresh tokens for same session 61 | | join kind=inner ( 62 | FociTokenRequest 63 | | where IncomingTokenType != "none" 64 | | project-rename 65 | SecondAppDisplayName = AppDisplayName, 66 | SecondRequestTimeGenerated = TimeGenerated, 67 | SecondAppId = AppId 68 | ) 69 | on SessionId, UserPrincipalName 70 | // Exclude when First App ID and Second are the same 71 | | where AppDisplayName != SecondAppDisplayName 72 | // Only get requests where refresh token was used after first sign-in 73 | | extend TimeDiff = datetime_diff('minute', SecondRequestTimeGenerated, TimeGenerated) 74 | | where TimeDiff >= 0 //and TimeDiff <= maxTimeDiff // Remove from comment you want to apply time difference restriction 75 | // Only project needed columns 76 | | project 77 | FirstRequestTimeGenerated = TimeGenerated, 78 | FirstResult = ResultType, 79 | FirstResultDescription = ResultDescription, 80 | Identity, 81 | Location, 82 | FirstAppDisplayName = AppDisplayName, 83 | FirstAppId = AppId, 84 | ClientAppUsed, 85 | DeviceDetail, 86 | SecondDeviceDetail = DeviceDetail1, 87 | IPAddress, 88 | LocationDetails, 89 | UserAgent, 90 | SecondRequestTimeGenerated, 91 | SecondResult = ResultType, 92 | SecondResultDescription = ResultDescription1, 93 | SecondAppDisplayName, 94 | SecondAppId, 95 | SeconIncomingTokenType = IncomingTokenType1, 96 | SessionId, 97 | TimeDiff, 98 | AuthenticationProcessingDetails, 99 | SecondAuthenticationProcessingDetails = AuthenticationProcessingDetails1 100 | // Flag logins to the following applications as second login (since they are very popular for attackers and we rather not see logins to these via foci tokens) 101 | | where SecondAppDisplayName in ("Microsoft Azure CLI", "Microsoft Azure PowerShell", "Office 365 Management") 102 | // ENVIRONMENT SPECIFIC FINETUNING - BEGIN 103 | // Most BP triggers are mainly on Microsoft Azure CLI, so we provide two ways of handling these BP detections (strongly depends on environment) 104 | // OPTION 1 - Flag login to Azure CLI using 'Global Administrator' ID in token scope 105 | //| where (SecondAppDisplayName in ("Microsoft Azure PowerShell", "Office 365 Management") or (SecondAppDisplayName == "Microsoft Azure CLI" and SecondAuthenticationProcessingDetails contains "62e90394-69f5-4237-9190-012177145e10")) 106 | // OPTION 2 - Flag login to Azure CLI using 'Global Administrator' ID in token scope from non compliant device 107 | //| where (SecondAppDisplayName in ("Microsoft Azure PowerShell", "Office 365 Management") or (SecondAppDisplayName == "Microsoft Azure CLI" and SecondAuthenticationProcessingDetails contains "62e90394-69f5-4237-9190-012177145e10" and todynamic(SecondDeviceDetail).isCompliant != "true")) 108 | // ENVIRONMENT SPECIFIC FINETUNING - END 109 | ``` -------------------------------------------------------------------------------- /Defender For Endpoint/HuntOrganizeDevicesBySubnet.md: -------------------------------------------------------------------------------- 1 | # *Hunt for devices organized by subnet* 2 | 3 | ## Query Information 4 | 5 | #### MITRE ATT&CK Technique(s) 6 | 7 | N/A 8 | 9 | #### Description 10 | This rule helps you organize devices by subnet in your networks. By doing this, you can identify how many not-onboarded devices, devices not supporting MDE containment, and types of devices live in your subnet ranges. 11 | 12 | #### Risk 13 | - This query is rather big and can probably be optimized a bit. The result will in most cases contain a supernet, which you will have to filter out yourself if needed. 14 | - The IsolateSupportedOS and ContainSupportedOS columns are calculated based on OS only. The correct agent version nuances as discussed earlier are not yet added. 15 | 16 | #### Author 17 | - **Name:** Robbe Van den Daele 18 | - **Github:** https://github.com/RobbeVandenDaele 19 | - **Twitter:** https://x.com/RobbeVdDaele 20 | - **LinkedIn:** https://www.linkedin.com/in/robbe-van-den-daele-677986190/ 21 | - **Website:** https://hybridbrothers.com/ 22 | 23 | #### References 24 | - https://hybridbrothers.com/device-isolation-and-containment-strategies/ 25 | ## Defender XDR 26 | ```KQL 27 | let isolationSupportedOS = dynamic(["Windows11", "Windows10", "WindowsServer2025", "WindowsServer2022", "WindowsServer2019", "WindowsServer2016", "WindowsServer2012R2", "Linux", "macOS"]); 28 | let containmentSupportedOS = dynamic(["Windows11", "Windows10", "WindowsServer2025", "WindowsServer2022", "WindowsServer2019", "WindowsServer2016", "WindowsServer2012R2"]); 29 | let base = DeviceNetworkInfo 30 | // Expand all IPs 31 | | mv-expand todynamic(IPAddresses) 32 | // Ignore IPv6 addresses 33 | | where tostring(IPAddresses.IPAddress) !contains ":" 34 | // Save the Prefix as an extra property and set it to /32 when empty 35 | | extend Prefix = iff(isnotempty(tostring(IPAddresses.SubnetPrefix)), tostring(IPAddresses.SubnetPrefix), "32"); 36 | let networks = base 37 | // Get network addresses with a non /32 prefix 38 | | where Prefix != "32" 39 | // Get the network address related to the IP 40 | | extend NetworkAddress = format_ipv4(tostring(IPAddresses.IPAddress), tolong(Prefix)) 41 | // Build the IP and Network Address with the CIDR notation 42 | | extend IPAddress = strcat(tostring(IPAddresses.IPAddress), "/", Prefix) 43 | | extend NetworkAddress = strcat(NetworkAddress, "/", Prefix) 44 | // Join the Device Info information 45 | | join kind=inner DeviceInfo on DeviceId, ReportId 46 | // Ignore APIPA addresses 47 | | where NetworkAddress != "169.254.0.0/16" 48 | // Ignore merged device IDs 49 | | where MergedToDeviceId == "" 50 | // Make a set of all the Device Objects belonging to the same subnet 51 | | extend DeviceObj = pack( 52 | "DeviceName", DeviceName, 53 | "IPAddress", IPAddress, 54 | "DeviceType", DeviceType, 55 | "DeviceCategory", DeviceCategory, 56 | "IsInternetFacing", IsInternetFacing, 57 | "OnboardingStatus", OnboardingStatus, 58 | "OSDistribution", OSDistribution, 59 | "OSPlatform", OSPlatform 60 | ) 61 | // Make a list of the objects in the same subnet 62 | | summarize make_set(DeviceObj) by NetworkAddress; 63 | let device_with_host_prefix = base 64 | // Get network addresses with /32 Prefix to try and match other networks 65 | | where Prefix == "32" 66 | // Build the IP Address with the CIDR notation 67 | | extend IPAddress = strcat(tostring(IPAddresses.IPAddress), "/", Prefix) 68 | // Join the Device Info information 69 | | join kind=inner DeviceInfo on DeviceId, ReportId 70 | // Ignore merged device IDs 71 | | where MergedToDeviceId == "" 72 | // Make a set of all the Device Objects 73 | | extend DeviceObj = pack( 74 | "DeviceName", DeviceName, 75 | "IPAddress", IPAddress, 76 | "DeviceType", DeviceType, 77 | "DeviceCategory", DeviceCategory, 78 | "IsInternetFacing", IsInternetFacing, 79 | "OnboardingStatus", OnboardingStatus, 80 | "OSDistribution", OSDistribution, 81 | "OSPlatform", OSPlatform 82 | ) 83 | | extend Joiner = 1; 84 | let network_addresses = base 85 | // Get network addresses with a non /32 prefix 86 | | where Prefix != "32" 87 | // Get the network address related to the IP 88 | | extend NetworkAddress = format_ipv4(tostring(IPAddresses.IPAddress), tolong(Prefix)) 89 | | extend NetworkAddress = strcat(NetworkAddress, "/", Prefix) 90 | // Create joiner to find host addresses related to certain networks 91 | | distinct NetworkAddress 92 | | extend Joiner = 1; 93 | let networks2 = device_with_host_prefix 94 | // Try to join /32 IPs 95 | | join kind=inner network_addresses on Joiner 96 | // Check if IP is in the network range, and only return those IPs 97 | | extend InRange = ipv4_is_in_range(IPAddress, NetworkAddress) 98 | | where InRange == 1 99 | // Make a list of the objects in the same subnet 100 | | summarize make_set(DeviceObj) by NetworkAddress; 101 | union networks, networks2 102 | // Expand the Device Objects 103 | | mv-expand set_DeviceObj 104 | // Save the DeviceType, DeviceCategory, and Onboarding Status 105 | | extend DeviceType = set_DeviceObj.DeviceType 106 | | extend DeviceCategory = set_DeviceObj.DeviceCategory 107 | | extend OnboardingStatus = set_DeviceObj.OnboardingStatus 108 | // Count how many servers, workstations, network devices, iot devices, and ot devices exists in a subnet, the onboarding estate, and OS Distribution 109 | | summarize Servers = countif(set_DeviceObj.DeviceType=="Server"), 110 | Workstations = countif(set_DeviceObj.DeviceType=="Workstation"), 111 | NetworkDevices = countif(set_DeviceObj.DeviceCategory=="NetworkDevice"), 112 | IoTDevices = countif(set_DeviceObj.DeviceCategory=="IoT"), 113 | OTDevices = countif(set_DeviceObj.DeviceCategory=="OT"), 114 | Onboarded = countif(set_DeviceObj.OnboardingStatus=="Onboarded"), 115 | NotOnboarded = countif(set_DeviceObj.OnboardingStatus!="Onboarded"), 116 | IsolateSupportedOS = countif((set_DeviceObj.OSDistribution has_any (isolationSupportedOS) or set_DeviceObj.OSPlatform == "Linux") and set_DeviceObj.OnboardingStatus == "Onboarded"), 117 | ContainSupportedOS = countif(set_DeviceObj.OSDistribution has_any (containmentSupportedOS) and set_DeviceObj.OnboardingStatus == "Onboarded") by NetworkAddress 118 | // Join the network subnets so we have the device objects again 119 | | join kind=leftouter networks on NetworkAddress 120 | | join kind=leftouter networks2 on NetworkAddress 121 | // Extend Array Concat 122 | | extend set_DeviceObj = array_concat(set_DeviceObj, set_DeviceObj1) 123 | // Remove duplicate columns 124 | | project-away NetworkAddress1, NetworkAddress2, set_DeviceObj1 125 | // Count how many IPs there are in one subnet 126 | | extend CountIPs = array_length(set_DeviceObj) 127 | | sort by CountIPs desc 128 | ``` 129 | 130 | ## Sentinel 131 | ```KQL 132 | N/A 133 | ``` -------------------------------------------------------------------------------- /Entra ID/DetectMultipleWhfbPrtTokensUsedSimultaneouslyForOneDevice.md: -------------------------------------------------------------------------------- 1 | # *Detect Multiple Hello for Business PRT tokens being used simultaneously for one device.* 2 | 3 | ## Query Information 4 | 5 | #### MITRE ATT&CK Technique(s) 6 | 7 | | Technique ID | Title | Link | 8 | | --- | --- | --- | 9 | | T1606 | Forge Web Credentials | https://attack.mitre.org/techniques/T1606/ | 10 | 11 | #### Description 12 | This detection rule tries to find multiple PRT tokens being used simultaneously for one device. This might indicate that an attacker was able to request a new PRT on a second device using exxported Windows Hello for Business keys. More information about the attack scenario can be found in the references. 13 | 14 | #### Risk 15 | By using this detections, we can try to detect an attacker requesting access tokens with a forged PRT token on a new device. 16 | 17 | #### Author 18 | - **Name:** Robbe Van den Daele 19 | - **Github:** https://github.com/RobbeVandenDaele 20 | - **Twitter:** https://x.com/RobbeVdDaele 21 | - **LinkedIn:** https://www.linkedin.com/in/robbe-van-den-daele-677986190/ 22 | - **Website:** https://hybridbrothers.com/ 23 | 24 | #### References 25 | - https://hybridbrothers.com/detecting-non-privileged-windows-hello-abuse/ 26 | 27 | ## Defender XDR 28 | ```KQL 29 | // Get the Sign-in logs we want to query 30 | let base = materialize( 31 | SigninLogs 32 | | where Timestamp > ago(1d) 33 | ); 34 | // Get all the WHfB signins by looking at the authentication method and incomming token 35 | let whfb = ( 36 | base 37 | // Get WHfB signins 38 | | mv-expand todynamic(AuthenticationDetails) 39 | | where AuthenticationDetails.authenticationMethod == "Windows Hello for Business" 40 | | where IncomingTokenType == "primaryRefreshToken" 41 | | extend DeviceID = tostring(DeviceDetail.deviceId), AuthenticationDateTime = todatetime(AuthenticationDetails.authenticationStepDateTime) 42 | // Remove empty Session and Device IDs 43 | | where SessionId != "" and DeviceID != "" 44 | ); 45 | // Save the time frame for each WHfB PRT token 46 | // We use the SessionID to identify a specific PRT token since the SessionID changes when a new refresh token is being used 47 | let prt_timeframes = ( 48 | whfb 49 | // Summarize the first and last PRT usage per device, by using the Session ID 50 | | summarize TimeMin = arg_min(AuthenticationDateTime,*), TimeMax=arg_max(AuthenticationDateTime,*) by DeviceID, SessionId 51 | | project DeviceID, SessionId, TimeMin, TimeMax 52 | ); 53 | // Save all the Session IDs for the logins that came from a WHfB authentication method 54 | let whfb_sessions = toscalar( 55 | whfb 56 | | summarize make_set(SessionId) 57 | ); 58 | base 59 | | mv-expand todynamic(AuthenticationDetails) 60 | | extend DeviceID = tostring(DeviceDetail.deviceId), AuthenticationDateTime = todatetime(AuthenticationDetails.authenticationStepDateTime) 61 | // Get all signins related to a WHfB Session 62 | | where SessionId in (whfb_sessions) 63 | // Join the access token requests comming from a WHfB session with all the PRT tokens used in the past for each device 64 | | join kind=inner prt_timeframes on DeviceID 65 | | extend CurrentSessionID = SessionId, OtherSessionID = SessionId1, OtherSessionTimeMin = TimeMin, OtherSessionTimeMax = TimeMax, DeviceName = tostring(DeviceDetail.displayName) 66 | // Get logins where the current SessionID is not the same as another one 67 | | where CurrentSessionID != OtherSessionID 68 | // Check if the new Session ID is seen while other Session IDs are still active (only check first login of the current Session ID) 69 | | summarize arg_min(AuthenticationDateTime, *) by DeviceID, CurrentSessionID 70 | | where AuthenticationDateTime between (OtherSessionTimeMin .. OtherSessionTimeMax) 71 | // Exclude Windows Sign In as application login since attackers will use the PRT to request access tokens for other applications (they do not need to signin into Windows anymore) 72 | | where AppDisplayName != "Windows Sign In" 73 | | project AuthenticationDateTime, UserPrincipalName, DeviceID, DeviceName, CurrentSessionID, OtherSessionID, OtherSessionTimeMin, OtherSessionTimeMax, AppDisplayName, ResourceDisplayName 74 | ``` 75 | 76 | ## Sentinel 77 | ```KQL 78 | // Get the Sign-in logs we want to query 79 | let base = materialize( 80 | SigninLogs 81 | | where TimeGenerated > ago(1d) 82 | ); 83 | // Get all the WHfB signins by looking at the authentication method and incomming token 84 | let whfb = ( 85 | base 86 | // Get WHfB signins 87 | | mv-expand todynamic(AuthenticationDetails) 88 | | where AuthenticationDetails.authenticationMethod == "Windows Hello for Business" 89 | | where IncomingTokenType == "primaryRefreshToken" 90 | | extend DeviceID = tostring(DeviceDetail.deviceId), AuthenticationDateTime = todatetime(AuthenticationDetails.authenticationStepDateTime) 91 | // Remove empty Session and Device IDs 92 | | where SessionId != "" and DeviceID != "" 93 | ); 94 | // Save the time frame for each WHfB PRT token 95 | // We use the SessionID to identify a specific PRT token since the SessionID changes when a new refresh token is being used 96 | let prt_timeframes = ( 97 | whfb 98 | // Summarize the first and last PRT usage per device, by using the Session ID 99 | | summarize TimeMin = arg_min(AuthenticationDateTime,*), TimeMax=arg_max(AuthenticationDateTime,*) by DeviceID, SessionId 100 | | project DeviceID, SessionId, TimeMin, TimeMax 101 | ); 102 | // Save all the Session IDs for the logins that came from a WHfB authentication method 103 | let whfb_sessions = toscalar( 104 | whfb 105 | | summarize make_set(SessionId) 106 | ); 107 | base 108 | | mv-expand todynamic(AuthenticationDetails) 109 | | extend DeviceID = tostring(DeviceDetail.deviceId), AuthenticationDateTime = todatetime(AuthenticationDetails.authenticationStepDateTime) 110 | // Get all signins related to a WHfB Session 111 | | where SessionId in (whfb_sessions) 112 | // Join the access token requests comming from a WHfB session with all the PRT tokens used in the past for each device 113 | | join kind=inner prt_timeframes on DeviceID 114 | | extend CurrentSessionID = SessionId, OtherSessionID = SessionId1, OtherSessionTimeMin = TimeMin, OtherSessionTimeMax = TimeMax, DeviceName = tostring(DeviceDetail.displayName) 115 | // Get logins where the current SessionID is not the same as another one 116 | | where CurrentSessionID != OtherSessionID 117 | // Check if the new Session ID is seen while other Session IDs are still active (only check first login of the current Session ID) 118 | | summarize arg_min(AuthenticationDateTime, *) by DeviceID, CurrentSessionID 119 | | where AuthenticationDateTime between (OtherSessionTimeMin .. OtherSessionTimeMax) 120 | // Exclude Windows Sign In as application login since attackers will use the PRT to request access tokens for other applications (they do not need to signin into Windows anymore) 121 | | where AppDisplayName != "Windows Sign In" 122 | | project AuthenticationDateTime, UserPrincipalName, DeviceID, DeviceName, CurrentSessionID, OtherSessionID, OtherSessionTimeMin, OtherSessionTimeMax, AppDisplayName, ResourceDisplayName 123 | ``` -------------------------------------------------------------------------------- /Entra ID/DetectSuspiciousFociTokenLoginsV2.md: -------------------------------------------------------------------------------- 1 | # *Detect suspicious foci token logins V2* 2 | 3 | ## Query Information 4 | 5 | #### MITRE ATT&CK Technique(s) 6 | 7 | | Technique ID | Title | Link | 8 | | --- | --- | --- | 9 | | T1651 | Cloud Administration Command | https://attack.mitre.org/techniques/T1651/ | 10 | | T1606 | Forge Web Credentials | https://attack.mitre.org/techniques/T1606/ | 11 | 12 | #### Description 13 | FOCI tokens (Family of Client IDs tokens) are special refresh tokens that allow multiple applications within the same "family" to share authentication tokens. This means that once a user authenticates with one application, they can access other applications in the same family without needing to re-authenticate. For adversaries, these are very interesting tokens to abuse since they can access a normal application (Microsoft Teams for example), and reuse that refresh token to access another application (like Azure CLI). 14 | 15 | To detect a suspicious foci token combination, we look for all the logins using foci tokens and group them by Session ID (since these belong to the same session). Then we take the first login where no refresh token was provided, and look at the logins that used refresh tokens as incomming token types within that same session. If the second login application is one that is typically abused by adversaries and the application for the first login is a 'normal' application, we flag the event. 16 | 17 | This version is the V2 version for this query in this repo, which also flags when an adversary is using the same application to get new access tokens but with another scope (compared to V1 which does not do this). The v2 version focusses more on RoadTool detection tho, while the v1 detection is more broad. 18 | 19 | Some organizations have a high BP hit count on Microsoft Azure CLI. To limit those hits, you have three finetune options to enable in the query: 20 | - Only alert when first and second login has X time between each other (default 90 minutes if enabled) 21 | - Only alert on Microsoft Azure CLI when Global Administrator scope is used in token 22 | - Only alert on Microsoft Azure CLI when Global Administrator scope is used in token and request came from a non-compliant device 23 | 24 | #### Risk 25 | With this detection rule we try to detect suspicious foci token usage. 26 | 27 | #### Author 28 | - **Name:** Robbe Van den Daele 29 | - **Github:** https://github.com/RobbeVandenDaele 30 | - **Twitter:** https://x.com/RobbeVdDaele 31 | - **LinkedIn:** https://www.linkedin.com/in/robbe-van-den-daele-677986190/ 32 | - **Website:** https://hybridbrothers.com/ 33 | 34 | #### References 35 | - https://swisskyrepo.github.io/InternalAllTheThings/cloud/azure/azure-access-and-token/#foci-refresh-token 36 | - https://github.com/secureworks/family-of-client-ids-research/tree/main 37 | 38 | ## Sentinel 39 | ```KQL 40 | // TimeDiff threshold in minutes. Needed for some environments with a lot of BP hits on long time frames. Used in scenario where you expect adversary to quickly request new tokens after first token request. 41 | let maxTimeDiff = 90; 42 | // External lookup to get list of FOCI applications 43 | let FociClientApplications = toscalar(externaldata(client_id: string) 44 | [@"https://raw.githubusercontent.com/secureworks/family-of-client-ids-research/refs/heads/main/known-foci-clients.csv"] with (format="csv", ignoreFirstRecord=true) 45 | //| project-rename FociClientId = client_id 46 | | summarize FociClientId = make_list(client_id) 47 | ); 48 | // Get all token requests for Foci clients 49 | let FociTokenRequest = materialize ( 50 | AADNonInteractiveUserSignInLogs 51 | | where TimeGenerated > ago(6h) 52 | // Filter for sign-ins to home tenant only 53 | | where HomeTenantId == ResourceTenantId 54 | // Lookup for FOCI client 55 | | where AppId in (FociClientApplications) 56 | ); 57 | FociTokenRequest 58 | // First get all initial logins without refresh tokens as incomming token type 59 | | where IncomingTokenType == "none" 60 | // Then get logins with refresh tokens for same session 61 | | join kind=inner ( 62 | FociTokenRequest 63 | | where IncomingTokenType != "none" 64 | | project-rename 65 | SecondAppDisplayName = AppDisplayName, 66 | SecondRequestTimeGenerated = TimeGenerated, 67 | SecondAppId = AppId 68 | ) 69 | on SessionId, UserPrincipalName 70 | | extend FirstOauthScopeInfo = extract("{\"key\":\"Oauth Scope Info\",\"value\":\"\\[(.*)\\]\"}", 1, AuthenticationProcessingDetails), 71 | SecondOauthScopeInfo = extract("{\"key\":\"Oauth Scope Info\",\"value\":\"\\[(.*)\\]\"}", 1, AuthenticationProcessingDetails1) 72 | // Only get requests where refresh token was used after first sign-in 73 | | extend TimeDiff = datetime_diff('minute', SecondRequestTimeGenerated, TimeGenerated) 74 | | where TimeDiff >= 1 and TimeDiff <= maxTimeDiff 75 | // Only project needed columns 76 | | project 77 | FirstRequestTimeGenerated = TimeGenerated, 78 | FirstResult = ResultType, 79 | FirstResultDescription = ResultDescription, 80 | Identity, 81 | Location, 82 | FirstAppDisplayName = AppDisplayName, 83 | FirstAppId = AppId, 84 | ClientAppUsed, 85 | DeviceDetail, 86 | SecondDeviceDetail = DeviceDetail1, 87 | IPAddress, 88 | LocationDetails, 89 | UserAgent, 90 | SecondRequestTimeGenerated, 91 | SecondResult = ResultType, 92 | SecondResultDescription = ResultDescription1, 93 | SecondAppDisplayName, 94 | SecondAppId, 95 | SeconIncomingTokenType = IncomingTokenType1, 96 | SessionId, 97 | TimeDiff, 98 | FirstOauthScopeInfo, 99 | SecondOauthScopeInfo, 100 | FirstResourceIdentity = ResourceIdentity, 101 | SecondResourceIdentity = ResourceIdentity1 102 | // Flag logins to the following applications as second login, since these are the most popular used for RoadTools (fico apps, localhost redirect URI, and Azure AD resource identity) 103 | | where SecondAppDisplayName in ("Microsoft Azure CLI", "Copilot App", "Microsoft Azure PowerShell", "Visual Studio - Legacy", "Microsoft Edge Enterprise New Tab Page") and SecondResourceIdentity == "00000002-0000-0000-c000-000000000000" 104 | | where SecondResult == 0 105 | // ENVIRONMENT SPECIFIC FINETUNING - BEGIN 106 | // Most BP triggers are mainly on Microsoft Azure CLI, so we provide two ways of handling these BP detections (strongly depends on environment) 107 | // OPTION 1 - Flag login to Azure CLI using 'Global Administrator' ID in token scope 108 | //| where (SecondAppDisplayName in ("Microsoft Azure PowerShell", "Office 365 Management") or (SecondAppDisplayName == "Microsoft Azure CLI" and SecondAuthenticationProcessingDetails contains "62e90394-69f5-4237-9190-012177145e10")) 109 | // OPTION 2 - Flag login to Azure CLI using 'Global Administrator' ID in token scope from non compliant device 110 | //| where (SecondAppDisplayName in ("Microsoft Azure PowerShell", "Office 365 Management") or (SecondAppDisplayName == "Microsoft Azure CLI" and SecondAuthenticationProcessingDetails contains "62e90394-69f5-4237-9190-012177145e10" and todynamic(SecondDeviceDetail).isCompliant != "true")) 111 | // ENVIRONMENT SPECIFIC FINETUNING - END 112 | ``` -------------------------------------------------------------------------------- /Entra ID/DetectSuspiciousNcryptUsageWithSuspiciousRdpSession.md: -------------------------------------------------------------------------------- 1 | # *Detect Suspicious ncrypt.dll usage with RDP connections to unmanaged or non TPM protected device* 2 | 3 | ## Query Information 4 | 5 | #### MITRE ATT&CK Technique(s) 6 | 7 | | Technique ID | Title | Link | 8 | | --- | --- | --- | 9 | | T1555.004 | Credentials from Password Stores: Windows Credential Manager | https://attack.mitre.org/techniques/T1555/004/ | 10 | | T1606 | Forge Web Credentials | https://attack.mitre.org/techniques/T1606/ | 11 | | T1021.001 | Remote Services: Remote Desktop Protocol | https://attack.mitre.org/techniques/T1021/001/ | 12 | 13 | #### Description 14 | This detection rule uses a WDAC audit policy to ingest missing DeviceImageLoad events in MDE, and check for suspicious processes using the ncrypt.dll and devices performing RDP connection to unmanaged or non-TPM devices. More information on the attack scenario this is detection is applicable for can be found in the references. 15 | 16 | #### Risk 17 | By using this detections, we can try to detect an attacker using the hellopoc.ps1 script in RoadTools to generate an assertion, and export the Windows Hello for Business keys using an RDP session. 18 | 19 | #### Author 20 | - **Name:** Robbe Van den Daele 21 | - **Github:** https://github.com/RobbeVandenDaele 22 | - **Twitter:** https://x.com/RobbeVdDaele 23 | - **LinkedIn:** https://www.linkedin.com/in/robbe-van-den-daele-677986190/ 24 | - **Website:** https://hybridbrothers.com/ 25 | 26 | #### References 27 | - https://hybridbrothers.com/detecting-non-privileged-windows-hello-abuse/ 28 | - https://github.com/dirkjanm/ROADtools/blob/master/winhello_assertion/hellopoc.ps1 29 | 30 | ## Defender XDR 31 | ```KQL 32 | let cli_tools = dynamic(["powershell", "python"]); 33 | // Get suspicious ncrypt.dll usage via WDAC audit policy 34 | let time_lookback = 1h; 35 | let no_tpm_devices = ( 36 | ExposureGraphNodes 37 | // Get device nodes with their inventory ID 38 | | where NodeLabel == "device" 39 | | mv-expand EntityIds 40 | | where EntityIds.type == "DeviceInventoryId" 41 | // Get interesting properties 42 | | extend OnboardingStatus = tostring(parse_json(NodeProperties)["rawData"]["onboardingStatus"]), 43 | TpmSupported = tostring(parse_json(NodeProperties)["rawData"]["tpmData"]["supported"]), 44 | TpmEnabled = tostring(parse_json(NodeProperties)["rawData"]["tpmData"]["enabled"]), 45 | TpmActivated = tostring(parse_json(NodeProperties)["rawData"]["tpmData"]["activated"]), 46 | DeviceName = tostring(parse_json(NodeProperties)["rawData"]["deviceName"]), 47 | DeviceId = tostring(EntityIds.id) 48 | // Search for distinct devices 49 | | distinct DeviceId, DeviceName, OnboardingStatus, TpmSupported, TpmEnabled, TpmActivated 50 | // Get Unmanaged devices and device not supporting a TPM 51 | | where OnboardingStatus != "Onboarded" or (TpmSupported != "true" and TpmActivated != "true" and TpmEnabled != "true") 52 | | extend TpmSupported = iff(TpmSupported == "", "unknown", TpmSupported), 53 | TpmActivated = iff(TpmActivated == "", "unknown", TpmActivated), 54 | TpmEnabled = iff(TpmEnabled == "", "unknown", TpmEnabled) 55 | ); 56 | let no_tpm_device_info = ( 57 | DeviceNetworkInfo 58 | | where Timestamp > ago(7d) 59 | // Get latest network info for each device ID 60 | | summarize arg_max(Timestamp, *) by DeviceId 61 | | mv-expand todynamic(IPAddresses) 62 | | extend IPAddress = tostring(IPAddresses.IPAddress) 63 | // Find no TPM devices and join with their network information 64 | | join kind=inner no_tpm_devices on DeviceId 65 | | project DeviceId, DeviceName, MacAddress, IPAddress, OnboardingStatus, TpmActivated, TpmEnabled, TpmSupported 66 | ); 67 | let dangerous_rdp_sessions = ( 68 | DeviceNetworkEvents 69 | | where Timestamp > ago(time_lookback) 70 | // Exclude MDI RDP Connections (known for NNR) 71 | | where InitiatingProcessFileName !~ "microsoft.tri.sensor.exe" 72 | // Search for RDP connections to non-tpm devices 73 | | where ActionType == "ConnectionSuccess" 74 | | where RemotePort == 3389 75 | | join kind=inner no_tpm_device_info on $left.RemoteIP == $right.IPAddress 76 | | project-rename RemoteDeviceId = DeviceId1, 77 | RdpRemoteDeviceName = DeviceName1, 78 | RdpRemoteMacAddress = MacAddress, 79 | RdpRemoteDeviceOnboardingStatus = OnboardingStatus, 80 | RdpRemoteDeviceTpmActivated = TpmActivated, 81 | RdpRemoteDeviceTpmEnabled = TpmEnabled, 82 | RdpRemoteDeviceTpmSupported = TpmSupported, 83 | RdpTimeGenerated = Timestamp, 84 | RdpInitiatingProcessFileName = InitiatingProcessFileName 85 | | project-away IPAddress 86 | ); 87 | // Get all possible nonce requests 88 | let nonce_requests = ( 89 | DeviceNetworkEvents 90 | | where Timestamp > ago(time_lookback) 91 | | where ActionType == "ConnectionSuccess" 92 | | where RemoteUrl =~ "login.microsoftonline.com" 93 | | project-rename NonceRequestTimestamp = Timestamp 94 | ); 95 | // Get suspicious ncrypt.dll usage via WDAC audit policy 96 | DeviceEvents 97 | | where Timestamp > ago(time_lookback) 98 | | where ActionType startswith "AppControl" and FileName =~ "ncrypt.dll" 99 | // Check if the same initiating process is doing a nonce request 100 | | join kind=inner nonce_requests on InitiatingProcessId, DeviceId 101 | // Only flag when nonce was request 10min before of after ncrypt usage 102 | | where Timestamp between (todatetime(NonceRequestTimestamp - 10m) .. todatetime(NonceRequestTimestamp + 10m)) 103 | // Check if the same device is doing RDP Connections 104 | | join kind=inner dangerous_rdp_sessions on DeviceId 105 | // Whitelist known good processes 106 | | where InitiatingProcessFileName !in ("backgroundtaskhost.exe","svchost.exe") 107 | // Project interesting columns 108 | | extend WdacPolicyName = parse_json(AdditionalFields)["PolicyName"] 109 | | project Timestamp, DeviceName, ActionType, FileName, InitiatingProcessSHA1, InitiatingProcessFileName, 110 | InitiatingProcessId, InitiatingProcessAccountName, InitiatingProcessParentFileName, WdacPolicyName, InitiatingProcessRemoteSessionDeviceName, InitiatingProcessRemoteSessionIP, 111 | NonceRequestTimestamp, RdpTimeGenerated, RdpInitiatingProcessFileName, RdpRemoteDeviceName, RdpRemoteMacAddress, RdpRemoteDeviceOnboardingStatus, 112 | RdpRemoteDeviceTpmActivated, RdpRemoteDeviceTpmEnabled, RdpRemoteDeviceTpmSupported 113 | ``` 114 | 115 | ## Sentinel 116 | ```KQL 117 | let time_lookback = 1h; 118 | let no_tpm_devices = ( 119 | ExposureGraphNodes 120 | // Get device nodes with their inventory ID 121 | | where NodeLabel == "device" 122 | | mv-expand EntityIds 123 | | where EntityIds.type == "DeviceInventoryId" 124 | // Get interesting properties 125 | | extend OnboardingStatus = tostring(parse_json(NodeProperties)["rawData"]["onboardingStatus"]), 126 | TpmSupported = tostring(parse_json(NodeProperties)["rawData"]["tpmData"]["supported"]), 127 | TpmEnabled = tostring(parse_json(NodeProperties)["rawData"]["tpmData"]["enabled"]), 128 | TpmActivated = tostring(parse_json(NodeProperties)["rawData"]["tpmData"]["activated"]), 129 | DeviceName = tostring(parse_json(NodeProperties)["rawData"]["deviceName"]), 130 | DeviceId = tostring(EntityIds.id) 131 | // Search for distinct devices 132 | | distinct DeviceId, DeviceName, OnboardingStatus, TpmSupported, TpmEnabled, TpmActivated 133 | // Get Unmanaged devices and device not supporting a TPM 134 | | where OnboardingStatus != "Onboarded" or (TpmSupported != "true" and TpmActivated != "true" and TpmEnabled != "true") 135 | | extend TpmSupported = iff(TpmSupported == "", "unknown", TpmSupported), 136 | TpmActivated = iff(TpmActivated == "", "unknown", TpmActivated), 137 | TpmEnabled = iff(TpmEnabled == "", "unknown", TpmEnabled) 138 | ); 139 | let no_tpm_device_info = ( 140 | DeviceNetworkInfo 141 | | where TimeGenerated > ago(7d) 142 | // Get latest network info for each device ID 143 | | summarize arg_max(TimeGenerated, *) by DeviceId 144 | | mv-expand todynamic(IPAddresses) 145 | | extend IPAddress = tostring(IPAddresses.IPAddress) 146 | // Find no TPM devices and join with their network information 147 | | join kind=inner no_tpm_devices on DeviceId 148 | | project DeviceId, DeviceName, MacAddress, IPAddress, OnboardingStatus, TpmActivated, TpmEnabled, TpmSupported 149 | ); 150 | let dangerous_rdp_sessions = ( 151 | DeviceNetworkEvents 152 | | where TimeGenerated > ago(time_lookback) 153 | // Exclude MDI RDP Connections (known for NNR) 154 | | where InitiatingProcessFileName !~ "microsoft.tri.sensor.exe" 155 | // Search for RDP connections to non-tpm devices 156 | | where ActionType == "ConnectionSuccess" 157 | | where RemotePort == 3389 158 | | join kind=inner no_tpm_device_info on $left.RemoteIP == $right.IPAddress 159 | | project-rename RemoteDeviceId = DeviceId1, 160 | RdpRemoteDeviceName = DeviceName1, 161 | RdpRemoteMacAddress = MacAddress, 162 | RdpRemoteDeviceOnboardingStatus = OnboardingStatus, 163 | RdpRemoteDeviceTpmActivated = TpmActivated, 164 | RdpRemoteDeviceTpmEnabled = TpmEnabled, 165 | RdpRemoteDeviceTpmSupported = TpmSupported, 166 | RdpTimeGenerated = Timestamp, 167 | RdpInitiatingProcessFileName = InitiatingProcessFileName 168 | | project-away IPAddress 169 | ); 170 | // Get all possible nonce requests 171 | let nonce_requests = ( 172 | DeviceNetworkEvents 173 | | where TimeGenerated > ago(time_lookback) 174 | | where ActionType == "ConnectionSuccess" 175 | | where RemoteUrl =~ "login.microsoftonline.com" 176 | | project-rename NonceRequestTimestamp = TimeGenerated 177 | ); 178 | // Get suspicious ncrypt.dll usage via WDAC audit policy 179 | DeviceEvents 180 | | where TimeGenerated > ago(time_lookback) 181 | | where ActionType startswith "AppControl" and FileName =~ "ncrypt.dll" 182 | // Check if the same initiating process is doing a nonce request 183 | | join kind=inner nonce_requests on InitiatingProcessId, DeviceId 184 | // Only flag when nonce was request 10min before of after ncrypt usage 185 | | where TimeGenerated between (todatetime(NonceRequestTimestamp - 10m) .. todatetime(NonceRequestTimestamp + 10m)) 186 | // Check if the same device is doing RDP Connections 187 | | join kind=inner dangerous_rdp_sessions on DeviceId 188 | // Whitelist known good processes 189 | | where InitiatingProcessFileName !in ("backgroundtaskhost.exe","svchost.exe") 190 | // Project interesting columns 191 | | extend WdacPolicyName = parse_json(AdditionalFields)["PolicyName"] 192 | | project TimeGenerated, DeviceName, ActionType, FileName, InitiatingProcessSHA1, InitiatingProcessFileName, 193 | InitiatingProcessId, InitiatingProcessAccountName, InitiatingProcessParentFileName, WdacPolicyName, InitiatingProcessRemoteSessionDeviceName, InitiatingProcessRemoteSessionIP, 194 | NonceRequestTimestamp, RdpTimeGenerated, RdpInitiatingProcessFileName, RdpRemoteDeviceName, RdpRemoteMacAddress, RdpRemoteDeviceOnboardingStatus, 195 | RdpRemoteDeviceTpmActivated, RdpRemoteDeviceTpmEnabled, RdpRemoteDeviceTpmSupported 196 | ``` -------------------------------------------------------------------------------- /Entra ID/DetectSuspiciousNcryptUsageWithSuspiciousAdminRdpSession.md: -------------------------------------------------------------------------------- 1 | # *Detect Suspicious ncrypt.dll usage on admin device with RDP connections to non TPM protected device* 2 | 3 | ## Query Information 4 | 5 | #### MITRE ATT&CK Technique(s) 6 | 7 | | Technique ID | Title | Link | 8 | | --- | --- | --- | 9 | | T1555.004 | Credentials from Password Stores: Windows Credential Manager | https://attack.mitre.org/techniques/T1555/004/ | 10 | | T1606 | Forge Web Credentials | https://attack.mitre.org/techniques/T1606/ | 11 | | T1021.001 | Remote Services: Remote Desktop Protocol | https://attack.mitre.org/techniques/T1021/001/ | 12 | 13 | #### Description 14 | This detection rule uses a WDAC audit policy to ingest missing DeviceImageLoad events in MDE, and check for suspicious processes using the ncrypt.dll and admin devices performing RDP connection to unmanaged or non-TPM devices. More information on the attack scenario this is detection is applicable for can be found in the references. 15 | 16 | #### Risk 17 | By using this detections, we can try to detect an attacker using the hellopoc.ps1 script in RoadTools to generate an assertion, and export the Windows Hello for Business keys using an RDP session. 18 | 19 | #### Author 20 | - **Name:** Robbe Van den Daele 21 | - **Github:** https://github.com/RobbeVandenDaele 22 | - **Twitter:** https://x.com/RobbeVdDaele 23 | - **LinkedIn:** https://www.linkedin.com/in/robbe-van-den-daele-677986190/ 24 | - **Website:** https://hybridbrothers.com/ 25 | 26 | #### References 27 | - https://hybridbrothers.com/detecting-non-privileged-windows-hello-abuse/ 28 | - https://github.com/dirkjanm/ROADtools/blob/master/winhello_assertion/hellopoc.ps1 29 | 30 | ## Defender XDR 31 | ```KQL 32 | let time_lookback = 30d; 33 | let admin_users = toscalar( 34 | IdentityInfo 35 | | where Timestamp > ago(7d) 36 | | where CriticalityLevel != "" or AccountDisplayName contains "Admin" 37 | | summarize make_set(AccountDisplayName) 38 | ); 39 | let devices_with_admin_accounts = ( 40 | ExposureGraphEdges 41 | // Get edges where source is a device and destination is a admin user 42 | | where SourceNodeLabel == "device" and TargetNodeLabel == "user" 43 | | where TargetNodeName in (admin_users) 44 | // Check which devices have the credentials of the admin user 45 | | make-graph SourceNodeId --> TargetNodeId with ExposureGraphNodes on NodeId 46 | | graph-match (SourceNode)-[hasCredentialsOf]->(TargetNode) 47 | project IncomingNodeName = SourceNode.NodeName, OutgoingNodeName = TargetNode.NodeName, CriticalityLevel = TargetNode.NodeProperties.rawData.criticalityLevel.criticalityLevel, CriticalityRuleNames = TargetNode.NodeProperties.rawData.criticalityLevel.ruleNames 48 | | summarize make_set(IncomingNodeName) 49 | ); 50 | let no_tpm_devices = ( 51 | ExposureGraphNodes 52 | // Get device nodes with their inventory ID 53 | | where NodeLabel == "device" 54 | | mv-expand EntityIds 55 | | where EntityIds.type == "DeviceInventoryId" 56 | // Get interesting properties 57 | | extend OnboardingStatus = tostring(parse_json(NodeProperties)["rawData"]["onboardingStatus"]), 58 | TpmSupported = tostring(parse_json(NodeProperties)["rawData"]["tpmData"]["supported"]), 59 | TpmEnabled = tostring(parse_json(NodeProperties)["rawData"]["tpmData"]["enabled"]), 60 | TpmActivated = tostring(parse_json(NodeProperties)["rawData"]["tpmData"]["activated"]), 61 | DeviceName = tostring(parse_json(NodeProperties)["rawData"]["deviceName"]), 62 | DeviceId = tostring(EntityIds.id) 63 | // Search for distinct devices 64 | | distinct DeviceId, DeviceName, OnboardingStatus, TpmSupported, TpmEnabled, TpmActivated 65 | // Get Unmanaged devices and device not supporting a TPM 66 | | where OnboardingStatus != "Onboarded" or (TpmSupported != "true" and TpmActivated != "true" and TpmEnabled != "true") 67 | | extend TpmSupported = iff(TpmSupported == "", "unknown", TpmSupported), 68 | TpmActivated = iff(TpmActivated == "", "unknown", TpmActivated), 69 | TpmEnabled = iff(TpmEnabled == "", "unknown", TpmEnabled) 70 | ); 71 | let no_tpm_device_info = ( 72 | DeviceNetworkInfo 73 | | where Timestamp > ago(7d) 74 | // Get latest network info for each device ID 75 | | summarize arg_max(Timestamp, *) by DeviceId 76 | | mv-expand todynamic(IPAddresses) 77 | | extend IPAddress = tostring(IPAddresses.IPAddress) 78 | // Find no TPM devices and join with their network information 79 | | join kind=inner no_tpm_devices on DeviceId 80 | | project DeviceId, DeviceName, MacAddress, IPAddress, OnboardingStatus, TpmActivated, TpmEnabled, TpmSupported 81 | ); 82 | let dangerous_rdp_sessions = ( 83 | DeviceNetworkEvents 84 | | where Timestamp > ago(time_lookback) 85 | // Only flag admin devices 86 | | where DeviceName in (devices_with_admin_accounts) 87 | // Exclude MDI RDP Connections (known for NNR) 88 | | where InitiatingProcessFileName !~ "microsoft.tri.sensor.exe" 89 | // Search for RDP connections to non-tpm devices 90 | | where ActionType == "ConnectionSuccess" 91 | | where RemotePort == 3389 92 | | join kind=inner no_tpm_device_info on $left.RemoteIP == $right.IPAddress 93 | | project-rename RemoteDeviceId = DeviceId1, 94 | RdpRemoteDeviceName = DeviceName1, 95 | RdpRemoteMacAddress = MacAddress, 96 | RdpRemoteDeviceOnboardingStatus = OnboardingStatus, 97 | RdpRemoteDeviceTpmActivated = TpmActivated, 98 | RdpRemoteDeviceTpmEnabled = TpmEnabled, 99 | RdpRemoteDeviceTpmSupported = TpmSupported, 100 | RdpTimeGenerated = Timestamp, 101 | RdpInitiatingProcessFileName = InitiatingProcessFileName 102 | | project-away IPAddress 103 | ); 104 | // Get all possible nonce requests 105 | let nonce_requests = ( 106 | DeviceNetworkEvents 107 | | where Timestamp > ago(time_lookback) 108 | | where ActionType == "ConnectionSuccess" 109 | | where RemoteUrl =~ "login.microsoftonline.com" 110 | | project-rename NonceRequestTimestamp = Timestamp 111 | ); 112 | // Get suspicious ncrypt.dll usage via WDAC audit policy 113 | DeviceEvents 114 | | where Timestamp > ago(time_lookback) 115 | // Only flag admin devices 116 | | where DeviceName in (devices_with_admin_accounts) 117 | | where ActionType startswith "AppControl" and FileName =~ "ncrypt.dll" 118 | // Check if the same initiating process is doing a nonce request 119 | | join kind=inner nonce_requests on InitiatingProcessId, DeviceId 120 | // Only flag when nonce was request 10min before of after ncrypt usage 121 | | where Timestamp between (todatetime(NonceRequestTimestamp - 10m) .. todatetime(NonceRequestTimestamp + 10m)) 122 | // Check if the same device is doing RDP Connections 123 | | join kind=inner dangerous_rdp_sessions on DeviceId 124 | // Whitelist known good processes 125 | | where InitiatingProcessFileName !in ("backgroundtaskhost.exe","svchost.exe") 126 | // Project interesting columns 127 | | extend WdacPolicyName = parse_json(AdditionalFields)["PolicyName"] 128 | | project Timestamp, DeviceName, ActionType, FileName, InitiatingProcessSHA1, InitiatingProcessFileName, 129 | InitiatingProcessId, InitiatingProcessAccountName, InitiatingProcessParentFileName, WdacPolicyName, InitiatingProcessRemoteSessionDeviceName, InitiatingProcessRemoteSessionIP, 130 | NonceRequestTimestamp, RdpTimeGenerated, RdpInitiatingProcessFileName, RdpRemoteDeviceName, RdpRemoteMacAddress, RdpRemoteDeviceOnboardingStatus, 131 | RdpRemoteDeviceTpmActivated, RdpRemoteDeviceTpmEnabled, RdpRemoteDeviceTpmSupported 132 | ``` 133 | 134 | ## Sentinel 135 | ```KQL 136 | let time_lookback = 30d; 137 | let admin_users = toscalar( 138 | IdentityInfo 139 | | where TimeGenerated > ago(7d) 140 | | where CriticalityLevel != "" or AccountDisplayName contains "Admin" 141 | | summarize make_set(AccountDisplayName) 142 | ); 143 | let devices_with_admin_accounts = ( 144 | ExposureGraphEdges 145 | // Get edges where source is a device and destination is a admin user 146 | | where SourceNodeLabel == "device" and TargetNodeLabel == "user" 147 | | where TargetNodeName in (admin_users) 148 | // Check which devices have the credentials of the admin user 149 | | make-graph SourceNodeId --> TargetNodeId with ExposureGraphNodes on NodeId 150 | | graph-match (SourceNode)-[hasCredentialsOf]->(TargetNode) 151 | project IncomingNodeName = SourceNode.NodeName, OutgoingNodeName = TargetNode.NodeName, CriticalityLevel = TargetNode.NodeProperties.rawData.criticalityLevel.criticalityLevel, CriticalityRuleNames = TargetNode.NodeProperties.rawData.criticalityLevel.ruleNames 152 | | summarize make_set(IncomingNodeName) 153 | ); 154 | let no_tpm_devices = ( 155 | ExposureGraphNodes 156 | // Get device nodes with their inventory ID 157 | | where NodeLabel == "device" 158 | | mv-expand EntityIds 159 | | where EntityIds.type == "DeviceInventoryId" 160 | // Get interesting properties 161 | | extend OnboardingStatus = tostring(parse_json(NodeProperties)["rawData"]["onboardingStatus"]), 162 | TpmSupported = tostring(parse_json(NodeProperties)["rawData"]["tpmData"]["supported"]), 163 | TpmEnabled = tostring(parse_json(NodeProperties)["rawData"]["tpmData"]["enabled"]), 164 | TpmActivated = tostring(parse_json(NodeProperties)["rawData"]["tpmData"]["activated"]), 165 | DeviceName = tostring(parse_json(NodeProperties)["rawData"]["deviceName"]), 166 | DeviceId = tostring(EntityIds.id) 167 | // Search for distinct devices 168 | | distinct DeviceId, DeviceName, OnboardingStatus, TpmSupported, TpmEnabled, TpmActivated 169 | // Get Unmanaged devices and device not supporting a TPM 170 | | where OnboardingStatus != "Onboarded" or (TpmSupported != "true" and TpmActivated != "true" and TpmEnabled != "true") 171 | | extend TpmSupported = iff(TpmSupported == "", "unknown", TpmSupported), 172 | TpmActivated = iff(TpmActivated == "", "unknown", TpmActivated), 173 | TpmEnabled = iff(TpmEnabled == "", "unknown", TpmEnabled) 174 | ); 175 | let no_tpm_device_info = ( 176 | DeviceNetworkInfo 177 | | where TimeGenerated > ago(7d) 178 | // Get latest network info for each device ID 179 | | summarize arg_max(TimeGenerated, *) by DeviceId 180 | | mv-expand todynamic(IPAddresses) 181 | | extend IPAddress = tostring(IPAddresses.IPAddress) 182 | // Find no TPM devices and join with their network information 183 | | join kind=inner no_tpm_devices on DeviceId 184 | | project DeviceId, DeviceName, MacAddress, IPAddress, OnboardingStatus, TpmActivated, TpmEnabled, TpmSupported 185 | ); 186 | let dangerous_rdp_sessions = ( 187 | DeviceNetworkEvents 188 | | where TimeGenerated > ago(time_lookback) 189 | // Only flag admin devices 190 | | where DeviceName in (devices_with_admin_accounts) 191 | // Exclude MDI RDP Connections (known for NNR) 192 | | where InitiatingProcessFileName !~ "microsoft.tri.sensor.exe" 193 | // Search for RDP connections to non-tpm devices 194 | | where ActionType == "ConnectionSuccess" 195 | | where RemotePort == 3389 196 | | join kind=inner no_tpm_device_info on $left.RemoteIP == $right.IPAddress 197 | | project-rename RemoteDeviceId = DeviceId1, 198 | RdpRemoteDeviceName = DeviceName1, 199 | RdpRemoteMacAddress = MacAddress, 200 | RdpRemoteDeviceOnboardingStatus = OnboardingStatus, 201 | RdpRemoteDeviceTpmActivated = TpmActivated, 202 | RdpRemoteDeviceTpmEnabled = TpmEnabled, 203 | RdpRemoteDeviceTpmSupported = TpmSupported, 204 | RdpTimeGenerated = Timestamp, 205 | RdpInitiatingProcessFileName = InitiatingProcessFileName 206 | | project-away IPAddress 207 | ); 208 | // Get all possible nonce requests 209 | let nonce_requests = ( 210 | DeviceNetworkEvents 211 | | where TimeGenerated > ago(time_lookback) 212 | | where ActionType == "ConnectionSuccess" 213 | | where RemoteUrl =~ "login.microsoftonline.com" 214 | | project-rename NonceRequestTimestamp = TimeGenerated 215 | ); 216 | // Get suspicious ncrypt.dll usage via WDAC audit policy 217 | DeviceEvents 218 | | where TimeGenerated > ago(time_lookback) 219 | // Only flag admin devices 220 | | where DeviceName in (devices_with_admin_accounts) 221 | | where ActionType startswith "AppControl" and FileName =~ "ncrypt.dll" 222 | // Check if the same initiating process is doing a nonce request 223 | | join kind=inner nonce_requests on InitiatingProcessId, DeviceId 224 | // Only flag when nonce was request 10min before of after ncrypt usage 225 | | where TimeGenerated between (todatetime(NonceRequestTimestamp - 10m) .. todatetime(NonceRequestTimestamp + 10m)) 226 | // Check if the same device is doing RDP Connections 227 | | join kind=inner dangerous_rdp_sessions on DeviceId 228 | // Whitelist known good processes 229 | | where InitiatingProcessFileName !in ("backgroundtaskhost.exe","svchost.exe") 230 | // Project interesting columns 231 | | extend WdacPolicyName = parse_json(AdditionalFields)["PolicyName"] 232 | | project TimeGenerated, DeviceName, ActionType, FileName, InitiatingProcessSHA1, InitiatingProcessFileName, 233 | InitiatingProcessId, InitiatingProcessAccountName, InitiatingProcessParentFileName, WdacPolicyName, InitiatingProcessRemoteSessionDeviceName, InitiatingProcessRemoteSessionIP, 234 | NonceRequestTimestamp, RdpTimeGenerated, RdpInitiatingProcessFileName, RdpRemoteDeviceName, RdpRemoteMacAddress, RdpRemoteDeviceOnboardingStatus, 235 | RdpRemoteDeviceTpmActivated, RdpRemoteDeviceTpmEnabled, RdpRemoteDeviceTpmSupported 236 | ``` -------------------------------------------------------------------------------- /Normalization queries/CefToCommonSecurityLog.md: -------------------------------------------------------------------------------- 1 | # *CEF to CommonSecurityLog normlization query* 2 | 3 | ## Query Information 4 | 5 | #### MITRE ATT&CK Technique(s) 6 | 7 | M/A 8 | 9 | #### Description 10 | This query can be used to normalize Syslog CEF data to the CommonSecurityLog table in Microsoft Sentinel. It extracts the CEF values via RegularExpressions, and add the values to new columns that allign with the CommonSecurityLog tables. 11 | 12 | #### Risk 13 | 1. Be aware that the source schema is based on data comming from Logstash, which means you might have to alter the query a bit if your source schema is different. 14 | 2. The CEF headers might differ based on the source that is sending the CEF messages (for example, CheckPoint CEF headers are different to the CEF headers of PaloAlto messages). Make sure to dubbel check your headers and change the query accordingly. 15 | 16 | #### Author 17 | - **Name:** Robbe Van den Daele 18 | - **Github:** https://github.com/RobbeVandenDaele 19 | - **Twitter:** https://x.com/RobbeVdDaele 20 | - **LinkedIn:** https://www.linkedin.com/in/robbe-van-den-daele-677986190/ 21 | - **Website:** https://hybridbrothers.com/ 22 | 23 | #### References 24 | - https://hybridbrothers.com/parsing-cef-messages-without-azure-monitor-agent/ 25 | 26 | ## Defender XDR 27 | ```KQL 28 | N/A 29 | ``` 30 | 31 | ## Sentinel 32 | ```KQL 33 | source 34 | // Normalize to CommonSecurityLog schema 35 | | parse message with CEF: string 36 | "|" DeviceVendor: string 37 | "|" DeviceProduct: string 38 | "|" DeviceVersion: string 39 | "|" DeviceEventClassID: string 40 | "|" Activity: string 41 | "|" LogSeverity: string 42 | "|" AdditionalExtensions: string 43 | // Extract fields 44 | | extend 45 | DeviceAction = extract("act=(.*?)(\\s\\w+=|$)", 1, AdditionalExtensions), 46 | ApplicationProtocol = extract("app=(.*?)(\\s\\w+=|$)", 1, AdditionalExtensions), 47 | DeviceEventCategory = extract("cat=(.*?)(\\s\\w+=|$)", 1, AdditionalExtensions), 48 | EventCount = toint(extract("cnt=(.*?)(\\s\\w+=|$)", 1, AdditionalExtensions)), 49 | DestinationDnsDomain = extract("destinationDnsDomain=(.*?)(\\s\\w+=|$)", 1, AdditionalExtensions), 50 | DestinationServiceName = extract("destinationServiceName=(.*?)(\\s\\w+=|$)", 1, AdditionalExtensions), 51 | DestinationTranslatedAddress = extract("destinationTranslatedAddress=(.*?)(\\s\\w+=|$)", 1, AdditionalExtensions), 52 | DestinationTranslatedPort = toint(extract("destinationTranslatedPort=(.*?)(\\s\\w+=|$)", 1, AdditionalExtensions)), 53 | CommunicationDirection = extract("deviceDirection=(.*?)(\\s\\w+=|$)", 1, AdditionalExtensions), 54 | DeviceDnsDomain = extract("deviceDnsDomain=(.*?)(\\s\\w+=|$)", 1, AdditionalExtensions), 55 | deviceExternalId = toint(extract("deviceExternalId=(.*?)(\\s\\w+=|$)", 1, AdditionalExtensions)), 56 | DeviceFacility = extract("deviceFacility=(.*?)(\\s\\w+=|$)", 1, AdditionalExtensions), 57 | DeviceInboundInterface = extract("deviceInboundInterface=(.*?)(\\s\\w+=|$)", 1, AdditionalExtensions), 58 | DeviceNtDomain = extract("deviceNtDomain=(.*?)(\\s\\w+=|$)", 1, AdditionalExtensions), 59 | DeviceOutboundInterface = extract("deviceOutboundInterface=(.*?)(\\s\\w+=|$)", 1, AdditionalExtensions), 60 | DevicePayloadId = extract("devicePayloadId=(.*?)(\\s\\w+=|$)", 1, AdditionalExtensions), 61 | ProcessName = extract("deviceProcessName=(.*?)(\\s\\w+=|$)", 1, AdditionalExtensions), 62 | DeviceTranslatedAddress = extract("deviceTranslatedAddress=(.*?)(\\s\\w+=|$)", 1, AdditionalExtensions), 63 | DestinationHostName = extract("dhost=(.*?)(\\s\\w+=|$)", 1, AdditionalExtensions), 64 | DestinationMacAddress = extract("dmac=(.*?)(\\s\\w+=|$)", 1, AdditionalExtensions), 65 | DestinationNTDomain = extract("dntdom=(.*?)(\\s\\w+=|$)", 1, AdditionalExtensions), 66 | DestinationProcessId = toint(extract("dpid=(.*?)(\\s\\w+=|$)", 1, AdditionalExtensions)), 67 | DestinationUserPrivileges = extract("dpriv=(.*?)(\\s\\w+=|$)", 1, AdditionalExtensions), 68 | DestinationProcessName = extract("dproc=(.*?)(\\s\\w+=|$)", 1, AdditionalExtensions), 69 | DestinationPort = toint(extract("dpt=(.*?)(\\s\\w+=|$)", 1, AdditionalExtensions)), 70 | DestinationIP = extract("dst=(.*?)(\\s\\w+=|$)", 1, AdditionalExtensions), 71 | DeviceTimeZone = extract("dtz=(.*?)(\\s\\w+=|$)", 1, AdditionalExtensions), 72 | DestinationUserId = extract("duid=(.*?)(\\s\\w+=|$)", 1, AdditionalExtensions), 73 | DestinationUserName = extract("duser=(.*?)(\\s\\w+=|$)", 1, AdditionalExtensions), 74 | DeviceAddress = extract("dvc=(.*?)(\\s\\w+=|$)", 1, AdditionalExtensions), 75 | DeviceName = extract("dvchost=(.*?)(\\s\\w+=|$)", 1, AdditionalExtensions), 76 | DeviceMacAddress = extract("dvcmac=(.*?)(\\s\\w+=|$)", 1, AdditionalExtensions), 77 | ProcessID = toint(extract("dvcpid=(.*?)(\\s\\w+=|$)", 1, AdditionalExtensions)), 78 | ExternalID = toint(extract("externalId=(.*?)(\\s\\w+=|$)", 1, AdditionalExtensions)), 79 | FileCreateTime = extract("fileCreateTime=(.*?)(\\s\\w+=|$)", 1, AdditionalExtensions), 80 | FileHash = extract("fileHash=(.*?)(\\s\\w+=|$)", 1, AdditionalExtensions), 81 | FileID = extract("fileId=(.*?)(\\s\\w+=|$)", 1, AdditionalExtensions), 82 | FileModificationTime = extract("fileModificationTime=(.*?)(\\s\\w+=|$)", 1, AdditionalExtensions), 83 | FilePath = extract("filePath=(.*?)(\\s\\w+=|$)", 1, AdditionalExtensions), 84 | FilePermission = extract("filePermission=(.*?)(\\s\\w+=|$)", 1, AdditionalExtensions), 85 | FileType = extract("fileType=(.*?)(\\s\\w+=|$)", 1, AdditionalExtensions), 86 | FileName = extract("fname=(.*?)(\\s\\w+=|$)", 1, AdditionalExtensions), 87 | FileSize = toint(extract("fsize=(.*?)(\\s\\w+=|$)", 1, AdditionalExtensions)), 88 | Computer = extract("host=(.*?)(\\s\\w+=|$)", 1, AdditionalExtensions), 89 | ReceivedBytes = tolong(extract("in=(.*?)(\\s\\w+=|$)", 1, AdditionalExtensions)), 90 | Message = extract("msg=(.*?)(\\s\\w+=|$)", 1, AdditionalExtensions), 91 | OldFileCreateTime = extract("oldFileCreateTime=(.*?)(\\s\\w+=|$)", 1, AdditionalExtensions), 92 | OldFileHash = extract("oldFileHash=(.*?)(\\s\\w+=|$)", 1, AdditionalExtensions), 93 | OldFileId = extract("oldFileId=(.*?)(\\s\\w+=|$)", 1, AdditionalExtensions), 94 | OldFileModificationTime = extract("oldFileModificationTime=(.*?)(\\s\\w+=|$)", 1, AdditionalExtensions), 95 | OldFileName = extract("oldFileName=(.*?)(\\s\\w+=|$)", 1, AdditionalExtensions), 96 | OldFilePath = extract("oldFilePath=(.*?)(\\s\\w+=|$)", 1, AdditionalExtensions), 97 | OldFilePermission = extract("oldFilePermission=(.*?)(\\s\\w+=|$)", 1, AdditionalExtensions), 98 | OldFileSize = toint(extract("oldFileSize=(.*?)(\\s\\w+=|$)", 1, AdditionalExtensions)), 99 | OldFileType = extract("oldFileType=(.*?)(\\s\\w+=|$)", 1, AdditionalExtensions), 100 | SentBytes = tolong(extract("out=(.*?)(\\s\\w+=|$)", 1, AdditionalExtensions)), 101 | EventOutcome = extract("outcome=(.*?)(\\s\\w+=|$)", 1, AdditionalExtensions), 102 | Protocol = extract("proto=(.*?)(\\s\\w+=|$)", 1, AdditionalExtensions), 103 | Reason = extract("reason=(.*?)(\\s\\w+=|$)", 1, AdditionalExtensions), 104 | RequestURL = extract("request=(.*?)(\\s\\w+=|$)", 1, AdditionalExtensions), 105 | RequestClientApplication = extract("requestClientApplication=(.*?)(\\s\\w+=|$)", 1, AdditionalExtensions), 106 | RequestContext = extract("requestContext=(.*?)(\\s\\w+=|$)", 1, AdditionalExtensions), 107 | RequestCookies = extract("requestCookies=(.*?)(\\s\\w+=|$)", 1, AdditionalExtensions), 108 | RequestMethod = extract("requestMethod=(.*?)(\\s\\w+=|$)", 1, AdditionalExtensions), 109 | ReceiptTime = extract("rt=(.*?)(\\s\\w+=|$)", 1, AdditionalExtensions), 110 | SourceHostName = extract("shost=(.*?)(\\s\\w+=|$)", 1, AdditionalExtensions), 111 | SourceMacAddress = extract("smac=(.*?)(\\s\\w+=|$)", 1, AdditionalExtensions), 112 | SourceNTDomain = extract("sntdom=(.*?)(\\s\\w+=|$)", 1, AdditionalExtensions), 113 | SourceDnsDomain = extract("sourceDnsDomain=(.*?)(\\s\\w+=|$)", 1, AdditionalExtensions), 114 | SourceServiceName = extract("sourceServiceName=(.*?)(\\s\\w+=|$)", 1, AdditionalExtensions), 115 | SourceTranslatedAddress = extract("sourceTranslatedAddress=(.*?)(\\s\\w+=|$)", 1, AdditionalExtensions), 116 | SourceTranslatedPort = toint(extract("sourceTranslatedPort=(.*?)(\\s\\w+=|$)", 1, AdditionalExtensions)), 117 | SourceProcessId = toint(extract("spid=(.*?)(\\s\\w+=|$)", 1, AdditionalExtensions)), 118 | SourceUserPrivileges = extract("spriv=(.*?)(\\s\\w+=|$)", 1, AdditionalExtensions), 119 | SourceProcessName = extract("sproc=(.*?)(\\s\\w+=|$)", 1, AdditionalExtensions), 120 | SourcePort = toint(extract("spt=(.*?)(\\s\\w+=|$)", 1, AdditionalExtensions)), 121 | SourceIP = extract("src=(.*?)(\\s\\w+=|$)", 1, AdditionalExtensions), 122 | SourceUserID = extract("suid=(.*?)(\\s\\w+=|$)", 1, AdditionalExtensions), 123 | SourceUserName = extract("suser=(.*?)(\\s\\w+=|$)", 1, AdditionalExtensions), 124 | EventType = toint(extract("type=(.*?)(\\s\\w+=|$)", 1, AdditionalExtensions)) 125 | // Extract custom fields 126 | | extend 127 | DeviceCustomString1 = extract("cs1=(.*?)(\\s\\w+=|$)", 1, AdditionalExtensions), 128 | DeviceCustomString1Label = extract("cs1Label=(.*?)(\\s\\w+=|$)", 1, AdditionalExtensions), 129 | DeviceCustomString2 = extract("cs2=(.*?)(\\s\\w+=|$)", 1, AdditionalExtensions), 130 | DeviceCustomString2Label = extract("cs2Label=(.*?)(\\s\\w+=|$)", 1, AdditionalExtensions), 131 | DeviceCustomString3 = extract("cs3=(.*?)(\\s\\w+=|$)", 1, AdditionalExtensions), 132 | DeviceCustomString3Label = extract("cs3Label=(.*?)(\\s\\w+=|$)", 1, AdditionalExtensions), 133 | DeviceCustomString4 = extract("cs4=(.*?)(\\s\\w+=|$)", 1, AdditionalExtensions), 134 | DeviceCustomString4Label = extract("cs4Label=(.*?)(\\s\\w+=|$)", 1, AdditionalExtensions), 135 | DeviceCustomString5 = extract("cs5=(.*?)(\\s\\w+=|$)", 1, AdditionalExtensions), 136 | DeviceCustomString5Label = extract("cs5Label=(.*?)(\\s\\w+=|$)", 1, AdditionalExtensions), 137 | DeviceCustomString6 = extract("cs6=(.*?)(\\s\\w+=|$)", 1, AdditionalExtensions), 138 | DeviceCustomString6Label = extract("cs6Label=(.*?)(\\s\\w+=|$)", 1, AdditionalExtensions), 139 | DeviceCustomNumber1 = toint(extract("cn1=(.*?)(\\s\\w+=|$)", 1, AdditionalExtensions)), 140 | DeviceCustomNumber1Label = extract("cn1Label=(.*?)(\\s\\w+=|$)", 1, AdditionalExtensions), 141 | DeviceCustomNumber2 = toint(extract("cn2=(.*?)(\\s\\w+=|$)", 1, AdditionalExtensions)), 142 | DeviceCustomNumber2Label = extract("cn2Label=(.*?)(\\s\\w+=|$)", 1, AdditionalExtensions), 143 | DeviceCustomNumber3 = toint(extract("cn3=(.*?)(\\s\\w+=|$)", 1, AdditionalExtensions)), 144 | DeviceCustomNumber3Label = extract("cn3Label=(.*?)(\\s\\w+=|$)", 1, AdditionalExtensions), 145 | FlexString1 = extract("flexString1=(.*?)(\\s\\w+=|$)", 1, AdditionalExtensions), 146 | FlexString1Label = extract("flexString1Label=(.*?)(\\s\\w+=|$)", 1, AdditionalExtensions), 147 | FlexString2 = extract("flexString2=(.*?)(\\s\\w+=|$)", 1, AdditionalExtensions), 148 | FlexString2Label = extract("flexString2Label=(.*?)(\\s\\w+=|$)", 1, AdditionalExtensions), 149 | DeviceCustomIPv6Address1 = extract("c6a1=(.*?)(\\s\\w+=|$)", 1, AdditionalExtensions), 150 | DeviceCustomIPv6Address1Label = extract("c6a1Label=(.*?)(\\s\\w+=|$)", 1, AdditionalExtensions), 151 | DeviceCustomIPv6Address2 = extract("c6a2=(.*?)(\\s\\w+=|$)", 1, AdditionalExtensions), 152 | DeviceCustomIPv6Address2Label = extract("c6a2Label=(.*?)(\\s\\w+=|$)", 1, AdditionalExtensions), 153 | DeviceCustomIPv6Address3 = extract("c6a3=(.*?)(\\s\\w+=|$)", 1, AdditionalExtensions), 154 | DeviceCustomIPv6Address3Label = extract("c6a3Label=(.*?)(\\s\\w+=|$)", 1, AdditionalExtensions), 155 | DeviceCustomIPv6Address4 = extract("c6a4=(.*?)(\\s\\w+=|$)", 1, AdditionalExtensions), 156 | DeviceCustomIPv6Address4Label = extract("c6a4Label=(.*?)(\\s\\w+=|$)", 1, AdditionalExtensions), 157 | DeviceCustomFloatingPoint1 = toreal(extract("cfp1=(.*?)(\\s\\w+=|$)", 1, AdditionalExtensions)), 158 | deviceCustomFloatingPoint1Label = extract("cfp1Label=(.*?)(\\s\\w+=|$)", 1, AdditionalExtensions), 159 | DeviceCustomFloatingPoint2 = toreal(extract("cfp2=(.*?)(\\s\\w+=|$)", 1, AdditionalExtensions)), 160 | deviceCustomFloatingPoint2Label = extract("cfp2Label=(.*?)(\\s\\w+=|$)", 1, AdditionalExtensions), 161 | DeviceCustomFloatingPoint3 = toreal(extract("cfp3=(.*?)(\\s\\w+=|$)", 1, AdditionalExtensions)), 162 | deviceCustomFloatingPoint3Label = extract("cfp3Label=(.*?)(\\s\\w+=|$)", 1, AdditionalExtensions), 163 | DeviceCustomFloatingPoint4 = toreal(extract("cfp4=(.*?)(\\s\\w+=|$)", 1, AdditionalExtensions)), 164 | deviceCustomFloatingPoint4Label = extract("cfp4Label=(.*?)(\\s\\w+=|$)", 1, AdditionalExtensions), 165 | DeviceCustomDate1 = extract("deviceCustomDate1=(.*?)(\\s\\w+=|$)", 1, AdditionalExtensions), 166 | DeviceCustomDate1Label = extract("deviceCustomDate1Label=(.*?)(\\s\\w+=|$)", 1, AdditionalExtensions), 167 | DeviceCustomDate2 = extract("deviceCustomDate2=(.*?)(\\s\\w+=|$)", 1, AdditionalExtensions), 168 | DeviceCustomDate2Label = extract("deviceCustomDate2Label=(.*?)(\\s\\w+=|$)", 1, AdditionalExtensions), 169 | FlexDate1 = extract("flexDate1=(.*?)(\\s\\w+=|$)", 1, AdditionalExtensions), 170 | FlexDate1Label = extract("flexDate1Label=(.*?)(\\s\\w+=|$)", 1, AdditionalExtensions), 171 | FlexNumber1 = toint(extract("flexNumber1=(.*?)(\\s\\w+=|$)", 1, AdditionalExtensions)), 172 | FlexNumber1Label = extract("flexNumber1Label=(.*?)(\\s\\w+=|$)", 1, AdditionalExtensions), 173 | FlexNumber2 = toint(extract("flexNumber2=(.*?)(\\s\\w+=|$)", 1, AdditionalExtensions)), 174 | FlexNumber2Label = extract("flexNumber2Label=(.*?)(\\s\\w+=|$)", 1, AdditionalExtensions) 175 | | extend TimeGenerated = todatetime(ls_timestamp), Computer = tostring(host) 176 | | project-away 177 | message, 178 | facility, 179 | facility_label, 180 | ls_version, 181 | priority, 182 | severity, 183 | severity_label, 184 | ['type'], 185 | CEF, 186 | ls_timestamp, 187 | host, 188 | tags 189 | ``` -------------------------------------------------------------------------------- /Entra ID/DetectSuspiciousCaChanges.md: -------------------------------------------------------------------------------- 1 | # *Detect suspicious conditional access policy modifications* 2 | 3 | ## Query Information 4 | 5 | #### MITRE ATT&CK Technique(s) 6 | 7 | | Technique ID | Title | Link | 8 | | --- | --- | --- | 9 | | T1556.009 | Modify Authentication Process: Conditional Access Policies | https://attack.mitre.org/techniques/T1556/009/ | 10 | 11 | #### Description 12 | This detection rule flags events where conditional access policies are getting weaker, when modifications to CA inclusion or exclusion groups are happening, or when the effectiveness of a policy is disabled. 13 | 14 | #### Risk 15 | By using this detections, we try to cover the risk of a malicious actor changing authorization policies in Entra ID. 16 | 17 | #### Author 18 | - **Name:** Robbe Van den Daele 19 | - **Github:** https://github.com/RobbeVandenDaele 20 | - **Twitter:** https://x.com/RobbeVdDaele 21 | - **LinkedIn:** https://www.linkedin.com/in/robbe-van-den-daele-677986190/ 22 | - **Website:** https://hybridbrothers.com/ 23 | 24 | #### References 25 | - https://hybridbrothers.com/suspicious-conditional-access-modifications/ 26 | 27 | ## Defender XDR 28 | ```KQL 29 | N/A 30 | ``` 31 | 32 | ## Sentinel 33 | ```KQL 34 | // !! TO DO: CHANGE TO YOUR CA GROUP NAMING CONVENTION !! 35 | let ca_include_naming_convention = "CA-Include"; 36 | let ca_exclude_naming_convention = "CA-Exclude"; 37 | // OPTIONAL - Get PIM activations with justifications for CA changes 38 | let ca_pim_activations = AuditLogs 39 | // Get PIM activations 40 | | where TimeGenerated > ago(24h) 41 | | where OperationName contains "completed (PIM activation)" 42 | // Parse details 43 | | parse AdditionalDetails with * "{\"key\":\"StartTime\",\"value\":\"" PimStartTime "\"" * "{\"key\":\"ExpirationTime\",\"value\":\"" PimExpirationTime "\"" * "{\"key\":\"Justification\",\"value\":\"" PimJustification "\"" * 44 | // Only get CA related PIM justifications 45 | | where PimJustification has_any ("Conditional Access", "CA", "Trusted", "Named", "Location") 46 | // Extend and projects 47 | | extend UserPrincipalName = tostring(InitiatedBy.user.userPrincipalName) 48 | | project OperationName, PimJustification, PimStartTime, PimExpirationTime, UserPrincipalName; 49 | // Get suspicious policy changes 50 | let policy_changes = AuditLogs 51 | // Get CA updates 52 | | where TimeGenerated > ago(24h) 53 | | where OperationName in ("Update conditional access policy", "Delete conditional access policy") 54 | // Expand Target resources and the modified properties 55 | | mv-expand TargetResources 56 | | mv-expand TargetResources.modifiedProperties 57 | // Save the new and old values 58 | | extend NewValueConditions = parse_json(tostring(parse_json(TargetResources_modifiedProperties.newValue))).conditions 59 | | extend OldValueConditions = parse_json(tostring(parse_json(TargetResources_modifiedProperties.oldValue))).conditions 60 | | extend NewValueGrandControls = parse_json(tostring(parse_json(TargetResources_modifiedProperties.newValue))).grantControls 61 | | extend OldValueGrandControls = parse_json(tostring(parse_json(TargetResources_modifiedProperties.oldValue))).grantControls 62 | | extend NewValueSessionControls = parse_json(tostring(parse_json(TargetResources_modifiedProperties.newValue))).sessionControls 63 | | extend OldValueSessionControls = parse_json(tostring(parse_json(TargetResources_modifiedProperties.oldValue))).sessionControls 64 | | extend NewState = parse_json(tostring(parse_json(TargetResources_modifiedProperties.newValue))).state 65 | | extend OldState = parse_json(tostring(parse_json(TargetResources_modifiedProperties.oldValue))).state 66 | // Count the new inlude arrays 67 | | extend CountNewUserIncludes = array_length(NewValueConditions.users.includeUsers), 68 | CountNewRoleIncludes = array_length(NewValueConditions.users.includeRoles), 69 | CountNewGroupIncludes = array_length(NewValueConditions.users.includeGroups), 70 | CountNewUserActionIncludes = array_length(NewValueConditions.applications.inlcudeUserActions), 71 | CountNewAuthContextIncludes = array_length(NewValueConditions.applications.includeAuthenticationContextClassReferences), 72 | CountNewApplicationIncludes = array_length(NewValueConditions.applications.inlcudeApplications), 73 | CountNewLocationIncludes = array_length(NewValueConditions.locations.includeLocations), 74 | CountNewPlatformIncludes = array_length(NewValueConditions.platforms.includePlatforms) 75 | // Count the old inlude arrays 76 | | extend CountOldUserIncludes = array_length(OldValueConditions.users.includeUsers), 77 | CountOldRoleIncludes = array_length(OldValueConditions.users.includeRoles), 78 | CountOldGroupIncludes = array_length(OldValueConditions.users.includeGroups), 79 | CountOldUserActionIncludes = array_length(OldValueConditions.applications.inlcudeUserActions), 80 | CountOldAuthContextIncludes = array_length(OldValueConditions.applications.includeAuthenticationContextClassReferences), 81 | CountOldApplicationIncludes = array_length(OldValueConditions.applications.inlcudeApplications), 82 | CountOldLocationIncludes = array_length(OldValueConditions.locations.includeLocations), 83 | CountOldPlatformIncludes = array_length(OldValueConditions.platforms.includePlatforms) 84 | // Count the new exclude arrays 85 | | extend CountNewUserExcludes = array_length(NewValueConditions.users.excludeUsers), 86 | CountNewRoleExcludes = array_length(NewValueConditions.users.excludeRoles), 87 | CountNewGroupExcludes = array_length(NewValueConditions.users.excludeGroups), 88 | CountNewApplicationExcludes = array_length(NewValueConditions.applications.excludeApplications), 89 | CountNewLocationExcludes = array_length(NewValueConditions.locations.excludeLocations), 90 | CountNewPlatformExcludes = array_length(NewValueConditions.platforms.excludePlatforms) 91 | // Count the old exclude arrays 92 | | extend CountOldUserExcludes = array_length(OldValueConditions.users.excludeUsers), 93 | CountOldRoleExcludes = array_length(OldValueConditions.users.excludeRoles), 94 | CountOldGroupExcludes = array_length(OldValueConditions.users.excludeGroups), 95 | CountOldApplicationExcludes = array_length(OldValueConditions.applications.excludeApplications), 96 | CountOldLocationExcludes = array_length(OldValueConditions.locations.excludeLocations), 97 | CountOldPlatformExcludes = array_length(OldValueConditions.platforms.excludePlatforms) 98 | // Alert when includes are taken away and excludes are added, application filter changes, or AppType changes 99 | | extend Reasons = dynamic([]) 100 | | extend Reasons = iff(CountNewUserIncludes < CountOldUserIncludes, array_concat(Reasons, dynamic(["User removed from include"])), Reasons) 101 | | extend Reasons = iff(CountNewRoleIncludes < CountOldRoleIncludes, array_concat(Reasons, dynamic(["Role removed from include"])), Reasons) 102 | | extend Reasons = iff(CountNewGroupIncludes < CountOldGroupIncludes, array_concat(Reasons, dynamic(["Group removed from include"])), Reasons) 103 | | extend Reasons = iff(CountNewUserExcludes > CountOldUserExcludes, array_concat(Reasons, dynamic(["User added to exclude"])), Reasons) 104 | | extend Reasons = iff(CountNewRoleExcludes > CountOldRoleExcludes, array_concat(Reasons, dynamic(["Role added to exclude"])), Reasons) 105 | | extend Reasons = iff(CountNewGroupExcludes > CountOldGroupExcludes, array_concat(Reasons, dynamic(["Group added to exclude"])), Reasons) 106 | | extend Reasons = iff(CountNewUserActionIncludes < CountOldUserActionIncludes, array_concat(Reasons, dynamic(["User action removed from include"])), Reasons) 107 | | extend Reasons = iff(CountNewAuthContextIncludes < CountOldAuthContextIncludes, array_concat(Reasons, dynamic(["Authentication context removed from include"])), Reasons) 108 | | extend Reasons = iff(CountNewApplicationIncludes < CountOldApplicationIncludes, array_concat(Reasons, dynamic(["Application removed from include"])), Reasons) 109 | | extend Reasons = iff(CountNewApplicationExcludes > CountOldApplicationExcludes, array_concat(Reasons, dynamic(["Application added to exclude"])), Reasons) 110 | | extend Reasons = iff(CountNewLocationIncludes < CountOldLocationIncludes, array_concat(Reasons, dynamic(["Locations removed from include"])), Reasons) 111 | | extend Reasons = iff(CountNewLocationExcludes > CountOldLocationExcludes, array_concat(Reasons, dynamic(["Locations added to exclude"])), Reasons) 112 | | extend Reasons = iff(CountNewPlatformIncludes < CountOldPlatformIncludes, array_concat(Reasons, dynamic(["Platforms removed from include"])), Reasons) 113 | | extend Reasons = iff(CountNewPlatformExcludes > CountOldPlatformExcludes, array_concat(Reasons, dynamic(["Platforms added to exclude"])), Reasons) 114 | // Flag general changes 115 | | extend Reasons = iff(tostring(NewValueConditions.applications.applicationFilter) != tostring(OldValueConditions.applications.applicationFilter), array_concat(Reasons, dynamic(["Application filter changed"])), Reasons) 116 | | extend Reasons = iff(tostring(NewValueConditions.clientAppTypes) != tostring(OldValueConditions.clientAppTypes), array_concat(Reasons, dynamic(["Client app type changed"])), Reasons) 117 | | extend Reasons = iff(tostring(NewValueConditions.userRiskLevels) != tostring(OldValueConditions.userRiskLevels), array_concat(Reasons, dynamic(["User risk levels changed"])), Reasons) 118 | | extend Reasons = iff(tostring(NewValueConditions.signInRiskLevels) != tostring(OldValueConditions.signInRiskLevels), array_concat(Reasons, dynamic(["Sign-in risk levels changed"])), Reasons) 119 | | extend Reasons = iff(tostring(NewValueConditions.servicePrincipalRiskLevels) != tostring(OldValueConditions.servicePrincipalRiskLevels), array_concat(Reasons, dynamic(["Service Principal risk levels changed"])), Reasons) 120 | | extend Reasons = iff(tostring(NewValueGrandControls) != tostring(OldValueGrandControls), array_concat(Reasons, dynamic(["Grant controls changed"])), Reasons) 121 | | extend Reasons = iff(tostring(NewValueSessionControls) != tostring(OldValueSessionControls), array_concat(Reasons, dynamic(["Session controls changed"])), Reasons) 122 | | extend Reasons = iff(tostring(NewValueConditions.devices) != tostring(OldValueConditions.devices), array_concat(Reasons, dynamic(["Device conditions changed"])), Reasons) 123 | // Flag Change from include 'all' to only include specifics (since this can evade the count detections) 124 | | extend Reasons = iff(tostring(OldValueConditions.locations.includeLocations) contains "all" and tostring(NewValueConditions.locations.includeLocations) !contains "all", array_concat(Reasons, dynamic(["Include locations changed from all to specific"])), Reasons) 125 | | extend Reasons = iff(tostring(OldValueConditions.platforms.includePlatforms) contains "all" and tostring(NewValueConditions.platforms.includePlatforms) !contains "all", array_concat(Reasons, dynamic(["Include platforms changed from all to specific"])), Reasons) 126 | | extend Reasons = iff(tostring(OldValueConditions.users.includeUsers) contains "all" and tostring(NewValueConditions.users.includeUsers) !contains "all", array_concat(Reasons, dynamic(["Include users changed from all to specific"])), Reasons) 127 | | extend Reasons = iff(tostring(OldValueConditions.applications.includeApplications) contains "all" and tostring(NewValueConditions.applications.includeApplications) !contains "all", array_concat(Reasons, dynamic(["Include applications changed from all to specific"])), Reasons) 128 | // Flag state change to inactive 129 | | extend Reasons = iff(tostring(OldState) == "enabled" and tostring(NewState) != "enabled", array_concat(Reasons, dynamic(["Policy was disabled"])), Reasons) 130 | // Flag policy deletion 131 | | extend Reasons = iff(OperationName == "Delete conditional access policy", array_concat(Reasons, dynamic(["Policy was deleted"])), Reasons); 132 | // Get trusted named location changes 133 | let named_locations = AuditLogs 134 | // Get named location changes 135 | | where TimeGenerated > ago(24h) 136 | | where OperationName in ("Add named location", "Update named location") 137 | // Expand Target resources and the modified properties 138 | | mv-expand TargetResources 139 | | mv-expand TargetResources.modifiedProperties 140 | // Always flag when the named location is trusted 141 | | extend NewValueIsTrusted = parse_json(tostring(parse_json(TargetResources_modifiedProperties.newValue))).isTrusted 142 | | where NewValueIsTrusted == "true" 143 | // Add reason 144 | | extend Reasons = dynamic([]) 145 | | extend Reasons = iff(OperationName == "Add named location", array_concat(Reasons, dynamic(["Trusted named location was added"])), Reasons) 146 | | extend Reasons = iff(OperationName == "Update named location", array_concat(Reasons, dynamic(["Trusted named location was updated"])), Reasons); 147 | // Get changes to groups used in CA policies 148 | let remove_from_include_group = AuditLogs 149 | | where TimeGenerated > ago(24h) 150 | | where OperationName == "Remove member from group" 151 | // Expand Target resources and the modified properties 152 | | mv-expand TargetResources 153 | | mv-expand TargetResources.modifiedProperties 154 | // Search for the display name of the edited group and find groups with CA naming convention 155 | | where TargetResources_modifiedProperties.displayName == "Group.DisplayName" and TargetResources_modifiedProperties contains ca_include_naming_convention 156 | // Add reason 157 | | extend Reasons = dynamic([]) 158 | | extend Reasons = dynamic(["Member removed from include group used in CA policy"]); 159 | let add_to_exclude_group = AuditLogs 160 | | where TimeGenerated > ago(24h) 161 | | where OperationName == "Add member to group" 162 | // Expand Target resources and the modified properties 163 | | mv-expand TargetResources 164 | | mv-expand TargetResources.modifiedProperties 165 | // Search for the display name of the edited group and find groups with CA naming convention 166 | | where TargetResources_modifiedProperties.displayName == "Group.DisplayName" and TargetResources_modifiedProperties contains ca_exclude_naming_convention 167 | // Add reason 168 | | extend Reasons = dynamic([]) 169 | | extend Reasons = dynamic(["Member added to exclude group used in CA policy"]); 170 | // Union all detections 171 | union policy_changes, named_locations, remove_from_include_group, add_to_exclude_group 172 | // Check if reason array is empty 173 | | where Reasons != "[]" 174 | // Sorting and project 175 | | sort by TimeGenerated desc 176 | | project TimeGenerated, OperationName, InitiatedBy, LoggedByService, Result, TargetResources, AADOperationType, Reasons 177 | | extend UserPrincipalName = tostring(InitiatedBy.user.userPrincipalName) 178 | // Look for PIM activations from the same user who performed changes 179 | | join kind=leftouter ca_pim_activations on UserPrincipalName 180 | | project-away UserPrincipalName1 181 | // Check if PIM was justified for user, and only show non-justified PIMs 182 | | extend JustifiedPIM = iff(isnotempty(PimStartTime) and isnotempty(PimExpirationTime) and TimeGenerated between (todatetime(PimStartTime) .. todatetime(PimExpirationTime)), true, false) 183 | | where JustifiedPIM == false 184 | ``` --------------------------------------------------------------------------------