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
.
This will, of course, print "True!" when run, because 1 is equal to 1.
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:
This is usually shortened (as we did above) to simply:
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
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!
String comparisons
"$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,
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:
-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:
If-Else
We already touched on the else
structure in the example above: Else handles taking actions if a statement is not true.
To handle multiple tests, we use the elif
option ("else if"):
Notice how we can combine elif
with else
; when no tests are true, the else
block is executed.
For a more practical example:
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:
The ls
program will set an error code if the file does not exist.
We could check for this using a traditional if
statement:
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:
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:
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
:
You can see this in action in the NETMODE
DuckyScript command:
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:
Or more condensed,
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:
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:
You can also use a for
loop to iterate over a range of values, using the seq
command. Here's an example:
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:
The list of items can be taken from a shell expansion, for instance to iterate over all files in a directory:
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:
or
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:
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:
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:
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):
This is a simple function which adds two numbers:
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:
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:
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:
This will output either "File exists
" or "File does not exist
".
Last updated