Shellshock: How does it actually work?

By now, you’ve probably seen this magic incantation, or variations, sent all around as a quick test for vulnerability to CVE-2014-6271, known as “Shellshock”, because in this post-Heartbleed world, apparently all security flaws will have cute over-dramatic names.

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

This will print “OOPS” on a vulnerable system, but exit silently if bash has been patched.

And you’ve probably heard that it has something to do with environment variables. But, why is code in environment variables getting executed? Well, it’s not supposed to be — but, because of a feature which I’m tempted to call a bit too clever for its own good, there’s some room for a flaw. Bash is what you see as a terminal prompt, but it also is a scripting language, and has the ability to define functions. You do that like this:

$ yayfedora() { echo "Fedora is awesome."; }

and then you have a new command. Keep in mind that the “echo” here isn’t actually run yet; it’s just saved as what will happen when we run our new command. This will be important in a minute!

$ yayfedora 
Fedora is awesome.

Useful! But, let’s say, for some reason, I need to execute a new instance of bash, as a subprocess, and want to run my awesome new command under that. The statement bash -c somecommand does exactly this: runs the given command in a new shell:

$ bash -c yayfedora
bash: yayfedora: command not found

Ooh. Sad. The child didn’t inherit the function definition. But, it does inherit the environment — a collection of key-value pairs that have been exported from the shell. (This is a whole ‘nuther concept; if you’re not familiar with this, trust me for now.) And, it turns out, bash can export functions as well. So:

$ export -f yayfedora
$ bash -c yayfedora
Fedora is awesome.

Which is all well and good — except that the mechanism by which this is accomplished is sorta dodgy. Basically, since there is no Linux/Unix magic for doing functions in environment variables, the export function actually just creates a regular environment variable containing the function definition. Then, when the second shell reads the “incoming” environment and encounters a variable with contents that look like a function, it evaluates it.

In theory, this is perfectly safe, because, remember, defining a function doesn’t actually execute it. Except — and this is why we’re here — there was a bug in the code where the evaluation didn’t stop when the end of the function definition was reached. It just kepts going.

That would never happen when the function stored in an environment variable is made legitimately, with export -f. But, why be legit? An attacker can just make up any old environment variable, and if it looks like a function, new bash shells will think it is!

So, in our first example:

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

The “env” command runs a command with a given variable set. In this case, we’re setting “x” to something that looks like a function. The function is just a single “:”, which is actually a simple command which is defined as doing nothing. But then, after the semi-colon which signals the end of the function definition, there’s an echo command. That’s not supposed to be there, but there’s nothing stopping us from doing it.

Then, the command given to run with this new environment is a new bash shell, again with a “do nothing :” command, after which it will exit, completely harmlessly.

But — oops! When that new shell starts up and reads the environment, it gets to the “x” variable, and since it looks like a function, it evaluates it. The function definition is harmlessly loaded — and then our malicious payload is triggered too. So, if you run the above on a vulnerable system, you’ll get “OOPS” printed back at you. Or, an attacker could do a lot worse than just print things.

Our update does several things.

First, it fixes the flaw where the interpretation doesn’t stop at the end of the function definition (including some more sanity checks).

Second, it always prefixes the magical environment variables which hold exported functions with the string BASH_FUNC_ and suffixes them with (). That means you can’t just set any arbitrary environment variable — like those passed to a web server as part of the CGI interface! — to look like a function and attack via any bash subshell that comes up later.

And there’s quite a lot of other little cleanups in there too — security people at Fedora, at Red Hat, and around the world sure have been busy for the couple of days. Thanks to all of you for your hard work, and to Fedora’s awesome QA and Release Engineering teams, who sprung into action to make sure that these updates got to you quickly and safely.

Shell based off "Shell" - CC-BY 3.0 by Guillaume Kurkdjian -- http://thenounproject.com/term/shell/40512/

Shell based off “Shell” – CC-BY 3.0 by Guillaume Kurkdjian — http://thenounproject.com/term/shell/40512/

Fedora Project community

23 Comments

  1. Sev Vanta

    I’m still not clear on how this could ever get executed on a system, even one that is world-facing. I do not have servers with BASH running as a public service, so while this is obviously a security issue that I will patch, why is it “such” big news? there are flaws found every day. Is there something obvious and more dangerous here that I am missing?

    • Check out Red Hat’s knowledge base article on this — there’s a section on common configurations that might be vulnerable.

      In general, it’s scary because it’s an avenue for unvalidated data that programmers likely not have considered. Anything that gets executed is cleaned as a matter of habit by any sane program, but environment variables seem harmless — if you even think of them, because they’re supposed to just be sitting there as key=value pairs in memory.

      On a web server using CGI, environment variables which come from the client are part of the protocol, and if you’re using PHP and call some external program, bash may be involved even if you don’t expect it. There’s a lot of CGI out there, and the potential for this to spread as an escalating automated worm is real.

      For client machines, dhclient is probably the most concerning. Information from a DHCP server is put into environment variables in a way that’s kind of similar to CGI, and then configuration scripts are executed under bash. Even if the legitimate DHCP server on a network isn’t compromised, it’s easy for any other machine to just fake it and send hostile packets to yours. So, be careful at the coffee shop!

      • Alexander

        On a web server using CGI, environment variables which come from the client are part of the protocol, and if you’re using PHP and call some external program, bash may be involved even if you don’t expect it

        How?

        I’m trying to write a PHP script, which is supposed to be exploitable. You can find the source at https://gist.github.com/alexs77/4859c72a77e1b6eaf785

        But for the life of me, I cannot get it to be exploitable. Neither manually with a

        curl -A "() { :; }; /usr/bin/wget http://localhost/exploited" http://localhost/SS3.php

        Nor with exploit testers.

        I would love to see an exploitable PHP script! At best, in source code, so that I could put it on my servers, so that I’ve got something to test.

    • Stef

      This bug is very nasty because it does not require that you run Bash script as a service. It can be enough for the service to call a bash script directly or indirectly. For instance one could imagine a CGI script in PHP calling a Perl script that would itself call gunzip to decompress a file needed to perform the request. Bad luck, gunzip is actually a bash script around gzip -d. If the user has any control over an environment variable set by the PHP or the Perl script then its game-over.

  2. Mary Biggs

    I’m running Fedora 20. Today (using Yum Extender) I noticed an update for bash (to bash-4.2.48-2). When I select only this single update, Yum Extender lists hundreds of package dependency updates, and even some completely new package installs (totalling more than 800MB)!!!!

    Is this huge list real?….or is it a bug in Yum Extender. If not a bug, why the huge derived update?

  3. Note: if the test command exits with a warning followed by an error, you have the older, partial patch. Time to update again!

    • Observer

      Don’t you think that the above should be placed in the article right below this line.

      This will print “OOPS” on a vulnerable system, but exit silently if bash has been patched.

      Note: if the test command exits with a warning followed by an error, you have the older, partial patch. Time to update again!

  4. Rodney Wade

    Best explanation I’ve seen. Ta muchly. (If this gets posted twice then ‘Oops and sorry!)

  5. scott owens

    Sorry,
    Former C/C++ programmer here.

    This sounds/looks like the same issue what would exist if I opened a browser to my bank wire transfer page and then wandered around looking for someone to want to type stuff on that page.

    I’m a home/business user with .. 3 Macs, 1 Xubuntu, and 2 Windows boxes. Why would I even put myself or those computers in any possible situation where this would be a feasible attack vector ?
    Sure .. I -could- win the Golden Glove award … but it ain’t going to happen in any non-dreaming environment

    • pet

      By cracking your wlan or a windows host and sending you a fake dhcp package. Boom, pwnd.

      • scott owens

        uh, that is about as specific ( read full of hot air ) as .:
        build rocket ship, take dry food, land on mars . boom – colony on mars.

        a dhcp packet is NOT going to force my mac to execute an ENV call.

        wanna try again ?

        • This is a Fedora blog, not a Mac one. I don’t know how the DHCP client works on OS X, but it is actually the case that at least your Xubuntu box works this way.

  6. Doub

    The real problem is not the vulnerability, but the reason it came into existence. All these security people you praise act like firemen when we need structrural architects. Less muscle and more brains please.

  7. Damon Miller

    Minor typo/word error:

    Ooh. Sad. The child didn’t inherit the function definition. But, it does inherent the environment — a collection of key-value pairs that have been exported from the shell. (This is a whole ‘nuther concept; if you’re not familiar with this, trust me for now.) And, it turns out, bash can export functions as well. So:

    Should read:

    Ooh. Sad. The child didn’t inherit the function definition. But, it does inherit the environment — a collection of key-value pairs that have been exported from the shell. (This is a whole ‘nuther concept; if you’re not familiar with this, trust me for now.) And, it turns out, bash can export functions as well. So:

    (“inherent” instead of “inherit”)

  8. Cary lewis

    Great article. Very succinct. It is ironic that it contains a typo. It kind of makes you think about how many new bugs were introduced due to this rush patch.

  9. Jan

    Hello Matthew, Thank you for the explanation, I finally got what it is about. I had already updated bash to 4.2.47(1) after reading about shellshock (I run Fedora 20) and I will wait for the final release of the bash fix. I have a NAS and I could play your example there on bash version 3.2.39(1) (oops! of course).

    But i am still puzzled why there are two ways of exporting “functions”
    1. the “official” one via export -f x (and ‘type -t x’ returns ‘function’ )
    2. the “dodgy” one via an exported string variable such as: export x='() { :;};’

    Because like you said:
    “That would never happen when the function stored in an environment variable is made legitimately, with export -f. But, why be legit? An attacker can just make up any old environment variable, and if it looks like a function, new bash shells will think it is!”

    Why then Bash needs picking up any old (attacker) string that looks like a function and turns it into a “official” looking function in the subshell ? (‘env’ is using the 2nd method apparently, but i don’t think that is the reason).

    Cheers

    Jan

    • Why then Bash needs picking up any old (attacker) string that looks like a function and turns it into a “official” looking function in the subshell ? (‘env’ is using the 2nd method apparently, but i don’t think that is the reason).

      The dodgy method and the official method have the same result because they actually do the same thing. There isn’t any way in the environment to distinguish between variables and anything else — but bash does it anyway and uses a “this looks like a function!” test to distinguish. That’s why I describe it as “a bit too clever”. And it’s why the patch (and new upstream) use a prefix and suffix, so only specially-named variables are checked.

      • Jan

        Hello Matthew,

        Thanks for the explanation again. I made a mistake in thinking that only the official functions in the environment had to (re)evaluated and not the dodgy ones, but it is the other way around: the dodgy ones need to be evaluated ( if they have their special names). As far as the ‘official’ functions are concerned one could use the type builtin bash function to find out if a name refers to a function . (but that’s no use for finding out which dodgy ones have to be evaluated. :-)) For people like myself : endusers, who read this,I have added a listing which shows what I meant.

        [Jan@Gnoom ~]$ # “official” function
        [Jan@Gnoom ~]$ func1 () { echo Hello1

        }

        [Jan@Gnoom ~]$ # “dodgy” function in a string
        [Jan@Gnoom ~]$ func2='() { echo Hello2;}’

        [Jan@Gnoom ~]$ # show typeset of func1
        [Jan@Gnoom ~]$ type -t func1
        function
        [Jan@Gnoom ~]$ # show typeset of func2 (func2 has no type, return code 1)
        [Jan@Gnoom ~]$ type -t func2
        [Jan@Gnoom ~]$ echo $?
        1

        [Jan@Gnoom ~]$ # export func1 as a function
        [Jan@Gnoom ~]$ export -f func1

        [Jan@Gnoom ~]$ # try export func2 as a function
        [Jan@Gnoom ~]$ export -f func2
        bash: export: func2: not a function
        [Jan@Gnoom ~]$ # export func2 as/is
        [Jan@Gnoom ~]$ export func2

        [Jan@Gnoom ~]$ # start subshell **************************************
        [Jan@Gnoom ~]$ bash -i

        [Jan@Gnoom ~]$ # show func1 and func2 in env
        [Jan@Gnoom ~]$ env | tail
        DISPLAY=:0
        XDG_RUNTIME_DIR=/run/user/1000
        LC_TIME=nl_NL.utf8
        XAUTHORITY=/run/gdm/auth-for-Jan-DIMp7h/database
        COLORTERM=gnome-terminal
        func2=() { echo Hello2
        }
        func1=() { echo Hello1
        }
        _=/usr/bin/env

        [Jan@Gnoom ~]$ # show typeset of func1
        [Jan@Gnoom ~]$ type -t func1
        function
        [Jan@Gnoom ~]$ type -t func2
        function
        [Jan@Gnoom ~]$ # The end result is the same, run 1 and 2
        [Jan@Gnoom ~]$ func1
        Hello1
        [Jan@Gnoom ~]$ func2
        Hello2

        [Jan@Gnoom ~]$ # exit subshell **************************************
        [Jan@Gnoom ~]$ exit
        exit
        [Jan@Gnoom ~]$

        Thanks, Jan

  10. Great explaination! So good in fact, that your article was used (and slightly changed 🙂 ) to answer this question over at Ask Ubuntu.

    http://askubuntu.com/questions/529511/explanation-of-the-command-to-check-shellshock/529518

Comments are Closed

The opinions expressed on this website are those of each author, not of the author's employer or of Red Hat. Fedora Magazine aspires to publish all content under a Creative Commons license but may not be able to do so in all cases. You are responsible for ensuring that you have the necessary permission to reuse any work on this site. The Fedora logo is a trademark of Red Hat, Inc. Terms and Conditions