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 | Crypto |
| Punkte | 454 |
| Angriffsklasse | AES-CTR Keystream Reuse (Two-Time-PadKeystream ReuseMehrfache Verwendung desselben Schlüsselstroms, wodurch verschlüsselte Nachrichten angreifbar werden.) |
| Flag | DBH{K3YSTR34M_R3CYCL1NG_1ST_K31N3_1D33_XXX} |
Die Challenge
Die Anwendung speichert Notizen nur verschlüsseltEncryptionWandelt Klartext mithilfe eines Schlüssels in nicht lesbaren Geheimtext um.. Laut Beschreibung wird AES-CTR mit
einem 256-Bit-Schlüssel verwendet. Der kritische Punkt: der IVInitialization VectorStartwert einer Blockchiffre-Betriebsart, der gleiche Klartexte zu unterschiedlichen Chiffraten macht. ist nicht pro Nachricht
zufällig, sondern hängt nur von der Kategorie ab:
allgemeinvertraulich
Der Klartext jeder Notiz hat immer dasselbe Format:
<kategorie>|<zeitstempel>|<benutzer>|<text>
Aufklärung
POST /api/notiz liefert nicht nur das Chiffrat zurück, sondern auch den kompletten
Klartext-Umschlag. Damit ist ein Known-Plaintext-Paar frei Haus geliefert — man muss es nur
noch anfordern.
Die Schwachstelle
Bei AES-CTR gilt:
Ciphertext = Plaintext XOR Keystream
Wird derselbe IV mit demselben Schlüssel wiederverwendet, entsteht für alle Nachrichten derselbe Keystream. Genau das passiert hier pro Kategorie. Aus einer selbst erzeugten Gastnotiz lässt sich der Keystream direkt berechnen:
Keystream = Ciphertext XOR Plaintext
Sobald der Keystream für allgemein oder vertraulich bekannt ist, lassen sich alle
Archiv-Einträge derselben Kategorie entschlüsseln:
Plaintext = Ciphertext XOR Keystream
Der Angriff
Es reichen zwei Testnotizen, eine pro Kategorie, mit möglichst langem Text — der Keystream muss mindestens so lang sein wie der längste Archiv-Eintrag.
Probe für allgemein:
TEXT=$(python3 -c 'print("A"*256, end="")')
curl -s '<URL>/api/notiz' \
-H 'Content-Type: application/json' \
--data "$(printf '{"kategorie":"allgemein","text":"%s"}' "$TEXT")"
Probe für vertraulich:
TEXT=$(python3 -c 'print("B"*256, end="")')
curl -s '<URL>/api/notiz' \
-H 'Content-Type: application/json' \
--data "$(printf '{"kategorie":"vertraulich","text":"%s"}' "$TEXT")"
Beide Antworten enthalten klartext und chiffrat. Der Rest ist ein XOR:
keystream = xor(probe_ciphertext, probe_plaintext)
plaintext = xor(archive_ciphertext, keystream[:len(archive_ciphertext)])
Da die Probeantworten länger waren als alle Archiv-Einträge, reichte der Keystream vollständig aus. Entschlüsselt ergab sich das komplette Archiv:
1: vertraulich|2026-02-03T08:41:12Z|admin|Backup-Rotation auf Montag vorgezogen, Bandwechsel wie besprochen.
2: allgemein|2026-02-03T09:02:55Z|gast|Kaffeekasse steht wieder im Kuechenschrank links.
3: vertraulich|2026-02-04T11:17:03Z|mkramer|Zugriff auf die Reisekostenmappe bitte erst nach Freigabe durch die BL.
4: allgemein|2026-02-05T07:30:00Z|gast|Drucker im zweiten Stock hat wieder Papierstau, Technik ist informiert.
5: vertraulich|2026-02-06T16:55:41Z|admin|Notfallzugang Notizblock-Admin. Wiederherstellungscode: DBH{K3YSTR34M_R3CYCL1NG_1ST_K31N3_1D33_XXX} -- nur fuer den Fall, dass die Zwei-Faktor-App verloren geht.
6: allgemein|2026-02-07T10:12:19Z|gast|Freitag faellt das Standup aus, stattdessen kurze Runde am Montag.
7: vertraulich|2026-02-08T09:04:37Z|jvogt|Vertragsentwurf liegt zur Durchsicht im Fach, Rueckmeldung bis Mittwoch.
8: vertraulich|2026-02-09T13:22:08Z|admin|Wartungsfenster naechsten Sonntag 02:00-04:00, Dienst dann nicht erreichbar.
9: vertraulich|2026-02-10T15:48:56Z|mkramer|Bitte die alten Notizen aus dem Vorjahr archivieren, das Archiv wird zu gross.
Die Flag
DBH{K3YSTR34M_R3CYCL1NG_1ST_K31N3_1D33_XXX}
Was ich mitnehme
CTR ist nur dann sicher, wenn niemals derselbe Keystream mehrfach verwendet wird. Ein
fester, aus der Kategorie abgeleiteter IV macht alle Nachrichten einer Kategorie zu einem
Two-Time-Pad und erlaubt damit die vollständige Entschlüsselung per bekanntem Klartext.
Verschärfend kommt hinzu, dass die API selbst das Klartext-Chiffrat-Paar zurückgibt. Man braucht dann nicht einmal statistische Two-Time-Pad-Analyse, sondern bekommt den Keystream geschenkt.