Zum Hauptinhalt springen

Offensive Code-Analyse — Praktischer Workshop

Sie sind dran. Sie bauen ein absichtlich verwundbares Flask-Repository, lesen es, lassen es durch Semgrep, Bandit und Gitleaks laufen, liefern fünf oder sechs Befunde, korrigieren und scannen erneut. Die Demonstration hat gezeigt, wie man eine Ausgabe liest. Hier zählt das Ergebnis (Liefergegenstand).

Zeitaufwand: 2 bis 3 Stunden.

Liefergegenstand: ein Ordner ~/analyse-code/flask-audit/ mit dem Quellcode, den Rohausgaben, einem rapport.md mit sechs Befunden und einem durch einen zweiten Scan verifizierten Patch.

Strikter Rahmen

Der einzige Code, den Sie auditieren, ist derjenige, den Sie gerade selbst in diesem Ordner erstellt haben, oder ein vom Trainer bereitgestelltes Lab-Repository. Kein GitHub-Repository von Unbekannten, kein Klonen eines öffentlichen Exploits, keine Drittanwendung online. Sie pushen dieses Lab nicht in ein öffentliches Repository: Es enthält Geheimnis-Muster, auch wenn sie fiktiv sind, sowie gefährliche Routen.


Schritt 1 — Das Labor vorbereiten (15 Min.)​

mkdir -p ~/analyse-code/flask-audit/{sorties,correctifs}
cd ~/analyse-code/flask-audit

Erstellen Sie vier Dateien. Übernehmen Sie sie unverändert: Die Tool-Kennungen im Selbstbewertungsraster beziehen sich auf genau diese Zeilen.

config.py:

# Lokales Labor — nicht bereitstellen, nicht pushen.
AWS_ACCESS_KEY_ID = "AKIAIOSFODNN7EXAMPLE"
AWS_SECRET_ACCESS_KEY = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
SLACK_WEBHOOK = "https://hooks.slack.com/services/T00000000/B00000000/exemplecours"
DB_PASSWORD = "SuperSecret123!"

utils.py:

import hashlib
import pickle

def hash_mot_de_passe(mot: str) -> str:
return hashlib.md5(mot.encode("utf-8")).hexdigest()

def charger_session(blob: bytes):
return pickle.loads(blob)

app.py:

import os
import sqlite3
from flask import Flask, request, render_template_string

import config
from utils import hash_mot_de_passe, charger_session

app = Flask(__name__)

@app.route("/user")
def user():
user_id = request.args.get("id", "1")
conn = sqlite3.connect("app.db")
cur = conn.cursor()
cur.execute("SELECT * FROM users WHERE id = %s" % (user_id,))
return str(cur.fetchone())

@app.route("/ping")
def ping():
host = request.args.get("host", "127.0.0.1")
os.system("ping -c 1 %s" % host)
return "pong"

@app.route("/hello")
def hello():
name = request.args.get("name", "monde")
return render_template_string("<h1>Bonjour " + name + "</h1>")

@app.route("/session", methods=["POST"])
def session_charge():
return str(charger_session(request.data))

@app.route("/login", methods=["POST"])
def login():
password = request.form.get("password", "")
attendu = hash_mot_de_passe(config.DB_PASSWORD)
fourni = hash_mot_de_passe(password)
return "ok" if fourni == attendu else "ko"

requirements.txt:

Flask==3.0.3

Sie installieren Flask nur, wenn Sie die Anwendung nach dem Patch lokal ausführen möchten. Für das SAST-Audit genügen die Analysewerkzeuge.

Schreiben Sie jetzt schon den Kopfbereich in rapport.md. Ohne ihn hat der Rest keinen Rahmen.

# Statisches Review — flask-audit
- Datum:
- Geltungsbereich: Dateien `app.py`, `utils.py`, `config.py` (lokales Lab)
- Tools: Bandit, Semgrep (`p/python`, `p/flask`, `p/secrets`), Gitleaks `--no-git`
- Ausführungsentscheidung: keine Payload gesendet, kein `flask run` vor Abschluss der Befunde

Schritt 2 — Statische Lektüre von Hand (20 Min.)​

Vor den Tools. Öffnen Sie die drei Python-Dateien und füllen Sie in sorties/lecture-manuelle.md diese Tabelle aus.

DateiZeilePrimitive oder SignalKategorie
config.pyfest codiertes Geheimnis
utils.pyMD5 / pickle
app.pySQLi / Befehl / XSS / Datenfluss

Ziel: mindestens sechs Zeilen, eine pro geforderter Kategorie (SQLi, Befehlsinjektion, pickle, MD5, Geheimnis, XSS). Wenn Sie nicht alle sechs vor Bandit haben, lesen Sie erneut: Sie sind alle ohne Tool sichtbar.

Suchen Sie auch nach dem, was die Tools möglicherweise übersehen: Der Slack-Webhook ist eine fest codierte URL (Signal aus dem Kurs), nicht nur ein „AWS-Geheimnis".

rg -n "eval|exec|system|pickle|md5|AKIA|authorized_keys|render_template_string|execute" .

Legen Sie die Ausgabe in sorties/rg.txt ab. Das ist Ihr Sicherheitsnetz, nicht Ihr Bericht.


Schritt 3 — Die drei Analysewerkzeuge ausführen (20 Min.)​

cd ~/analyse-code/flask-audit

bandit -r . -f txt -o sorties/bandit.txt
bandit -r . -f json -o sorties/bandit.json

semgrep --config p/python --config p/flask --config p/secrets \
--json --output sorties/semgrep.json .
semgrep --config p/python --config p/flask --config p/secrets \
--text --output sorties/semgrep.txt .

gitleaks detect --source . --no-git -v --report-path sorties/gitleaks.json \
| tee sorties/gitleaks.txt

Wenn Semgrep eine Konfiguration ablehnt, dokumentieren Sie den Ausweichweg im Bericht (--config auto oder lokale Regeln) und machen Sie weiter. Ein fehlendes Tool wird durch rg plus die Lektüre ersetzt; es wird nicht durch das Ausführen der Routen ersetzt.

Sie müssen für jedes Tool eine nicht leere Datei in sorties/ vorweisen können. Das ist das erste Zulässigkeitskriterium.


Schritt 4 — Abgleichen, nicht abschreiben (25 Min.)​

Öffnen Sie die drei Ausgaben. Erstellen Sie sorties/matrice.md:

| Interne ID | Zeile | Bandit | Semgrep | Gitleaks | Manuelle Lektüre | Urteil |
| --- | --- | --- | --- | --- | --- | --- |
| A-01 | | | | | | behalten / Rauschen / Beispiel |

Regeln für das Urteil:

  • Behalten: Die Zeile tut, was das Tool behauptet (eingehende Daten in execute, os.system, pickle.loads, verkettetes HTML, MD5-Passwort-Hash, Geheimnis im Quelltext).
  • Beispiel: AKIAIOSFODNN7EXAMPLE und das AWS-Geheimnis aus der Dokumentation. Sie behalten es trotzdem als Muster-Befund (das Repository lehrt, Schlüssel in config.py zu speichern), mit der Qualifizierung „Dokumentations-Schlüssel".
  • Rauschen: ein Alarm bei einer Konstante ohne eingehenden Datenfluss, oder ein exaktes Duplikat. Ein einziger Befund pro Schwachstelle, auch wenn drei Tools sich melden.

Sie liefern fünf oder sechs behaltene Befunde, keine dreißig Screenshots. Die sechs Kategorien des Themas müssen auftauchen. Wenn Bandit die XSS übersieht, trägt sie Semgrep oder Ihre manuelle Lektüre.

Ein Befund, eine Schwachstelle

login ruft hash_mot_de_passe zweimal auf. Das ist ein MD5-Befund (die Primitive in utils.py), nicht zwei. Die Route /session und charger_session sind ein Pickle-Befund. Der Kunde korrigiert eine Stelle, nicht einen Alarm pro Tool.


Schritt 5 — Die sechs Befunde verfassen (40 Min.)​

Jeder Befund in rapport.md folgt demselben Grundgerüst. Kopieren Sie die Vorlage sechsmal (A-01 bis A-06). Die inneren Codeblöcke bleiben bei drei Backticks; die äußere Vorlage wird hier mit vier angegeben, damit die Seite kompiliert — in Ihrem rapport.md (einfaches Markdown) genügen überall drei.

## A-0X — `<Kategorie>`: `<Titel in einer Zeile>`

- **Datei / Zeile**:
- **Tools**: (z. B. Bandit B608, Semgrep tainted-sql-string)
- **Voraussetzung**: wer kann was erreichen, ohne einen Exploit zu starten
- **Beweis** (Auszug):

```python
# fügen Sie NUR die fehlerhafte Zeile ein, nicht die ganze Datei
```

- **Auswirkung**: ein bis drei Sätze, Privileg und Konsequenz
- **Abhilfemaßnahme**: die Maßnahme, nicht „den Code absichern"
- **Nachprüfung**: Befehl und Kriterium (die Tool-ID verschwindet, oder die Zeile existiert nicht mehr)

Was jede Kategorie zeigen muss​

Sie senden keinerlei HTTP-Payload. Der Beweis ist der Quelltext.

Vorgeschlagene IDKategorieMindestbeweisZu formulierende AuswirkungErwartete Abhilfemaßnahme
A-01SQLicur.execute formatiert mit user_id aus request.argsSQL-Abfrage vom Client kontrolliertParametrisierte Abfrage ? / Bindung
A-02Befehlos.system + hostSystembefehl kontrolliertsubprocess als Liste, ohne Shell, validierter Host (Allowlist)
A-03XSSrender_template_string + Verkettung von nameReflektiertes HTML, Skript im Browser möglichAutomatisch escapte Jinja-Vorlage, keine Verkettung
A-04Picklepickle.loads auf request.dataCodeausführung beim Ladenjson + Schema, oder Entfernen der Route
A-05MD5hashlib.md5 auf einem PasswortKnackbarer Hash, kein passender Passwort-Speicherargon2 / bcrypt, niemals MD5 oder SHA-1 für ein Geheimnis
A-06GeheimnisSchlüssel in config.pyLeck sobald der Ordner geteilt wirdUmgebungsvariablen, Datei außerhalb des Repositorys, Beispielschlüssel entfernt

Ausformuliertes Beispiel für A-04 — die fünf anderen sind Ihre Aufgabe, im selben Präzisionsgrad.

## A-04 — Deserialisierung: `pickle.loads` auf `POST /session`

- **Datei / Zeile**: `utils.py` (`charger_session`), aufgerufen von `app.py` (`session_charge`)
- **Tools**: Bandit B301, Semgrep `python.lang.security.deserialization.pickle.avoid-pickle`
- **Voraussetzung**: `POST /session` erreichen (anonyme Route in diesem Lab)
- **Beweis**:

```python
def charger_session(blob: bytes):
return pickle.loads(blob)
```

- **Auswirkung**: Der Anfragekörper wird deserialisiert. Ein Pickle-Objekt ist keine Daten: Es ist ein Programm. Der Flask-Prozess läuft mit den Rechten des Kontos, das ihn startet.
- **Abhilfemaßnahme**: `charger_session` und die Route entfernen, oder nur ein begrenztes JSON akzeptieren (`json.loads` + erwartete Schlüssel).
- **Nachprüfung**: `rg -n pickle utils.py app.py` ohne Treffer; Bandit meldet B301 nicht mehr.

Schritt 6 — Ein Patch, dann der erneute Scan (30 Min.)​

Wählen Sie mindestens zwei Befunde aus A-01, A-02, A-04 (die drei, die Codeausführungen sind). Die Geheimnisse (A-06) zählen ebenfalls: Die Auslagerung in die Umgebung ist ein für Gitleaks sichtbarer Patch.

Arbeiten Sie mit Kopien, nicht ohne Sicherheitsnetz überschreibend:

cp app.py correctifs/app.py.avant
cp utils.py correctifs/utils.py.avant
cp config.py correctifs/config.py.avant

Akzeptierte minimale Patches (anzupassen, nicht blind einzufügen, falls sich Ihre Zeilen verschoben haben):

# A-01 — app.py
cur.execute("SELECT * FROM users WHERE id = ?", (user_id,))

# A-02 — app.py
import subprocess
subprocess.run(["ping", "-c", "1", "--", host], check=False)
# und vor dem Aufruf: jeden Host ablehnen, der nicht in einer Allowlist von IPs/Namen steht

# A-03 — app.py
from flask import render_template
# Vorlage templates/hello.html: <h1>Bonjour {{ name }}</h1>
return render_template("hello.html", name=name)

# A-04 — utils.py + app.py
import json
def charger_session(blob: bytes):
return json.loads(blob.decode("utf-8"))

# A-05 — utils.py
from hashlib import sha256 # unzureichend für ein echtes Passwort
# im Bericht erwartet: bcrypt oder argon2, kein anderer MD
# Beispielrichtung:
# import bcrypt
# return bcrypt.hashpw(mot.encode(), bcrypt.gensalt()).decode()

# A-06 — config.py
import os
AWS_ACCESS_KEY_ID = os.environ["AWS_ACCESS_KEY_ID"]
AWS_SECRET_ACCESS_KEY = os.environ["AWS_SECRET_ACCESS_KEY"]
DB_PASSWORD = os.environ["DB_PASSWORD"]

Für A-03 escaped die Jinja-Vorlage standardmäßig: Eine Interpolation {{ name }} ist keine HTML-Verkettung mehr. Erstellen Sie templates/hello.html, wenn Sie die XSS korrigieren.

Führen Sie dieselben Befehle erneut aus, in separate Dateien:

bandit -r . -f txt -o sorties/bandit-apres.txt
semgrep --config p/python --config p/flask --config p/secrets \
--text --output sorties/semgrep-apres.txt .
gitleaks detect --source . --no-git -v --report-path sorties/gitleaks-apres.json \
| tee sorties/gitleaks-apres.txt

Fügen Sie in rapport.md einen Abschnitt Nachprüfung hinzu:

## Nachprüfung

| ID | Vorher (Tool-ID oder Zeile) | Nachher | Status |
| --- | --- | --- | --- |
| A-01 | B608 / execute % | fehlt in bandit-apres.txt | behoben / teilweise / gescheitert |
| A-02 | | | |
| A-04 | | | |
| A-06 | AKIA in gitleaks.json | | |

„Teilweise" ist ehrlich: subprocess.run als Liste schließt die Shell-Injektion, aber ein nicht validierter host bleibt ein Problem. Sie notieren das. Ein Patch, der Bandit durch Hinzufügen von # nosec zum Schweigen bringt, ohne den Datenfluss zu ändern, ist ein Fehlschlag: Erwähnen Sie es, falls Sie es versucht haben, und entfernen Sie es dann wieder.

Nicht „korrigieren", indem man das Tool zum Schweigen bringt

# nosec, nosemgrep und Gitleaks-Ausschlüsse werden nur für ein im Befund begründetes Falsch-Positiv akzeptiert. Die sechs Kategorien dieses Workshops sind keine Falsch-Positive.


Schritt 7 — Überprüfbarer Liefergegenstand​

Die endgültige Ordnerstruktur, wie ein Prüfer sie wieder öffnet:

~/analyse-code/flask-audit/
app.py
utils.py
config.py
requirements.txt
templates/hello.html # falls A-03 behoben
sorties/
lecture-manuelle.md
rg.txt
matrice.md
bandit.txt
bandit.json
semgrep.txt
semgrep.json
gitleaks.txt
gitleaks.json
bandit-apres.txt
semgrep-apres.txt
gitleaks-apres.txt
gitleaks-apres.json
correctifs/
app.py.avant
utils.py.avant
config.py.avant
rapport.md

rapport.md enthält, in dieser Reihenfolge:

  1. Der Kopfbereich aus Schritt 1 (Geltungsbereich, Tools, Entscheidung, keine Payload auszuführen).
  2. Die Abgleichsmatrix (oder ein expliziter Verweis auf sorties/matrice.md).
  3. Fünf oder sechs Befunde nach der Vorlage aus Schritt 5, alle Kategorien vorhanden.
  4. Der Abschnitt Nachprüfung mit mindestens zwei Zeilen im Status behoben oder teilweise, Belege in sorties/*-apres.*.
  5. Ein Satz darüber, was Sie nicht getan haben: kein python app.py vor Abschluss der Befunde, kein öffentliches Pushen, keine Payload.

Selbstbewertungsraster​

  • Der Geltungsbereich besteht aus drei für den Workshop erstellten Python-Dateien (oder einem benannten Trainer-Lab).
  • Eine manuelle Lektüre geht den Tools voraus, mit sechs erkannten Kategorien.
  • sorties/bandit.txt, sorties/semgrep.txt und sorties/gitleaks.txt existieren und sind nicht leer.
  • Die Matrix gleicht die Tools ab: nicht ein Befund pro Alarm, sondern ein Befund pro Schwachstelle.
  • Sechs Befunde: SQLi, Befehl, XSS, Pickle, MD5, Geheimnis — jeweils mit Quelltext-Beweis, Auswirkung, Abhilfemaßnahme, Kriterium für den erneuten Scan.
  • Die Schlüssel AKIAIOSFODNN7EXAMPLE werden als Beispiel qualifiziert, nicht als echtes AWS-Leck dargestellt.
  • Mindestens zwei Patches angewendet, Kopien correctifs/*.avant aufbewahrt.
  • Ein zweiter Scan zeigt das Verschwinden (oder den teilweisen Status) der betroffenen IDs.
  • Es wurde keine Payload an eine Drittmaschine gesendet. Es wurde kein öffentlicher Exploit gestartet.
  • Der Ordner befindet sich nicht auf einem öffentlichen GitHub.

Alles abgehakt? Der Liefergegenstand ist zulässig.


Was normalerweise hakt​

  • Semgrep schlägt offline fehl. Verwenden Sie die bereits heruntergeladenen Konfigurationen, oder Bandit + rg, und vermerken Sie dies. Erfinden Sie keine Ausgabe.
  • Gitleaks sieht nichts. Überprüfen Sie --no-git und dass sich config.py tatsächlich im aktuellen Ordner befindet. Ein gitleaks protect auf einem leeren Repository scannt keine nicht verfolgten Dateien.
  • Bandit schweigt zur XSS. Das ist erwartet. Die XSS ist Ihr Lektüre-/Semgrep-Befund, keine Lücke in Ihrer Arbeit.
  • Versuchung, die Routen zu curlen. Der Workshop ist ein Review. Eine DAST-Bestätigung sprengt den Rahmen und verlässt bei einem falsch gestarteten Flask manchmal 127.0.0.1.
  • Bericht = Zusammenkleben der JSONs. Der Prüfer liest rapport.md. Die JSONs sind Anhänge.

Was Sie aus diesem Workshop mitnehmen​

  • Einen reproduzierbaren Ablauf: Lektüre, drei Tools, Matrix, Befunde, Patch, erneuter Scan.
  • Den Beweis, dass ein Code-Befund ohne das Senden einer Payload Bestand hat.
  • Den Reflex, ein Beispielgeheimnis zu qualifizieren, statt wegen des Schlüssels aus der AWS-Dokumentation Alarm zu schlagen.
  • Einen Ordner, den man in sechs Monaten wieder öffnen und nachprüfen kann.