
Master bash scripting fundamentals for penetration testing and ethical hacking. Learn variables, loops, conditionals, functions, and build practical automation scripts for port scanning, subd
Learn how to install Kali Linux dual boot alongside Windows 10/11. Complete step-by-step guide with partitioning, GRUB setup, troubleshooting, and safety warnings to prevent data loss.
13 min read
Transparent vs UUPS, uninitialized implementations, and storage-layout collisions — local forge inspect lab. No live upgrades.
35 min read
In the world of penetration testing and ethical hacking, bash scripting is one of the most powerful skills you can develop. While manual commands work for one-off tasks, automation through bash scripts enables you to perform complex operations efficiently, consistently, and at scale.
Whether you're conducting reconnaissance, analyzing logs, brute-forcing credentials, or enumerating targets, bash scripting transforms repetitive tasks into executable workflows. This tutorial will teach you the fundamentals of bash scripting specifically tailored for cybersecurity professionals and ethical hackers.
Prerequisites: Basic Linux terminal knowledge and familiarity with essential Linux commands.
Bash scripting enables penetration testers to:
✅ Automate Repetitive Tasks — Eliminate manual execution of the same commands across multiple targets
✅ Increase Efficiency — Run complex multi-stage attacks with a single command
✅ Ensure Consistency — Execute tests the same way every time, reducing human error
✅ Scale Operations — Process hundreds or thousands of targets simultaneously
✅ Create Custom Tools — Build specialized utilities tailored to your specific needs
✅ Chain Multiple Tools — Integrate Nmap, Metasploit, Burp Suite, and other tools seamlessly
✅ Schedule Reconnaissance — Set up automated cron jobs for continuous monitoring
Every professional penetration tester uses bash scripting daily. It's the glue that connects individual tools into powerful automated workflows.
Every bash script starts with a shebang (#!) that tells the system which interpreter to use:
#!/bin/bash
This line must be the first line of your script. It specifies the absolute path to the bash interpreter.
Alternative shebangs:
#!/usr/bin/env bash # More portable, finds bash in PATH
#!/bin/sh # POSIX shell (less features)
#!/bin/bash
# Script: basic_template.sh
# Description: Template for bash scripts
# Author: Andrax Pentester
# Date: 2026
# Variables
TARGET="192.168.1.1"
PORT=80
# Main logic
echo "Scanning target: $TARGET"
echo "Port: $PORT"
# Exit with status code
exit 0
After creating a script, make it executable:
chmod +x script.sh
./script.sh
Variables store data for use throughout your script.
#!/bin/bash
# Variable assignment (no spaces around =)
target="example.com"
port=443
protocol="https"
# Using variables (prefix with $)
echo "Target: $target"
echo "URL: ${protocol}://${target}:${port}"
Best practices:
= sign${variable} for clarity and to prevent ambiguity"$variable"Environment variables are available system-wide:
#!/bin/bash
# Read environment variables
echo "Current user: $USER"
echo "Home directory: $HOME"
echo "Current path: $PATH"
# Set environment variable for child processes
export API_KEY="your-api-key-here"
Capture arguments passed to your script:
#!/bin/bash
# $0 = script name
# $1, $2, $3... = arguments
# $# = number of arguments
# $@ = all arguments as separate words
# $* = all arguments as a single word
echo "Script name: $0"
echo "First argument: $1"
echo "Second argument: $2"
echo "Total arguments: $#"
echo "All arguments: $@"
Usage:
./script.sh 192.168.1.1 80 tcp
# $1 = 192.168.1.1
# $2 = 80
# $3 = tcp
$? # Exit status of last command (0 = success)
$$ # Process ID of current script
$! # Process ID of last background command
#!/bin/bash
port=$1
if [ -z "$port" ]; then
echo "Error: No port specified"
exit 1
elif [ "$port" -lt 1 ] || [ "$port" -gt 65535 ]; then
echo "Error: Port must be between 1-65535"
exit 1
else
echo "Valid port: $port"
fi
Numeric comparisons:
-eq # Equal to
-ne # Not equal to
-lt # Less than
-le # Less than or equal to
-gt # Greater than
-ge # Greater than or equal to
String comparisons:
= # Equal to
!= # Not equal to
-z # String is empty
-n # String is not empty
File tests:
-e # File exists
-f # File is a regular file
-d # File is a directory
-r # File is readable
-w # File is writable
-x # File is executable
-s # File is not empty
For multiple conditions:
#!/bin/bash
service=$1
case $service in
ssh)
port=22
;;
http)
port=80
;;
https)
port=443
;;
mysql)
port=3306
;;
*)
echo "Unknown service: $service"
exit 1
;;
esac
echo "$service runs on port $port"
Iterate over a list:
#!/bin/bash
# Loop through IP addresses
for ip in 192.168.1.1 192.168.1.2 192.168.1.3; do
echo "Scanning $ip"
ping -c 1 "$ip" > /dev/null 2>&1
if [ $? -eq 0 ]; then
echo "$ip is alive"
fi
done
Range-based loop:
#!/bin/bash
# Scan ports 1-100
for port in {1..100}; do
timeout 1 bash -c "echo > /dev/tcp/192.168.1.1/$port" 2>/dev/null
if [ $? -eq 0 ]; then
echo "Port $port is open"
fi
done
C-style loop:
#!/bin/bash
for ((i=1; i<=10; i++)); do
echo "Attempt $i"
done
Execute while condition is true:
#!/bin/bash
count=1
while [ $count -le 5 ]; do
echo "Try $count"
((count++))
done
Reading files line by line:
#!/bin/bash
while IFS= read -r line; do
echo "Processing: $line"
done < targets.txt
Execute until condition becomes true:
#!/bin/bash
count=1
until [ $count -gt 5 ]; do
echo "Count: $count"
((count++))
done
Functions organize reusable code blocks.
#!/bin/bash
# Define function
check_host() {
local host=$1
ping -c 1 "$host" > /dev/null 2>&1
if [ $? -eq 0 ]; then
echo "$host is reachable"
return 0
else
echo "$host is unreachable"
return 1
fi
}
# Call function
check_host "google.com"
check_host "192.168.1.1"
#!/bin/bash
port_scan() {
local target=$1
local port=$2
timeout 1 bash -c "echo > /dev/tcp/$target/$port" 2>/dev/null
return $?
}
if port_scan "192.168.1.1" 80; then
echo "Port 80 is open"
else
echo "Port 80 is closed"
fi
Arrays store multiple values in a single variable.
#!/bin/bash
# Declare array
targets=("192.168.1.1" "192.168.1.2" "192.168.1.3")
# Access elements
echo "First target: ${targets[0]}"
echo "Second target: ${targets[1]}"
# Array length
echo "Total targets: ${#targets[@]}"
# Iterate over array
for target in "${targets[@]}"; do
echo "Scanning $target"
done
# Add element
targets+=("192.168.1.4")
# All elements
echo "All targets: ${targets[@]}"
#!/bin/bash
# Declare associative array
declare -A ports
ports[ssh]=22
ports[http]=80
ports[https]=443
ports[mysql]=3306
# Access by key
echo "SSH port: ${ports[ssh]}"
# Iterate over keys
for service in "${!ports[@]}"; do
echo "$service runs on port ${ports[$service]}"
done
string="example.com"
echo ${#string} # Output: 11
url="https://example.com/path"
echo ${url:8:11} # Output: example.com (start at position 8, length 11)
path="/var/log/apache2/access.log"
echo ${path/apache2/nginx} # Replace first occurrence
echo ${path//\//-} # Replace all occurrences of / with -
filename="report.txt"
# Remove extension
echo ${filename%.txt} # Output: report
# Extract extension
echo ${filename##*.} # Output: txt
string="Example Domain"
echo ${string,,} # Lowercase: example domain
echo ${string^^} # Uppercase: EXAMPLE DOMAIN
#!/bin/bash
# Read entire file
content=$(cat targets.txt)
# Read line by line
while IFS= read -r line; do
echo "Processing: $line"
done < targets.txt
#!/bin/bash
# Overwrite file
echo "New content" > output.txt
# Append to file
echo "Additional line" >> output.txt
# Write multiple lines
cat > results.txt << EOF
Scan Results
============
Target: example.com
Status: Complete
EOF
#!/bin/bash
if [ -f "targets.txt" ]; then
echo "File exists"
while read -r target; do
echo "Target: $target"
done < targets.txt
else
echo "Error: targets.txt not found"
exit 1
fi
Capture command output and store it in variables.
# Modern syntax (preferred)
result=$(command)
# Old syntax (deprecated)
result=`command`
#!/bin/bash
# Get current date
today=$(date +%Y-%m-%d)
echo "Today: $today"
# Count files
file_count=$(ls -1 | wc -l)
echo "Files: $file_count"
# Store command output
open_ports=$(nmap -p- --open 192.168.1.1 | grep "open")
echo "$open_ports"
# Use in conditionals
if [ $(whoami) = "root" ]; then
echo "Running as root"
else
echo "Not root"
fi
#!/bin/bash
# File: port_scanner.sh
# Description: Simple TCP port scanner using /dev/tcp
if [ $# -ne 3 ]; then
echo "Usage: $0 <target> <start_port> <end_port>"
exit 1
fi
target=$1
start_port=$2
end_port=$3
echo "[*] Scanning $target from port $start_port to $end_port"
echo "[*] Started at $(date)"
echo ""
for ((port=start_port; port<=end_port; port++)); do
timeout 1 bash -c "echo > /dev/tcp/$target/$port" 2>/dev/null
if [ $? -eq 0 ]; then
echo "[+] Port $port is OPEN"
fi
done
echo ""
echo "[*] Scan completed at $(date)"
Usage:
chmod +x port_scanner.sh
./port_scanner.sh 192.168.1.1 1 1000
#!/bin/bash
# File: subdomain_enum.sh
# Description: Enumerate subdomains using wordlist
if [ $# -ne 2 ]; then
echo "Usage: $0 <domain> <wordlist>"
exit 1
fi
domain=$1
wordlist=$2
output="subdomains_${domain}.txt"
if [ ! -f "$wordlist" ]; then
echo "Error: Wordlist $wordlist not found"
exit 1
fi
echo "[*] Enumerating subdomains for $domain"
echo "[*] Using wordlist: $wordlist"
echo ""
> "$output" # Clear output file
while IFS= read -r subdomain; do
full_domain="${subdomain}.${domain}"
# Try DNS resolution
if host "$full_domain" > /dev/null 2>&1; then
echo "[+] Found: $full_domain"
echo "$full_domain" >> "$output"
fi
done < "$wordlist"
echo ""
echo "[*] Results saved to $output"
echo "[*] Total found: $(wc -l < "$output")"
Usage:
./subdomain_enum.sh example.com /usr/share/wordlists/subdomains.txt
#!/bin/bash
# File: ssh_log_parser.sh
# Description: Parse SSH logs for failed login attempts
log_file="/var/log/auth.log"
output="failed_ssh_attempts.txt"
if [ ! -r "$log_file" ]; then
echo "Error: Cannot read $log_file (run as root)"
exit 1
fi
echo "[*] Parsing SSH failed login attempts"
echo ""
# Extract failed password attempts
grep "Failed password" "$log_file" | \
awk '{print $1, $2, $3, $11, $13}' | \
sort | uniq -c | sort -rn > "$output"
echo "[*] Top 10 attacking IPs:"
echo ""
head -10 "$output"
echo ""
echo "[*] Full report saved to $output"
echo "[*] Total unique attempts: $(wc -l < "$output")"
Usage:
sudo ./ssh_log_parser.sh
#!/bin/bash
# File: credential_checker.sh
# Description: Test credentials against SSH service
if [ $# -ne 3 ]; then
echo "Usage: $0 <target> <username_file> <password_file>"
exit 1
fi
target=$1
username_file=$2
password_file=$3
if [ ! -f "$username_file" ] || [ ! -f "$password_file" ]; then
echo "Error: Username or password file not found"
exit 1
fi
echo "[*] Testing credentials against $target"
echo "[*] WARNING: Use only with permission!"
echo ""
while IFS= read -r username; do
while IFS= read -r password; do
echo -n "[*] Testing ${username}:${password}... "
# Use sshpass with timeout
timeout 5 sshpass -p "$password" ssh -o StrictHostKeyChecking=no \
-o ConnectTimeout=3 "${username}@${target}" "exit" 2>/dev/null
if [ $? -eq 0 ]; then
echo "SUCCESS!"
echo "[+] Valid credentials: ${username}:${password}" >> valid_creds.txt
else
echo "Failed"
fi
sleep 1 # Rate limiting
done < "$password_file"
done < "$username_file"
echo ""
echo "[*] Scan complete. Check valid_creds.txt for results."
Note: This is for educational purposes only. Always obtain proper authorization before testing.
#!/bin/bash
# Exit immediately on error
set -e
# Exit on undefined variable
set -u
# Pipe failure detection
set -o pipefail
# Combined
set -euo pipefail
#!/bin/bash
command_with_error() {
nmap -sV 192.168.1.1 -oN scan.txt
if [ $? -ne 0 ]; then
echo "Error: Nmap scan failed"
return 1
fi
return 0
}
if command_with_error; then
echo "Success"
else
echo "Failed"
exit 1
fi
#!/bin/bash
# Cleanup function
cleanup() {
echo "Cleaning up temporary files..."
rm -f /tmp/scan_results_*.txt
echo "Done"
}
# Execute cleanup on exit
trap cleanup EXIT
# Main script
echo "Running scan..."
nmap 192.168.1.1 > /tmp/scan_results_$(date +%s).txt
* * * * * command
│ │ │ │ │
│ │ │ │ └─── Day of week (0-7, Sunday=0 or 7)
│ │ │ └───── Month (1-12)
│ │ └─────── Day of month (1-31)
│ └───────── Hour (0-23)
└─────────── Minute (0-59)
# Edit crontab
crontab -e
# Run script every day at 2 AM
0 2 * * * /root/scripts/daily_scan.sh
# Run every hour
0 * * * * /root/scripts/hourly_check.sh
# Run every 15 minutes
*/15 * * * * /root/scripts/quick_scan.sh
# Run Monday-Friday at 9 AM
0 9 * * 1-5 /root/scripts/weekday_scan.sh
# Run on the 1st of every month
0 0 1 * * /root/scripts/monthly_report.sh
#!/bin/bash
# File: automated_recon.sh
# Description: Daily reconnaissance automation
target="example.com"
date_stamp=$(date +%Y-%m-%d)
output_dir="/root/recon/$target/$date_stamp"
mkdir -p "$output_dir"
echo "[*] Starting automated recon for $target"
# Subdomain enumeration
subfinder -d "$target" -o "$output_dir/subdomains.txt" 2>&1
# Port scanning
nmap -p- --open "$target" -oN "$output_dir/nmap_scan.txt" 2>&1
# Technology detection
whatweb "$target" > "$output_dir/whatweb.txt" 2>&1
echo "[*] Recon complete. Results in $output_dir"
Add to crontab:
0 3 * * * /root/scripts/automated_recon.sh >> /var/log/recon.log 2>&1
| Task | Manual Approach | Scripted Approach | Time Saved |
|---|---|---|---|
| Port Scan 10 hosts | Run nmap 10 times, type each IP | for ip in hosts; do nmap $ip; done | 90% |
| Subdomain Enumeration | Test each subdomain manually | Automated wordlist iteration | 95% |
| Log Analysis | Read logs line by line | Grep + awk parsing with sorting | 98% |
| Credential Testing | Type each user/pass combo | Loop through credential files | 99% |
| Daily Reconnaissance | Login daily and run commands | Cron-scheduled automation | 100% |
| Multi-tool Workflow | Run each tool separately | Chain tools with pipes and substitution | 85% |
| Report Generation | Copy/paste results manually | Automated formatting and file output | 90% |
| Error Handling | Notice and fix errors manually | Automatic error detection and logging | 80% |
Key Insight: Scripting transforms hours of manual work into seconds of automated execution, allowing you to focus on analysis rather than repetitive tasks.
#!/bin/bash and #!/bin/sh?#!/bin/bash — Uses the full-featured Bash shell with arrays, functions, and advanced features#!/bin/sh — Uses the minimal POSIX shell with limited functionality but better portabilityFor penetration testing scripts, use #!/bin/bash for access to all features. If you need maximum portability across different Unix systems, use #!/bin/sh or #!/usr/bin/env bash.
Use these debugging techniques:
# Enable debug mode (print each command)
bash -x script.sh
# Or add to script:
set -x # Enable debugging
set +x # Disable debugging
# Verbose mode
set -v
# Check syntax without running
bash -n script.sh
ShellCheck is an excellent static analysis tool:
sudo apt install shellcheck
shellcheck script.sh
Both have their place:
Use Bash when:
Use Python when:
Most professional pentesters use both: Bash for glue scripts and Python for custom tools.
Optimization techniques:
# Use GNU Parallel for concurrent execution
cat targets.txt | parallel -j 10 nmap -Pn {}
# Minimize subshells (slow)
count=$(cat file.txt | wc -l) # Bad
count=$(wc -l < file.txt) # Good
# Use built-in commands over external
echo ${#string} # Good
echo $string | wc -c # Bad (spawns process)
# Background processes for parallel tasks
for target in $targets; do
scan_target "$target" &
done
wait # Wait for all background jobs
Best practices:
# Never hardcode credentials
# BAD:
password="admin123"
# GOOD: Read from secure file
if [ -f "$HOME/.credentials" ]; then
source "$HOME/.credentials"
chmod 600 "$HOME/.credentials" # Restrict permissions
fi
# Or prompt user
read -sp "Enter password: " password
echo
# Or use environment variables
export API_KEY="your-key"
# Clear sensitive variables after use
unset password
unset API_KEY
Never commit credentials to git repositories!
Bash scripting is an essential skill for every penetration tester and ethical hacker. By mastering variables, conditionals, loops, functions, and file operations, you can automate complex workflows and dramatically increase your efficiency.
This tutorial covered:
✅ Bash script fundamentals (shebang, variables, arguments)
✅ Control structures (if/else, case, loops)
✅ Functions and arrays for organized code
✅ String manipulation and file I/O
✅ Command substitution for integrating tools
✅ Practical pentesting automation scripts
✅ Error handling and scheduling
Next Steps:
Remember: Automation is the key to scaling your penetration testing operations. Start small, build your script library, and continuously refine your workflows.
Happy scripting, and stay ethical!
Tutorial 16 of 105 in the Kali Linux Complete Security Series
Author: Andrax Pentester / Syed Abrar
Last Updated: 2026
Estimated Reading Time: 18 minutes
Related Tutorials:
Follow Andrax Pentester for more cybersecurity tutorials, tools, and ethical hacking guides.
How to write an Immunefi-ready bug report: structure, PoC expectations, severity language — template practice only.
40 min read