James Cao
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
DreamHack - Test site

DreamHack - Test site Web Challenge Writeup

Room / Challenge: Test site (Web) Metadata Author: jameskaois CTF: DreamHack Challenge: Test site (web) Link: https://dreamhack.io/wargame/challenges/2064 Level: 2 Date: 25-11-2025 Goal My Solution To get the flag in /flag we have to be the admin to get access to /admin however we’re still not the admin now, and the setcookie and readcookie is hidden: # def setcookie(id_str): # The code for this function is hidden # def readcookie(cookie_str): # The code for this function is hidden We can just blind testing to get admin role. There is a vulnerable in /logintest route: ...

November 25, 2025 · 2 min
DreamHack - Special Letter Translator

DreamHack - Special Letter Translator Web Challenge Writeup

Goal Gaining privileges and leveraging RCE to get the flag. My Solution Based on the description and the source code the first thing we have to do is gain VIP role, the SECRET_KEY in app.py: SECRET_KEY = "SECRET_KEY" Try logged in as guest with password guest check this is the wrong secret key, then we have to brute-force the secret key in order to create our own JWT token, my brute-force Python script: ...

November 24, 2025 · 2 min
DreamHack - Black-Hacker-Company

DreamHack - Black-Hacker-Company Web Challenge Write-up

Room / Challenge: Black-Hacker-Company (Web) Metadata Author: jameskaois CTF: DreamHack Challenge: Black-Hacker-Company (web) Link: https://dreamhack.io/wargame/challenges/1834 Level: 2 Date: 22-11-2025 Goal Bypass filter and leveraging SSRF to get the flag. My Solution The app.py is simple with three routes: @app.route('/user-page', methods=['GET']) def user(): url = request.args.get('url') if not url: return jsonify({"swap": "Write URL"}), 400 if not any(allow in url for allow in ALLOW_HOST): return jsonify({"swap": "URL not allowed"}), 403 for bad in BLACK_LIST: if bad in url.lower(): return jsonify({"swap": "BAN LIST"}), 403 try: result = subprocess.run( ["curl", "-s", url], text=True, capture_output=True, check=True ) return jsonify({"response": result.stdout}) except subprocess.CalledProcessError as e: return jsonify({"swap": "Error!~", "details": str(e)}), 500 @app.route('/access-token', methods=['GET']) def admin(): if request.remote_addr in ["127.0.0.1"]: password = request.args.get("password") if password: if password == PASSWORD: return jsonify({"server": TOKEN}), 200 else: return jsonify({"server": "Nop~ Password Wrong><"}), 403 else: return jsonify({"server": "Write Password!"}), 400 else: return jsonify({"server": "Only Localhost Can Access : )"}), 403 @app.route('/admin', methods=['GET']) def check(): if request.args.get("token") == TOKEN: return "<h1>dotori-company : $#@&*(@#&*(@)) BeePPP.. </h1>" + FLAG else: return jsonify({"server": "you are not admin..."}), 403 The /access-token will check our given password and if correct it will returned token which will be used to get the flag in /admin. However, /access-token just accepts request from 127.0.0.1 IP so we have to use the /user-page. The filter: ...

November 22, 2025 · 2 min