Skip to content

Enterprise Wi-Fi & Evil Twin

Enterprise Wi-Fi analysis covers 802.1X port-based authentication as it runs over the air: the EAP conversation between supplicant and authentication server (relayed by the AP), the method choices that determine what an attacker can harvest (EAP-TLS certificate auth vs PEAP-MSCHAPv2 password auth vs EAP-TTLS), and the RADIUS backend that renders the verdict. The evil-twin family is the attack surface this architecture exposes: a rogue access point impersonating the legitimate SSID, harvesting whatever the supplicant offers — EAP Identity strings, MSCHAPv2 challenge-responses (offline-crackable), or credentials into a captive-portal page — with KARMA/MANA variants responding to any probed SSID to harvest clients wholesale. The structural insight of the whole module: in enterprise Wi-Fi, the AP and the network are largely beside the point — the supplicant’s configuration is the target, because a client that validates the authentication server’s certificate correctly refuses the evil twin regardless of signal strength, and a client that doesn’t (“trust any CA”, “don’t validate”, “domain not required”) hands over its credentials to whoever is loudest.

Corporate networks run on 802.1X, and the wireless path is where identity meets physical proximity: an attacker within radio range of an office can present the company’s own SSID and negotiate directly with every laptop that walks in — no phishing email, no malware, just misconfigured trust. The PEAP-MSCHAPv2 reality makes this concrete: the vast majority of enterprise deployments still authenticate with domain username+password inside a TLS tunnel, and when the tunnel’s certificate validation is broken at the client (the single most common supplicant misconfiguration), the evil twin terminates its own TLS, receives the MSCHAPv2 response, and cracks it offline — turning a proximity attack into domain-credential compromise with full lateral-movement consequences (PE4/PE11 chain). The KARMA/MANA variants generalize the harvest: respond to every probe request, and the pile of devices that auto-join “known” networks provides fleet-level opportunistic capture. For defenders, this module defines the two controls that close the door — certificate validation discipline (pin the RADIUS cert/CA, require domain, never “validate automatically”) and WIDS rogue detection (the evil twin is a beacon in your airspace, and the baseline from WI1 makes it findable) — plus the monitoring that catches harvest attempts (EAP failure bursts against your RADIUS, identity leakage, association anomalies). The economics also matter: evil twin is one of the cheapest high-impact attacks in the catalog — a laptop, a known SSID, and patience — which is why the supplicant hardening baseline is the highest-leverage wireless control an enterprise owns.

The 802.1X conversation is a relay chain, and each hop has its exposure:

802.1X OVER THE AIR (PEAP-MSCHAPv2 example)
STA AP (authenticator) RADIUS
| EAPOL-Start | |
| EAP Identity ---->| ---- Identity ----------->|
| (leaks user@realm in cleartext!) |
| EAP Request-TLS Start |
|<-- TLS handshake (server cert!) ------------->|
| SUPPLICANT VALIDATES CERT <-- the crux |
| valid: session keys -> inner MSCHAPv2 |
| invalid+accept-any: EVIL TWIN TERMINATES |
| TLS and reads MSCHAPv2 response |
| MSCHAPv2 response: MD4(NT-hash) challenge |
| -> offline crackable (WI2 economics) |
EVIL TWIN ARCHITECTURE (lab-authorized shape)
hostapd (same SSID, stronger signal, own channel)
+ own RADIUS (accept anything) or captive portal
+ DHCP/DNS: route client traffic to harvest page
MANA/KARMA: respond to ANY probe -> fleet-scale harvest

The EAP Identity is the first harvest: the client’s identity response (user@realm, often the full UPN) crosses the air before any TLS protection — evil twins log it for the credential-cracking step and for targeting (which domain, which user). The method split defines the harvest’s value: EAP-TLS clients (certificate authentication) have no password to give — the evil twin gets a failed TLS handshake and nothing more (certificate theft is a different problem, and cert-bound credentials can’t be replayed); PEAP-MSCHAPv2 clients give an MSCHAPv2 response whose NT-hash mathematics fall to dictionary/rules at WI2’s economics; EAP-TTLS behaves like PEAP with different wrappers. The certificate validation crux is where the attack is won or lost: the supplicant must (a) validate against a trusted CA, (b) check the server name (CN/SAN matches the RADIUS cert), and (c) refuse on failure — three toggle-shaped decisions in every supplicant, misconfigured in every combination enterprise IT has ever seen (“don’t validate” left over from onboarding hell is the classic). The evil-twin mechanics are deliberately simple: hostapd with the cloned SSID on a channel with better signal at the victim’s position, a RADIUS that accepts everything (harvest mode) or a captive portal (phishing mode), DHCP to complete the association — the infrastructure fits on one laptop, which is why WIDS rogue detection and client-side validation are the only scalable defenses. The KARMA/MANA progression weaponized probe responses: KARMA answers every probe request (classic), MANA added listening to other APs’ responses to track which SSIDs each client actually accepts (defeating client-side randomization defenses that KARMA alone tripped), and modern builds combine loud/quiet modes — the counter is clients that don’t probe directed SSIDs (WI1’s discussion) plus corporate devices that refuse non-pinned networks outright.

TechniqueTargetWhat it harvestsClient counterNotes
Evil twin (PEAP harvest)Misconfigured supplicantsMSCHAPv2 response → NT-hash crackCert pinning + domain checkThe core attack
EAP Identity harvestingAll 802.1X clientsuser@realm pre-TLSAnonymized identity (RFC 7542)Targeting intel
Captive-portal phishAny corporate userPassword into fake portalWIDS + user trainingLooks like a captive portal
EAP downgradeTLS-capable fleetsForce method fallbackSupplicant policyMethod pinning
KARMAProbe-active clientsAuto-associationsRandomized probes/MACsClassic
MANAEven non-probing clientsKnown-network associationsCorporate profiles onlyListens to other APs
Rogue-on-wired-portPhysical access surfaceCorporate SSID from insidePort security/NACInsider-adjacent
Deauth + evil twin comboPinned-less clientsForced re-association to twinPMF + pinningWI2 bridge
Hotspot 2.0/Passpoint abuseRoaming clientsTrust-chain confusionProfile hygiene[verify deployment]
Supplicant misconfig auditYour own fleetThe exposure inventoryThis module’s auditThe defender move

eapanalyze.py — an EAP exchange analyzer from monitor-mode captures: reconstructs 802.1X conversations per client, identifies the EAP method negotiated (TLS, MSCHAPv2 inside TLS, TTLS), extracts cleartext Identity responses (the pre-TLS leak), flags clients that send detailed identities before encryption, and produces the supplicant-hygiene report (which clients leak what, which methods run). Dependency: scapy.

#!/usr/bin/env python3
"""eapanalyze.py — 802.1X/EAP conversation analyzer (pcap in).
Usage:
python eapanalyze.py capture.pcap [--json eap.json]
Extracts per-client: EAP identity strings (pre-TLS leak),
negotiated method, failure counts. Deps: scapy.
"""
import argparse, json, sys
from collections import defaultdict
from scapy.all import PcapReader, Dot11, EAPOL, EAP, Raw
EAP_CODES = {1: "Request", 2: "Response", 3: "Success",
4: "Failure"}
EAP_TYPES = {1: "Identity", 2: "Notification",
4: "MD5-Challenge", 13: "TLS",
21: "TTLS", 25: "PEAP", 26: "MSCHAPv2",
43: "FAST"}
def parse_eap(payload):
"""EAP header: code(1) id(1) len(2) type(1) data."""
if len(payload) < 4:
return None
code = payload[0]
eap_id = payload[1]
etype = payload[4] if code in (1, 2) and \
len(payload) > 4 else None
data = payload[5:] if etype is not None else \
payload[4:]
return {"code": EAP_CODES.get(code, code),
"id": eap_id,
"type": EAP_TYPES.get(etype, etype)
if etype is not None else None,
"data": bytes(data)}
def analyze(path):
clients = defaultdict(lambda: {
"identities": set(), "methods": set(),
"identity_leak": False, "eap_fail": 0,
"mschap_responses": 0, "frames": 0})
convos = 0
with PcapReader(path) as reader:
for pkt in reader:
if Dot11 not in pkt or EAPOL not in pkt:
continue
d11 = pkt[Dot11]
client = d11.addr2 or d11.addr1
eap = pkt[EAPOL]
# EAPOL type 0 = EAP-Packet
if getattr(eap, "type", 1) != 0:
continue
payload = bytes(eap.payload)
parsed = parse_eap(payload)
if not parsed:
continue
convos += 1
c = clients[client]
c["frames"] += 1
if parsed["code"] == "Response" and \
parsed["type"] == "Identity":
ident = parsed["data"].decode(
errors="replace").strip("\x00")
if ident:
c["identities"].add(ident)
if "@" in ident or "\\" in ident:
c["identity_leak"] = True
elif parsed["code"] == "Response" and \
parsed["type"] == "MSCHAPv2":
c["mschap_responses"] += 1
elif parsed["type"] in ("TLS", "TTLS", "PEAP",
"FAST"):
c["methods"].add(parsed["type"])
elif parsed["code"] == "Failure":
c["eap_fail"] += 1
return {"conversations": convos,
"clients": {k: {"identities": sorted(v[
"identities"]),
"methods": sorted(v["methods"]),
"identity_leak": v[
"identity_leak"],
"mschap_responses": v[
"mschap_responses"],
"eap_fail": v["eap_fail"],
"frames": v["frames"]}
for k, v in clients.items()}}
def main():
ap = argparse.ArgumentParser()
ap.add_argument("pcap")
ap.add_argument("--json")
args = ap.parse_args()
s = analyze(args.pcap)
print(f"== 802.1X analysis: {s['conversations']} EAP "
f"frames, {len(s['clients'])} clients ==")
leaking = [(k, v) for k, v in s["clients"].items()
if v["identity_leak"]]
print(f"\nidentity leakage (pre-TLS user@realm): "
f"{len(leaking)} clients")
for k, v in leaking[:10]:
idents = ", ".join(v["identities"][:3])
print(f" {k} {idents}")
mschap = [(k, v) for k, v in s["clients"].items()
if v["mschap_responses"]]
print(f"\nMSCHAPv2-in-use clients (crackable-inner "
f"method): {len(mschap)}")
for k, v in mschap[:10]:
print(f" {k} responses={v['mschap_responses']} "
f"methods={v['methods'] or '-'}")
failing = [(k, v) for k, v in s["clients"].items()
if v["eap_fail"] > 2]
if failing:
print(f"\nelevated EAP failures (harvest attempts "
f"often look like this):")
for k, v in failing[:8]:
print(f" {k} failures={v['eap_fail']}")
if args.json:
json.dump(s, open(args.json, "w"), indent=2)
print(f"\n[+] {args.json}")
if __name__ == "__main__":
main()
  • parse_eap() (lines 21-36) decodes the EAP header: code (Request/Response/Success/Failure), identifier, and — for code 1/2 — the EAP type byte with its payload. The type mapping (lines 16-18) names the methods that matter: Identity (the leak), TLS/TTLS/PEAP/FAST (tunnel methods), MSCHAPv2 (the crackable inner).
  • The identity-leak rule (lines 66-71): any Identity response containing @ or \ (UPN or DOMAIN\user shapes) marks the client as leaking a meaningful identity pre-TLS. Anonymous identities (RFC 7542 anonymous@realm) pass through without the flag — the sanitizer the report wants to see.
  • The MSCHAPv2 counter (lines 72-75) counts inner-auth responses — the crackable-material indicator. Note the honesty limit: MSCHAPv2 responses inside TLS are invisible to a passive capture (tunneled), so this counter fires on TLS-stripped exchanges (evil-twin captures) and legacy non-tunneled uses — which is exactly the exposure the tool is auditing, and the design comment states it.
  • The failure counter (lines 76-78) pairs with the evil-twin hunt: a client whose EAP conversation ends in repeated Failure (against a twin that accepts-then-drops) is the harvest-attempt shape — the report names it as such rather than overclaiming attribution.
  • The report’s three sections (lines 141-158) are the audit’s layers: leakage (identity hygiene), method exposure (MSCHAPv2 fleets), and anomaly (failure bursts) — the supplicant-hardening worklist, ranked by what an attacker would value.

The evil twin reaches the target airspace as a beacon that out-shouts the legitimate AP at the victim’s position — infrastructure cheaper than a phishing kit and positioned where the trust already exists (the company’s own SSID). The client’s supplicant reaches the decision point automatically: association attempts, EAP Identity (leaked pre-TLS), TLS handshake against the twin’s certificate — and the misconfigured validation setting converts that moment into credential capture. The harvested MSCHAPv2 response reaches the cracking rig offline (WI2 economics), the cracked NT-hash reaches the domain as a valid credential (PE4’s lateral movement, PE11’s enumeration), and the intrusion’s wireless origin reaches the IR report days later as “unknown initial access vector” unless the WIDS and RADIUS logs were watched. On the defender’s side, the controls reach the same moments: certificate pinning (CA + domain + refuse) makes the twin’s TLS unacceptable — the association fails, and the attempt becomes a RADIUS-side/WIDS-side event; anonymous outer identities make the leak worthless; method pinning (EAP-TLS only, no fallback) removes the downgrade surface; WIDS rogue detection finds the twin’s beacon via the WI1 baseline. The audit reaches the fleet as the worklist: this module’s analyzer output is the supplicant-hygiene inventory (who leaks, who runs crackable inners, who fails weirdly) that hardening projects are staffed from. And the KARMA/MANA dimension reaches the personal-device policy conversation: corporate laptops that probe and auto-join are harvestable anywhere on earth, which is why “corporate profiles only, no open networks, no unknown SSIDs” is a technical control, not a suggestion.

  1. Recon — WI1 survey: corporate SSID on ch 36, 802.1X, clients present; rogue-opportunity assessed
  2. Stand up — lab-authorized twin: hostapd same SSID, better signal at target’s desk, accept-all RADIUS
  3. Lure — client associates (validation misconfigured); EAP Identity leaked pre-TLS
  4. Harvest — twin terminates own TLS; MSCHAPv2 response captured; client sees failure
  5. Crack — NT-hash falls to rules in 2.5 hours on measured rig
  6. Access — domain credential valid: VPN + internal apps (PE4 chain opens)
  7. Detect — RADIUS failure bursts from twin’s MAC + WIDS rogue beacon: SOC correlation piece
  8. Report — twin-to-domain timeline; remediation: cert pinning, anonymous identity, method pinning

roguecheck.py — the rogue/evil-twin detector: consumes a WI1-class survey (or its JSON) and flags twin candidates — same SSID on multiple BSSIDs, cipher/profile mismatches among same-SSID beacons, vendor OUI inconsistent with the legitimate fleet, and signal-plausibility outliers — producing the WIDS-independent rogue list that baseline hygiene needs.

  • Baseline-relative, not absolute. A rogue is a discrepancy against your inventory, so the tool takes the known-good AP list (CSV: BSSID, SSID, OUI, channel) and flags everything that conflicts — absolute heuristics alone drown in neighbors’ legitimate APs.
  • Profile-mismatch as the strong signal. Evil twins rarely replicate the full beacon profile (cipher suites, WPA IE details, RSN capabilities) — same-SSID-different-profile is the strongest cheap discriminator.
  • OUI continuity. Corporate fleets buy APs in bulk; a twin’s vendor OUI differing from every legitimate AP with the same SSID is a high-signal flag.
  • Channel-duplication awareness. Same SSID on two BSSIDs is normal for enterprise (many radios) — the tool flags it only when the inventory doesn’t already account for the second BSSID.
#!/usr/bin/env python3
"""roguecheck.py — rogue/evil-twin detector vs known-good inventory.
Usage:
python roguecheck.py survey.json --inventory aps.csv \
[--out rogue_report.md]
aps.csv: bssid,ssid,vendor,channel (the known-good fleet)
survey.json: wifiscan.py output shape
stdlib only.
"""
import argparse, csv, json, sys
def load_inventory(path):
inv = {}
try:
with open(path, encoding="utf-8", newline="") as fh:
for row in csv.DictReader(fh):
inv[row["bssid"].lower()] = {
"ssid": row.get("ssid", ""),
"vendor": row.get("vendor", ""),
"channel": row.get("channel", "")}
except OSError:
sys.exit(f"cannot read inventory {path}")
return inv
def main():
ap = argparse.ArgumentParser()
ap.add_argument("survey")
ap.add_argument("--inventory", required=True)
ap.add_argument("--out")
args = ap.parse_args()
survey = json.load(open(args.survey, encoding="utf-8"))
inv = load_inventory(args.inventory)
aps = survey.get("aps", {})
findings = []
unknown = []
for bssid, rec in aps.items():
b = bssid.lower()
ssid = rec.get("ssid") or ""
if b not in inv:
unknown.append((bssid, ssid,
rec.get("channel"),
rec.get("rsn")))
continue
# profile mismatch on a known BSSID
inv_rec = inv[b]
if inv_rec["ssid"] and ssid and \
inv_rec["ssid"] != ssid:
findings.append(("HIGH", bssid,
f"known BSSID now advertises SSID "
f"'{ssid}' (inventory: "
f"'{inv_rec['ssid']}') — clone/override?"))
# unknown BSSIDs broadcasting OUR SSIDs = twin candidates
our_ssids = {r["ssid"] for r in inv.values() if r["ssid"]}
for bssid, ssid, ch, rsn in unknown:
if ssid in our_ssids:
findings.append(("HIGH", bssid,
f"unknown device broadcasting corporate "
f"SSID '{ssid}' (ch={ch}, rsn={rsn}) — "
f"evil-twin candidate"))
elif ssid:
findings.append(("LOW", bssid,
f"unknown neighbor network '{ssid}' "
f"(informational)"))
# same-SSID profile mismatch among unknowns
by_ssid = {}
for bssid, rec in aps.items():
if rec.get("ssid"):
by_ssid.setdefault(rec["ssid"], []).append(
(bssid, rec))
for ssid, members in by_ssid.items():
rsn_set = {r.get("rsn") for _, r in members}
if len(rsn_set) > 1 and len(members) > 1:
known_b = {b.lower() for b in inv
if inv[b]["ssid"] == ssid}
unknown_b = {b for b, _ in members
if b.lower() not in known_b}
if unknown_b:
findings.append(("MED", ssid,
f"profile mismatch across BSSIDs "
f"({len(rsn_set)} distinct RSN) with "
f"uninventoryed members: "
f"{sorted(unknown_b)[:3]}"))
order = {"HIGH": 0, "MED": 1, "LOW": 2}
findings.sort(key=lambda f: order[f[0]])
print(f"== rogue check: {len(findings)} findings ==")
for sev, who, msg in findings:
print(f" [{sev}] {who} {msg}")
if args.out:
with open(args.out, "w") as fh:
fh.write("# Rogue AP check\n\n")
for sev, who, msg in findings:
fh.write(f"- **{sev}** `{who}` {msg}\n")
print(f"[+] {args.out}")
if __name__ == "__main__":
main()
  • The inventory join (lines 44-58) is the tool’s spine: every surveyed BSSID is checked against the known-good list — known BSSIDs get profile checks (a known radio suddenly advertising a different SSID is an override/clone event), unknown BSSIDs get classified.
  • The twin-candidate rule (lines 61-66) is the strongest signal in the tool: an uninventoried device broadcasting a corporate SSID — the evil twin’s exact signature, independent of any heuristic tuning.
  • The profile-mismatch rule (lines 73-85) catches subtler twins: same corporate SSID, multiple BSSIDs, but the RSN profiles disagree — real enterprise multi-radio deployments share a profile, so distinct RSN strings among same-SSID beacons with uninventoryed members is clone-grade evidence.
  • Severity layering (HIGH/MED/LOW) keeps the report actionable: HIGH = act now (twin candidates), MED = verify (profile mismatch), LOW = inventory hygiene (neighbors) — the triage split that keeps the WIDS conversation focused.

build the survey + inventory from the WI1 lab

Section titled “build the survey + inventory from the WI1 lab”

$ python wifiscan.py lab_cap-01.cap —json survey.json $ cat > aps.csv <<‘EOF’ bssid,ssid,vendor,channel a4:2b:b0:xx:xx:01,LabCorp-Corp,cisco,36 a4:2b:b0:xx:xx:02,LabCorp-Guest,cisco,11 EOF

stand up the authorized lab twin on ch 6 and capture again

Section titled “stand up the authorized lab twin on ch 6 and capture again”

$ hostapd twin.conf & # SSID LabCorp-Corp, ch 6 $ python wifiscan.py twin_cap.cap —json survey2.json

$ python roguecheck.py survey2.json —inventory aps.csv == rogue check: 2 findings == [HIGH] 9c:3d:cf:xx:xx:09 unknown device broadcasting corporate SSID ‘LabCorp-Corp’ (ch=6, rsn=CCMP-128) — evil-twin candidate [LOW] 74:da:38:xx:xx:11 unknown neighbor network ‘NextDoor-2G’ (informational) [+] rogue_report.md

EAP analysis over the twin window: harvest shapes visible

Section titled “EAP analysis over the twin window: harvest shapes visible”

$ python eapanalyze.py twin_cap.cap —json eap.json identity leakage (pre-TLS user@realm): 2 clients 3c:5a:b4:xx:xx:0f labadmin@lab.local elevated EAP failures (harvest attempts often look like this): 3c:5a:b4:xx:xx:0f failures=5

The evil twin is always detectable in your airspace — the beacon exists — which makes the defense primarily an inventory-and-baseline discipline: WI1’s survey + this module’s inventory diff, running continuously through WIDS sensors or periodically through manual sweeps. The client-side moment is the other half: supplicant hardening (pin the CA, require domain, refuse invalid) converts harvest attempts into failed associations that appear in RADIUS logs as failure bursts from unexpected APs — and RADIUS-side monitoring (your own server seeing auth requests from a BSSID that isn’t yours — only possible with relayed/bridged twins) plus client-side failure telemetry are the two logs most estates never joined (the join is improvement territory). Your own tooling hygiene: all three tools read captures/JSON — the twin infrastructure (hostapd + RADIUS) is the authorized-engagement layer, and the lab runbook keeps it on RF-isolated or explicitly authorized channels. WIDS vendors classify rogue/twin natively (BT12) [verify per vendor]; the DIY detector’s independence matters for procurement testing (does the WIDS actually flag the twin my detector flags?) and for estates without WIDS budgets. The standing metric: rogue findings per sweep — near-zero on a managed airspace, and the twin drill (stand up an authorized twin, see which tool finds it first: WIDS or roguecheck) is the annual validation that the baseline actually works.

  1. Beacon-profile fingerprinting depth: full IE-set hashing per SSID, making profile-mismatch detection resistant to twins that clone the common IEs.
  2. Signal-plausibility: multi-position signal sampling to localize twin candidates physically (which floor, which desk).
  3. RADIUS join: correlate harvest-window EAP failures with the twin’s BSSID presence for the attribution chain.
  4. MANA-exposure audit: measure which corporate devices accept probe-responses from unknown APs (client posture, not AP posture).
  5. Continuous sweep mode: scheduled survey + diff vs inventory with SIEM-facing JSON (the WIDS-independent tripwire).
SignalSourceIDNotes
Unknown BSSID on corporate SSIDroguecheck / WIDSinventory diffEvil-twin candidate (HIGH)
Beacon profile mismatchroguecheck / WIDSprofile diffClone evidence
EAP failure burstsRADIUS logsper-client failuresHarvest-attempt shape
Identity leakage (pre-TLS)eapanalyzesupplicant auditHygiene worklist
MSCHAPv2 inner exposureeapanalyze / configmethod auditCrackable-inner fleets
Supplicant “don’t validate”Config audit / MDMposture findingThe core misconfiguration
KARMA/MANA responsesWIDSprobe-response anomaliesFleet-harvest attempts
Rogue on wired portNAC/port securityswitchport findingsInsider-adjacent twin
Client auto-join anomaliesEDR/MDMnetwork join logsCorporate devices off-profile
Wazuh join of WIDS+RADIUSHIDS/SIEM[verify rule IDs per deployment]The attribution join

The evil-twin operator’s evasion targets the inventory and validation layers. Beacon cloning depth: copying the SSID and channel is table stakes; better twins replicate cipher suites, vendor IEs, and even beacon interval to survive profile-diff detection — countered by fingerprint depth (full IE-set hashing) and by physical localization (the twin’s signal position is wrong, and multi-point sampling localizes it). Signal-position discipline: the twin places itself where the legitimate signal is weakest (so its own signal wins the client’s roaming logic) — countered by sensor placement density (twin pockets are exactly the coverage gaps, so the chanstat coverage check feeds sensor planning). Identity-harvest stealth: accepting EAP identity then failing gracefully (client sees “wrong password”) avoids the captive-portal red flag — countered by RADIUS-side anomaly correlation and client-side failure telemetry. KARMA/MANA opsec: harvest infrastructure that responds only to selected probe SSIDs (targeted MANA) avoids the mass-response signature that mass KARMA generates — countered by client posture (corporate devices that never join unknown SSIDs, MDM-enforced). Wired rogue silence: a rogue AP bridged to an internal switch port needs no radio signature to defeat — countered by NAC/port security (the wired side owns that boundary). Client-selection farming: long-horizon MANA that waits weeks for one executive laptop to auto-join trades speed for silence — countered by the fact that pinned supplicants make all of it irrelevant, which is why the pinning audit is the highest-leverage defender move in this entire module.

⚠️ What caught me: the first evil-twin drill I ran against our own fleet “failed” beautifully — zero clients associated, the twin sat ignored for two hours, and I wrote up “fleet hardened” with relief. Then the audit tool showed me why: the twin’s beacon was on channel 44, and our WIDS sensor plan covered 1/6/11 plus the 5GHz channels we used — 44 was a blind spot, and at least three clients did associate during a window I later reconstructed from their logs; their harvest attempts had simply never been visible to anything we operated. The drill didn’t fail; my sensor coverage did. Since then, every twin drill runs on a channel we believe is covered, and the drill’s real metric is which tool sees it first — WIDS, the inventory diff, or the users’ RADIUS failures. An evil twin you can’t see isn’t a failed attack; it’s a measurement of exactly where your airspace ends.

  1. Build the 802.1X lab: hostapd with EAP-PEAP against FreeRADIUS, one lab client with correct cert validation — capture the full exchange and run eapanalyze.py; confirm the identity-leak flags and method classification against your config.
  2. Break validation deliberately: change the client supplicant to “don’t validate” (lab only!), stand up the authorized twin with an accept-all RADIUS, and watch the harvest — the full chain, from beacon to MSCHAPv2 response, in a controlled space.
  3. Run roguecheck.py with the twin live and confirm the HIGH finding; then toggle the twin’s beacon to clone the full profile (cipher suites, IEs) and see which detector layer still catches it — the fingerprint-depth lesson in one hour.
  4. Measure the pinning payoff: re-enable validation + pin the lab CA on the client, re-run the twin, and confirm the association fails with an EAP failure burst — the control’s receipt, demonstrated.
  5. Channel-coverage drill: run the twin on a channel your (simulated) sensor plan doesn’t cover, then use channelstat.py to find the blind spot from the capture — the sensor-planning feedback loop from the WI1 ⚠️ story, practiced.
  • Technical: Walk the 802.1X/PEAP-MSCHAPv2 exchange end to end — which frames are cleartext, where does certificate validation sit, and what exactly does an evil twin harvest?
  • Technical: Why does EAP-TLS neutralize the credential-harvest evil twin, and what new risks does certificate-based auth introduce?
  • Scenario: Your rogue detector flags an unknown BSSID broadcasting the corporate SSID from the executive floor. What is the response sequence, in order and with evidence?
  • Conceptual: Why is supplicant configuration — not AP hardening — the decisive control against evil twins, and which three settings constitute the baseline?
  • Conceptual: How do KARMA and MANA differ in client exploitation, and why does client probe behavior determine exposure?
  • Deep-dive: Design the enterprise wireless-trust audit: supplicant posture inventory, RADIUS-side monitoring, WIDS baseline joins, and the annual twin-drill validation.
  • Practical: You inherit an estate where 60% of clients ship with “don’t validate” left from onboarding. What is the remediation sequence and how do you prove it held?

WI1 (Wireless Fundamentals & Recon (802.11)) · WI2 (WPA2-PSK Cracking (Handshake Capture)) · WI3 (WPA3 & SAE Analysis) · PE11 (Active Directory Enumeration) · BT12 (Wireless IDS)