elastic / elastic/detection-rules
[New Rule] DNS Tunneling via NULL or Long TXT Queries
- Dominant language
- Python
- Stars
- 2.7k
- Forks
- 696
- Avg merge
- 5d 1h
- Merged PRs (30d)
- 72
Description
**Description**
Detects DNS query-shape patterns characteristic of DNS tunneling tools (e.g. iodine, dnscat2): `NULL`-type DNS queries, which are essentially unused in legitimate modern DNS traffic, or `TXT`-type queries with an unusually long question name.
Non-ML, query-shape-based DNS tunneling detection is not currently covered by any active rule in this repo. The one prior plain rule (`command_and_control_dns_directly_to_the_internet.toml`) was deprecated in 2022 with no non-ML replacement, and current coverage (`dga`/`ded`/`beaconing`) requires a paid ML license and trained models.
Built and validated against a real DNS tunnel, not synthetic packets: ran real Packetbeat (the actual Elastic Beat, not a hand-built approximation) against this host's live traffic with its DNS module, then set up a genuine `iodine` DNS tunnel (server + client). First attempt silently fell back to raw UDP once iodine detected it could reach the server directly, which was useless for this rule since it bypasses DNS entirely; caught by reading the client's own log output and fixed by forcing pure DNS-wire tunneling (`-r`). Ran two separate tunnel sessions carrying real `ping`-generated payload data, one at iodine's default `NULL` query type and one forced to `TXT` (`-T TXT`), alongside real benign DNS traffic (external site lookups, ambient system queries) as the false-positive baseline.
Real captured data directly shaped the design: 75 real `NULL`-type queries captured from the tunnel (23-759 characters), zero in an 84-query benign baseline (max 71 characters), confirming `NULL` records are essentially unused in legitimate traffic on their own. 58 real `TXT`-type queries were also captured, but not uniformly long: iodine's own short control/keepalive packets were as short as 24 characters, and only data-carrying packets ran long (up to 588 characters). This is why the `TXT` branch requires the question *name* itself to exceed 100 characters rather than matching on record type alone (TXT is common and legitimate for SPF/DKIM/domain verification). Verified the exact final query against the real captured data: 102/102 correct matches (75 `NULL` + 27 long `TXT`), 0 false positives against 84 benign queries and 31 short `TXT` control packets.
**Target Ruleset**: network
**Target Rule Type**: ES|QL
**Tested ECS Version**: 9.4.0
**Query**
```sql
FROM logs-network_traffic.dns-* metadata _id, _version, _index
| EVAL Esql.name_length = LENGTH(dns.question.name)
| EVAL Esql.is_null_type = dns.question.type == "NULL"
| EVAL Esql.is_long_txt = dns.question.type == "TXT" AND Esql.name_length > 100
| WHERE Esql.is_null_type OR Esql.is_long_txt
| KEEP _id, _version, _index, @timestamp, dns.question.name, dns.question.type,
Esql.name_length, source.ip, destination.ip
```
**New fields required in ECS/data sources for this rule?**
None. Uses only fields already populated by the standard `network_traffic` integration's DNS dataset (`dns.question.name`, `dns.question.type`, `source.ip`, `destination.ip`).
**Related issues or PRs**
None found. Related to the deprecated `command_and_control_dns_directly_to_the_internet.toml` (deprecated 2022, no replacement). This rule provides a non-ML alternative to the current `dga`/`ded`/`beaconing` ML-only coverage.
**References**
- https://code.kryo.se/iodine/
- https://github.com/iagox86/dnscat2
- https://attack.mitre.org/techniques/T1071/004/
- https://attack.mitre.org/techniques/T1048/003/
**Redacted Example Data**
Real DNS event captured from a genuine local `iodine` tunnel during testing (a data-carrying tunnel query, not the longest captured example, chosen for readability):
```json
{"dns": {"question": {"name": "z3miaA0123456789\\188\\189\\190\\191\\192\\193\\194\\195\\196\\197\\198\\199\\200\\201\\202\\203\\204\\205\\206\\207.t.detlab.internal", "type": "NULL"}, "response_code": "NOERROR"}, "source": {"ip": "127.0.0.1"}, "destination": {"ip": "127.0.0.1"}}
```
Contributor guide
Assessment
This issue has not been assessed yet.