This is an old revision of the document!


Lab 8. Security in IoT

Breaking an insecure IoT sensor over HTTP

By the end of this activity, you will be able to flash and run ESP32-C6 firmware that connects to Wi-Fi, periodically sends JSON data over plain HTTP to a server, and intentionally leaks an API key in the HTTP headers.

You will use Wireshark to capture this HTTP traffic and closely inspect the URL, payload, and exposed secrets in cleartext. In addition, you will write or modify a Python script that spoofs sensor data and sends it via HTTP POST to the same endpoint, giving you an understanding of how insecure data transmission can be observed and exploited.

Platformio Setup

Use this standard .ini file for your project:

platformio.ini
[env:sparrow_http_lab]
platform = https://github.com/pioarduino/platform-espressif32/releases/download/54.03.20/platform-espressif32.zip
board = esp32-c6-devkitm-1
framework = arduino
 
monitor_speed = 115200
 
build_flags =
  -D ARDUINO_USB_MODE=1
  -D ARDUINO_USB_CDC_ON_BOOT=1

Insecure HTTP Sensor Node

Let's write a (horribly unsafe) but functional code that connects to a HTTP server and sends periodically sensor data. This is the “bad” example in which we are using plaintext messaging over an unsecured connection.

Get the code from here.

Simple Flask Server

We'll create a simple HTTP server in Python that will display the packages received from the Sparrow node and their payload.

server.py
# server.py
from flask import Flask, request
 
app = Flask(__name__)
 
@app.route("/ingest", methods=["POST"])
def ingest():
    print("=== New request ===")
    print("Client IP:", request.remote_addr)
    print("Headers:")
    for k, v in request.headers.items():
        print(f"  {k}: {v}")
    print("Body:", request.data.decode("utf-8", errors="ignore"))
    print("===================")
    return "OK\n"
 
if __name__ == "__main__":
    # Listen on all interfaces, port 8080
    app.run(host="0.0.0.0", port=8080) #replace the IP address with the server's IP

Set your computer's IP address as host and run the script on your machine by:

pip install flask
python server.py

Wireshark Capture

Use Wireshark to capture network traffic. Start Wireshark on the interface used for lab Wi-Fi/LAN and use a simple display filter such as: tcp.port == 8080 or filter by server IP: ip.addr == 192.168.1.100 && tcp.port == 8080

You should find a full HTTP request line: POST /ingest HTTP/1.1. with request headers: Host, User-Agent, Content-Type and X-API-Key: SUPER_SECRET_API_KEY_123 – this is the “secret”.

Also, HTTP body = JSON payload, fully readable:

{"device_id":"sparrow-01","temp_c":27.43,"humidity":55.7,"battery":88}

By simply sniffing on network traffic you can get all sensor information and data. Next step is to spoof sensor readings by impersonating the ESP32 Sparrow node.

Spoofing / Data Injection

This is the “attack” part in which we use captured data to impersonate the node. We will run a python script on our machine that matches the API key and JSON formatting. Use the script below, modifying only the server IP.

attack.py
# attack.py
import requests
import json
 
SERVER_URL = "http://192.168.1.100:8080/ingest"  # same as device
API_KEY    = "SUPER_SECRET_API_KEY_123"          # copied from Wireshark
DEVICE_ID  = "sparrow-01"                        # same as real device
 
def main():
    fake_payload = {
        "device_id": DEVICE_ID,
        "temp_c": 999.9,     # clearly fake, "overheating"
        "humidity": 0.0,
        "battery": 5
    }
 
    headers = {
        "Content-Type": "application/json",
        "X-API-Key": API_KEY
    }
 
    print("Sending spoofed measurement...")
    print("POST", SERVER_URL)
    print("Headers:", headers)
    print("Body:", json.dumps(fake_payload))
 
    r = requests.post(SERVER_URL, headers=headers, json=fake_payload)
    print("Status code:", r.status_code)
    print("Response:", r.text)
 
if __name__ == "__main__":
    main()

To run the attack script:

pip install requests
python attack.py

What you can observe is the backend prints both real ESP32 data and spoofed data — no way to distinguish. Wireshark shows identical HTTP requests except for payload values.

Secure Connection with HTTPS and Message Integrity

Let's modify the device to use HTTPS instead of HTTP and add a shared secret + simple MAC (message authentication code) to detect tampering. This will prevent passive sniffers from reading the payload and there will also be a complete failure of injecting spoofed data to the server.

First, let's generate a self-signed cert for Flask. In the same folder you have the two previous python scripts, run:

openssl req -x509 -newkey rsa:2048 -keyout server.key -out server.crt -days 365 -nodes \
  -subj "/CN=iot-lab.local"

This gives you server.crt and server.key in the current directory.

Now, let's upgrade our server with HTTPS instead of HTTP and a check function for message authentication codes (MAC). The new server script is below:

secure_server.py
# secure_server.py
from flask import Flask, request, abort
import json
 
app = Flask(__name__)
 
SECRET_KEY = "LAB2_SUPER_SECRET_MAC_KEY"  # this is shared only between the node and the server
 
def compute_mac(payload: str) -> int:
    """
    Very simple (non-crypto!) MAC: hash(payload + SECRET_KEY)
    This is just for teaching integrity, not real security.
    """
    data = (payload + SECRET_KEY).encode("utf-8", errors="ignore")
    h = 0
    for b in data:
        h = (h * 31) ^ b
        h &= 0xFFFFFFFF  # keep as 32-bit
    return h
 
@app.route("/ingest", methods=["POST"])
def ingest():
    raw_body = request.data.decode("utf-8", errors="ignore")
    print("=== New HTTPS request ===")
    print("Client IP:", request.remote_addr)
    print("Headers:")
    for k, v in request.headers.items():
        print(f"  {k}: {v}")
    print("Raw body:", raw_body)
 
    try:
        data = json.loads(raw_body)
    except json.JSONDecodeError:
        print("!! Invalid JSON, rejecting")
        abort(400, "Invalid JSON")
 
    # Expect fields: device_id, temp_c, humidity, battery, mac
    if "mac" not in data:
        print("!! Missing MAC, rejecting")
        abort(400, "Missing MAC")
 
    received_mac = int(data["mac"])
    # Compute MAC over body without the mac field
    check_obj = dict(data)
    del check_obj["mac"]
    # Use a canonical payload string for MAC computation
    mac_payload = json.dumps(check_obj, sort_keys=True, separators=(",", ":"))
    expected_mac = compute_mac(mac_payload)
 
    print(f"Received MAC: {received_mac}")
    print(f"Expected MAC: {expected_mac}")
 
    if received_mac != expected_mac:
        print("!! MAC mismatch, possible tampering, rejecting")
        abort(403, "Invalid MAC")
 
    print("MAC OK, accepting data:", check_obj)
    print("=========================")
    return "OK\n"
 
if __name__ == "__main__":
    # HTTPS on 8443, replace IP with your own server's address
    app.run(
        host="0.0.0.0",
        port=8443,
        ssl_context=("server.crt", "server.key")
    )

Run it and now the server has moved from http:…:8080/ingest to https:…:8443/ingest and expects a MAC.

Now, let's upgrade also the code on the node itself. Key changes we will need to make are URL is now https://...:8443/ingest, we use WiFiClientSecure + setInsecure() (so: encrypted but not properly authenticated), and we add a mac field based on the same simple MAC function as the server.

Get the new main.cpp code from here.

Wireshark Sniffing

Now, start a new capture while Sparrow sends to HTTPS. Filter: tcp.port == 8443 or ip.addr == <server-ip> && tcp.port == 8443 and look if you still see TCP + TLS records (Client Hello, Server Hello, etc). The key difference now is that we see no readable JSON or headers in the middle of the transport. There is no way of directly finding the secret API key now.

Attack Script

Let's now try to attack the server by injecting again false data.

  1. First try: re-run the attack script, changing only the port from 8080 to 8443 and HTTPS instead of HTTP. You will see messages being rejected by the server as they don't have the right MAC: server returns 400 (“Missing MAC”) because it enforces MAC presence.
  2. Second try: add a mac: field in the attack script and give it a constant value, such as 1234. What you will see now is the server returning a mismatch → 403 “Invalid MAC”.
  3. Third try: compute the mac in the attack script (this implies you have hacked the source code on the node or on the server and found out the algorithm used for computing the mac. Try the attack script now, it should again inject wrong data without being detected.

Man-in-the-Middle (MITM) with ''setInsecure()''

This example shows why using HTTPS without certificate validation is dangerous. We will:

  • put an attacker (mitmproxy) between the ESP32 and the Flask server
  • watch “encrypted” traffic in clear
  • see that the ESP32 happily trusts the attacker because of setInsecure()

Prerequisites

You should already have:

  • secure_server.py running on the lab server
  • ESP32 code from using:
    • WiFiClientSecure secureClient;
    • secureClient.setInsecure();
  • all devices on the same network

One extra machine will act as the attacker.

Step 1 – Start the real HTTPS server (as before)

On the server machine:

python secure_server.py
  • keep this running in a terminal
  • note the server IP, e.g. 192.168.0.104

Step 2 – Start mitmproxy in reverse mode (attacker machine)

On the attacker machine:

  • install mitmproxy once:
pip install mitmproxy
  • then run mitmproxy as a reverse proxy:
mitmproxy --mode reverse:https://192.168.0.104:8443 -p 8444

Adjust the IP to your real server IP.

What this does:

  • listens on :8444 on the attacker machine
  • accepts HTTPS from the ESP32
  • decrypts everything (using a fake cert)
  • opens a new HTTPS connection to the real server
  • forwards the traffic

Step 3 – Point the ESP32 to the attacker instead of the server

We are pretending that the device was tricked into talking to the attacker instead of the real server (e.g. via DNS or ARP spoofing).

While we're not going to do ARP poisoning here, there are a multitude of tutorials available on how to do this, for example this one.

We are going to pretend we have been a victim of a MITM attack and the ESP32 connection to the server has been rerouted through ARP spoofing to an attacker machine. For this, on the ESP32 code (main.cpp), change only the URL:

// OLD
// const char* SERVER_URL = "https://192.168.0.104:8443/ingest";
 
// NEW: attacker IP + mitmproxy port
const char* SERVER_URL = "https://192.168.0.200:8444/ingest";  // example
  • use the attacker machine IP and port ``8444``
  • keep:
secureClient.setInsecure();  // do NOT change this yet

Rebuild and flash the ESP32.

Step 4 – Observe what happens

On the ESP32 serial monitor:

  • everything looks normal
  • HTTPS POSTs succeed
  • responses are “OK”

On the real Flask server:

  • you still see valid requests
  • MAC checks still pass
  • the server has no idea there is an attacker in the middle

On mitmproxy (attacker UI):

  • you see each request in clear text
  • method: POST /ingest
  • headers: Content-Type: application/json etc.
  • full JSON body, including the MAC:
{"battery":83,"device_id":"sparrow-01","humidity":52.1,"temp_c":28.17,"mac":3171032033}

So, transport is HTTPS but the attacker can read everything because the device accepted the attacker's certificate without checking!

Step 5 – Try to tamper with the data

In mitmproxy:

  • pick one request from the ESP32 to /ingest
  • press e to edit
  • choose request body
  • change e.g.: “temp_c”:28.17“temp_c”:999.99
  • do not change “mac”

Forward the modified request.

On the Flask server:

  • the request should be rejected
  • HTTP 403 “Invalid MAC” (MAC mismatch)

This shows:

  • the attacker can still *see* everything
  • but cannot silently change values without also knowing the MAC key

Takeaway

  • Using HTTPS without validating certificates is almost as bad as using no HTTPS at all.
  • If an attacker can redirect traffic (DNS, ARP, rogue AP), and your device uses setInsecure(), they can sit in the middle:
    • read all messages
    • block or replay them
    • try to tamper with them

Next steps:

  • replace setInsecure() with proper certificate pinning
  • add timestamps / nonces to defend against replay attacks
  • think about where and how to store keys on the device securely

Trusted Certificates

At a high level, to stop MITM you need the ESP32 to trust only a specific certificate (or CA) and to check that the server it’s talking to presents that particular certificate.

That means we can't relay anymore on setInsecure() and load a CA / server cert into the firmware instead. We will the use WiFiClientSecure verify it.

Generate a CA

Instead of a random self-signed server cert, you create a tiny lab CA and have it sign your server cert. On the server machine delete the old server.crt and server.key and generate the following:

# 1) Create CA key + cert
openssl genrsa -out ca.key 2048
openssl req -x509 -new -key ca.key -out ca.crt -days 365 -subj "/CN=IoT Lab CA"
 
# 2) Create server key + CSR
openssl genrsa -out server.key 2048
openssl req -new -key server.key -out server.csr -subj "/CN=iot-lab.local"
 
# 3) Sign server cert with CA
openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out server.crt -days 365

Now you have the following files:

  • ca.crt – trust anchor you’ll embed in the ESP32
  • server.key + server.crt – used by Flask
  • CN of server cert is iot-lab.local (hostname you’ll use in the URL)

Run Flask again:

python secure_server.py  # same as before, just point it to server.crt/server.key

Modify the Firmware to Add the Certificate

Get the new modified code from here and paste the contents of the ca.crt file into the main.cpp file.

iothings/laboratoare/2025/lab8.1763205741.txt.gz · Last modified: 2025/11/15 13:22 by dan.tudose
CC Attribution-Share Alike 3.0 Unported
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0