Potential DNS Tunneling via Long and Unique Subdomains

edit
IMPORTANT: This documentation is no longer updated. Refer to Elastic's version policy and the latest documentation.

Potential DNS Tunneling via Long and Unique Subdomains

edit

Identifies a client generating many unique, unusually long DNS query names to the same registered domain within a five-minute window. Malware DNS tunnels and DNS command-and-control commonly encode data in lengthy subdomain portions under one apex domain.

Rule type: esql

Rule indices: None

Severity: medium

Risk score: 47

Runs every: 5m

Searches indices from: now-9m (Date Math format, see also Additional look-back time)

Maximum alerts per execution: 100

References:

Tags:

  • Domain: Network
  • Use Case: Threat Detection
  • Use Case: Network Security Monitoring
  • Rule Type: ESQL
  • Tactic: Command and Control
  • Tactic: Exfiltration
  • Data Source: Network Packet Capture
  • Data Source: Fortinet
  • Data Source: Network Traffic
  • Data Source: Zeek
  • Resources: Investigation Guide

Version: 1

Rule authors:

  • Elastic

Rule license: Elastic License v2

Investigation guide

edit

Triage and analysis

Investigating Potential DNS Tunneling via Long and Unique Subdomains

DNS tunneling encodes data in query labels and often produces many unique, unusually long subdomains under a single apex domain. This rule aggregates network DNS telemetry for that behavioral pattern without relying on threat intelligence feeds or machine learning jobs.

Compare overlapping apex domains against the machine learning DNS Tunneling rule when that job is enabled.

Possible investigation steps

  • Review Esql.dns_registered_domain, Esql.count_distinct_names, Esql.unique_name_ratio, Esql.max_subdomain_length, and sample values in Esql.sample_names.
  • Inspect Esql.dns_question_type_values. TXT, NULL, CNAME, or MX bursts increase confidence; A/AAAA-only activity can still be tunneling and should not be dismissed on type alone.
  • Use Esql.first_seen, Esql.last_seen, Esql.dataset_values, and Esql.observer_name_values to establish the event span and identify the integrations and sensors that contributed to the alert.
  • Confirm whether Esql.client_ip is a workstation, server, recursive resolver, forwarder, NAT address, or localhost DNS service. Resolver and localhost sources merge many clients and are a common false-positive pattern.
  • Review Esql.destination_ip_values to identify the resolver or authoritative destination observed by the sensor.
  • Pivot on the same client and apex domain in raw DNS events and look for follow-on process, file, or additional C2 activity.

False positive analysis

  • CDN, cloud load-balancer, certificate, and software-update services often create long hostnames. Confirm the apex domain reputation and whether the requesting host role normally uses that provider.
  • Security or network appliances performing DNS-based reachability or reputation checks can resemble tunneling. Exclude confirmed appliance addresses after validation.
  • Do not create a global resolver exception until the originating endpoint is known; a shared Esql.client_ip can hide a single infected host behind legitimate bulk lookups.

Response and remediation

  • Block the apex domain or forwarding from the affected host at recursive resolvers if malicious activity is confirmed.
  • Isolate the source host and inspect for tunneling tools or malware initiating the queries.
  • Add temporary blocks for the apex domain while scoping additional hosts querying the same name.

Setup

edit

Setup

This rule requires DNS transaction events from one of the following sources:

  • Elastic Network Packet Capture (network_traffic.dns) in logs-network_traffic.dns-*
  • Fortinet FortiGate DNS logs (fortinet_fortigate.log) in logs-fortinet_fortigate.log-*
  • Elastic Zeek (zeek.dns) in logs-zeek.dns-*
  • Legacy Packetbeat DNS events in packetbeat-* with event.dataset set to dns

Place the sensor where it observes endpoint-to-resolver DNS traffic. If the sensor is upstream of a recursive resolver, or if the captured client is a localhost listener such as 127.0.0.1, Esql.client_ip may identify shared DNS infrastructure instead of the originating endpoint.

DNS-over-HTTPS (DoH), DNS-over-TLS (DoT), and other encrypted DNS traffic are not visible to packet capture unless the sensor receives decrypted DNS telemetry or equivalent resolver logs mapped to ECS.

Rule query

edit
from logs-network_traffic.dns-*, logs-fortinet_fortigate.log-*, logs-zeek.dns-*, packetbeat-*
| where
    (
      data_stream.dataset in ("network_traffic.dns", "fortinet_fortigate.log", "zeek.dns")
      or event.dataset == "dns"
    )
    and dns.question.name is not null
    and dns.question.registered_domain is not null
| eval
    Esql.client_ip = COALESCE(client.ip, source.ip),
    Esql.dataset = COALESCE(data_stream.dataset, event.dataset),
    Esql.dns_question_name = TO_LOWER(dns.question.name),
    Esql.dns_registered_domain = TO_LOWER(dns.question.registered_domain),
    Esql.dns_question_type = TO_LOWER(dns.question.type),
    Esql.subdomain_length = LENGTH(Esql.dns_question_name) - LENGTH(Esql.dns_registered_domain) - 1
| where
    Esql.client_ip is not null
    and Esql.subdomain_length >= 50
    and (Esql.dns_question_type is null or Esql.dns_question_type != "ptr")
    and not ENDS_WITH(Esql.dns_question_name, ".arpa")
| eval Esql.time_window = DATE_TRUNC(5 minutes, @timestamp)
| stats
    Esql.count_queries = COUNT(*),
    Esql.count_distinct_names = COUNT_DISTINCT(Esql.dns_question_name),
    Esql.max_subdomain_length = MAX(Esql.subdomain_length),
    Esql.avg_subdomain_length = AVG(Esql.subdomain_length),
    Esql.dns_question_type_values = MV_SLICE(VALUES(Esql.dns_question_type), 0, 9),
    Esql.destination_ip_values = MV_SLICE(VALUES(destination.ip), 0, 4),
    Esql.sample_names = MV_SLICE(VALUES(Esql.dns_question_name), 0, 4),
    Esql.dataset_values = MV_SLICE(VALUES(Esql.dataset), 0, 9),
    Esql.observer_name_values = MV_SLICE(VALUES(observer.name), 0, 19),
    Esql.first_seen = MIN(@timestamp),
    Esql.last_seen = MAX(@timestamp)
  by Esql.time_window, Esql.client_ip, Esql.dns_registered_domain
| where Esql.count_queries >= 25 and Esql.count_distinct_names >= 15
| eval Esql.unique_name_ratio = TO_DOUBLE(Esql.count_distinct_names) / Esql.count_queries
| keep Esql.*

Framework: MITRE ATT&CKTM