Capturing Insecure IoT Traffic: ESP32 Lab with Wireshark

More than 70% of deployed IoT devices transmit sensitive data without encryption. This lab makes the consequence of that concrete: you will run an insecure HTTP server on an ESP32, capture its traffic in Wireshark, and watch passwords, API keys and device credentials appear in plain text in under 30 seconds — no hacking skills required. The goal is not to learn how to attack IoT devices but to understand, viscerally, why encryption is not optional. This is Lab 3 of the Embedded Systems Security course series and pairs directly with Lab 6, which implements the secure version of the same application over MQTT with TLS.
Why Unencrypted HTTP Is Dangerous on IoT Networks
HTTP transmits every byte of every request and response in plaintext. There is no encryption, no integrity protection and no replay protection. Anyone on the same network — a shared WiFi network in an office, a campus network, a home network with multiple devices — can run Wireshark or tcpdump and read everything. The threat model is not exotic: it does not require intercepting packets in transit between data centres. It requires only being on the same WiFi network as the IoT device.
The specific problem this lab demonstrates is credentials in GET request parameters. When a login form uses the GET method, the username and password appear in the URL: GET /login?username=admin&password=password123. That URL is logged by every intermediary, stored in browser history, sent to analytics services embedded in the page and visible to any packet capture tool. This is one of the oldest and best-documented vulnerabilities in web security, yet IoT devices continue to ship with HTTP-only admin interfaces.
Equipment and Prerequisites
You need an ESP32 DOIT DevKit V1, a USB cable, a computer with Wireshark installed, and a WiFi network where the ESP32 and your computer are on the same subnet. You do not need to be on the same physical cable — WiFi monitoring is sufficient because Wireshark captures at the host’s network interface level, seeing all traffic to and from your computer including responses from the ESP32. The required Arduino libraries are WiFi.h and WebServer.h, both built into the ESP32 Arduino core.
Complete Arduino Code: Insecure HTTP Server
The code runs an HTTP server on port 80 with hardcoded admin credentials and an API key. The login form uses GET method, placing credentials directly in the URL. The dashboard response returns the API key, admin password, WiFi SSID and device location in the response body. This faithfully replicates the architecture of many real IoT device admin interfaces.
/*
* INSECURE HTTP Web Server - Credential Exposure Demo
* WARNING: This deliberately demonstrates INSECURE practices.
* All vulnerabilities are intentional for educational purposes.
*
* Hardware: ESP32 DOIT DevKit V1
*/
#include <WiFi.h>
#include <WebServer.h>
// === CONFIGURATION - change to your network ===
const char* ssid = "YOUR_WIFI_SSID";
const char* wifi_password = "YOUR_WIFI_PASS";
// === CREDENTIALS (hardcoded - NEVER do this in production) ===
const char* admin_user = "admin";
const char* admin_pass = "password123";
const char* api_key = "sk-abc123def456ghi789";
// === DEVICE STATE ===
String deviceID = "ESP32_SENSOR_001";
String location = "Building A, Room 305";
float temperature = 25.5;
float humidity = 60.0;
WebServer server(80); // Port 80 = plaintext HTTP — insecure!
// === LOGIN PAGE ===
const char* loginPage = R"rawl(
<!DOCTYPE html><html><head><title>IoT Device Login</title></head>
<body style="font-family:Arial;margin:50px;background:#667eea;">
<div style="background:white;padding:40px;max-width:400px;margin:auto;border-radius:10px;">
<h2>IoT Device Login</h2>
<div style="background:#fff3cd;padding:15px;margin:15px 0;border-left:4px solid #ffc107;">
<strong>SECURITY DEMO:</strong> This connection is UNENCRYPTED!
All data sent in PLAINTEXT — visible to anyone on the network.
</div>
<form action="/login" method="GET">
<input type="text" name="username" placeholder="Username" style="width:100%;padding:10px;margin:5px 0;box-sizing:border-box;"><br>
<input type="password" name="password" placeholder="Password" style="width:100%;padding:10px;margin:5px 0;box-sizing:border-box;"><br>
<button type="submit" style="width:100%;padding:12px;background:#667eea;color:white;border:none;border-radius:5px;">Login</button>
</form>
<p style="color:#666;font-size:13px;text-align:center;">Hint: admin / password123</p>
</div></body></html>
)rawl";
// === HANDLERS ===
void handleRoot() {
server.send(200, "text/html", loginPage);
}
void handleLogin() {
// Credentials arrive in URL query string - fully visible in Wireshark
String user = server.arg("username");
String pass = server.arg("password");
Serial.println("Login attempt - User: " + user + " Pass: " + pass);
if (user == admin_user && pass == admin_pass) {
// Build dashboard with all secrets in the response body
String page = "<html><body style='font-family:Arial;margin:30px;'>";
page += "<h2>Device Dashboard</h2>";
page += "<div style='background:#f8d7da;padding:15px;border-left:4px solid #dc3545;'>";
page += "<strong>INSECURE SESSION</strong> - No HTTPS, no session token, plaintext only.</div>";
page += "<p>Temperature: " + String(temperature) + " C</p>";
page += "<p>Humidity: " + String(humidity) + "%</p>";
page += "<p>Location: " + location + "</p>";
page += "<p>Device ID: " + deviceID + "</p>";
// Secrets deliberately included in response to demonstrate exposure
page += "<div style='background:#fff3cd;padding:15px;border:2px dashed #ffc107;'>";
page += "<strong>EXPOSED SECRETS (visible in network traffic):</strong><br>";
page += "API Key: " + String(api_key) + "<br>";
page += "Admin User: " + String(admin_user) + "<br>";
page += "Admin Pass: " + String(admin_pass) + "<br>";
page += "WiFi SSID: " + String(ssid) + "</div>";
page += "</body></html>";
server.send(200, "text/html", page);
} else {
server.send(401, "text/plain", "Invalid credentials");
}
}
void handleAPIData() {
// JSON endpoint - also unencrypted
String json = "{";
json += "\"temperature\":" + String(temperature) + ",";
json += "\"humidity\":" + String(humidity) + ",";
json += "\"device_id\":\"" + deviceID + "\",";
json += "\"api_key\":\"" + String(api_key) + "\","; // Exposed!
json += "\"admin_pass\":\"" + String(admin_pass) + "\""; // Exposed!
json += "}";
server.send(200, "application/json", json);
}
void handleControl() {
String action = server.arg("action");
Serial.println("Control action: " + action);
// No authentication check - any request succeeds!
server.send(200, "text/plain", "Action '" + action + "' executed (no auth required!)");
}
void setup() {
Serial.begin(115200);
delay(2000);
Serial.println("\n=== INSECURE HTTP Server Demo ===");
Serial.println("Connecting to WiFi: " + String(ssid));
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, wifi_password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("\nConnected! IP: " + WiFi.localIP().toString());
Serial.println("\nOpen browser: http://" + WiFi.localIP().toString());
Serial.println("Credentials: admin / password123");
Serial.println("\nStart Wireshark with filter: http");
Serial.println("Log in and watch your credentials appear in packets!");
server.on("/", handleRoot);
server.on("/login", handleLogin);
server.on("/api/data", handleAPIData);
server.on("/control", handleControl);
server.begin();
}
void loop() {
server.handleClient();
}
Wireshark Setup and Capture Filters
Open Wireshark and select your network interface. For WiFi, this is typically labelled “Wi-Fi” on Windows or “wlan0” on Linux. Start the capture by clicking the blue shark fin icon, then apply a display filter in the filter bar to narrow the view to HTTP traffic:
# Show only HTTP traffic
http
# Alternative: show traffic on port 80 (includes non-HTTP)
tcp.port == 80
# Find packets containing the word "password"
http contains "password"
# Filter to the ESP32's IP only (replace with actual IP from Serial Monitor)
ip.addr == 192.168.1.100
# Find all HTTP GET requests
http.request.method == "GET"
The most useful filter for this lab is http contains "password" — this highlights every packet where the word “password” appears anywhere in the HTTP payload, including GET parameters, POST bodies and response HTML.
Generating Traffic for Capture
With Wireshark capturing and the HTTP filter active, open a browser on the same computer and navigate to the ESP32’s IP address shown in Serial Monitor. You will see the login page. Step 1: enter the credentials (admin / password123) and click Login. Step 2: on the dashboard, click the Reboot, Lock and Unlock control buttons. Step 3: visit the API endpoint directly by navigating to http://[ESP32_IP]/api/data. Each of these actions generates HTTP packets. Then examine what Wireshark captured.
Analysing the Captured Packets
In the Wireshark packet list, find a GET request to /login. The Info column will show the full URI including the query string. Click on the packet to see the full details, then right-click and choose “Follow → HTTP Stream” to see the entire HTTP conversation reconstructed as readable text.
What you will see in the HTTP stream:
GET /login?username=admin&password=password123 HTTP/1.1
Host: 192.168.1.100
Connection: keep-alive
User-Agent: Mozilla/5.0...
HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 2847
...API Key: sk-abc123def456ghi789
Admin User: admin
Admin Pass: password123
WiFi SSID: YourNetworkName...
Everything is visible: the credentials from the login request in the URL parameters, and the API key and admin password again in the response body. Use Edit → Find Packet and search for “password” to highlight every packet in the capture where a password appears — there will be multiple, including in the dashboard response HTML.
To demonstrate the replay attack: copy the full login URL from the Wireshark request (right-click the packet, “Copy → Value” from the URI field), paste it into a new browser tab and press Enter. The login succeeds without re-entering the password, because there is no session management or nonce — the URL alone is sufficient to authenticate.
Four Attack Scenarios This Enables
Credential theft. The attacker on the same WiFi network captures the login packet and extracts the admin username and password. They can now log in to the device at any time, change its configuration, execute control actions and monitor all its data.
API key theft. The API key visible in the dashboard response is typically used to authenticate the device to a cloud service. With the API key, the attacker can impersonate the device to the cloud backend: read its historical data, inject false sensor readings, disable alerts or access the billing account the key is associated with.
Location tracking. The device broadcasts its physical location in the response body. For a security camera, smart lock or industrial asset tracker, this gives an attacker the physical location of the device and the facility it monitors.
Full device control without credentials. The /control endpoint executes actions (reboot, lock, unlock) based on the GET parameter alone, with no authentication check. An attacker who has captured one control request can replay it, or craft new requests, without ever knowing the admin password.
HTTP vs. HTTPS: What Changes
| Aspect | HTTP (This Lab) | HTTPS / TLS (Lab 6) |
|---|---|---|
| Credentials in transit | Plaintext — fully readable in Wireshark | Encrypted — Wireshark shows binary ciphertext |
| API keys in response | Visible in any packet capture | Encrypted in TLS record |
| Sensor data | Readable plaintext | Encrypted |
| Man-in-the-middle | Trivial — read and modify without detection | Prevented by certificate validation |
| Replay attack | Any captured URL can be replayed | TLS session binds authentication to connection |
| Wireshark output | Complete plaintext conversation readable | TLS handshake visible; application data unreadable |
| Production suitability | Never | Always |
Discussion Questions
What information did we capture from a single login? Username, password, API key, admin credentials again in the response, device location, device ID and WiFi network name. All from one 30-second interaction with the browser.
Would POST be safer than GET? Slightly: POST parameters do not appear in the URL and are not stored in browser history. But over HTTP, POST body is equally visible in Wireshark — it is still plaintext. HTTPS is the only solution. HTTP POST is not meaningfully safer than HTTP GET against a network-level attacker.
How does an attacker end up on the same network? More easily than you might expect. Many IoT deployments use shared office or building WiFi. Devices at trade shows, in retail environments and in industrial facilities are on networks shared with many other parties. An attacker does not need physical proximity to a specific device — they need access to any part of the shared network infrastructure.
Why is replay attack possible? There is no session token, no CSRF protection and no nonce. The login URL alone is sufficient to authenticate because the server performs a stateless credential check on every request. TLS does not by itself prevent replay — you also need server-side session management with expiring tokens. Lab 6 covers both.
Key Takeaways
HTTP transmits everything in plaintext: passwords, API keys, sensor data, control commands and device location. Credentials in URL parameters are stored in browser history, server logs, analytics trackers and packet captures. Anyone on the same network can capture traffic with free tools in under a minute. This is how the majority of real-world IoT device compromises happen — not through sophisticated exploits but through trivial credential capture on unencrypted HTTP. HTTPS is not optional and not a nice-to-have: it is the minimum acceptable baseline for any IoT device that handles authentication or sensitive data. Lab 6 in this series implements the secure version and shows what Wireshark sees when TLS is in use.