Module 1 Complete: 11 Tools, 18 Days, and the Mindshift That Changed How I See Every System I've Ever Built
Digital Insights

Module 1 Complete: 11 Tools, 18 Days, and the Mindshift That Changed How I See Every System I've Ever Built

Apr 07, 2026
90 min read
Kuldeep Singh

18 days. 11 tools. Zero external dependencies. This is the definitive retrospective of Module 1 — documenting how 6.5 years of building production systems didn't prepare me for the moment I stopped asking 'how does this work?' and started asking 'how do I force this to fail?'

📍 Journey Navigation Prev: Day 13 — When Default Passwords Are the Real Vulnerability: Building a Multi-Protocol Credential Auditor | Day 1 | Day 5 | Day 10 | Day 13 |

Status: Day 18 of 90-day AI Security journey — Module 1: COMPLETE Arsenal Shipped: 11 Professional-Grade Tools (Pure Python Standard Library) GitHub: applied-ai-security-projects


The Day the Reset Button Hit

Eighteen days ago, I started this module as a Senior Full Stack Engineer with 6.5 years of experience building production systems — distributed backends, payment infrastructure, APIs handling millions of requests. I understood networking in the way that any competent engineer does: I knew TCP was reliable, UDP was fast, HTTPS was secure. I had strong opinions about database connection pooling. I had never once thought about what a TCP RST packet could tell an attacker about my firewall policy.

Module 1 changed that.

This is not a "I learned some hacking" post. This is an attempt to honestly document the specific technical mindshift that happens when you stop building for the happy path and start building for the unintended state. I shipped 11 tools in 18 days, all in pure Python, zero external dependencies. This post is the retrospective — every project, every architectural decision, every lesson that's now permanently changed how I think about the systems I build.

The GitHub repo is live: kuldeepstechwork/applied-ai-security-projects


Why Zero Dependencies Was a Non-Negotiable Constraint

Before any project breakdown: the constraint that shaped everything.

Every one of the 11 tools in this lab was built using the Python Standard Library alone. No Scapy. No Nmap-python. No Requests. No Paramiko.

This was a deliberate engineering choice, not a limitation.

The forensic environment problem. In a real post-exploitation scenario — you've gotten a shell on a jumpbox, or you're on a locked-down legacy server — you cannot pip install scapy. Third-party libraries are not available. The Python interpreter that ships with the OS is the only tool you have. A security researcher who can only operate with their full toolkit installed is a security researcher who stops working the moment they're inside the perimeter.

The abstraction tax. When you use Scapy, you write IP(dst="192.168.1.1")/TCP(dport=80). What you don't learn is that IP headers are 20 bytes in !BBHHHBBH4s4s format (big-endian network byte order), that the checksum field must be calculated manually, that TTL lives at byte offset 8. Scapy hides all of this correctly — which is great for productivity and terrible for understanding.

The trust problem. After building the sniffer by hand — 1,515 lines of pure struct.unpack() calls — I stopped trusting libraries at their word. I now have a mental model of what happens at the byte level when requests.get() runs. That model is what lets me reason about what those bytes look like to a firewall, a SIEM, or an IDS.

Every tool I'll ever write is different because of this constraint.


Part 1: The Reconnaissance Chain

The first six projects form a complete pipeline from "target has an IP" to "target's account credentials are compromised." Each tool is a stage. The output of one feeds the input of the next.


Project 01 — Professional TCP Port Scanner (port_scanner.py)

The engineering problem: Binary open/closed scanning is insufficient. Real reconnaissance needs to know not just that port 3306 is open, but what version of MySQL is running, whether it accepts unauthenticated connections, and what the banner reveals about the host configuration.

Architecture:

port_scanner.py
├── parse_port_spec()     — "top100", "22,80", "8000-8090" → [list of ints]
├── queue.Queue           — thread-safe work distribution
├── ThreadPoolExecutor    — configurable concurrency (default 100 workers)
├── scan_port()           — socket.connect_ex() → errno-based result
├── grab_banner()         — per-protocol handshake engine
│   ├── SSH    → "SSH-2.0-OpenSSH_8.4p1 Debian"
│   ├── FTP    → "220 (vsFTPd 3.0.3)"
│   ├── SMTP   → "220 mail.lab.local ESMTP Postfix"
│   ├── MySQL  → binary greeting packet → version extraction
│   └── Redis  → RESP protocol INFO command
└── json export + colored table output

The critical insight was using socket.connect_ex() instead of socket.connect(). The difference:

# Wrong approach — exception handling overhead at scale
try:
    s.connect((host, port))
    return "OPEN"
except ConnectionRefusedError:
    return "CLOSED"
except socket.timeout:
    return "FILTERED"

# Right approach — errno codes, zero exception overhead
result = s.connect_ex((host, port))
if result == 0:
    return "OPEN"
elif result == 111:   # ECONNREFUSED
    return "CLOSED"
elif result == 110:   # ETIMEDOUT
    return "FILTERED"

At 200 threads scanning 65,535 ports, the difference between exception-based and errno-based result handling is the difference between a 4-minute scan and a 90-second scan. At scale, exception overhead is real.

The protocol probe database was the other key component. Instead of reading whatever bytes came back, each protocol gets a specific greeting:

_PROBES: dict[int, bytes] = {
    21:   b"",                          # FTP sends banner immediately
    22:   b"",                          # SSH sends banner immediately
    25:   b"EHLO probe\r\n",            # SMTP needs EHLO to reveal STARTTLS
    80:   b"HEAD / HTTP/1.0\r\n\r\n",  # HTTP minimal request
    3306: b"\x00",                      # MySQL: trigger greeting packet
    6379: b"INFO server\r\n",           # Redis RESP protocol
}

A generic scanner that sends \r\n to every port gets a raw banner dump. My scanner speaks the protocol — so it gets the version negotiation response that contains the actual service version and configuration hints.

Deep lesson: Latency is intelligence. The difference between ECONNREFUSED (111ms response time, RST packet) and a timeout (8000ms response time, no packet) is not just an error type — it is the first indicator of the firewall policy. A host that RSTs connection attempts is reachable and actively rejecting. A host that silently drops packets has a firewall with a DROP rule. Two different security postures, visible in the response latency alone.


Project 02 — Professional Network Mapper (net_mapper.py)

The engineering problem: Standard ping sweeps are defeated by firewalls that block ICMP. A real network mapper needs multiple discovery methods and needs to infer information from the responses it gets — not just whether a host is alive.

Architecture:

net_mapper.py
├── SubnetParser          — CIDR → list of host IPs
├── HostDiscovery         — layered discovery strategy
│   ├── Layer 1: ICMP Echo (socket.SOCK_RAW + IPPROTO_ICMP)
│   ├── Layer 2: TCP SYN to ports 80, 443, 22 (fallback if ICMP blocked)
│   └── Layer 3: ARP cache integration (/proc/net/arp)
├── TTLAnalyzer           — OS fingerprinting from TTL values
│   ├── TTL 64  → Linux / macOS
│   ├── TTL 128 → Windows
│   └── TTL 255 → Cisco / network hardware
├── MACResolver           — OUI lookup for hardware vendor
└── ServiceAuditor        — per-host lightweight port check

The layered discovery was the key design decision. Running ICMP-only misses hosts behind firewalls that block ping. Running TCP-only to port 80 misses hosts that only run SSH. The tool tries ICMP first, and if it times out, falls back to TCP. If a host responds to either, it's considered alive.

But the more interesting piece was the ARP cache integration:

def _read_arp_cache() -> dict[str, str]:
    """Read /proc/net/arp for known MAC→IP mappings."""
    results = {}
    with open("/proc/net/arp") as f:
        next(f)  # skip header
        for line in f:
            parts = line.split()
            if len(parts) >= 4 and parts[2] == "0x2":  # 0x2 = reachable
                results[parts[0]] = parts[3]    # IP → MAC
    return results

The ARP cache is populated by normal network traffic. Any host that has communicated with your machine recently is in it. Reading /proc/net/arp discovers those hosts without generating any network traffic. This is the most effective reconnaissance technique: passive information gathering from the OS's own data structures.

Deep lesson: The stealth of persistence. Generating ICMP broadcasts is noisy — any half-decent network tap logs every echo request. Reading the ARP cache is invisible — no packets generated, no logs created. Understanding which data sources are passive versus active changes how you approach reconnaissance. The most lethal recon is the kind that leaves no evidence it happened.


Project 03 — Professional Service Banner Grabber (banner_grab.py)

The engineering problem: Banner grabbing is trivial. What's hard is building a grabber that extracts meaningful intelligence from what services return — including from encrypted services where the banner itself isn't readable.

Architecture:

banner_grab.py
├── ProbeDatabase         — frozen dataclasses: protocol-specific handshakes
│   ├── 20+ service protocols (MySQL, Redis, LDAP, Memcached, SMB...)
│   └── Immutable (frozen=True) → thread-safe without locking
├── TLSInspector          — certificate metadata extraction
│   ├── CN / SAN extraction
│   ├── Issuer organization
│   ├── Expiry calculation
│   └── Cipher suite analysis
├── RegexVersionEngine    — raw banner → clean version string
│   ├── "OpenSSH_8.4p1 Debian-5+deb11u2" → "8.4p1"
│   └── "5.7.34-0ubuntu0.20.04.1" → "5.7.34"
├── CVEMapper             — offline version → CVE correlation
│   └── ProFTPD 1.3.5 → CVE-2015-3306 (CVSS 7.5)
└── RiskScorer            — CVSS-based service risk classification

The frozen=True dataclass design for the probe database was an explicit concurrency decision:

@dataclass(frozen=True)
class ServiceProbe:
    name: str
    probe_bytes: bytes
    response_regex: str
    default_port: int
    tls_capable: bool = False

# Probes are defined once and shared across all threads without locking
_PROBES: tuple[ServiceProbe, ...] = (
    ServiceProbe("SSH",   b"",              r"SSH-[\d.]+-(\S+)", 22),
    ServiceProbe("FTP",   b"",              r"(\S+ [\d.]+)",     21),
    ServiceProbe("SMTP",  b"EHLO probe\r\n",r"(\d+\.\d+[\.\d]*)",25),
    ServiceProbe("MySQL", b"\x00",          r"[\d]+\.[\d]+\.[\d]+", 3306),
    ServiceProbe("Redis", b"INFO server\r\n",r"redis_version:([\d.]+)", 6379),
    # ... 15 more
)

The TLS certificate extraction was the most valuable offensive technique I implemented. For encrypted services (HTTPS on 443, IMAPS on 993, SMTPS on 465), you cannot read the service banner directly. But you can read the TLS certificate:

def _extract_tls_metadata(host: str, port: int) -> CertInfo | None:
    ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
    ctx.check_hostname = False
    ctx.verify_mode = ssl.CERT_NONE   # bypassing validation to read cert
    
    with ctx.wrap_socket(socket.socket(), server_hostname=host) as ssock:
        ssock.connect((host, port))
        cert = ssock.getpeercert()
        
    return CertInfo(
        subject_cn=cert["subject"][0][0][1],
        issuer_org=cert["issuer"][1][0][1],
        san_names=_extract_san(cert),        # Subject Alternative Names
        not_after=datetime.strptime(...),
        cipher=ssock.cipher()[0],
    )

Internal certificates routinely contain hostnames like dev-admin.internal, db-primary.company.net, backup-vault-01. These names reveal the internal network topology. An HTTPS service that you can't penetrate directly might expose the hostname of the database server you can — in its certificate's Common Name.

Deep lesson: Identity is a leak. A certificate is not just a security layer. It is a signed document that advertises the identity of the server. Bypassing validation doesn't make a service insecure; it makes the certificate's contents readable. In internal networks where certificates are self-signed or poorly managed, this is consistently one of the most useful sources of target intelligence.


Project 04 — Professional Deep-Packet Inspection Sniffer (sniffer.py)

The engineering problem: Build a network sniffer from raw sockets — no libpcap, no Scapy — that parses Ethernet frames through the full protocol stack and maintains stateful TCP session tracking.

This was the most technically demanding project in the module. 1,515 lines of pure Python. Every byte of every protocol parsed manually.

Architecture:

sniffer.py
├── RawCapture           — socket.AF_PACKET + socket.SOCK_RAW
│   └── Ethernet frames pulled directly from NIC
├── EthernetParser       — struct.unpack("!6s6sH", frame[:14])
│   ├── EtherType 0x0800 → IPv4
│   ├── EtherType 0x0806 → ARP
│   └── EtherType 0x86DD → IPv6
├── IPv4Parser           — struct.unpack("!BBHHHBBH4s4s", ip_header)
│   ├── Protocol 6  → TCP
│   ├── Protocol 17 → UDP
│   └── Protocol 1  → ICMP
├── TCPParser            — struct.unpack("!HHLLBBHHH", tcp_header)
│   └── RFC 793 State Machine (SYN → SYN-ACK → ESTABLISHED → FIN/RST)
├── DNSParser            — recursive label decoder with 0xC0 compression
├── AnomalyDetector      — stateful pattern analysis
│   ├── Port scan detection (>15 ports from same source in 30s)
│   └── ARP spoofing detection (IP→MAC mapping changes)
└── PCAPWriter           — libpcap format for Wireshark import

The raw socket setup is three lines, but understanding what those three lines do took a day:

sock = socket.socket(
    socket.AF_PACKET,    # L2 — gives you raw Ethernet frames
    socket.SOCK_RAW,     # raw — no kernel processing
    socket.htons(0x0003) # ETH_P_ALL — capture every protocol
)

AF_PACKET bypasses the kernel's protocol stack entirely. Normal sockets receive data after the kernel has parsed IP headers, stripped Ethernet headers, and delivered the payload. With AF_PACKET, you receive the raw Ethernet frame — every byte, starting from the destination MAC address. You are responsible for parsing everything.

The IPv4 header unpack was the moment networking stopped being abstract:

# 20 bytes, big-endian, field by field:
# B  B  H     H     H     B   B   H       4s      4s
# VHL TOS LEN   ID    FLAGS TTL PROTO CHECKSUM SRC     DST
iph = struct.unpack("!BBHHHBBH4s4s", raw_data[14:34])

version_ihl = iph[0]
version     = version_ihl >> 4        # top nibble
ihl         = (version_ihl & 0xF) * 4 # bottom nibble × 4 = header length
ttl         = iph[5]
protocol    = iph[6]
src_ip      = socket.inet_ntoa(iph[8])
dst_ip      = socket.inet_ntoa(iph[9])

Networking is not string manipulation. It is bitwise math on binary data aligned to specific byte boundaries. Until you've written this, the protocol exists as an abstraction. After writing it, you see the bytes.

The RFC 793 TCP state machine tracker was the most complex component. Each TCP flow (identified by (src_ip, src_port, dst_ip, dst_port)) has a state:

class TCPFlowState(Enum):
    LISTEN      = "LISTEN"
    SYN_SENT    = "SYN_SENT"
    SYN_RCVD    = "SYN_RCVD"
    ESTABLISHED = "ESTABLISHED"
    FIN_WAIT_1  = "FIN_WAIT_1"
    CLOSE_WAIT  = "CLOSE_WAIT"
    CLOSED      = "CLOSED"

def _update_state(self, flow_id: tuple, flags: int) -> TCPFlowState:
    state = self._flows.get(flow_id, TCPFlowState.LISTEN)
    
    SYN = flags & 0x02
    ACK = flags & 0x10
    FIN = flags & 0x01
    RST = flags & 0x04
    
    if state == TCPFlowState.LISTEN and SYN and not ACK:
        return TCPFlowState.SYN_SENT
    if state == TCPFlowState.SYN_SENT and SYN and ACK:
        return TCPFlowState.SYN_RCVD
    if state == TCPFlowState.SYN_RCVD and ACK:
        return TCPFlowState.ESTABLISHED
    if RST:
        return TCPFlowState.CLOSED
    ...

The DNS parser had its own complexity — recursive label decompression:

def _decode_dns_name(data: bytes, offset: int) -> tuple[str, int]:
    labels = []
    while True:
        length = data[offset]
        if length == 0:              # null terminator
            offset += 1
            break
        if (length & 0xC0) == 0xC0: # compression pointer (0xC0 = 11000000)
            pointer = ((length & 0x3F) << 8) | data[offset + 1]
            label, _ = _decode_dns_name(data, pointer)
            labels.append(label)
            offset += 2
            break
        labels.append(data[offset+1:offset+1+length].decode())
        offset += 1 + length
    return ".".join(labels), offset

The 0xC0 pointer is a DNS optimization: instead of repeating google.com in every record, the response uses a 2-byte pointer to the first occurrence. Understanding this binary efficiency is what lets you spot malformed DNS that tries to exploit parser vulnerabilities.

Deep lesson: The illusion of privacy. Running the sniffer on my lab network while running other tools revealed something uncomfortable: I could read every DNS query made by every device on the subnet. google.com, api.github.com, telemetry.microsoft.com — the queries reveal what services are running and what they're communicating with, even when the payload is encrypted. DNS is not encrypted by default. The metadata of encrypted traffic is itself intelligence.


Part 2: Weaponization and Delivery

Project 05 — Professional Shell Payload Generator (shellgen.py)

The engineering problem: Payload generation tutorials teach you to copy a one-liner. What they don't teach is that every payload has a detection footprint — a set of behavioral signatures that EDR systems and SIEM correlation rules are specifically looking for.

Architecture:

shellgen.py
├── PayloadTemplate       — typed dataclass with metadata
│   ├── name, language, template string
│   ├── stealth_score     (1–10: 10 = hardest to detect)
│   ├── detection_notes   (specific Sigma/YARA rules that trigger)
│   └── required_env      (python3, bash, perl, powershell...)
├── EncoderPipeline       — multi-stage encoding
│   ├── Stage 1: Base64   (basic obfuscation)
│   ├── Stage 2: URL      (evades simple string matching)
│   └── Stage 3: PowerShell EncodedCommand (-EncodedCommand flag)
├── ObfuscationEngine     — variable name randomization + IP splitting
│   ├── "192.168.1.1" → chr(49)+chr(57)+chr(50)+"."...
│   └── Variable names: a1b2c3 instead of host, port
├── StealthScorer         — quantitative detection footprint analysis
│   ├── Scores based on: syscall patterns, string signatures, entropy
│   └── Maps to specific Sigma rule IDs and YARA patterns
├── DeploymentPlanner     — 4-step tactical workflow generator
│   ├── Step 1: Listener setup (nc -lvnp / python listener)
│   ├── Step 2: Payload hosting (HTTP server command)
│   ├── Step 3: Execution method (curl | bash / python -c)
│   └── Step 4: Session stabilization (pty.spawn / stty)
└── BuiltinListener       — threaded TCP listener (replaces nc)

The 25+ payload templates span 12 languages — not for completeness, but because different environments have different runtimes available:

bash              — most Linux systems
python3           — most modern Linux + macOS
python2           — legacy systems
perl              — old enterprise Linux, always available
php               — web server compromise → shell
ruby              — rails environments
powershell        — Windows targets
powershell_b64    — Windows with script execution policy
nodejs            — JavaScript environments
java              — J2EE servers
groovy            — Jenkins pipeline compromise
golang_compile    — environments where you can compile

The stealth scoring was what changed my perspective on obfuscation:

@dataclass(frozen=True)
class PayloadTemplate:
    name: str
    stealth_score: int  # 1 = obvious, 10 = hard to detect
    detection_notes: list[str]

# Example scoring:
bash_tcp = PayloadTemplate(
    name="bash-tcp",
    stealth_score=3,
    detection_notes=[
        "Sigma: 'proc_creation_lnx_shell_reverse_connection'",
        "YARA: bash_with_dev_tcp matches /dev/tcp pattern",
        "EDR: bash spawning network socket is near-universally flagged",
    ]
)

python3_socket = PayloadTemplate(
    name="python3-socket",
    stealth_score=5,
    detection_notes=[
        "Sigma: 'proc_creation_lnx_python_reverse_shell'",
        "EDR: subprocess.Popen + socket.connect combo is signature",
        "Stealth: python3 is less suspicious than bash /dev/tcp",
    ]
)

Deep lesson: EDR monitors behavior, not bytes. A Python script obfuscated into 1,000 chr() calls that combine to form a socket connection still, ultimately, calls socket.connect(). The obfuscation changes the bytes of the file. The system call trace is identical. EDR systems that operate at the kernel level — hooking sys_connect() and friends — see through string-level obfuscation completely. Understanding this means understanding that stealth is a behavioral problem, not an encoding problem.


Project 06 — Network Credential Auditor (credaudit.py)

I wrote a full post about this one: Day 13. The short version: 900+ lines, ABC plugin pattern for 6 protocols, Gaussian jitter rate limiting, lockout state machine, MITRE ATT&CK–tagged JSON reporting.

The architectural insight that carries forward: an Abstract Base Class with a factory makes adding a seventh protocol a one-class addition without touching anything that already works. This is the Open/Closed Principle applied to offensive tooling — and it's the same reason good frameworks are extensible without being fragile.


Part 3: Web Delivery and Orchestration

Project 07 — Web Delivery Attack Simulator (attack_server.py)

The engineering problem: Running a simulated attack involves juggling three separate processes: the payload HTTP server, the reverse shell listener, and the terminal for the target. Manually managing all three is error-prone. Mistime one of them and the simulation fails.

Architecture:

attack_server.py
├── PayloadStager         — dynamic payload generation into /tmp/staging/
│   ├── Injects LHOST/LPORT into template at runtime
│   └── Generates per-session unique filenames
├── LoggingHTTPServer     — access-logged HTTP server
│   ├── Every request: timestamp, source IP, path, User-Agent
│   └── Forensic evidence for simulation reports
├── ListenerManager       — threaded TCP listener (non-blocking)
│   └── Captures connection + spawns PTY interaction
├── ProcessOrchestrator   — sequential Recon → Deliver → Access flow
│   ├── subprocess.Popen for background processes
│   └── Signal handlers: SIGINT/SIGTERM → cleanup all children
└── SessionCleanup        — removes staged payloads, kills orphan processes

The Signal-Aware Process Management was the lesson in production-grade tooling:

import signal, subprocess, atexit

_child_processes: list[subprocess.Popen] = []

def _cleanup_handler(signum, frame):
    """Ensure zero orphan processes on exit."""
    for proc in _child_processes:
        if proc.poll() is None:
            proc.terminate()
            try:
                proc.wait(timeout=3)
            except subprocess.TimeoutExpired:
                proc.kill()
    sys.exit(0)

signal.signal(signal.SIGINT, _cleanup_handler)
signal.signal(signal.SIGTERM, _cleanup_handler)
atexit.register(_cleanup_handler, None, None)

Without this, every Ctrl+C during a simulation leaves orphan HTTP servers and listeners running on their ports. The next simulation attempt fails with Address already in use. Production systems have the same problem with daemon processes and signal handling — I've debugged this exact issue in production deployments multiple times. The security context just makes the failure mode more obvious.

Deep lesson: Automation is lethality. A researcher who has to manually start three processes, carefully sequence them, and clean them up afterward is a researcher spending 30% of their cognitive load on infrastructure. Automating the delivery infrastructure means every mental cycle goes toward observing the target behavior, not managing tools. This applies equally to production engineering: automated infrastructure is what lets you focus on the actual problem.


Part 4: Post-Exploitation and Lateral Logic

Project 08 — Local Service Enumerator (enum_local.py)

The engineering problem: You have a shell on a target. What do you do next? Every tutorial says "enumerate services." None of them show you how to do it efficiently when you can't install nmap.

Architecture:

enum_local.py
├── PortEnumerator        — /proc/net/tcp + /proc/net/tcp6 parsing
│   ├── Maps hex ports to decimal (local_address: 0016 → port 22)
│   └── Categorizes: "localhost only" vs "externally exposed"
├── ProcessCorrelator     — /proc/[pid]/net/tcp + /proc/[pid]/cmdline
│   └── Links port → PID → binary path
├── HighValueFilter       — identifies database/web/root-owned processes
│   ├── Flags: mysql, postgres, redis, mongodb, nginx, apache
│   └── Flags any process with EUID 0 (root-owned)
├── SecretHunter          — recursive regex-driven file scanner
│   ├── Targets: .env, .git/config, settings.py, config.php
│   ├── Patterns: API keys, passwords, private keys, DB connection strings
│   └── Depth-limited search to avoid runaway traversal
├── SudoAuditor           — sudo -l parsing + SUID binary enumeration
└── PostureReport         — consolidated human-readable summary

The /proc/net/tcp parsing replaces netstat/ss — tools that are sometimes not installed:

def _parse_proc_net_tcp() -> list[PortInfo]:
    ports = []
    with open("/proc/net/tcp") as f:
        next(f)  # skip header
        for line in f:
            cols = line.split()
            local_addr = cols[1]           # "0100007F:0050" (hex IP:port)
            hex_ip, hex_port = local_addr.split(":")
            port = int(hex_port, 16)       # 0050 → 80
            
            # Decode the little-endian IP
            ip_bytes = bytes.fromhex(hex_ip)
            ip = socket.inet_ntoa(ip_bytes[::-1])  # reverse byte order
            
            ports.append(PortInfo(ip=ip, port=port, state=cols[3]))
    return ports

The hex encoding and the reversed byte order (little-endian IP in /proc/net/tcp) are the kind of details that only become obvious when you read the kernel documentation. There's no library for this. You have to parse it yourself.

The Secret Hunter was the most immediately practical component:

_SECRET_PATTERNS = {
    "API Key":     re.compile(r'(?:api[_-]?key|apikey)\s*[=:]\s*["\']?([A-Za-z0-9_\-]{20,})', re.I),
    "Password":    re.compile(r'(?:password|passwd|pwd)\s*[=:]\s*["\']?(\S{8,})', re.I),
    "AWS Secret":  re.compile(r'(?:aws_secret_access_key)\s*[=:]\s*([A-Za-z0-9+/]{40})', re.I),
    "Private Key": re.compile(r'-----BEGIN (?:RSA )?PRIVATE KEY-----'),
    "DB URL":      re.compile(r'(?:postgres|mysql|mongodb)://[^@]+@([^/\s]+)', re.I),
}

Deep lesson: The credential is the key. You don't always need an exploit. In my lab environment, running the secret hunter on the home directory of the compromised account surfaced an .env file with plaintext database credentials and an id_rsa without a passphrase. The entire privilege escalation path was readable files. The most complex "secure" systems are frequently brought down by a developer who checked in credentials that "nobody would ever find."


Project 09 — Professional Firewall Behavior Tester (fw_behavior_tester.py)

The engineering problem: A port scanner tells you open/closed. But a security professional needs to understand why a port is closed — is the service not running, or is a firewall hiding it?

Architecture:

fw_behavior_tester.py
├── PortProber            — connection attempt with precise timing
│   ├── socket.connect_ex() + elapsed_ms measurement
│   └── Returns (errno, latency_ms) tuple
├── BehaviorClassifier    — errno → firewall action mapping
│   ├── errno 0   → OPEN (service running, accepted connection)
│   ├── errno 111 → CLOSED (RST received — service not running)
│   ├── errno 110 → FILTERED/DROP (timeout — firewall silently drops)
│   └── errno 113 → REJECTED (ICMP unreachable — firewall sends RST)
├── LatencyProfiler       — jitter analysis across multiple probes
│   └── High jitter on "FILTERED" ports → load balancer in path
├── ICMPAnalyzer          — catches ICMP Type 3 unreachable responses
└── PolicyInferenceEngine — pattern analysis → firewall rule hypothesis

The core insight is the errno mapping:

def classify_port_behavior(host: str, port: int, timeout: float = 5.0) -> PortBehavior:
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.settimeout(timeout)
    
    start = time.monotonic()
    result = s.connect_ex((host, port))
    latency_ms = (time.monotonic() - start) * 1000
    s.close()
    
    if result == 0:
        return PortBehavior.OPEN           # TCP handshake completed
    elif result == 111:  # ECONNREFUSED
        return PortBehavior.CLOSED         # RST from target — no service
    elif result == 110:  # ETIMEDOUT
        return PortBehavior.DROP_FILTERED  # Silence — DROP rule
    elif result == 113:  # EHOSTUNREACH
        return PortBehavior.ICMP_REJECT    # ICMP unreachable — REJECT rule

The practical difference between DROP and REJECT is profound:

  • REJECT sends back a TCP RST or ICMP unreachable. The attacker learns "this port is blocked" in milliseconds. The firewall is polite.
  • DROP sends nothing. The attacker waits for the full timeout (5–30 seconds) before learning the port is blocked. At scale, scanning 1,000 filtered ports with DROP rules takes 80+ minutes versus seconds with REJECT.

But DROP reveals more than the firewall administrator intended: if most ports return in <1ms (RST) but some ports time out (DROP), those timeout ports are specifically being hidden. The firewall is silently advertising "there is something here that we don't want you to probe."

Deep lesson: Silence is strategy. A firewall that rejects tells you what it blocks. A firewall that drops tells you that there's something worth hiding. The absence of a response is itself a signal. Every design decision in a security architecture is visible to someone who knows how to read the responses.


Part 5: The Kill Chain Orchestration Layer

Project 10 — Professional Multi-Tool Recon Engine (recon_multitool.py)

The engineering problem: Real reconnaissance generates a lot of data from multiple sources. The value is not in the individual results — it's in the correlation between them. A tool that runs all stages and automatically pivots based on what it finds is categorically more powerful than running each stage manually.

Architecture:

recon_multitool.py
├── DiscoveryEngine       — port scan with service fingerprinting
├── BannerEngine          — banner grab + version extraction
├── ReactiveWebProber     — triggers only on HTTP/HTTPS confirmation
│   ├── Quick-fuzz wordlist: /admin, /.env, /.git, /config, /phpinfo
│   ├── /backup, /api, /swagger, /actuator (Spring Boot), /wp-admin
│   └── Each hit: HTTP status + Content-Length for quick triage
├── CorrelationEngine     — cross-source finding synthesis
│   └── Banner version + CVE + open web path → combined risk score
└── UnifiedReporter       — consolidated table + JSON export

The "Reactive Probing" pattern was the architectural insight:

def run_recon(target: str) -> ReconReport:
    # Stage 1: Discover
    open_ports = scan_ports(target, "top100")
    
    # Stage 2: Fingerprint
    banners = [grab_banner(target, p) for p in open_ports]
    
    # Stage 3: Pivot — only if web service confirmed
    web_findings = []
    for port_info in banners:
        if port_info.service in ("HTTP", "HTTPS"):
            # Automatically launch web artifact discovery
            web_findings.extend(quick_fuzz(target, port_info.port))
    
    return ReconReport(ports=open_ports, banners=banners, web=web_findings)

Running three disconnected tools requires: three separate invocations, three output files, manual correlation. The unified engine does it in one pass, and the correlation — "this service version has this CVE, and the web server has this exposed path" — is surfaced in the unified report rather than left to manual analysis.

Real outcome in the lab: Against a Metasploitable2 instance, the tool completed in 43 seconds and surfaced: ProFTPD 1.3.5 (CVE-2015-3306, CVSS 7.5), Apache 2.2.8 (CVE-2011-3368), MySQL 5.0.51 (multiple CVEs), and an exposed /phpinfo page revealing the full server configuration including loaded PHP extensions and environment variables. Four attack vectors, one command, 43 seconds.


Project 11 — Professional Attack Chain Simulator (attack_chain_simulator.py) — The Capstone

The engineering problem: The 10 previous tools demonstrate individual attack techniques. The final project demonstrates how they chain together into a complete attack operation — and generates forensic-grade evidence of every step.

Architecture:

attack_chain_simulator.py
├── ChainOrchestrator     — sequential phase execution with fail-fast
│   ├── Phase 1: RECON     — hostname resolution + ICMP check
│   ├── Phase 2: SCAN      — port discovery (1–1024)
│   ├── Phase 3: ENUM      — HTTP endpoint discovery + banner grab
│   ├── Phase 4: DELIVER   — payload staging + HTTP server launch
│   ├── Phase 5: ACCESS    — reverse shell listener + interaction
│   └── Phase 6: REPORT    — forensic log consolidation
├── ForensicLogger        — millisecond-precision event recording
│   ├── Every action: timestamp, MITRE technique tag, phase
│   └── Output: attack_log.txt (structured, Splunk-ingestible format)
├── FailFastValidator     — pre-flight checks before noisy phases
│   ├── Is target reachable? (ICMP/TCP) — exit if not
│   └── Are key ports filtered? — warn before scan phase
├── ProcessLifecycleManager — clean orchestration of subprocesses
└── PayloadCleanup        — removes staged files on exit

The forensic logging was the feature that elevated this from a "script" to a "tool":

class ForensicLogger:
    def log(self, phase: str, action: str, mitre_id: str, details: dict):
        entry = {
            "timestamp": datetime.utcnow().isoformat() + "Z",
            "phase": phase,
            "action": action,
            "mitre_technique": mitre_id,
            "mitre_url": f"https://attack.mitre.org/techniques/{mitre_id}/",
            **details
        }
        self._file.write(json.dumps(entry) + "\n")
        self._file.flush()   # each event persisted immediately

A sample log sequence:

{"timestamp":"2026-04-05T14:23:01.442Z","phase":"RECON","action":"hostname_resolved","mitre_technique":"T1590","details":{"target":"192.168.100.30","ip":"192.168.100.30","latency_ms":2.1}}
{"timestamp":"2026-04-05T14:23:01.890Z","phase":"SCAN","action":"port_open","mitre_technique":"T1595.001","details":{"port":21,"service":"FTP","banner":"ProFTPD 1.3.5"}}
{"timestamp":"2026-04-05T14:23:02.103Z","phase":"SCAN","action":"port_open","mitre_technique":"T1595.001","details":{"port":80,"service":"HTTP","banner":"Apache/2.2.8"}}
{"timestamp":"2026-04-05T14:23:04.771Z","phase":"ENUM","action":"web_path_found","mitre_technique":"T1046","details":{"path":"/phpinfo.php","status":200,"size":49823}}
{"timestamp":"2026-04-05T14:23:07.223Z","phase":"DELIVER","action":"payload_staged","mitre_technique":"T1105","details":{"payload":"python3-socket","lhost":"10.10.10.10","lport":4444}}

This log can be directly ingested into Splunk or any SIEM. It is designed to answer the question: "What exactly happened during this simulation, in what order, and which MITRE techniques were exercised?" That is the question a SOC team asks when building detection rules. Building the simulator to answer that question is the same mindset as writing a test suite: the simulation is only valuable if its outputs can be used to validate your defenses.

Deep lesson: A single tool is a script. A collection of tools is an arsenal. An automated chain is a system. The 10 individual tools demonstrate techniques. The simulator demonstrates that real attacks don't execute techniques in isolation — they chain them, automatically, with each stage feeding the next. Defending against a technique is table stakes. Defending against a coordinated chain of techniques is the real problem.


Part 6: The MITRE ATT&CK Full Mapping

Every project maps to the industry-standard framework. This is not just labeling — understanding the MITRE technique behind each tool tells you what detection rules exist, what log sources are relevant, and what the defender sees.

ProjectToolMITRE TechniqueSub-TechniqueKill Chain Phase
01Port ScannerT1595 — Active ScanningT1595.001 — Port ScanReconnaissance
02Network MapperT1595 — Active ScanningT1595.002 — Vuln ScanReconnaissance
03Banner GrabberT1046 — Network Service DiscoveryDiscovery
04Packet SnifferT1040 — Network SniffingDiscovery / Credential Access
05Payload GeneratorT1059 — Command Scripting InterpreterT1059.004 — Unix ShellExecution
06Credential AuditorT1110 — Brute ForceT1110.001 / T1110.003Credential Access
07Web DeliveryT1105 — Ingress Tool TransferCommand & Control
08Service EnumeratorT1082 — System Information DiscoveryDiscovery
09Firewall TesterT1210 — Exploit Remote ServicesLateral Movement
10Multi ReconT1595 + T1046CombinedRecon + Discovery
11Chain SimulatorT1590 → T1595 → T1046 → T1105 → T1059Full chainCOMPLETE LIFECYCLE

The full chain in Project 11 is the point. T1590 (Gather Victim Network Info) → T1595 (Active Scanning) → T1046 (Service Discovery) → T1105 (Tool Transfer) → T1059 (Execution). That is the Cyber Kill Chain expressed in MITRE technique IDs. Every SOC that runs a modern SIEM has correlation rules for this sequence. Building it taught me exactly what those rules look for — and why behavioral analytics (detecting the sequence) catches what signature-based detection (detecting individual techniques) misses.


Part 7: The 6.5-Year Mindshift

After 18 days, the change in how I think about systems is more durable than any of the code I wrote.

DimensionHow I Thought BeforeHow I Think Now
System Design"How do I make this work correctly?""What happens when I send it something it doesn't expect?"
Logic GapsBugs — things to be filed and fixedAttack surface — the entry point between the intended state and the unintended one
ProtocolsAbstractions handled by librariesBinary streams with exploitable byte-order assumptions and compression pointers
AuthenticationThe security layer I implement at the application layerThe residual check after rate limiting, IP throttling, and behavioral analytics
Error MessagesHelpful feedback for debuggingInformation leaks that tell an attacker exactly which input they need to refine
Default ConfigurationsThe baseline that works out of the boxThe most likely attack vector in any first-time deployment
TimeoutsNetwork conditionsFirewall policy inference

The Shadow State Rule

Every line of code has two states: the intended state (what happens when the inputs are valid and the system behaves as designed) and the shadow state (what happens when you send a negative sequence number, a DNS label with a malformed compression pointer, or a credential attempt at 3am from a new IP).

As an engineer, I spent 6.5 years optimizing the intended state. Security is the discipline of identifying and closing the shadow states. Every tool I built in this module was a mechanism for finding shadow states systematically.

This is not a philosophical reframe. It changes the code I write:

  • When I implement authentication, I think about what the error response reveals to an unauthenticated caller. "Invalid username" vs "Invalid password" — same semantic meaning to a valid user, radically different information to a credential auditor.
  • When I design API endpoints, I think about what the response times reveal. A 230ms response for a valid user and a 2ms response for an invalid one tells an attacker whether the account exists before they've guessed a single password.
  • When I configure a service, I think about what the banner reveals. Version numbers in HTTP Server: headers are CVE correlation data for anyone running a banner grabber.

These considerations existed before Module 1. I just didn't have the visceral evidence to make them feel urgent.


Part 8: Honest Self-Assessment of the Arsenal

Six and a half years of production engineering means I know when something is a proof-of-concept and when it's production-ready. These tools are strong POCs with production-quality architecture. Here's the gap:

Rate limiting is static. The credential auditor uses hardcoded lockout thresholds. Active Directory defaults to 10 failures; some enterprise LDAP setups lock after 3. Real engagements need configurable-per-target thresholds.

No distributed mode. Every tool runs from a single source IP. Real adversaries distribute across multiple sources. Single-source testing is correct for authorized assessments but doesn't represent the full threat model.

The packet sniffer requires root. AF_PACKET requires elevated privileges on Linux. In a real post-exploitation scenario you may not have root at the point where you need to sniff. An unprivileged fallback using /proc/net/tcp polling would address this.

JSON output is end-of-run only. Long-running scans with large wordlists don't persist intermediate results. A streaming append-mode writer would fix the "crash loses everything" problem. This is the same bug pattern as a batch job without checkpointing.

No IPv6 coverage. The entire arsenal assumes IPv4. Modern networks run dual-stack. The tools need IPv6 socket support in the next iteration.

These are the items in the backlog. They're documented in the READMEs. Knowing what your tool doesn't do is as important as knowing what it does.


Part 9: What Module 2 Changes

As I move into Module 2 (Web Application Security), the attack surface shifts from the network layer to the application layer. The same layer I've spent 6.5 years building on.

The reconnaissance chain I built targets infrastructure. Module 2 targets the application logic — SQLi, XSS, CSRF, authentication bypasses, insecure direct object references. These are not network vulnerabilities. They are logic vulnerabilities. They exist in code I've written.

That's the uncomfortable promise of Module 2: I will spend the next 14 days systematically demonstrating how to exploit the same OWASP Top 10 vulnerabilities I've been patching for years. The SQL injection I prevented in a Django ORM query last year — I'm about to build the tool that finds it. The CSRF token I validated in a FastAPI endpoint — I'm about to build the forger.

Building the attack is the only way to understand why the defense works and, more importantly, when it doesn't.


Module 1 Final Verified Checklist

  • 11/11 Labs Completed (Network Discovery → Attack Chain Simulator)
  • Zero-Dependency Arsenal (Pure Python 3.10+ stdlib throughout)
  • 1,515-Line DPI Engine (Full RFC 793 state machine + DNS label parsing)
  • Complete Reconnaissance Chain (Port Scan → Map → Banner → Sniff → Credential)
  • Full Weaponization Suite (Payload Generator + Credential Auditor + Web Delivery)
  • Post-Exploitation Coverage (Local Enum + Firewall Analysis + Recon Engine)
  • Kill Chain Capstone (Automated Recon → Deliver → Access → Report)
  • MITRE ATT&CK Mapping (T1046 through T1595, full lifecycle)
  • Forensic Logging (Splunk-ingestible structured JSON throughout)
  • Mindset Documented (Shadow State rule, the seven perception shifts)

Day 18 of 90 | Module 1: ✅ 100% COMPLETE | Arsenal: 11 Tools Shipped | GitHub: kuldeepstechwork/applied-ai-security-projects | Next: Module 2 — Web Application Security


AI SecurityNetworkingOffensive SecurityMITRE ATT&CKCyber Kill ChainDeep Packet InspectionPythonRed Team
Continue Reading

More Engineering Insights

Browse All Technical Posts