← Back to SOC feed Coverage →

EncodedDomainURL [Nobelium]

kql MEDIUM Azure-Sentinel
DeviceEventsDeviceNetworkEventsDnsEventsIdentityQueryEvents
aptbackdoorhuntingmicrosoftofficial
This rule was pulled from an open-source repository and enriched with AI. Validate in a test environment before deploying to production.
View original rule at Azure-Sentinel →
Retrieved: 2026-05-07T23:00:00Z · Confidence: medium

Hunt Hypothesis

EncodedDomainURL detects potential adversary activity by identifying suspiciously encoded domain URLs in Microsoft Entra ID logs, which may be used for command and control or exfiltration in a Nobelium-like campaign. SOC teams should proactively hunt for this behavior in Azure Sentinel to identify early-stage compromise and prevent lateral movement or data exfiltration.

KQL 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 =
   AADSignInEventsBeta
   //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

Analytic Rule Definition

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
  - AADSignInEventsBeta
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 =
     AADSignInEventsBeta
     //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 

Required Data Sources

Sentinel TableNotes
DeviceEventsEnsure this data connector is enabled
DeviceNetworkEventsEnsure this data connector is enabled
DnsEventsEnsure this data connector is enabled
IdentityQueryEventsEnsure this data connector is enabled

MITRE ATT&CK Context

References

False Positive Guidance

Original source: https://github.com/Azure/Azure-Sentinel/blob/main/Hunting Queries/Microsoft 365 Defender/Command and Control/EncodedDomainURL [Nobelium].yaml