56

A recent vulnerability, CVE-2014-6271, in how Bash interprets environment variables was disclosed. The exploit relies on Bash parsing some environment variable declarations as function definitions, but then continuing to execute code following the definition:

$ x='() { echo i do nothing; }; echo vulnerable' bash -c ':'
vulnerable

But I don't get it. There's nothing I've been able to find in the Bash manual about interpreting environment variables as functions at all (except for inheriting functions, which is different). Indeed, a proper named function definition is just treated as a value:

$ x='y() { :; }' bash -c 'echo $x'
y() { :; }

But a corrupt one prints nothing:

$ x='() { :; }' bash -c 'echo $x'

$ # Nothing but newline

The corrupt function is unnamed, and so I can't just call it. Is this vulnerability a pure implementation bug, or is there an intended feature here, that I just can't see?

Update

Per Barmar's comment, I hypothesized the name of the function was the parameter name:

$ n='() { echo wat; }' bash -c 'n'
wat

Which I could swear I tried before, but I guess I didn't try hard enough. It's repeatable now. Here's a little more testing:

$ env n='() { echo wat; }; echo vuln' bash -c 'n'
vuln
wat
$ env n='() { echo wat; }; echo $1' bash -c 'n 2' 3 -- 4

wat

…so apparently the args are not set at the time the exploit executes.

Anyway, the basic answer to my question is, yes, this is how Bash implements inherited functions.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
kojiro
  • 74,557
  • 19
  • 143
  • 201
  • 3
    How do you think inheriting functions works in the first place? The only type of inheritance available between processes is the environment. So exported functions have to be encoded in the environment somehow. – Barmar Sep 24 '14 at 16:50
  • the example code in the link you included starts with `env`, as in `env x='...`. Not sure how this makes a difference either. FYI `ksh` does not emit any output from these tests after changing references from `bash -c` to `ksh -c`. – shellter Sep 24 '14 at 16:55
  • The lastet bash 4.3.25-1 closed the execution of the command after the strange function definition, but the function is still get defined... – clt60 Sep 24 '14 at 19:21
  • 2
    There is a new vulnerability coming down the pipe as well **https://access.redhat.com/security/cve/CVE-2014-7169** – David C. Rankin Sep 25 '14 at 08:11
  • Defining a function this way is an intended feature; the bug is the extra code following the function definition being executed as well. – chepner Sep 25 '14 at 15:32
  • @chepner: To get this straight, can we still have this: `export ls='(){ /bin/ls $@; echo "Caught you";}'`? – cdarke Sep 25 '14 at 16:47
  • Almost. The string must begin *explicitly* with `() {` (note the space). Otherwise, yes, `bash` will turn that into a function called `ls` if it sees that string in its environment on startup. The closing brace, it turns out, is somewhat optional. – chepner Sep 25 '14 at 16:51
  • @Barmar in my defense, I actually didn't know what _inherit_ meant. Consider that you can set x=3 and access it in a subshell: `$(echo $x)`, but that's subtly different from exporting `x`. Until this bug came to light I didn't even know you could export functions. – kojiro Sep 29 '14 at 15:37
  • 1
    See [What does env x='() { :;}; command' bash do and why is it insecure?](http://unix.stackexchange.com/q/157329) – Martin Schröder Sep 30 '14 at 22:12

4 Answers4

49

This seems like an implementation bug.

Apparently, the way exported functions work in bash is that they use specially-formatted environment variables. If you export a function:

f() { ... }

it defines an environment variable like:

f='() { ... }'

What's probably happening is that when the new shell sees an environment variable whose value begins with (), it prepends the variable name and executes the resulting string. The bug is that this includes executing anything after the function definition as well.

The fix described is apparently to parse the result to see if it's a valid function definition. If not, it prints the warning about the invalid function definition attempt.

This article confirms my explanation of the cause of the bug. It also goes into a little more detail about how the fix resolves it: not only do they parse the values more carefully, but variables that are used to pass exported functions follow a special naming convention. This naming convention is different from that used for the environment variables created for CGI scripts, so an HTTP client should never be able to get its foot into this door.

Barmar
  • 741,623
  • 53
  • 500
  • 612
  • How come environment variables let you execute functions but not bash commands directly. – AbbasFaisal Oct 17 '14 at 14:28
  • It's not "letting" you do anything. Bash uses these specially-formatted environment variables as the way to inherit functions. If it's not formatted like this, there's no reason to execute it, it's just plain data. – Barmar Oct 17 '14 at 15:38
16

The following:

x='() { echo I do nothing; }; echo vulnerable' bash -c 'typeset -f'

prints

vulnerable
x () 
{ 
    echo I do nothing
}
declare -fx x

seems, than Bash, after having parsed the x=..., discovered it as a function, exported it, saw the declare -fx x and allowed the execution of the command after the declaration.

echo vulnerable

x='() { x; }; echo vulnerable' bash -c 'typeset -f'

prints:

vulnerable
x () 
{ 
    echo I do nothing
}

and running the x

x='() { x; }; echo Vulnerable' bash -c 'x'

prints

Vulnerable
Segmentation fault: 11

segfaults - infinite recursive calls

It doesn't overrides already defined function

$ x() { echo Something; }
$ declare -fx x
$ x='() { x; }; echo Vulnerable' bash -c 'typeset -f'

prints:

x () 
{ 
    echo Something
}
declare -fx x

e.g. the x remains the previously (correctly) defined function.

For the Bash 4.3.25(1)-release the vulnerability is closed, so

x='() { echo I do nothing; }; echo Vulnerable' bash -c ':'

prints

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

but - what is strange (at least for me)

x='() { x; };' bash -c 'typeset -f'

STILL PRINTS

x () 
{ 
    x
}
declare -fx x

and the

x='() { x; };' bash -c 'x'

segmentation faults too, so it STILL accept the strange function definition...

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
clt60
  • 62,119
  • 17
  • 107
  • 194
  • Your last test, showing `x` not being overridden, actually looks like a bug as well. I would expect the command environment override to take precedence; it does, at least, for "normal" environment variables. Consider `declare -x foo=5; bash -c 'echo $foo'; foo=9 bash -c 'echo $foo'`. The first outputs 5; the second outputs 9. – chepner Sep 24 '14 at 18:56
  • RE: update 2, that's the function export working as expected. The string assigned to `x` is a function definition and nothing more, so it is exported. – chepner Sep 24 '14 at 19:26
  • 2
    You can't define a function that way, at least not explicitly. Only an instance of `bash` that inherits such a value will "instantiate" it as an executable function. That is, with `export foo='() { echo 5; }'`, `foo` will remain a normal string parameter in the current shell, but a child `bash` will make it a function (`echo $foo` would print nothing, for example, because there is no parameter named `foo` in the child, just a function). – chepner Sep 24 '14 at 19:33
  • From what I can tell, this is *exactly* what `bash` did: it evaluated any string in the environment whose *prefix* was a valid function definition, without regard for whatever trailing code was evaluated along with it. – chepner Sep 24 '14 at 19:56
  • the vulnerability is NOT closed, the fix is incomplete and just make harder to use the vulnerability. CVE-2014-6271 has been closed, but CVE-2014-7169 has been opened. Another patch will follow. cheers – Lesto Sep 25 '14 at 09:04
13

I think it's worth looking at the Bash code itself. The patch gives a bit of insight as to the problem. In particular,

*** ../bash-4.3-patched/variables.c 2014-05-15 08:26:50.000000000 -0400
--- variables.c 2014-09-14 14:23:35.000000000 -0400
***************
*** 359,369 ****
      strcpy (temp_string + char_index + 1, string);

!     if (posixly_correct == 0 || legal_identifier (name))
!       parse_and_execute (temp_string, name, SEVAL_NONINT|SEVAL_NOHIST);
!
!     /* Ancient backwards compatibility.  Old versions of bash exported
!        functions like name()=() {...} */
!     if (name[char_index - 1] == ')' && name[char_index - 2] == '(')
!       name[char_index - 2] = '\0';

      if (temp_var = find_function (name))
--- 364,372 ----
      strcpy (temp_string + char_index + 1, string);

!     /* Don't import function names that are invalid identifiers from the
!        environment, though we still allow them to be defined as shell
!        variables. */
!     if (legal_identifier (name))
!       parse_and_execute (temp_string, name, SEVAL_NONINT|SEVAL_NOHIST|SEVAL_FUNCDEF|SEVAL_ONECMD);

      if (temp_var = find_function (name))

When Bash exports a function, it shows up as an environment variable, for example:

$ foo() { echo 'hello world'; }
$ export -f foo
$ cat /proc/self/environ | tr '\0' '\n' | grep -A1 foo
foo=() {  echo 'hello world'
}

When a new Bash process finds a function defined this way in its environment, it evalutes the code in the variable using parse_and_execute(). For normal, non-malicious code, executing it simply defines the function in Bash and moves on. However, because it's passed to a generic execution function, Bash will correctly parse and execute additional code defined in that variable after the function definition.

You can see that in the new code, a flag called SEVAL_ONECMD has been added that tells Bash to only evaluate the first command (that is, the function definition) and SEVAL_FUNCDEF to only allow functio0n definitions.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
FatalError
  • 52,695
  • 14
  • 99
  • 116
0

In regard to your question about documentation, notice here in the commandline documentation for the env command, that a study of the syntax shows that env is working as documented.

  • There are, optionally, 4 possible options
  • An optional hyphen as a synonym for -i (for backward compatibility I assume)
  • Zero or more NAME=VALUE pairs. These are the variable assignment(s) which could include function definitions.
  • Note that no semicolon (;) is required between or following the assignments.
  • The last argument(s) can be a single command followed by its argument(s). It will run with whatever permissions have been granted to the login being used. Security is controlled by restricting permissions on the login user and setting permissions on user-accessible executables such that users other than the executable's owner can only read and execute the program, not alter it.
[ spot@LX03:~ ] env --help
Usage: env [OPTION]... [-] [NAME=VALUE]... [COMMAND [ARG]...]
Set each NAME to VALUE in the environment and run COMMAND.

  -i, --ignore-environment   start with an empty environment
  -u, --unset=NAME           remove variable from the environment
      --help     display this help and exit
      --version  output version information and exit

A mere - implies -i.  If no COMMAND, print the resulting environment.

Report env bugs to bug-coreutils@gnu.org
GNU coreutils home page: <http://www.gnu.org/software/coreutils/>
General help using GNU software: <http://www.gnu.org/gethelp/>
Report env translation bugs to <http://translationproject.org/team/>
DocSalvager
  • 2,156
  • 1
  • 21
  • 28