Detection rules › Splunk

DNS Query Length With High Standard Deviation

Status
production
Severity
low
Group by
DNS.answer_count, DNS.query_count, DNS.vendor_product, dest_ip, dns.answers.name, dns.answers.type, dns.question.name, dns.response.code, src, src_ip
Author
Bhavin Patel, Splunk
Source
github.com/splunk/security_content

The following analytic identifies DNS queries with unusually large lengths by computing the standard deviation of query lengths and filtering those exceeding two times the standard deviation. It leverages DNS query data from the Network_Resolution data model, focusing on the length of the domain names being resolved. This activity is significant as unusually long DNS queries can indicate data exfiltration or command-and-control communication attempts. If confirmed malicious, this activity could allow attackers to stealthily transfer data or maintain persistent communication channels within the network.

MITRE ATT&CK coverage

Event coverage

ProviderEventTitle
SysmonEvent ID 22DNSEvent (DNS query)

Rule body splunk

name: DNS Query Length With High Standard Deviation
id: 1a67f15a-f4ff-4170-84e9-08cf6f75d6f5
version: 16
creation_date: '2019-10-16'
modification_date: '2026-05-13'
author: Bhavin Patel, Splunk
status: production
type: Anomaly
description: The following analytic identifies DNS queries with unusually large lengths by computing the standard deviation of query lengths and filtering those exceeding two times the standard deviation. It leverages DNS query data from the Network_Resolution data model, focusing on the length of the domain names being resolved. This activity is significant as unusually long DNS queries can indicate data exfiltration or command-and-control communication attempts. If confirmed malicious, this activity could allow attackers to stealthily transfer data or maintain persistent communication channels within the network.
data_source:
    - Sysmon EventID 22
search: |-
    | tstats `security_content_summariesonly` count min(_time) as firstTime max(_time) as lastTime FROM datamodel=Network_Resolution
      WHERE NOT DNS.record_type IN ("Pointer","PTR","SOA", "SRV") DNS.query != *.
      BY DNS.answer DNS.answer_count DNS.query
         DNS.query_count DNS.reply_code_id DNS.src
         DNS.vendor_product DNS.dest DNS.record_type
    | `drop_dm_object_name("DNS")`
    | `security_content_ctime(firstTime)`
    | `security_content_ctime(lastTime)`
    | eval tlds=split(query,".")
    | eval tld=mvindex(tlds,-1)
    | eval tld_len=len(tld)
    | search tld_len<=20
    | eval query_length = len(query)
    | table firstTime lastTime src dest query query_length record_type count record_type
    | eventstats stdev(query_length) AS stdev avg(query_length) AS avg p50(query_length) AS p50
    | where query_length>(avg+stdev*2)
    | eval z_score=(query_length-avg)/stdev
    | stats count values(query) as query values(dest) as dest avg(query_length) as avg_query_length values(record_type) as record_type min(firstTime) as firstTime latest(lastTime) as lastTime
      BY src
    | `dns_query_length_with_high_standard_deviation_filter`
how_to_implement: To successfully implement this search, you will need to ensure that DNS data is populating the Network_Resolution data model.
known_false_positives: It's possible there can be long domain names that are legitimate.
references: []
drilldown_searches:
    - name: View the detection results for - "$src$"
      search: '%original_detection_search% | search  src = "$src$"'
      earliest_offset: $info_min_time$
      latest_offset: $info_max_time$
    - name: View risk events for the last 7 days for - "$src$"
      search: '| from datamodel Risk.All_Risk | search normalized_risk_object IN ("$host$") | stats count min(_time) as firstTime max(_time) as lastTime values(search_name) as "Search Name" values(risk_message) as "Risk Message" values(analyticstories) as "Analytic Stories" values(annotations._all) as "Annotations" values(annotations.mitre_attack.mitre_tactic) as "ATT&CK Tactics" by normalized_risk_object | `security_content_ctime(firstTime)` | `security_content_ctime(lastTime)`'
      earliest_offset: 7d
      latest_offset: "0"
intermediate_findings:
    entities:
        - field: src
          type: system
          score: 20
          message: Potentially suspicious DNS query [$query$] with high standard deviation from src - [$src$]
threat_objects:
    - field: query
      type: url
analytic_story:
    - Hidden Cobra Malware
    - Suspicious DNS Traffic
    - Command And Control
asset_type: Endpoint
mitre_attack_id:
    - T1048.003
product:
    - Splunk Enterprise
    - Splunk Enterprise Security
    - Splunk Cloud
category: network
security_domain: network
tests:
    - name: True Positive Test
      attack_data:
        - data: https://media.githubusercontent.com/media/splunk/attack_data/master/datasets/attack_techniques/T1071.004/long_dns_query/dns-sysmon.log
          source: XmlWinEventLog:Microsoft-Windows-Sysmon/Operational
          sourcetype: XmlWinEventLog
      test_type: unit

Stages and Predicates

Stage 1: tstats

| tstats `security_content_summariesonly` count min(_time) as firstTime max(_time) as lastTime FROM datamodel=Network_Resolution
  WHERE NOT DNS.record_type IN ("Pointer","PTR","SOA", "SRV") DNS.query != *.
  BY DNS.answer DNS.answer_count DNS.query
     DNS.query_count DNS.reply_code_id DNS.src
     DNS.vendor_product DNS.dest DNS.record_type

Stage 2: search

| `drop_dm_object_name("DNS")`

Stage 3: search

| `security_content_ctime(firstTime)`

Stage 4: search

| `security_content_ctime(lastTime)`

Stage 5: eval

| eval tlds=split(query,".")

Stage 6: eval

| eval tld=mvindex(tlds,-1)

Stage 7: eval

| eval tld_len=len(tld)

Stage 8: search

| search tld_len<=20

Stage 9: eval

| eval query_length = len(query)

Stage 10: table

| table firstTime lastTime src dest query query_length record_type count record_type

Stage 11: eventstats

| eventstats stdev(query_length) AS stdev avg(query_length) AS avg p50(query_length) AS p50

Stage 12: where

| where query_length>(avg+stdev*2)

Stage 13: eval

| eval z_score=(query_length-avg)/stdev

Stage 14: stats

| stats count values(query) as query values(dest) as dest avg(query_length) as avg_query_length values(record_type) as record_type min(firstTime) as firstTime latest(lastTime) as lastTime
  BY src

Stage 15: search

| `dns_query_length_with_high_standard_deviation_filter`

Exclusions

Top-level NOT(...) conjuncts: predicates this rule actively suppresses.

FieldKindExcluded values
DNS.record_typein"PTR", "Pointer", "SOA", "SRV"

Indicators

Each row is a field, operator, and value that the rule matches. The corpus column counts how many other rules in the catalog look for the same combination: high numbers point to widely-used, community-vetted indicators. Blank or 1 shows that the indicator is specific to this rule.

FieldKindValues
DNS.queryne
  • *.
tld_lenle
  • 20