Elastic medium stable kql
Potential DNS Rebinding from Public to Private Address
Identifies a client resolving the same public registered domain to both a public IP address and a private, loopback, link-local, unique-local IPv6, or shared address. This includes both address classes being observed at the same timestamp, and a public answer followed within five minutes by a private answer where the minimum TTL across all answer records in the private-answer events is 60 seconds or less. Either pattern is consistent with DNS rebinding that pivots browser or application trust to internal resources.
Detection Logic
from logs-network_traffic.dns-*, logs-zeek.dns-*, packetbeat-*
| where
(
data_stream.dataset in ("network_traffic.dns", "zeek.dns") or
event.dataset == "dns"
) and
dns.question.name is not null and
dns.question.registered_domain is not null and
dns.resolved_ip is not null and
TO_UPPER(dns.response_code) == "NOERROR" and
TO_UPPER(dns.question.type) in ("A", "AAAA")
| eval
Esql.client_ip = COALESCE(client.ip, source.ip),
Esql.dataset = COALESCE(data_stream.dataset, event.dataset)
| where Esql.client_ip is not null
| mv_expand dns.resolved_ip
| eval Esql.is_private = CIDR_MATCH(
dns.resolved_ip,
"0.0.0.0/32",
"10.0.0.0/8",
"100.64.0.0/10",
"127.0.0.0/8",
"169.254.0.0/16",
"172.16.0.0/12",
"192.168.0.0/16",
"::1/128",
"fc00::/7",
"fe80::/10"
)
| eval
Esql.private_time = CASE(Esql.is_private, @timestamp, null),
Esql.public_time = CASE(not Esql.is_private, @timestamp, null),
Esql.private_event_ttl = CASE(Esql.is_private, MV_MIN(dns.answers.ttl), null),
Esql.private_ip = CASE(Esql.is_private, dns.resolved_ip, null),
Esql.public_ip = CASE(not Esql.is_private, dns.resolved_ip, null)
| stats
Esql.resolved_ip_observation_count = COUNT(*),
Esql.resolved_ip_count = COUNT_DISTINCT(dns.resolved_ip),
Esql.first_public_answer = MIN(Esql.public_time),
Esql.first_private_answer = MIN(Esql.private_time),
Esql.min_private_event_ttl = MIN(Esql.private_event_ttl),
Esql.public_ips = MV_SLICE(VALUES(Esql.public_ip), 0, 100),
Esql.private_ips = MV_SLICE(VALUES(Esql.private_ip), 0, 100),
Esql.dataset_values = MV_SLICE(VALUES(Esql.dataset), 0, 10),
Esql.observer_name_values = MV_SLICE(VALUES(observer.name), 0, 20)
by Esql.client_ip, dns.question.name, dns.question.registered_domain
| eval
Esql.same_timestamp = Esql.first_public_answer == Esql.first_private_answer,
Esql.transition_seconds = DATE_DIFF("seconds", Esql.first_public_answer, Esql.first_private_answer)
| where
Esql.first_public_answer is not null and
Esql.first_private_answer is not null and
(
Esql.same_timestamp or
(
Esql.first_public_answer < Esql.first_private_answer and
Esql.min_private_event_ttl is not null and
Esql.min_private_event_ttl <= 60 and
Esql.transition_seconds <= 300
)
)
| keep Esql.*, dns.* False Positives
- ⚠ Split-horizon DNS, VPN transitions, service discovery, failover, hairpin NAT, and dual-stack names that publish a public A record with a unique-local AAAA record can legitimately produce public and private answers for the same name. Recursive resolvers, DNS forwarders, and localhost listeners can also aggregate many endpoints under one client address. Security products may sinkhole suspicious domains to loopback or private addresses with short TTLs. Confirm the domain, resolver placement, and client identity before adding an exception, and scope exceptions by registered domain or client rather than globally.
Field Validations
Loading…
Comments (0)
Loading comments...