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"
vulnerable
appears in the output. The main problem is that bash is parsing and executing the code after the function definition as well. See the/bin/id
part of seclists.org/oss-sec/2014/q3/650 for another example.