Shellshock: arbitrary code execution in Bash
Bash evaluated trailing code in specially crafted environment variables, giving RCE through anything that passed attacker input into the environment — CGI especially.
- Severity
- 9.8critical
- Vendor
- GNU
- Disclosed
- 24 Sept 2014
- Affected
- GNU Bash through 4.3
CVSS breakdown
- Attack vector
- Network
- Attack complexity
- Low
- Privileges required
- None
- User interaction
- None
- Scope
- Unchanged
- Confidentiality
- High
- Integrity
- High
- Availability
- High
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
Analysis
Shellshock is what happens when a data channel turns out to have been a code channel for twenty-five years.
#The bug
Bash can export shell functions to child processes, not just variables. It does this by smuggling the function body through the environment: a variable whose value begins with () { is treated, on startup, as a function definition and parsed.
The parser did its job and then kept going. Anything trailing the closing brace was executed immediately, at shell startup, before Bash ran whatever it was actually invoked to run.
The canonical test is a single line:
env x='() { :;}; echo vulnerable' bash -c 'echo test'A patched Bash prints test. A vulnerable Bash prints vulnerable first — the trailing echo ran purely as a side effect of the environment being parsed.
Note what is not required: no authentication, no privileged context, no memory corruption. It is a parser that executes.
#Why it reached the internet
On its own this is a local curiosity. It became a 9.8 because of one convention: CGI copies HTTP request headers into environment variables.
A request header becomes HTTP_USER_AGENT, HTTP_REFERER, HTTP_COOKIE in the environment of the CGI process. If that process is a shell script — or invokes one, or is a binary that shells out — then Bash starts up, parses the attacker's header, and executes the trailing command as the web server user.
The attacker fully controls the value of those headers. That is the whole exploit.
CGI was the loudest path but not the only one. Anything that puts untrusted input into an environment variable and then reaches Bash qualified: some DHCP clients (which pass server-supplied options into hook scripts), ForceCommand in OpenSSH — which restricts which command runs but not the environment it inherits — and various mail handlers.
#Detection
Unusually for a memory-corruption-free RCE, this one is loud and very greppable. The payload has to contain the function-definition prefix, so it appears verbatim in access logs:
grep -rE '\(\s*\)\s*\{' /var/log/apache2/access.logLook in User-Agent, Referer, Cookie, and the request path. Exploitation attempts began within hours of disclosure and have never really stopped — scanners still spray it. Hits in a 2026 log are almost certainly background noise rather than a targeted campaign, but a hit with a 200 response and an outbound connection following it is not.
#Fixing it
Patch Bash — but note the first patch was incomplete. CVE-2014-6271 was followed within days by CVE-2014-7169, then 6277, 6278, 7186 and 7187 as the parser got more scrutiny. "Patched for Shellshock" in late September 2014 frequently meant "patched for the first one only." Verify with the test line above rather than trusting a version string.
Structurally: stop using CGI, and do not let untrusted input reach a shell's environment. dash as /bin/sh reduces the blast radius, but only because it lacks the exported-function feature — that is luck, not architecture.
#What generalises
Two things.
The first is the trust-boundary failure: everyone treated environment variables as inert data. They were an execution path, and had been since 1989. When you inventory your attack surface, ask which of your "data" channels are parsed by something that can also execute.
The second is that the exploit required no memory-safety bug at all. Rewriting Bash in Rust would have changed nothing here. Memory safety is worth having, but it is not the same thing as not executing attacker input.