Blog Projects, Tips, Tricks, and How-Tos

Writing Better Shell Scripts – Part 3

Quick Start

This post doesn’t really lend itself to being a quick read, but you can have a look at the How-To section of this post and skip the rest if you’re in a hurry. I would highly recommend reading everything though, since there’s a lot of information that may serve you well in the future. There is also Video attached to this post that may be a good quick reference for you. Don’t forget that the man and info pages of your Linux/Unix installation can be an invaluable resource as well when you’re trying to learn new concepts and solve problems.

Video

Audio

Download

Preface

To make things easier on you, all of the black command line and script areas are set up so that you can copy the text from them. This does make using the commands and scripts easier, but if you’re not already familiar with the concepts presented here, typing the commands/code yourself and working through why you’re typing them will help you learn more. If you hit problems along the way, take a look at the Troubleshooting section near the end of this post for help.

There are formatting conventions that are used throughout this post that you should be aware of. The following is a list outlining the color and font formats used.

Command Name or Directory Path
Warning or Error
Command Line Snippet With Commands/Options/Arguments
Command Options and Their Arguments Only
Hyperlink

Overview

There is no way for me to cover all of the issues surrounding shell script security in a single blog post. My goal with this post is to help you avoid some of the most common security holes that are often found in shell scripts. No script can be un-crackable, but you can make the cracker’s task more challenging by following a few guidelines. A secondary goal with this post is to make you more savvy about the scripts that you obtain to run on your systems. With the fact that scripts written for the BASH and SH shells are so portable in the Linux/Unix world, it can be easy for a cracker to write malware that will run on many different systems. Having some knowledge about the security issues surrounding shell scripts might just keep you from installing/running a malicious script such as a trojan, which gives the cracker a back door to your system. The Resources section holds books and links which will allow you to delve more deeply into this topic if you’re looking for more comprehensive knowledge. Listing 1 shows an example script that contains some of the security problems that we’ll talk about in this post.

Listing 1

#!/bin/bash # A SUID root script that demonstrates various security problems # Prepend the current path onto the PATH variable PATH=.:${PATH} #Count the number of lines in a listing of the current directory ls | wc -l # Get user input read USR_INPUT # Check to see if the user supplied the right password if [ $USR_INPUT == "mypassword" ];then echo "User input was $USR_INPUT and should have matched the string 'mypassword'" fi # Create a temp file touch /tmp/mytempfile # Set the temp file so that only the owner can read/write/execute the contents chmod 0700 /tmp/mytempfile # Save the password that the user supplied to the temp file echo $USR_INPUT > /tmp/mytempfile

Environment Variables

Your shell script has little to no chance of running securely if it trusts the environment that it runs in, and that environment has been compromised. You can help protect your script from unintended behavior by not trusting items like environment variables. Whenever possible, assume that input from the external environment has been designed to cause your script problems.

The PATH variable is a common source of security holes in scripts. Two of the most common issues are the inclusion of the current directory (via the . character) in the path, and using a PATH variable that’s been manipulated by a cracker. The reason that you don’t want the current directory included in the path is that a malicious version of a command like ls could have been placed in your current directory. For example, lets say your current directory is /tmp which is world writable. A cracker has written a script named ls and placed it in /tmp as well. Since you have the current directory at the front of your PATH variable in Listing 1, the malicious version of ls will be run instead of the normal system version. If the cracker wanted to help cover their tracks, they could run the real version of ls before or after running their own code. Listing 2 shows a very simple script that could replace the system’s ls command in this case.

Listing 2

#!/bin/bash # Run the real ls with the original arguments/options to cover our tracks /bin/ls "$@" # Run whatever malicious code we want here echo "Malicious code"

There’s a decent chance that any cracker who planted the fake ls would create it in such a way that it would look like ls was running normally. This is what I’ve done in Listing 2 by passing the @ variable to the real ls command so that the user doesn’t suspect anything. This brings up another point besides the use of the current directory in the path. Just because your script seems to be running fine from the user’s point-of-view doesn’t mean that it hasn’t been compromised. A good cracker knows how to cover their tracks, so if a security flaw has been exploited in your script the breach may go undetected for an indefinite period of time.

You can see in Listing 1 that the order of directories in the PATH variable makes a difference. This is important because if a cracker has write access to a directory that’s earlier in the search order, they can preempt the standard directories like /bin and /usr/bin that may be harder to gain access to. When you try to run the standard command, the malicious version will be found first and run instead. All the cracker has to do is insert a replacement command, like the one in Listing 2, earlier in the path search order.

The second main problem with the PATH environment variable is that it could have been manipulated by a cracker before, or as your script was run. If this happens, the cracker could point your script to a directory that they created which holds modified versions of the system utilities that your script relies on. Knowing this, it’s best if you add code to the top of your script to set the PATH variable to the minimal value your script needs to run. You can save the original PATH variable and restore it on exit. Listing 3 shows Listing 1 with the current directory removed from the PATH variable, and a minimal path set to lessen the chances of problems. Keep in mind though that a cracker could have compromised the actual system utilities that are in locations such as /bin and /sbin. Ways to detect and combat this occurrence fall more into the system security realm though and won’t be talked about in this post.

Listing 3

#!/bin/bash # A SUID root script that demonstrates various security problems # Save the current path variable to restore it later OLDPATH=${PATH} # Set a minimal path for our script to use PATH=/bin:/usr/bin #Count the number of lines ls | wc -l # Get user input read USR_INPUT # Check to see if the user supplied the right password if [ $USR_INPUT == "mypassword" ];then echo "User input was $USR_INPUT and should have matched the string 'test'" fi # Create a temp file touch /tmp/mytempfile # Set the temp file so that only we can read/write the contents chmod 0700 /tmp/mytempfile # Save the password that the user supplied to the temp file echo $USR_INPUT > /tmp/mytempfile # Reset the PATH variable to its original value PATH="$OLDPATH"

In your own scripts it would probably be best to put the reset of the PATH variable inside of a trap on the exit condition. That way PATH gets reset to the original value even if your script is terminated early. I wrote about traps in the last post in this series on error handling.

Another, less desirable way of avoiding malicious PATH exploits would be to use the full (absolute) path to the binary your script is trying to run. So, instead of just entering ls by itself, you would enter /bin/ls . This ensures that you’re running the binary that you want to, but it’s a more “brittle” approach. If your script is run on a system where the binary you are calling is in a different location, your script will break when the command is not found. One approach to help cut down on this drawback is to use the whereis command to locate the command for you. Caution needs to be applied with this approach too, but I’ve created an example in Listing 4 that shows how to do this. Remember that if the cracker has somehow compromised the system’s standard version of the command that you’re trying to run, this technique won’t help. That really starts being a system security problem rather than a script security problem at that point though.

Listing 4

#!/bin/bash - #File: findcmd.sh # Attempt to find the command with the whereis command CMD=$(whereis $1 | cut -d " " -f 2) # Check to make sure that the command was found if [ -n "$CMD" ];then echo "$CMD" fi

The script uses the command name to give the user the full path to the binary, if it can be found.There are of course numerous improvements that you could make to the script in Listing 4. My main suggestion would be to rewrite the script as a function, and then put that inside a script that you can source. That way you maximize code reuse throughout the rest of your scripts. I’ve done this in Listing 29 via the run_cmd function.

Another environment variable that can be problematic is IFS. IFS stands for “Internal Field Separator” and is the variable that the shell uses when it breaks strings down into fields, words, and so on. It can actually be a handy variable to manipulate when you’re doing things like using a for loop to deal with a string that has odd separator characters. If your shell inherits the IFS variable for it’s environment, a cracker can insert a character or characters that will make your script behave in an unexpected way. For example, suppose I have a few scripts in my ~/bin directory that I want to run together (or nearly together). The script in Listing 5 shows one very simple way of doing this.

Listing 5

#!/bin/bash - BINS="/home/jwright/bin/bin1.sh /home/jwright/bin/bin2.sh" for BIN in $BINS do echo $($BIN) done

When I run the script I get the output from bin1.sh and bin2.sh that I expect. In this case the scripts just output their name and exit. Everything is fine until a cracker comes along and sets the IFS variable to a forward slash (/). Now when I run my script I get the output in Listing 6.

Listing 6

$ ./ifscrack.sh ./ifscrack.sh: line 8: home: command not found ./ifscrack.sh: line 8: jwright: command not found ./ifscrack.sh: line 8: bin: command not found ./ifscrack.sh: line 8: bin1.sh : command not found ./ifscrack.sh: line 8: home: command not found ./ifscrack.sh: line 8: jwright: command not found ./ifscrack.sh: line 8: bin: command not found bin2.sh executing

Notice that since the directory /home/jwright/bin is in my path, the bin1.sh call should have run. If you look closely though you’ll see that there is a space after the filename, which causes the command to not be found. The IFS variable change has not only broken my script, it has allowed the cracker to open up a significant security hole. If the cracker creates a program or script with any of the names like home, jwright, or bin anywhere in the directories in PATH, their code will be executed with the privileges of my script. Because of the privilege issue, this security hole is an even bigger problem with SUID root scripts.

On some Linux distributions, the IFS variable is not inherited by a script and instead a default standard IFS value is used. You can still change the value of IFS within your script thought. With this said, it’s still a good idea to set the IFS variable to a known value at the beginning of your script and restore it before your script exits. This is similar to the change we made in Listing 3 to store and reset the PATH variable. This is a good idea because even though the distribution that your developing your script on may not allow IFS inheritance, your script may be moved to another distribution that does. It’s best to be safe and always set IFS to a known value.

Make sure that you never use the UID, USER, and HOME environment variables to do authentication. It’s too easy for a cracker to modify the values of these variables to give themselves elevated privileges. Now on the Fedora system that I’m using to write this blog post the UID variable is readonly, so I can’t change it. That doesn’t guarantee that every system that your script runs on will make UID readonly though. Err on the side of caution and use the id command or another mechanism to authenticate users instead of variables. The id command is very useful, and can give you information like effective user ID, real user ID, username, etc. Listing 7 is a quick reference of some of the id command’s options.

Listing 7

-g (--group) Print only the effective group ID -n (--name) Print a name instead of a number, for -ugG -r (--real) Print the real ID instead of the effective ID, with -ugG -u (--user) Print only the effective user ID -Z (--context) Print only the security context of the current user (SELinux)

You’ll need to use the options -u and -g with some of the other options (-r and -n) so that the id command knows whether you want information on the user or group. For example you would use /usr/bin/id -u -n to get the name of the user instead of their user ID.

The fact that the UID variable is set to readonly on my system gives you a hint at how to protect some variables. There is actually a command named readonly that sets variables to a readonly state. This does protect variables from being changed, but it also keeps you as the “owner” of the variable from making any changes to it too. You can’t even unset a readonly variable. To make a variable readonly, you would issue a command line like readonly MYVAR . Make sure to carefully evaluate whether or not a variable will ever need to change or be unset before setting it to readonly.

There’s an IBM developerWorks article in the Resources section (#20) that mentions security implications for some other environment variables such as LD_LIBRARY_PATH and LD_PRELOAD. That would be a good place to start digging a little deeper on the security issues surrounding environment variables.

Symbolic Links

You should always check symbolic links to make sure that a cracker is not redirecting you to their modified code. Symbolic links are a transparent part of everyday life for Linux users. Chances are that when you run sh on your favorite Linux distribution, /bin/sh is actually a link to /bin/bash . Go ahead and run ls -l /bin/sh if you’ve never noticed this before. Symbolic link attacks can take a few different forms, one of which is redirection of sensitive data. In one situation, you may think that you’re caching sensitive data to a file you’ve created in /tmp with 0700 file permissions. Instead, by exploiting a race condition in your script (we’ll talk about race conditions later) a cracker creates a symbolic link with the same filename that your script will be writing data into first, thus causing your creation of the temporary file to throw an error. If your script doesn’t stop on this error, it will begin dumping data into the file at the end of the symbolic link. The endpoint of the link could be on a mounted remote filesystem where the cracker can get easier access to it. There were several mistakes made in this scenario that we’ll talk more about later, but before that lets look at making sure we’re not writing data to a symbolic link.

Listing 8

#!/bin/bash - #File: symlink_test.sh # Poor method of temp file creation touch /tmp/mytempfile # Check the new temp file to see if it's a symbolic link IS_LINK=$(file /tmp/mytempfile | grep -i "symbolic link") # If the variable is not null, then we've detected a symbolic link if [ -n "$IS_LINK" ];then echo "Possible symbolic link exploit detected. Exiting." exit 1 #Exit before we dump the sensitive data into to the link fi # Dump our sensitive data into the temp file echo "Sensitive Data" > /tmp/mytempfile

If our script sees the string “symbolic link” in the output from the file command, it assumes that it’s looking at an attempted symbolic link exploit. Rather than continuing on and possibly sending data to a cracker, the script chooses to warn the user and exit with an exit status indicating an error. Be aware thought that this script doesn’t protect against the situation where a cracker creates your temp file in place with permissions to give themselves access to the data. In the case that you don’t expect a the temp file to already be there, you would throw an error and exit. This brings up another problem though – DoS (Denial of Service) attacks. If the cracker simply wants your script to fail, all they have to do is make sure your temp file has already been created so that your script will throw and error and exit. You’re not handing over sensitive data, but your users are being denied the use of your script. The answer to this is to create temporary files with less-predictable file names.

“Safe” Temporary Files

In the header for this section, I put the word safe in quotes to denote that it’s very difficult to make anything completely safe. What you have to do is make things as safe as possible, and then keep an eye out for suspicious activity. In the last blog post I created a a function named create_temp that used a simple, but risky mechanism to create temp files. A snippet of the code from that listing is shown in Listing 9.

Listing 9

# Function to create "safe" temporary files function create_temp { # Give preference to user tmp directory for security if [ -e "$HOME/tmp" ] then TEMP_DIR="$HOME/tmp" else TEMP_DIR="/tmp" fi # Construct a "safe" temp file name TEMP_FILE="$TEMP_DIR"/"$PROGNAME".$$.$RANDOM # Keep the file in an array to remove it later TEMPFILES+=( "$TEMP_FILE" ) { touch $TEMP_FILE &> /dev/null } || fatal_err $LINENO "Could not create temp file $TEMP_FILE" }

The problem with this function is that it uses a temporary file name with 2 elements that are easy to predict – the program name and the process ID. The fact that there is a random number on the end is only an inconvenience for the cracker, because all they have to do is create a file for each possible file name with an ending number between 0 and 32767. They can be sure that you’ll dump data into one of those files, and it’s easy to write a script to find out which file holds the data. A slightly better method would be to append multiple sets of random numbers onto the file name, separating each set with periods. This makes it much harder for the cracker to cover all the possible file names. A much better way to handle this situation is to use the mktemp command, which is available on most Linux systems.

The mktemp command takes a string template that you supply and creates a unique temporary file name. The form could be something like mktemp /tmp/test.XXXXXXXXXXXX which would print the random file name to standard out and create a file with that name and path. Running that command line on a Fedora 13 system once gave me the output /tmp/test.o0mTLAgSWTfX which of course will vary each time you run the command. The more X characters you add to the template, the harder it is for a cracker to predict the file name. From what I’ve read, 10 or so is the recommended minimum amount. Another nice thing about mktemp is that when it creates a temp file, it makes sure that only the owner has access to it. Some useful options for mktemp are shown in Listing 10. You should use mktemp in preference to commands like touch and echo to create temp files.

Listing 10

-d (--directory) Create a directory, not a file. -q (--quiet) Suppress diagnostics about file/dir-creation failure. --suffix=SUFF Append SUFF to TEMPLATE. SUFF must not contain slash. This option is implied if TEMPLATE does not end in X. --tmpdir[=DIR] Interpret TEMPLATE relative to DIR. If DIR is not specified, use $TMPDIR if set, else /tmp. With this option, TEMPLATE must not be an absolute name. Unlike with -t, TEMPLATE may contain slashes, but mktemp creates only the final component.

There are just a few other miscellaneous facts about mktemp that I want to make sure you’re aware of.

  1. The man pages for mktemp on both Ubuntu 9.10 and Fedora 13 systems specify that the minimum number of X characters that you can have in a template is three. Even though you can go this low, I wouldn’t recommend it because it greatly increases the predictability of your file names. Ten or more random alpha-numeric characters is better.
  2. mktemp is commonly part of the coreutils package.
  3. The default number of X characters that you get when you don’t specify a template with mktemp is 10. This held true on the Fedora 13 and Ubuntu 9.10 systems that I tested.

So what happens if you don’t have mktemp on your system? The LinuxSecurity.com article in the Resources section (#17) gives a way to use mkdir to create a temporary directory that only the creator has access to. A script based on the examples in that article is found in Listing 11, but should not be used in preference to the mktemp command unless you have a compelling reason.

Listing 11

#!/bin/bash - #File safetmp.sh # Give preference to user tmp directory for security if [ -e "$HOME/tmp" ];then TEMP_DIR="$HOME/tmp" else TEMP_DIR="/tmp" fi # Create somewhat secure directory name TEMP_NAME=${TEMP_DIR}/$$.$RANDOM.$RANDOM.$RANDOM # Create the directory while at the same time giving # only the user access to it (umask 077 && mkdir $TEMP_NAME) || { echo "Error creating the temporary directory." exit 1 }

Notice that this script does use multiple references of the RANDOM variable separated by periods to make the directory name harder to guess. Also, the umask is set to 077 just before the directory is created so that the end directory permissions are 700. That gives the owner full access to the file, but none to anyone else. At the top of the script I have reused code from the create_temp function in Listing 9. This code gives preference to the user’s home directory over the system (/tmp) directory. If the temporary file or directory that you are creating can be placed in the user’s home directory, that’s just one more layer of protection from prying eyes. I would suggest using the user’s own tmp directory whenever possible.

Keep in mind that as I mentioned above, even though you’ve protected the data in the temp files a cracker can still launch a DoS (Denial of Serivce) attack against your script. In this case since the cracker probably can’t guess the temporary file name, they might try to fill the /tmp directory so that there’s no more space for you to create your file. Things like user disk quotas can help mitigate this type of attack though.

Now that you know a little more about temp file safety, I’ll caution you not to overuse temporary files. When you store or use data in external files you are opening a door into your script that a knowledgeable individual may be able to exploit. Use temp files only when needed, and make sure to consistently follow safe guidelines for their use.

Race Conditions

A race condition occurs when a cracker has a window of opportunity to preempt and modify your scripts behavior, usually by exploiting a design flaw in the execution sequence of your script, or in its reliance on an external resource (like a lock file). The example that we’ve already talked about is creating a symbolic link or a file in place of the script’s temp file to capture data. The script that I’ve created in Listing 12 uses the sleep command to create a larger window for a race condition.

Listing 12

#!/bin/bash - #File: race_cond.sh TEMP_FILE=/tmp/predictable_temp # Make sure that the temp file doesn't already exist if [ ! -f $TEMP_FILE ];then # Do something here that takes 10 seconds. This # creates the race condition and is simulated by # the sleep command sleep 10 # Create the temp file touch $TEMP_FILE # Make sure only the user can view the contents chmod 0700 $TEMP_FILE # Dump our sensitive data to the temp file echo "secretpassword" > $TEMP_FILE fi

Once the script is run, the cracker has 10 seconds to create the temp file before the script does. The timing is rarely as simple as I have made it out to be in this example, but the 10 second gap between checking for the existence of the file and the creation of it illustrates the point. The two lines in Listing 13 can be entered as a different user. The touch command in Listing 12 will fail because the file is owned by a different user, but the script has another flaw in that it doesn’t check for that error before writing the data. Because of this the sensitive data is written into a file that is easy for the cracker to read. Checking for an error and making sure that the file you want to create doesn’t already exist and has the correct permissions would go a long way toward making this script more secure.

Listing 13

touch /tmp/predictable_temp chmod 0777 /tmp/predictable_temp

When the 10 second delay expires in my script I get the error chmod: changing permissions of `/tmp/predictable_temp': Operation not permitted just before the data is written to the file. The temp file is accessible to the cracker using the cat command, and an ls -l of the temp file shows that it’s owned by the user name that the cracker used. There are other race condition exploits, but the moral of the story is to not leave gaps between critical sections of your script. Listing 11 shows a good example of closing the gap between operations. In that case the permissions are set as the directory is created by setting the umask before the call to mkdir. Race conditions are certainly something to keep in mind as your attempt to increase the security of your scripts.

The Shebang Line

You may have noticed before that I put a dash and a space (a bare option) at the end of most of my script shebang lines. This is the same as the double dash (--) option and signals the end of all options for the shell. Any options that are tacked onto the end of the shebang line will be treated as arguments to the shell, and will most likely throw an error. The reason that this is important is that it prevents option spoofing. On some systems if the cracker can get the shebang line to effectively read #!/bin/sh -i they will get an interactive shell with the privileges of the script. It’s important to note that I was not able to get an interactive shell using a script on a Fedora 13 system, even when I entered the shebang line directly as having the -i option. Even so, you don’t always know which systems your script will run on, and it only takes a fraction of a second to add the dash (or double dash) at the end of your shebang line. That’s a very small price to pay for some added security.

User Input

As I discussed in the error handling post of this series, user input should be processed cautiously. Even when there is no malicious intent by a user very serious errors can result from incorrect input. At its worst, user input can give a cracker an open door into your system through things like injection attacks. Keeping this in mind, there are a few guidelines that you can follow to help keep user input from bringing your script down.

If you can avoid it, don’t pass user input to the eval command, or pipe the input into a shell binary. This is a script crash or security problem waiting to happen. Listing 14 shows the wrong way to handle user input when it’s captured with the read command.

Listing 14

#!/bin/bash - #File: badinput.sh # Get the input from the user read USR_INPUT # Don't use eval like this eval $USR_INPUT # Don't pipe input to a shell like this echo $USR_INPUT | sh

It’s probably pretty easy to agree with me that the script in Listing 14 is a bad idea. The user can type any command string they want (including rm -rf /*) and it will be executed with the privileges of the script. Depending on how much the permissions of the script are elevated, this could do a lot of damage. Another scenario that may seem more harmless is the one in Listing 15.

Listing 15

#!/bin/bash - read USR_INPUT if [ $USR_INPUT == "test" ];then echo "You should only see this if you typed test." fi

Everything works fine until a cracker enters the string random == random -o random and hits enter. What this effectively does is changes the if statement so that it reads if [ random == random -o random == "test" ] where the -o is a logical or. It tells the if statement that either the first statement or the second statement has to be true, but not both. Of course the first statement (random == random) is true, so what’s inside the if statement executes even though the cracker didn’t type the correct word or phrase. Depending on what’s inside the if statement, that security hole could range from a minor to major problem. The way to combat this is to quote your variables (i.e. "$USR_INPUT") so that they are tested as a whole string. In general quoting your variables is a good idea as you’ll also head off problems with things like spaces that might otherwise cause your script trouble.

This is an example of an injection attack where the cracker slips some extra information in with the input to trick your script into running unintended code. This is a very common attack “vector” for database and web servers where a cracker carefully crafts a request to cause arbitrary code to execute, or to bring down the web/database service. Another script that can be exploited by an injection attack is found in Listing 16.

Listing 16

#!/bin/bash - read USR_INPUT #This line contains the fatal security flaw echo ls $USR_INPUT > test.sh

This script isn’t necessarily something that you would do in the real world, but it’s a simple way to demonstrate this injection attack. What the script does is takes a list of directories from the user and then builds a script using the ls command to list the contents of the directories. The injection attack comes when a cracker types && rm randomfile and you find that the resulting script (test.sh) contains a line that will delete files (Listing 17). The && rm randomfile line could have just as easily be && rm -rf /* if the cracker wanted.

Listing 17

ls && rm randomfile

The && operator runs the second command in the sequence if the first command runs successfully (without an error). The ls command is not likely to fail by itself as it just lists the contents of the current directory, so the rm command will most likely run and delete files. The method to deal with this type of attack is similar to the previous method of quoting, except that in this case you escape the quotes around the user input to make sure that it is properly contained. Listing 18 shows the corrected script.

Listing 18

#!/bin/bash - read USR_INPUT #This line uses escaped quotes to enclose the potentially dangerous input echo ls ""$USR_INPUT"" >> test.sh

Along with quoting, it’s a good idea to search user input for unacceptable entries like meta or escape characters. You can search the user input for these undesirable characters and replace them with something harmless to your script like a blank character or underscore. When doing this, it may be easier to search for the characters that are acceptable instead of trying to cover every single character that’s not acceptable. The set of acceptable characters is almost always smaller, and it’s hard to anticipate every bad character that might be passed to your script. Listing 19 shows a simple way of cleaning the input using the tr command.

Listing 19

#!/bin/bash - #File: scrubinput.sh # Grab the user's input read USR_INPUT # Remove all characters that aren't alphanumeric or newline USR_INPUT=$(echo "$USR_INPUT" | tr -cd '[[:alnum:]n]')

This script takes the user input using the read command as before, but then pipes the value directly into the tr command. The tr command’s -c (--complement) and -d (--delete) options are used to cause tr to look for and delete the unmatched characters. So, anything that’s not an alphanumeric character (via the alnum character class) or a newline character will be deleted. It’s not hard to adapt the tr statement to your situation, maybe even replacing the characters instead of deleting them.

As with the other topics in this post I’m scratching the surface, but hopefully you can see how important it is to check user input before doing anything with it. The inability of a script or program to handle improper input is a common bug in the software world. Whether the user has malicious intent or not, bad user input is something that you must plan for.

SUID, SGID, and Scripts

There are several of the above scenarios that may not cause that much harm on their own because the user running the script has restricted permissions. This can all change with a script that has its SUID and/or SGID bit set though. The SUID and SGID bits show up in the first of four digits in the octal representation of a file’s permissions. The SUID bit has a value of 4 and the SGID bit has a value of 2. If both bits are set you get a value of 6, which is similar to how normal permission bits can be added. The other place that you normally see the SUID and SGID bits are in the symbolic permission string. There they show up as the character “s” in either the user execute permission space, or in the group execution space respectively. For example, if only the SUID bit was set on a script and the file had read/write/execute permissions of 755, the full permissions for the script would be 4755. The symbolic representation of this would be -rwsr-xr-x .

When the SUID bit is set on an executable, the file is run using the privileges of the file’s owner. In the same way if the SGID bit is set on an executable, it will be run with the rights of the file owner’s group. Typically a command/script executes using the real user ID (and rights), but when the SUID or SGID bits are set the script executes with the effective user ID of the file owner instead. A common use is to have the SUID bit set on a file that is owned by root so that a user can access files and resources that they normally wouldn’t have access to. The passwd command is a good example of this. In order to change a user’s password, passwd has to access protected files such as /etc/passwd and /etc/shadow . If a normal user is running the passwd command, they would need elevated privileges to access the files since they are only readable and writable by root. This is very handy, and as you’ve seen, sometimes required on Linux systems but is something that you should avoid doing with your scripts whenever possible. The problem with an SUID root script is that if a cracker compromises that script, they have superuser privileges that could be used to run commands like rm -rf /* . As a programmer and/or system administrator, you need to guard against the tendency to take the easiest route to a solution rather than the most secure one. All to many admins will set a script to be SUID root when with some thought the script could have been designed to run without superuser privileges. With that said, you may run into situations where you have to use SUID and SGID. Just make sure that it’s a true “have to” situation. Always follow the Rule of Least Privilege which says that you should never give a user or a program any more rights than you have to.

If you really need to use the SUID and SGID bits, you can set them with the chmod u+s FILENAME and chmod g+s FILENAME command lines respectively. Keep in mind that there are Linux distributions and Unix variants that do not honor the SUID bit when it is set on a script. You’ll need to check the documentation for your Linux distribution to be sure that setting the SUID bit will work.

You can use the find command to search for files on your system with the SUID and SGID bits set. You can use this as a security auditing tool to search for SUID/SGID scripts that look out of place. Listing 20 shows a quick and simple way to search for out of place SUID/SGID shell scripts that are on your system.

Listing 20

$ sudo find / -type f -user root -perm /4000 2> /dev/null | xargs file | grep "shell script" /usr/bin/malscript.sh: setuid Bourne-Again shell script text executable

Let’s take the command line from Listing 20 one step at a time. The first section is the actual find command (find / -type f -user root -perm +4000). The find command searches for a file of type regular file (-type f) and not a directory, it checks to make sure that the file is owned by root (-user root), and that it has the SUID bit set (-perm /4000). The next short section of 2> /dev/null redirects any errors to the null device so that they are thrown away. This effectively suppresses errors resulting from find trying to access things like Gnome’s virtual file system. The file command deciphers which type of file is being looked at. This command is not perfect, but will work for a quick and dirty security audit. The file command needs to work on each of the file names individually, so I use the xargs command to run file separately with each line of output from the find command. I could have also used the -exec option of find in the following way: -exec file '{}' ; . The command line up to this point gives me output telling what each type of file is, but I really only care about shell scripts. That’s where the grep statement comes in. I use grep to filter out only the lines that mention a “shell script”.

As you can see in the output of the command line, there is a suspicious file called malscript.sh in /usr/bin . Searching in this way made a file that normally would be overlooked stand out by itself. In this case I created that script and put it in /usr/bin myself so that I would have something to find, but it simulates something that you might find in the field. You could just as easily have searched for SGID scripts (-perm /2000), SUID/SGID combo scripts (-perm /6000), SUID root binaries, and much more. Be aware that if the owner execution bit is not set on a directory then it is not searchable. This would cause the find command to skip over the directory, possibly causing you to miss a suspicious file.

The SUID root mechanism can be especially dangerous if a cracker manages to make a copy of a shell binary and sets it to be SUID root. Some shells such as BASH will automatically relinquish their privileges if they’re being run this way. Keep an eye out for extra copies of shell binaries that are set SUID, as they could be part of an attack by a cracker. The shell binary could have been copied and modified using several of the security flaws that we’ve talked about above. You could use the script in Listing 20 to help you search for SUID root copies of shell binaries.

When running scripts manually as a system administrator, you should run scripts with temporary elevated privileges through a mechanism like sudo whenever possible, rather than setting a script to be SUID root. Even with sudo though you still need to make sure your script is secure as possible because sudo is still granting your script root privileges, and it doesn’t take much time to do a lot of damage. Item #16 in the Resources section touches on many of the security aspects that we’ve talked about here from the perspective of proper sudo usage.

In some cases a user may install or use your script improperly, running it as SUID root or with sudo. If you never want your script run as root, you could use the id command along with some text manipulation to warn the user and then exit. The script in Listing 21 shows one way of doing this.

Listing 21

#!/bin/bash - #File: droproot.sh # Check to see if we're running as root via sudo if [ $(/usr/bin/id -ur) -eq 0 ];then echo "This script cannot be run with sudo" exit 1 fi # Get the listing on this script INFO=$(ls -l $0) # Grab the permission at the SUID position PERM=$(echo "$INFO" | cut -d " " -f 1 | cut -c 4) # Grab the owner OWNER=$(echo "$INFO" | cut -d " " -f 3) # Check for the SUID bit and the owner of root if [ "$PERM" == "s" -a "$OWNER" == "root" ];then echo "This script cannot be run as SUID root" exit 1 fi

The script uses the id command to check the real user ID of the user, and if it’s 0 (root) then the script warns the user that the script is not supposed to be run with sudo or as root and exits. To check for the SUID root condition, I’ve taken a slightly more complicated route. I run the command line ls -l $0 which gives me a long listing for the script name (represented by $0) showing the symbolic permission string and the owner. I then extract the character in the permission string that would represent the SUID bit as an “s” if present so that I can check it. This is done with the cut -c 4 command line which extracts the fourth character. Once I have the SUID bit and the user, I just use an if statement to check to see if both the SUID bit is set and that the script is owned by root. If both of those conditions are true, I warn the user that the script can’t be SUID root and exit.

One of the nice things about the BASH shell is that if it detects that it has been run under the SUID root condition, it will automatically drop its superuser privileges. This is nice because even if an attacker is able to make a copy of the bash binary and set it as SUID root, it will not allow them to gain additional access to the system. Unfortunately, most crackers are going to know this and will try to make a copy of another shell like sh that doesn’t have this feature.

The last thing that I’ll mention about SUID root scripts is that I have seen it suggested by several system administrators that you should use Perl or C whenever you must use SUID root. There have been arguments for and against using Perl or C in place of shell scripting, and ultimately you must decide which you feel safer with. I’m not going to argue the point, but I will say that if you use unsafe practices when writing your Perl scripts or C programs, you’re going to end up no better off anyway. Take your time and make sure the code you write is as secure as you can make it. This is a rule to live by no matter what language you’re using.

Storing Sensitive Data In Scripts

This is just a bad idea, do your best to avoid it. If you store passwords in a script they’re just waiting to be found. Even if you set the permissions to 0700, the passwords will still be compromised if a cracker compromises your account. There’s also the risk that you might accidentally send the script to another user, and forget to scrub the passwords from it.

You should also not echo passwords as a user types them. Shoulder surfers could see the password as the user enters it if you have the shell set to echo user input. To avoid this in your script, you can use stty -echo as I have in the very simple example in Listing 22.

Listing 22

#!/bin/bash - # Turn echoing off stty -echo # Read the password from the user echo "Please enter a password: " read PASSWD # Turn echoing back on stty echo

Notice that only what the user types is suppressed and not the output from the echo command itself. This of course doesn’t protect the user from somebody watching what their fingers press on the keyboard, but there’s nothing that you as a programmer can do about that.

If you do end up storing passwords in your script or in files on your system, it would be a good idea to encrypt the information. You can encrypt passwords using the md5sum or sha*sum commands. You can pipe the password string straight into the command as with the line echo "secretpassword" | sha512sum . I would suggest writing a script that takes the password without echoing the input and converts it into an encrypted hash. Once you’ve encrypted the password this way it is never decrypted, you just encrypt the password given by the user and compare that to the stored password hash. That way the password is not out in clear text for a cracker to find. Granted, it’s still possible to crack encryption, but remember that no system is bulletproof and the goal is to make the crackers life as difficult as possible.

One habit that you should encourage with your users (and any system admins under you) is picking long and complex passwords. To ease the strain of having to remember a convoluted password, have users build passwords based on first letters and punctuation from a random phrase. For instance, the phrase “This is 1 fairly strong password, don’t you think Jeremy?” would reduce to “Ti1fsp,dytJ?”. The specific phrase doesn’t matter, but it should include a mix of numbers, letters (upper and lowercase), and symbols to be the most secure. Make sure that all of the symbols being used are acceptable for the system you’re choosing the password for though.

The shc Utility

The shc utility compiles a script in order to make it harder for a cracker to read its contents. This is especially useful if you find that you have to store passwords or other sensitive information inside of a script. Take note that I said “harder” and not “impossible” for a cracker to read. It’s been shown that shc compiled scripts can be reverse engineered to gain access to the contents. Remember that you should strive to make sure that your protection mechanisms are multi-layered. If you use shc to compile a script with passwords in it, encrypt the passwords with the md5sum command, and set the access permissions to be as restrictive as possible. That way you’re not just relying on shc to keep your data safe. Some of the options for the shc utility are shown in Listing 23.

Listing 23

-e date The date after which the script will refuse to run (dd/mm/yyyy) -f script_name The file name of the script to compile -m message The message that will be displayed after the expiration date -T Allow the binary form of the script to be traceable -v Verbose output

Using these options I compiled a sample script via the command line in Listing 24, looked at what files were created, and then tried to run the resulting binary. The version of shc that I used was 3.8.7 which I compiled from source. I then copied the shc binary to my ~/bin directory so that I could run it more conveniently.

Listing 24

$ shc -e 08/09/2010 -m "Please contact your administrator" -v -f test.sh shc shll=bash shc [-i]=-c shc [-x]=exec '%s' "$@" shc [-l]= shc opts=- : No real one. Removing opts shc opts= shc: cc test.sh.x.c -o test.sh.x shc: strip test.sh.x shc: chmod go-r test.sh.x $ ls test.sh test.sh.x test.sh.x.c $ ./test.sh.x ./test.sh.x: has expired! Please contact your administrator

You can see in Listing 24 that I’ve set an expiration date of September 8th, 2010, which is earlier than the date that I’m writing this. I supply the expiration message of “Please contact your administrator”, I ask shc for verbose output, and then I give it the script that I want it to compile (test.sh). When I list the files in the directory I see test.sh, test.sh.x, and test.sh.x.c . test.sh.x is the compiled binary that shc creates from my original script. test.sh.x.c is the C source code that is generated for test.sh . Be careful to keep this file in a safe place as it gives critical information that will compromise your compiled script. In Listing 24 I get an error when I try to run the compiled script (test.sh.x), but this is expected as I used an expiration date in the past. I did this just to show you how the compiled script would react when the expiration period expires. You don’t have to specify the expiration date, but it can be handy if you only want to give a user access to a script’s capabilities for a few days or weeks.

Overall shc is a nice tool to have at your disposal, but as I mentioned above don’t count on it for foolproof protection. The Linux Journal article in the Resources section (#5) talks about how shc compiled scripts can be cracked. Additional features have been added to newer versions of shc, such as the removal of group and other read permissions by default, to make the compiled scripts harder to get at. Even so, make sure that you have multiple layers of security surrounding your scripts as we’ve talked about earlier.

How-To

At this point, let’s take what we’ve discussed so far and apply it to the script in Listing 1. I’ve already removed the current directory from the PATH variable, and made sure that we start off with a clean path by resetting the variable in Listing 3. The script in Listing 25 shows the script that we’ll be starting with.

Listing 25

#!/bin/bash # A SUID root script that demonstrates various security problems # Save the current path variable to restore it later OLDPATH=${PATH} # Set a minimal path for our script to use PATH=/bin:/usr/bin #Count the number of lines ls | wc -l # Get user input read USR_INPUT # Check to see if the user supplied the right password if [ $USR_INPUT == "mypassword" ];then echo "User input was $USR_INPUT and should have matched the string 'test'" fi # Create a temp file touch /tmp/mytempfile # Set the temp file so that only we can read/write the contents chmod 0700 /tmp/mytempfile # Save the password that the user supplied to the temp file echo $USR_INPUT > /tmp/mytempfile # Reset the PATH variable to its original value PATH="$OLDPATH"

Now that we have a minimal and known PATH variable set, we can feel a little better about running the ls | wc -l command line. As stated before, we could use absolute paths for each command but that could lead to a portability issue on some systems where the binaries are stored in different locations.

The next step is to deal with the user input. I’m first going to put quotes around the variable to help ensure that it’s treated as a string, and not a part of the statement. Also, just after the read line I’m going to scrub the input to make sure there aren’t any inappropriate characters contained within it. Listing 26 shows the script with these changes.

Listing 26

#!/bin/bash # A SUID root script that demonstrates various security problems # Save the current path variable to restore it later OLDPATH=${PATH} # Set a minimal path for our script to use PATH=/bin:/usr/bin #Count the number of lines ls | wc -l # Get user input read USR_INPUT # Remove all characters that aren't alphanumeric or newline USR_INPUT=$(echo "$USR_INPUT" | tr -cd '[[:alnum:]n]') # Check to see if the user supplied the right password if [ "$USR_INPUT" == "mypassword" ];then echo "User input was $USR_INPUT and should have matched the string 'mypassword'" fi # Create a temp file touch /tmp/mytempfile # Set the temp file so that only we can read/write the contents chmod 0700 /tmp/mytempfile # Save the password that the user supplied to the temp file echo $USR_INPUT > /tmp/mytempfile # Reset the PATH variable to its original value PATH="$OLDPATH"

The section of code that scrubs the user input is taken from Listing 19, and a full explanation of the process can be found in the paragraphs following that listing. In short, the user input is echoed into the tr command so that all characters except alpha-numeric and newline characters are deleted.

Of course as I mentioned above, you wouldn’t want to store any password information in a script unless you have to. If it becomes necessary to store a password inside a script it’s best to encrypt the password using a command like md5sum. Think about this decision carefully because there is almost always a way to avoid storing a password inside of a script. For the purpose of this example, I’ve decided to leave the password in the file and use md5sum to encrypt it. Listing 27 shows the results of adding password encryption.

Listing 27

#!/bin/bash # A SUID root script that demonstrates various security problems # Create the array that will keep the list of temp files TEMPFILES=( ) # Function to create "safe" temporary files. function create_temp { # Give preference to user tmp directory for security if [ -e "$HOME/tmp" ] then TEMP_DIR="$HOME/tmp" else TEMP_DIR="/tmp" fi # Construct a "safe" temp file using mktemp TEMP_FILE=$(mktemp --tmpdir=$TEMP_DIR XXXXXXXXXX) # Keep the file in an array to remove it later TEMPFILES+=( "$TEMP_FILE" ) } # Save the current path variable to restore it later OLDPATH=${PATH} # Set a minimal path for our script to use PATH=/bin:/usr/bin #Count the number of lines ls | wc -l # Make sure that nobody can see the password as it's entered stty -echo # Get user input read USR_INPUT # Re-enable echoing of typed input stty echo # Remove all characters that aren't alphanumeric or newline USR_INPUT=$(echo "$USR_INPUT" | tr -cd '[[:alnum:]n]') # Check to see if the user supplied the right password, but use encryption if [ $(echo "$USR_INPUT" | md5sum | cut -d " " -f 1) == "d84c7934a7a786d26da3d34d5f7c6c86" ];then # Don't echo the user's password, just tell them it worked echo "Password Accepted." fi # Call the function that will create a "safe" temp file for us create_temp # Make sure that the temp file/name was added to the array echo ${TEMPFILES[0]} # Reset the PATH variable to its original value PATH="$OLDPATH"

Next, we start getting into the temporary file section of the script. I had created a function for this in the last blog post, but we’ll write the function from scratch here applying what we’ve learned so far. Listing 28 shows the new function and it’s implementation within the script.

Listing 28

#!/bin/bash # A SUID root script that demonstrates various security problems # Create the array that will keep the list of temp files TEMPFILES=( ) # Function to create "safe" temporary files. function create_temp { # Give preference to user tmp directory for security if [ -e "$HOME/tmp" ] then TEMP_DIR="$HOME/tmp" else TEMP_DIR="/tmp" fi # Construct a "safe" temp file using mktemp TEMP_FILE=$(mktemp --tmpdir=$TEMP_DIR XXXXXXXXXX) # Keep the file in an array to remove it later TEMPFILES+=( "$TEMP_FILE" ) } # Save the current path variable to restore it later OLDPATH=${PATH} # Set a minimal path for our script to use PATH=/bin:/usr/bin #Count the number of lines ls | wc -l # Make sure that nobody can see the password as it's entered stty -echo # Get user input read USR_INPUT # Re-enable echoing of typed input stty echo # Remove all characters that aren't alphanumeric or newline USR_INPUT=$(echo "$USR_INPUT" | tr -cd '[[:alnum:]n]') # Check to see if the user supplied the right password, but use encryption if [ $(echo "$USR_INPUT" | md5sum | cut -d " " -f 1) == "d84c7934a7a786d26da3d34d5f7c6c86" ];then # Don't echo the user's password, just tell them it worked echo "Password Accepted." fi # Call the function that will create a "safe" temp file for us create_temp # Make sure that the temp file/name was added to the array echo ${TEMPFILES[0]} # Reset the PATH variable to its original value PATH="$OLDPATH"

Within the create_temp function, I use the TEMPFILES array to hold the file names and paths of the temporary files that I create. That way I can remove them later when the script is finished. Normally I would add a trap to handle this which I talked about in the last blog post on error handling. I left the trap out of Listing 28 just to keep the example a little bit shorter. When the create_temp function is called, the script first checks to see if the user has their own tmp directory. If they do, it is used in preference to the main /tmp directory since it is world writable. Once the tmp folder has been selected it is passed to the mktemp command using the --tmpdir option. mktemp creates the temp file, and the pathname of the file that was created is stored in a variable. According to our error handling knowledge, I should be checking to make sure that the temp file was created and that there were no errors, but I’ve left this check out to keep the script more streamlined. In your own use of this script code you’ll want to apply the error handling techniques that we talked about in the last post. The path and file name that’s stored in the variable is then added to the TEMPFILES array to be dealt with later. Once that’s done, the temp file is ready for use. Normally you would redirect data into the temp file, but I just echoed the path and name of the temp file instead.

The last thing that I do is to restore the PATH variable using the saved value in the OLDPATH variable. This undoes the change that we made at the beginning of the script which helped us run system commands more safely.

There are still improvements that can be made to this script based on what has been discussed in previous posts. Please add your ideas about the script in the comments on this post.

Tips and Tricks

  • Never copy commands or code from forums, blogs, and the like without checking them to make sure they’re safe. Resource #2 has a list of malicious commands that have been given out by problem users in the Ubuntu Forums. Your best defense is to review the commands/code thoroughly yourself, or find someone who can review it for you before you execute it. You can also post the code to other forums and ask the users there if it’s safe.
  • Always be suspicious of external inputs to your script whether they be variables, user input, or anything else. We talked about validating user input in the last post on error handling as well as this one. It’s important to remember that incorrect input is not always the doing of a cracker. Many times users make honest mistakes, and your script needs to be able to handle that eventuality.
  • Make sure that your script is writable only by the owner. That makes direct code injection attacks harder for a cracker to accomplish.
  • Use the cd command to change into a trusted directory when your script starts. This way you have a known starting point.
  • When you’re writing a script, always assume that it will be installed and run incorrectly. If it’s designed to be in a directory that’s only readable/writable by the owner, and it holds sensitive information, assume that it’s going to be placed in a world writable directory with full permissions for everyone. Don’t hard code an installation directory into your script unless you have to.
  • Don’t assume that your script is always going to be run as a regular user, or just as the super user. You need to understand what your script will do when run by unprivileged and privileged users.
  • Attempt to keep your scripts and files out of world writable directories like /tmp as much as possible.
  • Don’t give users access to programs with shell escapes (like vi and vim) from your scripts, especially when elevated privileges are involved.
  • Do not rely only on one security technique to protect your script and your users. Putting all your faith in a method like “security through obscurity” (such as password encryption) while ignoring all of the other security tools in your box is asking for trouble. Some security methods can give you a false sense of security, and you need to be vigilant. Remember, try to make the crackers life as difficult as you possibly can. This involves a multi-tiered script security strategy.
  • Use secure versions of commands in your scripts whenever possible. For instance, use ssh and scp instead of telnet and rcp, or the slocate command rather than the locate command. The man page for the base command will sometimes point you toward the more secure versions.
  • Have other coders look over your script to check it for problems and security holes. You can even post your script to various forums and ask them to try to break it for you.
  • Make sure that any startup and configuration scripts that you add to your system are as secure and bug free as possible. Don’t add a script to the system’s init or Upstart mechanism without testing it thoroughly.
  • When using information like passwords within your script, try not to store the information within environment variables. Instead use pipes and redirection. The data will be harder to access by a cracker.
  • When creating and running scripts you should follow the Rule Of Least Privilege by only giving the minimal set of privileges that the script needs to do it’s job. Also, make sure that you’ve designed the script well so that it doesn’t need elevated privileges unnecessarily. For instance, if a script works well with ownership of nobody and a permission string of 0700, don’t set the script to be owned by root and have permissions of 4777 .
  • In the appropriate context, use options for commands that tend to enhance security and resistance to bad input. For instance, the find command has an option -print0 that causes the output to be null terminated instead of newline terminated. The xargs command has a similar option (-0). These options can help ensure that input containing things like newlines won’t break your script. This requires extra study of what can go wrong with your script, and how to use the available commands to avoid anything going wrong.
  • If you have scripts shared via something like a download repository, consider giving your users md5 and/or sha1 sum values so that they can check the integrity of a script they download. If you’re emailing a script, you might want to use GPG so that you can do things like ensuring that the contents of the script have not been tampered with, and that a third party cannot read the contents of the script in transit.

Scripting

These scripts are somewhat simplified and in most cases could be done other ways too, but they will work to illustrate the concepts. If you use these scripts, make sure you adapt them to your situation. Never run a script or command without understanding what it will do to your system.

This first script (Listing 29) is a compilation of the shell script code that I’ve demonstrated throughout this post. The code has been organized into functions and placed in a separate script that can be sourced to add security specific code to your own scripts. Keep in mind though that the functions in this script don’t give you comprehensive coverage. Once again, we’re barely scratching the surface.

Listing 29

#!/bin/bash - # File: security_src.sh # Script that you can source to add a few security features to # your own scripts. #Variables to store the old values of the IFS and PATH variables OLD_IFS="" OLD_PATH="" # Function to create "safe" temporary files. function create_temp { # Give preference to user tmp directory for security if [ -e "$HOME/tmp" ] then TEMP_DIR="$HOME/tmp" else TEMP_DIR="/tmp" fi # Construct a "safe" temp file using mktemp TEMP_FILE=$(mktemp --tmpdir=$TEMP_DIR XXXXXXXXXX) # Keep the file in an array to remove it later TEMPFILES+=( "$TEMP_FILE" ) } # Function that will keep this script from being run with any kind # of root privileges. function drop_root { # Check to see if we're running as root via sudo if [ $(/usr/bin/id -ur) -eq 0 ];then echo "This script cannot be run with sudo" exit 1 fi # Get the listing on this script INFO=$(ls -l $0) # Grab the permission at the SUID position PERM=$(echo "$INFO" | cut -d " " -f 1 | cut -c 4) # Grab the owner OWNER=$(echo "$INFO" | cut -d " " -f 3) # Check for the SUID bit and the owner of root if [ "$PERM" == "s" -a "$OWNER" == "root" ];then echo "This script cannot be run as SUID root" exit 1 fi } # Function that will get and scrub user input to make it safer to use. function scrub_input { # Grab the user's input read USR_INPUT # Remove all characters that aren't alphanumeric or newline USR_INPUT=$(echo "$USR_INPUT" | tr -cd '[[:alnum:]n]') } # Function that sets certain environment variables to known values function clear_vars { # Save the old variables so that they can be restored OLD_IFS="$IFS" OLD_PATH="$PATH" # Set the variables to known safer values IFS=$' tn' #Set IFS to include whitespace characters PATH='/bin:/usr/bin' #Assumed safe paths } # Function that restores environment variables to what the were at the # start of the script. function restore_vars { IFS="$OLD_IFS" PATH="$OLD_PATH" } # Function that attempts to run a command safely via the whereis command. function run_cmd { # Attempt to find the command with the whereis command CMD=$(whereis $1 | cut -d " " -f 2) # Check to make sure that the command was found if [ -f "$CMD" ];then eval "$CMD" else echo "The command $CMD was not found" exit 127 fi }

This script starts out with our new and improved function which creates relatively safe temp files for us (create_temp). This was taken directly from Listing 28 which we’ve already discussed. After that, there’s the drop_root function that encapsulates the functionality from Listing 21. We can just call this function at the beginning of the script to make sure that we’re not being run with sudo and that the script is not SUID root. This function merely warns the user and exits, it does not give up it’s root privileges like BASH does. The next function reads input from the user and then removes everything but alphanumeric characters and the newline character. This is taken from Listing 19. The next two functions deal with environment variables. The first (clear_vars) saves the old variable values for both IFS and PATH, and then sets new values for each. The restore_vars function uses the saved variable values to reset the variables back to their original condition. This is the same concept as what we talked about in Listing 3 enclosed in functions. The last function (run_cmd) is similar to Listing 4, but I’ve expanded it a little bit to check if a file with the name of the command exists or not before trying to run it. If the command exists, it is run via the eval command. If the command does not exist, we warn the user and exit.

Listing 30 shows a simple script where I implement the collection of security specific functions in Listing 29.

Listing 30

#!/bin/bash - # File: security_src_test.sh # Script to test the sourcable script security_src.sh # Function to clean up after ourselves function clean_up { # Step through and delete all of the temp files for TMP_FILE in "${TEMPFILES[@]}" do # Make sure that the tempfile exists if [ -e "$TMP_FILE" ]; then echo "Temp file: $TMP_FILE" rm $TMP_FILE fi done # Reset the variables to their original values restore_vars } # Source the script that holds the security functions . security_src.sh # Make sure that we delete the temp files when we exit trap 'clean_up' EXIT # Array to hold the temporary files TEMPFILES=( ) # Variable to hold the user's input USR_INPUT="" # Make sure that we're not running with root privileges drop_root # Make sure that we have safe variables to work with clear_vars # Call the function that will create a temp file for us create_temp # Check to make sure that the temp file was created echo "${TEMPFILES[0]}" # Let the user know that input is expected printf "Please enter your input: " # Get and scrub the user input scrub_input # Test the user input echo $USR_INPUT # Try to safely run a command that exists run_cmd ls > /dev/null # Try to safely run a command that does not exist run_cmd foo

At the very top of the script I create a clean_up function that handles the removal of any temporary files, and calls the sourced function that restores the IFS and PATH variables to their original values. This function is used in the trap statement so that it will be called whenever the script exits. Just above the trap statement is where the script is sourced (security_src.sh) that gives us access to the security related functions. Continuing on down the script you see that I’ve created a couple of variables to hold the temporary file names and the user input. The names of these variables are from the sourced script. The sourced function drop_root ensures that the script is not being run with root privileges, and then clear_vars is called to make sure that IFS and PATH are safer to use. After that I call the create_temp function to set up a temporary file for me, and then immediately echo the name/path of the file by accessing the first element of the TEMPFILES array (echo "${TEMPFILES[0]}").

I prompt the user for input with an echo statement next, but instead of putting the read command directly in my script I call the scrub_input function and let it handle the task of getting the input from the user. When I ran the script I tried inputting several symbols that should not be allowed in the user input, and upon hitting enter I saw via the echo $USR_INPUT statement that the symbols were properly scrubbed from the input. The last two things that I do is to try to run two commands via the run_cmd function. The first time that I use the function I run the ls command, which I would expect to succeed. I use the > /dev/null section of the line to suppress the output from the ls command so that the output of the script doesn’t get too cluttered. The second command that I try to run with the run_cmd function is foo. I would not expect this command to be found, and have added it to show what the function does. Listing 31 shows the output that I get when I run the script in Listing 30.

Listing 31

$ ./security_src_test.sh /home/jwright/tmp/mEAPJhqgyb Please enter your input: ?blog blog The command foo: was not found Temp file: /home/jwright/tmp/mEAPJhqgyb

When I check the /home/jwright/tmp folder for the temporary file, I see that it was properly deleted by the script. I also see that the ls command was found since there is no error, but the foo command was not. This is exactly what was expected. The example script in Listing 30 is not a real world script by any means, but works to show you how you would use the sourced script, and what order you might want to call the sourced functions in. As always, I welcome any input on corrections, additions, and tweaks that you think should be added to these scripts or any scripts in this post. Tell me what you think in the comments section.

Troubleshooting

If you get any capital letters in the symbolic permission string for a file, it means that something is wrong. Usually if you get a capital “S” in the string, it means that you need to set execute rights for the owner or the group. A capital “T” means that you set the sticky bit without setting the execute permission for other/world on the file or directory.

Conclusion

As I stated when we started this post, I haven’t been able to cover every aspect shell script security, and for the most part I avoided the issue of system security as that’s an even larger (but related) subject. It’s simply been my hope that I’ve given you a good starting point to plug some of the common security holes in your own scripts. Using this as a starting point, have a look at the Resources section for more information, and make sure to take opportunities to continue your learning on script, program, and system security whenever they arise.

Resources

Books

Links

  1. Purdue University’s Center for Education and Research in Information Assurance and Security
  2. Ubuntu Forums Announcement A Few Malicious Commands To Avoid In Forums/Posts/Lists
  3. LinuxSecurity.com Article On The shc Utility That Encrypts Shell Scripts
  4. shc Utility Homepage
  5. Linux Journal Article On Security Concerns When Using shc
  6. 7 Tips On Script Security By James Turnbull (requires registration)
  7. TLDP Advanced Bash-Scripting Guide: Chapter 35 – Miscellany
  8. Mac OS X Article On Shell Script Security That Gives Examples Of Attacks
  9. Article From faq.org On SUID Shell Scripts
  10. Practical Unix & Internet Security – Chapter 5 – Section 5.5 (SUID)
  11. Practical Unix & Internet Security – Chapter 23 – Writing Secure SUID and Network Programs
  12. Help Net Security Article On Unix Shell Scripting Malware
  13. More SUID Vulnerability Information
  14. Short Article On Linuxtopia About THe Dangers Of Running Untrusted Shell Scripts
  15. etutorials.org Article On Useful Shell Utilities For Scripts
  16. Examples Of Risky Scripts To Use With sudo
  17. Very Good LinuxSecurity.com Article On Creating Safe Temporary Files
  18. IBM developerWorks Article: Secure programmer: Developing secure programs
  19. IBM developerWorks Article: Secure programmer: Validating input
  20. IBM developerWorks Article: Secure programmer: Keep an eye on inputs
  21. Article on SUID, SGID, and Stick Bits In Linux And Unix

Comments (4)

  1. [...] This post was mentioned on Twitter by Yee Hon Choong, Kevin Kent, domacitutoriali.com, Joe Hermando, Innovations Tech and others. Innovations Tech said: The latest Innovations blog post is an introduction to some shell script security issues: http://www.innovationsts.com/blog/?p=2363 [...]

  2. [...] Writing Better Shell Scripts – Part 3 [...]

  3. nmset

    2010/09/25 at 5:18 PM

    A ‘super cracker’ can also blank a whole system while copmpletely ignoring user scripts. Once such a guy is in, the system is naked as in the sun. Of course, it’a good practice to write code that can defend against ‘would be crackers’.

  4. bash_script_writer

    2010/09/25 at 10:15 PM

    This is so AWESOME!!!!! Thank you for writing this! I am learning bash scripting now. This will help me tremendously.

The comments are now closed.