📍 Journey Navigation Prev: Day 3 — From Theory to Packets · Next: Day 5 — HTTP, Reverse Shells & Three Minds | Day 1 | Day 3 | Day 5 | Day 6 |
Status: Day 4 of 90-day AI Security journey
Where Yesterday Left Off
Yesterday I built the lab, saw TCP handshakes in Wireshark, and watched HTTP data flow in plaintext. That was the moment theory became real.
Today I went deeper. Six labs. Back to back. By the end I had scanned a live server, fingerprinted its OS, blocked all access with a firewall, and understood exactly why intrusion detection systems exist.
Here is everything that happened.
The pfSense Problem — And Why I Dropped It Fast
Before the labs started, I had planned to use pfSense as the lab firewall and router. pfSense is the industry standard for open source firewall labs — it has a full GUI dashboard, built-in VPN, traffic logging, IDS integration, and is what most professional lab guides recommend.
I downloaded the ISO, created the VM in VMware Fusion, and tried to boot it.
It did not boot.
After a few minutes of debugging I found the reason: pfSense is built for x86_64 architecture (Intel/AMD). Mac M4 is ARM. VMware Fusion on Apple Silicon only runs ARM virtual machines. pfSense has no ARM build. There is no workaround, no compatibility layer, no config change that fixes this. It is a hard architectural wall.
I made a decision immediately: do not waste time trying to force something that cannot work. Move on.
The alternative was actually better for learning. Instead of a GUI that abstracts everything, I built the firewall from scratch on Ubuntu using iptables — the same packet filtering engine that sits underneath pfSense, ufw, and virtually every Linux-based firewall in production. No GUI. Just commands, rules, and direct kernel interaction.
# The core of what I built on the Ubuntu router VM:
echo 1 | sudo tee /proc/sys/net/ipv4/ip_forward
sudo iptables -t nat -A POSTROUTING -o ens160 -j MASQUERADE
sudo iptables -A FORWARD -i ens256 -o ens160 -j ACCEPT
sudo iptables -A FORWARD -i ens160 -o ens256 -m state \
--state RELATED,ESTABLISHED -j ACCEPT
Four commands. That is a functioning NAT router and stateful firewall. pfSense does exactly this under the hood — it just wraps it in a web interface.
The constraint forced a deeper understanding than the easy path would have given. That is a recurring theme in this journey.
Lab 1 — Network Basics: Mapping My Own Machine
Before attacking anything, I needed to understand how my own machine sees the network. This sounds simple. It is not.
ip a # all interfaces and IPs
hostname -I # quick IP lookup
ping 127.0.0.1 # loopback test
ping 192.168.100.1 # router reachability
arp -a # who has been resolved on LAN
ip route # routing table — where does traffic go
The ARP table was the first real insight. It shows every IP-to-MAC mapping your machine has learned — a record of every device it has spoken to on the LAN recently. Before this lab I knew ARP existed. After seeing it live I understood why ARP spoofing works: there is no authentication, no verification, no challenge. Your machine just believes whatever it is told. (I would later exploit this exact weakness in Day 6's MITM labs.)
The routing table was the second insight. One line does all the work:
default via 192.168.100.1 dev eth0
Everything not on your local subnet goes to that address. The router. If that entry is wrong or missing, you have no internet. Simple as that.
Lab 2 — Ports & Services: What Is Actually Listening
This lab was about understanding service exposure — which services are running, what port they are on, and who can reach them.
ss -tulnp # show all listening ports with process names
The output maps every open port to the process that owns it. Port 8080 → python3. Port 22 → sshd. This is exactly the same information an attacker collects during reconnaissance.
Then I tested the most important distinction in network security:
Binding to 127.0.0.1 vs 0.0.0.0
# Visible only to the machine itself:
python3 -m http.server 8080 --bind 127.0.0.1
# Visible to the entire network:
python3 -m http.server 8080 --bind 0.0.0.0
When bound to 127.0.0.1, Kali cannot reach it. When bound to 0.0.0.0, Kali reaches it instantly.
This is how databases get compromised. MySQL bound to 0.0.0.0 on a server with a weak firewall is not a database — it is an open invitation. The difference between those two bind addresses is the difference between an internal service and an exposed one.
Lab 3 — Nmap: The Lab That Surprised Me Most
This was the lab that genuinely surprised me. Not because nmap is unknown — every security person knows the name. What surprised me was how much information it extracts and how.
Basic SYN scan first:
sudo nmap -sS 192.168.100.30
Port 8080 open. Expected.
Then version detection:
sudo nmap -sV 192.168.100.30
Output:
8080/tcp open http SimpleHTTP/0.6 Python/3.11.0
It identified the software, the version, and the runtime language. From a single port scan. Without touching the application directly.
Then OS detection:
sudo nmap -O 192.168.100.30
It fingerprinted the operating system. From packet timing, TTL values, TCP window sizes, and response patterns — nmap infers the OS without the server ever announcing it.
This is what hit me: the server is not volunteering this information. Nmap is inferring it from how the server behaves at the packet level. Every OS implements TCP/IP slightly differently. Those behavioural differences are measurable and nmap has a database of thousands of fingerprints to match against.
From an attacker's perspective this changes everything. Before touching a single application, before attempting any exploit, you already know: what ports are open, what software is running, what version it is, and what OS it runs on. You can then search every known CVE for that exact version before writing a single line of attack code.
Then I saw what filtered looks like.
On the router I added one iptables rule:
sudo iptables -A FORWARD -p tcp --dport 8080 -j DROP
Rescanned from Kali:
sudo nmap -sS 192.168.100.30 -p 8080
Output: filtered
In Wireshark: SYN packets leave Kali. Nothing returns. The router drops them silently. Nmap cannot tell if the port is firewalled, the host is down, or the port does not exist. That ambiguity is by design. DROP gives attackers less information than REJECT.
Lab 4 — TCP/IP Behavior: The Handshake Made Visual
With Wireshark capturing on eth0, I generated traffic and read it at the packet level.
The three-way handshake in real time:
Client → Server: SYN (flag: SYN=1, ACK=0)
Server → Client: SYN-ACK (flag: SYN=1, ACK=1)
Client → Server: ACK (flag: SYN=0, ACK=1)
↓
DATA FLOWS
Expanding a SYN packet showed every layer: Ethernet → IP → TCP. Source port, destination port, sequence number, flags, window size. The entire negotiation is visible and readable.
The HTTP request was the starkest moment. Filter http in Wireshark after a curl:
GET / HTTP/1.1
Host: 192.168.100.30:8080
User-Agent: curl/7.88.1
Accept: */*
Completely readable. No scrambling. No encryption. Just text over a wire. Anyone on this network running Wireshark sees everything.
Running the nmap scan while Wireshark was capturing made the attacker signature unmistakable — dozens of SYN packets in rapid succession, each to a different port, each with no ACK completing the handshake. No legitimate user generates that pattern. This is exactly what IDS systems watch for.
Lab 5 — Firewall & Filtering: DROP, REJECT, and LOG
Three firewall actions. Three completely different outcomes.
DROP — silent discard:
sudo iptables -A FORWARD -p tcp --dport 8080 -j DROP
Curl hangs until timeout. Wireshark shows SYN packets sent, nothing returned. Nmap shows filtered. The attacker gets no confirmation the host even exists.
REJECT — active refusal:
sudo iptables -A FORWARD -p tcp --dport 8080 -j REJECT
Curl fails instantly with connection refused. An ICMP port unreachable reply is sent. Nmap shows closed. The attacker now knows the host is alive — just blocking access.
The difference matters: DROP is slower for the attacker (they wait for timeout on every port) and reveals less. REJECT is faster but confirms the host exists. For inbound internet-facing rules, DROP is almost always the right choice.
LOG + DROP — the correct production approach:
sudo iptables -A FORWARD -p tcp --dport 8080 -j LOG --log-prefix "BLOCKED: "
sudo iptables -A FORWARD -p tcp --dport 8080 -j DROP
sudo grep "BLOCKED" /var/log/syslog
Every blocked attempt is now recorded: source IP, destination port, timestamp, TCP flags. This is your forensic record. Without LOG, DROP is invisible to you — you block attacks but have zero evidence they happened.
Lab 6 — Web Server Lab: Data Exposure Made Concrete
The final lab made the risk tangible.
On the webserver:
mkdir -p ~/site
echo 'SECRET_API_KEY=abc123' > ~/site/.env
echo 'DB_PASS=SuperSecret' > ~/site/config.txt
python3 -m http.server 8080 --directory ~/site
From Kali:
curl http://192.168.100.30:8080/.env
curl http://192.168.100.30:8080/config.txt
Both return file contents immediately. No authentication. No prompting. Just the data.
This is not a theoretical misconfiguration. This is a pattern found constantly in production:
- S3 buckets left public
- Nginx with directory listing enabled
.gitfolders accessible on web servers.envfiles served alongside application code
The server's access log recorded every request Kali made — IP address, timestamp, path, status code. The attacker left a full trail. That trail becomes forensic evidence for the defender.
The Bigger Picture: Firewalls, IDS, and IPS
Six labs completed. But the pattern connecting all of them is worth stepping back to examine.
Why Firewalls Exist
A firewall is not magic. It is a decision engine sitting on the network path, applying rules to every packet passing through. What makes firewalls powerful is position — they sit between zones. Traffic crossing a zone boundary must pass through the firewall. There is no way around it if the network is correctly architected.
There are three generations you need to understand:
Stateless packet filter: Checks each packet individually against rules. Fast, simple, but blind to context. Does not know if a packet belongs to an established connection or is the start of an attack. Treats every packet in isolation.
Stateful firewall: Tracks connection state. Knows which packets belong to established sessions. Can allow return traffic for connections initiated from inside while blocking all unsolicited inbound traffic. This is what your iptables -m state --state ESTABLISHED,RELATED rule implements. Your Ubuntu router VM is a stateful firewall.
Next-generation firewall (NGFW): Goes beyond ports and IPs — understands application protocols, can inspect encrypted HTTPS traffic, identify applications regardless of port, and detect anomalies in traffic patterns. Palo Alto, Fortinet, and Cisco Firepower are the enterprise standard. Cloud equivalents: AWS WAF, Azure Firewall Premium, GCP Cloud Armor.
Why IDS and IPS Exist — And Why Firewalls Are Not Enough
Firewalls operate on rules. Rules require knowing what to block in advance. But attackers do not announce themselves. Novel attacks, zero-days, and slow reconnaissance evade firewall rules because no rule exists for them yet.
This is why IDS and IPS exist — to detect the unknown from behaviour.
IDS — Intrusion Detection System: Reads network traffic and compares it against two things: signatures (known attack patterns) and behavioural baselines (what normal traffic looks like). When it finds a match it alerts. It does not block. It watches and reports.
Think of IDS as a security camera. It records everything. It alerts on suspicious behaviour. But it does not stop the intruder — it gives you evidence and early warning.
IPS — Intrusion Prevention System: Everything IDS does, plus active blocking. When it detects an attack pattern it drops the packets in real time. It sits inline — traffic passes through it rather than just being observed.
Think of IPS as a security guard who both watches and physically intervenes.
The tradeoff: IPS can cause false positives — legitimate traffic matching an attack pattern gets blocked, potentially breaking applications. Many organisations run IDS in detection-only mode first, tune it for weeks to eliminate false positives, then switch to prevention mode.
Tools in this space:
- Suricata — modern open source IDS/IPS with multi-threading. Rule-based and protocol-aware. Runs in inline mode (IPS) or passive mode (IDS). This is what you will use in the labs.
- Snort — the original open source IDS. Massive community rule set. Slightly older architecture but still widely deployed.
- Zeek — network analysis framework. Less signature-based, more behavioural. Generates structured logs of every protocol transaction rather than just alerting.
- Kismet — wireless-specific IDS. Detects rogue access points, deauth floods, and probe attacks.
What IDS actually catches:
- Port scans: the rapid SYN pattern from Lab 3 triggers an immediate alert in any properly configured IDS
- Known exploit payloads: shellcode patterns in packet data
- Protocol anomalies: malformed HTTP headers, oversized DNS queries
- C2 traffic: known command-and-control server IPs, unusual outbound connection patterns
- Lateral movement: internal hosts scanning other internal hosts
What IDS misses:
- Encrypted traffic unless SSL inspection is deployed
- Slow scans designed to stay below detection thresholds
- Living-off-the-land attacks using legitimate tools
- Zero-day payloads with no existing signatures
This is why layered defence matters. Firewall + IDS + endpoint monitoring + log analysis + human review. Each layer catches what the others miss.
What Six Labs Taught Me
1. Reconnaissance is devastatingly effective. Nmap told me the software, version, language, and OS of the target without me touching the application. Every exposed service is an information leak before any attack even begins.
2. The binding address is a security decision. 0.0.0.0 vs 127.0.0.1 is not a configuration detail — it is the line between an internal service and an exposed one.
3. DROP and LOG together are the correct default. DROP without LOG is blind. LOG without DROP is ineffective. Together they block attacks while preserving evidence.
4. Default configurations are almost always wrong for security. Python's HTTP server exposes the entire directory. Every default that exists for convenience is a potential vulnerability.
5. The attacker and the defender use the same tools. Wireshark, nmap, ss, iptables — not attacker tools or defender tools. Network visibility tools. The question is whose hands they are in and what they are looking for.
Today — Rest Day
Today is Sunday and I am taking the day off completely.
A quick note on the reality of this journey: I work a full-time Monday to Friday job. The 2 hours of daily learning plus 30 minutes writing and publishing each blog post adds up. It is sustainable but it requires deliberate recovery too. Burning out on day 5 of a 90-day plan helps nobody.
Sunday is for recharging. No labs, no terminals, no Wireshark.
Monday the grind resumes. HTTP inspection, local vs remote access, and the lab I have been building toward all week: reverse shells.
That is where it gets serious.
Day 4 of 90 | Labs 1–6: ✅ Complete | Nmap Fingerprinting: ✅ | Firewall Control: ✅ | IDS/IPS: ✅ | Sunday: Rest 🔋 | Monday: HTTP Inspection + Reverse Shells
🔗 Related Posts
- Day 3 — From Theory to Packets — Building the lab that these labs ran on
- Day 5 — HTTP, Reverse Shells & Three Minds — Picking up where Lab 6 left off: exploitation, post-access, and the defender habit
- Day 6 — The Wiretap Protocol — How ARP spoofing exploits the trust I first observed in Lab 1
- Day 1 — The AI Security Roadmap — The full curriculum that structured these labs