SUID 位是 Linux 权限模型里最反直觉的一块:一个普通用户执行某个文件时,能获得文件属主的权限。这是 passwd 能改 /etc/shadow 的原因,也是绝大多数提权漏洞的入口。网上教你「检测 SUID 提权」的文章成千,但几乎没人讲真在运维里该怎么管——既不能让必要的 SUID 程序消失导致系统不工作,又不能让多出来的 SUID 文件变成后门。这篇讲三件事:SUID 到底是什么、怎么盘出一份可信清单、以及出了事怎么处置。
一、SUID/SGID 与三个易混概念
Linux 文件的权限位有 12 位,用 ls -l 看到的是后 9 位加三个特殊位:
ls -l /usr/bin/passwd
# -rwsr-xr-x 1 root root 68208 ... /usr/bin/passwd
# ^ s 在属主执行位上 = SUID
ls -l /usr/bin/wall
# -rwxr-sr-x → SGID,执行时获得属组权限
find /tmp -maxdepth 1 -perm -1000 -ls
# 目录上的 t 位(sticky),只允许属主删除自己的文件| 位 | 数值 | 对文件 | 对目录 |
|---|---|---|---|
| SUID | 4000 | 执行时获得属主权限 | 无意义(会被忽略) |
| SGID | 2000 | 执行时获得属组权限 | 新建文件继承目录属组 |
| Sticky | 1000 | 无意义 | 只能删除自己的文件 |
关键是SUID 只对「可执行文件」有意义。一个 chmod 4755 的文本文件不会自动提权,但如果它是脚本而且解释器处理不当,情况就复杂了——这是后话。
二、盘一份可信的 SUID 清单
不管你打算做什么,第一步一定是「知道现在有什么」。标准命令:
# 全盘扫 SUID 与 SGID,排除 /proc /sys
find / -xdev \( -perm -4000 -o -perm -2000 \) -type f \
-printf '%m %u:%g %p\n' 2>/dev/null | sort -k3
# 只扫 SUID
find / -xdev -perm -4000 -type f -ls 2>/dev/null一台最小化的 Debian 12 上,正常的 SUID 文件大概 15~25 个,通常是这些:
/usr/bin/passwd /usr/bin/chsh
/usr/bin/su /usr/bin/chfn
/usr/bin/newgrp /usr/bin/gpasswd
/usr/bin/mount /usr/bin/umount
/usr/bin/sudo /usr/bin/pkexec
/usr/lib/openssh/ssh-keysign
/usr/lib/dbus-1.0/dbus-daemon-launch-helper
/usr/bin/fusermount3 /usr/bin/crontab
# SGID 常见:
/usr/bin/wall /usr/bin/write /usr/bin/expiry /usr/sbin/unix_chkpwd把这份清单存成一个基线文件,每月比对一次,比装任何扫描器都实在:
mkdir -p /root/baseline
find / -xdev \( -perm -4000 -o -perm -2000 \) -type f \
-printf '%m %u:%g %p\n' 2>/dev/null | sort -k3 \
> /root/baseline/suid-$(date +%Y%m).txt
# 与上月对比
diff /root/baseline/suid-202609.txt /root/baseline/suid-202610.txtdiff 出来有变化就必须解释清楚。新增的如果不是你装的软件带的(dpkg -S /path 能查归属),那就得当安全事件处理。
dpkg -S /usr/bin/pkexec # polkitd: /usr/bin/pkexec
dpkg -S /usr/bin/xxx # 报 no path found matching → 不是包管理器装的,重点怀疑三、五个在真实运维里遇到的 SUID 相关问题
问题 1:/usr/bin/pkexec 是老版本 Polkit 的提权入口
CVE-2021-4034(PwnKit)影响几乎所有主流发行版的 pkexec,利用只需几行代码,本地普通用户直接拿 root。Debian/Ubuntu 早就修了,但**容器镜像和老机器上可能还留着**。检查版本:
dpkg -l policykit-1 | tail -1
pkexec --version
# 如果你不用图形界面/桌面环境,这个文件可以直接去掉 SUID
chmod u-s /usr/bin/pkexec这条路很值得走:服务器上根本不需要 pkexec,留着就是白送一个攻击面。同理还有 /usr/bin/fusermount3(不用用户态文件系统就去掉)、/usr/lib/dbus-1.0/dbus-daemon-launch-helper(不用 D-Bus 就去掉)。
问题 2:脚本文件被 chmod 4755
Linux 内核对脚本的 SUID 是「忽略」的——这是因为内核无法安全地以提权状态启动一个解释器。所以 chmod 4755 script.sh 实际上不会提权,看起来「安全」。但如果有人执行 chmod 4755 /usr/bin/python3 再跑脚本,或者用 perl 的 -s 特性,那就是真的提权了。
所以扫描规则应该是:**任何非二进制文件的 SUID 都属于异常**。
find / -xdev -perm -4000 -type f 2>/dev/null | while read f; do
file -b "$f" | grep -qi 'script\|text' && echo "SUSPECT: $f"
done问题 3:自建服务为了「省事」给了 SUID
站长圈里常见的一幕:某个备份脚本需要读 MySQL 数据目录,写不动,索性 chmod 4755 让普通用户能跑。这等于把那台机器的一部分 root 权限交给了任何能执行这个文件的用户,而 web 目录下的脚本往往所有 www-data 都能执行。
正确替代方案,按推荐度排:
- systemd 服务 + 定时器:以 root 运行,业务代码不需要任何特权。
systemctl start backup.service由 sudoers 白名单允许特定用户触发。 - sudoers 精确授权:
backup_user ALL=(root) NOPASSWD: /usr/local/bin/backup.sh,且脚本必须属主 root、权限 755、不可写。 - 能力(capability):如果只是要绑低端口,
setcap cap_net_bind_service=+ep /path/bin比 SUID 精细一百倍,权限范围只有那一项。 - ACL:如果只是要读某个目录,
setfacl -R -m u:www-data:rX /var/backups/site而不是提权。
# 查看文件的能力位
getcap -r / 2>/dev/null
# /usr/bin/ping cap_net_raw=ep
# 新内核上 SUID 与 capability 可以并存,后者更安全问题 4:备份/同步工具把 SUID 一起复制走了
rsync -a 会保留权限位(包括 SUID)。如果你从一台机器把 /usr 或者某个自编译目录同步到另一台,SUID 会一起带过去。更麻烦的是从**不可信来源**解压 tar 包:
# 危险:直接从 tar 里恢复,等于信任包里的所有权限
tar xzf backup.tar.gz -C /
# 安全:先看清单
tar tzvf backup.tar.gz | grep '^-rws\|^-rwxr-s'
# 或用 --no-same-permissions 恢复
tar xzf backup.tar.gz -C / --no-same-permissions同理 unzip 会保留 Unix 权限位,docker cp 也会。任何「从外部拿文件进系统」的动作之后,都应该跑一次 SUID 扫描。
问题 5:SUID 位「看起来在」但其实不生效
有两种情况:分区以 nosuid 挂载,或者文件所在文件系统不支持。前者很常见——/tmp、/var/tmp、/home 常被挂成 nosuid,nodev,noexec,这是好事。检查:
findmnt -no TARGET,OPTIONS | grep nosuid
# 一个带 SUID 的文件在 nosuid 分区上
ls -l /tmp/x
# -rwsr-xr-x ... /tmp/x
/tmp/x
# 实际以调用者身份运行,不会提权这意味着:**如果你的 web 目录在一个 nosuid 分区,攻击者放进去的 SUID 后门是无效的**。所以我强烈建议把 /tmp 单独挂 nosuid,nodev,noexec,把上传目录也挂上同样的选项(或者用 Nginx 直接对 uploads 里的 PHP 返回 403——这两条配合最稳)。
四、白名单化的正确姿势:按需裁剪而不是全删
盘完清单之后,很多人第一反应是「能删的都删了」。全删是错的——sudo 的 SUID 一掉,你自己就上不了 root;passwd 一掉,所有用户改不了密码。正确的做法是按「要不要」逐项判断,做成一张决策表:
| 文件 | 必需性 | 处置 |
|---|---|---|
| /usr/bin/sudo | 必需 | 保留,但 sudoers 精确到命令 |
| /usr/bin/su | 视情况 | 若只用 sudo + 禁 root 登录,可去掉 SUID |
| /usr/bin/passwd | 必需 | 保留 |
| /usr/bin/crontab | 视情况 | 已用 systemd timer 的话可去掉 |
| /usr/bin/mount / umount | 一般不需要 | 服务器上非 root 挂载很少用,可去掉 |
| /usr/bin/pkexec | 不需要 | 去掉(无桌面环境) |
| /usr/bin/fusermount3 | 不需要 | 去掉(不用 sshfs 等) |
去掉的方式统一用 chmod u-s /path,并把每一处改动连同原因写进上面的基线文件注释里。这样下次做 diff 时,看到「少了 SUID」的条目不会以为是被人动了手脚。
还有一个更彻底的思路:把 SUID 文件的所在分区用 nosuid 挂载。比如自建的软件装到 /opt,单独一块数据盘挂成 nosuid,那里面的任何 SUID 位都不生效。这比逐个文件 chmod u-s 抗「重新安装后又回来了」的折腾——重装软件会恢复原始权限位,但挂载选项不会变。
# /etc/fstab 里加 nosuid
/dev/vdb1 /opt ext4 defaults,nosuid,nodev 0 2
mount -o remount,nosuid /opt
findmnt -no OPTIONS /opt五、出事了怎么处置:四步止血
如果你在 SUID 清单里发现了来路不明的文件,按这个顺序处理,别慌着一把 chmod -R u-s /(会把 sudo 也废掉):
- 保留现场:
cp -a /path/to/suspect /root/evidence/,同时记下stat的时间、own、md5、lsattr看有没有 immutable 位。别直接删。 - 摘权限:
chmod u-s,g-s /path只去掉这两个位,不动其他权限。这样文件的执行能力还在(如果它是正常程序的包装),但提权路断。 - 查根源:
lsof +D /path/to/dir看谁在用,grep -r "chmod.*4[0-7][0-7][0-7]" /var/log /root /home看有没有留下命令历史,journalctl -S -7d | grep -i 'chmod'。 - 查横向:
crontab -l/cat /etc/crontab/authorized_keys/ld.so.preload四处都要看,SUID 提权往往是「已经进来了」的结果,不是入口本身。
六、日常管理的三条硬规矩
折腾这么多年,最后沉淀下来的规矩只有三条:
- 新装软件后立刻跑一次 SUID diff,新增项要么能用
dpkg -S解释,要么就删。 - 能用 capability 就不给 SUID,能用 sudoers 就不给 SUID,能用 systemd 服务就不给 SUID。SUID 是最后的选项,不是省事的选项。
- 上传目录、临时目录一律 nosuid,nodev,noexec,让攻击者就算传上来 SUID 后门也执行不了。这一条成本最低、收益最高。
最后说个容易被忽略的细节:SUID 检查要和「文件完整性」检查分开做。SUID 扫描只能告诉你「这个文件变得能提权了」,但如果攻击者只是替换了 sudo 的二进制文件内容、权限位一点没变,SUID diff 完全看不出来。这时候要靠 debsums 或 AIDE 那类完整性工具,比对的是哈希而不是权限。两套手段管两件事,缺一个都有盲区。
顺带提一句:Linux 上还有个 chattr +i 也能当加固手段(让文件不可修改),但它和 SUID 是两回事,别搞混——+i 是「不能改」,SUID 是「执行时提权」。有人为了「防止 SUID 被删」给某个文件加 +i,结果后面更新软件包时 dpkg 直接失败,又在半夜慌一遍。
最后的心态问题:SUID 检查不是「扫一遍就完事」的活儿,而是「建立基线 + 定期比对」的持续动作。扫描器给你的是一堆路径,真正有价值的是你能说清每一个为什么在那。说不清的那些,就是你要处理的。