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.
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?
Matthew Miller
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
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.
Matthew Miller
Are you using PHP via the CGI interface, not mod_php?
Michal Ambroz
For the mod_php to be vulnerable you need to export some user input to the shell variables:
This is my example of vulnerable php code:
This vulnerable code can be exploited by setting the user agent to something nasty like:
curl –user-agent ‘() { ignored;} ; /usr/bin/id ;’ http://example.com/serverinfo.php
Michal Ambroz
Michal Ambroz
php code is not welcome here so I pasted to you git.
The function for exporting the environment variables is putenv.
Best regards
Michal Ambroz
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.
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?
Stephen Gallagher
Mary Biggs: That’s probably either a bug in yumex or an incompletely-synced mirror. Sometimes if a mirror doesn’t sync properly, you end up with multilib bugs where it tries to install a huge 32-bit runtime on what should be a 64-bit system. I’d try just downloading the update directly from http://koji.fedoraproject.org/koji/buildinfo?buildID=581023 (select the correct CPU architecture) and installing it with yum.
Matthew Miller
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!
Rodney Wade
Best explanation I’ve seen. Ta muchly. (If this gets posted twice then ‘Oops and sorry!)
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 ?
Matthew Miller
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.
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.
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”)
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.
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
Matthew Miller
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
Ryan Lerch
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