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: