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")
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
# 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
# Loop
for i in {1..5}; do
echo "$i"
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!"
}
# 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 test
if [ -f "/path/to/file" ]; then
echo "File exists"
fi
# String test
if [ "$str" = "Hello, World!" ]; then
echo "Strings are equal"
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"
# 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
str="Hello, World!"
echo "${str/Hello/Hi}" # Output: Hi, World!
# Remove a substring
str="Hello, World!"
echo "${str//o/}" # Output: Hell, Wrld!
# 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
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
# 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 attempt to exploit any 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, attempt to exploit any vulnerabilities
if echo "$line" | grep -q 'open'; then
# Suggest using searchsploit or nmap scripts to check for known vulnerabilities
echo "Consider checking for known exploits for port $port using searchsploit or nmap --script=vuln"
# If there's no Metasploit module, try a generic exploit
if [ "$port" -eq 21 ]; then
# FTP exploit
echo "FTP exploit for port $port"
elif [ "$port" -eq 22 ]; then
# SSH exploit
echo "SSH exploit for port $port"
else
# Generic exploit
echo "Generic exploit for port $port"
fi
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.
- 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 using searchsploit or nmap scripts to check for known vulnerabilities.
- If there’s no Metasploit module, the script attempts a generic exploit based on the port number.
- 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 scanning for and exploiting vulnerabilities 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 exploit vulnerabilities.
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 scanning open ports, manipulating files and directories, and educational password testing.
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!