Stomach Bug
Stomach Bug
Platform: Metactf | Category: Steganography | Type: Challenge | Difficulty: Easy | OS: NA | Author: D3v0o0Nu11 | 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.
Recon
Port scan
nmap -p- -sV -sC <TARGET> --min-rate 1000 -Pn
| Port | Service | Version | Notes |
|---|---|---|---|
| <PORT> | <SVC> | <VER> | <notes> |
Enumeration highlights
- Event:
metactf| ID:20260410_metactf_stomach_bug - Tags: base64, qr_code, dawgctf, png_reconstruction, hex_dump, streaming_response
- Indicators: an HTTP response streams forever as an attachment instead of returning a normal page, numbered hex lines like |000|...|161| can be concatenated by index into a binary file, the recovered image is grayscale and visually contains a QR code, QR output looks like corrupted binary until UTF-8 text is re-encoded as latin1 bytes
- Source:
20260410_metactf_stomach_bug.md
Foothold
Vulnerability / Misconfiguration
- Nested_qr_decoding
- Ordered_hex_reassembly
- Utf8_to_latin1_byte_recovery
<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
- nested_qr_decoding
- ordered_hex_reassembly
- utf8_to_latin1_byte_recovery
- Tags: base64, qr_code, dawgctf, png_reconstruction, hex_dump, streaming_response
Original Writeup
<details><summary>Click to expand original content</summary>Description
Source challenge:
Stomach BugURL:
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:
- The server does not return a normal HTML page; it starts downloading
spew.txtand keeps streaming. - The body alternates between sliding printable ASCII text and lines of the form
|000|...through|161|.... - The numbered lines are hex-only payload chunks. Sorting by index and concatenating them reconstructs a valid PNG.
- The recovered image is a 625x625 grayscale QR code.
- 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. - The second QR contains a base64 string, which decodes directly to the flag.
Solution
Extraction pipeline:
- Download only a short slice of the endless response.
- Regex-extract all numbered hex chunks.
- Sort them by numeric index and concatenate the hex payload.
- Convert the hex to bytes and save the first PNG.
- Decode the first QR.
- Repair the UTF-8-expanded PNG bytes with
.decode("utf-8").encode("latin1"). - Decode the nested QR.
- Base64-decode the nested QR text to recover the flag.
#!/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()
</details>
Auto-tracked: saved to WriteUps; run
/xesor-reviseto fold lessons into XESXor_Methodology.md.
signed by XESXOR