Notes

CVE-2014-6271: Shellshock

How a 25-year-old Bash parsing bug allowed remote code execution through environment variables, and why it took multiple patches to fix.

Why I wrote this

Shellshock demonstrates how a single parsing assumption in a ubiquitous tool can create an internet-scale attack surface — and why defense in depth matters more than any individual patch.

teardown 10 min read

On September 24, 2014, Stephane Chazelas disclosed a vulnerability in GNU Bash that had existed since version 1.03, released in 1989. The bug was simple: Bash continued parsing and executing code after the closing brace of an exported function definition. That parsing behavior turned every network service that invoked Bash into a potential remote code execution vector. The vulnerability was assigned CVE-2014-6271 and quickly became known as Shellshock.

What made Shellshock devastating was not its complexity. It was the combination of a trivial exploit, a ubiquitous target, and decades of accumulated exposure. Understanding how it worked — and why it took multiple patches to fix — is one of the best case studies in why parser bugs are uniquely dangerous.

How Bash function export works

Bash allows you to define shell functions and export them to child processes. This is a legitimate feature. If you define a function in a parent shell and export it, child Bash processes inherit that function through the environment.

my_func() { echo "hello from function"; }
export -f my_func
bash -c 'my_func'
# Output: hello from function

Under the hood, Bash serializes the function into an environment variable. In modern, patched versions of Bash, the variable name uses the format BASH_FUNC_name%% and the value starts with () {:

BASH_FUNC_my_func%%=() { echo "hello from function"; }

Before the Shellshock fix, the format was simpler. Any environment variable whose value started with () { was treated as a function definition:

my_func=() { echo "hello from function"; }

When a new Bash process starts, it scans its environment variables. If it finds one that looks like a function definition — a value beginning with () { — it parses that value to reconstruct the function. This is where the vulnerability lived.

The vulnerability

The parsing logic that reconstructed exported functions did not stop at the end of the function body. After encountering the closing }, Bash continued to parse and execute whatever came next in the environment variable’s value.

The classic proof of concept:

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

If your Bash is vulnerable, this prints:

vulnerable
test

If your Bash is patched, it prints only:

test

Here is what each piece does:

  • env x='...' — sets an environment variable x for the command that follows
  • () { :;} — a minimal function body (: is a no-op, the Bash equivalent of true)
  • ; echo vulnerable — the injected command, placed after the function’s closing brace
  • bash -c "echo test" — spawns a new Bash process, which scans its environment during initialization

The new Bash process sees that variable x starts with () {, so it begins parsing a function definition. It parses the function body { :;} successfully. Then — and here is the bug — it keeps parsing. It hits echo vulnerable and executes it. This happens during shell initialization, before the -c "echo test" command even runs.

Warning

Lab safety

Only test this on isolated systems — a throwaway VM or container. The one-liner itself is harmless (it runs echo), but testing against production Bash versions you have not verified could interact unpredictably with other running services. On any modern, maintained system, Bash has been patched for over a decade.

The fundamental issue was a failure to enforce a boundary. Bash’s parser treated the function body and everything after it as a single parseable unit. There was no check that said “stop parsing after the function definition ends.” The parser consumed whatever was there, and in Bash, parsing means executing.

Attack vectors

The severity of Shellshock came from the number of services that passed untrusted data into environment variables and then invoked Bash.

CGI scripts

This was the most widely exploited vector. The CGI (Common Gateway Interface) specification requires the web server to pass HTTP request headers as environment variables to the CGI script. Apache, for example, maps headers like this:

HTTP headerEnvironment variable
User-AgentHTTP_USER_AGENT
CookieHTTP_COOKIE
RefererHTTP_REFERER
Custom headersHTTP_X_CUSTOM_HEADER

If the CGI script is a Bash script — or if it invokes Bash at any point — those environment variables are scanned during Bash initialization. An attacker simply sets a malicious HTTP header:

GET /cgi-bin/status HTTP/1.1
Host: target.example.com
User-Agent: () { :;}; /bin/cat /etc/passwd

The web server stores the User-Agent header in the HTTP_USER_AGENT environment variable. When the CGI script runs, Bash parses the environment, hits the function definition, continues past the closing brace, and executes /bin/cat /etc/passwd. The output goes back to the attacker as part of the HTTP response.

Within hours of disclosure, mass scanning for vulnerable CGI endpoints was observed across the internet. Exploit payloads included reverse shells, cryptocurrency miners, and botnet installers.

SSH ForceCommand bypass

OpenSSH supports a ForceCommand directive that restricts authenticated users to a specific command. This is commonly used in environments where SSH keys grant access to a single operation — Git hosting (like Gitolite), backup scripts, or restricted admin tools.

When ForceCommand is set, the user’s original command is placed in the SSH_ORIGINAL_COMMAND environment variable. If the forced command is a Bash script, or if ForceCommand points to Bash itself, the environment is scanned during initialization.

An attacker with valid SSH credentials (even restricted ones) could set an environment variable via the SSH protocol:

ssh user@target '() { :;}; /bin/cat /etc/shadow'

The ForceCommand restriction is bypassed entirely. Bash parses the injected function definition, executes the trailing command, and the attacker gets arbitrary execution despite the intended command restriction.

DHCP clients

Several DHCP client implementations, including dhclient on many Linux distributions, process server-supplied DHCP options by passing them through shell scripts. A malicious DHCP server on a local network could supply crafted option values (such as hostnames or domain names) that, when processed by Bash, triggered the Shellshock vulnerability.

This vector required local network access but was particularly dangerous in environments like hotels, airports, and conference networks where DHCP servers are untrusted by default.

Detection

Testing your Bash version

The simplest check is running the original proof of concept:

env x='() { :;}; echo VULNERABLE' bash -c "echo patched"

Tip

Quick version check

You can also check the Bash version directly. The fix landed in Bash 4.3 patch 25 and was backported by every major distribution. On most systems:

bash --version
# GNU bash, version 5.2.x — anything 4.3-25+ is patched for the original CVE

But version checks alone are insufficient — you need to test for the follow-up CVEs as well. Run the PoC to be sure.

Wazuh and OSSEC rules

Shellshock attempts in web logs have a distinctive signature: HTTP headers containing () {. A Wazuh rule to detect this in Apache or Nginx access logs:

<rule id="100200" level="12">
  <if_group>web|accesslog</if_group>
  <regex>\(\)\s*\{</regex>
  <description>Shellshock attempt — function definition in HTTP request</description>
  <mitre>
    <id>T1190</id>
    <tactic>Initial Access</tactic>
  </mitre>
</rule>

For more granular detection, match against specific header fields in decoded logs:

<rule id="100201" level="14">
  <if_sid>100200</if_sid>
  <regex>/bin/|/usr/|/etc/|/tmp/</regex>
  <description>Shellshock exploit — function injection with path traversal</description>
  <mitre>
    <id>T1190</id>
    <tactic>Initial Access</tactic>
  </mitre>
</rule>

Tip

IDS signatures

Snort and Suricata both have well-maintained rules for Shellshock. The Emerging Threats ruleset includes signatures that match () { patterns in HTTP headers across all request types. Enable ET WEB_SERVER rules if you have not already.

Log analysis

Even without custom rules, you can search historical logs for evidence of Shellshock probes:

grep -r '() {' /var/log/apache2/ /var/log/nginx/ /var/log/httpd/

Any match in access logs is worth investigating. Legitimate HTTP headers never contain Bash function syntax.

The multi-patch saga

One of the most instructive aspects of Shellshock is that it was not fixed in a single patch. The initial fix was incomplete, and researchers found bypasses within hours. The full remediation involved multiple CVEs (commonly tracked as CVE-2014-6271, CVE-2014-7169, CVE-2014-6277, CVE-2014-6278, CVE-2014-7186, and CVE-2014-7187) and multiple rounds of patching.

CVE-2014-6271 — the original

Disclosed by Stephane Chazelas on September 24, 2014. Bash parsed and executed trailing commands after function definitions in environment variables. The initial patch attempted to restrict function imports so that only the function body would be parsed.

CVE-2014-7169 — the incomplete fix

Within a day, Tavis Ormandy demonstrated that the initial patch was insufficient. His bypass used a different syntax to achieve code execution through the same parser path:

env X='() { (a)=>\' bash -c "echo date"

This created a file named echo containing the output of the date command. The parser was still not properly terminating after the function body — it was just more careful about the specific pattern from the original PoC. The fix addressed the symptoms, not the root cause.

CVE-2014-6277 and CVE-2014-6278 — deeper parser issues

Michal Zalewski (lcamtuf) from Google discovered additional parser vulnerabilities in the function import code. CVE-2014-6277 involved a use-after-free in the parser when handling malformed function definitions. CVE-2014-6278 was yet another code execution bypass that worked against systems patched for both previous CVEs.

These were not edge cases in obscure configurations. They were fundamental problems with how the parser handled untrusted input. Each successive patch narrowed the attack surface, but the parser’s architecture made it difficult to guarantee that no further bypasses existed.

Additional parser flaws (CVE-2014-7186 and CVE-2014-7187) were also patched in the same response window, reinforcing that the issue extended beyond a single exploit string.

The final resolution required a more comprehensive change: Bash stopped importing function definitions from arbitrary environment variables entirely. Exported functions were moved to a separate namespace (BASH_FUNC_name%%), so only variables with that specific prefix would be parsed as functions. This was a design-level fix, not a parser patch.

Insight

Parser bugs resist incremental fixes

The Shellshock patch sequence is a textbook example of why parser vulnerabilities are hard to fix incrementally. Each patch addressed a specific bypass without rearchitecting the underlying parsing logic. Only the final change — moving exported functions to a distinct namespace — actually closed the class of vulnerability rather than individual instances of it.

Lessons learned

Twenty-five years of latent vulnerability

The vulnerable code was introduced in Bash 1.03, released in September 1989. For twenty-five years, every Bash installation on every Unix and Linux system carried this bug. No one noticed — or at least, no one disclosed it publicly — because the interaction between environment variables and function parsing was not an obvious attack surface. It took a deliberate, adversarial reading of the code to see the problem.

This is a reminder that time-in-production is not evidence of security. Code that has “worked fine for decades” may simply have never been examined by someone with the right adversarial mindset.

Defense in depth was the real lesson

CGI scripts were already considered a legacy architecture by 2014. Most modern web frameworks had moved to FastCGI, WSGI, or application servers that did not invoke a new shell process for every request. Organizations that had modernized their web stacks were largely unaffected by the CGI vector — not because they had patched Bash, but because they had eliminated the exposure.

This is defense in depth in practice. The vulnerability existed in Bash, but the exploitability depended on the surrounding architecture. Reducing shell exposure — using compiled languages, avoiding system() calls, preferring exec-style process creation over shell invocation — eliminates entire categories of vulnerabilities, including ones that have not been discovered yet.

Reducing shell exposure

Shellshock made the security community re-examine how often Bash (or any shell) was invoked in network-facing code paths. The answer was: far too often. DHCP clients, mail filters, CGI handlers, SSH wrappers, cron jobs processing external data — all of these were potential vectors because they passed untrusted input through environment variables into shell contexts.

The practical takeaway is straightforward:

  • Avoid invoking shells from network-facing services. Use execve-style calls that do not involve shell parsing.
  • If you must use shell scripts, sanitize or drop environment variables before invoking Bash.
  • Treat any service that passes external input into environment variables as a potential injection point.
  • Audit ForceCommand and restricted shell configurations — they are only as strong as the shell they invoke.

Insight

Systemic takeaway

Shellshock was not a failure of one developer or one code review. It was a failure of abstraction. The assumption that “environment variables are just data” was baked into decades of Unix conventions. When Bash treated that data as code, the abstraction broke, and the blast radius was the entire internet. The lesson is not “audit Bash more carefully.” The lesson is: minimize the surface where untrusted data can become code.


Shellshock remains one of the most important vulnerabilities to study — not because the exploit is sophisticated (it is trivially simple), but because it reveals how deeply a single assumption can embed itself in infrastructure. The bug was easy to exploit, hard to fully fix, and impossible to scope. That combination is what makes parser bugs in foundational tools so dangerous, and why eliminating unnecessary shell exposure is one of the highest-leverage security investments you can make.