The complete, in-depth guide to mastering Bash scripting for penetration testing, automation, and red team operations — from zero to advanced.
Bash (Bourne Again SHell) is a command-line interpreter and scripting language that comes pre-installed on virtually every Linux and macOS system. It was created by Brian Fox in 1989 as a free replacement for the original Bourne shell (sh). Today, Bash is the default shell on most Linux distributions — including Kali Linux, Parrot OS, and Ubuntu — making it the lingua franca of the Linux command line.
A Bash script is simply a text file containing a sequence of commands that the shell executes in order. But Bash is far more than a command runner — it's a full programming language with variables, loops, conditionals, functions, arrays, and even basic arithmetic. This makes it incredibly powerful for automation.
For ethical hackers and penetration testers, Bash is not optional — it's essential. Almost every tool you'll use (Nmap, Metasploit, Gobuster, Nikto, Hydra, Netcat) is either written in Bash, called from Bash, or best orchestrated through Bash. Mastering Bash means you can chain tools together, automate repetitive tasks, and build custom tooling — skills that separate a script kiddie from a professional.
You might wonder: "Why not just use Python for everything?" Great question. Python is fantastic, but Bash has unique advantages in the security world:
Every Linux box has Bash. No installation, no dependencies, no setup — just open a terminal and go.
Direct access to /dev/tcp, /proc, netstat, ps, and every system tool — no wrappers needed.
Chain Nmap → Grep → Awk → Curl in one line. Bash is the duct tape of the security toolkit.
Write a working recon script in 30 seconds. No compiler, no virtual environment, no boilerplate.
Bash scripts often bypass AV detection better than compiled binaries — they're just text files.
Every sysadmin, DevOps engineer, and security pro knows Bash. It's the common language.
Every Bash script starts with a shebang — a special first line that tells the operating system which interpreter to use. Without it, the system doesn't know your script is Bash.
#!/bin/bash
# This is a comment. It's ignored by the shell.
echo "Hello, ethical hacker!"
After writing your script, you need to give it execute permissions:
chmod +x myscript.sh
./myscript.sh
Every command follows this pattern: command [options] [arguments]
ls, nmap, curl)-l, --help)ls -la /var/log
# ls = command, -la = options, /var/log = argument
| Command | Purpose | Example |
|---|---|---|
ls | List directory contents | ls -la |
cd | Change directory | cd /etc |
pwd | Print working directory | pwd |
cat | View file contents | cat /etc/passwd |
grep | Search text patterns | grep "root" /etc/passwd |
find | Search for files | find / -name "*.conf" |
chmod | Change file permissions | chmod 755 script.sh |
ps | List running processes | ps aux |
netstat | Show network connections | netstat -tuln |
curl | Transfer data over HTTP | curl -I https://target.com |
Variables store data that you can reuse throughout your script. In Bash, you assign a value with = (no spaces around it!) and access it with $.
# Variable assignment (NO spaces around =)
target="192.168.1.100"
port=22
username="admin"
# Accessing variables (use $ prefix)
echo "Scanning $target on port $port"
# Best practice: always quote your variables
echo "User: $username"
$0 — Name of the script$1, $2, $3... — Command-line arguments$# — Number of arguments passed$@ — All arguments as separate strings$? — Exit code of the last command (0 = success)$$ — Process ID (PID) of the current script$USER — Current username$HOME — Home directory path$RANDOM — Random number between 0 and 32767Bash supports both indexed and associative arrays. Indexed arrays are perfect for wordlists, IP ranges, and port lists.
# Indexed array
ports=(21 22 80 443 3306 8080)
# Access an element
echo "First port: ${ports[0]}"
# Loop through all elements
for p in "${ports[@]}"; do
echo "Checking port $p"
done
# Array length
echo "Total ports: ${#ports[@]}"
read -p "Enter target IP: " target
read -sp "Enter password: " password
echo "Target: $target"
"$var". This prevents word splitting and glob expansion — two of the most common sources of Bash bugs.
Control flow is what makes scripts intelligent. Instead of running commands blindly, your script can make decisions and repeat actions.
if [ "$USER" = "root" ]; then
echo "[+] Running as root — full access"
elif [ "$USER" = "admin" ]; then
echo "[!] Running as admin — limited access"
else
echo "[-] Running as $USER — restricted"
fi
| Operator | Meaning | Type |
|---|---|---|
-eq | Equal to | Numeric |
-ne | Not equal to | Numeric |
-gt | Greater than | Numeric |
-lt | Less than | Numeric |
== or = | Equal to | String |
!= | Not equal | String |
-z | String is empty | String |
-n | String is not empty | String |
-f | File exists | File test |
-d | Directory exists | File test |
-x | File is executable | File test |
Perfect for iterating over lists, files, IP ranges, or wordlist entries.
# Loop over a range
for i in {1..10}; do
echo "Attempt $i"
done
# Loop over a file line by line
while IFS= read -r line; do
echo "Processing: $line"
done < targets.txt
# C-style for loop
for ((i=1; i<=254; i++)); do
ping -c 1 -W 1 "192.168.1.$i" &>/dev/null && echo "Live: 192.168.1.$i"
done
# While loop — runs as long as condition is TRUE
count=0
while [ $count -lt 5 ]; do
echo "Count: $count"
((count++))
done
# Until loop — runs UNTIL condition becomes TRUE
until [ -f "/tmp/flag" ]; do
sleep 1
done
Cleaner than multiple if/elif when matching many values.
case "$1" in
scan) echo "Running scan..." ;;
exploit) echo "Running exploit..." ;;
report) echo "Generating report..." ;;
*) echo "Usage: $0 {scan|exploit|report}" ;;
esac
Functions let you package reusable blocks of code. They make scripts shorter, cleaner, and easier to debug.
# Define a function
scan_port() {
host="$1"
port="$2"
timeout 1 bash -c "echo >/dev/tcp/$host/$port" 2>/dev/null
if [ $? -eq 0 ]; then
echo "[+] $host:$port OPEN"
fi
}
# Call the function
scan_port "192.168.1.1" 22
scan_port "192.168.1.1" 80
# Return values with echo (Bash functions return exit codes, not values)
get_ip() {
echo "192.168.1.50"
}
my_ip=$(get_ip)
echo "Target: $my_ip"
return. To return actual data, use echo and capture it with $(function_name).
Pipes and redirection are what make Bash so powerful. They let you connect commands together like Lego bricks — the output of one becomes the input of another.
|Send the output of one command directly into another:
# Find all IPs in a log file and count unique ones
cat access.log | grep -oE '[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' | sort -u | wc -l
# Check for open SSH port via Nmap output
nmap -p 22 192.168.1.1 | grep "open"
| Operator | Meaning |
|---|---|
> | Redirect stdout to a file (overwrite) |
>> | Redirect stdout to a file (append) |
< | Redirect file as stdin |
2> | Redirect stderr to a file |
2>&1 | Redirect stderr to same place as stdout |
&> | Redirect both stdout and stderr |
>/dev/null | Discard output (silence) |
# Save scan results to a file, suppress errors
nmap -sV 192.168.1.1 > scan.txt 2>/dev/null
# Append results
echo "[+] Scan complete at $(date)" >> scan.txt
# Silence both output streams
ping -c 1 192.168.1.1 &>/dev/null
90% of a pentester's job involves parsing text: extracting IPs from logs, filtering Nmap output, and pulling URLs from web pages. Master these three tools and you'll never struggle again.
# Basic search
grep "error" /var/log/syslog
# Case-insensitive
grep -i "failed" auth.log
# Recursive search
grep -r "password" /var/www/
# Invert match (show lines NOT containing pattern)
grep -v "#" config.conf
# Extended regex — extract IPs
grep -oE '([0-9]{1,3}\.){3}[0-9]{1,3}' access.log
# Print first column of a file
awk '{print $1}' users.txt
# Print last column
awk '{print $NF}' data.txt
# Filter by condition
awk '$3 > 1000 {print $1, $3}' traffic.log
# Custom delimiter (CSV)
awk -F',' '{print $2}' users.csv
# Replace text
sed 's/old/new/g' file.txt
# Delete lines matching a pattern
sed '/^#/d' config.conf
# Print only specific lines (5 to 10)
sed -n '5,10p' file.txt
# In-place edit (careful!)
sed -i 's/localhost/127.0.0.1/g' config.ini
cut — Extract fieldssort — Sort linesuniq — Remove duplicateswc — Count lines/wordstr — Translate charactershead/tail — First/last linespaste — Merge filesjq — Parse JSONBash scripts become truly powerful when they orchestrate these security tools. Here's what you'll use daily:
# Extract all open ports from an Nmap scan
nmap -p- -T4 192.168.1.1 -oG - | grep "open" | awk '{print $1, $2}'
# Find live hosts on a /24 subnet
for i in {1..254}; do
ping -c 1 -W 1 "192.168.1.$i" >/dev/null 2>&1 && echo "192.168.1.$i UP" &
done
wait
Quickly discover live hosts on a subnet using parallel background jobs.
#!/bin/bash
# Ping sweep an entire /24 subnet
subnet="${1:-192.168.1}"
echo "[*] Sweeping $subnet.0/24..."
for i in {1..254}; do
(
ping -c 1 -W 1 "$subnet.$i" >/dev/null 2>&1 && \
echo "[+] $subnet.$i is ALIVE"
) &
done
wait
echo "[✓] Sweep complete."
Uses Bash's built-in /dev/tcp — no external tools needed.
#!/bin/bash
# Port scanner using /dev/tcp
target="$1"
ports=(21 22 23 25 53 80 110 143 443 445 3306 3389 8080)
if [ -z "$target" ]; then
echo "Usage: $0 <target-ip>"
exit 1
fi
echo "[*] Scanning $target..."
for port in "${ports[@]}"; do
timeout 1 bash -c "echo >/dev/tcp/$target/$port" 2>/dev/null
if [ $? -eq 0 ]; then
echo " [+] Port $port OPEN"
fi
done
echo "[✓] Done."
Lightweight Gobuster alternative using curl.
#!/bin/bash
# Simple web directory brute-forcer
url="$1"
wordlist="$2"
if [ ! -f "$wordlist" ]; then
echo "Wordlist not found: $wordlist"
exit 1
fi
while IFS= read -r dir; do
code=$(curl -s -o /dev/null -w "%{http_code}" "$url/$dir")
if [ "$code" != "404" ]; then
echo "[+] $url/$dir → $code"
fi
done < "$wordlist"
Blue-team style monitoring of SSH authentication logs.
#!/bin/bash
# Watch for failed SSH logins
logfile="/var/log/auth.log"
echo "[*] Monitoring $logfile for failed logins..."
tail -Fn0 "$logfile" | while read -r line; do
if echo "$line" | grep -q "Failed password"; then
ip=$(echo "$line" | grep -oE '([0-9]{1,3}\.){3}[0-9]{1,3}')
echo "[!] Brute-force attempt from: $ip"
fi
done
Loop through subdomains, run dig and whois, and log results automatically.
Ping sweep an entire subnet and identify live hosts in seconds with parallel jobs.
Wrap nmap, gobuster, or hydra into repeatable workflows.
Extract IPs, errors, and patterns from massive log files with grep + awk.
Monitor auth logs and alert on suspicious SSH login attempts in real time.
Automate file transfers, reverse shells, and post-exploitation cleanup.
Collect scan results and format them into clean reports automatically.
"$var" to prevent word splitting and glob expansion.set -euo pipefail — Fail fast on errors, undefined variables, and pipe failures.$? and act on failures.mktemp for temp files — Avoid predictable filenames in /tmp.#!/bin/bash
set -euo pipefail
# -e : Exit immediately if a command fails
# -u : Treat unset variables as errors
# -o pipefail : Catch failures in piped commands
set -x temporarily when debugging. It prints every command before executing it, making it easy to spot where things go wrong.
Professional scripts don't crash silently. They detect problems, log them, and either recover or exit cleanly.
nmap -sV 192.168.1.1 -oN scan.txt
if [ $? -ne 0 ]; then
echo "[!] Nmap failed. Check target and permissions." >&2
exit 1
fi
echo "[+] Scan completed successfully."
Clean up on exit — even if the user presses Ctrl+C.
cleanup() {
echo "[*] Cleaning up temporary files..."
rm -f "/tmp/scan_$$.tmp"
}
trap cleanup EXIT INT TERM
tmpfile="/tmp/scan_$$.tmp"
nmap -sV 192.168.1.1 > "$tmpfile"
error() {
echo "[ERROR] $1" >&2
exit 1
}
[ $# -lt 1 ] && error "Usage: $0 <target>"
[ ! -f "$1" ] && error "File not found: $1"
a=10
b=3
echo $((a + b)) # 13
echo $((a - b)) # 7
echo $((a * b)) # 30
echo $((a / b)) # 3
echo $((a % b)) # 1
echo $((a ** b)) # 1000
# Increment
((a++))
echo "$a" # 11
Treat command output as a file — powerful for diffing and comparisons.
# Compare output of two commands
diff <(nmap 192.168.1.1) <(nmap 192.168.1.2)
# Feed multiple inputs to a single command
cat <(echo "host1") <(echo "host2") > hosts.txt
# Run tasks in parallel and wait for all to finish
for ip in 192.168.1.{1..10}; do
(nmap -T4 -p 22,80,443 "$ip" > "scan_$ip.txt") &
done
wait
echo "[+] All scans finished."
cat <<EOF > payload.sh
#!/bin/bash
echo "Hello from payload"
EOF
chmod +x payload.sh
# Preserve whitespace and backslashes with IFS= and -r
while IFS= read -r line; do
echo "Line: $line"
done < targets.txt
str="https://example.com/path"
echo ${str#https://} # example.com/path (remove prefix)
echo ${str%.com*} # https://example (remove suffix)
echo ${str/https/http} # http://example.com/path (replace)
echo ${#str} # string length
echo ${str^^} # HTTPS://EXAMPLE.COM/PATH
echo ${str,,} # lowercase
Bash scripting is the backbone of Linux-based ethical hacking. It's the skill that transforms you from someone who runs tools into someone who orchestrates entire operations. Every professional penetration tester, red teamer, and security engineer has Bash in their toolbox — and for good reason.
The journey from beginner to expert follows a clear path:
Remember: the terminal is your weapon, and Bash is your language. Master it, and you'll be able to build custom tooling that rivals commercial products — all with plain text and a shell.
Happy hacking — ethically. 🐉