WebSockFish picoCTF 2025 Solution

Published: April 2, 2025

Description

The chess bot relays evaluation updates over a WebSocket. Instead of playing perfectly, hijack the sendMessage helper in the browser console and feed the server ridiculous evaluation scores.

Open the site and view-source / open DevTools. In the Network tab filter for WS to see the live WebSocket; in the Sources tab grep the script for sendMessage so you know it's a global function attached to window.

Switch to the DevTools Console tab. Because sendMessage is global, calling sendMessage("ping") from the console sends a frame on the same WebSocket the game uses.

Solution

Want to try it yourself first?

The guided walkthrough reveals hints one step at a time.

Walk me through it
  1. Step 1
    Trigger the mate branch
    Observation
    I noticed the setup step revealed that sendMessage is attached to window as a global, which suggested I could call it directly from the DevTools console to send arbitrary WebSocket frames without any UI involvement.
    Sending sendMessage("mate 1") updates the chat to "drown in 1 moves," proving the server accepts arbitrary client messages.
    bash
    sendMessage("mate 1")
    What didn't work first

    Tried: Intercept and replay the WebSocket handshake using a proxy like Burp Suite to inject messages

    Burp can intercept the HTTP upgrade request but replaying raw WebSocket frames from outside the browser session loses the cookie/origin context needed to authenticate the connection. The server closes the forged connection immediately. Since sendMessage is already exposed as a global on window, calling it from the DevTools console is the direct path - no separate proxy needed.

    Tried: Send the message by typing it into the chat input box on the game page instead of the console

    The chat UI sanitizes or wraps input before sending, so typing 'mate 1' into the visible text box sends it as a chat message payload, not as a raw eval command frame. The server treats that differently and produces no state change. The console call bypasses the UI layer and sends the exact frame format the server's WebSocket handler expects.

    Learn more

    WebSockets provide full-duplex communication channels over a single TCP connection. Unlike HTTP where the client always initiates requests, WebSocket connections allow both the client and server to send messages at any time after the initial handshake. They are commonly used in real-time applications: chat systems, live feeds, multiplayer games, and collaborative editing tools.

    The browser's DevTools WebSocket inspector (in the Network tab, filter for "WS") shows all messages exchanged on open WebSocket connections. You can see both sent (green) and received (red) frames in real time. When a function like sendMessage is assigned to a JavaScript global, calling it from the browser console is identical to the application calling it internally; the server has no way to distinguish the source.

    This is a fundamental principle of client-side trust issues: any JavaScript variable, function, or WebSocket connection accessible from the browser console can be manipulated by the user. Security-critical logic (access control, game state, score validation) must always be enforced server-side. Relying on client-side checks is equivalent to asking users to validate their own inputs, a guarantee that cannot be made. Web challenges and real-world bug patterns covers more business-logic bypasses.

  2. Step 2
    Force a wild eval
    Observation
    I noticed the server responded to sendMessage("mate 1") by trusting the client-supplied move count, which suggested the eval score field was also accepted without validation and that sending an extreme negative value would satisfy any threshold check for the flag.
    The server checks eval < -50000 before handing out the flag, so anything more negative than -50000 wins. Sending -1,000,000,000 is overkill; even -51000 works. Use a value with margin (say -100000) so an off-by-one in your reading of the threshold doesn't cost you the round.
    bash
    sendMessage("eval -100000")
    What didn't work first

    Tried: Send a positive large value like sendMessage("eval 100000") to claim the engine is winning

    The server checks whether eval is less than -50000, meaning a large positive value tells the server the opponent (you) is losing badly - not that you are winning. A positive centipawn score indicates white (the bot) is ahead, so the condition is never triggered and no flag is issued. The threshold requires a deeply negative value to represent the bot's position as hopeless.

    Tried: Send sendMessage("eval -50000") to hit the threshold exactly

    The server's condition is strictly less than -50000, so the boundary value -50000 does not satisfy it and the flag branch is not reached. Using -51000 or lower clears the threshold unambiguously. The step recommends -100000 to give comfortable margin so an off-by-one misread of the threshold does not waste a round.

    Learn more

    Chess engines express position evaluations in centipawns, hundredths of a pawn value. An evaluation of 0 is a dead-level position; +100 means white is ahead by roughly one pawn; +300 means white is ahead by a minor piece. Evaluations beyond ±500 typically indicate a decisive advantage, and beyond ±1000 (10 pawns equivalent) suggest a near-certain win. Values like -1,000,000,000 are physically impossible in real chess analysis.

    The server's failure to validate that the evaluation falls within a reasonable range is a classic input validation vulnerability. Even for data that "should" only come from a trusted source (like a chess engine), the server should verify constraints before acting on them. If the evaluation comes from the client over WebSocket, any user can forge it, as this challenge demonstrates.

    This vulnerability pattern appears in many real-world gaming cheats and fraud schemes: forging score submissions to leaderboard APIs, sending impossible game state updates to multiplayer servers, or manipulating auction bids or financial calculations sent client-to-server. Server-side validation is the only reliable defense; client-side validation is UX polish, not security.

  3. Step 3
    Grab the response
    Observation
    I noticed the previous step triggered a server-side branch with no visible output yet, which suggested the flag would arrive as an inbound WebSocket frame visible either in the chat window or the DevTools Network tab under the WS messages view.
    The flag arrives as an inbound WebSocket frame and the page renders it as a chat message in the game's chat window. If you'd rather not look at the UI, watch the WS frames in DevTools (Network tab, filter WS, click the connection, then the Messages tab) and the picoCTF flag will be visible in the green inbound frame as soon as the server processes your eval.
    Learn more

    The speed of this exploit (a single WebSocket message yields the flag instantly) illustrates how quickly server-side trust of client data can be exploited. There is no need to play a single chess move, bypass authentication, or understand the chess engine. The attack requires only the ability to read JavaScript source (which is always possible in a browser) and call a function from the console.

    The right way to frame this finding is not "business logic bug" but trust boundary violation. The client owns its WebSocket connection completely: it can send any frame at any time, with any payload, regardless of what the page's JavaScript would normally produce. That's a property of how WebSockets work, not a flaw in the client. The server's mistake is treating frames from that channel as if they came from a trusted internal component (the chess engine) when they actually came from an untrusted source (the user's browser, which the user controls). Once you draw the trust boundary at the network edge, the fix becomes obvious: the server must compute its own evaluation from the board state and ignore whatever number the client claims.

    For developers building WebSocket applications: treat every WebSocket message from a client as untrusted input, validate all fields against expected types and ranges, maintain authoritative state server-side (never trust the client's reported state), and use rate limiting to prevent abuse. The same security principles that apply to REST API endpoints apply equally to WebSocket message handlers.

Interactive tools
  • Strings ExtractorPull printable text from any binary, library, or image. ASCII and UTF-16 detection, configurable minimum length, flag-like highlight, no command line needed.

Flag

Reveal flag

picoCTF{c1i3nt_s1d3_w3b_s0ck3t5_e5e7...}

Because the logic runs entirely in the client, you don't need chess knowledge; just forge the WebSocket message.

Key takeaway

WebSocket connections are owned entirely by the browser tab that opens them, meaning any JavaScript global function that sends WebSocket frames can be called directly from the browser console by the user. Servers that accept client-reported values (scores, evaluations, game state) without independently recomputing or range-checking them are vulnerable to trivial manipulation, regardless of the transport protocol. This trust boundary failure is the same class of bug exploited in game score spoofing, auction manipulation, and financial calculation forgery; the fix is always to treat every message arriving at the server as untrusted user input and validate it against authoritative server-side state.

Related reading

Want more picoCTF 2025 writeups?

Useful tools for Web Exploitation

What to try next