Skip to main content

WebSockFish picoCTF 2025 Solution

The client reports its own evaluation over a WebSocket, so send a winning score and the server concedes the flag.

Published: April 2, 2025Updated: August 25, 2026

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.

Web

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 1Trigger the mate branch
    Observation
    sendMessage is attached to window as a global, so the DevTools console can call it directly and send arbitrary WebSocket frames with no UI involved.
    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 upgrade request, but replaying frames from outside the browser session loses the cookie and origin context the connection needs, and the server closes it immediately. sendMessage is already a global, so the console is the direct route.

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

    The chat UI wraps and sanitizes input, so typing into the box sends a chat message payload rather than a raw command frame, and the server handles it differently with no state change. The console call skips that layer and sends the format the 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 frames (tinted green) and received frames (plain background) 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 2Force a wild eval
    Observation
    The server trusts the client-supplied move count without checking it, so the eval score is probably unvalidated too. An extreme negative value should clear whatever threshold guards 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 the eval is below -50000, so a large positive number tells it you are losing badly, not winning. A positive centipawn score means the bot is ahead, the condition never fires, and no flag comes. The threshold wants a deeply negative value, describing the bot's position as hopeless.

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

    The comparison is strictly less than, so -50000 itself does not satisfy it and the branch never runs. Anything below clears it; the step suggests -100000 so an off-by-one reading of the threshold cannot cost you 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 3Grab the response
    Observation
    That triggered a server-side branch with nothing visible yet. The flag arrives as an inbound WebSocket frame, either in the chat window or in the Network tab's 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 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

A WebSocket belongs entirely to the tab that opened it, so any global function that sends frames is callable from the console. A server that accepts client-reported values, scores or evaluations or game state, without recomputing or range-checking them is trivially manipulated whatever the transport. The same trust-boundary failure drives game score spoofing, auction manipulation, and forged financial calculations. Treat every arriving message as untrusted input and validate it against authoritative server-side state.

Related reading

Useful tools for Web Exploitation

Where to go next