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.
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.
| 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.
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.
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.
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
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.
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.
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.
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.
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.