# Project Sekai CTF 2022 ## Game start ```php= <?php include('./flag.php'); getenv('flag'); class Sekai_Game{ public $start = True; public function __destruct(){ if($this->start === True){ $result = "Sekai Game Start Here is your flag " . getenv('flag'); echo $result; } } public function __wakeup(){ $this->start=False; } } if(isset($_GET['sekai_game.run'])){ unserialize($_GET['sekai_game.run']); }else{ highlight_file(__FILE__); } ?> ``` Một bài unserialize php, flag sẽ được in ra nếu ta chạy xong `__destruct()`, tuy nhiên có vẻ khó vì sau khi `__destruct()` chạy xong thì `__wakeup()` sẽ thực thi ngay và đổi cái `$start` kia thành False -> không in được flag Tìm thử hướng làm thì được bài này [https://bugs.php.net/bug.php?id=81151](https://bugs.php.net/bug.php?id=81151), sau khi đã có serialize của object nào đó, thay vì để O đầu tiên mà dùng C và không gán hay chạy bất kì thứ gì trong object, nó sẽ bỏ qua method `__wakeup()`. Dựa vào ý tưởng này mình làm thử thôi. ![](https://i.imgur.com/HMLUsAQ.png) Đổi lại thành `C:10:"Sekai_Game":0:{}` ![](https://i.imgur.com/EfY7DJE.png) Ủa, không có gì xảy ra ??? Sau khi debug thử bằng việc mình print hẳn cái _GET ra thì nhận biết được vấn đề như sau ![](https://i.imgur.com/VCJzLdK.png) Dấu . trong param của ta đã bị đổi sang dấu _ nên nó không unserialize cũng phải thôi, mình đi tìm hiểu vấn đề này thì được bài người ta giải thích [https://stackoverflow.com/questions/33126289/php-get-url-param-with-dot-in-name](https://stackoverflow.com/questions/33126289/php-get-url-param-with-dot-in-name) ![](https://i.imgur.com/U35e6p7.png) Đại khái là php sẽ đổi những kí tự như trên thành gạch dưới khi kiểm tra tên biến. Vậy tới đây mình cố gắng làm sao bypass được chỗ nó đổi là được, khi check trong source php thì thấy [https://github.com/php/php-src/blob/master/main/php_variables.c](https://github.com/php/php-src/blob/master/main/php_variables.c) ![](https://i.imgur.com/WM3WdTR.png) Đoạn code kiểm tra kí tự space hay các kí tự khác trong tên biến, ở dưới ta để ý nó có kiểm tra kí tự `[` nếu xuất hiện đầu tiên thì sẽ break luôn -> ta chỉ cần đổi dấu gạch dưới trong param của ta thành dấu `[`, để khi thông qua bước kiểm tra nó sẽ thành định dạng ta mong muốn ``` sekai[game.run -> sekai_game.run ``` Test thử nè ![](https://i.imgur.com/MiiyTe1.png) unserialize thành công, áp dụng thêm payload ở trên là được phờ lác ![](https://i.imgur.com/IoOhz3G.png) # issues Bài này có source khá dài nhưng cũng không khó để hiểu luồng. ```python= from flask import Flask, request, session, url_for, redirect, render_template, Response import secrets from api import api from werkzeug.exceptions import HTTPException app = Flask(__name__, template_folder=".") app.secret_key = secrets.token_bytes() jwks_file = open("jwks.json", "r") jwks_contents = jwks_file.read() jwks_file.close() app.register_blueprint(api) @app.after_request def after_request_callback(response: Response): # your code here print(response.__dict__) if response.headers["Content-Type"].startswith("text/html"): updated = render_template("template.html", status=response.status_code, message=response.response[0].decode()) response.set_data(updated) return response @app.errorhandler(Exception) def handle_exception(e): if isinstance(e, HTTPException): return e return str(e), 500 @app.route("/", defaults={"path": ""}) @app.route("/<path:path>") def home(path): return "OK", 200 return render_template("template.html", status=200, message="OK") @app.route("/login", methods=['GET', 'POST']) def login(): return "Not Implemented", 501 return render_template("template.html", status=501, message="Not Implemented"), 501 @app.route("/.well-known/jwks.json") def jwks(): return jwks_contents, 200, {'Content-Type': 'application/json'} @app.route("/logout") def logout(): session.clear() redirect_uri = request.args.get('redirect', url_for('home')) return redirect(redirect_uri) ``` Trên đây là app.py, ta thấy với hầu hết route chỉ return về 501 Not Implemented thôi, chỉ có `/.well-known/jwks.json` là trả về content của file đấy, đây là file chứa public key. Còn `/logout` sẽ redirect tới parameter get là redirect. Tiếp theo là tới api.py ```python= from flask import Blueprint, request from urllib.parse import urlparse import os import jwt import requests api = Blueprint("api", __name__, url_prefix="/api") valid_issuer_domain = os.getenv("HOST") valid_algo = "RS256" def get_public_key_url(token): is_valid_issuer = lambda issuer: urlparse(issuer).netloc == valid_issuer_domain header = jwt.get_unverified_header(token) if "issuer" not in header: raise Exception("issuer not found in JWT header") token_issuer = header["issuer"] if not is_valid_issuer(token_issuer): raise Exception( "Invalid issuer netloc: {issuer}. Should be: {valid_issuer}".format( issuer=urlparse(token_issuer).netloc, valid_issuer=valid_issuer_domain ) ) pubkey_url = "{host}/.well-known/jwks.json".format(host=token_issuer) return pubkey_url def get_public_key(url): resp = requests.get(url) resp = resp.json() key = resp["keys"][0]["x5c"][0] return key def has_valid_alg(token): header = jwt.get_unverified_header(token) algo = header["alg"] return algo == valid_algo def authorize_request(token): pubkey_url = get_public_key_url(token) if has_valid_alg(token) is False: raise Exception("Invalid algorithm. Only {valid_algo} allowed.".format(valid_algo=valid_algo)) pubkey = get_public_key(pubkey_url) pubkey = "-----BEGIN PUBLIC KEY-----\n{pubkey}\n-----END PUBLIC KEY-----".format(pubkey=pubkey).encode() decoded_token = jwt.decode(token, pubkey, algorithms=["RS256"]) if "user" not in decoded_token: raise Exception("user claim missing") if decoded_token["user"] == "admin": return True return False @api.before_request def authorize(): if "Authorization" not in request.headers: raise Exception("No Authorization header found") authz_header = request.headers["Authorization"].split(" ") if len(authz_header) < 2: raise Exception("Bearer token not found") token = authz_header[1] if not authorize_request(token): return "Authorization failed" f = open("flag.txt") secret_flag = f.read() f.close() @api.route("/flag") def flag(): return secret_flag ``` File này sẽ xử lí các route /api/<j j đó>. Lần lượt phân tích các hàm quan trọng. Tại `get_public_key_url` và `get_public_key`, nó sẽ lấy header của jwt tại key `issuer`, và truy cập đến <host của issuer>/.well-known/jwks.json để lấy public key, trong bài này ngụ ý của coder muốn truy cập đến file mà ta đã phân tích ở trên để lấy key. Mà nghĩ kĩ một xíu, sao phải lấy key từ việc request đến url nhỉ? Cộng thêm việc cái route /logout ở trên ta hoàn toàn có thể spoof biến redirect để redirect đến bất kì url nào. Tới đây chắc ta cũng đã hình thành được ý tưởng tấn công mà lát nữa sẽ nói sau, giờ sẽ phân tích tiếp. `has_valid_alg()` và `authorize_request()` sẽ kiểm tra token của ta, nếu header có key alg là RS256 cũng như gán public key lấy được ở trên thành một key hoàn chỉnh. Sau đó decode token ra và kiểm tra payload nếu key user là admin thì sẽ trả về True. Với hàm `authorize()`, nó sẽ kiểm tra http header Authorization của request và check theo dạng Bearer. Mục đích là thay vì kiểm tra token jwt dưới dạng cookie thì nó kiểm tra theo header này. Sau khi kiểm tra nếu authorized thì ta sẽ đọc được phờ lác. Theo phân tích ở trên, tác giả phải get url đến để đọc được key, quá là phức tạp, cộng thêm việc route /logout ta hoàn toàn có thể redirect đến url ta mong muốn, vậy how about ta forge một jwt token xài pair key của ta tự tạo mà ở đó key `issuer` sẽ trỏ về host của ta, trên host của ta sẽ chứa thư mục `.well_known/jwks.json` trong đó sẽ chứa public key mà ta tự tạo. Khi web xử lí qua `api.py` sẽ nhận public key đó và decode token jwt mà ta đã forge -> valid signature -> user sẽ thành admin -> đọc phờ lác. Tiến hành thôi, ta sẽ dùng jwt.io và chỉnh lại token có header và payload theo mong muốn, còn public key và private key web tự tạo cho ta luôn nên không cần quan tâm ![](https://i.imgur.com/g4hnLwZ.png) ![](https://i.imgur.com/agioUUp.png)