Work the Shell: Writing Secure Shell Scripts

Don't expose your system with sloppy scripts! By Dave Taylor

Although a Linux desktop or server is less susceptible to viruses and malware than a typical Windows device, there isn't a device on the internet that isn't eventually attacked. The culprit might be the stereotypical nerd in a bedroom testing his or her hacker chops (think Matthew Broderick in War Games or Angelina Jolie in Hackers). Then again, it might be an organized military, criminal, terrorist or other funded entity creating massive botnets or stealing millions of credit cards via a dozen redirected attack vectors.

In any case, modern systems face threats that were unimaginable in the early days of UNIX development and even in the first few years of Linux as a hobbyist reimplementation of UNIX. Ah, back in the day, the great worry was about copyrighted code, and so useful tools constantly were being re-implemented from scratch to get away from the AT&T Bell Labs licenses and so forth.

I have personal experience with this too. I rewrote the Hunt the Wumpus game wumpus from scratch for BSD 4.2 when the Berkeley crowd was trying to get away from AT&T UNIX legal hassles. I know, that's not the greatest claim to fame, but I also managed to cobble together a few other utilities in my time too.

Evolution worked backward with the internet, however. In real life, the lawless Wild West was gradually tamed, and law-abiding citizens replaced the outlaws and thugs of the 1850s and the Gold Rush. Online, it seems that there are more, smarter and better organized digital outlaws than ever.

Which is why one of the most important steps in learning how to write shell scripts is to learn how to ensure that your scripts are secure—even if it's just your own home computer and an old PC you've converted into a Linux-based media server with Plex or similar.

Let's have a look at some of the basics.

Know the Utilities You Invoke

Here's a classic trojan horse attack: an attacker drops a script called ls into /tmp, and it simply checks to see the userid that invoked it, then hands off its entire argument sequence to the real /bin/ls. If it recognizes userid = root, it makes a copy of /bin/sh into /tmp with an innocuous name, then changes its permission to setuid root.

This is super easy to write. Here's a version off the top of my head:


#!/bin/sh

if [ "$USER" = "root" ] ; then
  /bin/cp /bin/sh /tmp/.secretshell
  /bin/chown root /tmp/.secretshell
  /bin/chmod 4666 root /tmp/.secretshell
fi

exec /bin/ls $*

I hope you understand what just happened. This simple little script has created a shell that always grants its user root access to the Linux system. Yikes. Fancier versions would remove themselves once the root shell has been created, leaving no trace of how this transpired.

Because irony should be, well, ironic, I demonstrate above how to avoid this danger within the little trojan horse script. Never invoke programs by just their name; make sure you include their path. A script that has ls $HOME is begging for trouble, so fix it with /bin/ls $HOME instead.

An interesting additional place this risk can appear is in your PATH. Again, imagine if your PATH is set like this:


PATH=".:/bin:/usr/bin:$HOME/bin:/usr/local/bin"

95% of the time, that's no problem, and an invocation to ls or cp or even date will do just what you expect by failing to be found in the first directory in your PATH and so cascading down to /bin where the legit binary is stored. But what happens if you happen to be in /tmp when you invoke the command? Without realizing it, you actually invoke the trojan version and have created that root shell again (if you were root at the time, of course).

Solution: either never have dot as a directory in your PATH (my recommendation) or have it as the last entry in the chain, not the first.

Don't Store Passwords in Scripts

I admit, I'm not the best at this because I have some aliases that actually push passwords into my copy/paste buffer and then invoke an ssh or sftp connection to a remote computer. It's a dumb solution because it rather inevitably means that I have a shell script—or aliases file, in this case—that has lines like this:


PASSWORD="froBOZ69"

Or like this:


alias synth='echo secretpw | pbcopy; sftp adt@wsynth.net'

Solution: just don't do this. If you must, well, then at least don't use such a ridiculously obvious (and easy to identify during a scan) variable name. But really, find an alternative utility to accomplish the job, it's not worth the security risk.

Beware of Invoking Anything the User Inputs

This is a subtle one, but there's a big security risk in a simple sequence like the following:


echo -n "What file do you seek? "
read name
ls -l $name

What happens if the user enters something malicious like this as the filename:


. ; /bin/rm -Rf /

Quite dire consequences, whether the script is being invoked as root or just a regular user. Bash has some level of protection if you quote the argument, so the earlier sequence would be protected with the change to:


/bin/ls -l "$name"

But, if it's invoked as eval /bin/ls -l "$name", that doesn't apply. Oh, and there's also the infamous backticks. Imagine user input like this:


. `/bin/rm -Rf /`

This is another risky one because the `` pair is the lazy shortcut to $( ) and invokes a subshell when it's encountered on the command line. The invocation of ls will be performed by just such a shell too.

To fix this danger, if you have reason to believe that your script might have malicious users, scan and scrub your input. Easy solution: error out if you encounter a character that's not alphanumeric or a small set of safe punctuation marks.

Don't Use Shell Scripts for CGI Scripts

Running a Linux web server and learning about CGI scripts? It's not only tempting to use shell scripts for basic CGI functions, it's quick and easy too. Here's a script that tells you the load on the server:


#!/bin/sh

echo "Content-type: text/html"; echo ""
echo "Uptime on the Server:<pre>"
uptime -a
echo "</pre>"

This'll work fine once you get the permissions set properly. It's not too dangerous, but what if you wanted to do something similar as a home-grown search system for your site? Again, any time you have a script that runs with input from an unknown user, you've added some major risk factors.

In this case, the solution is don't do it. Use a compiled program instead that can implement safe and proper security or just use a third-party search system like Google Custom Search Engine to be maximally safe.

Be Smart about Your Coding

There are lots of reasons to love programming in the Linux shell, not the least of which is that it's fast and easy to prototype. But if you're really going to create a safe computing environment, you need to focus on security as you go, not realize after the fact that you did something dumb.

Some good online resources cover these topics in more depth. Check out the Shell Style Guide on GitHub to get started. Also, Apple has a document on shell script security that's also well worth a read.

Be careful out there! A little extra time making sure your scripts are safe from major known risks is time well spent.

About the Author

Dave Taylor has been hacking shell scripts on UNIX and Linux systems for a really long time. He's the author of Learning Unix for Mac OS X and Wicked Cool Shell Scripts. You can find him on Twitter as @DaveTaylor, and you can reach him through his tech Q&A site: Ask Dave Taylor.

Dave Taylor