6 min read

Threat Hunting in the Homelab

When Zenarmor flagged a high-severity beacon, I feared my NAS was infected. It wasn't. Here is the step-by-step threat hunt of how I tracked down a malware implant inside a compromised Docker container using tcpdump—and the supply chain mistake that let it in.
Digital illustration of a server room. Glowing blue network paths converge on an isolated, glowing red cube. A text overlay reads "TRAP SPRUNG: PID ISOLATED," symbolizing threat containment.
Gotcha. Visualizing the moment the hunt moved from passive monitoring to active identification. By using tcpdump on the docker bridge network, we trapped the specific container IP responsible for the beacons, proving it was an isolated workload and not the host OS.

Like many homelabbers, I rely on automated tools to keep my media stack running. But earlier this week, the cybersecurity world started buzzing about VoidLink, a new malware framework targeting Linux and Docker environments.

Naturally, I checked my firewall logs. I didn't expect to find anything—until Zenarmor on my OPNsense firewall flagged a high-severity alert.

Whether it was specifically VoidLink or another advanced threat, I found an active malicious beacon inside my network! What followed was a 4-hour threat hunt to isolate a "Patient Zero" that was actively hiding its traffic...

Here is a step-by-step guide on how I tracked down a compromised container using tcpdump and forensic logic, and how you can root out similar evil in your own self-hosted environment.


The Symptoms: Anomalies on the Wire

The first sign of trouble wasn't a slow server or high CPU; it was a blocked egress connection.

  • The C2 Beacon: Zenarmor blocked an outbound connection to a 216.21.13.x IP (a known malicious subnet).
  • The DGA Domain: The destination hostname was auydbswopjvtre.com. Legitimate software doesn't use random keyboard-mash domains; this is a classic sign of a Domain Generation Algorithm (DGA) used by C2 servers to evade blocklists.
  • The Ghost Source: The alert claimed the traffic was coming from my Host IP (10.0.0.42), not a specific container IP.

This created massive confusion: Was my host OS (OMV7) infected? Or was a container "masquerading" behind the host via Docker’s default NAT? 🫣

Zenarmor dashboard screenshot showing blocked outbound network traffic. The logs show repeated attempts from a local IP to a malicious DGA domain "auydbswopjvtre.com" categorized as Malware.
The Anomaly: Zenarmor's Live Session view captures the blocked beacon. Note the random domain auydbswopjvtre.com and the persistent retries.

Step 1: The Disk Scan (Hunting for Artifacts)

My first move was to scan my Docker volumes (/opt/DOCKERS/) for known malicious file hashes associated with recent Linux threats. I used a custom bash script to scan recursively, optimizing it to skip massive media files so it wouldn't take a week to run.

The "Quick & Dirty" Hash Scanner Script:

#!/bin/bash
# VoidLink Scanner - Checks files against known bad hashes
# Skips files > 100MB to avoid scanning media libraries.

KNOWN_HASHES=(
    "070aa5b3516d331e9d1876f3b8994fc8c18e2b1b9f15096e6c790de8cdadb3fc9"
    "13025f83ee515b299632d267f94b37c71115b22447a0425ac7baed4bf60b95cd"
    "05eac3663d47a29da0d32f67e10d161f831138e10958dcd88b9dc97038948f69"
    # ... (Add full list from Threat Reports)
)

TARGET_DIR="${1:-.}"

echo "Starting VoidLink Hash Scan on: $TARGET_DIR"
find "$TARGET_DIR" -type f -size -100M -print0 | while IFS= read -r -d '' file; do
    CURRENT_HASH=$(sha256sum "$file" | awk '{print $1}')
    for BAD_HASH in "${KNOWN_HASHES[@]}"; do
        if [[ "$CURRENT_HASH" == "$BAD_HASH" ]]; then
            echo -e "\n[!!!] ALERT: MATCH FOUND [!!!]"
            echo "File: $file"
        fi
    done
done
echo "Scan complete."

The Result: Clean. 👌

This scan came back empty. This suggested the threat wasn't a dropped file in my persistent config volumes, but likely memory-resident or living inside a container's ephemeral storage layer.


Step 2: The Network Trap (Tcpdump)

Since the file scan was clean, I had to catch the malware in the act of "phoning home."

My firewall logs showed the traffic appearing to come from the Host IP, which usually means a container running in bridge mode (using NAT). I needed to see the internal Docker IP to identify the specific container.

I set up a tcpdump listener on the host, specifically filtering for the malicious subnet.

The "Smoking Gun" Command:

sudo tcpdump -i any net 216.21.13.0/24 -nn -v

(I listened on any interface to catch the packet as it traversed the internal Docker bridge).

After waiting for the next beacon interval, the trap snapped shut. I captured this packet:

Sample System Output (Heavily reduced)

tcpdump -i any net 216.21.13.0/24 -nn -v
tcpdump: data link type LINUX_SLL2
tcpdump: listening on any, link-type LINUX_SLL2 (Linux cooked v2), snapshot length 262144 bytes
14:01:30.860156 veth2faca07 P IP (tos 0x0, ttl 64, id 38176, offset 0, flags [DF], proto TCP (6), length 60)
172.19.0.8.54474 > 216.21.13.14.443: Flags [S], cksum 0x916d (incorrect -> 0xf481), seq 245092942, win 64240, options [mss 1460,sackOK,TS val 896326258 ecr 0,nop,wscale 7], length 0
14:01:30.860158 br-4c829ea900b8 In IP (tos 0x0, ttl 64, id 38176, offset 0, flags [DF], proto TCP (6), length 60)
172.19.0.8.54474 > 216.21.13.14.443: Flags [S], cksum 0x916d (incorrect -> 0xf481), seq 245092942, win 64240, options [mss 1460,sackOK,TS val 896326258 ecr 0,nop,wscale 7], length 0
14:01:30.860169 enp40s0f1.7 Out IP (tos 0x0, ttl 63, id 38176, offset 0, flags [DF], proto TCP (6), length 60)
10.0.0.42.54474 > 216.21.13.14.443: Flags [S], cksum 0xef7b (incorrect -> 0x9673), seq 245092942, win 64240, options [mss 1460,sackOK,TS val 896326258 ecr 0,nop,wscale 7], length 0
14:01:30.860170 enp40s0f1 Out IP (tos 0x0, ttl 63, id 38176, offset 0, flags [DF], proto TCP (6), length 60)
10.0.0.42.54474 > 216.21.13.14.443: Flags [S], cksum 0xef7b (incorrect -> 0x9673), seq 245092942, win 64240, options [mss 1460,sackOK,TS val 896326258 ecr 0,nop,wscale 7], length 0

Gotcha. Visualizing the moment the hunt moved from passive monitoring to active identification. By using tcpdump on the docker bridge network, we trapped the specific container IP responsible for the beacons.

14:01:30.860156 ... 172.19.0.8.54474 > 216.21.13.14.443: Flags [S] ...
There it was.
  • Source: 172.19.0.8 (Internal Docker IP)
  • Destination: 216.21.13.14 (Malicious C2)

It wasn't my host. It was a container living at 172.19.0.8. Well at least it wasn't my entire NAS that was infected!


Step 3: Identifying the Culprit

With the IP in hand, I ran a quick Docker inspection command to unmask the container name.

docker inspect -f '{{.Name}} - {{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' $(docker ps -aq) | grep "172.19.0.8"

The end result:

/flaresolverr - 172.19.0.8

The Verdict: The malicious traffic was originating from my Flaresolverr container.


Analysis: Why This Was "Advanced" Malware

Without reverse-engineering the binary (which I deleted to save my network), I cannot definitively attribute this to VoidLink or any specific group. However, the behavior I observed aligns with the "Tier 1" threats detailed in recent security reports (like the Check Point Research report from Jan 13, 2026).

Here is the forensic profile of what I found:

1. Container Awareness & Masquerading Generic malware usually lands on a box and runs a noisy crypto-miner (XMRig) that spikes the CPU.

  • What I saw: The infection didn't just blast traffic; it routed its C2 beacon through a specific bridged network interface (172.19.0.8) to masquerade as my Host IP (10.0.0.42), effectively bypassing my simpler VLAN isolation rules. This suggests the malware was "container-aware."

2. Supply Chain Entry Vector Advanced malware often targets developer environments via compromised tools.

  • What I saw: The infection wasn't a random brute-force login. It arrived inside a specific, unofficial Docker image fork (21hsmw/flaresolverr) that I had installed months ago to fix a driver bug. This image was likely unmaintained or compromised—a classic supply chain vector.

3. Modular C2 Heartbeat

  • What I saw:
    • DGA Domain: The destination auydbswopjvtre.com is a classic algorithmic domain.
    • Timing: The beacon fired roughly every 10 minutes—a "low and slow" heartbeat designed to blend in with normal traffic, rather than the constant noise of a botnet.

Conclusion: I didn't just clean a script-kiddie virus; I evicted a tool designed for persistence and stealth.💪


The Root Cause & Remediation

How did this happen? A quick audit of my docker-compose.yml revealed two critical mistakes:

  1. Supply Chain Risk: I wasn't using the official image. I was using a random fork (21hsmw/flaresolverr) to fix a bug.
  2. Network Bridging: I had attached the container to two networks: my secure VLAN and the default backend bridge. The malware found the bridge and used it to escape my VLAN firewall rules.

The Fix:

  1. Kill & Prune: Stopped the container and deleted the image immediately.
  2. Secure Configuration: I rewrote the Compose entry to use the official image, dropped all Linux capabilities, and isolated it strictly to the internal VLAN.

Here is the hardened configuration I used:

flaresolverr:
    container_name: flaresolverr
    # 1. USE OFFICIAL IMAGE ONLY
    image: ghcr.io/flaresolverr/flaresolverr:latest
    
    # 2. NETWORK ISOLATION (VLAN Only)
    networks:
      dmz_vlan:
        ipv4_address: 10.0.0.15 # Static IP for strict firewalling
        
    # 3. SECURITY HARDENING
    # Prevent privilege escalation and drop root capabilities
    security_opt:
      - no-new-privileges:true
    cap_drop:
      - ALL
      
    environment:
      - LOG_LEVEL=info
      - CAPTCHA_SOLVER=none
      - TZ=America/Toronto
    restart: unless-stopped

The Lesson

In the world of self-hosting, Supply Chain Security is real. Using a random fork on Docker Hub because "it fixes a bug" is a gamble.

Always stick to official images (ghcr.io/... or verified publishers), and—most importantly—monitor your egress traffic. If Zenarmor hadn't blocked that call, this malware would have dialed home, downloaded something and who knows what would have happened to my home lab!

OPNsense Layer-7 Control: A Deep Dive into Zenarmor (Part 3)
In Part 1, we built the firewall (Layer 3/4). In Part 2, we hardened it with user accounts, 2FA, blocklists, and CrowdSec. Now, it’s time to add Layer 7 (Application) awareness. Your firewall is great at blocking IPs and ports, but it has no idea what is running inside

Maybe it's time to setup graylog, or similar... HHmmm🤔

Trust, but verify.