EncodedDomainURL [Nobelium]


Description

Looks for a logon domain in the Microsoft Entra ID logs, encoded with the same DGA encoding used in the Nobelium campaign. See Important steps for customers to protect themselves from recent nation-state cyberattacks for more on the Nobelium campaign (formerly known as Solorigate). This query is inspired by an Azure Sentinel detection. References: https://blogs.microsoft.com/on-the-issues/2020/12/13/customers-protect-nation-state-cyberattacks/ https://raw.githubusercontent.com/Azure/Azure-Sentinel/master/Hunting%20Queries/DnsEvents/Solorigate-Encoded-Domain-URL.yaml

Query · kql

let timeFrame = ago(1d);
let relevantDeviceNetworkEvents = 
  DeviceNetworkEvents
  | where Timestamp >= timeFrame
  | where RemoteUrl !has "\\" and RemoteUrl !has "/"
  | project-rename DomainName = RemoteUrl
  | summarize by DomainName;
let relevantDeviceEvents =
  DeviceEvents
  | where Timestamp >= timeFrame
  | where ActionType == "DnsQueryResponse"
  | extend query = extractjson("$.DnsQueryString", AdditionalFields)  
  | where isnotempty(query)
  | project-rename DomainName = query
  | summarize by DomainName;
let relevantIdentityQueryEvents =
  IdentityQueryEvents 
  | where Timestamp >= timeFrame
  | where ActionType == "DNS query"
  | where Protocol == "Dns"
  | project-rename DomainName = QueryTarget
  | summarize by DomainName;
let DnsEvents =
  relevantIdentityQueryEvents
  | union
  relevantDeviceNetworkEvents  
  | union
  relevantDeviceEvents
  | summarize by DomainName;
let dictionary = dynamic(["r","q","3","g","s","a","l","t","6","u","1","i","y","f","z","o","p","5","7","2","d","4","9","b","n","x","8","c","v","m","k","e","w","h","j"]);
let regex_bad_domains =
   EntraIdSignInEvents
   //Collect domains from tenant from signin logs
   | where Timestamp >= timeFrame
   | extend domain = tostring(split(AccountUpn, "@", 1)[0])
   | where domain != ""
   | summarize by domain
   | extend split_domain = split(domain, ".")
   //This cuts back on domains such as na.contoso.com by electing not to match on the "na" portion
   | extend target_string = iff(strlen(split_domain[0]) <= 2, split_domain[1], split_domain[0])
   | extend target_string = split(target_string, "-")  | mv-expand target_string
   //Rip all of the alphanumeric out of the domain name
   | extend string_chars = extract_all(@"([a-z0-9])", tostring(target_string))
   //Guid for tracking our data
   | extend guid = new_guid()//Expand to get all of the individual chars from the domain
   | mv-expand string_chars
   | extend chars = tostring(string_chars)
   //Conduct computation to encode the domain as per actor spec
   | extend computed_char = array_index_of(dictionary, chars)
   | extend computed_char = dictionary[(computed_char + 4) % array_length(dictionary)] 
   | summarize make_list(computed_char) by guid, domain
   | extend target_encoded = tostring(strcat_array(list_computed_char, ""))
   //These are probably too small, but can be edited (expect FP's when going too small)
   | where strlen(target_encoded) > 5
   | distinct target_encoded
   | summarize make_set(target_encoded)
   //Key to join to DNS
   | extend key = 1;
DnsEvents
  | extend key = 1
  //For each DNS query join the malicious domain list
  | join kind=inner (
      regex_bad_domains
  ) on key
  | project-away key
  //Expand each malicious key for each DNS query observed
  | mv-expand set_target_encoded
  //IndexOf allows us to fuzzy match on the substring
  | extend match = indexof(DomainName, set_target_encoded)
  | where match > -1
Raw source EncodedDomainURL [Nobelium] · KQL
Esc
Published by Azure/Azure-Sentinel ↗, licensed under MIT ↗. Reproduced here unmodified.
id: c561bf69-6a6c-4d0a-960a-b69e0e7c8f51
name: EncodedDomainURL [Nobelium]
description: |
  Looks for a logon domain in the Microsoft Entra ID logs,  encoded with the same DGA encoding used in the Nobelium campaign.
  See Important steps for customers to protect themselves from recent nation-state cyberattacks for more on the Nobelium campaign (formerly known as Solorigate).
  This query is inspired by an Azure Sentinel detection.
  References:
  https://blogs.microsoft.com/on-the-issues/2020/12/13/customers-protect-nation-state-cyberattacks/
  https://raw.githubusercontent.com/Azure/Azure-Sentinel/master/Hunting%20Queries/DnsEvents/Solorigate-Encoded-Domain-URL.yaml
requiredDataConnectors:
- connectorId: MicrosoftThreatProtection
  dataTypes:
  - DeviceNetworkEvents
  - DeviceEvents
  - IdentityQueryEvents
  - EntraIdSignInEvents
tactics:
- Command and control
tags:
- Nobelium
query: |
  let timeFrame = ago(1d);
  let relevantDeviceNetworkEvents = 
    DeviceNetworkEvents
    | where Timestamp >= timeFrame
    | where RemoteUrl !has "\\" and RemoteUrl !has "/"
    | project-rename DomainName = RemoteUrl
    | summarize by DomainName;
  let relevantDeviceEvents =
    DeviceEvents
    | where Timestamp >= timeFrame
    | where ActionType == "DnsQueryResponse"
    | extend query = extractjson("$.DnsQueryString", AdditionalFields)  
    | where isnotempty(query)
    | project-rename DomainName = query
    | summarize by DomainName;
  let relevantIdentityQueryEvents =
    IdentityQueryEvents 
    | where Timestamp >= timeFrame
    | where ActionType == "DNS query"
    | where Protocol == "Dns"
    | project-rename DomainName = QueryTarget
    | summarize by DomainName;
  let DnsEvents =
    relevantIdentityQueryEvents
    | union
    relevantDeviceNetworkEvents  
    | union
    relevantDeviceEvents
    | summarize by DomainName;
  let dictionary = dynamic(["r","q","3","g","s","a","l","t","6","u","1","i","y","f","z","o","p","5","7","2","d","4","9","b","n","x","8","c","v","m","k","e","w","h","j"]);
  let regex_bad_domains =
     EntraIdSignInEvents
     //Collect domains from tenant from signin logs
     | where Timestamp >= timeFrame
     | extend domain = tostring(split(AccountUpn, "@", 1)[0])
     | where domain != ""
     | summarize by domain
     | extend split_domain = split(domain, ".")
     //This cuts back on domains such as na.contoso.com by electing not to match on the "na" portion
     | extend target_string = iff(strlen(split_domain[0]) <= 2, split_domain[1], split_domain[0])
     | extend target_string = split(target_string, "-")  | mv-expand target_string
     //Rip all of the alphanumeric out of the domain name
     | extend string_chars = extract_all(@"([a-z0-9])", tostring(target_string))
     //Guid for tracking our data
     | extend guid = new_guid()//Expand to get all of the individual chars from the domain
     | mv-expand string_chars
     | extend chars = tostring(string_chars)
     //Conduct computation to encode the domain as per actor spec
     | extend computed_char = array_index_of(dictionary, chars)
     | extend computed_char = dictionary[(computed_char + 4) % array_length(dictionary)] 
     | summarize make_list(computed_char) by guid, domain
     | extend target_encoded = tostring(strcat_array(list_computed_char, ""))
     //These are probably too small, but can be edited (expect FP's when going too small)
     | where strlen(target_encoded) > 5
     | distinct target_encoded
     | summarize make_set(target_encoded)
     //Key to join to DNS
     | extend key = 1;
  DnsEvents
    | extend key = 1
    //For each DNS query join the malicious domain list
    | join kind=inner (
        regex_bad_domains
    ) on key
    | project-away key
    //Expand each malicious key for each DNS query observed
    | mv-expand set_target_encoded
    //IndexOf allows us to fuzzy match on the substring
    | extend match = indexof(DomainName, set_target_encoded)
    | where match > -1

Detection rules belong to the projects that publish them and remain under their own licenses. This site indexes and links to them; it claims no rights in them.