Modify Bash and Python Scripts for Recon and Enumeration

Tested on Kali GNU/Linux Rolling 2026.2 (kali-rolling)
Package bash 5.3.9
python3 3.14.6
nmap 7.99+dfsg-1kali1
curl 8.21.0
Lab environment Pentest lab
Privilege Normal user for scripts shown; sudo only when your engagement policy requires elevated capture or install steps
Scope Modify Bash and Python recon scripts—loops, functions, arithmetic operators, conditionals, data manipulation, and standard libraries—for port probes, ping sweeps, HTTP checks, and Nmap wrappers against Metasploitable 2. Covers PenTest+ Objective 2.3; not exploit development or post-exploitation frameworks.
Related guides Network sniffing with Wireshark and tcpdump
Short on time?Beta Get the gist of this 15-minute read in seconds, then ask follow-up questions

PenTest+ Objective 2.3 tests whether you can modify recon scripts, not only run them as-is. On the exam you usually get a short Bash or Python snippet and must:

  • Change a port list, timeout, or loop range
  • Predict what prints after the edit

In this walkthrough you will:

  • Build a Bash port scanner with a function, a for loop, and an arithmetic counter
  • Modify that script for a narrower engagement scope
  • Wrap Nmap output with a conditional HTTP header pull
  • Run a Python HTTP probe and adjust its port list
  • Ping-sweep the host-only subnet and parse saved output with grep and awk

Set $TARGET from your pentest lab guide before you start.

IMPORTANT
Run recon scripts only against IP ranges and rates defined in your rules of engagement. Automated probes against out-of-scope hosts look the same as unauthorized scanning in client logs.

What is recon scripting on PenTest+?

Recon scripting automates small enumeration steps:

  • Checking whether ports answer
  • Grabbing HTTP headers
  • Listing ICMP-responsive hosts on a subnet

On the exam you usually receive an existing script and must fix or extend it. Objective 2.3 covers:

  • Loops such as for and while over IP lists or port arrays
  • if / elif / else on exit codes or file tests
  • Boolean, string, and arithmetic operators (&&, ||, -eq, ((OPEN_COUNT++)))
  • Functions, libraries, and (in snippets) classes
  • Data manipulation: filtering, sorting, and transforming saved output

PowerShell is also listed in Objective 2.3. This lesson keeps Bash and Python together because they run natively on Kali. For Windows and Active Directory scripting, see PowerShell for pentesters.

Scripts do not replace dedicated tools. A typical workflow:


Lab variables for Metasploitable 2

Variable What it holds
TARGET Metasploitable 2 IP from your lab guide
LAB Folder for scripts and saved output (/tmp/recon-scripting-lab)
SUBNET First three octets of the host-only network (192.168.56)

On the VirtualBox host-only layout with DHCP disabled, addresses look like this:

  • VirtualBox host adapter: 192.168.56.1
  • Kali (attacker): 192.168.56.120
  • Metasploitable 2 (target): 192.168.56.127

Each VM uses a static address from your pentest lab guide. Run ip -4 addr on each VM if your local layout differs.

Export the lab variables once per terminal session. These names are reused in every script below:

bash
TARGET=192.168.56.127
LAB=/tmp/recon-scripting-lab
SUBNET=192.168.56
mkdir -p "$LAB"

$LAB is where you save scripts and scan output. mkdir -p creates the folder if it does not exist yet.

Confirm Bash and Python are available before you write scripts:

bash
bash --version

Sample output:

output
GNU bash, version 5.3.9(1)-release (x86_64-pc-linux-gnu)

A 5.x release on Kali is fine. Bash also provides the /dev/tcp probes used later in this guide.

Check Python 3 next for the HTTP script:

bash
python3 --version

Sample output:

output
Python 3.14.6

Any recent Python 3.x on Kali works. The outputs in this guide were captured on the versions above.


Bash port probe with a function and arithmetic counter

Start from a script you can modify on the exam: a for loop, a reusable function, and an arithmetic counter that tracks how many ports answered.

How the pieces fit together:

  • check_port() wraps the /dev/tcp probe so the loop stays readable
  • ((OPEN_COUNT++)) increments a counter each time a port is open—an arithmetic operator Objective 2.3 expects you to recognize
  • echo "total open ports: $OPEN_COUNT" prints a summary line for reporting

Save $LAB/port-scan.sh:

bash
#!/usr/bin/env bash
TARGET="${1:?usage: port-scan.sh <target>}"
PORTS=(21 22 23 25 80 139 445 3306)
OPEN_COUNT=0

check_port() {
  local target="$1"
  local port="$2"
  timeout 1 bash -c "echo >/dev/tcp/${target}/${port}" 2>/dev/null
}

for port in "${PORTS[@]}"; do
  if check_port "$TARGET" "$port"; then
    ((OPEN_COUNT++))
    echo "open ${port}/tcp"
  fi
done
echo "total open ports: $OPEN_COUNT"

Make the script executable:

bash
chmod +x "$LAB/port-scan.sh"

Run it against Metasploitable:

bash
bash "$LAB/port-scan.sh" "$TARGET"

Sample output:

output
open 21/tcp
open 22/tcp
open 23/tcp
open 25/tcp
open 80/tcp
open 139/tcp
open 445/tcp
open 3306/tcp
total open ports: 8

Eight open ports printed, and total open ports: 8 at the bottom. That summary line is the kind of edit exam items ask you to add with ((OPEN_COUNT++)).


Modify the recon scripts for a new target

Objective 2.3 tests whether you can change an existing script and predict the new behavior. You will practice four common edits:

  • Narrow the port list to match rules of engagement (RoE)
  • Increase a probe timeout on slow targets
  • Widen or shrink a ping-sweep range
  • Add HTTP ports in Python and wrap Nmap with curl

Narrow the port list

Your RoE may allow only SSH and web checks. Replace the full Metasploitable port array:

bash
PORTS=(21 22 23 25 80 139 445 3306)

with a scoped list:

bash
PORTS=(22 80 443)

Only the PORTS= line changes. Keep the same check_port function and ((OPEN_COUNT++)) counter from $LAB/port-scan.sh.

Save the narrowed copy as $LAB/port-scan-narrow.sh, then run it:

bash
bash "$LAB/port-scan-narrow.sh" "$TARGET"

Sample output:

output
open 22/tcp
open 80/tcp
total open ports: 2

Only two lines print because Metasploitable answers on 22 and 80 but not on 443 in this lab. The counter drops from 8 to 2. That output change is what many exam items describe.

Increase the probe timeout

On a slow or loaded target, edit check_port and change timeout 1 to timeout 2:

bash
timeout 2 bash -c "echo >/dev/tcp/${target}/${port}" 2>/dev/null

Open ports still print the same lines. The longer timeout mainly helps when a one-second probe fails on a filtered or busy host.

Widen or shrink the ping sweep range

In $LAB/subnet-ping.sh, the second argument sets the highest last octet to ping. A sweep to 127 finishes faster than 130 on a /24:

bash
bash "$LAB/subnet-ping.sh" "$SUBNET" 127

Sample output:

output
up 192.168.56.1
up 192.168.56.120
up 192.168.56.127

Three up lines mean three hosts answered ICMP in that range. Raise the second number to include more addresses; lower it when you already know Metasploitable sits at .127.

Add an HTTP port in Python

In $LAB/http-probe.py, extend the port list when your scope includes an alternate web port:

python
ports = [80, 443, 8080, 8443]
Output

Metasploitable does not listen on 8080 in this lab, so output stays the same until a target actually opens that port. Then a new line appears without rewriting the whole script.

Wrap Nmap and pull headers when port 80 is open

Real recon automation usually wraps tools you already trust. This script runs Nmap first, then calls curl only when port 80 is open.

Save $LAB/nmap-http-check.sh:

bash
#!/usr/bin/env bash
TARGET="${1:?usage: nmap-http-check.sh <target>}"
if nmap -Pn -p80,443 --open "$TARGET" 2>/dev/null | grep -q "80/tcp.*open"; then
  echo "port 80 open per nmap — fetching headers:"
  curl -sI "http://${TARGET}/" | head -5
else
  echo "port 80 not open — skipping HTTP probe"
fi

The if reads Nmap output. Only when 80/tcp is open does the script call curl. See the curl command cheat sheet for header and redirect flags if you extend this wrapper.

Make the wrapper executable:

bash
chmod +x "$LAB/nmap-http-check.sh"

Run it against $TARGET:

bash
bash "$LAB/nmap-http-check.sh" "$TARGET"

Sample output:

output
port 80 open per nmap — fetching headers:
HTTP/1.1 200 OK
Date: Mon, 31 Aug 2026 16:48:40 GMT
Server: Apache/2.2.8 (Ubuntu) DAV/2
X-Powered-By: PHP/5.2.4-2ubuntu5.10
Content-Type: text/html

HTTP/1.1 200 OK means the web server answered. The Server and X-Powered-By lines are quick fingerprint hints for your notes.


Python HTTP probe with sockets and urllib

The Bash port scanner only tests TCP reachability. This Python script goes one step further:

  • Opens a TCP connection with socket.connect_ex
  • Sends an HTTP request with urllib.request
  • Prints the status code and Server header

Why Python here:

  • socket.connect_ex returns an error code instead of raising an exception when a port is closed
  • sock.settimeout(1.0) limits how long each connection attempt can wait
  • urllib.request fetches the page and reads response headers
  • Everything comes from the standard library. No pip install is required

Save $LAB/http-probe.py:

python
#!/usr/bin/env python3
import socket
import sys
import urllib.request

target = sys.argv[1] if len(sys.argv) > 1 else "127.0.0.1"
ports = [80, 443, 8080, 8443]

for port in ports:
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
        sock.settimeout(1.0)
        if sock.connect_ex((target, port)) != 0:
            continue
    scheme = "https" if port in (443, 8443) else "http"
    url = f"{scheme}://{target}:{port}/"
    try:
        with urllib.request.urlopen(url, timeout=3) as resp:
            server = resp.headers.get("Server", "-")
            print(f"{port}/tcp open status={resp.status} server={server}")
    except Exception as exc:
        print(f"{port}/tcp open http_error={exc.__class__.__name__}")
Output

What each step does:

  • connect_ex returns 0 when the TCP handshake succeeds. Any other value means skip HTTP for that port.
  • Ports 443 and 8443 use HTTPS in this script. The others use HTTP. On a real engagement, fingerprint the service instead of guessing from the port number alone.
  • urlopen sends a GET request. resp.status is the HTTP code (200 means OK).
  • The except block still reports open ports where HTTP fails (for example a TLS error on 443).

Exam snippets may show Python classes. This small script does not need one. A class is useful when you package target state and several methods in one reusable object.

Run the probe against $TARGET:

bash
python3 "$LAB/http-probe.py" "$TARGET"

Sample output:

output
80/tcp open status=200 server=Apache/2.2.8 (Ubuntu) DAV/2

Port 80 is open, returned HTTP 200, and Apache identified itself in the Server header. Other ports in the list either stayed closed or hit the except branch.


Bash subnet discovery with ping loops

Before you port-scan, you may want a list of ICMP-responsive hosts on the lab subnet. This script pings each address from .1 through the last octet you choose.

Keep these limits in mind:

  • A host that does not answer ICMP may still be online
  • Treat this as ICMP-responsive discovery, not proof that every silent address is down
  • Use Nmap when ICMP is filtered and you need broader host discovery

Save $LAB/subnet-ping.sh:

bash
#!/usr/bin/env bash
PREFIX="${1:?usage: subnet-ping.sh <prefix like 192.168.56>}"
LAST="${2:-30}"
for host in $(seq 1 "$LAST"); do
  ip="${PREFIX}.${host}"
  if ping -c1 -W1 "$ip" >/dev/null 2>&1; then
    echo "up $ip"
  fi
done

Make it executable:

bash
chmod +x "$LAB/subnet-ping.sh"

Sweep the host-only segment (use 127 when Metasploitable sits at .127):

bash
bash "$LAB/subnet-ping.sh" "$SUBNET" 127

Sample output:

output
up 192.168.56.1
up 192.168.56.120
up 192.168.56.127

What those addresses usually are:

  • .1 — VirtualBox host adapter
  • .120 — Kali (your attacker VM)
  • .127 — Metasploitable 2 (set $TARGET to this line before you port-scan)

While loops for probe retries

Objective 2.3 also lists while loops. Use one when a probe may fail once on a busy host and you want a few retries before giving up.

Save $LAB/retry-probe.sh:

bash
#!/usr/bin/env bash
TARGET="${1:?usage: retry-probe.sh <target>}"
PORT="${2:-80}"
attempt=1
while [ "$attempt" -le 3 ]; do
  echo "probe attempt $attempt"
  if timeout 1 bash -c "echo >/dev/tcp/${TARGET}/${PORT}" 2>/dev/null; then
    echo "port ${PORT}/tcp open on attempt $attempt"
    break
  fi
  ((attempt++))
done

((attempt++)) is the same arithmetic style as ((OPEN_COUNT++)) in the port scanner. break exits the while loop as soon as a port answers.

Make the retry script executable:

bash
chmod +x "$LAB/retry-probe.sh"

Run it against port 80 on $TARGET:

bash
bash "$LAB/retry-probe.sh" "$TARGET" 80

Sample output:

output
probe attempt 1
port 80/tcp open on attempt 1

Port 80 answered on the first try, so the loop never reached attempt 2.


Compare Bash and Python for recon tasks

Task Bash approach Python approach
Quick TCP open check /dev/tcp with timeout socket.connect_ex
HTTP header grab curl -sI in a loop urllib.request.urlopen
Host discovery ping + seq loop subprocess.run(["ping", ...])
Parse JSON API OSINT Awkward without jq json module
Glue existing CLI tools Native (nmap, dig) subprocess.run

Pick Bash when:

  • The script mostly calls tools you already know (ping, curl, nmap)
  • You need a quick one-off loop with no extra packages

Pick Python when:

  • You need structured error handling (try / except)
  • Output must feed a report parser or JSON file
  • You are working with sockets, HTTP, or APIs in one script

Operators and constructs to recognize on the exam

Construct Example in this lab Typical recon use
for … in for port in "${PORTS[@]}" Walk ports or IP lists
while while [ "$attempt" -le 3 ] Retry a flaky probe
if on exit code if check_port …; then Host or port up/down
&& / || nmap … | grep -q … && curl … Chain scan and follow-up
((var++)) ((OPEN_COUNT++)) Count open ports
:? parameter expansion ${1:?usage…} Require target argument
${var:-default} ${2:-30} Optional scan range
Function check_port() { … } Reuse one probe in a loop
socket.connect_ex Python HTTP probe TCP connection test with return code
urllib.request Fetch Server header HTTP fingerprint

On a real engagement, change port lists and subnet prefixes to match your written scope. Never hard-code addresses you are not authorized to touch.


Save output and manipulate scan data

Pipe script output through tee so you see results on screen and keep a file for later parsing:

bash
bash "$LAB/port-scan.sh" "$TARGET" | tee "$LAB/scan.txt"

tee writes the same lines to the terminal and to scan.txt.

Objective 2.3 also expects basic data manipulation. List only the open-port lines from the saved file:

bash
grep '^open' "$LAB/scan.txt"

Sample output:

output
open 21/tcp
open 22/tcp
open 23/tcp
open 25/tcp
open 80/tcp
open 139/tcp
open 445/tcp
open 3306/tcp

Each line starts with open, which makes later filtering easy.

Pull only the port numbers for a CSV or report column:

bash
grep '^open' "$LAB/scan.txt" | awk '{print $2}' | sed 's|/tcp||'

Sample output:

output
21
22
23
25
80
139
445
3306

awk prints the second field (21/tcp, 22/tcp, …). sed removes the /tcp suffix.

After a ping sweep, save live hosts the same way:

bash
bash "$LAB/subnet-ping.sh" "$SUBNET" 127 | tee "$LAB/live-hosts.txt"

Remove duplicate up lines if you ran the sweep more than once:

bash
sort -u "$LAB/live-hosts.txt"

Sample output:

output
up 192.168.56.1
up 192.168.56.120
up 192.168.56.127

sort -u keeps one copy of each unique line.

Good filename habits:

  • Include the target IP and date (date +%F prints YYYY-MM-DD)
  • Name the check type so you know what each file contains later

Recon scripting troubleshooting

Symptom Likely cause Fix
Script prints nothing Wrong $TARGET or filtered ports Ping $TARGET; widen port list; compare with Nmap
Hangs on one port Missing timeout Add timeout 1 on Bash probes or sock.settimeout in Python
/dev/tcp fails on minimal shell Dash or BusyBox without the feature Use nc -zv or Python socket instead
permission denied on ping Some networks block ICMP from non-root Use TCP SYN probe script or Nmap -sn
Python urlopen SSL errors on 443 Self-signed cert on lab HTTPS Use http:// on port 80 for this lesson; see TLS guides for verify flags
up hosts missing in sweep Wrong SUBNET prefix or ICMP filtered Confirm with ip -4 addr; remember silent hosts may still be up
Subnet sweep feels stuck Large last-octet range with -W1 Lower the second argument (for example 127) or accept ~1 second per dead host
Narrow port list shows fewer opens Ports not in RoE or not listening Expected—verify the array matches scope and the target

Summary

You built and modified recon scripts on Kali against Metasploitable 2:

  • Bash port scanner with check_port(), ((OPEN_COUNT++)), and a scoped port-list edit
  • Nmap wrapper that conditionally runs curl when port 80 is open
  • Python HTTP probe with socket.connect_ex and urllib
  • Ping sweep plus grep, awk, and sort -u on saved output
  • while loop with ((attempt++)) for probe retries

Watch for these pitfalls:

  • Wrong $TARGET or out-of-scope IPs
  • Missing timeouts on network probes (scripts can hang on filtered ports)
  • ICMP sweeps that miss hosts when ping is blocked

On engagements, keep Nmap for the broad picture and use short Bash or Python helpers for checks you repeat every test. Feed confirmed services into Nmap NSE scripts when you need scripted follow-up beyond your own loops.


References


Frequently Asked Questions

Why write recon scripts instead of only using Nmap?

Nmap remains the primary scanner, but short Bash and Python scripts let you chain exactly the checks your engagement needs, parse output for reporting, and adapt when a client blocks full port scans. PenTest+ expects you to read and modify loops, conditionals, and libraries in existing scripts—not replace professional scanners entirely.

How do I modify a recon script for PenTest+?

Start from the script fragment in the question. Common edits: change the PORTS or IP array, adjust a timeout value, add ((counter++)), wrap an external command in if, or add a second argument with ${2:-default}. Run the logic in your head—or on a lab VM—and count which lines print.

When should I use Bash versus Python for recon?

Bash excels at gluing shell tools together—ping sweeps, calling nc or curl in loops, and quick /dev/tcp probes. Python fits structured parsing, socket timeouts, HTTP libraries, and JSON APIs. Many engagements use Bash for host discovery and Python for service-specific follow-up.

What Python libraries appear on PenTest+ scripting questions?

Exam scenarios often reference the standard library—socket for TCP probes, urllib or http.client for HTTP headers, and sys for arguments. Third-party requests is common in practice but not always preinstalled; urllib keeps lab scripts portable without pip installs.

Does Kali Linux support Bash /dev/tcp probes?

Yes on the default Bash shell. /dev/tcp/host/port is a Bash feature, not a separate binary. Dash, BusyBox, or minimal shells may not support it—use nc -zv or Python socket instead.

Why does my ping sweep miss hosts?

Many hosts block ICMP echo requests. A silent address is not proof the host is down—use Nmap -sn or TCP-based discovery when ICMP is filtered.

How do I avoid hanging recon scripts?

Always set timeouts on network calls—timeout 1 on Bash /dev/tcp probes, sock.settimeout in Python, and short curl or urllib timeouts on HTTP checks. Without timeouts, a filtered port can stall your loop for minutes.

Finished reading?Beta Get the key takeaways in seconds, then ask follow-up questions
Deepak Prasad

R&D Engineer

Founder of GoLinuxCloud with 15+ years of expertise in Linux, Python, Go, Laravel, DevOps, Kubernetes, Git, Shell scripting, OpenShift, AWS, Networking, and Security. With extensive experience, he excels across development, DevOps, networking, and security, delivering robust and efficient solutions for diverse projects.

  • Go (programming language)
  • Python (programming language)
  • DevOps
  • Computer Security
  • Cloud Computing
  • Kubernetes
  • Linux
  • Ansible (software)