ZCL (Zigbee Cluster Library)
The application language every Zigbee device speaks: clusters, attributes and commands, carried in a three-byte frame.
The Zigbee Cluster Library is the application layer of Zigbee: the shared vocabulary that lets a switch from one vendor turn on a bulb from another. Everything a Zigbee device exposes, and everything an attacker reads or forges once the network key is known, is a ZCL frame.
What it is
A device presents one or more endpoints (numbered 1 to 240), and each endpoint implements a set of clusters. A cluster is a small object with an identifier, a table of attributes (state) and a set of commands (actions). The common ones are worth memorising:
| Cluster | ID | What lives there |
|---|---|---|
| Basic | 0x0000 | manufacturer name, model, location description, software build |
| Identify | 0x0003 | make the device blink so you can find it |
| On/Off | 0x0006 | Off (0x00), On (0x01), Toggle (0x02) |
| Level Control | 0x0008 | Move to Level, with or without On/Off |
| Colour Control | 0x0300 | hue, saturation, colour temperature |
| Temperature Measurement | 0x0402 | MeasuredValue, reported periodically |
| IAS Zone | 0x0500 | intrusion sensors, Zone Status Change Notification |
| Door Lock | 0x0101 | Lock Door, Unlock Door, PIN code management |
| OTA Upgrade | 0x0019 | firmware images, block by block |
Cluster identifiers from 0xFC00 upward are manufacturer specific: the identifier alone means nothing without the manufacturer code that goes with it.
The frame
The ZCL header is three octets, or five when the manufacturer-specific bit is set:
frame control (1) [ manufacturer code (2) ] transaction sequence (1) command id (1)
The frame control byte carries the frame type (0 = profile-wide, 1 = cluster specific), the manufacturer-specific bit, the direction (client to server, or server to client), and the disable-default-response bit. Direction matters when reading a capture: command 0x05 on the OTA cluster is an Image Block Response coming from the server, while 0x05 client to server on another cluster is something else entirely.
Profile-wide commands are the same on every cluster:
| Command | ID | Meaning |
|---|---|---|
| Read Attributes | 0x00 | ask for a list of attribute identifiers |
| Read Attributes Response | 0x01 | attribute id, status, data type, value |
| Write Attributes | 0x02 | attribute id, data type, value |
| Configure Reporting | 0x06 | ask for periodic reports |
| Report Attributes | 0x0A | the periodic report itself |
| Default Response | 0x0B | command id and status |
Attribute values are typed. Type 0x42 is a character string, length prefixed on one octet; 0x29 is a signed 16-bit integer; 0x21 an unsigned 16-bit integer. A character-string attribute is where installers put site names, asset tags and, too often, secrets.
Why it matters
ZCL is where the meaning is. A NWK frame tells you two short addresses; the ZCL frame inside tells you that a door lock was told to unlock. In an assessment the ZCL layer is both the read (attribute reports leak occupancy, energy use, firmware versions) and the write (a forged On/Off or Door Lock command is a single cluster-specific frame under the network key).
How to read it
Wireshark dissects the whole stack once it can decrypt, and the display filters are worth knowing:
# every ZCL frame in the capture
tshark -r capture.pcap -Y 'zbee_zcl'
# only the OTA cluster, and pull structured fields out of it
tshark -r capture.pcap -Y 'zbee_zcl_general.ota' \
-T fields -e zbee_zcl_general.ota.file.offset \
-e zbee_zcl_general.ota.image.data
# attribute reports on a temperature sensor
tshark -r capture.pcap -Y 'zbee_zcl.cmd.id == 0x0a && zbee_aps.cluster == 0x0402'
In scapy, ZigbeeClusterLibrary parses the header and the general commands; manufacturer-specific payloads come back as raw bytes and you decode them yourself.
Pitfalls
- The cluster identifier is only meaningful together with the profile identifier. 0x1000 under profile 0xC05E is ZLL commissioning; the same number under Home Automation is not.
- Wireshark shows a manufacturer-specific frame's payload undissected. That is not a broken capture, it is a vendor extension, and the bytes are still yours to read.
- A cluster-specific command identifier repeats across clusters. Always read the cluster first, then the command.
- The OTA cluster carries firmware in Image Block Responses but no tool reassembles them for you: bucket by file version, index by file offset, drop the duplicates.