Deutschlands Bester Hacker - Flashback (Web) - 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 Web
Punkte 241
Angriffsklasse Client-Side Reverse EngineeringReverse EngineeringAnalyse eines Systems oder Programms zur Rekonstruktion seiner Funktionsweise. (ActionScript 3 / SWF)

Die Challenge

Die Seite lädt eine alte Flash-Anwendung über Ruffle, den WASM-Flash-Emulator:

<script src="<URL>/ruffle"></script>
...
player.load("Main.swf");

Gesucht ist eine Passwort-Sequenz aus Button-Klicks, nach der die App die Flag anzeigt.

Aufklärung

Aus den Ruffle-Debug-Infos:

  • SWF URL: .../Main.swf
  • isActionScript3: true, swfVersion: 38, uncompressedLength: 3059
  • Allows script access: false, numFrames: 1

Das ist eine reine Client-Side-Challenge. Die gesamte Logik — und die Flag — steckt in Main.swf; die index.html ist nur der Loader, die mitgelieferte ruffle.js der unveränderte Standard-Player und damit irrelevant.

Die Schwachstelle

Die SWF ist binär — als Text kopieren zerstört Bytes. Also als Datei laden:

curl -o Main.swf <URL>/Main.swf
file Main.swf
# Macromedia Flash data (compressed), version 38

Header CWS bedeutet zlib-komprimiert. Dekomprimieren (CWS → FWS) und Strings aus dem ABC-Bytecode ziehen:

import zlib, struct, re
data = open("Main.swf", "rb").read()
ver, length = data[3], struct.unpack("<I", data[4:8])[0]
body = zlib.decompress(data[8:])
out  = b"FWS" + bytes([ver]) + struct.pack("<I", length) + body
open("Main_uncompressed.swf", "wb").write(out)
for s in re.findall(rb"[\x20-\x7e]{4,}", out):
    print(s.decode())

Die Methodennamen im String-Pool verraten den Ablauf:

inputSequence, createSquares, onSquareClick, revF, flashSquare, decode, makeLabel, orig

Es gibt 4 anklickbare Felder (createSquares). Jeder Klick hängt an inputSequence an (onSquareClick) und wird mit der Soll-Sequenz orig verglichen. Bei Übereinstimmung baut decode/revF die Flag zusammen.

Der Angriff

Zwei Konstanten stechen heraus — im String-Pool mit $ als Trenner abgelegt, eine simple VerschleierungObfuscationErschwert die Analyse von Code, Daten oder Kommunikation durch absichtliche Verkomplizierung. gegen naives strings:

$3$1$4$2$1$3$4$2$1$1$4$2$3$3$4$2                       <- orig  (Klick-Sequenz)
$D$B$H${$f$l$4$s$h$_$s$t$1$r$b$t$_$n$1$3$_$g$4$n$z$}   <- Flag  (Char-Array)

Die Soll-Sequenz orig sind 16 Klicks auf die Buttons 1–4:

3 · 1 · 4 · 2 · 1 · 3 · 4 · 2 · 1 · 1 · 4 · 2 · 3 · 3 · 4 · 2

Gibt man diese Reihenfolge in der App ein, zeigt sie die Flag an.

Die Flag

Der $-getrennte Char-Array wird von decode() einfach konkateniert und ergibt eine Flag im Format DBH{...} — thematisch passend zum „Flash lebt weiter“-Motiv. Der genaue Wert ergibt sich unmittelbar aus dem Char-Array oben; er lässt sich also sowohl über die Klicksequenz als auch rein statisch aus dem Bytecode gewinnen.

Was ich mitnehme

  • Flash lebt via Ruffle weiter — alte SWFs bleiben angreifbar und analysierbar, auch ohne Plugin.
  • CWS-SWFs sind nur zlib-komprimiert: zlib.decompress(data[8:]) reicht, um an den ABC-Bytecode und die Strings zu kommen. Kein Spezial-Tool nötig.
  • Für vollständige Decompilation (lesbares AS3) wäre JPEXS ffdec der Weg gewesen — hier überflüssig, weil Soll-Sequenz und Flag als Klartext-Konstanten im Pool lagen.
  • Sicherheitslektion: Client-Side-„Geheimnisse“ im SWF sind kein Schutz. Alles im Auslieferungsartefakt ist trivial extrahierbar; die $-Trennung verzögert nur strings, verhindert nichts.