Featured image of post Threat Hunting: Network-Based Techniques

Threat Hunting: Network-Based Techniques

Network traffic analysis techniques for hunting C2 beaconing, DNS tunnelling, data exfiltration, and anomalous lateral movement using SIEM queries and flow data.

Overview

Endpoint logs tell you what ran. Network logs tell you where it called home. Many attackers — especially APT groups — operate with minimal endpoint footprint but leave distinctive patterns in network traffic: regular beaconing intervals, unusual DNS queries, encrypted tunnels to unexpected destinations.

This page documents network-based hunting techniques that complement endpoint hunting, covering:

  • C2 beaconing detection
  • DNS tunnelling and exfiltration
  • Abnormal outbound traffic patterns
  • Encrypted channel abuse (T1573)
  • Lateral movement via SMB and WMI

Hunt 1: C2 Beaconing Detection (T1071, T1132)

Hypothesis

A compromised host is communicating with a C2 server on a regular interval. Most C2 frameworks (Cobalt Strike, Metasploit, custom implants) beacon home every few seconds to minutes with a consistent timing pattern.

What Beaconing Looks Like

1
2
3
4
10:00:00  host → 185.220.101.47:443  (64 bytes)
10:00:30  host → 185.220.101.47:443  (64 bytes)
10:01:00  host → 185.220.101.47:443  (64 bytes)
10:01:30  host → 185.220.101.47:443  (64 bytes)

Consistent size + consistent interval = strong beaconing indicator.

Query — Splunk (flow data / NetFlow / firewall logs)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
index=firewall action=allowed
| bucket _time span=1h
| stats count, stdev(bytes_out) as byte_variance,
         stdev(relative_time(_time,"@s")) as time_variance
  by src_ip, dest_ip, dest_port
| where count > 50
| where time_variance < 10       /* low jitter = regular timing */
| where byte_variance < 50       /* consistent payload size */
| where NOT (dest_ip IN (known_good_ips))
| sort -count

Query — KQL (Microsoft Sentinel / Defender for Endpoint)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
DeviceNetworkEvents
| where ActionType == "ConnectionSuccess"
| where not (RemoteIP startswith "10." or RemoteIP startswith "192.168." or RemoteIP startswith "172.")
| summarize
    ConnectionCount = count(),
    ByteStdDev = stdev(tolong(SentBytes)),
    Intervals = make_list(bin(Timestamp, 1m))
  by DeviceName, RemoteIP, RemotePort
| where ConnectionCount > 30
| where ByteStdDev < 100
| sort by ConnectionCount desc

Jitter Analysis

Advanced C2 frameworks add jitter (random timing variation) to avoid simple interval detection. Hunt for these with a statistical approach:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
import numpy as np
from scipy import stats

def detect_beaconing(timestamps, threshold_cv=0.3):
    """
    Coefficient of Variation (CV) < 0.3 indicates
    low variance relative to the mean — consistent beaconing.
    """
    if len(timestamps) < 10:
        return False, None

    intervals = np.diff(sorted(timestamps))
    mean_interval = np.mean(intervals)
    std_interval  = np.std(intervals)
    cv = std_interval / mean_interval if mean_interval > 0 else 1

    is_beaconing = cv < threshold_cv
    return is_beaconing, {
        'mean_interval_seconds': round(mean_interval, 2),
        'cv': round(cv, 4),
        'sample_count': len(intervals)
    }

Hunt 2: DNS Tunnelling (T1071.004, T1048.003)

Hypothesis

An attacker is encoding data in DNS queries to exfiltrate information or tunnel C2 traffic through DNS, bypassing firewall controls that allow outbound DNS.

What DNS Tunnelling Looks Like

Normal DNS queries are short:

1
2
query: google.com → A record
query: api.github.com → A record

Tunnelled DNS queries are long and often Base32/Base64 encoded:

1
2
3
query: aGVsbG8gd29ybGQ.evil-domain.com
query: dGhpcyBpcyBhIHRlc3Q.evil-domain.com
query: c2Vuc2l0aXZlIGRhdGE.evil-domain.com

Query — Splunk (DNS logs)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
index=dns
| eval query_length=len(query)
| eval subdomain_labels=mvcount(split(query,"."))
| where query_length > 50 OR subdomain_labels > 5
| stats count, avg(query_length) as avg_query_len,
         dc(query) as unique_queries
  by src_ip, domain
| where unique_queries > 20
| where avg_query_len > 40
| sort -unique_queries

Entropy Analysis — High Entropy = Encoded Data

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import math
from collections import Counter

def calculate_entropy(text):
    """Shannon entropy — encoded/random strings score > 3.5"""
    if not text:
        return 0
    freq = Counter(text)
    length = len(text)
    return -sum((count/length) * math.log2(count/length)
                for count in freq.values())

def is_suspicious_dns(query, entropy_threshold=3.5, length_threshold=40):
    subdomain = query.split('.')[0]
    entropy = calculate_entropy(subdomain)
    return {
        'query':     query,
        'entropy':   round(entropy, 3),
        'length':    len(subdomain),
        'suspicious': entropy > entropy_threshold and len(subdomain) > length_threshold
    }

# Example output:
# {'query': 'aGVsbG8gd29ybGQ.evil.com', 'entropy': 4.12, 'length': 24, 'suspicious': True}
# {'query': 'google.com', 'entropy': 2.58, 'length': 6, 'suspicious': False}

Additional Signals

  • Single domain receiving hundreds of unique subdomains from one host
  • DNS queries to domains registered < 30 days ago (correlate with WHOIS)
  • DNS over non-standard ports (port 5353, 8053, 53/UDP to unexpected IPs)
  • TXT record queries — commonly abused for C2 data delivery

Hunt 3: Data Exfiltration via HTTPS (T1048.002)

Hypothesis

An attacker is exfiltrating data over HTTPS to a cloud storage service or attacker-controlled server, blending in with normal web traffic.

Volume Anomaly Detection

1
2
3
4
5
6
7
8
9
index=proxy OR index=firewall
| where dest_port=443
| bucket _time span=1d
| stats sum(bytes_out) as daily_upload by src_ip, dest_domain
| eventstats avg(daily_upload) as avg_upload, stdev(daily_upload) as std_upload
  by src_ip, dest_domain
| where daily_upload > (avg_upload + (3 * std_upload))
| where daily_upload > 50000000   /* > 50MB in a day */
| sort -daily_upload

Suspicious Upload Destinations

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
index=proxy
| where bytes_out > 10000000   /* > 10MB per session */
| where NOT (dest_domain IN (
    "*.microsoft.com", "*.office365.com", "*.sharepoint.com",
    "*.google.com", "*.dropbox.com", "*.box.com"
  ))
| stats sum(bytes_out) as total_uploaded, count as sessions
  by src_ip, dest_domain
| where sessions > 3
| sort -total_uploaded

Long Connection Duration

Short HTTPS connections are normal. Hours-long connections to unknown IPs are not.

1
2
3
4
5
6
index=firewall action=allowed dest_port=443
| eval duration_minutes=duration/60
| where duration_minutes > 60
| where NOT (dest_ip IN (known_cdn_ips))
| table _time, src_ip, dest_ip, dest_port, bytes_out, duration_minutes
| sort -duration_minutes

Hunt 4: Lateral Movement via SMB (T1021.002)

Hypothesis

An attacker with valid credentials is spreading through the network using SMB, accessing admin shares (C$, ADMIN$, IPC$) on multiple hosts.

Query — Splunk

1
2
3
4
5
6
index=wineventlog EventCode=5140
ShareName IN ("\\\\*\\C$", "\\\\*\\ADMIN$", "\\\\*\\IPC$")
| stats dc(ComputerName) as targets_accessed, values(ComputerName) as targets
  by SubjectUserName, IpAddress
| where targets_accessed > 3
| sort -targets_accessed

Correlate with Login Failures

A sweep preceded by login failures indicates credential bruteforcing before successful lateral movement:

1
2
3
4
5
6
index=wineventlog EventCode IN (4625, 5140)
| eval event_type=if(EventCode==4625, "failed_login", "smb_access")
| stats values(event_type) as events, dc(ComputerName) as targets
  by SubjectUserName, IpAddress
| where (events="failed_login" AND events="smb_access")
| sort -targets

Hunt 5: Anomalous DNS Resolutions Before Network Connections

Hypothesis

Malware often resolves a C2 domain immediately before making a connection. Correlating DNS queries with subsequent connections reveals implants that use DGA (Domain Generation Algorithms) or freshly registered domains.

1
2
3
4
5
6
7
8
9
index=dns
| join type=inner src_ip
  [search index=firewall action=allowed
   | rename dest_ip as resolved_ip]
| where dns_query_time < connection_time
| where (connection_time - dns_query_time) < 5
| eval domain_age=now() - domain_registration_date
| where domain_age < 2592000   /* Domain registered < 30 days ago */
| table _time, src_ip, query, resolved_ip, dest_port

Building a Network Hunt Programme

Frequency Hunt Type Data Source
Daily Beaconing anomalies (new destinations) Firewall / NDR
Daily DNS entropy spike DNS resolver logs
Weekly Large outbound transfers Proxy / firewall
Weekly SMB lateral movement sweep Windows Security Events
Monthly Full port scan of internal east-west traffic NetFlow

Contact me at contact@malsayegh.ae to discuss building a network hunting programme for your environment.

comments powered by Disqus
All rights Reserved for malsayegh.ae
Built with Hugo
Theme Stack designed by Jimmy