Tool

Hunt pack: Agrius

1,106 vendor-native detections · ready to paste into your SIEM · cross-linked to ATT&CK
hunt pack: Agrius ×
Vendor-native detections covering the ATT&CK techniques attributed to Agrius - a ready-to-deploy hunt pack across Splunk, Elastic and Sentinel.

Detections

50 shown of 1,106
Microsoft Sentinel Original KQL T1078.004 ↗
End-user consent stopped due to risk-based consent
'Detects a user's consent to an OAuth application being blocked due to it being too risky. These events should be investigated to understand why the user attempted to consent to the applicaiton and what other applicaitons they may have consented to. Ref: https://docs.microsoft.com/azure/active-directory/fundamentals/security-operations-applications#end-user-stopped-due-to-risk-based-consent'
Show query
AuditLogs
  | where OperationName has "Consent to application"
  | where Result =~ "failure"
  | extend InitiatingUserPrincipalName = tostring(InitiatedBy.user.userPrincipalName)
  | extend InitiatingAadUserId = tostring(InitiatedBy.user.id)
  | extend InitiatingIPAddress = tostring(InitiatedBy.user.ipAddress)
  | extend userAgent = iif(AdditionalDetails[0].key == "User-Agent", tostring(AdditionalDetails[0].value), tostring(AdditionalDetails[1].value))
  | where isnotempty(TargetResources)
  | extend TargetAppName = tostring(TargetResources[0].displayName)
  | extend TargetAppId = tostring(TargetResources[0].id)
  | mv-expand TargetResources[0].modifiedProperties
  | extend TargetResources_0_modifiedProperties = columnifexists("TargetResources_0_modifiedProperties", '')
  | where isnotempty(TargetResources_0_modifiedProperties)
  | where TargetResources_0_modifiedProperties.displayName =~ "MethodExecutionResult."
  | extend TargetPropertyDisplayName = tostring(TargetResources_0_modifiedProperties.displayName)
  | extend FailureReason = tostring(parse_json(tostring(TargetResources_0_modifiedProperties.newValue)))
  | where FailureReason contains "Risky"
  | extend InitiatingAccountName = tostring(split(InitiatingUserPrincipalName, "@")[0]), InitiatingAccountUPNSuffix = tostring(split(InitiatingUserPrincipalName, "@")[1])
  | project-reorder TimeGenerated, OperationName, Result, TargetAppName, TargetAppId, FailureReason, InitiatingUserPrincipalName, InitiatingAadUserId, InitiatingIPAddress, userAgent
Microsoft Sentinel Original KQL T1071 ↗
Europium - Hash and IP IOCs - September 2022
'Identifies a match across various data feeds for hashes and IP IOC related to Europium Reference: https://www.microsoft.com/security/blog/2022/09/08/microsoft-investigates-iranian-attacks-against-the-albanian-government'
Show query
let iocs = externaldata(DateAdded:string,IoC:string,Type:string,TLP:string) [@"https://raw.githubusercontent.com/Azure/Azure-Sentinel/master/Sample%20Data/Feeds/Europium_September2022.csv"] with (format="csv", ignoreFirstRecord=True);
let sha256Hashes = (iocs | where Type =~ "sha256" | project IoC);
let IPList = (iocs | where Type =~ "ip"| project IoC);
let IPRegex = '[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}';
(union isfuzzy=true
(CommonSecurityLog
| where SourceIP in (IPList) or DestinationIP in (IPList) or Message has_any (IPList)
| parse Message with * '(' DNSName ')' * 
| project TimeGenerated, SourceIP, DestinationIP, Message, SourceUserID, RequestURL, DNSName, Type
| extend MessageIP = extract(IPRegex, 0, Message), RequestIP = extract(IPRegex, 0, RequestURL)
| extend IPMatch = case(SourceIP in (IPList), "SourceIP", DestinationIP in (IPList), "DestinationIP", MessageIP in (IPList), "Message",  "NoMatch")
| extend timestamp = TimeGenerated, IPEntity = case(IPMatch == "SourceIP", SourceIP, IPMatch == "DestinationIP", DestinationIP, IPMatch == "Message", MessageIP, "NoMatch")
),
(DnsEvents
| where IPAddresses in (IPList)  
| project TimeGenerated, Computer, IPAddresses, Name, ClientIP, Type
| extend DestinationIPAddress = IPAddresses, DNSName = Name, Computer 
| extend timestamp = TimeGenerated, IPEntity = DestinationIPAddress, HostEntity = Computer
),
(VMConnection
| where SourceIp in (IPList) or DestinationIp in (IPList)
| parse RemoteDnsCanonicalNames with * '["' DNSName '"]' *
| project TimeGenerated, Computer, Direction, ProcessName, SourceIp, DestinationIp, DestinationPort, RemoteDnsQuestions, DNSName,BytesSent, BytesReceived, RemoteCountry, Type
| extend IPMatch = case( SourceIp in (IPList), "SourceIP", DestinationIp in (IPList), "DestinationIP", "None") 
| extend timestamp = TimeGenerated, IPEntity = case(IPMatch == "SourceIP", SourceIp, IPMatch == "DestinationIP", DestinationIp, "NoMatch"), File = ProcessName, HostEntity = Computer
),
(Event
| where Source == "Microsoft-Windows-Sysmon"
| where EventID == 3
| extend EvData = parse_xml(EventData)
| extend EventDetail = EvData.DataItem.EventData.Data
| extend SourceIP = tostring(EventDetail.[9].["#text"]), DestinationIP = tostring(EventDetail.[14].["#text"]), Image = tostring(EventDetail.[4].["#text"])
| where SourceIP in (IPList) or DestinationIP in (IPList)
| project TimeGenerated, SourceIP, DestinationIP, Image, UserName, Computer, Type
| extend IPMatch = case( SourceIP in (IPList), "SourceIP", DestinationIP in (IPList), "DestinationIP", "None")
| extend timestamp = TimeGenerated, File = tostring(split(Image, '\\', -1)[-1]), IPEntity = case(IPMatch == "SourceIP", SourceIP, IPMatch == "DestinationIP", DestinationIP, "None"), 
HostEntity = Computer, AccountName = tostring(split(UserName, @'\')[1]), AccountDomain = tostring(split(UserName, @'\')[0])
| extend InitiatingProcessAccount = UserName
), 
(OfficeActivity
| where ClientIP in (IPList) 
| project TimeGenerated, UserAgent, Operation, RecordType, UserId, ClientIP, Type
| extend timestamp = TimeGenerated, IPEntity = ClientIP, AccountName = tostring(split(UserId, "@")[0]), AccountDomain = tostring(split(UserId, "@")[1])
| extend InitiatingProcessAccount = UserId
),
(DeviceNetworkEvents
| where RemoteIP in (IPList) or InitiatingProcessSHA256 in (sha256Hashes)
| project TimeGenerated, ActionType, DeviceId, Computer = DeviceName, InitiatingProcessAccountDomain, InitiatingProcessAccountName, InitiatingProcessCommandLine, 
InitiatingProcessFolderPath, InitiatingProcessId, InitiatingProcessParentFileName, InitiatingProcessFileName, RemoteIP, RemoteUrl, RemotePort, LocalIP, Type
| extend timestamp = TimeGenerated, IPEntity = RemoteIP, HostEntity = Computer, AccountName = InitiatingProcessAccountName, AccountDomain = InitiatingProcessAccountDomain
| extend InitiatingProcessAccount = strcat(AccountDomain, "\\", AccountName)
),
(WindowsFirewall
| where SourceIP in (IPList) or DestinationIP in (IPList) 
| project TimeGenerated, Computer, CommunicationDirection, SourceIP, DestinationIP, SourcePort, DestinationPort, Type
| extend IPMatch = case( SourceIP in (IPList), "SourceIP", DestinationIP in (IPList), "DestinationIP", "None")
| extend timestamp = TimeGenerated, HostEntity = Computer, IPEntity = case(IPMatch == "SourceIP", SourceIP, IPMatch == "DestinationIP", DestinationIP, "None")
), 
(imFileEvent
| where TargetFileSHA256 has_any (sha256Hashes)
| extend Account = ActorUsername, Computer = DvcHostname, IPAddress = SrcIpAddr, CommandLine = ActingProcessCommandLine, FileHash = TargetFileSHA256
| project Type, TimeGenerated, Computer, Account, IPAddress, CommandLine, FileHash
| extend timestamp = TimeGenerated, IPEntity = IPAddress,  HostEntity = Computer, Algorithm = "SHA256", FileHash = tostring(FileHash)
| extend AccountName = tostring(split(Account, @'\')[1]), AccountDomain = tostring(split(Account, @'\')[0])
| extend InitiatingProcessAccount = Account
),
(DeviceFileEvents
| where SHA256 has_any (sha256Hashes)
| project TimeGenerated, ActionType, DeviceId, DeviceName, InitiatingProcessAccountDomain, InitiatingProcessAccountName, InitiatingProcessCommandLine, InitiatingProcessFolderPath, 
InitiatingProcessId, InitiatingProcessParentFileName, InitiatingProcessFileName, InitiatingProcessSHA256, Type
| extend timestamp = TimeGenerated, HostEntity = DeviceName, AccountName = InitiatingProcessAccountName, AccountDomain = InitiatingProcessAccountDomain, 
Algorithm = "SHA256", FileHash = tostring(InitiatingProcessSHA256), CommandLine = InitiatingProcessCommandLine,Image = InitiatingProcessFolderPath
| extend InitiatingProcessAccount = strcat(AccountDomain, "\\", AccountName)
),
(DeviceImageLoadEvents
| where SHA256 has_any (sha256Hashes)
| project TimeGenerated, ActionType, DeviceId, DeviceName, InitiatingProcessAccountDomain, InitiatingProcessAccountName, InitiatingProcessCommandLine, InitiatingProcessFolderPath, 
InitiatingProcessId, InitiatingProcessParentFileName, InitiatingProcessFileName, InitiatingProcessSHA256, Type
| extend timestamp = TimeGenerated, HostEntity = DeviceName, AccountName = InitiatingProcessAccountName, AccountDomain = InitiatingProcessAccountDomain, 
Algorithm = "SHA256", FileHash = tostring(InitiatingProcessSHA256),  CommandLine = InitiatingProcessCommandLine,Image = InitiatingProcessFolderPath
| extend InitiatingProcessAccount = strcat(AccountDomain, "\\", AccountName)
),
(Event
| where Source =~ "Microsoft-Windows-Sysmon"
| where EventID == 1
| extend EvData = parse_xml(EventData)
| extend EventDetail = EvData.DataItem.EventData.Data
| extend Image = EventDetail.[4].["#text"],  CommandLine = EventDetail.[10].["#text"], Hashes = tostring(EventDetail.[17].["#text"])
| extend Hashes = extract_all(@"(?P<key>\w+)=(?P<value>[a-zA-Z0-9]+)", dynamic(["key","value"]), Hashes)
| extend Hashes = column_ifexists("Hashes", dynamic(["", ""])), CommandLine = column_ifexists("CommandLine", "")
| mv-expand Hashes
| where Hashes[0] =~ "SHA256" and Hashes[1] has_any (sha256Hashes)  
| project TimeGenerated, EventDetail, UserName, Computer, Type, Source, Hashes, CommandLine, Image
| extend Type = strcat(Type, ": ", Source)
| extend timestamp = TimeGenerated, HostEntity = Computer, AccountName = tostring(split(UserName, @'\')[1]), AccountUPNSuffix = tostring(split(UserName, @'\')[0]), FileHash = tostring(Hashes[1])
| extend InitiatingProcessAccount = UserName
)
)
| extend HostName = tostring(split(HostEntity, ".")[0]), DomainIndex = toint(indexof(HostEntity, '.'))
| extend HostNameDomain = iff(DomainIndex != -1, substring(HostEntity, DomainIndex + 1), HostEntity)
Microsoft Sentinel Original KQL T1190 ↗
Exchange SSRF Autodiscover ProxyShell - Detection
'This query looks for suspicious request patterns to Exchange servers that fit patterns recently blogged about by PeterJson. This exploitation chain utilises an SSRF vulnerability in Exchange which eventually allows the attacker to execute arbitrary Powershell on the server. In the example powershell can be used to write an email to disk with an encoded attachment containing a shell. Reference: https://peterjson.medium.com/reproducing-the-proxyshell-pwn2own-exploit-49743a4ea9a1'
Show query
let successCodes = dynamic([200, 302, 401]);
W3CIISLog
| where scStatus has_any (successCodes)
| where ipv4_is_private(cIP) == False
| where csUriStem hasprefix "/autodiscover/autodiscover.json"
| project TimeGenerated, cIP, sIP, sSiteName, csUriStem, csUriQuery, Computer, csUserName, _ResourceId, FileUri
| where (csUriQuery !has "Protocol" and isnotempty(csUriQuery))
or (csUriQuery has_any("/mapi/", "powershell"))
or (csUriQuery contains "@" and csUriQuery matches regex @"\.[a-zA-Z]{2,4}?(?:[a-zA-Z]{2,4}\/)")
or (csUriQuery contains ":" and csUriQuery matches regex @"\:[0-9]{2,4}\/")
| extend HostName = tostring(split(Computer, ".")[0]), DomainIndex = toint(indexof(Computer, '.'))
| extend HostNameDomain = iff(DomainIndex != -1, substring(Computer, DomainIndex + 1), Computer)
| extend AccountName = tostring(split(csUserName, "@")[0]), AccountUPNSuffix = tostring(split(csUserName, "@")[1])
Microsoft Sentinel Original KQL T1190 ↗
Exchange Server Suspicious File Downloads.
'This query looks for messages related to file downloads of suspicious file types on an Exchange Server. This could indicate attempted deployment of webshells. This query uses the Exchange HttpProxy AOBGeneratorLog, you will need to onboard this log as a custom log under the table http_proxy_oab_CL before using this query. This log is commonly found at C:\Program Files\Microsoft\Exchange Server\V15\Logging\OABGeneratorLog on the Exchange server. Details on collecting custom logs into Sentinel
Show query
let scriptExtensions = dynamic([".php", ".jsp", ".js", ".aspx", ".asmx", ".asax", ".cfm", ".shtml"]);
http_proxy_oab_CL
| where RawData contains "Download failed and temporary file"
| extend File = extract("([^\\\\]*)(\\\\[^']*)",2,RawData)
| extend Extension = strcat(".",split(File, ".")[-1])
| extend InteractiveFile = iif(Extension in (scriptExtensions), "Yes", "No")
// Uncomment the following line to alert only on interactive file download type
//| where InteractiveFile =~ "Yes"
| extend HostName = tostring(split(Computer, ".")[0]), DomainIndex = toint(indexof(Computer, '.'))
| extend HostNameDomain = iff(DomainIndex != -1, substring(Computer, DomainIndex + 1), Computer)
Microsoft Sentinel Original KQL T1059.001 ↗
Exchange Worker Process Making Remote Call
'This query dynamically identifies Exchange servers and then looks for instances where the IIS worker process initiates a call out to a remote URL using either cmd.exe or powershell.exe. This behaviour was described as post-compromise behaviour following exploitation of CVE-2022-41040 and CVE-2022-41082, this pattern of activity was use to download additional tools to the server. This suspicious activity is generic.'
Show query
let suspiciousCmdLineKeywords = dynamic(["http://", "https://"]);
// Identify exchange servers based on known paths
// Summarize these to get a list of exchange server hostnames
let exchangeServers = W3CIISLog
| where csUriStem has_any("/owa/","/ews/","/ecp/","/autodiscover/")
// Only where successful, rule out failed scanning
| where scStatus startswith "2"
| summarize by Computer;
DeviceProcessEvents
| where DeviceName in~ (exchangeServers)
// Where the IIS worker process initiated CMD or PowerShell
| where InitiatingProcessParentFileName == "w3wp.exe"
| where InitiatingProcessFileName has_any("cmd.exe", "powershell.exe")
// Where CMD or PowerShell command line included parameters associated with CVE-2022-41040/CVE-2022-41082 exploitation
| where ProcessCommandLine has_any(suspiciousCmdLineKeywords)
| project TimeGenerated, DeviceId, DeviceName, InitiatingProcessFileName, ProcessCommandLine, AccountDomain = InitiatingProcessAccountDomain, AccountName = InitiatingProcessAccountName
| extend Account = strcat(AccountDomain, "\\", AccountName)
| extend HostName = tostring(split(DeviceName, ".")[0]), DomainIndex = toint(indexof(DeviceName, '.'))
| extend HostNameDomain = iff(DomainIndex != -1, substring(DeviceName, DomainIndex + 1), DeviceName)
Microsoft Sentinel Original KQL T1078 ↗
Failed AWS Console logons but success logon to AzureAD
'Identifies a list of IP addresses with a minimum number (default of 5) of failed logon attempts to AWS Console. Uses that list to identify any successful Microsoft Entra ID logons from these IPs within the same timeframe.'
Show query
//Adjust this threshold to fit environment
let  signin_threshold = 5;
//Make a list of IPs with failed AWS console logins
let aws_fails = AWSCloudTrail
| where EventName == "ConsoleLogin"
| extend LoginResult = tostring(parse_json(ResponseElements).ConsoleLogin)
| where LoginResult != "Success"
| where SourceIpAddress != "127.0.0.1"
| summarize count() by SourceIpAddress
| where count_ >  signin_threshold
| summarize make_set(SourceIpAddress);
//See if any of those IPs have sucessfully logged into Azure AD.
SigninLogs
| where ResultType in ("0", "50125", "50140")
| where IPAddress in (aws_fails)
| extend Reason = "Multiple failed AWS Console logins from IP address"
| extend timestamp = TimeGenerated, AccountName = tostring(split(UserPrincipalName, "@")[0]), AccountUPNSuffix = tostring(split(UserPrincipalName, "@")[1])
Microsoft Sentinel Original KQL T1078 ↗
Failed AzureAD logons but success logon to AWS Console
'Identifies a list of IP addresses with a minimum number (defualt of 5) of failed logon attempts to Microsoft Entra ID. Uses that list to identify any successful AWS Console logons from these IPs within the same timeframe.'
Show query
//Adjust this threshold to fit your environment
let signin_threshold = 5;
//Make a list of IPs with AAD signin failures above our threshold
let aadFunc = (tableName:string){
let Suspicious_signins =
table(tableName)
| where ResultType !in ("0", "50125", "50140")
| where IPAddress !in ("127.0.0.1", "::1")
| summarize count() by IPAddress
| where count_ >  signin_threshold
| summarize make_set(IPAddress);
Suspicious_signins
};
let aadSignin = aadFunc("SigninLogs");
let aadNonInt = aadFunc("AADNonInteractiveUserSignInLogs");
let Suspicious_signins =
union isfuzzy=true aadSignin, aadNonInt
| summarize make_set(set_IPAddress);
//See if any of those IPs have sucessfully logged into the AWS console
AWSCloudTrail
| where EventName =~ "ConsoleLogin"
| extend LoginResult = tostring(parse_json(ResponseElements).ConsoleLogin)
| where LoginResult =~ "Success"
| where SourceIpAddress in (Suspicious_signins)
| extend Reason = "Multiple failed AAD logins from IP address"
| extend MFAUsed = tostring(parse_json(AdditionalEventData).MFAUsed)
| extend UserIdentityArn = iif(isempty(UserIdentityArn), tostring(parse_json(Resources)[0].ARN), UserIdentityArn)
| extend UserName = tostring(split(UserIdentityArn, '/')[-1])
| extend AccountName = case( UserIdentityPrincipalid == "Anonymous", "Anonymous", isempty(UserIdentityUserName), UserName, UserIdentityUserName)
| extend AccountName = iif(AccountName contains "@", tostring(split(AccountName, '@', 0)[0]), AccountName),
  AccountUPNSuffix = iif(AccountName contains "@", tostring(split(AccountName, '@', 1)[0]), "")
| summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated) by Reason, LoginResult, EventTypeName, UserIdentityType, RecipientAccountId, AccountName, AccountUPNSuffix, AWSRegion, SourceIpAddress, UserAgent, MFAUsed
| extend timestamp = StartTime
Microsoft Sentinel Original KQL T1078 ↗
Failed AzureAD logons but success logon to host
'Identifies a list of IP addresses with a minimum number (default of 5) of failed logon attempts to Microsoft Entra ID. Uses that list to identify any successful remote logons to hosts from these IPs within the same timeframe.'
Show query
//Adjust this threshold to fit the environment
let signin_threshold = 5;
//Make a list of all IPs with failed signins to AAD above our threshold
let aadFunc = (tableName:string){
let suspicious_signins =
table(tableName)
| where ResultType !in ("0", "50125", "50140")
| where IPAddress !in ('127.0.0.1', '::1', '')
| summarize count() by IPAddress
| where count_ > signin_threshold
| summarize make_set(IPAddress);
//See if any of these IPs have sucessfully logged into *nix hosts
let linux_logons =
Syslog
| where Facility contains "auth" and ProcessName != "sudo"
| where SyslogMessage has "Accepted"
| extend SourceIP = extract("(([0-9]{1,3})\\.([0-9]{1,3})\\.([0-9]{1,3})\\.(([0-9]{1,3})))",1,SyslogMessage)
| where SourceIP in (suspicious_signins)
| extend Reason = "Multiple failed AAD logins from IP address"
| project TimeGenerated, Computer, HostIP, IpAddress = SourceIP, SyslogMessage, Facility, ProcessName, Reason;
//See if any of these IPs have sucessfully logged into Windows hosts
let win_logons = (union isfuzzy=true
(SecurityEvent
| where EventID == 4624
| where LogonType in (10, 7, 3)
| where IpAddress != "-"
| where IpAddress in (suspicious_signins)
| extend Reason = "Multiple failed AAD logins from IP address"
| project TimeGenerated, Account, AccountType, Computer, Activity, EventID, LogonProcessName, IpAddress, LogonTypeName, TargetUserSid, TargetUserName, TargetDomainName, _ResourceId, Reason
),
(WindowsEvent
| where EventID == 4624 and has_any_ipv4(EventData, toscalar(suspicious_signins))
| extend LogonType = tostring(EventData.LogonType)
| where LogonType in (10, 7, 3)
| extend  IpAddress = tostring(EventData.IpAddress)
| where IpAddress != "-"
| where IpAddress in (suspicious_signins)
| extend Reason = "Multiple failed AAD logins from IP address"
| extend Activity = "4624 - An account was successfully logged on."
| extend TargetUserName = tostring(EventData.TargetUserName), TargetDomainName = tostring(EventData.TargetDomainName)
| extend Account =  strcat(TargetDomainName,"\\", TargetUserName)
| extend TargetUserSid = tostring(EventData.TargetUserSid)
| extend TargetAccount = strcat(EventData.TargetDomainName,"\\", EventData.TargetUserName)
| extend AccountType =case(Account endswith "$" or TargetUserSid in ("S-1-5-18", "S-1-5-19", "S-1-5-20"), "Machine", isempty(TargetUserSid), "", "User")
| extend LogonProcessName = tostring(EventData.LogonProcessName)
| project TimeGenerated, Account, AccountType, Computer, Activity, EventID, LogonProcessName, IpAddress, TargetUserSid, TargetUserName, TargetDomainName, _ResourceId, Reason
)
);
union isfuzzy=true linux_logons,win_logons
| extend timestamp = TimeGenerated
| extend HostName = tostring(split(Computer, ".")[0]), DomainIndex = toint(indexof(Computer, '.'))
| extend HostNameDomain = iff(DomainIndex != -1, substring(Computer, DomainIndex+1), Computer)
};
let aadSignin = aadFunc("SigninLogs");
let aadNonInt = aadFunc("AADNonInteractiveUserSignInLogs");
union isfuzzy=true aadSignin, aadNonInt
Microsoft Sentinel Original KQL T1078 ↗
Failed host logons but success logon to AzureAD
'Identifies a list of IP addresses with a minimum number(default of 5) of failed logon attempts to remote hosts. Uses that list to identify any successful logons to Microsoft Entra ID from these IPs within the same timeframe.'
Show query
//Adjust this threshold to fit environment
let signin_threshold = 5; 
//Make a list of IPs with failed Windows host logins above threshold
let win_fails = 
SecurityEvent
| where EventID == 4625
| where LogonType in (10, 7, 3)
| where IpAddress != "-"
| summarize count() by IpAddress
| where count_ > signin_threshold
| summarize make_list(IpAddress);
let wef_fails =
WindowsEvent
| where EventID == 4625
| extend LogonType = tostring(EventData.LogonType)
| where LogonType in (10, 7, 3)
| extend IpAddress = tostring(EventData.IpAddress)
| where IpAddress != "-"
| summarize count() by IpAddress
| where count_ > signin_threshold
| summarize make_list(IpAddress);
//Make a list of IPs with failed *nix host logins above threshold
let nix_fails = 
Syslog
| where Facility contains 'auth' and ProcessName != 'sudo' and SyslogMessage has 'from' and not(SyslogMessage has_any ('Disconnecting', 'Disconnected', 'Accepted', 'disconnect', @'[preauth]'))
| extend SourceIP = extract("(([0-9]{1,3})\\.([0-9]{1,3})\\.([0-9]{1,3})\\.(([0-9]{1,3})))",1,SyslogMessage)
| where SourceIP != "" and SourceIP != "127.0.0.1"
| summarize count() by SourceIP
| where count_ > signin_threshold
| summarize make_list(SourceIP);
//See if any of the IPs with failed host logins hve had a sucessful Azure AD login
let aadFunc = (tableName:string){
table(tableName)
| where ResultType in ("0", "50125", "50140")
| where IPAddress in (win_fails) or IPAddress in (nix_fails) or IPAddress in (wef_fails)
| extend Reason=  "Multiple failed host logins from IP address with successful Azure AD login"
| extend timestamp = TimeGenerated, Type = Type
| extend AccountName = tostring(split(UserPrincipalName, "@")[0]), AccountUPNSuffix = tostring(split(UserPrincipalName, "@")[1])
};
let aadSignin = aadFunc("SigninLogs");
let aadNonInt = aadFunc("AADNonInteractiveUserSignInLogs");
union isfuzzy=true aadSignin, aadNonInt
Microsoft Sentinel Original KQL T1071 ↗
Fortinet - Beacon pattern detected
'Identifies patterns in the time deltas of contacts between internal and external IPs in Fortinet network data that are consistent with beaconing. Accounts for randomness (jitter) and seasonality such as working hours that may have been introduced into the beacon pattern. The lookback is set to 1d, the minimum granularity in time deltas is set to 60 seconds and the minimum number of beacons required to emit a detection is set to 4. Increase the lookback period to capture beacons with larger
Show query
let starttime = 1d;
let TimeDeltaThresholdInSeconds = 60; // we ignore beacons diffs that fall below this threshold
let TotalBeaconsThreshold = 4; // minimum number of beacons required in a session to surface a row
let JitterTolerance = 0.2; // tolerance to jitter, e.g. - 0.2 = 20% jitter is tolerated either side of the periodicity
CommonSecurityLog
| where DeviceVendor == "Fortinet"
| where TimeGenerated > ago(starttime)
// eliminate bad data
| where isnotempty(SourceIP) and isnotempty(DestinationIP) and SourceIP != "0.0.0.0"
// filter out deny, close, rst and SNMP to reduce data volume
| where DeviceAction !in ("close", "client-rst", "server-rst", "deny") and DestinationPort != 161
// map input fields
| project TimeGenerated , SourceIP, DestinationIP, DestinationPort, ReceivedBytes, SentBytes, DeviceAction
// where destination IPs are public
| where ipv4_is_private(DestinationIP) == false
// sort into source->destination 'sessions'
| sort by SourceIP asc, DestinationIP asc, DestinationPort asc, TimeGenerated asc
| serialize
// time diff the contact times between source and destination to get a list of deltas
| extend nextTimeGenerated = next(TimeGenerated, 1), nextSourceIP = next(SourceIP, 1), nextDestIP = next(DestinationIP, 1), nextDestPort = next(DestinationPort, 1)
| extend TimeDeltainSeconds = datetime_diff("second",nextTimeGenerated,TimeGenerated)
| where SourceIP == nextSourceIP and DestinationIP == nextDestIP and DestinationPort == nextDestPort
// remove small time deltas below the set threshold
| where TimeDeltainSeconds > TimeDeltaThresholdInSeconds
// summarize the deltas by source->destination
| summarize count(), StartTime=min(TimeGenerated), EndTime=max(TimeGenerated), sum(ReceivedBytes), sum(SentBytes), makelist(TimeDeltainSeconds), makeset(DeviceAction) by SourceIP, DestinationIP, DestinationPort
// get some statistical properties of the delta distribution and smooth any outliers (e.g. laptop shut overnight, working hours)
| extend series_stats(list_TimeDeltainSeconds), outliers=series_outliers(list_TimeDeltainSeconds)
// expand the deltas and the outliers
| mvexpand list_TimeDeltainSeconds to typeof(double), outliers to typeof(double)
// replace outliers with the average of the distribution
| extend list_TimeDeltainSeconds_normalized=iff(outliers > 1.5 or outliers < -1.5, series_stats_list_TimeDeltainSeconds_avg , list_TimeDeltainSeconds)
// summarize with the smoothed distribution
| summarize BeaconCount=count(), makelist(list_TimeDeltainSeconds), list_TimeDeltainSeconds_normalized=makelist(list_TimeDeltainSeconds_normalized), makeset(set_DeviceAction) by StartTime, EndTime, SourceIP, DestinationIP, DestinationPort, sum_ReceivedBytes, sum_SentBytes
// get stats on the smoothed distribution
| extend series_stats(list_TimeDeltainSeconds_normalized)
// match jitter tolerance on smoothed distrib
| extend MaxJitter = (series_stats_list_TimeDeltainSeconds_normalized_avg*JitterTolerance)
| where series_stats_list_TimeDeltainSeconds_normalized_stdev < MaxJitter
// where the minimum beacon threshold is satisfied and there was some data transfer
| where BeaconCount > TotalBeaconsThreshold and (sum_SentBytes > 0 or sum_ReceivedBytes > 0)
// final projection
| project StartTime, EndTime, SourceIP, DestinationIP, DestinationPort, BeaconCount, TimeDeltasInSeconds=list_list_TimeDeltainSeconds, Periodicity=series_stats_list_TimeDeltainSeconds_normalized_avg, ReceivedBytes=sum_ReceivedBytes, SentBytes=sum_SentBytes, Actions=set_set_DeviceAction
// where periodicity is order of magnitude larger than time delta threshold (eliminates FPs whose periodicity is close to the values we ignored)
| where Periodicity >= (10*TimeDeltaThresholdInSeconds)
Microsoft Sentinel Original KQL T1078.004 ↗
Guest Users Invited to Tenant by New Inviters
'Detects when a Guest User is added by a user account that has not been seen adding a guest in the previous 14 days. Monitoring guest accounts and the access they are provided is important to detect potential account abuse. Accounts added should be investigated to ensure the activity was legitimate. Ref: https://docs.microsoft.com/azure/active-directory/fundamentals/security-operations-user-accounts#monitoring-for-failed-unusual-sign-ins'
Show query
let inviting_users = (AuditLogs
  | where TimeGenerated between(ago(14d)..ago(1d))
  | where OperationName =~ "Invite external user"
  | where Result =~ "success"
  | extend InitiatingUserPrincipalName = tostring(parse_json(tostring(InitiatedBy.user)).userPrincipalName)
  | where isnotempty(InitiatingUserPrincipalName)
  | summarize by InitiatingUserPrincipalName);
  AuditLogs
  | where TimeGenerated > ago(1d)
  | where OperationName =~ "Invite external user"
  | where Result =~ "success"
  | extend InitiatingUserPrincipalName = tostring(parse_json(tostring(InitiatedBy.user)).userPrincipalName)
  | extend InitiatingAadUserId = tostring(InitiatedBy.user.id)
  | extend InitiatingIPAddress = tostring(InitiatedBy.user.ipAddress)
  | where isnotempty(InitiatingUserPrincipalName) and InitiatingUserPrincipalName !in (inviting_users)
  | extend TargetUserPrincipalName = tostring(TargetResources[0].userPrincipalName)
  | extend TargetAadUserId = tostring(TargetResources[0].id)
  | extend invitingUser = InitiatingUserPrincipalName, invitedUserPrincipalName = TargetUserPrincipalName
  | extend InitiatingAccountName = tostring(split(InitiatingUserPrincipalName, "@")[0]), InitiatingAccountUPNSuffix = tostring(split(InitiatingUserPrincipalName, "@")[1])
  | extend TargetAccountName = tostring(split(TargetUserPrincipalName, "@")[0]), TargetAccountUPNSuffix = tostring(split(TargetUserPrincipalName, "@")[1])
  | project-reorder TimeGenerated, OperationName, Result, TargetUserPrincipalName, TargetAadUserId, InitiatingUserPrincipalName, InitiatingAadUserId, InitiatingIPAddress
Microsoft Sentinel Original KQL T1190 ↗
High count of connections by client IP on many ports
'Identifies when 30 or more ports are used for a given client IP in 10 minutes occurring on the IIS server. This could be indicative of attempted port scanning or exploit attempt at internet facing web applications. This could also simply indicate a misconfigured service or device. References: IIS status code mapping - https://support.microsoft.com/help/943891/the-http-status-code-in-iis-7-0-iis-7-5-and-iis-8-0 Win32 Status code mapping - https://msdn.microsoft.com/library/cc231199.aspx'
Show query
let timeBin = 10m;
let portThreshold = 30;
W3CIISLog
| extend scStatusFull = strcat(scStatus, ".",scSubStatus)
// Map common IIS codes
| extend scStatusFull_Friendly = case(
scStatusFull == "401.0", "Access denied.",
scStatusFull == "401.1", "Logon failed.",
scStatusFull == "401.2", "Logon failed due to server configuration.",
scStatusFull == "401.3", "Unauthorized due to ACL on resource.",
scStatusFull == "401.4", "Authorization failed by filter.",
scStatusFull == "401.5", "Authorization failed by ISAPI/CGI application.",
scStatusFull == "403.0", "Forbidden.",
scStatusFull == "403.4", "SSL required.",
"See - https://support.microsoft.com/help/943891/the-http-status-code-in-iis-7-0-iis-7-5-and-iis-8-0")
// Mapping to Hex so can be mapped using website in comments above
| extend scWin32Status_Hex = tohex(tolong(scWin32Status))
// Map common win32 codes
| extend scWin32Status_Friendly = case(
scWin32Status_Hex =~ "775", "The referenced account is currently locked out and cannot be logged on to.",
scWin32Status_Hex =~ "52e", "Logon failure: Unknown user name or bad password.",
scWin32Status_Hex =~ "532", "Logon failure: The specified account password has expired.",
scWin32Status_Hex =~ "533", "Logon failure: Account currently disabled.",
scWin32Status_Hex =~ "2ee2", "The request has timed out.",
scWin32Status_Hex =~ "0", "The operation completed successfully.",
scWin32Status_Hex =~ "1", "Incorrect function.",
scWin32Status_Hex =~ "2", "The system cannot find the file specified.",
scWin32Status_Hex =~ "3", "The system cannot find the path specified.",
scWin32Status_Hex =~ "4", "The system cannot open the file.",
scWin32Status_Hex =~ "5", "Access is denied.",
scWin32Status_Hex =~ "8009030e", "SEC_E_NO_CREDENTIALS",
scWin32Status_Hex =~ "8009030C", "SEC_E_LOGON_DENIED",
"See - https://msdn.microsoft.com/library/cc231199.aspx")
// decode URI when available
| extend decodedUriQuery = url_decode(csUriQuery)
// Count of attempts by client IP on many ports
| summarize makeset(sPort), makeset(decodedUriQuery), makeset(csUserName), makeset(sSiteName), makeset(sPort), makeset(csUserAgent), makeset(csMethod), makeset(csUriQuery), makeset(scStatusFull), makeset(scStatusFull_Friendly), makeset(scWin32Status_Hex), makeset(scWin32Status_Friendly), ConnectionsCount = count() by bin(TimeGenerated, timeBin), cIP, Computer, sIP
| extend portCount = arraylength(set_sPort)
| where portCount >= portThreshold
| project TimeGenerated, cIP, set_sPort, set_csUserName, set_decodedUriQuery, Computer, set_sSiteName, sIP, set_csUserAgent, set_csMethod, set_scStatusFull, set_scStatusFull_Friendly, set_scWin32Status_Hex, set_scWin32Status_Friendly, ConnectionsCount, portCount
| order by portCount
Microsoft Sentinel Original KQL T1078 ↗
High risk Office operation conducted by IP Address that recently attempted to log into a disabled account
'It is possible that a disabled user account is compromised and another account on the same IP is used to perform operations that are not typical for that user. The query filters the SigninLogs for entries where ResultType is indicates a disabled account and the TimeGenerated is within a defined time range. It then summarizes these entries by IPAddress and AppId, calculating various statistics such as number of login attempts, distinct UPNs, App IDs etc and joins these results with another set
Show query
let threshold = 100;
let timeRange = ago(7d);
let timeBuffer = 1;
SigninLogs 
| where TimeGenerated > timeRange
| where ResultType == "50057" 
| summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated), disabledAccountLoginAttempts = count(), 
disabledAccountsTargeted = dcount(UserPrincipalName), applicationsTargeted = dcount(AppDisplayName), disabledAccountSet = make_set(UserPrincipalName), 
applicationSet = make_set(AppDisplayName) by IPAddress, AppId
| order by disabledAccountLoginAttempts desc
| join kind=inner (
    // IPs are considered suspicious - and any related successful sign-ins are detected
    SigninLogs
    | where TimeGenerated > timeRange
    | where ResultType == 0
    | summarize successSigninStart = min(TimeGenerated), successSigninEnd = max(TimeGenerated), successfulAccountSigninCount = dcount(UserPrincipalName), successfulAccountSigninSet = make_set(UserPrincipalName, 15) by IPAddress
    // Assume IPs associated with sign-ins from 100+ distinct user accounts are safe
    | where successfulAccountSigninCount < threshold
) on IPAddress  
// IPs where attempts to authenticate as disabled user accounts originated, and had a non-zero success rate for some other account
| where successfulAccountSigninCount != 0
// Successful Account Signins occur within the same lookback period as the failed 
| extend SuccessBeforeFailure = iff(successSigninStart >= StartTime and successSigninEnd <= EndTime, true, false)  
| project StartTime, EndTime, IPAddress, disabledAccountLoginAttempts, disabledAccountsTargeted, disabledAccountSet, applicationSet, 
successfulAccountSigninCount, successfulAccountSigninSet, successSigninStart, successSigninEnd, AppId
| order by disabledAccountLoginAttempts
// Break up the string of Succesfully signed into accounts into individual events
| mvexpand successfulAccountSigninSet
| extend JoinedOnIp = IPAddress
| join kind = inner (
      OfficeActivity
    | where TimeGenerated > timeRange
    | where Operation in~ ( "Add-MailboxPermission", "Add-MailboxFolderPermission", "Set-Mailbox", "New-ManagementRoleAssignment", "New-InboxRule", "Set-InboxRule", "Set-TransportRule") and not(UserId has_any ('NT AUTHORITY\\SYSTEM (Microsoft.Exchange.ServiceHost)', 'NT AUTHORITY\\SYSTEM (w3wp)', 'devilfish-applicationaccount'))
    // Remove port from the end of the IP and/or square brackets around IP, if they exist 
    | extend JoinedOnIp = case(
      ClientIP matches regex @'\[((25[0-5]2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]2[0-4][0-9]|[01]?[0-9][0-9]?)\]-\d{1,5}', tostring(extract('\\[([0-9]+\\.[0-9]+\\.[0-9]+)\\]-[0-9]+', 1, ClientIP)),
      ClientIP matches regex @'\[((25[0-5]2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]2[0-4][0-9]|[01]?[0-9][0-9]?)\]', tostring(extract('\\[([0-9]+\\.[0-9]+\\.[0-9]+)\\]', 1, ClientIP)),  
      ClientIP matches regex @'(((25[0-5]2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]2[0-4][0-9]|[01]?[0-9][0-9]?))-\d{1,5}', tostring(extract('([0-9]+\\.[0-9]+\\.[0-9]+)-[0-9]+', 1, ClientIP)),
      ClientIP matches regex @'((25[0-5]2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]2[0-4][0-9]|[01]?[0-9][0-9]?)', ClientIP,       
      ClientIP matches regex @'\[((?:[0-9a-fA-F]{1,4}::?){1,8}[0-9a-fA-F]{1,4}|\d{1,3}(?:\.\d{1,3}){3})\]-\d{1,5}', tostring(extract('\\[((?:[0-9a-fA-F]{1,4}::?){1,8}[0-9a-fA-F]{1,4}|\\d{1,3}(?:\\.\\d{1,3}){3})\\]-[0-9]+', 1, ClientIP)),
      ClientIP matches regex @'\[((?:[0-9a-fA-F]{1,4}::?){1,8}[0-9a-fA-F]{1,4}|\d{1,3}(?:\.\d{1,3}){3})\]', tostring(extract('\\[((?:[0-9a-fA-F]{1,4}::?){1,8}[0-9a-fA-F]{1,4}|\\d{1,3}(?:\\.\\d{1,3}){3})\\]', 1, ClientIP)),  
      ClientIP matches regex @'((?:[0-9a-fA-F]{1,4}::?){1,8}[0-9a-fA-F]{1,4}|\d{1,3}(?:\.\d{1,3}){3})-\d{1,5}', tostring(extract('((?:[0-9a-fA-F]{1,4}::?){1,8}[0-9a-fA-F]{1,4}|\\d{1,3}(?:\\.\\d{1,3}){3})-[0-9]+', 1, ClientIP)),
      ClientIP matches regex @'((?:[0-9a-fA-F]{1,4}::?){1,8}[0-9a-fA-F]{1,4}|\d{1,3}(?:\.\d{1,3}){3})', ClientIP,
      "")
    | where isnotempty(JoinedOnIp)
    | extend OfficeTimeStamp = ElevationTime, UserPrincipalName = UserId
) on JoinedOnIp
// Rare and risky operations only happen within a certain time range of the successful sign-in
| where OfficeTimeStamp >= successSigninStart and datetime_diff('day', OfficeTimeStamp, successSigninEnd) <= timeBuffer
| extend AccountName = tostring(split(UserPrincipalName, "@")[0]), AccountUPNSuffix = tostring(split(UserPrincipalName, "@")[1])
Microsoft Sentinel Original KQL T1041 ↗
IP address of Windows host encoded in web request
'This detection will identify network requests in HTTP proxy data that contains Base64 encoded IP addresses. After identifying candidates the query joins with DeviceNetworkEvents to idnetify any machine within the network using that IP address. Alerts indicate that the IP address of a machine within your network was seen with it's IP address base64 encoded in an outbound web request. This method of egressing the IP was seen used in POLONIUM's RunningRAT tool, however the detection is generic.'
Show query
// Extracts plaintext IPv4 addresses
let ipv4_plaintext_extraction_regex = @"((?:(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])(?:\.)){3}(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5]){1,3})";
// Identified base64 encoded IPv4 addresses
let ipv4_encoded_identification_regex = @"\=([a-zA-Z0-9\/\+]*(?:(?:MC|Au|wL|MS|Eu|xL|Mi|Iu|yL|My|Mu|zL|NC|Qu|0L|NS|Uu|1L|Ni|Yu|2L|Ny|cu|3L|OC|gu|4L|OS|ku|5L){1}[a-zA-Z0-9\/\+]{2,4}){3}[a-zA-Z0-9\/\+\=]*)";
// Extractes IPv4 addresses as hex values
let ipv4_decoded_hex_extract = @"((?:(?:61|62|63|64|65|66|67|68|69|6a|6b|6c|6d|6e|6f|70|71|72|73|74|75|76|77|78|79|7a|41|42|43|44|45|46|47|48|49|4a|4b|4c|4d|4e|4f|50|51|52|53|54|55|56|57|58|59|5a|2f|2b|3d),){7,15})";
CommonSecurityLog
| where isnotempty(RequestURL)
// Identify requests with encoded IPv4 addresses
| where RequestURL matches regex ipv4_encoded_identification_regex
| project TimeGenerated, RequestURL
// Extract IP candidates in their base64 encoded format, significantly reducing the dataset
| extend extracted_encoded_ip_candidate = extract_all(ipv4_encoded_identification_regex, RequestURL)
// We could have more than one candidate, expand them out
| mv-expand extracted_encoded_ip_candidate to typeof(string)
| summarize Start=min(TimeGenerated), End=max(TimeGenerated), make_set(RequestURL) by extracted_encoded_ip_candidate
// Pad if we need to
| extend extracted_encoded_ip_candidate = iff(strlen(extracted_encoded_ip_candidate) % 2 == 0, extracted_encoded_ip_candidate, strcat(extracted_encoded_ip_candidate, "="))
// Now decode the candidate to a long array, we cannot go straight to string as it cannot handle non-UTF8, we need to strip that first
| extend extracted_encoded_ip_candidate = tostring(base64_decode_toarray(extracted_encoded_ip_candidate))
// Extract the IP candidates from the array
| extend hex_extracted = extract_all(ipv4_decoded_hex_extract, extracted_encoded_ip_candidate)
// Expand, it's still possible that we might have more than 1 IP
| mv-expand hex_extracted
// Now we should have a clean string. We need to put it back into a dynamic array to convert back to a string.
| extend hex_extracted = trim_end(",", tostring(hex_extracted))
| extend hex_extracted = strcat("[",hex_extracted,"]")
| extend hex_extracted = todynamic(hex_extracted)
| extend extracted_encoded_ip_candidate = todynamic(extracted_encoded_ip_candidate)
// Convert the array back into a string
| extend decoded_ip_candidate = make_string(hex_extracted)
| summarize by decoded_ip_candidate, tostring(set_RequestURL), Start, End
// Now the IP candidates will be in plaintext, extract the IPs using a regex
| extend ipmatch = extract_all(ipv4_plaintext_extraction_regex, decoded_ip_candidate)
// If it's not an IP, throw it out
| where isnotnull(ipmatch)
| mv-expand ipmatch to typeof(string)
// Join with DeviceNetworkEvents to find instances where an IP of a machine in our MDE estate sent it's IP in a base64 encoded string
| join (
    DeviceNetworkEvents
    | summarize make_set(DeviceId), make_set(DeviceName) by RemoteIP
) on $left.ipmatch == $right.RemoteIP
| project Start, End, IPmatch=ipmatch, RequestURL=set_RequestURL, DeviceNames=set_DeviceName, DeviceIds=set_DeviceId, RemoteIP
Microsoft Sentinel Original KQL T1078 ↗
IP with multiple failed Microsoft Entra ID logins successfully logs in to Palo Alto VPN
This query creates a list of IP addresses with the number of failed login attempts to Entra ID above a set threshold ( default of 5 ). It then looks for any successful Palo Alto VPN logins from any of these IPs within the same timeframe.
Show query
//Set a threshold of failed AAD signins from an IP address within 1 day above which we want to deem those logins suspicious.
let signin_threshold = 5; 
//Make a list of IPs with AAD signin failures above our threshold.
let aadFunc = (tableName:string){
let suspicious_signins = 
    table(tableName)
    //Looking for logon failure results
    | where ResultType !in ("0", "50125", "50140")
    //Exclude localhost addresses to reduce the chance of FPs
    | where IPAddress !in ("127.0.0.1", "::1")
    | summarize count() by IPAddress
    | where count_ >  signin_threshold
    | summarize make_set(IPAddress);
    suspicious_signins
};
let aadSignin = aadFunc("SigninLogs");
let aadNonInt = aadFunc("AADNonInteractiveUserSignInLogs");
let suspicious_signins = 
union isfuzzy=true aadSignin, aadNonInt
| summarize make_set(set_IPAddress);
//See if any of those IPs have sucessfully logged into PA VPNs during the same timeperiod
CommonSecurityLog
    //Select only PA VPN sucessful logons
    | where DeviceVendor == "Palo Alto Networks" and DeviceEventClassID == "globalprotect"
    | where Message has "GlobalProtect gateway user authentication succeeded"
    //Parse out the logon source IP from the Message field to match on
    | extend SourceIP = extract("Login from: ([^,]+)", 1, Message) 
    | where SourceIP in (suspicious_signins)
    | extend Reason = "Multiple failed AAD logins from SourceIP"
    //Parse out other useful information from Message field
    | extend User = extract('User name: ([^,]+)', 1, Message) 
    | extend ClientOS = extract('Client OS version: ([^,\"]+)', 1, Message)
    | extend Location = extract('Source region: ([^,]{2})',1, Message)
    | project TimeGenerated, Reason, SourceIP, User, ClientOS, Location, Message, DeviceName, ReceiptTime, DeviceVendor, DeviceEventClassID, Computer, FileName
    | extend timestamp = TimeGenerated
Microsoft Sentinel Original KQL T1071 ↗
Known Forest Blizzard group domains - July 2019
'Matches domain name IOCs related to Forest Blizzard group activity published July 2019 with CommonSecurityLog, DnsEvents and VMConnection dataTypes. References: https://blogs.microsoft.com/on-the-issues/2019/07/17/new-cyberthreats-require-new-ways-to-protect-democracy/.'
Show query
let DomainNames = dynamic(["irf.services","microsoft-onthehub.com","msofficelab.com","com-mailbox.com","my-sharefile.com","my-sharepoints.com",
"accounts-web-mail.com","customer-certificate.com","session-users-activities.com","user-profile-credentials.com","verify-linke.com","support-servics.net",
"onedrive-sharedfile.com","onedrv-live.com","transparencyinternational-my-sharepoint.com","transparencyinternational-my-sharepoints.com","soros-my-sharepoint.com"]);
(union isfuzzy=true
  (CommonSecurityLog
  | where Message has_any (DomainNames)
  | parse Message with * '(' DNSName ')' *
  | extend AccountName = SourceUserID, DeviceName, IPAddress = SourceIP
  ),
  (_Im_Dns(domain_has_any=DomainNames)
  | where DnsQuery has_any (DomainNames)
  | extend IPAddress = SrcIpAddr, DeviceName = Dvc
  ),
  (VMConnection
  | where RemoteDnsCanonicalNames has_any (DomainNames)
  | parse RemoteDnsCanonicalNames with * '["' DNSName '"]' *
  | extend IPAddress = RemoteIp, DeviceName = Computer
  ),
  (AzureDiagnostics 
  | where ResourceType == "AZUREFIREWALLS"
  | where Category == "AzureFirewallApplicationRule"
  | parse msg_s with Protocol 'request from ' SourceHost ':' SourcePort 'to ' DestinationHost ':' DestinationPort '. Action:' Action
  | where DestinationHost has_any (DomainNames)
  | extend DNSName = DestinationHost 
  | extend IPAddress = SourceHost
  ),
  (AzureDiagnostics
  | where ResourceType == "AZUREFIREWALLS"
  | where Category == "AzureFirewallDnsProxy"
  | project TimeGenerated,Resource, msg_s, Type
  | parse msg_s with "DNS Request: " ClientIP ":" ClientPort " - " QueryID " " Request_Type " " Request_Class " " Request_Name ". " Request_Protocol " " Request_Size " " EDNSO_DO " " EDNS0_Buffersize " " Responce_Code " " Responce_Flags " " Responce_Size " " Response_Duration
  | where Request_Name  has_any (DomainNames)
  | extend DNSName = Request_Name
  | extend IPAddress = ClientIP
  ),
  (AZFWApplicationRule
  | where isnotempty(Fqdn)
  | where Fqdn has_any (DomainNames)  
  | extend DNSName = Fqdn 
  | extend IPAddress = SourceIp
  ),
  (AZFWDnsQuery
  | where isnotempty(QueryName)
  | where QueryName has_any (DomainNames)
  | extend DNSName = QueryName
  | extend IPAddress = SourceIp
  ),
  (
    _Im_WebSession(url_has_any=DomainNames)
    | extend IPAddress=IpAddr, DeviceName=Hostname, AccountName = tostring(split(User, "@")[0]), AccountDomain = tostring(split(User, "@")[1])
  )
)
| where DNSName has_any (DomainNames)
| extend timestamp = TimeGenerated
| extend HostName = tostring(split(DeviceName, ".")[0]), DomainIndex = toint(indexof(DeviceName, '.'))
| extend HostNameDomain = iff(DomainIndex != -1, substring(DeviceName, DomainIndex + 1), DeviceName)
Microsoft Sentinel Original KQL T1078 ↗
M365D Alerts Correlation to non-Microsoft Network device network activity involved in successful sign-in Activity
'This content is employed to correlate with Microsoft Defender XDR phishing-related alerts. It focuses on instances where a user successfully connects to a phishing URL from a non-Microsoft network device and subsequently makes successful sign-in attempts from the phishing IP address.'
Show query
let Alert_List= dynamic([
"Phishing link click observed in Network Traffic",
"Phish delivered due to an IP allow policy",
"A potentially malicious URL click was detected",
"High Risk Sign-in Observed in Network Traffic",
"A user clicked through to a potentially malicious URL",
"Suspicious network connection to AitM phishing site",
"Messages containing malicious entity not removed after delivery",
"Email messages containing malicious URL removed after delivery",
"Email reported by user as malware or phish",
"Phish delivered due to an ETR override",
"Phish not zapped because ZAP is disabled"]);
SecurityAlert
| where AlertName in~ (Alert_List)
//Findling Alerts which has the URL
| where Entities has "url"
//extracting Entities
| extend Entities = parse_json(Entities)
| mv-apply Entity = Entities on
    (
    where Entity.Type == 'url'
    | extend EntityUrl = tostring(Entity.Url)
    )
| summarize
    Url=tostring(tolower(take_any(EntityUrl))),
    AlertTime= min(TimeGenerated),
    make_set(SystemAlertId, 100)
    by ProductName, AlertName
// matching with 3rd party network logs and 3p Alerts
| join kind= inner (CommonSecurityLog
    | where DeviceVendor has_any  ("Palo Alto Networks", "Fortinet", "Check Point", "Zscaler")
    | where DeviceProduct startswith "FortiGate" or DeviceProduct startswith  "PAN" or DeviceProduct startswith  "VPN" or DeviceProduct startswith "FireWall" or DeviceProduct startswith  "NSSWeblog" or DeviceProduct startswith "URL"
    | where DeviceAction != "Block"
    | where isnotempty(RequestURL)
    | project
        3plogTime=TimeGenerated,
        DeviceVendor,
        DeviceProduct,
        Activity,
        DestinationHostName,
        DestinationIP,
        RequestURL=tostring(tolower(RequestURL)),
        MaliciousIP,
        SourceUserName=tostring(tolower(SourceUserName)),
        IndicatorThreatType,
        ThreatSeverity,
        ThreatConfidence,
        SourceUserID,
        SourceHostName)
    on $left.Url == $right.RequestURL
// matching successful Login from suspicious IP
| join kind=inner (SigninLogs
    //filtering the Successful Login
    | where ResultType == 0
    | project
        IPAddress,
        SourceSystem,
        SigniningTime= TimeGenerated,
        OperationName,
        ResultType,
        ResultDescription,
        AlternateSignInName,
        AppDisplayName,
        AuthenticationRequirement,
        ClientAppUsed,
        RiskState,
        RiskLevelDuringSignIn,
        UserPrincipalName=tostring(tolower(UserPrincipalName)),
        Name = tostring(split(UserPrincipalName, "@")[0]),
        UPNSuffix =tostring(split(UserPrincipalName, "@")[1]))
    on $left.DestinationIP == $right.IPAddress and $left.SourceUserName == $right.UserPrincipalName
| where SigniningTime between ((AlertTime - 6h) .. (AlertTime + 6h)) and 3plogTime between ((AlertTime - 6h) .. (AlertTime + 6h))
Microsoft Sentinel Original KQL T1071 ↗
Mercury - Domain, Hash and IP IOCs - August 2022
'Identifies a match across various data feeds for domains, hashes and IP IOC related to Mercury Reference: https://www.microsoft.com/security/blog/2022/08/25/mercury-leveraging-log4j-2-vulnerabilities-in-unpatched-systems-to-target-israeli-organizations/'
Show query
let iocs = externaldata(DateAdded:string,IoC:string,Type:string,TLP:string) [@"https://raw.githubusercontent.com/Azure/Azure-Sentinel/master/Sample%20Data/Feeds/Mercury_August2022.csv"] with (format="csv", ignoreFirstRecord=True);
let sha256Hashes = (iocs | where Type =~ "sha256" | project IoC);
let IPList = (iocs | where Type =~ "ip"| project IoC);
let domains = (iocs | where Type =~ "domainname"| project IoC);
let IPRegex = '[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}';
(union isfuzzy=true
(CommonSecurityLog
| where SourceIP in (IPList) or DestinationIP in (IPList) or DestinationHostName has_any (domains) or RequestURL has_any (domains) or Message has_any (IPList)
| parse Message with * '(' DNSName ')' * 
| project TimeGenerated, SourceIP, DestinationIP, Message, SourceUserID, RequestURL, DNSName, Type
| extend MessageIP = extract(IPRegex, 0, Message), RequestIP = extract(IPRegex, 0, RequestURL)
| extend IPMatch = case(SourceIP in (IPList), "SourceIP", DestinationIP in (IPList), "DestinationIP", MessageIP in (IPList), "Message", RequestURL has_any (domains), "RequestUrl", "NoMatch")
| extend IPAddress = case(IPMatch == "SourceIP", SourceIP, IPMatch == "DestinationIP", DestinationIP, IPMatch == "Message", MessageIP, "NoMatch")
| extend AccountName = tostring(split(SourceUserID, "@")[0]), AccountUPNSuffix = tostring(split(SourceUserID, "@")[1])
),
(DnsEvents
| where IPAddresses in (IPList) or Name in~ (domains)
| project TimeGenerated, Computer, IPAddresses, Name, ClientIP, Type
| extend IPAddress = IPAddresses, DNSName = Name, Computer
),
(VMConnection
| where SourceIp in (IPList) or DestinationIp in (IPList) or RemoteDnsCanonicalNames has_any (domains)
| parse RemoteDnsCanonicalNames with * '["' DNSName '"]' *
| project TimeGenerated, Computer, Direction, ProcessName, SourceIp, DestinationIp, DestinationPort, RemoteDnsQuestions, DNSName,BytesSent, BytesReceived, RemoteCountry, Type
| extend IPMatch = case( SourceIp in (IPList), "SourceIP", DestinationIp in (IPList), "DestinationIP", "None") 
| extend IPAddress = case(IPMatch == "SourceIP", SourceIp, IPMatch == "DestinationIP", DestinationIp, "NoMatch"), File = ProcessName
),
(Event
| where Source == "Microsoft-Windows-Sysmon"
| where EventID == 3
| extend EvData = parse_xml(EventData)
| extend EventDetail = EvData.DataItem.EventData.Data
| extend SourceIP = tostring(EventDetail.[9].["#text"]), DestinationIP = tostring(EventDetail.[14].["#text"]), Image = tostring(EventDetail.[4].["#text"])
| where SourceIP in (IPList) or DestinationIP in (IPList)
| project TimeGenerated, SourceIP, DestinationIP, Image, UserName, Computer, Type
| extend IPMatch = case( SourceIP in (IPList), "SourceIP", DestinationIP in (IPList), "DestinationIP", "None")
| extend AccountNT = UserName, File = tostring(split(Image, '\\', -1)[-1]), IPAddress = case(IPMatch == "SourceIP", SourceIP, IPMatch == "DestinationIP", DestinationIP, "None")
), 
(OfficeActivity
| where ClientIP in (IPList) 
| project TimeGenerated, UserAgent, Operation, RecordType, UserId, ClientIP, Type
| extend IPAddress = ClientIP, AccountUPN = UserId, AccountUPNName = tostring(split(UserId, "@")[0]), AccountUPNSuffix = tostring(split(UserId, "@")[1])
),
(DeviceNetworkEvents
| where RemoteUrl has_any (domains) or RemoteIP in (IPList) or InitiatingProcessSHA256 in (sha256Hashes)
| project TimeGenerated, ActionType, DeviceId, Computer = DeviceName, InitiatingProcessSHA256, InitiatingProcessAccountDomain, InitiatingProcessAccountName, InitiatingProcessCommandLine, InitiatingProcessFolderPath, InitiatingProcessId, InitiatingProcessParentFileName, InitiatingProcessFileName, RemoteIP, RemoteUrl, RemotePort, LocalIP, Type
| extend IPAddress = RemoteIP, FileHash = InitiatingProcessSHA256
| extend AccountUPN = InitiatingProcessAccountName, AccountUPNName = tostring(split(InitiatingProcessAccountName, "@")[0]), AccountUPNSuffix = tostring(split(InitiatingProcessAccountName, "@")[1])
),
(WindowsFirewall
| where SourceIP in (IPList) or DestinationIP in (IPList) 
| project TimeGenerated, Computer, CommunicationDirection, SourceIP, DestinationIP, SourcePort, DestinationPort, Type
| extend IPMatch = case( SourceIP in (IPList), "SourceIP", DestinationIP in (IPList), "DestinationIP", "None")
| extend IPAddress = case(IPMatch == "SourceIP", SourceIP, IPMatch == "DestinationIP", DestinationIP, "None")
),
(AzureDiagnostics 
| where ResourceType == "AZUREFIREWALLS"
| where Category == "AzureFirewallApplicationRule"
| parse msg_s with Protocol 'request from ' SourceHost ':' SourcePort 'to ' DestinationHost ':' DestinationPort '. Action:' Action
| where isnotempty(DestinationHost)
| where DestinationHost has_any (IPList) or DestinationHost has_any (domains) 
| extend DNSName = DestinationHost, IPAddress = SourceHost
),
(AzureDiagnostics
| where ResourceType == "AZUREFIREWALLS"
| where Category == "AzureFirewallNetworkRule"
| where msg_s has_any (IPList)
| parse msg_s with Protocol " request from " SourceIP ":" SourcePortInt:int " to " TargetIP ":" TargetPortInt:int *
| parse kind=regex flags=U msg_s with * ". Action\\: " Action1a "\\."
| parse msg_s with * ". Policy: " Policy ". Rule Collection Group: " RuleCollectionGroup "." *
| parse msg_s with * " Rule Collection: "  RuleCollection ". Rule: " Rule 
| extend IPAddress = SourceIP
),
(AzureDiagnostics
| where ResourceType == "AZUREFIREWALLS"
| where Category == "AzureFirewallDnsProxy"
| where msg_s has_any (domains)
| parse msg_s with "DNS Request: " SourceIP ":" SourcePortInt:int " - " QueryID:int " " RequestType " " RequestClass " " hostname ". " protocol " " details
| extend
    ResponseDuration = extract("[0-9]*.?[0-9]+s$", 0, msg_s),
    SourcePort = tostring(SourcePortInt),
    QueryID = tostring(QueryID)
| project TimeGenerated,SourceIP,hostname,RequestType,ResponseDuration,details,msg_s
| extend IPAddress = SourceIP
),
(AZFWApplicationRule
| where Fqdn has_any (domains) or Fqdn has_any (IPList)
| extend IPAddress = SourceIp
),
(AZFWDnsQuery
| where isnotempty(QueryName)
| where QueryName has_any (domains)
| extend DNSName = QueryName, IPAddress = SourceIp
),
(AZFWNetworkRule
| where DestinationIp has_any (IPList)
| extend IPAddress = SourceIp
),
(CommonSecurityLog
| where FileHash in (sha256Hashes)
| project TimeGenerated, Message, SourceUserID, FileHash, Type
| extend Algorithm = "SHA256", FileHash = tostring(FileHash), AccountUPN = SourceUserID, AccountUPNName = tostring(split(SourceUserID, "@")[0]), AccountUPNSuffix = tostring(split(SourceUserID, "@")[1])
),
(imFileEvent
| where TargetFileSHA256 has_any (sha256Hashes)
| extend AccountNT = ActorUsername, Computer = DvcHostname, IPAddress = SrcIpAddr, CommandLine = ActingProcessCommandLine, FileHash = TargetFileSHA256
| project Type, TimeGenerated, Computer, AccountNT, IPAddress, CommandLine, FileHash, Algorithm = "SHA256"
),
(DeviceFileEvents
| where SHA256 has_any (sha256Hashes)
| project TimeGenerated, ActionType, DeviceId, Computer = DeviceName, InitiatingProcessAccountDomain, InitiatingProcessAccountName, InitiatingProcessCommandLine, InitiatingProcessFolderPath, InitiatingProcessId, InitiatingProcessParentFileName, InitiatingProcessFileName, InitiatingProcessSHA256, Type
| extend Algorithm = "SHA256", FileHash = tostring(InitiatingProcessSHA256), CommandLine = InitiatingProcessCommandLine,Image = InitiatingProcessFolderPath
| extend AccountUPN = InitiatingProcessAccountName, AccountUPNName = tostring(split(InitiatingProcessAccountName, "@")[0]), AccountUPNSuffix = tostring(split(InitiatingProcessAccountName, "@")[1])
),
(DeviceImageLoadEvents
| where SHA256 has_any (sha256Hashes)
| project TimeGenerated, ActionType, DeviceId, Computer = DeviceName, InitiatingProcessAccountDomain, InitiatingProcessAccountName, InitiatingProcessCommandLine, InitiatingProcessFolderPath, InitiatingProcessId, InitiatingProcessParentFileName, InitiatingProcessFileName, InitiatingProcessSHA256, Type
| extend Algorithm = "SHA256", FileHash = tostring(InitiatingProcessSHA256), CommandLine = InitiatingProcessCommandLine,Image = InitiatingProcessFolderPath
| extend AccountUPN = InitiatingProcessAccountName, AccountUPNName = tostring(split(InitiatingProcessAccountName, "@")[0]), AccountUPNSuffix = tostring(split(InitiatingProcessAccountName, "@")[1])
),
(Event
| where Source =~ "Microsoft-Windows-Sysmon"
| where EventID == 1
| extend EvData = parse_xml(EventData)
| extend EventDetail = EvData.DataItem.EventData.Data
| extend Image = EventDetail.[4].["#text"], CommandLine = EventDetail.[10].["#text"], Hashes = tostring(EventDetail.[17].["#text"])
| extend Hashes = extract_all(@"(?P<key>\w+)=(?P<value>[a-zA-Z0-9]+)", dynamic(["key","value"]), Hashes)
| extend Hashes = column_ifexists("Hashes", dynamic(["", ""])), CommandLine = column_ifexists("CommandLine", "")
| mv-expand Hashes
| where Hashes[0] =~ "SHA256" and Hashes[1] has_any (sha256Hashes) 
| project TimeGenerated, EventDetail, AccountNT = UserName, Computer, Type, Source, Hashes, CommandLine, Image
| extend Type = strcat(Type, ": ", Source), FileHash = tostring(Hashes[1]), Algorithm = tostring(Hashes[0])
)
)
| extend AccountNTName = tostring(split(AccountNT, "\\")[1]), AccountNTDomain = tostring(split(AccountNT, "\\")[0])
| extend HostName = tostring(split(Computer, ".")[0]), DomainIndex = toint(indexof(Computer, '.'))
| extend HostNameDomain = iff(DomainIndex != -1, substring(Computer, DomainIndex + 1), Computer)
| project-away DomainIndex
Microsoft Sentinel Original KQL T1190 ↗
Microsoft Defender for Endpoint (MDE) signatures for Azure Synapse pipelines and Azure Data Factory
'This query looks for Microsoft Defender for Endpoint detections related to the remote command execution attempts on Azure IR with Managed VNet or SHIR. In Microsoft Sentinel, the SecurityAlerts table includes the name of the impacted device. Additionally, this query joins the DeviceInfo table to connect other information such as device group, IP address, signed in users, and others allowing analysts using Microsoft Sentinel to have more context related to the alert. Reference: https://msrc.mi
Show query
let mde_threats = dynamic(["Behavior:Win32/SuspAzureRequest.A", "Behavior:Win32/SuspAzureRequest.B", "Behavior:Win32/SuspAzureRequest.C", "Behavior:Win32/LaunchingSuspCMD.B"]);
DeviceInfo
| extend DeviceName = tolower(DeviceName)
| join kind=inner ( SecurityAlert
| where ProviderName == "MDATP"
| extend ThreatName = tostring(parse_json(ExtendedProperties).ThreatName)
| extend ThreatFamilyName = tostring(parse_json(ExtendedProperties).ThreatFamilyName)
| where ThreatName in~ (mde_threats) or ThreatFamilyName in~ (mde_threats)
| extend CompromisedEntity = tolower(CompromisedEntity)
) on $left.DeviceName == $right.CompromisedEntity
| summarize by bin(TimeGenerated, 1d), DisplayName, ThreatName, ThreatFamilyName, PublicIP, AlertSeverity, Description, tostring(LoggedOnUsers), DeviceId, TenantId, CompromisedEntity, tostring(LoggedOnUsers), ProductName, Entities
| extend HostName = tostring(split(CompromisedEntity, ".")[0]), DomainIndex = toint(indexof(CompromisedEntity, '.'))
| extend HostNameDomain = iff(DomainIndex != -1, substring(CompromisedEntity, DomainIndex + 1), CompromisedEntity)
| project-away DomainIndex
Microsoft Sentinel Original KQL T1005 ↗
Microsoft Entra ID Health Monitoring Agent Registry Keys Access
'This detection uses Windows security events to detect suspicious access attempts to the registry key of Microsoft Entra ID Health monitoring agent. This detection requires an access control entry (ACE) on the system access control list (SACL) of the following securable object HKLM\SOFTWARE\Microsoft\Microsoft Online\Reporting\MonitoringAgent. You can find more information in here https://github.com/OTRF/Set-AuditRule/blob/master/rules/registry/aad_connect_health_monitoring_agent.yml '
Show query
// ADHealth Monitoring Agent Registry Key
let aadHealthMonAgentRegKey = "\\REGISTRY\\MACHINE\\SOFTWARE\\Microsoft\\Microsoft Online\\Reporting\\MonitoringAgent";
// Filter out known processes
let aadConnectHealthProcs = dynamic ([
    'Microsoft.Identity.Health.Adfs.DiagnosticsAgent.exe',
    'Microsoft.Identity.Health.Adfs.InsightsService.exe',
    'Microsoft.Identity.Health.Adfs.MonitoringAgent.Startup.exe',
    'Microsoft.Identity.Health.Adfs.PshSurrogate.exe',
    'Microsoft.Identity.Health.Common.Clients.ResourceMonitor.exe',
    'Microsoft.Identity.Health.AadSync.MonitoringAgent.Startup.exe',
    'Microsoft.Identity.AadConnect.Health.AadSync.Host.exe',
    'Microsoft.Azure.ActiveDirectory.Synchronization.Upgrader.exe',
    'miiserver.exe'
]);
(union isfuzzy=true
(
SecurityEvent
| where EventID == '4656'
| where EventData has aadHealthMonAgentRegKey
| extend EventData = parse_xml(EventData).EventData.Data
| mv-expand bagexpansion=array EventData
| evaluate bag_unpack(EventData)
| extend Key = tostring(column_ifexists('@Name', "")), Value = column_ifexists('#text', "")
| evaluate pivot(Key, any(Value), TimeGenerated, Computer, EventID)
| extend ObjectName = column_ifexists("ObjectName", ""),
    ObjectType = column_ifexists("ObjectType", "")
| where ObjectType == 'Key'
| where ObjectName == aadHealthMonAgentRegKey
| extend SubjectUserName = column_ifexists("SubjectUserName", ""),
    SubjectDomainName = column_ifexists("SubjectDomainName", ""),
    ProcessName = column_ifexists("ProcessName", "")
| extend Process = split(ProcessName, '\\', -1)[-1],
    Account = strcat(SubjectDomainName, "\\", SubjectUserName)
| where Process !in (aadConnectHealthProcs)
| summarize StartTime = max(TimeGenerated), EndTime = min(TimeGenerated), count() by EventID, Account, Computer, Process, SubjectUserName, SubjectDomainName, ObjectName, ObjectType, ProcessName
),
(
WindowsEvent
| where EventID == '4656' and EventData has aadHealthMonAgentRegKey
| extend ObjectType = tostring(EventData.ObjectType)
| where ObjectType == 'Key'
| extend ObjectName = tostring(EventData.ObjectName)
| where ObjectName == aadHealthMonAgentRegKey
| extend ProcessName = tostring(EventData.ProcessName)
| extend Process = tostring(split(ProcessName, '\\')[-1])
| where Process !in (aadConnectHealthProcs)
| extend Account =  strcat(tostring(EventData.SubjectDomainName),"\\", tostring(EventData.SubjectUserName))
| extend SubjectUserName = tostring(EventData.SubjectUserName)
| extend SubjectDomainName = tostring(EventData.SubjectDomainName)
| summarize StartTime = max(TimeGenerated), EndTime = min(TimeGenerated), count() by EventID, Account, Computer, Process, SubjectUserName, SubjectDomainName, ObjectName, ObjectType, ProcessName
),
(
SecurityEvent
| where EventID == '4663'
| where ObjectType == 'Key'
| where ObjectName == aadHealthMonAgentRegKey
| extend Process = tostring(split(ProcessName, '\\', -1)[-1])
| where Process !in (aadConnectHealthProcs)
| summarize StartTime = max(TimeGenerated), EndTime = min(TimeGenerated), count() by EventID, Account, Computer, Process, SubjectUserName, SubjectDomainName, ObjectName, ObjectType, ProcessName
),
  (
WindowsEvent
| where EventID == '4663' and EventData has aadHealthMonAgentRegKey
| extend ObjectType = tostring(EventData.ObjectType)
| where ObjectType == 'Key'
| extend ObjectName = tostring(EventData.ObjectName)
| where ObjectName == aadHealthMonAgentRegKey
| extend ProcessName = tostring(EventData.ProcessName)
| extend Process = tostring(split(ProcessName, '\\')[-1])
| where Process !in (aadConnectHealthProcs)
| extend Account =  strcat(tostring(EventData.SubjectDomainName),"\\", tostring(EventData.SubjectUserName))
| extend SubjectUserName = tostring(EventData.SubjectUserName)
| extend SubjectDomainName = tostring(EventData.SubjectDomainName)
| summarize StartTime = max(TimeGenerated), EndTime = min(TimeGenerated), count() by EventID, Account, Computer, Process, SubjectUserName, SubjectDomainName, ObjectName, ObjectType, ProcessName
)
)
// You can filter out potential machine accounts
//| where AccountType != 'Machine'
| extend HostName = tostring(split(Computer, ".")[0]), DomainIndex = toint(indexof(Computer, '.'))
| extend HostNameDomain = iff(DomainIndex != -1, substring(Computer, DomainIndex + 1), Computer)
| extend Name = tostring(split(Account, "\\")[1]), NTDomain = tostring(split(Account, "\\")[0])
| project-away DomainIndex
Microsoft Sentinel Original KQL T1005 ↗
Microsoft Entra ID Health Service Agents Registry Keys Access
'This detection uses Windows security events to detect suspicious access attempts to the registry key values and sub-keys of Microsoft Entra ID Health service agents (e.g AD FS). Information from AD Health service agents can be used to potentially abuse some of the features provided by those services in the cloud (e.g. Federation). This detection requires an access control entry (ACE) on the system access control list (SACL) of the following securable object: HKLM:\SOFTWARE\Microsoft\ADHealthAge
Show query
// ADHealth Monitoring Agent Registry Key
let aadHealthMonAgentRegKey = "\\REGISTRY\\MACHINE\\SOFTWARE\\Microsoft\\MicrosoftOnline\\Reporting\\MonitoringAgent";
// Filter out known processes
let aadConnectHealthProcs = dynamic ([
    'Microsoft.Identity.Health.Adfs.DiagnosticsAgent.exe',
    'Microsoft.Identity.Health.Adfs.InsightsService.exe',
    'Microsoft.Identity.Health.Adfs.MonitoringAgent.Startup.exe',
    'Microsoft.Identity.Health.Adfs.PshSurrogate.exe',
    'Microsoft.Identity.Health.Common.Clients.ResourceMonitor.exe',
    'Microsoft.Identity.Health.AadSync.MonitoringAgent.Startup.exe',
    'Microsoft.Identity.AadConnect.Health.AadSync.Host.exe',
    'Microsoft.Azure.ActiveDirectory.Synchronization.Upgrader.exe',
    'miiserver.exe'
]);
(union isfuzzy=true
(
SecurityEvent
| where EventID == '4656'
| where EventData has aadHealthMonAgentRegKey
| extend EventData = parse_xml(EventData).EventData.Data
| mv-expand bagexpansion=array EventData
| evaluate bag_unpack(EventData)
| extend Key = tostring(column_ifexists('@Name', "")), Value = column_ifexists('#text', "")
| evaluate pivot(Key, any(Value), TimeGenerated, Computer, EventID)
| extend ObjectName = column_ifexists("ObjectName", ""),
    ObjectType = column_ifexists("ObjectType", "")
| where ObjectType == 'Key'
| where ObjectName == aadHealthMonAgentRegKey
| extend SubjectUserName = column_ifexists("SubjectUserName", ""),
    SubjectDomainName = column_ifexists("SubjectDomainName", ""),
    ProcessName = column_ifexists("ProcessName", "")
| extend Process = split(ProcessName, '\\', -1)[-1],
    Account = strcat(SubjectDomainName, "\\", SubjectUserName)
| where Process !in (aadConnectHealthProcs)
| summarize StartTime = max(TimeGenerated), EndTime = min(TimeGenerated), count() by EventID, Account, Computer, Process, SubjectUserName, SubjectDomainName, ObjectName, ObjectType, ProcessName
),
  ( WindowsEvent
| where EventID == '4656' and EventData has aadHealthMonAgentRegKey
| extend ObjectType = tostring(EventData.ObjectType)
| where ObjectType == 'Key'
| extend ObjectName = tostring(EventData.ObjectName)
| where ObjectName == aadHealthMonAgentRegKey
| extend ProcessName = tostring(EventData.ProcessName)
| extend Process = tostring(split(ProcessName, '\\')[-1])
| where Process !in (aadConnectHealthProcs)
| extend Account =  strcat(tostring(EventData.SubjectDomainName),"\\", tostring(EventData.SubjectUserName))
| extend SubjectUserName = tostring(EventData.SubjectUserName)
| extend SubjectDomainName = tostring(EventData.SubjectDomainName)
| summarize StartTime = max(TimeGenerated), EndTime = min(TimeGenerated), count() by EventID, Account, Computer, Process, SubjectUserName, SubjectDomainName, ObjectName, ObjectType, ProcessName
),
(
SecurityEvent
| where EventID == '4663'
| where ObjectType == 'Key'
| where ObjectName == aadHealthMonAgentRegKey
| extend Process = tostring(split(ProcessName, '\\', -1)[-1])
| where Process !in (aadConnectHealthProcs)
| summarize StartTime = max(TimeGenerated), EndTime = min(TimeGenerated), count() by EventID, Account, Computer, Process, SubjectUserName, SubjectDomainName, ObjectName, ObjectType, ProcessName
),
( WindowsEvent
| where EventID == '4663' and EventData has aadHealthMonAgentRegKey
| extend ObjectType = tostring(EventData.ObjectType)
| where ObjectType == 'Key'
| extend ObjectName = tostring(EventData.ObjectName)
| where ObjectName == aadHealthMonAgentRegKey
| extend ProcessName = tostring(EventData.ProcessName)
| extend Process = tostring(split(ProcessName, '\\')[-1])
| where Process !in (aadConnectHealthProcs)
| extend Account =  strcat(tostring(EventData.SubjectDomainName),"\\", tostring(EventData.SubjectUserName))
| extend SubjectUserName = tostring(EventData.SubjectUserName)
| extend SubjectDomainName = tostring(EventData.SubjectDomainName)
| summarize StartTime = max(TimeGenerated), EndTime = min(TimeGenerated), count() by EventID, Account, Computer, Process, SubjectUserName, SubjectDomainName, ObjectName, ObjectType, ProcessName
)
)
// You can filter out potential machine accounts
//| where AccountType != 'Machine'
| extend HostName = tostring(split(Computer, ".")[0]), DomainIndex = toint(indexof(Computer, '.'))
| extend HostNameDomain = iff(DomainIndex != -1, substring(Computer, DomainIndex + 1), Computer)
| extend Name = tostring(split(Account, "\\")[1]), NTDomain = tostring(split(Account, "\\")[0])
Microsoft Sentinel Original KQL T1059 ↗
Midnight Blizzard - Script payload stored in Registry
'This query identifies when a process execution command-line indicates that a registry value is written to allow for later execution a malicious script References: https://www.microsoft.com/security/blog/2021/03/04/goldmax-goldfinder-sibot-analyzing-nobelium-malware/'
Show query
let cmdTokens0 = dynamic(['vbscript','jscript']);
let cmdTokens1 = dynamic(['mshtml','RunHTMLApplication']);
let cmdTokens2 = dynamic(['Execute','CreateObject','RegRead','window.close']);
(union isfuzzy=true 
(SecurityEvent
| where TimeGenerated >= ago(14d)
| where EventID == 4688
| where CommandLine has @'\Microsoft\Windows\CurrentVersion'
| where not(CommandLine has_any (@'\Software\Microsoft\Windows\CurrentVersion\Run', @'\Software\Microsoft\Windows\CurrentVersion\RunOnce'))
// If you are receiving false positives, then it may help to make the query more strict by uncommenting one or both of the lines below to refine the matches
//| where CommandLine has_any (cmdTokens0)
//| where CommandLine has_all (cmdTokens1)
| where CommandLine has_all (cmdTokens2)
| project TimeGenerated, Computer, Account, Process, NewProcessName, CommandLine, ParentProcessName, _ResourceId
),
(WindowsEvent
| where TimeGenerated >= ago(14d)
| where EventID == 4688 and EventData has_all(cmdTokens2) and  EventData has @'\Microsoft\Windows\CurrentVersion'
| where not(EventData has_any (@'\Software\Microsoft\Windows\CurrentVersion\Run', @'\Software\Microsoft\Windows\CurrentVersion\RunOnce'))
| extend CommandLine = tostring(EventData.CommandLine)
| where CommandLine has @'\Microsoft\Windows\CurrentVersion'
| where not(CommandLine has_any (@'\Software\Microsoft\Windows\CurrentVersion\Run', @'\Software\Microsoft\Windows\CurrentVersion\RunOnce'))
// If you are receiving false positives, then it may help to make the query more strict by uncommenting one or both of the lines below to refine the matches
//| where CommandLine has_any (cmdTokens0)
//| where CommandLine has_all (cmdTokens1)
| where CommandLine has_all (cmdTokens2)
| extend Account =  strcat(EventData.SubjectDomainName,"\\", EventData.SubjectUserName)
| extend NewProcessName = tostring(EventData.NewProcessName)
| extend Process=tostring(split(NewProcessName, '\\')[-1])
| extend ParentProcessName = tostring(EventData.ParentProcessName)  
| project TimeGenerated, Computer, Account, Process, NewProcessName, CommandLine, ParentProcessName, _ResourceId)
| extend Name = tostring(split(Account, "\\")[1]), NTDomain = tostring(split(Account, "\\")[0])
| extend DnsDomain = tostring(strcat_array(array_slice(split(Computer, '.'), 1, -1), '.')), HostName = tostring(split(Computer, '.', 0)[0]))
Microsoft Sentinel Original KQL T1078 ↗
Multiple Password Reset by user
'This query will determine multiple password resets by user across multiple data sources. Account manipulation including password reset may aid adversaries in maintaining access to credentials and certain permission levels within an environment.'
Show query
let selfServicePasswordReset = dynamic(["Self-service password reset flow activity progress", "Change password (self-service)", "Reset password (self-service)"]); 
//Self-service password reset flow activity progress is typically caused by a password policy which requires users to rotate passwords. This operation already implies the user has signed in successfully and therefore the password reset is non-malicious.
let PerUserThreshold = 5;
let TotalThreshold = 100;
let action = dynamic(["change", "changed", "reset"]);
let pWord = dynamic(["password", "credentials"]);
let PasswordResetMultiDataSource =
(union isfuzzy=true
(//Password reset events
//4723: An attempt was made to change an account's password
//4724: An attempt was made to reset an accounts password
SecurityEvent
| where EventID in ("4723","4724")
| project TimeGenerated, Computer, AccountType, Account, Type, TargetUserName),
(//Password reset events
//4723: An attempt was made to change an account's password
//4724: An attempt was made to reset an accounts password
WindowsEvent
| where EventID in ("4723","4724")
| extend SubjectUserSid = tostring(EventData.SubjectUserSid)
| extend TargetUserName = tostring(EventData.TargetUserName)
| extend Account = strcat(tostring(EventData.SubjectDomainName),"\\", tostring(EventData.SubjectUserName))
| extend AccountType=case(Account endswith "$" or SubjectUserSid in ("S-1-5-18", "S-1-5-19", "S-1-5-20"), "Machine", isempty(SubjectUserSid), "", "User")
| project TimeGenerated, Computer, AccountType, Account, Type, TargetUserName),
(//Azure Active Directory Password reset events
AuditLogs
| where OperationName has_any (pWord) and OperationName has_any (action) and Result =~ "success"
| where OperationName !in (selfServicePasswordReset)
| mv-apply TargetResource = TargetResources on 
  (
      where TargetResource.type =~ "User"
      | extend AccountType = tostring(TargetResource.type),
               Account = tostring(InitiatedBy.user.userPrincipalName),
               TargetUserName = tolower(tostring(TargetResource.userPrincipalName))
  )
| project TimeGenerated, AccountType, Account, TargetUserName, Computer = "", Type),
(//OfficeActive ActiveDirectory Password reset events
OfficeActivity
| where OfficeWorkload == "AzureActiveDirectory"
| where (ExtendedProperties has_any (pWord) or ModifiedProperties has_any (pWord)) and (ExtendedProperties has_any (action) or ModifiedProperties has_any (action))
| extend AccountType = UserType, Account = OfficeObjectId
| project TimeGenerated, AccountType, Account, Type, Computer = ""),
(// Unix syslog password reset events
Syslog
| where Facility in ("auth","authpriv")
| where SyslogMessage has_any (pWord) and SyslogMessage has_any (action)
| extend AccountType = iif(SyslogMessage contains "root", "Root", "Non-Root")
| where SyslogMessage matches regex ".*password changed for.*"
| parse SyslogMessage with * "password changed for" Account
| project TimeGenerated, AccountType, Account, Computer = HostName, Type)
);
let pwrmd = PasswordResetMultiDataSource
| project TimeGenerated, Computer, AccountType, Account, Type, TargetUserName;
(union isfuzzy=true
(pwrmd
| summarize StartTimeUtc = min(TimeGenerated), EndTimeUtc = max(TimeGenerated), Computerlist = make_set(Computer, 25), AccountType = make_set(AccountType, 25), Computer = arg_max(Computer , TimeGenerated), TargetUserList = make_set(TargetUserName, 25), TargetUserName = arg_max(TargetUserName, TimeGenerated), Total=count() by Account, Type
| where Total > PerUserThreshold
| extend ResetPivot = "PerUserReset"),
(pwrmd
| summarize StartTimeUtc = min(TimeGenerated), EndTimeUtc = max(TimeGenerated), ComputerList = make_set(Computer, 25), AccountList = make_set(Account, 25), AccountType = make_set(AccountType, 25), Computer = arg_max(Computer , TimeGenerated), TargetUserList = make_set(TargetUserName, 25), TargetUserName = arg_max(TargetUserName, TimeGenerated), Total=count() by Type
| where Total > TotalThreshold
| extend ResetPivot = "TotalUserReset")
)
| extend timestamp = StartTimeUtc, HostName = tostring(split(Computer, '.', 0)[0]), DnsDomain = tostring(strcat_array(array_slice(split(Computer, '.'), 1, -1), '.')), Name = tostring(split(Account, '@', 0)[0]), UPNSuffix = tostring(split(Account, '@', 1)[0]), TargetName = tostring(split(TargetUserName,'@',0)[0]), TargetUPNSuffix = tostring(split(TargetUserName,'@',1)[0])
Microsoft Sentinel Original KQL T1021 ↗
Multiple RDP connections from Single System
'Identifies when an RDP connection is made to multiple systems and above the normal connection count for the previous 7 days. Connections from the same system with the same account within the same day. RDP connections are indicated by the EventID 4624 with LogonType = 10'
Show query
let endtime = 1d;
let starttime = 8d;
let threshold = 2.0;
(union isfuzzy=true
(SecurityEvent
| where TimeGenerated >= ago(endtime)
| where EventID == 4624 and LogonType == 10
| summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated), ComputerCountToday = dcount(Computer), ComputerSet = makeset(Computer), ProcessSet = makeset(ProcessName)
by Account = tolower(Account), IpAddress, AccountType, Activity, LogonTypeName),
(WindowsEvent
| where TimeGenerated >= ago(endtime)
| where EventID == 4624
| extend LogonType = tostring(EventData.LogonType)
| where  LogonType == 10
| extend ProcessName = tostring(EventData.ProcessName)
| extend Account = strcat(tostring(EventData.TargetDomainName),"\\", tostring(EventData.TargetUserName))
| extend IpAddress = tostring(EventData.IpAddress)
| extend TargetUserSid = tostring(EventData.TargetUserSid)
| extend AccountType=case(Account endswith "$" or TargetUserSid in ("S-1-5-18", "S-1-5-19", "S-1-5-20"), "Machine", isempty(TargetUserSid), "", "User")
| extend Activity="4624 - An account was successfully logged on."
| extend LogonTypeName="10 - RemoteInteractive"
| summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated), ComputerCountToday = dcount(Computer), ComputerSet = makeset(Computer), ProcessSet = makeset(ProcessName)
by Account = tolower(Account), IpAddress, AccountType, Activity, LogonTypeName)
)
| join kind=inner (
(union isfuzzy=true
(SecurityEvent
| where TimeGenerated >= ago(starttime) and TimeGenerated < ago(endtime)
| where EventID == 4624 and LogonType == 10
| summarize ComputerCountPrev7Days = dcount(Computer) by Account = tolower(Account), IpAddress
),
( WindowsEvent
| where TimeGenerated >= ago(starttime) and TimeGenerated < ago(endtime)
| where EventID == 4624  and EventData has ("10")
| extend LogonType = toint(EventData.LogonType)
| where  LogonType == 10
| extend Account = strcat(tostring(EventData.TargetDomainName),"\\", tostring(EventData.TargetUserName))
| extend IpAddress = tostring(EventData.IpAddress)
| summarize ComputerCountPrev7Days = dcount(Computer) by Account = tolower(Account), IpAddress)
)
) on Account, IpAddress
| extend Ratio = iff(isempty(ComputerCountPrev7Days), toreal(ComputerCountToday), ComputerCountToday / (ComputerCountPrev7Days * 1.0))
// Where the ratio of today to previous 7 days is more than double.
| where Ratio > threshold
| project StartTime, EndTime, Account, IpAddress, ComputerSet, ComputerCountToday, ComputerCountPrev7Days, Ratio, AccountType, Activity, LogonTypeName, ProcessSet
| extend AccountName = tostring(split(Account, @"\")[1]), AccountNTDomain = tostring(split(Account, @"\")[0])
Microsoft Sentinel Original KQL T1078 ↗
New country signIn with correct password
'Identifies an interrupted sign-in session from a country the user has not sign-in before in the last 7 days, where the password was correct. Although the session is interrupted by other controls such as multi factor authentication or conditional access policies, the user credentials should be reset due to logs indicating a correct password was observed during sign-in.'
Show query
// Creating a list of successful sign-in by users in the last 7 days.
let KnownUserCountry = (
SigninLogs
| where TimeGenerated between (ago(7d) .. ago(1d) ) 
| where ResultType == 0
| summarize KnownCountry = make_set(Location,1048576) by UserPrincipalName
);
// Identify sign-ins that are no successful but have the auth details indicating a correct password.
SigninLogs
| where TimeGenerated >= ago(1d)
| where ResultType != 0
| extend ParseAuth = parse_json(AuthenticationDetails)
| extend AuthMethod = tostring(ParseAuth.[0].authenticationMethod),
    PasswordResult = tostring(ParseAuth.[0].authenticationStepResultDetail),
    AuthSucceeded = tostring(ParseAuth.[0].succeeded)
| where PasswordResult == "Correct Password" or AuthSucceeded == "true"
| where AuthMethod == "Password"
| extend failureReason = tostring(Status.failureReason)
| summarize NewCountry = make_set(Location,1048576), LastObservedTime = max(TimeGenerated), AppName = make_set(AppDisplayName,1048576) by UserPrincipalName, PasswordResult, AuthSucceeded, failureReason
// Combining both tables by user
| join kind=inner KnownUserCountry on UserPrincipalName
// Compare both arrays and identify if the country has been observed in the past.
| extend CountryDiff = set_difference(NewCountry,KnownCountry)
| extend CountryDiffCount = array_length(CountryDiff)
// Count the new column to only alert if there is a difference between both arrays
| where CountryDiffCount != 0
| extend NewCountryEvent = CountryDiff
// Getting UserName and Domain
| extend Name = split(UserPrincipalName,"@",0),
    Domain = split(UserPrincipalName,"@",1)
| mv-expand Name,Domain
Microsoft Sentinel Original KQL T1190 ↗
OMI Vulnerability Exploitation
Following the September 14th, 2021 release of three Elevation of Privilege (EoP) vulnerabilities (CVE-2021-38645, CVE-2021-38649, CVE-2021-38648) and one unauthenticated Remote Code Execution (RCE) vulnerability (CVE-2021-38647) in the Open Management Infrastructure (OMI) Framework. This detection validates that any OMS-agent that is reporting to the Microsoft Sentinel workspace is updated with the patch. The detection will go over the heartbeats received from all agents over the last day and wi
Show query
let OMIVulnerabilityPatchVersion = "OMIVulnerabilityPatchVersion:1.13.40-0";
Heartbeat
| where Category == "Direct Agent"
| summarize arg_max(TimeGenerated,*) by Computer
| parse strcat("Version:" , Version) with * "Version:" Major:long "."
Minor:long "." Patch:long "-" *
| parse OMIVulnerabilityPatchVersion with * "OMIVulnerabilityPatchVersion:"
OMIVersionMajor:long "." OMIVersionMinor:long "." OMIVersionPatch:long "-" *
| where Major <OMIVersionMajor or (Major==OMIVersionMajor and Minor
<OMIVersionMinor) or (Major==OMIVersionMajor and Minor==OMIVersionMinor and
Patch<OMIVersionPatch) 
| project Version, Major,Minor,Patch,
Computer,ComputerIP,OSType,OSName,ResourceId
Microsoft Sentinel Original KQL T1485 ↗
Potential re-named sdelete usage (ASIM Version)
'This detection looks for command line parameters associated with the use of Sysinternals sdelete (https://docs.microsoft.com/sysinternals/downloads/sdelete) to delete multiple files on a host's C drive. A threat actor may re-name the tool to avoid detection and then use it for destructive attacks on a host. This detection uses the ASIM imProcess parser, this will need to be deployed before use - https://docs.microsoft.com/azure/sentinel/normalization'
Show query
imProcess
| where CommandLine has_all ("accepteula", "-s", "-r", "-q")
| where Process !endswith "sdelete.exe"
| where CommandLine !has "sdelete"
| extend AccountName = tostring(split(ActorUsername, @'\')[1]), AccountNTDomain = tostring(split(ActorUsername, @'\')[0])
Microsoft Sentinel Original KQL T1078.004 ↗
Privileged User Logon from new ASN
'Detects a successful logon by a privileged account from an ASN not logged in from in the last 14 days. Monitor these logons to ensure they are legitimate and identify if there are any similar sign ins.'
Show query
let admins=(IdentityInfo
  | where AssignedRoles contains "admin" or GroupMembership has "Admin"
  | summarize by tolower(AccountUPN));
  let known_asns = (
  SigninLogs
  | where TimeGenerated between(ago(14d)..ago(1d))
  | where ResultType == 0
  | summarize by AutonomousSystemNumber);
  SigninLogs
  | where TimeGenerated > ago(1d)
  | where ResultType == 0
  | where tolower(UserPrincipalName) in (admins)
  | where AutonomousSystemNumber !in (known_asns)
  | project-reorder TimeGenerated, UserPrincipalName, UserAgent, IPAddress, AutonomousSystemNumber
  | extend AccountName = tostring(split(UserPrincipalName, "@")[0]), AccountUPNSuffix = tostring(split(UserPrincipalName, "@")[1])
Microsoft Sentinel Original KQL T1018 ↗
Probable AdFind Recon Tool Usage (Normalized Process Events)
'Identifies the host and account that executed AdFind by hash and filename in addition to common and unique flags that are used by many threat actors in discovery. To use this analytics rule, make sure you have deployed the [ASIM normalization parsers](https://aka.ms/ASimProcessEvent)'
Show query
let args = dynamic(["objectcategory","domainlist","dcmodes","adinfo","trustdmp","computers_pwdnotreqd","Domain Admins", "objectcategory=person", "objectcategory=computer", "objectcategory=*","dclist"]);
let parentProcesses = dynamic(["pwsh.exe","powershell.exe","cmd.exe"]);
imProcessCreate
//looks for execution from a shell
| where ActingProcessName has_any (parentProcesses)
| extend ActingProcessFileName = tostring(split(ActingProcessName, '\\')[-1])
| where ActingProcessFileName in~ (parentProcesses)
// main filter
| where Process hassuffix "AdFind.exe" or TargetProcessSHA256 == "c92c158d7c37fea795114fa6491fe5f145ad2f8c08776b18ae79db811e8e36a3"
// AdFind common Flags to check for from various threat actor TTPs
or CommandLine has_any (args)
| extend AlgorithmType = "SHA256"
| extend AccountName = tostring(split(User, @'\')[1]), AccountNTDomain = tostring(split(User, @'\')[0])
| extend HostName = tostring(split(Dvc, ".")[0]), DomainIndex = toint(indexof(Dvc, '.'))
| extend HostNameDomain = iff(DomainIndex != -1, substring(Dvc, DomainIndex + 1), Dvc)
| project-away DomainIndex
Microsoft Sentinel Original KQL T1190 ↗
PulseConnectSecure - CVE-2021-22893 Possible Pulse Connect Secure RCE Vulnerability Attack
'This query identifies exploitation attempts using Pulse Connect Secure(PCS) vulnerability (CVE-2021-22893) to the VPN server'
Show query
let threshold = 3;
PulseConnectSecure
| where Messages contains "Unauthenticated request url /dana-na/"
| summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated), count() by Source_IP
| where count_ > threshold
Microsoft Sentinel Original KQL T1021 ↗
RDP Nesting
'Query detects potential lateral movement within a network by identifying when an RDP connection (EventID 4624, LogonType 10) is made to an initial system, followed by a subsequent RDP connection from that system to another, using the same account within a 60-minute window. To reduce false positives, it excludes scenarios where the same account has made 5 or more connections to the same set of computers in the previous 7 days. This approach focuses on highlighting unusual RDP behaviour that sug
Show query
let endtime = 1d;
// Function to resolve hostname to IP address using DNS logs or a lookup table (example syntax)
let rdpConnections =
(union isfuzzy=true
(
SecurityEvent
| where TimeGenerated >= ago(endtime)
| where EventID == 4624 and LogonType == 10
// Labeling the first RDP connection time, computer and ip
| extend
FirstHop = bin(TimeGenerated, 1m),
FirstComputer = toupper(Computer),
FirstRemoteIPAddress = IpAddress,
Account = tolower(Account)
),
(
WindowsEvent
| where TimeGenerated >= ago(endtime)
| where EventID == 4624 and EventData has ("10")
| extend LogonType = tostring(EventData.LogonType)
| where LogonType == 10 // Labeling the first RDP connection time, computer and ip
| extend Account = strcat(tostring(EventData.TargetDomainName), "", tostring(EventData.TargetUserName))
| extend IpAddress = tostring(EventData.IpAddress)
| extend
FirstHop = bin(TimeGenerated, 1m),
FirstComputer = toupper(Computer),
FirstRemoteIPAddress = IpAddress,
Account = tolower(Account)
))
| join kind=inner (
(union isfuzzy=true
(
SecurityEvent
| where TimeGenerated >= ago(endtime)
| where EventID == 4624 and LogonType == 10
// Labeling the second RDP connection time, computer and ip
| extend
SecondHop = bin(TimeGenerated, 1m),
SecondComputer = toupper(Computer),
SecondRemoteIPAddress = IpAddress,
Account = tolower(Account)
),
(
WindowsEvent
| where TimeGenerated >= ago(endtime)
| where EventID == 4624 and EventData has ("10")
| extend LogonType = toint(EventData.LogonType)
| where LogonType == 10 // Labeling the second RDP connection time, computer and ip
| extend Account = strcat(tostring(EventData.TargetDomainName), "", tostring(EventData.TargetUserName))
| extend IpAddress = tostring(EventData.IpAddress)
| extend
SecondHop = bin(TimeGenerated, 1m),
SecondComputer = toupper(Computer),
SecondRemoteIPAddress = IpAddress,
Account = tolower(Account)
))
)
on Account
| distinct
Account,
FirstHop,
FirstComputer,
FirstRemoteIPAddress,
SecondHop,
SecondComputer,
SecondRemoteIPAddress,
AccountType,
Activity,
LogonTypeName,
ProcessName;
// Resolve hostnames to IP addresses device network Ip's
let listOfFirstComputer = rdpConnections | distinct FirstComputer;
let listOfSecondComputer = rdpConnections | distinct SecondComputer;
let resolvedIPs =
DeviceNetworkInfo
| where TimeGenerated >= ago(endtime)
| where isnotempty(ConnectedNetworks) and NetworkAdapterStatus == "Up"
| extend ClientIP = tostring(parse_json(IPAddresses[0]).IPAddress)
| where isnotempty(ClientIP)
| where DeviceName in~ (listOfFirstComputer) or DeviceName in~ (listOfSecondComputer)
| summarize arg_max(TimeGenerated, ClientIP) by Computer= DeviceName
| project Computer=toupper(Computer), ResolvedIP = ClientIP;
// Join resolved IPs with the RDP connections
rdpConnections
| join kind=inner (resolvedIPs) on $left.FirstComputer == $right.Computer
| join kind=inner (resolvedIPs) on $left.SecondComputer == $right.Computer
// | where ResolvedIP != ResolvedIP1
| distinct
Account,
FirstHop,
FirstComputer,
FirstComputerIP = ResolvedIP,
FirstRemoteIPAddress,
SecondHop,
SecondComputer,
SecondComputerIP = ResolvedIP1,
SecondRemoteIPAddress,
AccountType,
Activity,
LogonTypeName,
ProcessName
// Ensure the first connection is before the second connection
// Identify only RDP to another computer from within the first RDP connection by only choosing matches where the Computer names do not match
// Ensure the IPAddresses do not match by excluding connections from the same computers with first hop RDP connections to multiple computers
| where FirstComputer != SecondComputer
and FirstRemoteIPAddress != SecondRemoteIPAddress
and SecondHop > FirstHop
// Ensure the second hop occurs within 30 minutes of the first hop
| where SecondHop <= FirstHop + 30m
| where SecondRemoteIPAddress == FirstComputerIP
| summarize FirstHopFirstSeen = min(FirstHop), FirstHopLastSeen = max(FirstHop)
by
Account,
FirstComputer,
FirstComputerIP,
FirstRemoteIPAddress,
SecondHop,
SecondComputer,
SecondComputerIP,
SecondRemoteIPAddress,
AccountType,
Activity,
LogonTypeName,
ProcessName
| extend
AccountName = tostring(split(Account, @"")[1]),
AccountNTDomain = tostring(split(Account, @"")[0])
| extend
HostName1 = tostring(split(FirstComputer, ".")[0]),
DomainIndex = toint(indexof(FirstComputer, '.'))
| extend HostNameDomain1 = iff(DomainIndex != -1, substring(FirstComputer, DomainIndex + 1), FirstComputer)
| extend
HostName2 = tostring(split(SecondComputer, ".")[0]),
DomainIndex = toint(indexof(SecondComputer, '.'))
| extend HostNameDomain2 = iff(DomainIndex != -1, substring(SecondComputer, DomainIndex + 1), SecondComputer)
| project-away DomainIndex
Microsoft Sentinel Original KQL T1021 ↗
Rare RDP Connections
'Identifies when an RDP connection is new or rare related to any logon type by a given account today compared with the previous 14 days. RDP connections are indicated by the EventID 4624 with LogonType = 10'
Show query
let starttime = 14d;
let endtime = 1d;
(union isfuzzy=true
(SecurityEvent
| where TimeGenerated >= ago(endtime)
| where EventID == 4624 and LogonType == 10
| summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated), ConnectionCount = count()
by Account = tolower(Account), Computer = toupper(Computer), IpAddress, AccountType, Activity, LogonTypeName, ProcessName
// use left anti to exclude anything from the previous 14 days that is not rare
),
(WindowsEvent
| where TimeGenerated >= ago(endtime)
| where EventID == 4624 and EventData has ("10")
| extend LogonType = tostring(EventData.LogonType)
| where  LogonType == 10
| extend Account = strcat(tostring(EventData.TargetDomainName),"\\", tostring(EventData.TargetUserName))
| extend ProcessName = tostring(EventData.ProcessName)
| extend IpAddress = tostring(EventData.IpAddress)
| extend TargetUserSid = tostring(EventData.TargetUserSid)
| extend AccountType=case(Account endswith "$" or TargetUserSid in ("S-1-5-18", "S-1-5-19", "S-1-5-20"), "Machine", isempty(TargetUserSid), "", "User")
| extend Activity="4624 - An account was successfully logged on."
| extend LogonTypeName="10 - RemoteInteractive"
| summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated), ConnectionCount = count()
by Account = tolower(Account), Computer = toupper(Computer), IpAddress, AccountType, Activity, LogonTypeName, ProcessName
))
| join kind=leftanti (
(union isfuzzy=true
(SecurityEvent
| where TimeGenerated between (ago(starttime) .. ago(endtime))
| where EventID == 4624
| summarize by Computer = toupper(Computer), IpAddress, Account = tolower(Account)
),
( WindowsEvent
| where TimeGenerated between (ago(starttime) .. ago(endtime))
| where EventID == 4624
| extend IpAddress = tostring(EventData.IpAddress)
| extend Account = strcat(tostring(EventData.TargetDomainName),"\\", tostring(EventData.TargetUserName))
| summarize by Computer = toupper(Computer), IpAddress, Account = tolower(Account)
))
) on Account, Computer
| summarize StartTime = min(StartTime), EndTime = max(EndTime), ConnectionCount = sum(ConnectionCount)
by Account, Computer, IpAddress, AccountType, Activity, LogonTypeName, ProcessName
| extend HostName = tostring(split(Computer, ".")[0]), DomainIndex = toint(indexof(Computer, '.'))
| extend HostNameDomain = iff(DomainIndex != -1, substring(Computer, DomainIndex + 1), Computer)
| extend AccountName = tostring(split(Account, @"\")[1]), AccountNTDomain = tostring(split(Account, @"\")[0])
| project-away DomainIndex
Microsoft Sentinel Original KQL T1071 ↗
Risky user signin observed in non-Microsoft network device
'This content is utilized to identify instances of successful login by risky users, who have been observed engaging in potentially suspicious network activity on non-Microsoft network devices.'
Show query
SigninLogs
//Find risky Signin
| where RiskState == "atRisk" and ResultType == 0
| extend Signin_Time = TimeGenerated
| summarize
    AppDisplayName=make_set(AppDisplayName),
    ClientAppUsed=make_set(ClientAppUsed),
    UserAgent=make_set(UserAgent),
    CorrelationId=make_set(CorrelationId),
    Signin_Time= min(Signin_Time),
    RiskEventTypes=make_set(RiskEventTypes)
    by
    ConditionalAccessStatus,
    IPAddress,
    IsRisky,
    ResourceDisplayName,
    RiskDetail,
    ResultType,
    RiskLevelAggregated,
    RiskLevelDuringSignIn,
    RiskState,
    UserPrincipalName=tostring(tolower(UserPrincipalName)),
    SourceSystem
| join kind=inner (
    CommonSecurityLog
    | where DeviceVendor has_any  ("Palo Alto Networks", "Fortinet", "Check Point", "Zscaler")
    | where DeviceProduct startswith "FortiGate" or DeviceProduct startswith  "PAN" or DeviceProduct startswith  "VPN" or DeviceProduct startswith "FireWall" or DeviceProduct startswith  "NSSWeblog" or DeviceProduct startswith "URL"
    | where DeviceAction != "Block"
    | where isnotempty(RequestURL)
    | where isnotempty(SourceUserName)
    | extend SourceUserName = tolower(SourceUserName)
    | summarize
        min(TimeGenerated),
        max(TimeGenerated),
        Activity=make_set(Activity)
        by DestinationHostName, DestinationIP, RequestURL, SourceUserName=tostring(tolower(SourceUserName)),DeviceVendor,DeviceProduct
    | extend 3p_observed_Time= min_TimeGenerated,Name = tostring(split(SourceUserName,"@")[0]),UPNSuffix =tostring(split(SourceUserName,"@")[1]))
    on $left.IPAddress == $right.DestinationIP and $left.UserPrincipalName == $right.SourceUserName
| extend Timediff = datetime_diff('day', 3p_observed_Time, Signin_Time)
| where Timediff <= 1 and Timediff >= 0
Microsoft Sentinel Original KQL T1041 ↗
RunningRAT request parameters
'This detection will alert when RunningRAT URI parameters or paths are detect in an HTTP request. Id the device blocked this communication presence of this alert means the RunningRAT implant is likely still executing on the source host.'
Show query
let runningRAT_parameters = dynamic(['/ui/chk', 'mactok=', 'UsRnMe=', 'IlocalP=', 'kMnD=']);
CommonSecurityLog
| where RequestMethod == "GET"
| project TimeGenerated, DeviceVendor, DeviceProduct, DeviceAction, DestinationDnsDomain, DestinationIP, RequestURL, SourceIP, SourceHostName, RequestClientApplication
| where RequestURL has_any (runningRAT_parameters)
Microsoft Sentinel Original KQL T1195 ↗
SUNBURST and SUPERNOVA backdoor hashes (Normalized File Events)
Identifies SolarWinds SUNBURST and SUPERNOVA backdoor file hash IOCs in File Events To use this analytics rule, make sure you have deployed the [ASIM normalization parsers](https://aka.ms/ASimFileEvent) References: - https://www.fireeye.com/blog/threat-research/2020/12/evasive-attacker-leverages-solarwinds-supply-chain-compromises-with-sunburst-backdoor.html - https://gist.github.com/olafhartong/71ffdd4cab4b6acd5cbcd1a0691ff82f
Show query
let SunburstMD5=dynamic(["b91ce2fa41029f6955bff20079468448","02af7cec58b9a5da1c542b5a32151ba1","2c4a910a1299cdae2a4e55988a2f102e","846e27a652a5e1bfbd0ddd38a16dc865","4f2eb62fa529c0283b28d05ddd311fae"]);
let SupernovaMD5="56ceb6d0011d87b6e4d7023d7ef85676";
imFileEvent
| where TargetFileMD5 in (SunburstMD5) or TargetFileMD5 in (SupernovaMD5)
| extend AccountName = tostring(split(User, @'\')[1]), AccountNTDomain = tostring(split(User, @'\')[0])
| extend AlgorithmType = "MD5"
Microsoft Sentinel Original KQL T1059 ↗
SUNBURST suspicious SolarWinds child processes (Normalized Process Events)
Identifies suspicious child processes of SolarWinds.Orion.Core.BusinessLayer.dll that may be evidence of the SUNBURST backdoor References: - https://www.fireeye.com/blog/threat-research/2020/12/evasive-attacker-leverages-solarwinds-supply-chain-compromises-with-sunburst-backdoor.html - https://gist.github.com/olafhartong/71ffdd4cab4b6acd5cbcd1a0691ff82f To use this analytics rule, make sure you have deployed the [ASIM normalization parsers](https://aka.ms/ASimProcessEvent)'
Show query
let excludeProcs = dynamic([@"\SolarWinds\Orion\APM\APMServiceControl.exe", @"\SolarWinds\Orion\ExportToPDFCmd.Exe", @"\SolarWinds.Credentials\SolarWinds.Credentials.Orion.WebApi.exe", @"\SolarWinds\Orion\Topology\SolarWinds.Orion.Topology.Calculator.exe", @"\SolarWinds\Orion\Database-Maint.exe", @"\SolarWinds.Orion.ApiPoller.Service\SolarWinds.Orion.ApiPoller.Service.exe", @"\Windows\SysWOW64\WerFault.exe"]);
imProcessCreate
| where Process hassuffix 'solarwinds.businesslayerhost.exe'
| where not(Process has_any (excludeProcs))
| extend AccountName = tostring(split(ActorUsername, @'\')[1]), AccountNTDomain = tostring(split(ActorUsername, @'\')[0])
| extend HostName = tostring(split(Dvc, ".")[0]), DomainIndex = toint(indexof(Dvc, '.'))
| extend HostNameDomain = iff(DomainIndex != -1, substring(Dvc, DomainIndex + 1), Dvc)
| project-away DomainIndex
Microsoft Sentinel Original KQL T1485 ↗
Sdelete deployed via GPO and run recursively (ASIM Version)
'This query looks for the Sdelete process being run recursively after being deployed to a host via GPO. Attackers could use this technique to deploy Sdelete to multiple host and delete data on them. This query uses the Advanced Security Information Model. Parsers will need to be deployed before use: https://docs.microsoft.com/azure/sentinel/normalization'
Show query
_Im_ProcessEvent
| where EventType =~ "ProcessCreated"
| where Process endswith "svchost.exe"
| where CommandLine has "-k GPSvcGroup" or CommandLine has "-s gpsvc"
| extend timekey = bin(TimeGenerated, 1m)
| project timekey, ActingProcessId, Dvc
| join kind=inner (
  _Im_ProcessEvent
  | where EventType =~ "ProcessCreated"
  | where Process =~ "sdelete.exe" or CommandLine has "sdelete"
  | where ActingProcessName endswith "svchost.exe"
  | where CommandLine has_all ("-s", "-r")
  | extend timekey = bin(TimeGenerated, 1m)
  ) 
  on $left.ActingProcessId == $right.ParentProcessId, timekey, Dvc
| extend AccountName = tostring(split(ActorUsername, @'\')[1]), AccountNTDomain = tostring(split(ActorUsername, @'\')[0])
| extend HostName = tostring(split(Dvc, ".")[0]), DomainIndex = toint(indexof(Dvc, '.'))
| extend HostNameDomain = iff(DomainIndex != -1, substring(Dvc, DomainIndex + 1), Dvc)
| project-away DomainIndex
Microsoft Sentinel Original KQL T1078.004 ↗
Service Principal Assigned App Role With Sensitive Access
'Detects a Service Principal being assigned an app role that has sensitive access such as Mail.Read. A threat actor who compromises a Service Principal may assign it an app role to allow it to access sensitive data, or to perform other actions. Ensure that any assignment to a Service Principal is valid and appropriate. Ref: https://docs.microsoft.com/azure/active-directory/fundamentals/security-operations-applications#application-granted-highly-privileged-permissions'
Show query
// Add other permissions to this list as needed
let permissions = dynamic([".All", "ReadWrite", "Mail.", "offline_access", "Files.Read", "Notes.Read", "ChannelMessage.Read", "Chat.Read", "TeamsActivity.Read",
"Group.Read", "EWS.AccessAsUser.All", "EAS.AccessAsUser.All"]);
let auditList = 
AuditLogs
| where OperationName =~ "Add app role assignment to service principal"
| mv-expand TargetResources[0].modifiedProperties
| extend TargetResources_0_modifiedProperties = column_ifexists("TargetResources_0_modifiedProperties", '')
| where isnotempty(TargetResources_0_modifiedProperties)
;
let detailsList = auditList
| where TargetResources_0_modifiedProperties.displayName =~ "AppRole.Value" or TargetResources_0_modifiedProperties.displayName =~ "DelegatedPermissionGrant.Scope"
| extend Permissions = split((parse_json(tostring(TargetResources_0_modifiedProperties.newValue))), " ")
| where Permissions has_any (permissions)
| summarize AddedPermissions=make_set(Permissions,200) by CorrelationId
| join kind=inner auditList on CorrelationId
| extend InitiatingAppName = tostring(InitiatedBy.app.displayName)
| extend InitiatingAppServicePrincipalId = tostring(InitiatedBy.app.servicePrincipalId)
| extend InitiatingUserPrincipalName = tostring(InitiatedBy.user.userPrincipalName)
| extend InitiatingAadUserId = tostring(InitiatedBy.user.id)
| extend InitiatingIPAddress = tostring(InitiatedBy.user.ipAddress)
| extend InitiatedBy = tostring(iff(isnotempty(InitiatingUserPrincipalName),InitiatingUserPrincipalName, InitiatingAppName))
| extend displayName = tostring(TargetResources_0_modifiedProperties.displayName), newValue = tostring(parse_json(tostring(TargetResources_0_modifiedProperties.newValue)))
| where displayName == "ServicePrincipal.ObjectID" or displayName == "ServicePrincipal.DisplayName"
| extend displayName = case(displayName == "ServicePrincipal.ObjectID", "ServicePrincipalObjectID", displayName == "ServicePrincipal.DisplayName", "ServicePrincipalDisplayName", displayName)
| project TimeGenerated, CorrelationId, Id, AddedPermissions = tostring(AddedPermissions), InitiatingAadUserId, InitiatingAppName, InitiatingAppServicePrincipalId, InitiatingIPAddress, InitiatingUserPrincipalName, InitiatedBy, displayName, newValue
;
detailsList | project Id, displayName, newValue
| evaluate pivot(displayName, make_set(newValue))
| join kind=inner detailsList on Id
| extend ServicePrincipalObjectID = todynamic(column_ifexists("ServicePrincipalObjectID", "")), ServicePrincipalDisplayName = todynamic(column_ifexists("ServicePrincipalDisplayName", ""))
| mv-expand ServicePrincipalObjectID, ServicePrincipalDisplayName
| project-away Id1, displayName, newValue
| extend ServicePrincipalObjectID = tostring(ServicePrincipalObjectID), ServicePrincipalDisplayName = tostring(ServicePrincipalDisplayName)
| summarize FirstSeen = min(TimeGenerated), LastSeen = max(TimeGenerated), EventIds = make_set(Id,200) by CorrelationId, AddedPermissions, InitiatingAadUserId, InitiatingAppName, InitiatingAppServicePrincipalId, InitiatingIPAddress, InitiatingUserPrincipalName, InitiatedBy, ServicePrincipalDisplayName, ServicePrincipalObjectID
| extend InitiatingAccountName = tostring(split(InitiatingUserPrincipalName, "@")[0]), InitiatingAccountUPNSuffix = tostring(split(InitiatingUserPrincipalName, "@")[1])
Microsoft Sentinel Original KQL T1078.004 ↗
Service Principal Assigned Privileged Role
'Detects a privileged role being added to a Service Principal. Ensure that any assignment to a Service Principal is valid and appropriate - Service Principals should not be assigned to very highly privileged roles such as Global Admin. Ref: https://docs.microsoft.com/azure/active-directory/fundamentals/security-operations-privileged-accounts#changes-to-privileged-accounts'
Show query
AuditLogs
  | where OperationName has_all ("member to role", "add")
  | where Result =~ "Success"
  | extend type_ = tostring(TargetResources[0].type)
  | where type_ =~ "ServicePrincipal"
  | where isnotempty(TargetResources)
  | extend InitiatingAppName = tostring(InitiatedBy.app.displayName)
  | extend InitiatingAppServicePrincipalId = tostring(InitiatedBy.app.servicePrincipalId)
  | extend InitiatingUserPrincipalName = tostring(InitiatedBy.user.userPrincipalName)
  | extend InitiatingAadUserId = tostring(InitiatedBy.user.id)
  | extend InitiatingIPAddress = tostring(InitiatedBy.user.ipAddress)
  | extend InitiatedBy = tostring(iff(isnotempty(InitiatingUserPrincipalName),InitiatingUserPrincipalName, InitiatingAppName))
  | extend ServicePrincipalName = tostring(TargetResources[0].displayName)
  | extend ServicePrincipalId = tostring(TargetResources[0].id)
  | mv-expand TargetResources[0].modifiedProperties
  | extend TargetResources_0_modifiedProperties = columnifexists("TargetResources_0_modifiedProperties", '')
  | where isnotempty(TargetResources_0_modifiedProperties)
  | extend displayName = tostring(TargetResources_0_modifiedProperties.displayName), newValue = tostring(parse_json(tostring(TargetResources_0_modifiedProperties.newValue)))
  | where displayName == "Role.DisplayName" and newValue contains "admin"
  | extend InitiatingAccountName = tostring(split(InitiatingUserPrincipalName, "@")[0]), InitiatingAccountUPNSuffix = tostring(split(InitiatingUserPrincipalName, "@")[1])
  | extend TargetRole = newValue
  | project-reorder TimeGenerated, ServicePrincipalName, ServicePrincipalId, InitiatedBy, TargetRole, InitiatingIPAddress
Microsoft Sentinel Original KQL T1078.004 ↗
Service Principal Authentication Attempt from New Country
'Detects when there is a Service Principal login attempt from a country that has not seen a successful login in the previous 14 days. Threat actors may attempt to authenticate with credentials from compromised accounts - monitoring attempts from anomalous locations may help identify these attempts. Authentication attempts should be investigated to ensure the activity was legitimate and if there is other similar activity. Ref: https://docs.microsoft.com/azure/active-directory/fundamentals/s
Show query
let known_locations = (
  AADServicePrincipalSignInLogs
  | where TimeGenerated between(ago(14d)..ago(1d))
  | where ResultType == 0
  | summarize by Location);
  AADServicePrincipalSignInLogs
  | where TimeGenerated > ago(1d)
  | where ResultType != 50126
  | where Location !in (known_locations)
  | extend City = tostring(parse_json(LocationDetails).city)
  | extend State = tostring(parse_json(LocationDetails).state)
  | extend Place = strcat(City, " - ", State)
  | extend Result = strcat(tostring(ResultType), " - ", ResultDescription)
  | summarize FirstSeen=min(TimeGenerated), LastSeen=max(TimeGenerated), make_set(Result), make_set(IPAddress), make_set(Place) by ServicePrincipalName, Location
Microsoft Sentinel Original KQL T1078 ↗
Sign-ins from IPs that attempt sign-ins to disabled accounts (Uses Authentication Normalization)
'Identifies IPs with failed attempts to sign in to one or more disabled accounts signed in successfully to another account. To use this analytics rule, make sure you have deployed the [ASIM normalization parsers](https://aka.ms/ASimAuthentication)'
Show query
imAuthentication
| where EventResult =='Failure'
| where EventResultDetails == 'User disabled'
| summarize StartTime=min(EventStartTime), EndTime=max(EventEndTime), disabledAccountLoginAttempts = count()
      , disabledAccountsTargeted = dcount(TargetUsername), disabledAccountSet = make_set(TargetUsername)
      , applicationsTargeted = dcount(TargetAppName)
      , applicationSet = make_set(TargetAppName) 
      by SrcDvcIpAddr, Type
| order by disabledAccountLoginAttempts desc
| join kind=leftouter 
    (
    // Consider these IPs suspicious - and alert any related  successful sign-ins
    imAuthentication
    | where EventResult=='Success'
    | summarize successfulAccountSigninCount = dcount(TargetUsername), successfulAccountSigninSet = makeset(TargetUsername, 15) by SrcDvcIpAddr, Type
    // Assume IPs associated with sign-ins from 100+ distinct user accounts are safe
    | where successfulAccountSigninCount < 100
    )
    on SrcDvcIpAddr
| where isnotempty(successfulAccountSigninCount)
| project StartTime, EndTime, SrcDvcIpAddr, disabledAccountLoginAttempts, disabledAccountsTargeted, disabledAccountSet, applicationSet, 
successfulAccountSigninCount, successfulAccountSigninSet, Type
| order by disabledAccountLoginAttempts
Microsoft Sentinel Original KQL T1190 ↗
Silk Typhoon New UM Service Child Process
'This query looks for new processes being spawned by the Exchange UM service where that process has not previously been observed before. Reference: https://www.microsoft.com/security/blog/2021/03/02/hafnium-targeting-exchange-servers/'
Show query
let lookback = 14d;
let timeframe = 1d;
(union isfuzzy=true
(SecurityEvent
| where TimeGenerated > ago(lookback) and TimeGenerated < ago(timeframe)
| where EventID == 4688
| where ParentProcessName has_any ("umworkerprocess.exe", "UMService.exe")
| join kind=rightanti (
SecurityEvent
| where TimeGenerated > ago(timeframe)
| where ParentProcessName has_any ("umworkerprocess.exe", "UMService.exe")
| where EventID == 4688) on NewProcessName
),
(WindowsEvent
| where TimeGenerated > ago(lookback) and TimeGenerated < ago(timeframe)
| where EventID == 4688 and EventData has_any ("umworkerprocess.exe", "UMService.exe")
| extend ParentProcessName = tostring(EventData.ParentProcessName)
| where ParentProcessName has_any ("umworkerprocess.exe", "UMService.exe")
| extend NewProcessName = tostring(EventData.NewProcessName)
| extend Account = strcat(tostring(EventData.SubjectDomainName),"\\", tostring(EventData.SubjectUserName))
| extend IpAddress = tostring(EventData.IpAddress)
| join kind=rightanti (
WindowsEvent
| where TimeGenerated > ago(timeframe)
| where EventID == 4688  and EventData has_any ("umworkerprocess.exe", "UMService.exe")
| extend ParentProcessName = tostring(EventData.ParentProcessName)
| where ParentProcessName has_any ("umworkerprocess.exe", "UMService.exe")
| extend NewProcessName = tostring(EventData.NewProcessName)
| extend Account = strcat(tostring(EventData.SubjectDomainName),"\\", tostring(EventData.SubjectUserName))
| extend IpAddress = tostring(EventData.IpAddress)) on NewProcessName
| extend HostName = tostring(split(Computer, ".")[0]), DomainIndex = toint(indexof(Computer, '.'))
| extend HostNameDomain = iff(DomainIndex != -1, substring(Computer, DomainIndex + 1), Computer)
| extend SubjectUserName = tostring(EventData.SubjectUserName), SubjectDomainName = tostring(EventData.SubjectDomainName)
| project-away DomainIndex
))  
Microsoft Sentinel Original KQL T1190 ↗
Silk Typhoon Suspicious Exchange Request
'This query looks for suspicious request patterns to Exchange servers that fit a pattern observed by Silk Typhoon actors. The same query can be run on HTTPProxy logs from on-premise hosted Exchange servers. Reference: https://www.microsoft.com/security/blog/2021/03/02/hafnium-targeting-exchange-servers/'
Show query
let exchange_servers = (
W3CIISLog
| where TimeGenerated > ago(14d)
| where sSiteName =~ "Exchange Back End"
| summarize by Computer);
W3CIISLog
| where TimeGenerated > ago(1d)
| where Computer in (exchange_servers)
| where csUriQuery startswith "t="
| project-reorder TimeGenerated, Computer, csUriStem, csUriQuery, csUserName, csUserAgent, cIP
| extend HostName = tostring(split(Computer, ".")[0]), DomainIndex = toint(indexof(Computer, '.'))
| extend HostNameDomain = iff(DomainIndex != -1, substring(Computer, DomainIndex + 1), Computer)
| extend AccountName = tostring(split(csUserName, "@")[0]), AccountUPNSuffix = tostring(split(csUserName, "@")[1])
Microsoft Sentinel Original KQL T1190 ↗
Silk Typhoon Suspicious File Downloads.
'This query looks for messages related to file downloads of suspicious file types. This query uses the Exchange HttpProxy AOBGeneratorLog, you will need to onboard this log as a custom log under the table http_proxy_oab_CL before using this query. Reference: https://www.microsoft.com/security/blog/2021/03/02/hafnium-targeting-exchange-servers/'
Show query
let scriptExtensions = dynamic([".php", ".jsp", ".js", ".aspx", ".asmx", ".asax", ".cfm", ".shtml"]);
http_proxy_oab_CL
| where RawData contains "Download failed and temporary file"
| extend File = extract("([^\\\\]*)(\\\\[^']*)",2,RawData)
| extend Extension = strcat(".",split(File, ".")[-1])
| extend InteractiveFile = iif(Extension in (scriptExtensions), "Yes", "No")
// Uncomment the following line to alert only on interactive file download type
//| where InteractiveFile =~ "Yes"
| extend HostName = tostring(split(Computer, ".")[0]), DomainIndex = toint(indexof(Computer, '.'))
| extend HostNameDomain = iff(DomainIndex != -1, substring(Computer, DomainIndex + 1), Computer)
Microsoft Sentinel Original KQL T1190 ↗
Silk Typhoon Suspicious UM Service Error
'This query looks for errors that may indicate that an attacker is attempting to exploit a vulnerability in the service. Reference: https://www.microsoft.com/security/blog/2021/03/02/hafnium-targeting-exchange-servers/'
Show query
Event
| where EventLog =~ "Application"
| where Source startswith "MSExchange"
| where EventLevelName =~ "error"
| where (RenderedDescription startswith "Watson report" and RenderedDescription contains "umworkerprocess" and RenderedDescription contains "TextFormattingRunProperties") or RenderedDescription startswith "An unhandled exception occurred in a UM worker process" or RenderedDescription startswith "The Microsoft Exchange Unified Messaging service" or RenderedDescription contains "MSExchange Unified Messaging"
| where RenderedDescription !contains "System.OutOfMemoryException"
| extend HostName = tostring(split(Computer, ".")[0]), DomainIndex = toint(indexof(Computer, '.'))
| extend HostNameDomain = iff(DomainIndex != -1, substring(Computer, DomainIndex + 1), Computer)
| project-away DomainIndex
Microsoft Sentinel Original KQL T1195 ↗
Solorigate Defender Detections
'Surfaces any Defender Alert for Solorigate Events. In Microsoft Sentinel the SecurityAlerts table includes only the Device Name of the affected device, this query joins the DeviceInfo table to clearly connect other information such as Device group, ip, logged on users etc. This way, the Microsoft Sentinel user can have all the pertinent device info in one view for all the the Solarigate Defender alerts.'
Show query
DeviceInfo
| extend DeviceName = tolower(DeviceName)
| join (SecurityAlert
| where ProviderName =~ "MDATP"
| extend ThreatName = tostring(parse_json(ExtendedProperties).ThreatName)
| where ThreatName has "Solorigate"
) on $left.DeviceName == $right.CompromisedEntity
| project TimeGenerated, DisplayName, ThreatName, CompromisedEntity, PublicIP, MachineGroup, AlertSeverity, Description, LoggedOnUsers, DeviceId, TenantId
| extend HostName = tostring(split(CompromisedEntity, ".")[0]), DomainIndex = toint(indexof(CompromisedEntity, '.'))
| extend HostNameDomain = iff(DomainIndex != -1, substring(CompromisedEntity, DomainIndex + 1), CompromisedEntity)
| project-away DomainIndex
Microsoft Sentinel Original KQL T1078.004 ↗
Suspicious Login from deleted guest account
' This query will detect logins from guest account which was recently deleted. For any successful logins from deleted identities should be investigated further if any existing user accounts have been altered or linked to such identity prior deletion'
Show query
let query_frequency = 1h;
let query_period = 1d;
AuditLogs
| where TimeGenerated > ago(query_frequency)
| where Category =~ "UserManagement" and OperationName =~ "Delete user"
| mv-expand TargetResource = TargetResources
| where TargetResource["type"] == "User" and TargetResource["userPrincipalName"] has "#EXT#"
| extend ParsedDeletedUserPrincipalName = extract(@"^[0-9a-f]{32}([^\#]+)\#EXT\#", 1, tostring(TargetResource["userPrincipalName"]))
| extend
    Initiator = iif(isnotempty(InitiatedBy["app"]), tostring(InitiatedBy["app"]["displayName"]), tostring(InitiatedBy["user"]["userPrincipalName"])),
    InitiatorId = iif(isnotempty(InitiatedBy["app"]), tostring(InitiatedBy["app"]["servicePrincipalId"]), tostring(InitiatedBy["user"]["id"])),
    Delete_IPAddress = tostring(InitiatedBy[tostring(bag_keys(InitiatedBy)[0])]["ipAddress"])
| project Delete_TimeGenerated = TimeGenerated, Category, Identity, Initiator, Delete_IPAddress, OperationName, Result, ParsedDeletedUserPrincipalName, InitiatedBy, AdditionalDetails, TargetResources, InitiatorId, CorrelationId
| join kind=inner (
    SigninLogs
    | where TimeGenerated > ago(query_period)
    | where ResultType == 0
    | summarize take_any(*) by UserPrincipalName
    | extend ParsedUserPrincipalName = translate("@", "_", UserPrincipalName)
    | project SigninLogs_TimeGenerated = TimeGenerated, UserPrincipalName, UserDisplayName, ResultType, ResultDescription, IPAddress, LocationDetails, AppDisplayName, ResourceDisplayName, ClientAppUsed, UserAgent, DeviceDetail, UserId, UserType, OriginalRequestId, ParsedUserPrincipalName
    ) on $left.ParsedDeletedUserPrincipalName == $right.ParsedUserPrincipalName
| where SigninLogs_TimeGenerated > Delete_TimeGenerated
| project-away ParsedDeletedUserPrincipalName, ParsedUserPrincipalName
| extend
    AccountName = tostring(split(UserPrincipalName, "@")[0]),
    AccountUPNSuffix = tostring(split(UserPrincipalName, "@")[1])
Microsoft Sentinel Original KQL T1078.004 ↗
Suspicious Sign In by Entra ID Connect Sync Account
'This query looks for sign ins by the Microsoft Entra ID Connect Sync account to Azure where properties about the logon are anomalous. This query uses Microsoft Sentinel's UEBA features to detect these suspicious properties. A threat actor may attempt to steal the Sync account credentials and use them to access Azure resources. This alert should be reviewed to ensure that the log in came was from a legitimate source.'
Show query
BehaviorAnalytics
// User modification is expected from this account so focus on logons
| where ActivityType =~ "LogOn"
| where UserName startswith "Sync_" and UsersInsights.AccountDisplayName =~ "On-Premises Directory Synchronization Service Account"
// Filter out this expected activity
| where ActivityInsights.App !~ "Microsoft Azure Active Directory Connect"
| where InvestigationPriority > 0
| extend Name = split(UserPrincipalName, "@")[0], UPNSuffix = split(UserPrincipalName, "@")[1]
Microsoft Sentinel Original KQL T1078 ↗
Suspicious VM Instance Creation Activity Detected
'This detection identifies high-severity alerts across various Microsoft security products, including Microsoft Defender XDR and Microsoft Entra ID, and correlates them with instances of Google Cloud VM creation. It focuses on instances where VMs were created within a short timeframe of high-severity alerts, potentially indicating suspicious activity.'
Show query
// Filter alerts from specific Microsoft security products with medium and high severity
SecurityAlert 
| where ProductName in ("Microsoft 365 Defender", "Azure Active Directory", "Microsoft Defender Advanced Threat Protection", "Microsoft Cloud App Security", "Azure Active Directory Identity Protection", "Microsoft Defender ATP")
| where AlertSeverity has_any ("Medium", "High")
// Parse JSON entities and extend AlertTimeGenerated
| extend Entities = parse_json(Entities), AlertTimeGenerated=TimeGenerated
// Extract and process IP entities
| mv-apply Entity = Entities on 
    ( 
    where Entity.Type == 'ip' 
    | extend EntityIp = tostring(Entity.Address) 
    ) 
// Extract and process account entities
| mv-apply Entity = Entities on 
    ( 
    where Entity.Type == 'account' 
    | extend AccountObjectId = tostring(Entity.AadUserId)
    )
// Filter out records with empty EntityIp
| where isnotempty(EntityIp)
// Summarize data and create sets of entities and system alert IDs
| summarize Entitys=make_set(Entity), SystemAlertIds=make_set(SystemAlertId)
    by 
    AlertName,
    ProductName,
    AlertSeverity,
    EntityIp,
    Tactics,
    Techniques,
    ProviderName,
    AlertTime= bin(AlertTimeGenerated, 1d),
    AccountObjectId
// Join with GCPAuditLogs for VM instance creation
| join kind=inner (
    GCPAuditLogs
    | where ServiceName == "compute.googleapis.com" and MethodName endswith "instances.insert"
    | extend
        GCPUserUPN= tostring(parse_json(AuthenticationInfo).principalEmail),
        GCPUserIp = tostring(parse_json(RequestMetadata).callerIp),
        GCPUserUA= tostring(parse_json(RequestMetadata).callerSuppliedUserAgent),
        VMStatus =  tostring(parse_json(Response).status),
        VMOperation=tostring(parse_json(Response).operationType),
        VMName= tostring(parse_json(Request).name),
        VMType = tostring(split(parse_json(Request).machineType, "/")[-1])
    | where GCPUserUPN !has "gserviceaccount.com"
    | where VMOperation == "insert" and isnotempty(GCPUserIp) and GCPUserIp != "private"
    | project
        GCPOperationTime=TimeGenerated,
        VMName,
        VMStatus,
        MethodName,
        GCPUserUPN,
        ProjectId,
        GCPUserIp,
        GCPUserUA,
        VMOperation,
        VMType
    )
    on $left.EntityIp == $right.GCPUserIp 
// Join with IdentityInfo to enrich user identity details
| join kind=inner (IdentityInfo 
    | distinct AccountObjectId, AccountUPN, JobTitle
    )
    on AccountObjectId 
// Calculate the time difference between the alert and VM creation for further analysis
| extend TimeDiff= datetime_diff('day', AlertTime, GCPOperationTime),Name = split(GCPUserUPN, "@")[0], UPNSuffix = split(GCPUserUPN, "@")[1]
Microsoft Sentinel Original KQL T1078.004 ↗
Suspicious linking of existing user to external User
' This query will detect when an attempt is made to update an existing user and link it to an guest or external identity. These activities are unusual and such linking of external identities should be investigated. In some cases you may see internal Entra ID sync accounts (Sync_) do this which may be benign'
Show query
AuditLogs
| where OperationName=~ "Update user" 
| where Result =~ "success" 
| mv-expand TargetResources 
| mv-expand TargetResources.modifiedProperties 
| extend displayName = tostring(TargetResources_modifiedProperties.displayName), 
TargetUPN_oldValue = tostring(parse_json(tostring(TargetResources_modifiedProperties.oldValue))[0]), 
TargetUPN_newValue = tostring(parse_json(tostring(TargetResources_modifiedProperties.newValue))[0])
| where displayName == "UserPrincipalName" and TargetUPN_oldValue !has "#EXT" and TargetUPN_newValue has "#EXT"
| extend InitiatingAppName = tostring(InitiatedBy.app.displayName)
| extend InitiatingAppServicePrincipalId = tostring(InitiatedBy.app.servicePrincipalId)
| extend InitiatingUserPrincipalName = tostring(InitiatedBy.user.userPrincipalName)
| extend InitiatingAadUserId = tostring(InitiatedBy.user.id)
| extend InitiatingIPAddress = tostring(InitiatedBy.user.ipAddress)
| extend InitiatedBy = tostring(iff(isnotempty(InitiatingUserPrincipalName),InitiatingUserPrincipalName, InitiatingAppName))
| summarize arg_max(TimeGenerated, *) by CorrelationId
| project-reorder TimeGenerated, InitiatedBy, InitiatingAppName, InitiatingAppServicePrincipalId, InitiatingUserPrincipalName, InitiatingAadUserId, InitiatingIPAddress, TargetUPN_oldValue, TargetUPN_newValue
| extend InitiatingAccountName = tostring(split(InitiatingUserPrincipalName, "@")[0]), InitiatingAccountUPNSuffix = tostring(split(InitiatingUserPrincipalName, "@")[1])
| extend TargetAccountName = tostring(split(TargetUPN_oldValue, "@")[0]), TargetUPNSuffix = tostring(split(TargetUPN_oldValue, "@")[1])
Showing 251-300 of 1,106