What Was Shellshock? The Bug Bash Carried for Twenty-Five Years

Almost everyone in the industry ran the same line that week, on their own machines, to find out where they stood:

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

If the shell printed vulnerable before it printed this is a test, the machine was affected. It printed vulnerable on essentially everything.

Shellshock is filed almost everywhere as a web server vulnerability. It was not. It was a bug in bash, the default shell on nearly every Unix and Linux system and on every Mac, and it had been sitting in the source since 1989. The web server was only the most convenient way to reach it.

What was Shellshock?

CVE-2014-6271, disclosed 24 September 2014, was a flaw in how bash imported exported shell functions from its environment.

Bash can pass a function to a child process. It does this by putting the function body into an environment variable, and the child shell, on startup, parses that variable back into a function again. That is the whole feature, and it is obscure enough that most people who use bash daily have never used it deliberately.

The parser did not stop at the end of the function.

It was given a base CVSS v2 score of 10.0 — the maximum — and it earned it.

The bug, in one line

An environment variable whose value began with () { was treated as a function definition and evaluated. Bash parsed the definition, stored the function, and then kept reading. Anything after the closing brace was executed immediately, as a command, at shell startup — before bash had run a single line of whatever script it had actually been invoked to run.

So the payload is:

() { :; }; <anything you like>

The : is the shell's null command. The function is a deliberate no-op. It exists only to get the parser into the state where it will read and run whatever follows.

Nothing here requires authentication, a memory corruption primitive, or a race. It is arbitrary command execution as a direct consequence of a parser reading past the thing it was asked to parse.

Twenty-five years

The function-import behaviour dates to bash 1.03, in September 1989. Every version from then until 4.3 was affected.

Twenty-five years is the headline, and it deserves the weight, but the more useful observation is about who was carrying it. Bash has been maintained since 1990, essentially single-handedly, by Chet Ramey — for most of that time alongside a full-time job at Case Western Reserve University. One of the most widely deployed pieces of software ever written had, in practice, one maintainer.

Nobody decided that. It accumulated, in exactly the way OpenSSL's funding had accumulated, which the industry had discovered five months earlier with Heartbleed.

It was found by Stéphane Chazelas, a Unix specialist working in the UK, who reported it privately to Ramey on 12 September 2014. Twelve days of coordinated disclosure followed before it went public.

Why CGI made it catastrophic

A bug in a shell is only as dangerous as the paths that let a stranger put text into that shell's environment. In 2014 there were several, and one of them was everywhere.

Under CGI, a web server copies the incoming HTTP request headers into environment variables before invoking the script — User-Agent becomes HTTP_USER_AGENT, and so on. If the script is a shell script, or invokes one, bash starts up with those variables in its environment.

Which means the exploit was, in full:

User-Agent: () { :; }; /bin/cat /etc/passwd

A header. Sent to a normal URL. Running as whatever user the web server ran as. No payload delivery, no staging, nothing to detect beyond a slightly odd header in a log nobody reads.

The other reachable paths mattered less but are worth knowing, because they are the reason patching "the web servers" was not sufficient:

  • DHCP clients. A malicious DHCP server could return an option value that a client's shell hook would evaluate — which means joining a hostile wireless network was enough.
  • OpenSSH with ForceCommand. The restricted-command mechanism used to limit what a key could do passed the original command through the environment, so the restriction could be stepped around by an authenticated but limited user.
  • CUPS, and various mail and monitoring tools that shelled out with attacker-influenced environment variables.

The first patch was wrong

This is the part that made the week genuinely unpleasant for anyone doing the patching.

The initial fix went out with the disclosure on 24 September. Tavis Ormandy had a working bypass publicly within about a day, which became CVE-2014-7169. Organisations that had done everything right — patched immediately, on the day, across the estate — woke up to find they had to do it again.

Then it kept going. Michał Zalewski, pointing his fuzzer american fuzzy lop at the parser, turned up CVE-2014-6277 and CVE-2014-6278. Florian Weimer found CVE-2014-7186 and CVE-2014-7187. Six CVEs in about a week, all in the same corner of the same file.

The fix that actually held was Weimer's, and it is an architectural change rather than a parser correction: bash now only imports a function from a variable whose name carries a special prefix, so an ordinary environment variable can never be interpreted as a function definition no matter what its value looks like. The vulnerable behaviour was not made safe. It was made unreachable.

The half a billion machines number

The figure repeated at the time was that Shellshock affected around 500 million machines. It is worth being precise about what that counts, because it is not a count of exploitable systems.

It is roughly a count of installed bash: Linux servers, Macs, embedded devices, network appliances. Every one of those had the vulnerable code.

Being exploitable required something more — a path that puts attacker-controlled text into an environment variable and then starts bash. CGI was by far the largest such path, and CGI was already legacy technology in 2014. A great many of the affected machines had no reachable route to the bug at all.

The honest version is: hundreds of millions of machines contained the flaw, a much smaller and genuinely unknown number were exploitable from the internet, and the ones that were got found within hours because the exploit fits in a header and the whole IPv4 space can be scanned in an afternoon.

The embedded devices are the part that never got resolved. A router or camera running an old busybox-era system with a vulnerable bash and no update mechanism is still vulnerable today. It simply stopped being interesting.

What it actually did in the wild

Mass scanning began within hours of disclosure. The payloads were, almost without exception, unimaginative: fetch a script, run it, join a botnet.

One of the families that spread through Shellshock was Bashlite, also called gafgyt — a small, crude DDoS bot for Linux devices. Its source leaked in 2015, was widely copied, and its descendants are the direct ancestors of the Mirai family that took down Dyn two years later.

Several early reports of major compromises — including at Yahoo and Lycos — were walked back or substantially downgraded on investigation. The lasting damage was not a marquee breach. It was the permanent enlargement of the Linux botnet population.

Scored 10.0, six months after a 5.0

Shellshock arrived five months after Heartbleed, and the comparison was made constantly at the time, usually badly.

Heartbleed's original CVSS v2 base score was 5.0. Shellshock's was 10.0. On the scoring framework's own terms that is defensible: Heartbleed leaked memory you could not choose, while Shellshock ran commands you did choose, as a service account, unauthenticated.

Operationally, the two demanded opposite responses. Shellshock was worse per host and simpler to close: patch bash, and the host is fixed. Heartbleed was less severe per host and impossible to close, because the patch did nothing about keys that had already left and there was no log to tell you whether any had.

If you have ever argued that a CVSS score should not drive a patching queue on its own, these two are the pair to reach for.

Further reading

  • Chet Ramey — the bash 4.3 patch series and the release notes for patches 25 through 30, September and October 2014.
  • Red Hat Security Blog — Bash specially-crafted environment variables code injection attack, 24 September 2014, and the follow-up on CVE-2014-7169.
  • Michał Zalewski — Bash bug: the other two RCEs, or how we chipped away at the original fix, September 2014.
  • Troy Hunt — Everything you need to know about the Shellshock Bash bug, 25 September 2014.
  • NIST NVD entries for CVE-2014-6271, 6277, 6278, 7169, 7186 and 7187.

EXHIBIT 0018 — SHELLSHOCK

We put it on a shirt. A brass maker's plate bolted to a riveted metal panel, drawn as an engraving, with () { :; }; cut into the brass in deep monospaced characters — and the engraving does not stop at the edge of the plate. It runs off the brass and keeps cutting into the bare panel behind, where it reads /bin/cat /etc/passwd. Cast into the metal below, small: EST. 1989.

The callout points at the one place that matters, the plate's right edge: THE PARSER DID NOT STOP HERE. Most people will read an old machine plate. Anyone who ran that line in September 2014 will read the bug.

That's the whole idea.

View the Shellshock shirt — EXHIBIT 0018

Back to blog

Leave a comment

Please note, comments need to be approved before they are published.