| Tested on | Kali GNU/Linux Rolling 2026.2 (kali-rolling) |
|---|---|
| Package | bash 5.3.9python3 3.14.6nmap 7.99+dfsg-1kali1curl 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 |
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
forloop, 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
grepandawk
Set $TARGET from your pentest lab guide before you start.
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
forandwhileover IP lists or port arrays if/elif/elseon 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:
- Run network reconnaissance with Nmap for a broad port scan
- Modify a short Bash wrapper to act only when Nmap reports a specific port
- Use Python or banner grabbing on confirmed services
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:
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 --versionSample 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:
python3 --versionSample output:
Python 3.14.6Any 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/tcpprobe 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 recognizeecho "total open ports: $OPEN_COUNT"prints a summary line for reporting
Save $LAB/port-scan.sh:
#!/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:
chmod +x "$LAB/port-scan.sh"Run it against Metasploitable:
bash "$LAB/port-scan.sh" "$TARGET"Sample 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: 8Eight 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:
PORTS=(21 22 23 25 80 139 445 3306)with a scoped list:
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 "$LAB/port-scan-narrow.sh" "$TARGET"Sample output:
open 22/tcp
open 80/tcp
total open ports: 2Only 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:
timeout 2 bash -c "echo >/dev/tcp/${target}/${port}" 2>/dev/nullOpen 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 "$LAB/subnet-ping.sh" "$SUBNET" 127Sample output:
up 192.168.56.1
up 192.168.56.120
up 192.168.56.127Three 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:
ports = [80, 443, 8080, 8443]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:
#!/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"
fiThe 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:
chmod +x "$LAB/nmap-http-check.sh"Run it against $TARGET:
bash "$LAB/nmap-http-check.sh" "$TARGET"Sample 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/htmlHTTP/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
Serverheader
Why Python here:
socket.connect_exreturns an error code instead of raising an exception when a port is closedsock.settimeout(1.0)limits how long each connection attempt can waiturllib.requestfetches the page and reads response headers- Everything comes from the standard library. No
pip installis required
Save $LAB/http-probe.py:
#!/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__}")What each step does:
connect_exreturns0when the TCP handshake succeeds. Any other value means skip HTTP for that port.- Ports
443and8443use HTTPS in this script. The others use HTTP. On a real engagement, fingerprint the service instead of guessing from the port number alone. urlopensends a GET request.resp.statusis the HTTP code (200means OK).- The
exceptblock still reports open ports where HTTP fails (for example a TLS error on443).
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:
python3 "$LAB/http-probe.py" "$TARGET"Sample output:
80/tcp open status=200 server=Apache/2.2.8 (Ubuntu) DAV/2Port 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:
#!/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
doneMake it executable:
chmod +x "$LAB/subnet-ping.sh"Sweep the host-only segment (use 127 when Metasploitable sits at .127):
bash "$LAB/subnet-ping.sh" "$SUBNET" 127Sample output:
up 192.168.56.1
up 192.168.56.120
up 192.168.56.127What those addresses usually are:
.1— VirtualBox host adapter.120— Kali (your attacker VM).127— Metasploitable 2 (set$TARGETto 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:
#!/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:
chmod +x "$LAB/retry-probe.sh"Run it against port 80 on $TARGET:
bash "$LAB/retry-probe.sh" "$TARGET" 80Sample output:
probe attempt 1
port 80/tcp open on attempt 1Port 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 "$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:
grep '^open' "$LAB/scan.txt"Sample output:
open 21/tcp
open 22/tcp
open 23/tcp
open 25/tcp
open 80/tcp
open 139/tcp
open 445/tcp
open 3306/tcpEach line starts with open, which makes later filtering easy.
Pull only the port numbers for a CSV or report column:
grep '^open' "$LAB/scan.txt" | awk '{print $2}' | sed 's|/tcp||'Sample output:
21
22
23
25
80
139
445
3306awk prints the second field (21/tcp, 22/tcp, …). sed removes the /tcp suffix.
After a ping sweep, save live hosts the same way:
bash "$LAB/subnet-ping.sh" "$SUBNET" 127 | tee "$LAB/live-hosts.txt"Remove duplicate up lines if you ran the sweep more than once:
sort -u "$LAB/live-hosts.txt"Sample output:
up 192.168.56.1
up 192.168.56.120
up 192.168.56.127sort -u keeps one copy of each unique line.
Good filename habits:
- Include the target IP and date (
date +%FprintsYYYY-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
curlwhen port 80 is open - Python HTTP probe with
socket.connect_exandurllib - Ping sweep plus
grep,awk, andsort -uon saved output whileloop with((attempt++))for probe retries
Watch for these pitfalls:
- Wrong
$TARGETor 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
- GNU Bash manual — shell loops
- GNU Bash manual — conditional expressions
- Python 3 — socket library
- Python 3 — urllib.request
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.

