James Cao
DreamHack - weblog-1

DreamHack - weblog-1 Web Challenge Write-up

Room / Challenge: weblog-1 (Web) Metadata Author: jameskaois CTF: DreamHack Challenge: weblog-1 (web) Link: https://dreamhack.io/wargame/challenges/71 Level: 2 Date: 14-11-2025 Goal Examining the source code and access.log to answer questions then get the flag. My Solution This challenge is a web challenge with a mix with forensics challenge. First question: Please enter the password for the admin account that was stolen by the attacker. In the access.log scroll down we can see a bunch of requests of blind SQL Injection: ...

November 14, 2025 · 3 min
DreamHack - Client Side Template Injection

DreamHack - Client Side Template Injection Web Challenge Write-up

Room / Challenge: Client Side Template Injection (Web) Metadata Author: jameskaois CTF: DreamHack Challenge: Client Side Template Injection (web) Link: https://dreamhack.io/wargame/challenges/437 Level: 2 Date: 14-11-2025 Goal Bypassing CSP rules and get the flag with XSS. My Solution This challenge is similar to CSP Bypass and DOM XSS, however the CSP policy is different: @app.after_request def add_header(response): global nonce response.headers['Content-Security-Policy'] = f"default-src 'self'; img-src https://dreamhack.io; style-src 'self' 'unsafe-inline'; script-src 'nonce-{nonce}' 'unsafe-eval' https://ajax.googleapis.com; object-src 'none'" nonce = os.urandom(16).hex() return response The app accepts script from https://ajax.googleapis.com, this is a huge security vulnerability we can check it in CSP Evaluator ...

November 14, 2025 · 1 min
DreamHack - file-csp-1

DreamHack - file-csp-1 Web Challenge Write-up

Room / Challenge: file-csp-1 (Web) Metadata Author: jameskaois CTF: DreamHack Challenge: file-csp-1 (web) Link: https://dreamhack.io/wargame/challenges/36 Level: 2 Date: 14-11-2025 Goal Crafted the correct CSP satisfying the needs to get the flag. My Solution There are 3 routes in this challenge /test, /live and /verify. The /verify is the route we need to satisfy to get the flag: @APP.route('/verify', methods=['GET', 'POST']) def verify_csp(): global CSP if request.method == 'POST': csp = request.form.get('csp') try: options = webdriver.ChromeOptions() for _ in ['headless', 'window-size=1920x1080', 'disable-gpu', 'no-sandbox', 'disable-dev-shm-usage']: options.add_argument(_) driver = webdriver.Chrome('/chromedriver', options=options) driver.implicitly_wait(3) driver.set_page_load_timeout(3) driver.get(f'http://localhost:8000/live?csp={quote(csp)}') try: a = driver.execute_script('return a()'); except: a = 'error' try: b = driver.execute_script('return b()'); except: b = 'error' try: c = driver.execute_script('return c()'); except Exception as e: c = 'error' c = e try: d = driver.execute_script('return $(document)'); except: d = 'error' if a == 'error' and b == 'error' and c == 'c' and d != 'error': return FLAG return f'Try again!, {a}, {b}, {c}, {d}' except Exception as e: return f'An error occured!, {e}' return render_template('verify.html') The /test and /live is where we can use to test our payloads. The app requirements is we have to crafted the correct CSP policy which satisfy the needs in the csp.html: ...

November 14, 2025 · 3 min
DreamHack - web-deserialize-python

DreamHack - web-deserialize-python Web Challenge Write-up

Room / Challenge: web-deserialize-python (Web) Metadata Author: jameskaois CTF: DreamHack Challenge: web-deserialize-python (web) Link: https://dreamhack.io/wargame/challenges/40 Level: 2 Date: 13-11-2025 Goal Leveraging insecure deserialization to retrieve the flag. My Solution The vulnerability is in the /check-session route: @app.route('/check_session', methods=['GET', 'POST']) def check_session(): if request.method == 'GET': return render_template('check_session.html') elif request.method == 'POST': session = request.form.get('session', '') info = pickle.loads(base64.b64decode(session)) return render_template('check_session.html', info=info) The server will loads whatever we pass to the session data: info = pickle.loads(base64.b64decode(session)) The vulnerability here is the pickle. Therefore, we can create a malicious payload through Python: ...

November 13, 2025 · 1 min
DreamHack - baby-sqlite

DreamHack - baby-sqlite Web Challenge Write-up

Room / Challenge: baby-sqlite (Web) Metadata Author: jameskaois CTF: DreamHack Challenge: baby-sqlite (web) Link: https://dreamhack.io/wargame/challenges/1 Level: 2 Date: 13-11-2025 Goal Leveraging SQL Injection to bypass the check and get the flag. My Solution The app has the /login route: @app.route('/login', methods=['GET', 'POST']) def login(): if request.method == 'GET': return render_template('login.html') uid = request.form.get('uid', '').lower() upw = request.form.get('upw', '').lower() level = request.form.get('level', '9').lower() sqli_filter = ['[', ']', ',', 'admin', 'select', '\'', '"', '\t', '\n', '\r', '\x08', '\x09', '\x00', '\x0b', '\x0d', ' '] for x in sqli_filter: if uid.find(x) != -1: return 'No Hack!' if upw.find(x) != -1: return 'No Hack!' if level.find(x) != -1: return 'No Hack!' with app.app_context(): conn = get_db() query = f"SELECT uid FROM users WHERE uid='{uid}' and upw='{upw}' and level={level};" try: req = conn.execute(query) result = req.fetchone() if result is not None: uid = result[0] if uid == 'admin': return FLAG except: return 'Error!' return 'Good!' So here we have to login as admin to get the flag: ...

November 13, 2025 · 2 min
DreamHack - sql injection bypass waf Advanced

DreamHack - sql injection bypass waf Advanced Web Challenge Write-up

Room / Challenge: sql injection bypass WAF Advanced (Web) Metadata Author: jameskaois CTF: DreamHack Challenge: sql injection bypass WAF Advanced (web) Link: https://dreamhack.io/wargame/challenges/416 Level: 2 Date: 12-11-2025 Goal Bypass WAF and use SQL Injection to get the flag My Solution The app.py has this WAF check: keywords = ['union', 'select', 'from', 'and', 'or', 'admin', ' ', '*', '/', '\n', '\r', '\t', '\x0b', '\x0c', '-', '+'] def check_WAF(data): for keyword in keywords: if keyword in data.lower(): return True return False It is harder for us to make requests that we want to do some injection. This is what it looked like when we are blocked by WAF: ...

November 12, 2025 · 2 min
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