适用场景
Linux Capabilities 提权防护是主机加固中容易被跳过的一环。很多管理员清理了 SUID 文件、收紧了 sudo 配置,却忽略了文件 capabilities 这条等价的特权通道。setcap 写入的权限存储在文件的扩展属性中,常规的”查找 SUID”扫描命令完全查不到它。
以下场景需要做 capabilities 审计与收敛:
- 服务器已完成 SUID 清理,但合规基线仍要求覆盖全部特权通道
- 运行需要绑定低端口、原始套接字、挂载能力的自研服务
- 使用 Docker / containerd 部署容器,需校验容器实际持有的 capability 集
- 应急响应中排查攻击者植入的持久化后门(
cap_setuid+cap_setgid可让普通进程任意切换 UID)
本文给出盘点、收敛、迁移到 systemd 托管、容器加固、变更监控五个步骤的完整可复制方案。
前置条件
- Linux 内核 2.6.24 及以上(capabilities 正式可用),
CONFIG_SECURITY_FILE_CAPABILITIES已开启 - 已安装 libcap 工具集:
getcap、setcap、capsh(CentOS 对应包名libcap,Debian/Ubuntu 为libcap2-bin) - 具备 root 权限或
CAP_SETFCAP - 文件系统支持扩展属性,且挂载时未使用
nouser_xattr之类会剥离 xattr 的选项 - 测试环境可用于验证服务在移除 capability 后的行为
# 安装工具(按发行版择一)
yum install -y libcap attr # RHEL / CentOS / 麒麟
apt-get install -y libcap2-bin attr # Debian / Ubuntu
原理说明
传统 Unix 权限模型是二元的:进程要么是 root(全部权限),要么是普通用户(几乎没有特权)。SUID 位把可执行文件临时提权到文件属主,粒度极粗且难以审计。Capabilities 把 root 的权限拆成约 40 个独立的特权位,进程可以只持有其中若干项。
关键概念:
- File capabilities:存储在文件的
security.capability扩展属性里,由setcap写入。任何用户执行该文件时,进程会获得相应特权位。这就是”替代 SUID 的提权通道”。 - Process capabilities:进程当前持有的集合,可通过
/proc/<pid>/status的CapEff读取,十六进制位掩码格式。 - Bounding set:进程能获取的特权上限。普通用户的 bounding set 通常是全集,因此文件 capability 可以生效;通过
capsh --drop或 systemd 的CapabilityBoundingSet可以缩小它,这是防提权的关键手段。
需要重点关注的高危 capability:
cap_sys_admin:几乎等于 root,可挂载文件系统、操作内核模块参数。授予其任意可执行文件都是严重配置错误。cap_setuid/cap_setgid:任意切换进程身份,等价于无条件提权。cap_dac_read_search:绕过文件的读权限与目录搜索权限检查,可读取/etc/shadow。cap_sys_ptrace:注入并操纵其他进程内存,可窃取凭据。cap_net_raw:原始套接字,可抓包与构造任意报文。cap_sys_module:加载内核模块,直接内核态执行。
还需要注意一个常见误区:文件 capability 不随文件复制而可靠保留。用 cp -a 或 rsync -X 可以带过去,用普通 cp、tar(不带 --xattrs)或从 ZIP 包解压则不会。这既是安全特性(解压漏洞无法直接带来特权文件),也导致恢复备份后服务异常。
操作步骤
1. 全盘盘点现有文件 capabilities
# 递归扫描全盘,忽略 proc/sys 等伪文件系统
getcap -r / 2>/dev/null
# 只输出到文件便于比对(作为基线快照)
getcap -r / 2>/dev/null | sort > /root/cap-baseline-$(date +%F).txt
wc -l /root/cap-baseline-$(date +%F).txt
典型输出形如 /usr/bin/ping cap_net_raw=ep,其中 =ep 表示 effective 与 permitted 位同时置位,即”执行时立即生效”。
2. 逐项判定并移除不必要的能力
原则是能移除就移除。系统自带的 ping、traceroute 若业务确实需要普通用户使用,可保留 cap_net_raw;若服务器为纯 Web 角色、普通用户不需要 ping,可直接清除:
# 移除单个文件的全部 capability
setcap -r /usr/bin/ping
# 只保留必要的子集(示例:仅保留 net_bind_service)
setcap 'cap_net_bind_service=+ep' /usr/local/bin/myservice
# 校验结果
getcap /usr/local/bin/myservice
# 批量清除高危授权,输出被处理清单
getcap -r / 2>/dev/null | grep -Ei 'cap_sys_admin|cap_setuid|cap_setgid|cap_sys_ptrace|cap_sys_module' \
| awk '{print $1}' | tee /root/cap-risky.txt \
| while read -r f; do setcap -r "$f"; done
处理前建议先确认 /root/cap-risky.txt 中的路径是否属于第三方安全产品(部分 EDR、监控代理会使用 capability),误删会导致其静默失效。
3. 用 systemd 托管替代文件 capability
直接给二进制文件打 capability 的问题是:任何用户执行该文件都会获得特权,且升级覆盖后配置丢失。推荐改为让 systemd 在启动服务时授予,并同时收窄 bounding set:
# /etc/systemd/system/myservice.service
[Unit]
Description=My privileged service
After=network.target
[Service]
Type=simple
User=svcuser
Group=svcuser
ExecStart=/usr/local/bin/myservice
# 仅授予绑定低端口所需的最小能力
AmbientCapabilities=CAP_NET_BIND_SERVICE
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
# 禁止进程再获得任何新特权(防提权的核心开关)
NoNewPrivileges=yes
# 配合文件系统与命名空间隔离
PrivateTmp=yes
ProtectSystem=strict
ProtectHome=yes
ProtectKernelModules=yes
ProtectKernelTunables=yes
RestrictNamespaces=yes
ReadWritePaths=/var/lib/myservice
[Install]
WantedBy=multi-user.target
systemctl daemon-reload
systemctl restart myservice
# 读回实际生效的能力集
systemctl show myservice -p CapabilityBoundingSet -p AmbientCapabilities -p NoNewPrivileges
cat /proc/$(systemctl show -p MainPID --value myservice)/status | grep -E 'CapEff|CapBnd|NoNewPrivs'
NoNewPrivileges=yes 会在内核层面阻止 execve 时获得任何新特权,等价于设置 prctl(PR_SET_NO_NEW_PRIVS)。开启后,即使进程被诱导执行了带 SUID 位或带文件 capability 的二进制,也无法提权。这一项对防提权价值最高,且对绝大多数服务无副作用。
4. 容器侧的 capability 收敛
Docker 默认就给容器约 14 项 capability,其中包含 cap_net_raw、cap_chown、cap_setuid 等。生产容器应改为白名单模式:
# 丢弃全部,只加回真正需要的
docker run -d --name web \
--cap-drop=ALL \
--cap-add=NET_BIND_SERVICE \
--security-opt=no-new-privileges:true \
--user 1000:1000 \
nginx:stable
# 校验实际能力集,应为空或仅含 net_bind_service
docker inspect web --format '{{json .HostConfig.CapDrop}} {{json .HostConfig.CapAdd}}'
docker exec web grep CapEff /proc/1/status
使用 Compose 或 Kubernetes 时同样配置,Kubernetes 中对应:
securityContext:
runAsNonRoot: true
runAsUser: 1000
allowPrivilegeEscalation: false # 对应 no-new-privileges
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
add: ["NET_BIND_SERVICE"]
5. 建立变更监控
攻击者若通过文件 capability 做持久化,落地动作必然调用 setcap 或直接写 xattr。用 auditd 同时覆盖两个入口:
# /etc/audit/rules.d/cap.rules
# 监控 setcap/setfcap 二进制执行
-a always,exit -F path=/usr/sbin/setcap -F perm=x -F auid>=1000 -F auid!=4294967295 -k cap_change
-a always,exit -F path=/usr/sbin/setfcap -F perm=x -F auid>=1000 -F auid!=4294967295 -k cap_change
# 监控对扩展属性的写操作(绕过命令直接写 xattr 的情况)
-a always,exit -F arch=b64 -S setxattr -S lsetxattr -S fsetxattr -F auid>=1000 -F auid!=4294967295 -k xattr_change
# 监控可疑的高危 capability 文件名变更
-w /usr/local/bin/ -p wa -k bin_change
augenrules --load
auditctl -l | grep -E 'cap_change|xattr_change'
# 定期基线比对(建议加入 crontab 每日执行)
getcap -r / 2>/dev/null | sort > /var/log/cap-now.txt
diff /root/cap-baseline-*.txt /var/log/cap-now.txt && echo "OK: no capability change" \
|| logger -t capwatch "WARN: file capabilities changed"
配置验证
1. 验证特权确实被移除
# 在普通用户下执行,期望 Permission denied
sudo -u nobody /usr/bin/ping -c1 127.0.0.1 ; echo "exit=$?"
# 确认该文件不再带 capability
getcap /usr/bin/ping # 期望无输出
2. 验证 NoNewPrivileges 生效
# 以一个带 SUID 的文件做测试目标,普通用户在服务沙箱内不应获得 root
systemctl show myservice -p NoNewPrivileges # 期望 yes
grep NoNewPrivs /proc/$(systemctl show -p MainPID --value myservice)/status
3. 验证容器能力集
# 输入为 8 位十六进制,全 0 表示无任何特权
docker exec web grep CapEff /proc/1/status
# 可读化解析
docker exec web capsh --decode=$(docker exec web grep CapEff /proc/1/status | awk '{print $2}')
验证通过的标准:cap_chown、cap_setuid、cap_setgid、cap_net_raw、cap_sys_admin 均不出现在解码结果中。
4. 验证审计规则触发
setcap 'cap_net_bind_service=+ep' /tmp/testcap ; rm -f /tmp/testcap
ausearch -k cap_change -ts recent | tail -20 # 期望看到上述动作记录
常见问题
FAQ 1:移除了 cap_net_raw 之后普通用户无法使用 ping,怎么兼顾安全与运维?
三个方案,按推荐度排序:一是仅对运维组放行,使用 setcap 'cap_net_raw=+ep' /usr/bin/ping 保留文件能力,同时确认 /usr/bin/ping 属主为 root、且普通用户组外不可写,风险可控;二是改用无特权探测工具,如 curl 或 tcping 做端口连通性检测,绕开 ICMP 需求;三是在堡垒机侧统一提供探测能力,生产节点不放行任何普通用户的网络探测。若服务器为纯 Web 角色且无运维交互,直接移除是更彻底的选择。
FAQ 2:清除了某个服务的 capability 后服务启动失败,如何快速回滚?
先确认错误类型。bind() failed: Permission denied 说明缺少 cap_net_bind_service,此时不要立刻把文件 capability 加回去,正确做法是在 systemd unit 中通过 AmbientCapabilities=CAP_NET_BIND_SERVICE 授予,作用域限定在该服务而非全局。若服务以非 root 用户运行且需要绑定 80 端口,还可用 sysctl -w net.ipv4.ip_unprivileged_port_start=80 放开低端口限制,这比授予 capability 更细粒度。回滚期间可临时执行 setcap 'cap_net_bind_service=+ep' /path/to/bin 恢复服务,但必须记录并在后续替换为 systemd 方案。
FAQ 3:容器里丢弃了 capability,为什么从容器内看 CapEff 仍非全 0?
Docker 的 --cap-drop=ALL 清空的是”额外授予集”,但容器内 PID 1 仍可能因 --privileged、root 用户默认集或运行时保留项而持有部分能力。排查顺序:先确认启动命令确实包含 --cap-drop=ALL(docker inspect 的 HostConfig.CapDrop 字段),再确认未使用 --privileged;随后检查是否以 root 运行(CapEff 会继承),改用 --user 1000:1000;最后核对是否由 Compose/K8s 覆盖了配置。需要强调的是:只要容器内进程是 root 且允许 privilege escalation,丢弃 capability 的收益会大幅缩水,因此 allowPrivilegeEscalation: false 与 runAsNonRoot: true 必须同时开启。
总结
Linux Capabilities 是 SUID 之外的独立特权通道,配置不当会成为提权与权限维持的入口。落地要点:
- 盘点:
getcap -r /建立基线,任何新增都可被 diff 发现。 - 收敛:清除
cap_sys_admin、cap_setuid、cap_sys_ptrace等高危授予,只保留业务必需项。 - 迁移:从”给文件打 capability”改为 systemd
AmbientCapabilities+NoNewPrivileges=yes,作用域更小、可审计。 - 容器:
--cap-drop=ALL配合非 root 运行与allowPrivilegeEscalation: false。 - 监控:auditd 覆盖
setcap执行与 xattr 写入两条路径。
其中 NoNewPrivileges 的投入产出比最高:一行配置即可阻断整条”借 SUID / capability 提权”的链条。