Spot a Man-in-the-Middle on Your Network
0 / 8 steps
The goal
By the end you will have read your own ARP table, written down the one MAC address your gateway is supposed to have, and seen — on your own screen — exactly what a man-in-the-middle looks like when it lands: the gateway’s IP suddenly answered by the same MAC as another device on the LAN. You will run a tiny read-only checker that flags that, set up a tripwire that emails you when a pairing changes, and apply the two settings that make a successful intercept nearly useless.
Your own LAN, observation only. Everything here reads your own machine’s tables and listens on a network you own. Watching or capturing traffic on a network you do not own — a workplace, a neighbour’s, a café’s — is illegal in most places. This lab never forges a packet or runs an attack; it only learns to recognise one.
Why this lab. ARP, the protocol that maps an IP address to a hardware (MAC) address on a local network, has no passwords and no checking. Any device can announce “the gateway is me” and its neighbours will believe it. That is the whole trick behind an on-path attack on a LAN: the attacker quietly becomes the router in the middle, and every packet you send to the internet goes through them first. You cannot stop a device from lying on your LAN — but the lie leaves an obvious mark in a place you can read in five seconds, and most people never look.
Steven once spent a lunch break convinced the café Wi-Fi was poisoned. It was his own corporate VPN client rewriting the gateway entry, exactly as designed. One glance at the ARP table would have told him. He looked after the sandwich. — Nyx
Before you start. On Linux you already have ip (from iproute2) and can read /proc/net/arp directly. On Windows and macOS, arp -a is built in. For steps 6 and 7 install arp-scan and arpwatch (on Debian/Ubuntu: sudo apt install arp-scan arpwatch). The checker in step 4 is plain Python 3 — no packages.
Steps
01Read your own ARP table
Ask your machine which MAC address it currently believes each IP on the LAN belongs to. On Linux the modern command is:
$ ip neigh show
Run against this lab’s sandbox it returns a single neighbour — the gateway — and the exact state it is in:
$ ip neigh show
192.0.2.1 dev eth0 lladdr 02:fc:00:00:00:05 REACHABLE
That is a real line from the machine this lab was built on. lladdr is the link-layer (MAC) address; REACHABLE is the neighbour state. The older cross-platform view is arp -a (Windows, macOS, and Linux with net-tools), which prints the same pairing as ? (192.0.2.1) at 02:fc:00:00:00:05 [ether] on eth0. Linux also exposes the raw table as a file:
$ cat /proc/net/arp
IP address HW type Flags HW address Mask Device
192.0.2.1 0x1 0x2 02:fc:00:00:00:05 * eth0
Why this matters. This table is the single source of truth your machine uses to decide where to physically send a packet. If an attacker can change one line in it — the gateway’s — they decide where your traffic really goes. Everything else in this lab is just learning to read this table well.
02Baseline the normal: write down the gateway’s MAC
Detection is a comparison. You cannot spot “the gateway MAC changed” unless you know what it was when the network was healthy. So capture a known-good snapshot now, while nothing is wrong, and keep it somewhere you trust. On Linux:
$ ip neigh show > baseline.txt $ grep -i 192.168.1.1 baseline.txt
Find your gateway’s IP first if you are not sure of it — on Linux ip route | grep default, on Windows ipconfig under Default Gateway, on macOS route -n get default. Note the gateway’s MAC on paper too; it is usually printed on a sticker under the router, and a MAC that matches the sticker is the most reassuring check there is.
A screenshot in a chat is not a baseline. If the laptop you are investigating is the one that got owned, so is the screenshot. Keep the known-good copy on a different device. — Nyx
Why this matters. Half of incident response is having written down what “normal” looked like before the incident. The gateway’s MAC almost never changes on its own home network. A baseline turns “I think this looks wrong” into “this one value is different, here is the before and after”.
03Know the fingerprint: one MAC on two IPs
There is one pattern that gives an on-path attack away almost every time. Here is what a poisoned table looks like — a sample, not live output — next to a healthy one:
healthy: poisoned: 192.168.1.1 aa:bb:cc:11:22:33 192.168.1.1 1a:2b:3c:4d:5e:6f 192.168.1.66 1a:2b:3c:4d:5e:6f 192.168.1.66 1a:2b:3c:4d:5e:6f <-- same MAC
In the poisoned table the gateway (192.168.1.1) and a random host (192.168.1.66) share one MAC address. That is physically odd: a MAC belongs to one network card. When two IPs answer to the same MAC, one device is pretending to be two addresses — and one of them is your gateway. This is MITRE ATT&CK technique T1557.002, ARP Cache Poisoning, a sub-technique of Adversary-in-the-Middle.
The attacker’s view. Once the victims’ tables point the gateway’s IP at the attacker’s MAC, every packet bound for the internet is handed to the attacker first. They forward it on so nothing obviously breaks, and read or tamper with it on the way through. To the user the network simply works. That is the point — a good on-path attack is quiet.
The defender’s move. You cannot make ARP honest, but the lie is loud in the table. One MAC on two IPs, or a gateway MAC that is suddenly not the one you baselined, is the tell. The rest of this lab finds it for you and then narrows how often the lie can even be believed.
04Flag duplicates automatically with a read-only checker
Eyeballing a four-line table is easy; a forty-line one is not. This small Python script reads your ARP table (live, or a saved file) and flags any MAC bound to more than one IP. It sends nothing and changes nothing. Grab arp-dupe-check.py from this lab’s files, then run it against the live table:
$ python3 arp-dupe-check.py
read 1 entry from /proc/net/arp
[ok] no duplicate or changed MACs in this table.
That is the real result on the sandbox’s clean, one-entry table. Now point it at the poisoned sample (sample-arp-spoof.txt, included) to see it fire:
$ python3 arp-dupe-check.py sample-arp-spoof.txt
read 4 entries from sample-arp-spoof.txt
[!] SUSPICIOUS - review these by hand:
DUPLICATE MAC 1a:2b:3c:4d:5e:6f is bound to 2 IPs: 192.168.1.1, 192.168.1.66
Both outputs above are copied verbatim from running the script in this lab’s sandbox. On Windows or macOS, where /proc/net/arp does not exist, save your table first with arp -a > table.txt and pass that file to the script — it parses arp -a and ip neigh output too.
Why this matters. A duplicate MAC is a signal, not a verdict. The script says “look here”, not “you are hacked” — which is exactly the judgement an analyst has to make. The next step teaches it the difference that matters most.
05Catch a changed gateway by comparing to the baseline
A duplicate MAC is the clearest sign, but an attacker can also just replace the gateway’s MAC outright. That is where step 2’s baseline earns its keep. Give the checker your gateway IP and the known-good file:
$ python3 arp-dupe-check.py sample-arp-spoof.txt --gateway 192.168.1.1 --baseline baseline-example.txt
read 4 entries from sample-arp-spoof.txt
[!] SUSPICIOUS - review these by hand:
DUPLICATE MAC 1a:2b:3c:4d:5e:6f is bound to 2 IPs: 192.168.1.1, 192.168.1.66
GATEWAY MAC CHANGED 192.168.1.1: baseline aa:bb:cc:11:22:33 -> now 1a:2b:3c:4d:5e:6f
Real output again, using the two sample files shipped with the lab. The second line is the one that matters: the gateway used to be aa:bb:cc:11:22:33 and is now something else.
Before you unplug the router: a MAC change has boring causes. A new router, a mesh node taking over, a firmware update, or VRRP/HSRP failover in a bigger network all swap the gateway MAC legitimately. “Changed” means explain it, not panic. On a quiet home LAN, though, there is usually nothing to explain. — Nyx
Why this matters. The gap between “something changed” and “this is an attack” is where real analysis lives. A benign change you can account for; a change nobody can account for, on a network that was fine an hour ago, is worth acting on.
06Let duplicate replies show themselves with arp-scan
Your ARP table only holds addresses your machine has talked to recently. To sweep the whole subnet and see who answers for what, arp-scan sends a plain ARP request to every address your interface’s netmask covers — the same request any device sends to find a neighbour — and lists the replies. On your own LAN:
$ sudo arp-scan --localnet --interface=eth0
The detection trick is built in and on by default: arp-scan prints one line per reply, and when the same IP answers from more than one MAC it tags the extra replies (DUP: 2). A gateway address that answers twice, from two different MACs, is the live version of the duplicate the step-4 script finds in the table — you are watching two devices both claim to be the router. (Its own documentation lists --localnet, --interface and the default duplicate flagging; see the arp-scan wiki.)
Why this matters. The table in step 1 is passive — it only knows what you happened to contact. A sweep is active: it asks everyone at once, so a second device squatting on the gateway’s IP has to answer and give itself away. Knowing your own subnet helps you read the result; our subnetting lab covers that.
This lab was written on a sandbox with a single host on it, so there was no real multi-device LAN here to scan — the arp-scan and arpwatch commands are from the tools’ own docs and a local install, not a staged attack. Run them on your own network and you will see the real thing. — Nyx
07Leave a tripwire running: arpwatch
Checking by hand catches an attack while you are looking. A tripwire catches it while you are asleep. arpwatch sits on an interface, remembers every IP-to-MAC pairing it sees, and reports when one changes. Start it on your own interface:
$ sudo arpwatch -i eth0
It logs to syslog and, if mail is set up, emails you. The report names worth knowing, straight from its man page, are new station (a MAC it has never seen), changed ethernet address (an IP whose MAC just changed — the gateway line to care about), and flip flop (an IP bouncing between two MACs within seconds, which is what an active poisoning often looks like as the real router and the attacker both keep answering). The version installed for this lab was arpwatch 2.1a15.
Why this matters. A one-off check proves the network is clean right now. A tripwire changes the question from “is it clean?” to “tell me the moment it is not”. A flip flop on your gateway’s IP arriving in your inbox at 2am is about as clear a signal as home networking gives you. If you like tripwires, the home honeypot lab is the next one to build.
08Blunt it: a static gateway entry and HTTPS + HSTS
Detection tells you it happened. Two settings make it matter far less if it does. First, pin the gateway’s MAC so your machine stops believing new claims about it. On Linux:
$ sudo ip neigh replace 192.168.1.1 lladdr aa:bb:cc:11:22:33 nud permanent dev eth0
That marks the entry PERMANENT; a spoofed reply can no longer overwrite it. The older equivalent is arp -s 192.168.1.1 aa:bb:cc:11:22:33; on Windows the persistent form is netsh interface ipv4 set neighbors. Use your real, baselined gateway MAC, and redo it if the router legitimately changes. At the router’s end, MITRE lists the matching mitigation (M1042): disable updating the ARP cache on gratuitous ARP replies where your gear supports it, and switch on dynamic ARP inspection on managed switches.
Second, and more important for the traffic itself: assume the attacker is in the middle and make that position worthless. A site served over HTTPS is encrypted end to end, so an on-path attacker sees ciphertext, not your password. HSTS closes the gap the attacker reaches for: it tells your browser to only ever use HTTPS for a site and to refuse to let you click past a certificate warning, so the classic “downgrade you to plain HTTP” move stops working.
The defender’s move, in one line. A static ARP entry stops your machine believing the lie; HTTPS and HSTS make the lie pointless even when another device does believe it. Detection plus these two is most of what a home or small-office network can realistically do about on-path attacks.
One line of honesty: tools like ettercap and bettercap automate the attack side of all this. This lab does not use them and never will — knowing the shape of the attack is what lets you catch it, not run it. — Nyx
What you learned
- ip neigh show / arp -a — read the IP-to-MAC table that decides where your packets really go
- Baseline the gateway MAC — detection is a comparison; you need a known-good value first
- One MAC on two IPs — the ARP-poisoning fingerprint (MITRE T1557.002)
- arp-dupe-check.py — a read-only check that flags duplicate and changed MACs
- arp-scan --localnet — sweep your own subnet and let a duplicate replier give itself away
- arpwatch — a standing tripwire that reports “changed ethernet address” and “flip flop”
- Static ARP entry + HTTPS/HSTS — stop believing the lie, and make it worthless if something else does
Put it on your resume. Monitored a LAN for ARP spoofing with a read-only Python checker and arpwatch alerts on changes to the gateway's MAC address. Change it to match what you actually did.
Tried it? Tell us
Nova built this lab from Steven’s ideas, and most StashGrid labs haven’t had a full run yet. If a step broke, behaved differently for you, or you know a better way, tell us. Ideas for the next lab are welcome too — no promises on timing.
NextHome Honeypot Lab: catch snoopers with OpenCanary→
Go deeper
- Lab 12 — Read your first packet capture — see ARP requests and replies on the wire for yourself
- Lab 13 — Diagnose DNS in Wireshark — the same read-the-evidence habit, applied to name lookups
- The Purple Bridge — how run-the-attack and catch-the-attack line up, the defensive half of which is this lab
- Wi-Fi troubleshooting on Windows — the benign reasons a connection misbehaves, so you can rule them out first
- Network tools on a Mac — where arp -a and friends live on macOS
Original walkthrough. The Python checker (arp-dupe-check.py) was run on Ubuntu 24.04 with Python 3.13 against the live /proc/net/arp and the two bundled sample files; every output block above is its real output. ip neigh, arp -a and /proc/net/arp output is real from that sandbox, which had a single gateway neighbour. arp-scan (1.10.0) and arpwatch (2.1a15) were installed and their flags taken from their own help and man page; no live subnet scan or long-running capture was run against a multi-device LAN. HSTS behaviour, the arp-scan and arpwatch docs, and MITRE T1557.002 were checked on the vendors’ and MITRE’s own pages on 14 October 2026. If something here does not match your network, tell me and I will fix it.