|
| 1 | +# OnlyLeveling Documentation |
| 2 | + |
| 3 | +## Table of Contents |
| 4 | + |
| 5 | +1. [Introduction](#1-introduction) |
| 6 | +2. [Architecture Overview](#2-architecture-overview) |
| 7 | +3. [Installation](#3-installation) |
| 8 | + - [3.1. Clone the Repository](#31-clone-the-repository) |
| 9 | + - [3.2. Running the Service](#32-running-the-service) |
| 10 | + - [3.3. Running the Checker](#33-running-the-checker) |
| 11 | +4. [Usage](#4-usage) |
| 12 | +5. [Exploits and Fixes](#5-exploits-and-fixes) |
| 13 | + - [5.1. Directory Traversal in File Access](#51-directory-traversal-in-file-access) |
| 14 | + - [5.2. Collision-Prone Seed Hash for Item IDs](#52-collisionprone-seed-hash-for-item-ids) |
| 15 | + - [5.3. Insecure JWT SECRET_KEY Generation](#53-insecure-jwt-secret_key-generation) |
| 16 | +6. [File Structure](#6-file-structure) |
| 17 | + - [6.1. Checker](#61-checker) |
| 18 | + - [6.2. Service](#62-service) |
| 19 | + |
| 20 | +--- |
| 21 | + |
| 22 | +## 1. Introduction |
| 23 | + |
| 24 | +**OnlyLeveling** is a multiplayer RPG-style web application where players progress by opening lootboxes, creating/playing dungeons, battling others, and leveling up. The system includes a frontend, backend, and a seed generator component used for item ID derivation. |
| 25 | + |
| 26 | +**Main functionalities:** |
| 27 | +- Register & Login |
| 28 | +- Open Lootboxes to obtain Items |
| 29 | +- Create & Play Dungeons |
| 30 | +- Fight other players |
| 31 | +- Gain XP and level up |
| 32 | + |
| 33 | +--- |
| 34 | + |
| 35 | +## 2. Architecture Overview |
| 36 | + |
| 37 | +OnlyLeveling consists of several components: |
| 38 | + |
| 39 | +- **Frontend (Port 2627):** Player-facing UI for authentication, lootboxes, dungeons, PvP, and inventory management. |
| 40 | +- **Backend (Port 2626):** Core API for auth, item management, dungeon logic, battles, and file serving (e.g., `/images/{filename}`). |
| 41 | +- **SeedGen (Port 2628):** Item seed/hash helper used to derive ItemIDs. Note: Since the main functionality for this service is written in OPAL I used Python to handle Connections to this service. |
| 42 | +- **Checker (Port 12627):** External process that interacts with the service to validate behavior and flag availability. |
| 43 | + |
| 44 | +--- |
| 45 | + |
| 46 | +## 3. Installation |
| 47 | + |
| 48 | +### 3.1. Clone the Repository |
| 49 | + |
| 50 | +```bash |
| 51 | +git clone https://github.com/enowars/enowars9-service-only-leveling.git |
| 52 | +cd enowars9-service-only-leveling |
| 53 | +``` |
| 54 | + |
| 55 | +### 3.2. Running the Service |
| 56 | + |
| 57 | +From the project root (see the `service/` folder), start the stack (frontend, backend, seedgen). |
| 58 | + |
| 59 | +```bash |
| 60 | +cd Service |
| 61 | +docker compose up --build -d |
| 62 | +``` |
| 63 | + |
| 64 | +- **Frontend:** http://localhost:2627 |
| 65 | +- **Backend:** http://localhost:2626 |
| 66 | +- **SeedGen:** http://localhost:2628 |
| 67 | + |
| 68 | +### 3.3. Running the Checker |
| 69 | + |
| 70 | +From the `checker/` directory: |
| 71 | + |
| 72 | +```bash |
| 73 | +cd checker |
| 74 | +docker compose up --build -d |
| 75 | +``` |
| 76 | + |
| 77 | +- **Checker:** reachable on port **12627** |
| 78 | + |
| 79 | +--- |
| 80 | + |
| 81 | +## 4. Usage |
| 82 | + |
| 83 | +Once running: |
| 84 | +### Registration and Login |
| 85 | +#### Open the **Frontend** at `http://localhost:2627/` to register/login. |
| 86 | + |
| 87 | + |
| 88 | +#### After Logging in you will see the **Dashboard** with your current Level /XP |
| 89 | + |
| 90 | +### Items Page - Opening a Lootbox and Saving notes |
| 91 | +#### Switch to the "Items"-Page. Use the **"Open Lootbox!"** Button clicking on it to obtain items (max. 2). |
| 92 | + |
| 93 | +#### After Opening a Lootbox you will see your obtained Items here |
| 94 | + |
| 95 | +#### With a click on the "Edit"-icon you will be able to type in and save a note |
| 96 | + |
| 97 | +### Create Dungeon |
| 98 | +#### Switch to the **Create Dungeon Page**. Before creating your own dungeon you also can upload a own image for it |
| 99 | + |
| 100 | +#### After that you can just fill in the required Dungeon information and create you own dungeon |
| 101 | +### Play Dungeons |
| 102 | +#### Switch to the **"Dungeons" Page**. here you will see already existing Dungeons and Dungeons you created your own. To Play a Dungeon simply click on **"Enter Dungeon"**. You will gain XP for successful entered Dungeons. |
| 103 | + |
| 104 | +### Fight other Players |
| 105 | +#### Switch to the **Fight Page**. Here you can simply select your opponent in the Drop-Down-List and click **"Fight!"** to fight them |
| 106 | + |
| 107 | +#### After the Fight you will see if you won the fight, the items used by each player and the amount of XP you've earned. |
| 108 | +#### Having more power than the opponent will lead to an Win and having less will lead to an Loose |
| 109 | + |
| 110 | +### Leaderboard |
| 111 | +#### There is also a Leaderboard where you can see the Top players (Name, Level & Power) |
| 112 | + |
| 113 | +#### Clicking on **"view"** will result on a detailed vie of the respective user. |
| 114 | + |
| 115 | + |
| 116 | +--- |
| 117 | + |
| 118 | +## 5. Exploits and Fixes |
| 119 | +This service contains 2 Flagstores with 3 possible exploit Pathes. |
| 120 | + |
| 121 | +### 5.1 Directory Traversal in File Access |
| 122 | + |
| 123 | +**Issue** |
| 124 | +The route `/images/{filename}` does not adequately validate the `filename` parameter since the method uses **":path"**: |
| 125 | +```bash |
| 126 | +@app.get("/images/{filename:path}") |
| 127 | +``` |
| 128 | +Inputs containing path traversal tokens can break out of the intended directory. |
| 129 | + |
| 130 | +**Flag location** |
| 131 | +A **JPG** in another user’s upload directory contains the flag (steganographically embedded with `steghide`). |
| 132 | + |
| 133 | +**Exploit steps:** |
| 134 | +1. Upload image to have your own directory created |
| 135 | +2. Send crafted request to `/images/%2e%2e/[USERNAME]/stegano.jpg` using directory traversal. |
| 136 | +2. Save `stegano.jpg`. |
| 137 | +3. Extract flag: |
| 138 | + ```bash |
| 139 | + steghide extract -sf [image_path] -xf [flagfile_path] -p |
| 140 | + ``` |
| 141 | + |
| 142 | +**Fix:** Remove ":path" from method, restrict filenames, normalize and check paths or serve files by ID. |
| 143 | + |
| 144 | +**Example-Fix:** |
| 145 | + ```bash |
| 146 | + #service/backend/main.py |
| 147 | + def is_safe_filename(filename): |
| 148 | + if ".." in filename or "/" in filename or "\\" in filename or "\x00" in filename: |
| 149 | + return False |
| 150 | + if not re.match(r'^[\w\-. ]+$', filename): |
| 151 | + return False |
| 152 | + return True |
| 153 | + |
| 154 | +@app.get("/images/{filename}") |
| 155 | +async def get_private_dungeon_image( |
| 156 | + filename: str, |
| 157 | + current_user: schemas.User = Depends(get_current_active_user) |
| 158 | +): |
| 159 | + if not is_safe_filename(filename): |
| 160 | + raise HTTPException(status_code=400, detail="Invalid filename.") |
| 161 | + |
| 162 | + user_dir = os.path.abspath(os.path.join(UPLOADS_DIR, current_user.username)) |
| 163 | + abs_file_path = os.path.abspath(os.path.join(user_dir, filename)) |
| 164 | + real_file_path = os.path.realpath(abs_file_path) |
| 165 | + |
| 166 | + if os.path.commonpath([real_file_path, user_dir]) != user_dir: |
| 167 | + raise HTTPException(status_code=404, detail="Image not found or you do not have permission to access it.") |
| 168 | + |
| 169 | + if not os.path.isfile(real_file_path): |
| 170 | + raise HTTPException(status_code=404, detail="Image not found or you do not have permission to access it.") |
| 171 | + |
| 172 | + return FileResponse( |
| 173 | + real_file_path |
| 174 | + ) |
| 175 | + ``` |
| 176 | +--- |
| 177 | +
|
| 178 | +### 5.2 Collision-Prone Seed Hash for Item IDs |
| 179 | +
|
| 180 | +**Issue** |
| 181 | +Weak arithmetic with small modulo causes collisions. Attackers can choose usernames mapping to same ItemID as target. |
| 182 | +
|
| 183 | +**Flag location** |
| 184 | +Flag in **Item Note** of item with colliding ID. |
| 185 | +
|
| 186 | +**Exploit steps:** |
| 187 | +1. Reverse seed function. |
| 188 | +2. Pick username with matching ItemID. |
| 189 | +3. Login and open Lootbox with respective user |
| 190 | +3. Read flagged Item Note. |
| 191 | +
|
| 192 | +**Fix:** Use a secure UUID/random generator |
| 193 | +
|
| 194 | +**Example-Fix:** |
| 195 | +```bash |
| 196 | +#service/backend/main.py |
| 197 | +random.getrandbits(14) |
| 198 | +``` |
| 199 | +or use "**Random**"-Structure of the Opal lib |
| 200 | +
|
| 201 | +--- |
| 202 | +
|
| 203 | +### 5.3 Insecure JWT SECRET_KEY Generation |
| 204 | +
|
| 205 | +**Issue** |
| 206 | +`SECRET_KEY` is derived from time and truncated to 2 chars. Predictable => JWT forgery. |
| 207 | +
|
| 208 | +**Flag location** |
| 209 | +With forged JWT, attacker logs in as target account to access images/items. |
| 210 | +
|
| 211 | +**Exploit steps:** |
| 212 | +1. Reconstruct `SECRET_KEY`. |
| 213 | +2. Forge JWT for target. |
| 214 | +3. Download secret image / read Item Note. |
| 215 | +
|
| 216 | +**Fix:** Use strong random secret, rotate regularly, harden token validation. |
| 217 | +**Example-Fix:** |
| 218 | +```bash |
| 219 | +#service/backend/generate_secret.sh |
| 220 | +SECRET=$(openssl rand -hex 32) |
| 221 | +``` |
| 222 | +--- |
| 223 | +
|
| 224 | +## 6. File Structure |
| 225 | +
|
| 226 | +### 6.1 Checker |
| 227 | +
|
| 228 | +``` |
| 229 | +checker |
| 230 | +├── docker-compose.yaml # Docker compose file |
| 231 | +└── src # Checker source code |
| 232 | + ├── checker.py # Main checker script |
| 233 | +``` |
| 234 | +
|
| 235 | +### 6.2 Service |
| 236 | +
|
| 237 | +``` |
| 238 | +Service |
| 239 | +├── backend/ # Core API |
| 240 | + ├── main.py # Contains Routes and Logic |
| 241 | +├── cleanup/ # Maintenance Service (Cleanup DB) |
| 242 | +├── frontend/ # Web UI for user interaction |
| 243 | +└── seed/ # SeedGen for item ID and JWT Secret derivation |
| 244 | +``` |
0 commit comments