Welcome to Programming Thursdays, where we dive deep into the world of coding and explore new tools and techniques for the discerning pen tester and red teamer. Today, we’ll examine Bash Scripting Language, one of the most powerful and versatile languages in the hacker’s toolkit.
As a pen tester or red teamer, you’re no doubt familiar with the concept of automation. Writing scripts to automate everyday tasks and exploit vulnerabilities is essential to your success, and that’s where Bash comes in. Whether you’re looking to brute force a password, execute a series of commands, or manipulate files and directories, Bash is the perfect language for the job.
Security and Safety Considerations
Note: Before proceeding with any scripting examples in this article, consider these security practices:
Before Running Any Script
- Always get written authorization before testing on systems you don’t own
- Test in isolated environments first (VMs, containers, or dedicated lab systems)
- Use shellcheck to validate your scripts for security issues
- Follow the principle of least privilege - never run scripts with unnecessary permissions
- Understand the legal and ethical implications of your actions
Essential Safety Settings
Every professional Bash script should start with these safety settings:
#!/bin/bash
# Essential safety settings - ALWAYS include these
set -euo pipefail
# -e: Exit immediately if a command exits with non-zero status
# -u: Treat unset variables as error
# -o pipefail: Return exit status of last command in pipe that failed
Basic Syntax
Before we get started, let’s cover some basic concepts and syntax in Bash. If you’re already familiar with programming languages, feel free to skip ahead, but if you’re new to the game, read on.
Variables and Data Types
Like most programming languages, Bash uses variables to store data. Variables can hold any data type, including strings, integers, and arrays. To declare a variable, assign a value to it:
#!/bin/bash
set -euo pipefail
# Declare a string variable
str="Hello, World!"
# Declare an integer variable
num=42
# Declare an array
arr=("apple" "banana" "cherry")
# Access array elements
echo "${arr[0]}" # Output: apple
echo "${arr[@]}" # Output: apple banana cherry
Operators
Bash supports various operators, including arithmetic, comparison, and logical operators. Here are a few examples:
#!/bin/bash
set -euo pipefail
# Arithmetic operators
a=10
b=5
echo $((a + b)) # Output: 15
echo $((a - b)) # Output: 5
echo $((a * b)) # Output: 50
echo $((a / b)) # Output: 2
echo $((a % b)) # Output: 0
# Comparison operators
a=10
b=5
if [ "$a" -gt "$b" ]; then
echo "a is greater than b"
fi
# String comparison
str1="hello"
str2="world"
if [[ "$str1" < "$str2" ]]; then
echo "str1 comes before str2 alphabetically"
fi
# Logical operators
a=10
b=5
if [ "$a" -gt "$b" ] && [ "$a" -lt 20 ]; then
echo "a is between 5 and 20"
fi
Control Structures
Control structures allow you to execute code conditionally or repeatedly. Bash supports if/else statements, loops, and case statements. Here are a few examples:
#!/bin/bash
set -euo pipefail
# If/else statement
a=10
b=5
if [ "$a" -gt "$b" ]; then
echo "a is greater than b"
else
echo "b is greater than a"
fi
# For loop with range
for i in {1..5}; do
echo "$i"
done
# While loop
counter=0
while [ "$counter" -lt 5 ]; do
echo "Counter: $counter"
((counter++))
done
# Case statement
a="apple"
case "$a" in
"apple") echo "It's an apple";;
"banana") echo "It's a banana";;
"cherry") echo "It's a cherry";;
esac
Functions
Functions allow you to encapsulate code and reuse it throughout your script. To define a function, use the following syntax:
#!/bin/bash
set -euo pipefail
# Define a function
function greet {
local name="$1" # Use local variables for function parameters
echo "Hello, $name!"
}
# Alternative function syntax
greet_alt() {
local name="$1"
echo "Hello, $name!"
}
# Call the function
greet "World"
Advanced Elements
In this section, we’ll cover more advanced examples of Bash Scripting Language, including conditional tests, taking arguments for the script, default values, Heredoc, boolean logic, redirection, reading input, special variables, and variable substitutions.
Conditional Tests
Bash supports a wide range of conditional tests, including file tests, string tests, and numeric tests. Here are a few examples:
#!/bin/bash
set -euo pipefail
# File tests
if [ -f "/path/to/file" ]; then
echo "File exists and is regular file"
fi
if [ -d "/path/to/directory" ]; then
echo "Directory exists"
fi
if [ -r "/path/to/file" ]; then
echo "File is readable"
fi
# String tests
if [ "$str" = "Hello, World!" ]; then
echo "Strings are equal"
fi
if [ -n "$str" ]; then
echo "String is not empty"
fi
if [ -z "$empty_var" ]; then
echo "Variable is empty or unset"
fi
# Numeric test
if [ "$num" -gt 10 ]; then
echo "Number is greater than 10"
fi
Taking Arguments for the Script
Bash scripts can take arguments from the command line, allowing you to customize their behavior for different use cases. Here’s an example:
#!/bin/bash
set -euo pipefail
# Validate required arguments
if [ $# -lt 2 ]; then
echo "Usage: $0 <arg1> <arg2>" >&2
exit 1
fi
# Take two arguments from the command line
echo "First argument: $1"
echo "Second argument: $2"
You could then run this script with two arguments:
./script.sh arg1 arg2
Default Values
You can also set default values for arguments in case they’re not provided. Here’s an example:
#!/bin/bash
set -euo pipefail
# Set default values for the first two arguments
arg1="${1:-default1}"
arg2="${2:-default2}"
echo "First argument: $arg1"
echo "Second argument: $arg2"
You could then run this script with only one argument:
./script.sh arg1
Heredoc
Heredoc allows you to define a text block as a variable, which can be helpful for creating templates or multi-line strings. Here’s an example:
#!/bin/bash
set -euo pipefail
# Define a Heredoc block as a variable
myvar=$(cat << 'EOF'
This is a multi-line string.
It can contain any characters, including "quotes" and 'apostrophes'.
EOF
)
# Print the variable
echo "$myvar"
Boolean Logic
Bash supports boolean logic by using the “&&” and “||” operators. Here are a few examples:
#!/bin/bash
set -euo pipefail
# Boolean AND
if [ "$a" -gt 10 ] && [ "$a" -lt 20 ]; then
echo "a is between 10 and 20"
fi
# Boolean OR
if [ "$a" -lt 5 ] || [ "$a" -gt 20 ]; then
echo "a is less than 5 or greater than 20"
fi
Redirection
Bash supports redirection of input and output, allowing you to read input from files and redirect output to files. Here are a few examples:
#!/bin/bash
set -euo pipefail
# Read input from a file
while IFS= read -r line; do
echo "Line: $line"
done < "input.txt"
# Read with line numbers
while IFS= read -r line; do
((line_num++))
echo "Line $line_num: $line"
done < "input.txt"
# Redirect output to a file
echo "Output" > "output.txt"
Reading Input
Bash supports reading input from the user, which can be helpful for interactive scripts. Here’s an example:
#!/bin/bash
set -euo pipefail
# Ask the user for input
read -p "Enter your name: " name
# Validate input
if [[ -z "$name" ]]; then
echo "Name cannot be empty" >&2
exit 1
fi
# Print the input
echo "Hello, $name!"
Special Variables
Bash includes several special variables that provide information about the environment, such as the current working directory and the number of arguments passed to the script. Here are a few examples:
#!/bin/bash
set -euo pipefail
# Current working directory
echo "Current directory: $PWD"
# Number of arguments
echo "Number of arguments: $#"
# Script name
echo "Script name: $0"
# Last argument
echo "Last argument: ${!#}"
Variable Substitutions
Bash supports variable substitutions, which allow you to manipulate variables in various ways. Here are a few examples:
#!/bin/bash
set -euo pipefail
# Replace a substring (first occurrence)
str="Hello, World!"
echo "${str/Hello/Hi}" # Output: Hi, World!
# Replace all occurrences of a substring
str="Hello, World!"
echo "${str//o/}" # Output: Hell, Wrld!
# Extract substring
str="Hello, World!"
echo "${str:0:5}" # Output: Hello
echo "${str:7:5}" # Output: World
# Replace with default value if variable is empty
echo "${var:-default}"
# Replace with default value if variable is unset
echo "${var:=default}"
# Replace with default value if variable is empty or unset
echo "${var:-default}"
# Return error if variable is empty or unset
echo "${var:?error}"
Modern Bash Best Practices
Shellcheck Integration
Shellcheck is an essential tool for validating Bash scripts and catching security issues:
# Install shellcheck
sudo apt-get install shellcheck
# Check your scripts
shellcheck myscript.sh
# Fix common issues automatically
shellcheck --format=diff myscript.sh | patch -p1
Proper Quoting
Always quote your variables to prevent word splitting and globbing issues:
#!/bin/bash
set -euo pipefail
# BAD - Word splitting issues with spaces in filenames
for file in $(ls *.txt); do
echo "$file"
done
# GOOD - Proper quoting with find
while IFS= read -r -d '' file; do
echo "$file"
done < <(find . -name "*.txt" -print0)
Input Validation
Always validate and sanitize inputs:
#!/bin/bash
set -euo pipefail
# Validate inputs
validate_input() {
local input="$1"
if [[ ! "$input" =~ ^[a-zA-Z0-9_-]+$ ]]; then
echo "Invalid input detected" >&2
exit 1
fi
}
# Sanitize file paths
sanitize_path() {
local path="$1"
# Remove dangerous characters
path="${path//..\/}" # Remove parent directory traversal
echo "$path"
}
Advanced Error Handling
Using Traps for Cleanup
#!/bin/bash
set -euo pipefail
# Cleanup function
cleanup() {
echo "Cleaning up temporary files..."
rm -f /tmp/script_temp_*
}
# Set trap to run cleanup on exit
trap cleanup EXIT INT TERM
# Your script logic here
main() {
temp_file=$(mktemp /tmp/script_temp_XXXXXX)
echo "Working with $temp_file"
# Script continues...
}
main "$@"
Debug Mode
#!/bin/bash
set -euo pipefail
# Enable debug mode with -x flag
[[ "${DEBUG:-}" == "1" ]] && set -x
debug_log() {
[[ "${DEBUG:-}" == "1" ]] && echo "DEBUG: $*" >&2
}
debug_log "Starting script with args: $*"
Using Bash for Pen Testing and Red Teaming
Now that we’ve covered the basics, let’s examine how Bash can be used for pen testing and red teaming in an ethical and professional manner.
Ethical Pen Testing with Bash
Note: All examples in this section assume you have proper authorization for testing.
Authorization Checks
#!/bin/bash
# Professional penetration testing script template
set -euo pipefail
# Color codes for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# Check for authorization file
check_authorization() {
local auth_file="./AUTHORIZATION.txt"
if [[ ! -f "$auth_file" ]]; then
echo -e "${RED}Authorization file missing${NC}" >&2
echo "Create AUTHORIZATION.txt with client approval before proceeding" >&2
exit 1
fi
echo -e "${GREEN}Authorization verified${NC}"
}
# Network discovery with rate limiting
safe_network_scan() {
local target="$1"
local delay="${2:-1}" # Default 1 second delay
echo -e "${YELLOW}Scanning $target with $delay second delay${NC}"
# Rate-limited port scan
for port in 22 80 443 3389; do
if timeout 3 bash -c "echo >/dev/tcp/$target/$port" 2>/dev/null; then
echo -e "${GREEN}Port $port is open${NC}"
fi
sleep "$delay" # Respectful scanning
done
}
# Usage
check_authorization
safe_network_scan "192.168.1.1" 2
Safe File Operations
One of the most common use cases for Bash is automating tasks. For example, you might write a script to scan a network for open ports or search for specific files. Here’s an example script that uses nmap to scan for open ports on a given IP address:
#!/bin/bash
set -euo pipefail
# Validate target input
target="$1"
if [[ ! "$target" =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then
echo "Invalid IP address format" >&2
exit 1
fi
# Additional validation for valid IP ranges
IFS='.' read -r -a octets <<< "$target"
for octet in "${octets[@]}"; do
if ((octet < 0 || octet > 255)); then
echo "Invalid IP address: octet $octet out of range" >&2
exit 1
fi
done
# Scan for open ports on the target IP address
nmap "$target"
You could then run this script with the target IP address as a parameter:
./scan.sh 192.168.0.1
Another common use case for Bash is manipulating files and directories. For example, you might write a script to search for a specific file on a target system. Here’s an example script that searches for a file called “passwords.txt” on a target system:
#!/bin/bash
set -euo pipefail
# Search for a file called "passwords.txt"
find / -name "passwords.txt" 2>/dev/null
You could then run this script on the target system to search for the file:
./search.sh
Password Testing Example
Here’s an example that demonstrates password validation concepts:
#!/bin/bash
# Password testing example - ensure you have authorization
set -euo pipefail
# Simple authorization check
read -p "Are you authorized to test on this system? (yes/no): " auth
if [[ "$auth" != "yes" ]]; then
echo "Authorization required. Exiting."
exit 1
fi
# Demonstrate password validation
validate_password() {
local password="$1"
echo "Testing password: $password"
# In a real scenario, you would attempt sudo here
}
echo "Password Testing Example"
# Read in a list of passwords from a file
if [[ -f "passwords.txt" ]]; then
while IFS= read -r password; do
validate_password "$password"
done < "passwords.txt"
else
echo "No password file found."
fi
Putting it together
Let’s say you’ve been tasked with creating a script that automates scanning a target network for open ports and then attempting to exploit any vulnerabilities found on those ports. Here’s an example script that uses many of the Bash features we’ve covered:
#!/bin/bash
set -euo pipefail
# Take the target IP address as an argument
target="$1"
# Validate target input
if [[ ! "$target" =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then
echo "Invalid IP address format" >&2
exit 1
fi
# Scan for open ports using nmap and save the output to a file
nmap -oN scan.txt "$target"
# Loop through the open ports and check for potential vulnerabilities
while IFS= read -r line; do
# Extract the port number from the nmap output
port=$(echo "$line" | cut -d '/' -f 1)
# If the port is open, suggest vulnerability assessment
if echo "$line" | grep -q 'open'; then
echo "Port $port is open - consider vulnerability assessment:"
echo " - searchsploit -p $port"
echo " - nmap --script=vuln -p $port $target"
# Suggest service-specific checks
case "$port" in
21) echo " - Check for anonymous FTP access" ;;
22) echo " - Check for weak SSH configurations" ;;
80|443) echo " - Check for web application vulnerabilities" ;;
3389) echo " - Check for RDP security settings" ;;
*) echo " - Research common vulnerabilities for port $port" ;;
esac
fi
done < <(grep -E '^[0-9]+/' scan.txt)
# Clean up the scan output file
rm scan.txt
Let’s break down what’s happening in this script:
- The script takes the target IP address as an argument.
- The script validates the input to ensure it’s a proper IP address format and valid octet ranges.
- The script uses nmap to scan the target for open ports and saves the output to a file.
- The script loops through the open ports found by nmap.
- The script extracts the port number from the nmap output for each open port.
- If the port is open, the script suggests appropriate vulnerability assessment tools and techniques.
- The script provides service-specific recommendations based on common port numbers.
- The script cleans up the scan output file.
As you can see, this script uses several Bash Scripting Language features, including taking arguments for the script, file input/output, conditional tests, loop structures, variable substitutions, and executing commands using command substitution. Combining these features allows us to create a powerful and flexible script that automates network reconnaissance and vulnerability assessment on a target network.
Performance Considerations
Efficient Loops
#!/bin/bash
set -euo pipefail
# BAD - Command substitution in loop
for file in $(find . -name "*.txt"); do
echo "$file"
done
# GOOD - Process substitution
while IFS= read -r file; do
echo "$file"
done < <(find . -name "*.txt")
Avoiding Subshells
#!/bin/bash
set -euo pipefail
# BAD - Creates subshell
result=$(some_command)
# GOOD - Use process substitution when possible
while IFS= read -r line; do
process_line "$line"
done < <(some_command)
Conclusion
Bash Scripting is a powerful and versatile language that every pen tester and red teamer should have in their arsenal. With its support for variables, operators, control structures, and functions, Bash makes it easy to automate tasks, manipulate data, and perform security assessments.
This article covered the basics of Bash Scripting, including variables and data types, operators, control structures, and functions. We also looked at how Bash can be used for pen testing and red teaming, with specific code examples for everyday tasks like network reconnaissance, file manipulation, and security assessment techniques.
Remember: Use tools like shellcheck to validate your scripts and ensure they follow modern Bash conventions.
Whether you’re a seasoned hacker or just starting out, mastering Bash scripting is essential for success in red teaming and pen testing—so get scripting!