Flow control

Typically, a payload will start at the first command, and continue executing until the last command.

There are, of course, ways to change this. Generally referred to as "flow control", these options change the course of the payload being run.

If

The simplest flow control is the if statement. Like a word problem in math, an if says simply:

"If this condition is true, perform this action".

What constitutes "true"-ness? As in the majority of programming languages, a true statement is one where the result is not zero.

if [ 1 -eq 1 ]; then
    echo "True!"
fi

This will, of course, print "True!" when run, because 1 is equal to 1.

if [ "true" = "not true" ]; then
    echo "True!"
fi

Similarly, this will print nothing, as the string true is not equal to the string not true.

Writing if statements

The basic syntax of an if statement is:

if [ condition ]
then
    commands
fi

This is usually shortened (as we did above) to simply:

if [ condition ]; then
    commands
fi

You'll notice the if block ends with fi (if backwards). Bash uses this convention in several places to indicate the end of a special block.

Conditions

In the if statement, [ condition ] is a test that evaluates to true or false. You can test conditions based on file attributes, strings, numbers, or expressions using various comparison operators such as -eq (equal to), -ne (not equal to), -lt (less than), -gt (greater than), -le (less than or equal to), -ge (greater than or equal to), and -z (tests if a string is empty).

Integer comparisons

TestReturns true when...

a -eq b

a is equal to b

a -ne b

a is not equal to b

a -gt b

a is greater than b

a -ge b

a is greater than or equal to b

a -lt b

a is less than b

a -le b

a is less than or equal to b

Notice that we use -eq instead of =, -gt instead of >=, and so on? This is because bash treats strings and numbers differently!

When doing numerical comparisons, be sure to use the right operators!

X=10
Y=20

if [ "$X" -lt "$Y" ]; then
    echo "$X is less than $Y"
fi

String comparisons

TestReturns true when...

"$a" = "$b"

a is equal to b

"$a" == "$b"

a is equal to b

"$a" != "$b"

a is not equal to b

-z "$a"

a is empty

For example,

OPTION="green"

if [ "$OPTION" == "green" ]; then
    echo "Green"
fi

Notice the spaces: You must use spaces around the = or == operators!

Remember to use quotes!

Whenever you're comparing strings, be sure to enclose the value in quotes. This prevents an empty string from causing a syntax error!

File conditions

There are many tests we can perform on files:

TestReturns true when...

-e

File exists

-f

File exists and is a regular file (not a directory or special file type)

-s

File exists and has data (has a size greater than zero)

-d

File is a directory

-b

File is a block device (a special file in Linux that represents hardware like disks and other storage systems)

-c

File is a character device (a special file in Linux that represents hardware like serial ports)

-p

File is a pipe (a special file in Linux that represents a two-way communication system between processes)

-h

File is a symbolic link (essentially a shortcut or pointer to another file)

-r

File has read permission for the user running the test

-w

File has write permission for the user running the test

-x

File has execute permission for the user running the test

f1 -nt f2

File f1 is newer than file f2

f1 -ot f2

File f1 is older than file f2

For example, to determine if a file exists:

#!/bin/bash

if [ -e /tmp/walnuts.txt ]; then
   echo "The file exists."
else
   echo "The file does not exist."
fi

If-Else

We already touched on the else structure in the example above: Else handles taking actions if a statement is not true.

if [ some-test ]; then
    action-when-test-is-true
else
    action-when-test-is-false
fi

To handle multiple tests, we use the elif option ("else if"):

if [ some-test ]; then
    action-when-test-is-true
elif [ some-other-test ]; then
    action-when-some-other-test-is-true
elif [ some-third-test ]; then
    action-when-some-third-test-is-true
else
    action-when-no-tests-are-true
fi

Notice how we can combine elif with else ; when no tests are true, the else block is executed.

For a more practical example:

if [ "$OPT" = "option1" ]; then
    echo "option 1 selected"
elif [ "$OPT" = "option2" ]; then
    echo "option 2 selected"
elif [ "$OPT" = "option3" ]; then
    echo "option 3 selected"
else
    echo "unknown option"
fi

Condensed return codes

A variant of an if statement can be used when handling the return codes from a process.

Every process has a return code; these are numeric codes that indicate if the process succeeded or failed. A return code of 0 is considered a success, while any other number is considered an error.

Return codes are stored in the internal variable $?. For example:

root@squirrel:~# ls /i-dont-exist
root@squirrel:~# echo $?
1
root@squirrel:~# ls /tmp
root@squirrel:~# echo $?
0

The ls program will set an error code if the file does not exist.

We could check for this using a traditional if statement:

ls /i-dont-exist >/dev/null
if [ "$?" -ne 0 ]; then
    echo "I don't exist"
else
    echo "I exist"
fi

What's the >/dev/null in the above example? Check out the section about redirecting output!

Often, however, it is much quicker to use the bash shorthand of && and ||. This will execute the next statement only if the previous one succeeded. To implement the same using these shorthand options:

ls /i-dont-exist >/dev/null && echo "I dont't exist"; || echo "I exist"

The short methods are not always clearer, but are often very useful for short tests of command success. They're covered more in the Return codes & success section!

Case

To handle multiple options in a more elegant fashion than constant if-else blocks, the case statement is a conditional statement that is used to test a variable or an expression against a series of patterns or values. It is similar to the switch statement in other programming languages.

The basic syntax of the case statement is as follows:

case expression in
  pattern1)
    do-something-for-pattern-1
    ;;
  pattern2)
    do-something-for-pattern-2
    ;;
  pattern3|pattern4|pattern5)
    do-something-for-pattern3-pattern4-or-pattern5
    ;;
  *)
    do-something-if-nothing-else-matches
    ;;
esac

Here, expression is the variable or expression to be tested, and pattern1, pattern2, pattern3, etc., are the patterns or values against which the expression is to be tested.

Each pattern is followed by a list of commands to execute if the expression matches that pattern. The ;; symbol is used to indicate the end of each pattern. In other languages, the ;; is equivalent to a break statement in a switch block.

The | symbol is used to separate multiple patterns that should be treated as equivalent.

The * pattern matches any value, and is used as a default case if none of the other patterns match.

For example, the following script demonstrates the use of the case statement to test the value of a variable called day:

# Day is the day of the week, 1-7
day="1"

case $day in
  1)
    echo "Monday"
    ;;
  2)
    echo "Tuesday"
    ;;
  3)
    echo "Wednesday"
    ;;
  4)
    echo "Thursday"
    ;;
  5)
    echo "Friday"
    ;;
  6)
    echo "Saturday"
    ;;
  7)
    echo "Sunday"
    ;;
  *)
    echo "Invalid day"
    ;;
esac

You can see this in action in the NETMODE DuckyScript command:

root@squirrel:~# cat /usr/bin/NETMODE
#!/bin/bash

function show_usage() {
        echo "Usage: $0 [NAT|BRIDGE|JAIL|TRANSPARENT|ISOLATE]"
        echo ""
}

case $1 in
        "NAT")
                cat /usr/lib/network_config/nat /tmp/wireguard.conf 2>/dev/null > /etc/config/network
                ;;
        "BRIDGE")
                cat /usr/lib/network_config/bridge /tmp/wireguard.conf 2>/dev/null > /etc/config/network
                ;;
        "TRANSPARENT")
                cp /usr/lib/network_config/transparent /etc/config/network
                ;;
        "ISOLATE")
                cp /usr/lib/network_config/isolate /etc/config/network
                ;;
        "JAIL")
                cat /usr/lib/network_config/jail /tmp/wireguard.conf 2>/dev/null > /etc/config/network
                ;;
        "VPN")
                # VPN is now NAT + vpn configs
                cat /usr/lib/network_config/nat /tmp/wireguard.conf 2>/dev/null > /etc/config/network
                ;;
        *)
                show_usage
                exit 0
                ;;
esac

For

A for loop is used to iterate over a set of values, such as a list of files, or a range of numbers. The general syntax for a for loop is as follows:

for var in list
do
   some-stuff-per-item
done

Or more condensed,

for var in list; do
   some-stuff-per-item
done

Here, var is a variable that is used to hold each value from the list as the loop iterates. list is a sequence of items, separated by spaces, that the loop will iterate over.

The commands block contains the instructions to be executed for each iteration of the loop. This block can contain any valid commands, including DuckyScript commands, other loops, conditionals, and function calls.

Here's an example of a simple for loop that iterates over a list of values:

for tool in squirrel pineapple ducky turtle bunny; do
   echo "Doing a thing with $tool"
done

This will iterate over the list of tools ("squirrel", "pineapple", "ducky", "turtle", "bunny"), and for each iteration print the line "Doing a thing with [tool]". This would output:

Doing a thing with squirrel
Doing a thing with pineapple
Doing a thing with ducky
Doing a thing with turtle
Doing a thing with bunny

You can also use a for loop to iterate over a range of values, using the seq command. Here's an example:

for i in $(seq 1 5); do
   echo "Counting: $i"
done

This loop will iterate over the numbers 1 to 5, and for each iteration, it will print the message "Counting: [number]". The output of this script will be:

Counting: 1
Counting: 2
Counting: 3
Counting: 4
Counting: 5

The list of items can be taken from a shell expansion, for instance to iterate over all files in a directory:

for f in /usb/captures/*; do
    C2EXFIL "$f"
done

Notice we place quotes around the variable $f: This protects against a file with a space (or other special characters) in the name. Without the quotes, it would appear as two arguments to the C2EXFIL command.

While

A while loop is used to execute a block of commands repeatedly as long as a certain condition is true. The general syntax for a while loop is as follows:

while condition
do
   commands
done

or

while condition; do
   commands
done

Here, condition is a test that is evaluated at the beginning of each iteration of the loop. If the condition is true, the commands block will be executed; if it is false, the loop will terminate and execution will continue with the command after the done keyword.

Conditions are evaluated the same as if statement conditions, allowing for quite complex controls.

The commands block contains the instructions to be executed for each iteration of the loop. This block can contain any valid commands, including DuckyScript commands, other loops, conditionals, and function calls.

Here's an example of a simple while loop that iterates as long as a certain condition is true:

count=0
while [ $count -lt 5 ]; do
   echo "Counting: $count"
   count=$((count+1))
done

This loop will iterate as long as the value of count is less than 5. For each iteration, it will print the message "Counting: [count]", and then increment the value of count by 1. The output of this script will be:

Counting: 0
Counting: 1
Counting: 2
Counting: 3
Counting: 4

Functions

A function is a block of code that can be called by name from within a script. Functions allow you to modularize your code and reuse it in different parts of your script, making it easier to write and maintain complex scripts.

The general syntax for defining a function is as follows:

function_name () {
   commands
}

Here, function_name is the name you choose for your function, and commands is the block of code that will be executed when the function is called.

You can call a function by simply typing its name, followed by any arguments you want to pass to it (if any):

function_name argument1 argument2 ...

This is a simple function which adds two numbers:

add_numbers () {
   sum=$(( $1 + $2 ))
   echo $sum
}

The function add_numbers takes two arguments, num1 and num2, adds them together, and then prints the result to the console. You would call this function using:

add_numbers 5 10

This will output 15 to the console, which is the sum of the two arguments.

You can also use functions to encapsulate complex logic, or to perform tasks that need to be repeated multiple times in your payload. For example, you could write a function to check if a file exists:

file_exists () {
   if [ -e "$1" ]; then
      echo "File exists"
   else
      echo "File does not exist"
   fi
}

This function takes one argument, which is the name of the file you want to check. It then uses the -e operator to test if the file exists, and prints a message indicating whether the file exists or not.

To call this function from within your payload, you could use:

file_exists /path/to/file.txt

This will output either "File exists" or "File does not exist".

Last updated