This chapter covers bash scripting for automation, a critical skill for penetration testers as defined in CompTIA PenTest+ PT0-002 objective 5.2. Bash scripting allows testers to automate repetitive tasks, process data, and chain tools together efficiently. Approximately 5-10% of exam questions touch on scripting concepts, including variable usage, loops, conditionals, and common automation patterns. Mastery of bash scripting not only speeds up testing but also demonstrates a deep understanding of Linux environments, which is essential for the exam and real-world engagements.
Jump to a section
Think of bash scripting as an automated task scheduler for a busy office. You have a receptionist (the shell) who follows a set of written instructions (the script) to handle routine tasks. The receptionist can read a list of names (variables), look up phone numbers (commands), and make calls (execute programs). If a call fails, she can try again (loops) or check a different list (conditionals). She can also take notes (output redirection) and file them in specific folders (files). The receptionist doesn't think; she just follows the instructions precisely, which is both a strength and a weakness. If the instructions say to call the same person 100 times, she will do it without question. This is like a bash script: it automates repetitive tasks without deviation. However, if the instructions are wrong—like a typo in a phone number—she will keep dialing the wrong number. Similarly, a bash script will faithfully execute incorrect commands. The key is to write clear, tested instructions that handle errors gracefully, just as you would train a receptionist to handle busy signals or wrong numbers.
What is Bash Scripting and Why It Exists
Bash (Bourne Again SHell) is both a command-line interpreter and a scripting language. A bash script is a plain text file containing a series of commands that the shell executes sequentially. Scripts automate tasks that would otherwise be performed manually, saving time and reducing errors. For penetration testers, scripts are essential for:
Automating reconnaissance (e.g., scanning multiple hosts with Nmap)
Parsing output from tools (e.g., extracting IP addresses from scan results)
Chaining exploits or post-exploitation commands
Creating custom tools for specific testing scenarios
How Bash Scripts Work Internally
When a bash script is executed, the shell reads the file line by line. Each line is parsed into commands, arguments, and operators. The shell then forks a child process to execute each command, unless the command is a built-in (like echo or cd). Redirection operators (>, >>, <, |) modify file descriptors before execution. Environment variables and local variables are expanded using ${} or $. The exit status of each command is stored in $?, which can be checked by conditionals.
Key Components and Defaults
Shebang: The first line of a script is #!/bin/bash, which tells the system which interpreter to use. If missing, the script runs under the current shell, which may cause compatibility issues.
Variables: Declared as VAR=value (no spaces around =). Accessed as $VAR or ${VAR}. By default, variables are global within the script. Local variables can be declared with local inside functions.
Arrays: Indexed arrays use arr=(item1 item2); associative arrays (bash 4+) use declare -A arr. Access elements with ${arr[index]}.
Special Variables:
- $0 – script name
- $1, $2, ... – positional parameters
- $# – number of arguments
- $@ – all arguments as separate words
- $* – all arguments as a single string
- $? – exit status of last command (0 success, non-zero failure)
- $$ – process ID of the current script
Exit Codes: By convention, exit code 0 means success, 1-255 indicates failure. The script can exit with a specific code using exit N.
Conditionals and Loops
if-elif-else:
if [ condition ]; then
commands
elif [ condition ]; then
commands
else
commands
fiConditions use [ ] (test command) or [[ ]] (extended test with regex). Common tests:
- -f file – file exists and is regular
- -d dir – directory exists
- -z string – string is empty
- -n string – string is not empty
- string1 = string2 – string equality
- num1 -eq num2 – numeric equality
for loop:
for var in list; do
commands
doneThe list can be a space-separated list, a range {1..5}, or a command substitution $(command).
while loop:
while [ condition ]; do
commands
doneuntil loop:
until [ condition ]; do
commands
doneFunctions
Functions group commands for reuse:
function_name() {
commands
[return N]
}Arguments are passed as positional parameters inside the function. Use local to avoid variable pollution.
Input and Output Redirection
> – redirect stdout to file (overwrite)
>> – redirect stdout to file (append)
2> – redirect stderr
&> – redirect both stdout and stderr
< – redirect file to stdin
| – pipe stdout of left command to stdin of right command
Command Substitution
Capture output of a command into a variable:
current_date=$(date)
# or backticks: `date`Arithmetic
Integer arithmetic uses $(( expression )):
a=$(( 5 + 3 * 2 ))Error Handling
Use set -e to exit on error, set -u to treat unset variables as errors, set -o pipefail to catch errors in pipes. Trap signals with trap 'commands' SIGNAL.
Common Automation Patterns for Penetration Testing
Mass scanning:
#!/bin/bash
for ip in $(cat targets.txt); do
nmap -sV $ip >> scan_results.txt
doneParsing Nmap output:
#!/bin/bash
# Extract open ports from Nmap grepable output
grep -oP '\d+/open' $1 | cut -d'/' -f1Automated exploitation:
#!/bin/bash
# Run multiple exploits against a target
for exploit in /usr/share/exploitdb/exploits/*.py; do
python3 $exploit -t $1
if [ $? -eq 0 ]; then
echo "Exploit $exploit succeeded"
break
fi
doneData extraction and reporting:
#!/bin/bash
# Extract IP:port from probe results
cat probe.txt | awk '{print $1":"$2}' > live_hosts.txtHow Scripting Interacts with Other Technologies
Bash scripts often call other tools (nmap, curl, netcat, etc.) and parse their output. They can also interact with databases via CLI clients, send emails, and generate reports. On the PT0-002 exam, you may be asked to interpret or complete a script that uses common pentesting tools. Understanding how to chain commands and handle output is crucial.
Best Practices
Always include a shebang.
Use meaningful variable names.
Quote variables to prevent word splitting and globbing: "$var".
Check exit statuses and handle errors.
Use functions for reusable code.
Comment complex logic.
Test scripts in a safe environment before running on targets.
Create the script file
Use a text editor like vim or nano to create a new file, e.g., `myscript.sh`. The `.sh` extension is conventional but not required. The file must be executable: `chmod +x myscript.sh`. The first line should be `#!/bin/bash` to ensure the script runs with bash. Without the shebang, the script will run under the user's current shell, which may differ (e.g., sh, zsh) and cause syntax errors. On many systems, bash is not the default shell, so the shebang is critical for portability.
Define variables and functions
Variables store data like target IPs, usernames, or paths. Example: `target="192.168.1.1"`. Functions encapsulate reusable logic. For instance, a function `scan_host()` could run an Nmap scan and return results. Use `local` inside functions to avoid overwriting global variables. Variables are case-sensitive and should be lowercase by convention to avoid conflict with environment variables (which are uppercase).
Implement logic with conditionals and loops
Conditionals (`if`, `case`) allow branching based on conditions like file existence or command success. Loops (`for`, `while`) iterate over lists or until a condition is met. For example, a `for` loop can iterate over IP addresses from a file: `for ip in $(cat targets.txt); do ... done`. A `while` loop can read a file line by line: `while read line; do ... done < file`. Always quote variables in conditions to handle spaces correctly.
Execute commands and capture output
Use command substitution `$(command)` to capture output into a variable. For example, `ports=$(nmap -p- $target | grep ^[0-9] | cut -d'/' -f1)`. This allows further processing. Redirections (`>`, `>>`, `2>`) can save output to files. Pipes (`|`) chain commands. For error handling, check `$?` after each command. The script can also use `set -e` to abort on any error, but this may be too aggressive for some tasks.
Test and debug the script
Run the script with `bash -x myscript.sh` to enable debug mode, which prints each command before execution. This helps identify syntax errors or logic flaws. Use `echo` statements to print variable values. Check for common mistakes like missing spaces around brackets in `[ ]`, unquoted variables, or incorrect arithmetic syntax. Test with sample data before using on actual targets. Also, ensure the script handles edge cases like empty files or missing arguments gracefully.
In enterprise penetration testing, bash scripting is indispensable for automating reconnaissance and post-exploitation tasks. For example, during a large-scale external assessment, a tester may need to scan hundreds of IP addresses across multiple subnets. Writing a manual Nmap command for each host is impractical. Instead, a bash script can read a list of targets from a file, run Nmap with specific options (e.g., -sV -p1-1000), and output results to individual files or a consolidated report. The script can also parse Nmap's grepable output to extract open ports and services, then feed those into further tools like Nikto or SQLmap. This automation reduces the time from hours to minutes.
Another scenario is during internal network testing where a tester needs to enumerate SMB shares, check for null sessions, and test default credentials. A bash script can loop through a list of IPs, run smbclient -L //ip -N, check the exit code, and log successful enumerations. If a share is accessible, the script can attempt to mount it and download files. This automation ensures consistent testing across many hosts and reduces the chance of missing a critical finding.
A common mistake in production scripts is failing to handle errors. For instance, if a script uses nmap -oG - $target and the target is unreachable, the output may be empty, causing downstream commands to fail. Without error checking, the script may continue with corrupted data. Similarly, not quoting variables can lead to word splitting issues when filenames or IPs contain spaces. Performance considerations include running too many parallel processes (e.g., backgrounding multiple Nmap scans) which can overload the network or the testing machine. Using xargs -P or GNU parallel can control concurrency. Finally, scripts should be idempotent where possible, so re-running them doesn't cause unintended side effects.
For PT0-002 objective 5.2, the exam tests your ability to read, interpret, and write bash scripts for automation. You will not be asked to write a full script from scratch, but you may be given a script snippet and asked to identify its purpose, correct an error, or predict the output. Common exam traps include:
**Confusing $@ and $*:** Candidates often think they are identical. $@ preserves word boundaries (e.g., "$@" expands each argument as a separate quoted string), while $* combines all arguments into a single string. The exam may ask which one correctly handles arguments with spaces.
Forgetting to quote variables: In a script like for file in $(ls *.txt), if a filename contains a space, the loop will split it. The correct approach is for file in *.txt (no command substitution) or using find -print0 with xargs -0.
Misunderstanding exit codes: The exam may show a script that uses if [ $? -eq 0 ] after a command, but candidates forget that $? is overwritten by the test command itself. The correct pattern is to store $? in a variable immediately after the command.
Ignoring the shebang: A script without #!/bin/bash may run under a different shell (e.g., sh) which lacks features like [[ ]] or arrays. The exam may test whether a script will work as expected on a system where bash is not the default.
Arithmetic vs. string comparison: Using -eq for strings or = for numbers will cause errors. The exam expects you to know the correct operators.
Specific values to memorize: $? holds exit code, $0 is script name, $# is argument count. The test command ([ ]) requires spaces around brackets. The -z test checks for empty string. set -e causes exit on error. $(()) is for arithmetic. $() is preferred over backticks for command substitution.
To eliminate wrong answers, trace the script step by step, keeping track of variable values and exit codes. Look for missing spaces, incorrect operators, or unquoted variables. Remember that bash is whitespace-sensitive in many contexts.
Always include `#!/bin/bash` as the first line to ensure the script runs with bash.
Quote variables (`"$var"`) to prevent word splitting and globbing.
Use `$()` for command substitution; backticks are deprecated.
Check exit codes with `$?` immediately after the command, before it gets overwritten.
Use `set -e` to exit on error, but be aware it can cause unexpected exits.
For loops over files, use `for file in *.txt` instead of `for file in $(ls *.txt)`.
Use `[[ ]]` for extended test conditions with regex support.
Functions should use `local` variables to avoid side effects.
Debug scripts with `bash -x script.sh`.
Make scripts executable with `chmod +x script.sh`.
These come up on the exam all the time. Here's how to tell them apart.
Bash Scripting
Native to Unix/Linux, no additional runtime required
Best for simple automation and command chaining
String manipulation can be cumbersome
Less verbose, but syntax can be cryptic
Limited data structures (associative arrays only in bash 4+)
Python Scripting
Cross-platform, requires Python interpreter
Better for complex logic, data parsing, and networking
Rich string and list manipulation methods
More readable and maintainable for large scripts
Full object-oriented programming support
Mistake
Bash scripts must have a .sh extension to run.
Correct
The .sh extension is a convention, not a requirement. The script can be named anything, but it must be executable (`chmod +x`) and have a correct shebang. The system uses the shebang to determine the interpreter, not the file extension.
Mistake
Using `$*` and `$@` is the same.
Correct
They differ when quoted. `"$*"` expands to a single string (e.g., `"arg1 arg2 arg3"`), while `"$@"` expands to separate quoted strings (e.g., `"arg1" "arg2" "arg3"`). This affects how arguments with spaces are handled.
Mistake
The `if` statement must use `[ ]` for conditions.
Correct
`[ ]` is the test command. You can also use `[[ ]]` (extended test) which supports regex and pattern matching, and is safer with empty variables. Additionally, you can use `(( ))` for arithmetic conditions without needing `$` on variables.
Mistake
A script will always use bash as the interpreter.
Correct
If the shebang is missing or incorrect, the script runs under the user's current shell, which could be sh, dash, or zsh. These shells may not support bash-specific features like arrays or `[[ ]]`. Always include `#!/bin/bash` for portability.
Mistake
Exit code 0 always means success.
Correct
By convention, 0 means success, but a command or script can return any exit code. Some tools use non-zero codes for specific types of failures (e.g., 1 for general error, 2 for misuse). Always check the documentation.
Reveal each answer, then mark whether you got it right. Score 60%+ to unlock the next chapter.
Arguments are accessed via positional parameters: `$1`, `$2`, etc. `$0` is the script name, `$#` is the count of arguments, and `$@` or `$*` represent all arguments. For example, if you run `./script.sh arg1 arg2`, then `$1` is 'arg1', `$2` is 'arg2'. Use `"$@"` to pass all arguments to another command while preserving spaces.
`[ ]` is the standard `test` command; it requires spaces around brackets and does not support regex. `[[ ]]` is a bash keyword that supports pattern matching, regex (`=~`), and logical operators (`&&`, `||`). It also handles empty variables safely. For example, `[[ "$var" =~ ^[0-9]+$ ]]` checks if var is numeric. Use `[[ ]]` in bash scripts for more robust conditionals.
Use a while loop with the `read` command: `while IFS= read -r line; do echo "$line"; done < file.txt`. The `-r` prevents backslash interpretation, and `IFS=` preserves leading/trailing whitespace. This is safer than `for line in $(cat file.txt)` because it handles spaces and special characters correctly.
Use `&` to background a command: `command1 & command2 & wait`. The `wait` command waits for all background jobs to finish. For more control, use `xargs -P` or GNU `parallel`. Example: `cat targets.txt | xargs -P 5 -I {} nmap {} -oN {}.nmap` runs up to 5 Nmap scans in parallel.
`set -e` causes the script to exit immediately if any command returns a non-zero exit code (i.e., fails). This is useful for catching errors early, but it can be too aggressive if a command is expected to fail (e.g., `grep` may return 1 if no match). Use `set +e` to disable it temporarily, or use `|| true` to allow failure.
Declare an indexed array: `arr=('item1' 'item2' 'item3')`. Iterate: `for item in "${arr[@]}"; do echo "$item"; done`. For associative arrays (bash 4+): `declare -A assoc; assoc[key1]='value1';` iterate keys with `for key in "${!assoc[@]}"; do echo "${assoc[$key]}"; done`.
Use the `-f` test: `if [ -f "$file" ]; then echo "File exists"; fi`. For directory, use `-d`. You can also check for non-existence with `! -f`. The `-e` test checks for any file type (including directories, symlinks). Always quote the variable to handle spaces.
You've just covered Bash Scripting for Automation — now see how well it sticks with free PT0-002 practice questions. Full explanations included, no account needed.
Done with this chapter?