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
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
| Sentinel Table | Notes |
|---|---|
DeviceEvents | Ensure this data connector is enabled |
DeviceNetworkEvents | Ensure this data connector is enabled |
DnsEvents | Ensure this data connector is enabled |
IdentityQueryEvents | Ensure this data connector is enabled |
Scenario: Scheduled Job Using Encoded Domain for Internal Monitoring
Description: A scheduled job (e.g., PowerShell.exe or Task Scheduler) uses an encoded domain as part of a legitimate monitoring tool (e.g., Splunk, ELK Stack, or Graylog) to communicate with an internal logging server.
Filter/Exclusion: Exclude processes associated with internal monitoring tools (e.g., splunkforwarder.exe, logstash.exe, graylog2-server.exe) or check for internal IP ranges in the domain resolution.
Scenario: Admin Task Using Base64 Encoding for Secure Communication
Description: An admin task (e.g., certutil.exe, base64.exe, or PowerShell script) encodes a domain using Base64 as part of a secure communication protocol (e.g., for API calls or encrypted payloads).
Filter/Exclusion: Exclude processes initiated by admin accounts (e.g., Administrator, Domain Admins) or check for known secure communication tools (e.g., OpenSSL, curl, Postman).
Scenario: Legacy Tool Using Base64 for Data Encoding
Description: A legacy tool (e.g., SQL Server, Exchange Online, or PowerShell scripts) uses Base64 encoding to encode data (e.g., for secure data transfer or encryption) and includes a domain in the encoded payload.
Filter/Exclusion: Exclude processes related to legacy systems (e.g., sqlservr.exe, exchange.exe, powershell.exe) or check for known encoding patterns used in internal data transfer.
Scenario: Internal DNS Server Using Encoded Domains for Internal Services
Description: An internal DNS server (e.g., Windows Server DNS, Bind9, or PowerDNS) resolves