Strange Bash function export for the Shellshock bug

512 Views Asked by At

Why does the code

date
bash -c "date"
declare -x date='() { echo today; }' #aka export date='() { echo today; }'
date
bash -c "date"

print

Wed Sep 24 22:01:50 CEST 2014
Wed Sep 24 22:01:50 CEST 2014
Wed Sep 24 22:01:50 CEST 2014
today

?

Where (and why) does the evaluation

 date$date

happen and getting

 date() {echo today; }

Ad: @Etan Reisner

  1. I exporting a variable - not a function. Bash makes a function from it. The
export date='someting'

is still a variable regardless of its content. So, why is

export date='() { echo something; }' #Note, it is a variable, not function.

converted to an function?

  1. The mentioned security advisory talks about the execution of the command following the variable, for example,
x='() { echo I do nothing; }; echo vulnerable' bash -c ':'
                              ^^^^^^^^^^^^^^^
                              This is executed - this vunerability is CLOSED in version 4.3.25(1).

The command after the env-definition isn't executed in the latest Bash.

But the question remains - Why does Bash convert the exported variable to a function?

It is a bug ;) Full demo, based on @chepner's answer:

#Define three variables
foo='() { echo variable foo; }'    # ()crafted
qux='() { echo variable qux; }'    # ()crafted
bar='variable bar'                 # Normal
export foo qux bar                 # Export

#Define the same name functions (but not qux!)
foo() { echo "function foo"; }
bar() { echo "function bar"; }
declare -fx foo bar                 #Export

#printouts
echo "current shell foo variable:=$foo="
echo "current shell foo function:=$(foo)="
echo "current shell bar variable:=$bar="
echo "current shell bar function:=$(bar)="
echo "current shell qux variable:=$qux="
echo "current shell qux function:=$(qux)="

#subshell
bash -c 'echo subshell foo variable:=$foo='
bash -c 'echo subshell foo command :=$(foo)='
bash -c 'echo subshell bar variable:=$bar='
bash -c 'echo subshell bar command :=$(bar)='
bash -c 'echo subshell qux variable:=$qux='
bash -c 'echo subshell qux command :=$(qux)='

prints

current shell foo variable:=() { echo variable foo; }=
current shell foo function:=function foo=
current shell bar variable:=variable bar=
current shell bar function:=function bar=
current shell qux variable:=() { echo variable qux; }=
tt: line 20: qux: command not found
current shell qux function:==
subshell foo variable:==                   #<-- LOST the exported foo variable
subshell foo command :=function foo=
subshell bar variable:=variable bar=
subshell bar command :=function bar=
subshell qux variable:==                   #<-- And the variable qux got converted to
subshell qux command :=variable qux=       #<-- function qux in the subshell (!!!).

Avoiding the long comments, here is code from the Bash sources:

 if (privmode == 0 && read_but_dont_execute == 0 && STREQN ("() {", string, 4))
                                                           ^^^^^^^^ THE PROBLEM
    {
      string_length = strlen (string);
      temp_string = (char *)xmalloc (3 + string_length + char_index);

      strcpy (temp_string, name);
      temp_string[char_index] = ' ';
      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()=() {...} */

The "ancient" (seems) was better... :)

      if (name[char_index - 1] == ')' && name[char_index - 2] == '(')
        name[char_index - 2] = '\0';
3

There are 3 best solutions below

1
chepner On BEST ANSWER

The key point to remember is that

foo='() { echo 5; }'

only defines a string parameter with a string that looks a lot like a function. It's still a regular string:

$ echo $foo
() { echo 5; }

And not a function:

$ foo
bash: foo: command not found

Once foo is marked for export,

$ export foo

any child Bash will see the following string in its environment:

foo=() { echo 5; }

Normally, such strings become shell variables, using the part preceding the = as the name and the part following the value. However, Bash treats such strings specially by defining a function instead:

$ echo $foo

$ foo
5

You can see that the environment itself is not changed by examining it with something other than Bash:

$ perl -e 'print $ENV{foo}\n"'
() { echo 5
}

(The parent Bash replaces the semicolon with a newline when creating the child's environment, apparently). It's only the child Bash that creates a function instead of a shell variable from such a string.

The fact that foo could be both a parameter and a function within the same shell;

$ foo=5
$ foo () { echo 9; }
$ echo $foo
5
$ foo
9

explains why -f is needed with export. export foo would cause the string foo=5 to be added to the environment of a child; export -f foo is used to add the string foo=() { echo 9; }.

2
Etan Reisner On

You are essentially manually exporting a function with the name date. (Since that is the format that bash uses internally to export functions. Which is suggested by Barmar in his answer. This mechanism is mentioned here at the very least.)

Then when you run bash it sees that exported function and uses it when you tell it to run date.

Is the question then where is that mechanism specified? My guess is it isn't since it is an internal detail.

This should show the merging of the behaviours if that helps anything.

$ bar() { echo automatic; }; export -f bar
$ declare -x foo='() { echo manual; }'
$ declare -p foo bar
declare -x foo="() { echo manual; }"
-bash: declare: bar: not found
$ type foo bar
-bash: type: foo: not found
bar is a function
bar ()
{
    echo automatic
}
$ bash -c 'type foo bar'
foo is a function
foo ()
{
    echo manual
}
bar is a function
bar ()
{
    echo automatic
}
1
David C. Rankin On

The answer to your question comes directly from man bash:

The export and declare -x commands allow parameters and functions to be added to and deleted from the environment. If the value of a parameter in the environment is modified, the new value becomes part of the environment, replacing the old.

Thus

declare -x date='() { echo today; }'

replaces date in the environment. The next immediate call to date gives date as it exists in the script (which is unchanged). The call to bash -c "date" creates a new shell and executes date as defined by declare -x.