SUID提权面盘点:基线清单与按需裁剪加固

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),只允许属主删除自己的文件
位数值对文件对目录
SUID4000执行时获得属主权限无意义(会被忽略)
SGID2000执行时获得属组权限新建文件继承目录属组
Sticky1000无意义只能删除自己的文件

关键是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.txt

diff 出来有变化就必须解释清楚。新增的如果不是你装的软件带的(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 都能执行。

正确替代方案,按推荐度排:

  1. systemd 服务 + 定时器:以 root 运行,业务代码不需要任何特权。systemctl start backup.service 由 sudoers 白名单允许特定用户触发。
  2. sudoers 精确授权:backup_user ALL=(root) NOPASSWD: /usr/local/bin/backup.sh,且脚本必须属主 root、权限 755、不可写。
  3. 能力(capability):如果只是要绑低端口,setcap cap_net_bind_service=+ep /path/bin 比 SUID 精细一百倍,权限范围只有那一项。
  4. 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 也废掉):

  1. 保留现场:cp -a /path/to/suspect /root/evidence/,同时记下 stat 的时间、own、md5、lsattr 看有没有 immutable 位。别直接删。
  2. 摘权限:chmod u-s,g-s /path 只去掉这两个位,不动其他权限。这样文件的执行能力还在(如果它是正常程序的包装),但提权路断。
  3. 查根源: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'。
  4. 查横向:crontab -l / cat /etc/crontab / authorized_keys / ld.so.preload 四处都要看,SUID 提权往往是「已经进来了」的结果,不是入口本身。

六、日常管理的三条硬规矩

折腾这么多年,最后沉淀下来的规矩只有三条:

  1. 新装软件后立刻跑一次 SUID diff,新增项要么能用 dpkg -S 解释,要么就删。
  2. 能用 capability 就不给 SUID,能用 sudoers 就不给 SUID,能用 systemd 服务就不给 SUID。SUID 是最后的选项,不是省事的选项。
  3. 上传目录、临时目录一律 nosuid,nodev,noexec,让攻击者就算传上来 SUID 后门也执行不了。这一条成本最低、收益最高。

最后说个容易被忽略的细节:SUID 检查要和「文件完整性」检查分开做。SUID 扫描只能告诉你「这个文件变得能提权了」,但如果攻击者只是替换了 sudo 的二进制文件内容、权限位一点没变,SUID diff 完全看不出来。这时候要靠 debsums 或 AIDE 那类完整性工具,比对的是哈希而不是权限。两套手段管两件事,缺一个都有盲区。

顺带提一句:Linux 上还有个 chattr +i 也能当加固手段(让文件不可修改),但它和 SUID 是两回事,别搞混——+i 是「不能改」,SUID 是「执行时提权」。有人为了「防止 SUID 被删」给某个文件加 +i,结果后面更新软件包时 dpkg 直接失败,又在半夜慌一遍。

最后的心态问题:SUID 检查不是「扫一遍就完事」的活儿,而是「建立基线 + 定期比对」的持续动作。扫描器给你的是一堆路径,真正有价值的是你能说清每一个为什么在那。说不清的那些,就是你要处理的。

Last modification:October 22nd, 2026 at 09:26 pm

Leave a Comment