James Cao
WannaGame Championship CTF 2025 longtrip Web Writeup

WannaGame Championship CTF 2025 longtrip Web Writeup

Room / Challenge: longtrip (Web) Metadata Author: jameskaois CTF: WannaGame Championship CTF 2025 Challenge: longtrip (web) Target / URL: https://ctf.cnsc.com.vn/games/1/challenges?challenge=24 Points: 947 Solved: 6 Date: 10-12-2025 Goal Enjoying the longtrip and get the flag. My Solution The challenge comes with no source code, visit the home page: Doing some enumeration steps with dirsearch, found /login page, login with admin:1234 as in description, found a XML Parser tool, tried using the template to see how it works: ...

December 16, 2025 · 6 min
WannaGame Championship CTF 2025 CTFguideline Web Writeup

WannaGame Championship CTF 2025 CTFguideline Web Writeup

Room / Challenge: CTFguideline (Web) Metadata Author: jameskaois CTF: WannaGame Championship CTF 2025 Challenge: CTFguideline (web) Target / URL: https://ctf.cnsc.com.vn/games/1/challenges?challenge=23 Points: 967 Solved: 5 Date: 09-12-2025 Goal Leveraging the Race Condition XSS in the home page, submit to the bot to get the flag. My Solution Visit the home page and view the page source we should find the script.js which is the main script for the page: /* ============================================================ Load config ============================================================ */ var config; fetch('config.json') .then(resp => resp.json()) .then(async resp => { config = resp; document.body.style.backgroundColor = config.background; document.body.style.color = config.text_color; await loadContentsList(config); if (location.hash.length > 0) (config.skin = 'Pane'), initPane(config); else { const url = new URL(location.href); if (url.searchParams.get('doc')) (config.skin = 'Modern'), initModern(config); else (config.skin = 'Default'), initDefault(config); } }); if (location.hash.length > 0) { sanitizeHash(); window.onhashchange = () => loadPaneContent(config); } /* ============================================================ Build navigation list ============================================================ */ async function loadContentsList(config) { const nav = document.getElementById('nav'); nav.innerHTML = ''; config.contents.forEach(file => { const name = file.replace('.html', ''); nav.innerHTML += ` <li><a href="#" class="nav-item" data-file="${file}">${name}</a></li> `; }); attachNavHandlers(config); } /* ============================================================ Navigation Handlers ============================================================ */ function attachNavHandlers(config) { document.querySelectorAll('.nav-item').forEach(item => { item.addEventListener('click', e => { e.preventDefault(); const file = item.getAttribute('data-file'); if (config.skin === 'Pane') { location.hash = file; // Pane uses hash } else if (config.skin === 'Default') { loadDefault(file); } else if (config.skin === 'Modern') { const url = new URL(location.href); url.searchParams.set('doc', file); location.href = url.toString(); } }); }); } /* ============================================================ Pane Theme — 3 Panel View ============================================================ */ function initPane(config) { hideAllViewers(); document.body.classList.add('pane'); document.getElementById('viewerLeft').style.display = 'block'; document.getElementById('viewerCenter').style.display = 'block'; document.getElementById('viewerRight').style.display = 'block'; loadPaneContent(config); } function sanitizeHash() { currentHash = location.hash.substring(1); if (currentHash) { var decodedHash = decodeURIComponent(currentHash); var sanitizedHash = decodedHash.replace(/(javascript:|data:|[<>])/gi, ''); if (decodedHash != sanitizedHash) { document.location.hash = encodeURI(sanitizedHash); } } } function loadPaneContent(config) { sanitizeHash(); let file = location.hash.substring(1) || config.default; if (file) { document.getElementById( 'viewerLeft', ).innerHTML = `<h2>Overview</h2><p>You selected: ${encodeURIComponent(file)}</p>`; url = new URL(location.href); if (config) { if (!config.load_remote) { file = 'contents/' + file; } else { if (!file.startsWith('http://') && !file.startsWith('https://')) file = url.origin + url.pathname + '/contents' + file; } } document.getElementById('viewerCenter').contentWindow.location.replace(decodeURI(file)); document.getElementById('viewerRight').innerHTML = `<h2>Extras</h2><p>Copyright.</p>`; } } /* ============================================================ Default Theme ============================================================ */ function initDefault(config) { hideAllViewers(); document.body.classList.add('default'); document.getElementById('viewer').style.display = 'block'; loadDefault(config.default); } async function loadDefault(file) { const text = await fetch('contents/' + file).then(r => r.text()); document.getElementById('viewer').innerHTML = text; } /* ============================================================ Modern Theme ============================================================ */ function initModern(config) { hideAllViewers(); document.body.classList.add('modern'); document.getElementById('viewer').style.display = 'block'; const params = new URLSearchParams(location.search); const doc = params.get('doc') || config.default; loadModern(doc); } async function loadModern(file) { const text = await fetch('contents/' + file).then(r => r.text()); document.getElementById('viewer').innerHTML = `<div class="modern-card">${text}</div>`; } /* ============================================================ Helpers ============================================================ */ function hideAllViewers() { document.querySelectorAll('#viewer, .pane-panel').forEach(el => { el.style.display = 'none'; }); } Also, from this we can find the config.json: ...

December 16, 2025 · 4 min
WannaGame Championship CTF 2025 Trust Web Writeup

WannaGame Championship CTF 2025 Trust Web Writeup

Room / Challenge: Trust (Web) Metadata Author: jameskaois CTF: WannaGame Championship CTF 2025 Challenge: Trust (web) Target / URL: https://ctf.cnsc.com.vn/games/1/challenges?challenge=20 Points: 500 Solved: 61 Date: 10-12-2025 Goal Bypassing the vulnerability chain with 3 main vulnerabilities to get the flag. My Solution This app is vulnerable with a vulnerability chain: # Vulnerability Purpose 1 CVE-2025-23419 Bypassing the client certificate of hidden service 2 Signature Bypass Bypass the signature check to upload exploit plugin 3 Zip Slip Achieve RCE to read the flag CVE-2025-23419 SSL Session Reuse This vulnerability can be easily noticed by the comment in the nginx.conf: ...

December 16, 2025 · 5 min
DreamHack - spring-view

DreamHack - spring-view Web Challenge Writeup

Room / Challenge: spring-view (Web) Metadata Author: jameskaois CTF: DreamHack Challenge: spring-view (web) Link: https://dreamhack.io/wargame/challenges/99 Level: 4 Date: 28-11-2025 Goal Decompile the app.jar and leveraging SSTI to get the flag. My Solution Using Java Decompiler to decompile the app.jar we received this source code In UserController.class is where we have to examine to find the vulnerability: public class UserController { Logger log = LoggerFactory.getLogger(com.dreamhack.spring.UserController.class); @GetMapping({"/"}) public String index(@RequestParam(value = "lang", required = false) String lang, Model model, HttpServletRequest request, HttpServletResponse response) { if (lang != null) { response.addCookie(new Cookie("lang", lang)); return "redirect:/"; } Cookie cookie_lang = WebUtils.getCookie(request, "lang"); if (cookie_lang == null) response.addCookie(new Cookie("lang", "en")); model.addAttribute("message", "Spring World !"); return "index"; } @GetMapping({"/welcome"}) public String welcome(@CookieValue(value = "lang", defaultValue = "en") String lang) { return lang + "/welcome"; } @GetMapping({"/signup"}) public String signup(@CookieValue(value = "lang", defaultValue = "en") String lang) { return lang + "/underconstruction"; } @GetMapping({"/signin"}) public String signin(@CookieValue(value = "lang", defaultValue = "en") String lang) { return lang + "/underconstruction"; } } We can see here all three routes /welcome, /signup and /signin all used the cookie lang value to render the template, here we can think of SSTI vulnerability. ...

November 28, 2025 · 2 min
DreamHack - dreamschool

DreamHack - dreamschool Web Challenge Writeup

Room / Challenge: dreamschool (Web) Metadata Author: jameskaois CTF: DreamHack Challenge: dreamschool (web) Link: https://dreamhack.io/wargame/challenges/259 Level: 7 Date: 28-11-2025 Goal Examining the code, leveraging SSTI, brute-forcing the secret board id and get the flag. My Solution The app is a complicated one, but it is vulnerable to SSTI related to error handling I suggest you examining the code and force the app returns the config of the app. From this you can get 2 important variables AUTH_PUBLIC_KEY and FLAG_SCHOOL. You will take this and create the JWT, since the app is using PyJWT==1.7.1, this library JWT is vulnerable to algorithm confusion, so we can force the server to decode a token using HS256. With this code: ...

November 28, 2025 · 2 min
DreamHack - KeyCat

DreamHack - KeyCat Web Challenge Writeup

Room / Challenge: KeyCat (Web) Metadata Author: jameskaois CTF: DreamHack Challenge: KeyCat (web) Link: https://dreamhack.io/wargame/challenges/905 Level: 4 Date: 27-11-2025 Goal Leveraging the vulnerablilities to get 2 parts of the flag. My Solution Examining the code we can find 2 routes to get the flag which is /cat/flag and /cat/admin: router.get('/flag', Auth, (req, res) => { if (req.filename !== undefined && req.filename.indexOf(FLAG_FILE_NAME) !== -1) { return res.status(200).send(`🙀🙀🙀🙀🙀🙀 ${FLAG_CONTENT_1}`); } else { return res.status(401).render('error', { img_path: '/img/error.png', err_msg: 'Unauthorized...', }); } }); router.get('/admin', Auth, (req, res) => { if (req.username !== undefined && req.username === 'cat_master') { return res.status(200).send(`Hello Cat Master😸 this is for you ${FLAG_CONTENT_2}`); } else { return res.status(403).send("Hello dreamhack! But I've got nothing you want"); } }); Now, let’s focus on getting the first part of the flag. It will check the req.filename with the variable FLAG_FILE_NAME, if we open the entrypoint.sh we should see how FLAG_FILE_NAME is created: ...

November 27, 2025 · 6 min
DreamHack - Flask-Dev

DreamHack - Flask-Dev Web Challenge Writeup

Room / Challenge: Flask-Dev (Web) Metadata Author: jameskaois CTF: DreamHack Challenge: Flask-Dev (web) Link: https://dreamhack.io/wargame/challenges/74 Level: 4 Date: 27-11-2025 Goal Leveraging debugging mode in production to get the flag. My Solution The app.py is short and simple: #!/usr/bin/python3 from flask import Flask import os app = Flask(__name__) app.secret_key = os.urandom(32) @app.route('/') def index(): return 'Hello !' @app.route('/<path:file>') def file(file): return open(file).read() app.run(host='0.0.0.0', port=8000, threaded=True, debug=True) The Dockerfile tells us that the /flag is an executable C script: RUN gcc /app/flag.c -o /flag \ && chmod 111 /flag && rm /app/flag.c Since the app is vulnerable in this route: ...

November 27, 2025 · 3 min
DreamHack - 거북이

DreamHack - 거북이 Web Challenge Writeup

Room / Challenge: 거북이 (Web) Metadata Author: jameskaois CTF: DreamHack Challenge: 거북이 (web) Link: https://dreamhack.io/wargame/challenges/2194 Level: 2 Date: 25-11-2025 Goal Leveraging Zip Slip vulnerability and get the flag. My Solution The app is vulnerable to Zip Slip vulnerability, the vulnerable block of code in /upload: if (f.filename or "").lower().endswith(".zip") or "zip" in (f.content_type or "").lower(): data = f.read() zf = zipfile.ZipFile(io.BytesIO(data)) names = [] for info in zf.infolist(): target = UPLOAD_DIR / info.filename # <--- VULNERABLE CODE HERE if info.is_dir(): target.mkdir(parents=True, exist_ok=True) else: target.parent.mkdir(parents=True, exist_ok=True) with zf.open(info) as src, open(target, "wb") as dst: shutil.copyfileobj(src, dst) names.append(info.filename) return jsonify(ok=True, saved=names) The app doesn’t have any filetering or sanitizing so if we upload with a file with name ../templates/test.html, it will replace the test.html in templates folder with our file, then since this is a Flask app, we can use SSTI to get the flag content. ...

November 25, 2025 · 2 min
DreamHack - I wish A grade

DreamHack - I wish A grade Web Challenge Writeup

Room / Challenge: I wish A grade (Web) Metadata Author: jameskaois CTF: DreamHack Challenge: I wish A grade (web) Link: https://dreamhack.io/wargame/challenges/2380 Level: 2 Date: 25-11-2025 Goal Follow the hints, leverage SQL Injection to get the flag. My Solution Based on the readme.txt got we can tried logging in as 202511037 to see what we got. After some time examining the website, I found a hidden announcements which is in /announce/1: ...

November 25, 2025 · 2 min
DreamHack - I LOVE XSS!

DreamHack - I LOVE XSS! Web Challenge Writeup

Room / Challenge: I LOVE XSS! (Web) Metadata Author: jameskaois CTF: DreamHack Challenge: I LOVE XSS! (web) Link: https://dreamhack.io/wargame/challenges/2061 Level: 2 Date: 25-11-2025 Goal Leveraging XSS Scriptin to get the flag. My Solution The app has a banned list for XSS Scripting: banlist = ["`","'","alert(","fetch(","replace(","[","]","javascript","@","!","%","location","href","window","eval"] Also in sanitizer.py it do allow <script> tag however no any attributes is allowed: import bleach ALLOWED_TAGS = ['script'] #I only love script tags! ALLOWED_ATTRIBUTES = {} ALLOWED_PROTOCOLS = ['http', 'https'] def sanitize_input(user_input: str) -> str: return bleach.clean( user_input, tags=ALLOWED_TAGS, attributes=ALLOWED_ATTRIBUTES, protocols=ALLOWED_PROTOCOLS, strip=True, strip_comments=True ) Initially, I create a payload: ...

November 25, 2025 · 1 min