← Back to Writeups
CUSTOMEasySteganography

Stomach Bug

XESXOR9/10/20262 min read
#steganography#custom#easy#nested_qr_decoding#ordered_hex_reassembly#utf8_to_latin1_byte_recovery

Stomach Bug

Platform: metactf | Category: Steganography | Type: Challenge | Difficulty: Easy | OS: NA | Author: XESXOR | Date: 2026-04-10 | Status: Solved Techniques: nested_qr_decoding, ordered_hex_reassembly, utf8_to_latin1_byte_recovery

Summary

Task: a web endpoint continuously streams printable junk mixed with indexed hex chunks that hide an image. Solution: extract the numbered hex records, rebuild the PNG, decode two QR layers, repair UTF-8-expanded bytes with latin1 re-encoding, then base64-decode the final payload.

Stomach Bug — metactf

Description

Source challenge: Stomach Bug

URL: https://stomachbug.umbccd.net

English summary: the endpoint returns an endless attachment named spew.txt. Inside the stream, useful data appears as numbered hex records mixed with distracting printable ASCII lines.

Analysis

Recon observations:

  1. The server does not return a normal HTML page; it starts downloading spew.txt and keeps streaming.
  2. The body alternates between sliding printable ASCII text and lines of the form |000|... through |161|....
  3. The numbered lines are hex-only payload chunks. Sorting by index and concatenating them reconstructs a valid PNG.
  4. The recovered image is a 625x625 grayscale QR code.
  5. Decoding that QR yields PNG bytes that were expanded through UTF-8 text encoding, so they must be converted back with .decode("utf-8").encode("latin1") before opening the nested image.
  6. The second QR contains a base64 string, which decodes directly to the flag.

Solution

Extraction pipeline:

  1. Download only a short slice of the endless response.
  2. Regex-extract all numbered hex chunks.
  3. Sort them by numeric index and concatenate the hex payload.
  4. Convert the hex to bytes and save the first PNG.
  5. Decode the first QR.
  6. Repair the UTF-8-expanded PNG bytes with .decode("utf-8").encode("latin1").
  7. Decode the nested QR.
  8. Base64-decode the nested QR text to recover the flag.
python
#!/usr/bin/env python3

import base64
import io
import re

import requests
from PIL import Image
from pyzbar.pyzbar import decode


URL = "https://stomachbug.umbccd.net"
LINE_RE = re.compile(r"^\|(\d{3})\|([0-9a-fA-F]+)$")


def decode_qr(image_bytes: bytes) -> bytes:
    img = Image.open(io.BytesIO(image_bytes))
    result = decode(img)
    if not result:
        raise RuntimeError("QR code not found")
    return result[0].data


def fetch_sample(max_lines: int = 2000) -> str:
    lines = []
    with requests.get(URL, stream=True, timeout=15) as r:
        r.raise_for_status()
        for i, line in enumerate(r.iter_lines(decode_unicode=True), 1):
            if line is not None:
                lines.append(line)
            if i >= max_lines:
                break
    return "\n".join(lines)


def main() -> None:
    sample = fetch_sample()

    chunks = {}
    for line in sample.splitlines():
        m = LINE_RE.match(line.strip())
        if m:
            idx = int(m.group(1))
            chunks[idx] = m.group(2)

    if not chunks:
        raise RuntimeError("No numbered hex chunks found")

    first_png = bytes.fromhex("".join(chunks[i] for i in sorted(chunks)))
    qr1 = decode_qr(first_png)

    nested_png = qr1.decode("utf-8").encode("latin1")
    qr2 = decode_qr(nested_png)

    flag = base64.b64decode(qr2).decode()
    print(flag)


if __name__ == "__main__":
    main()