249

There is apparently a vulnerability (CVE-2014-6271) in bash: Bash specially crafted environment variables code injection attack

I am trying to figure out what is happening, but I'm not entirely sure I understand it. How can the echo be executed as it is in single quotes?

$ env x='() { :;}; echo vulnerable' bash -c "echo this is a test"
vulnerable
this is a test

EDIT 1: A patched system looks like this:

$ env x='() { :;}; echo vulnerable' bash -c "echo this is a test"
bash: warning: x: ignoring function definition attempt
bash: error importing function definition for `x'
this is a test

EDIT 2: There is a related vulnerability / patch: CVE-2014-7169 which uses a slightly different test:

$ env 'x=() { :;}; echo vulnerable' 'BASH_FUNC_x()=() { :;}; echo vulnerable' bash -c "echo test"

unpatched output:

vulnerable
bash: BASH_FUNC_x(): line 0: syntax error near unexpected token `)'
bash: BASH_FUNC_x(): line 0: `BASH_FUNC_x() () { :;}; echo vulnerable'
bash: error importing function definition for `BASH_FUNC_x'
test

partially (early version) patched output:

bash: warning: x: ignoring function definition attempt
bash: error importing function definition for `x'
bash: error importing function definition for `BASH_FUNC_x()'
test

patched output up to and including CVE-2014-7169:

bash: warning: x: ignoring function definition attempt
bash: error importing function definition for `BASH_FUNC_x'
test

EDIT 3: story continues with:

8
  • It's not the echo that gets executed. its the function definition of x. If the function defined in x does some sneaky underhanded work, there is no way bash can check the return value if function x were real. Notice the function is empty in the test code. An unchecked return value can lead to script injection. Script injection leads to privilege escalation and privilege escalation leads to root access. The patch disables the creation of x as a function
    – eyoung100
    Commented Sep 24, 2014 at 20:25
  • 26
    eyoung100, no the echo is getting executed. You can see it's getting executed because the word 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.
    – Mikel
    Commented Sep 25, 2014 at 1:42
  • 4
    Just a quick side comment. Red Hat have advised that the patch that has been released is only a partial patch and leaves systems still at risk.
    – Peter
    Commented Sep 25, 2014 at 13:49
  • 2
    @eyoung100 the difference is that the code inside the function only executes when the environment variable is explicitly called. The code after the function definition executes every time a new Bash process starts. Commented Sep 26, 2014 at 17:42
  • 1
    See stackoverflow.com/questions/26022248/… for additional details
    – Barmar
    Commented Oct 1, 2014 at 19:24

5 Answers 5

215

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.

9
  • 3
    Could this affect any other Bash-like shell then, like Zsh? Commented Sep 24, 2014 at 22:52
  • 3
    @user815423426 No, zsh doesn't have this feature. Ksh has it but implemented differently, I think functions can only be transmitted in very narrow circumstances, only if the shell forks, not through the environment. Commented Sep 24, 2014 at 23:19
  • 20
    @user815423426 rc is the other shell that passes functions in the environment, but its with variable with names prefixed with "fn_" and they are only interpreted when invoked. Commented Sep 24, 2014 at 23:48
  • 19
    @StéphaneChazelas - thanks for reporting the bug. Commented Sep 25, 2014 at 4:55
  • 14
    @gnclmorais You mean you run export bar='() { echo "bar" ; }'; zsh -c bar and it displays bar rather than zsh:1: command not found: bar? Are you sure you aren't confusing the shell you're invoking with the shell that you're using to set up the test? Commented Sep 25, 2014 at 13:06
85

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.

6
  • 18
    While the accepted answer does actually say this if you read it carefully, I found this answer even clearer and more helpful in understanding that it is the evaluation of the definition (rather than executing the function itself) that is the problem.
    – natevw
    Commented Sep 25, 2014 at 18:21
  • 1
    why does this example has export command while the others had env? i was thinking env is being used to define the environmental variables that would be called when another bash shell is launched. then how is this working with export
    – Haris
    Commented Sep 25, 2014 at 19:12
  • Up until this moment there has not been an accepted answer. I'll probably wait for another couple days before accepting one. The downside of this answer is that it doesn't break down the original command, nor discusses how to get from the original command in the question to the commands in this answer, showing they are identical. Apart from that it is a good explanation.
    – jippie
    Commented Sep 25, 2014 at 19:31
  • @ralph - both env and export export environment definitions so that they are available in a subshell. The problem is actually in how these exported definitions are imported into a subshell's environment, and specifically in the mechanism that imports function definitions.
    – sdenham
    Commented Sep 26, 2014 at 14:46
  • 1
    @ralph - env runs a command with some options and environment variables set. Note that in the original question examples, env sets x to a string, and calls bash -c with a command to run. If you do env x='foo' vim, Vim will launch, and in there you can call out to its containing shell/environment with !echo $x, and it will print foo, but if you then exit and echo $x, it won't be defined, as it only existed while vim was running via the env command. The export command instead sets persistent values in the current environment so that a subshell run later will use them. Commented Sep 27, 2014 at 8:15
75

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:

  1. New bash is spawned
  2. An environment variable is ingested
  3. 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"
1
  • 12
    Nice explanation. This question is receiving a lot of views (probably not everybody being as proficient in bash as the others) and I believe no-one yet spent a couple words on what { :;}; actually says. That would be a nice addition to your answer in my opinion. May explain how you get from your example to the original command in the question?
    – jippie
    Commented Sep 26, 2014 at 5:27
20

If you can feed arbitrary environment variables to a program, you can cause it to do just about anything by having it load libraries of your choosing. In most cases this is not considered a vulnerability in the program receiving those environment variables, but rather in the mechanism by which an outsider could feed in arbitrary environment variables.

However CVE-2014-6271 is different.

There is nothing wrong in having untrusted data in an environment variable. One just has to ensure it doesn't get put in any of those environment variables which can modify program behavior. Put a bit more abstract, for a particular invocation, you can create a whitelist of environment variable names, which are allowed to be specified directly by an outsider.

An example which has been put forward in the context of CVE-2014-6271 is scripts used for parsing logfiles. Those may have a very legitimate need for passing untrusted data around in environment variables. Of course the name for such an environment variable is chosen such that it doesn't have any adverse affects.

But here is what is bad about this particular bash vulnerability. It can be exploited through any variable name. If you create an environment variable called GET_REQUEST_TO_BE_PROCESSED_BY_MY_SCRIPT, you wouldn't expect any other program besides your own script to interpret the contents of that environment variable. But by exploiting this bash bug, every single environment variable becomes an attack vector.

Notice that this doesn't mean names of environment variables are expected to be secret. Knowing the names of the environment variables involved doesn't make an attack any easier.

If program1 calls program2 which in turn calls program3, then program1 could pass data to program3 through environment variables. Each program has a specific list of environment variables which it sets and a specific list which it acts upon. If you chose a name not recognized by program2, you can pass data from program1 to program3 without worrying about this having any adverse affects on program2.

An attacker knowing the exact names of variables exported by program1 and names of variables interpreted by program2 cannot exploit this knowledge to modify the behavior of 'program2` if there is no overlap between the set of names.

But this broke down if program2 was a bash script, because due to this bug bash would interpret every environment variable as code.

1
  • 1
    "every single environment variable becomes an attack vector" - that's the part that I was missing. Thanks. Commented Sep 27, 2014 at 1:17
9

It is explained in the article you linked ...

you can create environment variables with specially-crafted values before calling the bash shell. These variables can contain code, which gets executed as soon as the shell is invoked.

Which means the bash that is called with -c "echo this is a test" executes the the code in the single quotes when it is invoked.

Bash has functions, though in a somewhat limited implementation, and it is possible to put these bash functions into environment variables. This flaw is triggered when extra code is added to the end of these function definitions (inside the enivronment variable).

Means the code example you posted exploits the fact that the invoked bash does not stop evaluating this string after performing the assignment. A function assignment in this case.

The actually special thing about the code snippet you posted, as I understand it, is that by using putting a function definition before the code we want to execute, some security mechanisms can be circumvented.

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .