What does env x='() { :;}; command' bash do and why is it insecure?
bash stores exported function definitions as environment variables. Exported functions look like this:
$ foo() { bar; }
$ export -f foo
$ env | grep -A1 foo
foo=() { bar
}
That is, the environment variable foo
has the literal contents:
() { bar
}
When a new instance of bash launches, it looks for these specially crafted environment variables, and interprets them as function definitions. You can even write one yourself, and see that it still works:
$ export foo='() { echo "Inside function"; }'
$ bash -c 'foo'
Inside function
Unfortunately, the parsing of function definitions from strings (the environment variables) can have wider effects than intended. In unpatched versions, it also interprets arbitrary commands that occur after the termination of the function definition. This is due to insufficient constraints in the determination of acceptable function-like strings in the environment. For example:
$ export foo='() { echo "Inside function" ; }; echo "Executed echo"'
$ bash -c 'foo'
Executed echo
Inside function
Note that the echo outside the function definition has been unexpectedly executed during bash startup. The function definition is just a step to get the evaluation and exploit to happen, the function definition itself and the environment variable used are arbitrary. The shell looks at the environment variables, sees foo
, which looks like it meets the constraints it knows about what a function definition looks like, and it evaluates the line, unintentionally also executing the echo (which could be any command, malicious or not).
This is considered insecure because variables are not typically allowed or expected, by themselves, to directly cause the invocation of arbitrary code contained in them. Perhaps your program sets environment variables from untrusted user input. It would be highly unexpected that those environment variables could be manipulated in such a way that the user could run arbitrary commands without your explicit intent to do so using that environment variable for such a reason declared in the code.
Here is an example of a viable attack. You run a web server that runs a vulnerable shell, somewhere, as part of its lifetime. This web server passes environment variables to a bash script, for example, if you are using CGI, information about the HTTP request is often included as environment variables from the web server. For example, HTTP_USER_AGENT
might be set to the contents of your user agent. This means that if you spoof your user agent to be something like '() { :; }; echo foo', when that shell script runs, echo foo
will be executed. Again, echo foo
could be anything, malicious or not.
This may help to further demonstrate what is going on:
$ export dummy='() { echo "hi"; }; echo "pwned"'
$ bash
pwned
$
If you are running a vulnerable shell, then when you start a new subshell (here, simply by using the bash statement), you will see that the arbitrary code (echo "pwned"
) is immediately executed as part of its initiation. Apparently, the shell sees that the environment variable (dummy) contains a function definition, and evaluates the definition in order to define that function in its environment (note that it is not executing the function: that would print 'hi'.)
Unfortunately, it does not just evaluate the function definition, it evaluates the entire text of the environment variable's value, including the possibly malicious statement(s) that follow the function definition. Note that without the initial function definition, the environment variable would not be evaluated, it would merely be added to the environment as a text string. As Chris Down pointed out, this is a specific mechanism to implement the importing of exported shell functions.
We can see the function that has been defined in the new shell (and that it has been marked as exported there), and we can execute it. Furthermore, dummy has not been imported as a text variable:
$ declare -f
dummy ()
{
echo "hi"
}
declare -fx dummy
$ dummy
hi
$echo $dummy
$
Neither the creation of this function, nor anything it would do were it to be run, is part of the exploit - it is only the vehicle by which the exploit is executed. The point is that if an attacker can supply malicious code, preceded by a minimal and unimportant function definition, in a text string that gets put into an exported environment variable, then it will be executed when a subshell is started, which is a common event in many scripts. Furthermore, it will be executed with the privileges of the script.
I wrote this as a tutorial-style recasting of the excellent answer by Chris Down above.
In bash you can have shell variables like this
$ t="hi there"
$ echo $t
hi there
$
By default, these variables are not inherited by child processes.
$ bash
$ echo $t
$ exit
But if you mark them for export, bash will set a flag that means they will go into the environment of subprocesses (although the envp
parameter is not much seen, the main
in your C program has three parameters: main(int argc, char *argv[], char *envp[])
where that last array of pointers is an array of shell variables with their definitions).
So let's export t
as follows:
$ echo $t
hi there
$ export t
$ bash
$ echo $t
hi there
$ exit
Whereas above t
was undefined in the subshell, it now appears after we exported it (use export -n t
if you want to stop exporting it).
But functions in bash are a different animal. You declare them like this:
$ fn() { echo "test"; }
And now you can just invoke the function by calling it as if it were another shell command:
$ fn
test
$
Once again, if you spawn a subshell, our function is not exported:
$ bash
$ fn
fn: command not found
$ exit
We can export a function with export -f
:
$ export -f fn
$ bash
$ fn
test
$ exit
Here's the tricky part: an exported function like fn
is converted into an environment variable just like our export of the shell variable t
was above. This doesn't happen when fn
was a local variable, but after export we can see it as a shell variable. However, you can also have a regular (ie, non function) shell variable with the same name. bash distinguishes based on the contents of the variable:
$ echo $fn
$ # See, nothing was there
$ export fn=regular
$ echo $fn
regular
$
Now we can use env
to show all shell variables marked for export and both the regular fn
and the function fn
show up:
$ env
.
.
.
fn=regular
fn=() { echo "test"
}
$
A sub-shell will ingest both definitions: one as a regular variable and one as a function:
$ bash
$ echo $fn
regular
$ fn
test
$ exit
You can define fn
as we did above, or directly as a regular variable assignment:
$ fn='() { echo "direct" ; }'
Note this is a high unusual thing to do! Normally we would define the function fn
as we did above with fn() {...}
syntax. But since bash exports it through the environment, we can "short cut" directly to the regular definition above. Note that (counter to your intuition, perhaps) this does not result in a new function fn
available in the current shell. But if you spawn a **sub**shell, then it will.
Let's cancel export of the function fn
and leave the new regular fn
(as shown above) intact.
$ export -nf fn
Now the function fn
is no longer exported, but the regular variable fn
is, and it contains () { echo "direct" ; }
in it.
Now when a subshell sees a regular variable that begins with ()
it interprets the rest as a function definition. But this is only when a new shell begins. As we saw above, just defining a regular shell variable starting with ()
does not cause it to behave like a function. You have to start a subshell.
And now the "shellshock" bug:
As we just saw,when a new shell ingests the definition of a regular variable starting with ()
it interprets it as a function. However, if there is more given after the closing brace that defines the function, it executes whatever is there as well.
These are the requirements, once more:
- New bash is spawned
- An environment variable is ingested
- This environment variable starts with "()" and then contains a function body inside braces, and then has commands afterward
In this case, a vulnerable bash will execute the latter commands.
Example:
$ export ex='() { echo "function ex" ; }; echo "this is bad"; '
$ bash
this is bad
$ ex
function ex
$
The regular exported variable ex
was passed to the subshell which was interpreted as a function ex
but the trailing commands were executed (this is bad
) as the subshell spawned.
Explaining the slick one-line test
A popular one-liner for testing for the Shellshock vulnerability is the one cited in @jippie's question:
env x='() { :;}; echo vulnerable' bash -c "echo this is a test"
Here is a break-down: first the :
in bash is just a shorthand for true
. true
and :
both evaluate to (you guessed it) true, in bash:
$ if true; then echo yes; fi
yes
$ if :; then echo yes; fi
yes
$
Second, the env
command (also built into bash) prints the environment variables (as we saw above) but also can be used to run a single command with an exported variable (or variables) given to that command, and bash -c
runs a single command from its command-line:
$ bash -c 'echo hi'
hi
$ bash -c 'echo $t'
$ env t=exported bash -c 'echo $t'
exported
$
So sewing all of this stuff together, we can run bash as a command, give it some dummy thing to do (like bash -c echo this is a test
) and export a variable that starts with ()
so the subshell will interpret it as a function. If shellshock is present, it will also immediately execute any trailing commands in the subshell. Since the function we pass is irrelevant to us (but must parse!) we use the shortest valid function imaginable:
$ f() { :;}
$ f
$
The function f
here just executes the :
command, which returns true and exits. Now append to that some "evil" command and export a regular variable to a subshell and you win. Here is the one-liner again:
$ env x='() { :;}; echo vulnerable' bash -c "echo this is a test"
So x
is exported as a regular variable with a simple valid function with echo vulnerable
tacked on to the end. This is passed to bash, and bash interprets x
as a function (which we don't care about) then perhaps executes the echo vulnerable
if shellshock is present.
We could shorten the one-liner a little by removing the this is a test
message:
$ env x='() { :;}; echo vulnerable' bash -c :
This doesn't bother with this is a test
but runs the silent :
command yet again. (If you leave off the -c :
then you sit in the subshell and have to exit manually.) Perhaps the most user-friendly version would be this one:
$ env x='() { :;}; echo vulnerable' bash -c "echo If you see the word vulnerable above, you are vulnerable to shellshock"