James Cao
DreamHack - login-1

DreamHack - login-1 Web Challenge Write-up

Room / Challenge: login-1 (Web) Metadata Author: jameskaois CTF: DreamHack Challenge: login-1 (web) Link: https://dreamhack.io/wargame/challenges/47 Level: 2 Date: 10-11-2025 Goal Login as the account with admin rights to get the flag. My Solution The app.py has one route that we have to focus on /reset-password: @app.route('/forgot_password', methods=['GET', 'POST']) def forgot_password(): if request.method == 'GET': return render_template('forgot.html') else: userid = request.form.get("userid") newpassword = request.form.get("newpassword") backupCode = request.form.get("backupCode", type=int) conn = get_db() cur = conn.cursor() user = cur.execute('SELECT * FROM user WHERE id = ?', (userid,)).fetchone() if user: # security for brute force Attack. time.sleep(1) if user['resetCount'] == MAXRESETCOUNT: return "<script>alert('reset Count Exceed.');history.back(-1);</script>" if user['backupCode'] == backupCode: newbackupCode = makeBackupcode() updateSQL = "UPDATE user set pw = ?, backupCode = ?, resetCount = 0 where idx = ?" cur.execute(updateSQL, (hashlib.sha256(newpassword.encode()).hexdigest(), newbackupCode, str(user['idx']))) msg = f"<b>Password Change Success.</b><br/>New BackupCode : {newbackupCode}" else: updateSQL = "UPDATE user set resetCount = resetCount+1 where idx = ?" cur.execute(updateSQL, (str(user['idx']))) msg = f"Wrong BackupCode !<br/><b>Left Count : </b> {(MAXRESETCOUNT-1)-user['resetCount']}" conn.commit() return render_template("index.html", msg=msg) return "<script>alert('User Not Found.');history.back(-1);</script>"; Firstly, this code may appears safely without exploitation can be made however the time.sleep(1) is the key where Race Condition come in play. There are 100 possible backup code from 0 -> 100. Therefore, it can be brute-forced. In order to bypass the the max reset count check we can make 100 requests at a time with different backup code. From this we can change the password. ...

November 10, 2025 · 2 min
DreamHack - funjs

DreamHack - funjs Web Challenge Write-up

Room / Challenge: funjs (Web) Metadata Author: jameskaois CTF: DreamHack Challenge: funjs (web) Link: https://dreamhack.io/wargame/challenges/116 Level: 2 Date: 10-11-2025 Goal Examining the index.html and solving the flag. My Solution The web app is the index.html. I will comment the moveBox function to test more comfortably: function init() { box = document.getElementById("formbox"); {{ /* setInterval(moveBox,1000); */ }} } We have to submit the correct flag in order to get the flag, if not we will received NOP!: ...

November 10, 2025 · 2 min
DreamHack - blind-command

DreamHack - blind-command Web Challenge Write-up

Room / Challenge: blind-command (Web) Metadata Author: jameskaois CTF: DreamHack Challenge: blind-command (web) Link: https://dreamhack.io/wargame/challenges/73 Level: 2 Date: 10-11-2025 Goal My Solution The app is simple with just one app.py: #!/usr/bin/env python3 from flask import Flask, request import os app = Flask(__name__) @app.route('/' , methods=['GET']) def index(): cmd = request.args.get('cmd', '') if not cmd: return "?cmd=[cmd]" if request.method == 'GET': '' else: os.system(cmd) return cmd app.run(host='0.0.0.0', port=8000) It is only one route that we can leverage. It needs us to have cmd arguments which is executed by os.system(cmd) however there is a check: ...

November 10, 2025 · 1 min
DreamHack - web-ssrf

DreamHack - web-ssrf Web Challenge Write-up

Room / Challenge: web-ssrf (Web) Metadata Author: jameskaois CTF: DreamHack Challenge: web-ssrf (web) Link: https://dreamhack.io/wargame/challenges/75 Level: 2 Date: 10-11-2025 Goal Using image viewer service to capture the flag. My Solution You can download and examine the source code here The source code has one main app.py which is the file runs the application, there is a route that we have to pay attention to /img_viewer: @app.route("/img_viewer", methods=["GET", "POST"]) def img_viewer(): if request.method == "GET": return render_template("img_viewer.html") elif request.method == "POST": url = request.form.get("url", "") urlp = urlparse(url) if url[0] == "/": url = "http://localhost:8000" + url elif ("localhost" in urlp.netloc) or ("127.0.0.1" in urlp.netloc): data = open("error.png", "rb").read() img = base64.b64encode(data).decode("utf8") return render_template("img_viewer.html", img=img) try: data = requests.get(url, timeout=3).content img = base64.b64encode(data).decode("utf8") except: data = open("error.png", "rb").read() img = base64.b64encode(data).decode("utf8") return render_template("img_viewer.html", img=img) Firstly, I tried several payloads to get the flag.txt content like /flag.txt, /static/../flag.txt, … however all of them returns this base64 encoded: ...

November 10, 2025 · 2 min
DreamHack - Mango

DreamHack - Mango Web Challenge Write-up

Room / Challenge: Mango (Web) Metadata Author: jameskaois CTF: DreamHack Challenge: Mango (web) Link: https://dreamhack.io/wargame/challenges/90 Level: 2 Date: 07-11-2025 Goal Get the flag by leveraging blind NoSQL Injection. My Solution You can download and examine the source code here. The web app just have one main.js file to examine: const express = require('express'); const app = express(); const mongoose = require('mongoose'); mongoose.connect('mongodb://localhost/main', { useNewUrlParser: true, useUnifiedTopology: true, }); const db = mongoose.connection; // flag is in db, {'uid': 'admin', 'upw': 'DH{32alphanumeric}'} const BAN = ['admin', 'dh', 'admi']; filter = function (data) { const dump = JSON.stringify(data).toLowerCase(); var flag = false; BAN.forEach(function (word) { if (dump.indexOf(word) != -1) flag = true; }); return flag; }; app.get('/login', function (req, res) { if (filter(req.query)) { res.send('filter'); return; } const { uid, upw } = req.query; db.collection('user').findOne( { uid: uid, upw: upw, }, function (err, result) { if (err) { res.send('err'); } else if (result) { res.send(result['uid']); } else { res.send('undefined'); } }, ); }); app.get('/', function (req, res) { res.send('/login?uid=guest&upw=guest'); }); app.listen(8000, '0.0.0.0'); We have to leverage /login route to get the flag. There is a filter function that prevents us from use admin, dh and admi in the route. For example, when we visit /login?uid=admin&upw=DH{ received filter: ...

November 8, 2025 · 2 min
QnQSec CTF - Secure Letter

QnQSec CTF - Secure Letter Writeup

Room / Challenge: Secure-Letter (Web) Metadata Author: jameskaois CTF: QnQSec CTF 2025 Challenge: Secure-Letter (web) Target / URL: http://161.97.155.116:3001/ Points: 50 Date: 20-10-2025 Goal We have to get the flag by using XSS to get the flag from bot. My Solution This solution is written after the server has beed shut down, so I will use my mind. First let’s examine the source code, there is a route that we can use to inject Javascript code (XSS): /letter route ...

October 27, 2025 · 2 min
QnQSec CTF - s3cr3ct w3b Revenge

QnQSec CTF - s3cr3ct w3b revenge Writeup

Room / Challenge: s3cr3ct_w3b revenge (Web) Metadata Author: jameskaois CTF: QnQSec CTF 2025 Challenge: s3cr3ct_w3b revenge (web) Target / URL: http://161.97.155.116:8088/ Points: 50 Date: 20-10-2025 Goal We have to get the flag by leveraging XML viewer. My Solution Examine the source code, the source code is written in PHP however examine the Dockerfile, unlike s3cre3ct_web the DockerFile now is different: FROM php:8.2-apache RUN docker-php-ext-install pdo pdo_mysql RUN a2enmod rewrite COPY public/ /var/www/html/ RUN mkdir -p /var/flags && chown www-data:www-data /var/flags COPY flag.txt /var/flags/flag.txt WORKDIR /var/www/html/ EXPOSE 80 The flag.txt file is copied to /var/flags/flag.txt so we cannot access it like the s3cre3ct_web challenge anymore. ...

October 27, 2025 · 1 min
QnQSec CTF - s3cr3ct w3b

QnQSec CTF - s3cr3ct w3b Writeup

Room / Challenge: s3cr3ct_w3b (Web) Metadata Author: jameskaois CTF: QnQSec CTF 2025 Challenge: s3cr3ct_w3b (web) Target / URL: http://161.97.155.116:8081/ Points: 50 Date: 20-10-2025 Goal We have to get the flag by finding the secret. My Solution Examine the source code, the source code is written in PHP however examine the Dockerfile, we can find something really “secret”: FROM php:8.2-apache RUN docker-php-ext-install pdo pdo_mysql RUN a2enmod rewrite COPY public/ /var/www/html/ COPY includes/ /var/www/html/includes/ COPY flag.txt /var/www/html/ WORKDIR /var/www/html/ EXPOSE 80 The flag.txt file is copied to /var/www/html where it is normally served. So we can easily get the flag by visiting http://161.97.155.116:8081/flag.txt. ...

October 27, 2025 · 1 min
QnQSec CTF - QnQSec Portal

QnQSec CTF - QnQSec Portal Writeup

Room / Challenge: QnQSec Portal (Web) Metadata Author: jameskaois CTF: QnQSec CTF 2025 Challenge: QnQSec Portal (web) Target / URL: http://161.97.155.116:5001/ Points: 50 Date: 20-10-2025 Goal We have to get the flag by get access as admin. My Solution First we have to examine the app.py. There are some noticable routes: /login route: @app.route('/login', methods=['GET', 'POST']) def login(): if request.method == 'GET': return render_template('login.html') username = (request.form.get('username') or '').strip() password = request.form.get('password') or '' if not username or not password: flash('Missing username or password', 'error') return render_template('login.html') db = get_db() row = db.execute( 'select username, password from users where username = lower(?) and password = ?', (username, md5(password.encode()).hexdigest()) ).fetchone() if row: session['user'] = username.title() role = "admin" if username.lower() == "flag" else "user" token = generate_jwt(session['user'],role,app.config['JWT_EXPIRES_MIN'],app.config['JWT_SECRET']) resp = make_response(redirect(url_for('account'))) resp.set_cookie("admin_jwt", token, httponly=False, samesite="Lax") return resp flash('Invalid username or password', 'error') return render_template('login.html') /account route: ...

October 27, 2025 · 4 min
QnQSec CTF - A Easy Web

QnQSec CTF - A Easy Web Writeup

Room / Challenge: A Easy Web (Web) Metadata Author: jameskaois CTF: QnQSec CTF 2025 Challenge: A Easy Web (web) Target / URL: http://161.97.155.116:5000/ Points: 50 Date: 20-10-2025 Goal We have to get the flag by guessing the UID to gain access as admin. My Solution This is an easy challenge however we need to do some guessing and hope for luck. The description of the challenge is: This is the web I mad for testing but I don’t know if there anything strange can you help me figure out? We need to find something strange in the website to leverage it and gain access as admin. Let’s visit the page: ...

October 27, 2025 · 2 min