Module 09 — Credential Sniffing (Attack / Understand / Fix)
Objective
Capture plaintext credentials from HTTP traffic on the internal network using tcpdump, understand why SSL/TLS matters, then secure external access with HTTPS via Tailscale Funnel.
By the end of this module you will have:
| Outcome | Detail |
|---|---|
| Captured | Plaintext passwords and customer PII from network traffic |
| Understood | Why HTTP exposes everything to anyone on the same network |
| Fixed | External access secured with HTTPS via Tailscale Funnel |
| Acknowledged | Internal traffic gap and the plan to address it in Module 11 |
Prerequisites
- Module 08 complete (SQL injection fixed)
- App running and accessible via Nginx at http://192.168.56.13
- Tailscale Funnel configured (from Module 07)
- SSH access to web-server and app-server
1. Introduction — From application to network
In Module 08 you attacked the application code. Now you are going to attack the network.
All traffic between your browser, Nginx, and the Go backend is unencrypted HTTP. This means anyone who can see network traffic can read everything — including passwords, session cookies, and customer data.
You do not need to find a bug in the code. You just need to be on the same network and run a packet capture tool.
2. CTF Challenge
Challenge: SSH into web-server. Using only command-line tools, capture the admin password by watching network traffic while someone logs in. You have 20 minutes.
Rules:
- You can only use tools available on the web-server (or that you install)
- You need two terminal windows: one to capture, one to trigger the login
- Your goal: see the plaintext password in the captured output
If you get stuck, reveal the hints below one at a time:
Hint 1
The tool tcpdump can capture network packets. Install it with sudo apt install -y tcpdump.
Hint 2
HTTP traffic on the Host-Only network uses interface enp0s8. You can verify with ip addr.
Hint 3
Try filtering for port 80 and look for POST data: sudo tcpdump -i enp0s8 -A port 80
Once you have tried the challenge (or after 20 minutes), continue to the guided attacks below.
3. Guided Attack — Sniff HTTP traffic with tcpdump
You need two terminal windows open simultaneously.
Terminal 1 — Start capture on web-server
ssh web-server
sudo apt install -y tcpdump
sudo tcpdump -i enp0s8 -A port 80 2>/dev/null | grep -E "password|username|Password|Username"
This command:
-i enp0s8— listens on the Host-Only network interface-A— prints packet contents in ASCII (human-readable)port 80— filters for HTTP traffic onlygrep— shows only lines containing credential-related keywords
Leave this running.
Terminal 2 — Login to the app (from your Mac)
curl -X POST http://192.168.56.13/api/login \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"admin123"}'
Expected output in Terminal 1
Switch back to Terminal 1. You should see output like:
{"username":"admin","password":"admin123"}
The JSON body containing the admin password appears in plaintext in the capture. Anyone running tcpdump on this network just stole the admin credentials.
Press Ctrl+C to stop the capture.
4. Guided Attack — Sniff traffic between Nginx and Go backend
The Nginx reverse proxy on web-server forwards requests to the Go backend on app-server over HTTP. That hop is also unencrypted.
Terminal 1 — Capture on app-server (port 8080)
ssh app-server
sudo apt install -y tcpdump
sudo tcpdump -i enp0s8 -A port 8080 2>/dev/null | grep -E "password|username"
Terminal 2 — Login again via Nginx
curl -X POST http://192.168.56.13/api/login \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"admin123"}'
The same credentials appear in the capture on app-server. The password is visible on both network hops.
Press Ctrl+C to stop the capture.
Bonus — Capture customer PII
Start a new capture filtering for customer data:
Terminal 1 — Capture customer data on app-server
sudo tcpdump -i enp0s8 -A port 8080 2>/dev/null | grep -E "name|email|phone"
Terminal 2 — List customers
curl -b cookies.txt http://192.168.56.13/api/customers
All customer data — names, email addresses, phone numbers, addresses — appears in plaintext in the capture. Every piece of PII your application handles is visible to anyone sniffing the network.
5. Why This Works
HTTP is plaintext
HTTP sends everything as plain text. There is zero encryption. The protocol was designed in 1991 when the internet was a trusted academic network. It was never designed to protect data in transit.
The attack surface
Anyone on the same network segment can capture traffic. On the Host-Only network (192.168.56.0/24), all three VMs share the same Layer 2 segment. Any VM can see traffic destined for any other VM.
Real-world equivalents:
- Shared WiFi at a coffee shop or hotel
- A compromised network switch with port mirroring
- A man-in-the-middle attack via ARP spoofing
- A rogue device on a corporate network
What is exposed
| Data type | Example from capture |
|---|---|
| Login credentials | {"username":"admin","password":"admin123"} |
| Session cookies | Cookie: session=abc123... |
| Customer names | "name":"John Smith" |
| Email addresses | "email":"john@example.com" |
| Phone numbers | "phone":"555-0100" |
| Addresses | "address":"123 Main St" |
The Tailscale Funnel gap
Even with Tailscale Funnel from Module 07, the funnel only encrypts traffic from the browser to the web-server. Internal traffic between VMs is still plaintext:
Browser --[HTTPS]--> Tailscale Funnel --[encrypted]--> web-server --[HTTP]--> app-server
^^^^^^^^^^^^^^^^^^^^^^^
This part is plaintext
6. The Fix — HTTPS via Tailscale Funnel
You cannot easily add TLS certificates to the internal network in a training environment. Instead, you will secure external access by using Tailscale Funnel, and acknowledge the internal gap.
Step A — Tailscale Funnel serves HTTPS by default
Tailscale Funnel provides HTTPS automatically — no manual SSL configuration needed. When you enable Funnel on the web-server, it:
- Obtains and manages TLS certificates automatically
- Always serves HTTPS — HTTP requests are redirected to HTTPS
- Handles certificate renewal without intervention
No dashboard configuration is required. Funnel is secure by default.
Now all public access to your app is encrypted from the browser to the web-server via Tailscale.
Step B — Understand the remaining gap
After Step A, the encryption looks like this:
| Segment | Encrypted? | How |
|---|---|---|
| Browser to Tailscale Funnel | Yes | HTTPS (TLS certificate from Tailscale) |
| Tailscale Funnel to web-server | Yes | Encrypted via Tailscale |
| Nginx to Go backend (web-server to app-server) | No | Still plaintext HTTP |
The internal traffic between Nginx and the Go backend is still unencrypted. In production, you would fix this with:
- Internal TLS between services — each service gets its own certificate
- A service mesh (like Istio or Linkerd) — handles mutual TLS automatically
- Network segmentation — restrict who can see internal traffic
For this training, you will address the internal gap in Module 11 by using firewall rules to restrict which machines can communicate with each other. This does not encrypt the traffic, but it limits who can sniff it.
Step C — Note on Nginx configuration
Tailscale Funnel connects to localhost:80 on the web-server, so external traffic already arrives through the encrypted funnel. In Module 11 (firewalls), you will add rules so that Nginx on port 80 only accepts connections from localhost (the funnel) and rejects direct access from other machines on the network.
7. Verify
Verify external HTTPS works
Open your Tailscale Funnel URL in a browser. Confirm:
- The URL starts with
https:// - The browser shows a lock icon (valid TLS certificate)
- Accessing
http://redirects tohttps://
Verify tcpdump no longer captures credentials from external access
Terminal 1 — Start capture on web-server:
ssh web-server
sudo tcpdump -i enp0s8 -A port 80 2>/dev/null | grep -E "password|username"
Terminal 2 — Login via the Tailscale Funnel HTTPS URL:
curl -X POST https://YOUR-TUNNEL-DOMAIN/api/login \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"admin123"}'
Expected: No plaintext credentials appear in the tcpdump output on enp0s8. The traffic from the browser arrives through Tailscale Funnel, which terminates on localhost — it never crosses the Host-Only network interface.
Acknowledge the internal gap
Run the capture on app-server (port 8080) and login via the HTTPS URL. You will still see plaintext on the internal hop between Nginx and the Go backend. This is expected. The firewall rules in Module 11 will restrict access to this internal traffic.
Troubleshooting
| Problem | Solution |
|---|---|
tcpdump: permission denied | Use sudo. tcpdump requires root privileges to capture packets. |
| No packets captured | Wrong interface. Run ip addr to check — look for the interface with a 192.168.56.x address. |
| Tailscale Funnel not serving HTTPS | Verify Funnel is running with tailscale funnel status. Restart if needed. |
| Still seeing plaintext on internal network | Expected. The fix in this module only covers external traffic. Internal traffic is addressed in Module 11. |
tcpdump: command not found | Install it: sudo apt install -y tcpdump |
Summary
| What you did | What you learned |
|---|---|
| Captured login credentials with tcpdump | HTTP is plaintext — anyone on the network can read everything |
| Captured customer PII from API responses | It is not just passwords — all data is exposed |
| Sniffed traffic between Nginx and backend | Every internal hop without TLS is a risk |
| Enabled HTTPS via Tailscale Funnel | External traffic is now encrypted end-to-edge |
| Identified the internal traffic gap | Defense in depth requires multiple layers (TLS + firewalls) |
The one rule to remember: Never send sensitive data over an unencrypted connection. If the network is not encrypted, assume someone is watching.