> For the complete documentation index, see [llms.txt](https://0xb0b.gitbook.io/writeups/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://0xb0b.gitbook.io/writeups/tryhackme/2026/fools-mate-revenge.md).

# Fools Mate, Revenge

{% embed url="<https://tryhackme.com/room/foolsm8v2>" %}

The following post by 0xb0b is licensed under [CC BY 4.0<img src="https://mirrors.creativecommons.org/presskit/icons/cc.svg?ref=chooser-v1" alt="" data-size="line"><img src="https://mirrors.creativecommons.org/presskit/icons/by.svg?ref=chooser-v1" alt="" data-size="line">](http://creativecommons.org/licenses/by/4.0/?ref=chooser-v1)

***

## Scenario

I see my client-side defences were no match for you, well done, my apprentice! Let's see if you have what it takes to claim your prize.

You can access the web app from your AttackBox's browser via: `http://IP:3000`

## Summary

<details>

<summary>Summary</summary>

In Fools Mate, Revenge, we almost face the same app but this time the reward gate checks a `session.config.unlocked` flag before releasing the flag. Since `session.config` is a plain object, any property it lacks is inherited through the prototype chain. Testing reveals that `/api/settings` merges user-supplied JSON without sanitizing dangerous keys, allowing a `constructor.prototype` payload to pollute `Object.prototype` globally. By injecting `unlocked: true` this way and then submitting the same checkmate move (`a1` → `a8`) within the same session, `session.config.unlocked` resolves to `true` via the polluted prototype revealing the flag.&#x20;

</details>

## Exploitation

We'll start right away by visiting the page as specified in the scenario. Here we have a chess game like in `Fools Mate` infront of us. We are able to reset postions, make our moves and also save some preferences.

```
http://10.112.187.200:3000/
```

<figure><img src="/files/bL6xqutgJFis1beARFOS" alt=""><figcaption></figcaption></figure>

We're testing the application and examining each request in BurpSuite. We try to change settings...

```
POST /api/settings HTTP/1.1
```

<figure><img src="/files/QOUELnFaWUvOgjLzsm5g" alt=""><figcaption></figcaption></figure>

And repeat our step like in `Fools Mate` to catch a benign move and make it a checkmate move afterwards. We are able to make our checkmate move, but we do not receive a reward this time. The server explicitly states that the reward is blocked by a not set `session.config.unlocked` object.

```
POST /api/move HTTP/1.1
```

<figure><img src="/files/qZTcMarl2fU4K7nJZOGM" alt=""><figcaption></figcaption></figure>

**Before we continue, we reset our positions. As the game has ended.**

{% code overflow="wrap" %}

```
POST /api/reset HTTP/1.1
```

{% endcode %}

Next, we test for prototype pollution on other endpoints, hypothesizing that if any part of the app merges user-supplied JSON into an object without sanitizing dangerous keys (`__proto__`, `constructor`, `prototype`), we could inject properties directly onto `Object.prototype`. Since `session.config` is a plain object, any property it doesn't already define would be inherited from the prototype chain. Meaning we don't need to write to `session.config` directly, only to `Object.prototype` itself.

We find that `/api/settings` is vulnerable. Sending a normal-looking settings update alongside a `constructor.prototype` payload succeeds in polluting the global prototype as we get the reward after making our checkmate move.

```
POST /api/settings HTTP/1.1
```

```
"constructor":{"prototype":{"unlocked":true}}
```

<figure><img src="/files/iQisEBthPJfloUt1bzWm" alt=""><figcaption></figcaption></figure>

After we make our checkmate move with the same session we retrieve the flag.

```
POST /api/move HTTP/1.1
```

<figure><img src="/files/eYyEID2EPtOvb4ZZlnK8" alt=""><figcaption></figcaption></figure>

We recreate the steps taken in BurpSuite using cURL.

First we reset the game session on the server while using a shared cookie file (`-c` to save, `-b` to send) so the session persists into your next curl call.

```
target=10.112.187.200:3000
```

{% code overflow="wrap" %}

```
curl -s -c cookies.txt -b cookies.txt -X POST http://$target/api/reset -H "Content-Length: 0" >/dev/null 
```

{% endcode %}

Next, we send a POST request to `/api/settings` including the prototype-pollution payload `constructor.prototype.unlocked: true`, reusing the same cookie jar so this request lands in the session we just reset. The goal is to pollute `Object.prototype` so that any object without its own `unlocked` property like `session.config` will inherit `unlocked: true` through the prototype chain.

{% code overflow="wrap" %}

```
curl -s -c cookies.txt -b cookies.txt -X POST http://$target/api/settings -H "Content-Type: application/json" -d '{"theme":"forest","pieceSet":"classic","animationMs":180,"constructor":{"prototype":{"unlocked":true}}}'
```

{% endcode %}

Finally, we send the checkmate move (`a1` to `a8`) to `/api/move` using the same cookie jar, so the server checks `session.config.unlocked` on the reward gate. Like tested before in BurpSuite the prototype pollution from the previous step lands and `config.unlocked` now resolves to `true` via the polluted `Object.prototype`. We are now able to make our move and it gets evaluated as unlocked, revealing the flag.

{% code overflow="wrap" %}

```
curl -s -c cookies.txt -b cookies.txt -X POST http://$target/api/move -H "Content-Type: application/json" -d '{"from":"a1","to":"a8"}'
```

{% endcode %}

<figure><img src="/files/tiqy6W5ZlqQcNFUWe8zn" alt=""><figcaption></figcaption></figure>
