0%

Was schiefgehen kann · Übung

Ein Angriff ist ein Test, der rot beginnt

Der dritte Angreifer aus Lektion 2 hat eine Kopie der Daten und sendet nie eine Anfrage. Diese Lektion zeigt, was diese Kopie ist. Führ im das erste Szenario des Kurs-Hilfsprogramms aus:

python pl_sec.py stolen-backup

pl_sec.py startet deinen eigenen Server auf 127.0.0.1, sendet ihm ein paar Anfragen und gibt aus, was passiert ist. Das ist die Ausgabe, bis auf die , die bei jedem Lauf neu ist:

pl_sec: stolen-backup, your server on 127.0.0.1 (a port the system chose)
--- two ordinary requests ---
ana logs in with her password       POST /login   -> 200
ana asks who she is, with a cookie  GET /me       -> 200  {"user": "ana"}
--- a copy of your server's state, as a thief would read it ---
books     2 books (not shown)
next_id   3
users     'ana': 'correct horse'
users     'ben': 'battery staple'
tokens    'tok-ana-writes-7Qx2': {'user': 'ana', 'can_write': True}
tokens    'tok-ben-reads-K9pz': {'user': 'ben', 'can_write': False}
sessions  'sJFKdwVWzmnyheWotZw-vA': {'user': 'ana'}
--- what the thief tries next: each stored value as that user's password ---
log in as ana with the value stored for ana  -> 200  the thief is logged in
log in as ben with the value stored for ben  -> 200  the thief is logged in
--- the code in reading_server.py that checks a password ---
            if credentials is None or state["users"].get(credentials["user"]) != credentials["password"]:
--- result ---
2 of 2 stored values worked as a password
serve() returned after 5 connections; the server is closed
pl_sec: every thread has stopped

Lies das Protokoll

Der erste Block ist normale Benutzung: ana meldet sich an und fragt, wer sie ist. Er steht da, damit der Zustand eine aktive Session enthält.

Der zweite Block ist die Kopie. Sie enthält beide Passwörter, beide Tokens und anas Session-ID, alles lesbar. Im dritten Block tut der Dieb das Erste, was jeder versuchen würde: Er sendet jeden gespeicherten Wert als Passwort des jeweiligen Benutzers, und beide Anmeldungen antworten mit 200. Der vierte Block zitiert die Zeile deines Servers, die ein Passwort prüft.

Der Dieb im Protokoll meldet sich als ben an. Was brauchte er zuerst?

Zwei Befunde

F1 kommt aus den zwei Zeilen von users. Exposed: das Passwort jedes Benutzers. Attack: Jeder mit einer Kopie des Zustands liest die Passwörter und meldet sich als jeder Benutzer an. Fix: Speichere etwas, das beweist, dass ein Passwort richtig ist, ohne das Passwort zu sein. Kapitel 3 baut das.

F2 kommt aus der zitierten Zeile. Die Anmeldung vergleicht den Versuch mit dem gespeicherten Wert mit !=. Kapitel 2, Lektion 3 erklärt, warum dieser Vergleich ein eigener Befund ist. Halte vorerst seine vier Teile fest, damit die Zeile existiert. Exposed: wie viel von einem geratenen Geheimnis richtig ist. Attack: != kann bei einem schlechteren Versuch früher antworten. Fix: Vergleiche ein Geheimnis mit hmac.compare_digest. Test: none yet.

Vom Befund zum Test

Der vierte Teil eines Befunds ist sein Nachweis. In diesem Kurs ist ein Nachweis ein Angriffstest (attack test): ein Test, der den Angriff ausführt und zusichert, dass der Angriff abgelehnt wird. Solange die Schwachstelle da ist, schlägt der Test fehl. Sobald sie geschlossen ist, läuft der Test durch und hält Wache.

Für F1 hat der Test drei Schritte, die Form aus Vorbereiten, Ausführen und Prüfen, die du kennst, wobei das Ausführen ein Angriff ist:

  1. Vorbereiten. Bau mit reading_server.sample_state() einen Zustand auf. Melde dich dann als ana mit ihrem Passwort an und sichere zu, dass der Status 200 ist. Diese erste Zusicherung ist eine Vorbedingung. Sie sorgt dafür, dass der Test einen Server betrifft, auf dem anas Passwort funktioniert.

  2. Angreifen. Nimm die Kopie: str(state) macht aus dem ganzen Zustand Text, wie es eine Sicherung täte.

  3. Die Ablehnung zusichern. correct horse darf in diesem Text nicht vorkommen.

Nimm die Kopie nach der Anmeldung. Ein Server könnte ein Passwort auch während der Bearbeitung der Anfrage preisgeben, zum Beispiel in eine Session, und eine früher genommene Kopie würde das übersehen.

tests/helpers.py erspart dir, Anfragen von Hand zu bauen. Lies jetzt make_request und basic. Der Rest der Datei ist für spätere Kapitel.

headers = {"Authorization": helpers.basic("ana", "correct horse")}
request = helpers.make_request("POST", "/login", headers=headers)

make_request baut das Anfrage-, das handle() bekommt, dieselbe , die deine -Tests hatten. basic(user, password) baut den Wert für Authorization mit Basic-Zugangsdaten.

Warum meldet sich der Test zuerst an und erwartet den Status 200?

Rot ist heute die richtige Farbe

Wenn der Test geschrieben ist, führ die Datei aus:

python -m pytest -q

Sie endet mit 1 failed, und pytest zeigt den Grund: 'correct horse' is contained here, gefolgt von dem Stück des Zustands, das es enthält. Dieses Fehlschlagen ist der Befund, wiederholbar gemacht. Jeder kann jetzt einen Befehl ausführen und F1 sehen.

Der Test bleibt rot bis Kapitel 3, Lektion 6, wo du änderst, wie Passwörter gespeichert werden. Der Kommentar oben in tests/test_attacks.py sagt das, und keine Prüfung vorher verlangt, dass dieser Test durchläuft.

Du führst deinen neuen Angriffstest gegen den Server in seinem jetzigen Zustand aus, und er läuft durch. Was sagt dir das?

Eine Grenze, klar gesagt: Dieser Test beweist, dass kein gespeicherter Wert das Passwort als Text ist. Ein Server, der das Passwort in speichert, würde ihn bestehen und nichts schützen. Kapitel 2 beginnt genau mit dieser Verwechslung.

Warum über das Modul

Die Testdatei beginnt mit import reading_server, und dein Test ruft reading_server.sample_state() und reading_server.handle(...) auf. Lass es so und schreib nie from reading_server import handle.

Der Grund ist, wie dein Test geprüft wird. Check my work führt deinen Test gegen andere Server aus: einen schwachen, der Passwörter im Klartext speichert, einen korrigierten, der kein lesbares Passwort behält, und einen, der jede Anmeldung ablehnt, bei dem die Vorbedingung deinen Test fehlschlagen lassen muss. Es setzt sie ein, indem es reading_server.handle und reading_server.sample_state ersetzt. Ein Test, der auf dem schwachen Server fehlschlägt und auf dem korrigierten durchläuft, hat gezeigt, dass er die Schwachstelle sieht. Ein einzeln importierter Name würde weiter auf deinen Server zeigen, und der Austausch bliebe unbemerkt. Ein Zustands-Dictionary, das du von Hand baust, würde auch nicht zu den anderen Servern passen, deshalb beginnt der Test mit sample_state().

Deine Aufgabe

  1. Schreib in tests/test_attacks.py unter dem Kommentar für diese Lektion test_attack_backup_reveals_no_password() mit den drei Schritten oben.

  2. Führ python -m pytest -q aus und sieh zu, wie er bei der letzten Zusicherung fehlschlägt.

  3. Öffne SECURITY.md und fülle die Zeilen F1 bis F4 aus. Nimm F1 und F2 aus dieser Lektion und F3 und F4 aus Lektion 3, gern in deinen eigenen Worten. Halte jede Zeile auf einer Zeile. Trag den Namen deines Tests in die Zelle Test von F1 ein, schreib none yet in die anderen drei und lass jeden Status auf open.

Klicke dann auf Check my work. Du verlässt dieses Kapitel mit vier festgehaltenen Befunden, von denen einer durch einen fehlschlagenden Test bewiesen ist. Kapitel 2 baut die zwei Werkzeuge, die die Behebungen brauchen.

Aufgabe

Schreib den ersten Angriffstest und halte vier Befunde fest.

Schreib in tests/test_attacks.py die test_attack_backup_reveals_no_password():

  • Bau mit reading_server.sample_state() einen Zustand auf.

  • Sende über reading_server.handle(...) POST /login für ana mit ihrem Passwort correct horse und sichere zu, dass der Status 200 ist. helpers.basic("ana", "correct horse") baut den für Authorization, und helpers.make_request("POST", "/login", headers=...) baut die Anfrage.

  • Mach dann mit str(state) aus dem ganzen Zustand Text und sichere zu, dass correct horse nicht darin vorkommt.

Führ im python -m pytest -q aus: Der Test muss fehlschlagen. Er bleibt rot bis Kapitel 3, Lektion 6.

Fülle dann die Zeilen F1 bis F4 von SECURITY.md aus: F1 und F2 aus dem, was python pl_sec.py stolen-backup ausgegeben hat, F3 und F4 aus Lektion 3. Halte eine Zeile pro Befund, trag den Namen deines Tests in die Zelle Test von F1 ein und lass jeden Status auf open.

Klicke dann auf Check my work. Es führt deinen Test viermal aus, jedes Mal gegen einen anderen Server, den es an die Stelle von reading_server.handle und reading_server.sample_state setzt: einen, dessen Zustand Passwörter im Klartext enthält, wo dein Test fehlschlagen muss; einen, der anas Passwort akzeptiert und keine lesbare Kopie davon behält, wo dein Test durchlaufen muss; einen, der das Passwort bei der Anmeldung in die kopiert, wo dein Test wieder fehlschlagen muss; und einen, der jede Anmeldung ablehnt, wo dein Test ebenfalls fehlschlagen muss. Außerdem liest es SECURITY.md: Die Zeilen F1 bis F4 brauchen Text in ihren Zellen Exposed, Attack und Fix, und F1 braucht den Namen deines Tests. Die Formulierung deiner Befunde wird nicht bewertet, und nichts aus deinem Terminal wird bewertet.