20. setuid/setgid安全:特权提升风险,最小权限原则,capability机制,安全执行外部程序
好,咱们今天聊一个老生常谈,但又特别容易翻车的话题——setuid/setgid。
说白了,这就是个“临时借权限”的机制。你一个普通用户,想改密码,但密码文件只有root能写,怎么办?系统就让你临时“变成”root,改完再退回来。setuid就是干这个的。
但问题来了——这个“借权限”的机制,一旦没设计好,就成了后门。我在项目中见过太多次了,一个setuid程序写得糙一点,整个服务器就裸奔了。
20.1 setuid/setgid 的基本原理
先看一个最简单的例子。你写了一个C程序,想让它以root身份运行:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main() {
printf("当前有效用户ID: %d\n", geteuid());
printf("当前实际用户ID: %d\n", getuid());
return 0;
}
编译后,用root执行:
chown root:root myprogram
chmod u+s myprogram # 设置setuid位
这时候,普通用户运行这个程序,geteuid()返回的就是0(root),而不是他自己的UID。
嗯,这里要注意——setuid只影响有效用户ID,不影响实际用户ID。实际用户ID还是那个普通用户。这个区别很关键,后面会用到。
20.2 特权提升的风险
为什么说setuid危险?我举个例子你就明白了。
假设你写了一个setuid程序,功能是让用户读取某个日志文件:
#include <stdio.h>
#include <string.h>
int main(int argc, char *argv[]) {
char cmd[256];
snprintf(cmd, sizeof(cmd), "cat %s", argv[1]);
system(cmd); // 危险!
return 0;
}
这个程序以root权限运行。用户传一个文件名进去,它就用system()去读。
你想想看,如果用户传的是 ; rm -rf / 呢?
这就是典型的命令注入。我在一次安全审计中遇到过类似的代码,当时那个程序是用来备份数据库的,结果因为没做输入校验,攻击者直接删了整库。
20.3 最小权限原则
我个人习惯,写setuid程序时,脑子里始终绷着一根弦:用完权限就扔。
什么意思?就是程序启动后,先完成需要特权的操作,然后立刻把权限降下来。
看代码:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main() {
// 先以root身份打开一个需要特权的文件
FILE *fp = fopen("/etc/shadow", "r");
if (!fp) {
perror("fopen");
return 1;
}
// 读取完关键数据后,立刻降权
if (setuid(getuid()) == -1) {
perror("setuid");
return 1;
}
// 现在程序以普通用户身份运行
// 即使后面有漏洞,攻击者也拿不到root权限
printf("权限已降为普通用户\n");
// ... 后续处理
fclose(fp);
return 0;
}
这里的关键是 setuid(getuid())。它把有效用户ID设回实际用户ID,也就是那个普通用户。一旦降权,就再也升不回去了——除非你保留了capability。
20.4 capability机制
setuid有个大问题:它给的是全部root权限。你只想让程序绑定一个低端口(1024以下),结果它连改密码、删文件的能力都有了。
这就像你只想借把螺丝刀,结果对方把整个工具箱都给你了。
Linux的capability机制就是来解决这个问题的。它把root权限拆成一个个小单元,你可以只给程序需要的那个。
| Capability | 说明 |
|---|---|
| CAP_NET_BIND_SERVICE | 绑定低于1024的端口 |
| CAP_DAC_OVERRIDE | 绕过文件权限检查 |
| CAP_SYS_ADMIN | 系统管理操作(慎用) |
| CAP_SETUID | 允许改变UID |
| CAP_KILL | 允许发送信号给任意进程 |
用法很简单。比如你写了一个HTTP服务,想监听80端口:
# 给程序添加绑定低端口的能力,不需要setuid
setcap cap_net_bind_service=+ep ./myhttpd
# 查看capability
getcap ./myhttpd
# 输出: ./myhttpd = cap_net_bind_service+ep
这样,程序运行时只有绑定低端口的能力,其他root权限一概没有。即使被攻破,攻击者也干不了别的。
我个人强烈建议:能用capability就别用setuid。这是Linux安全机制里最优雅的设计之一。
20.5 安全执行外部程序
有时候,你的setuid程序需要调用外部程序。比如一个监控工具,需要调用df命令查看磁盘使用情况。
这时候要格外小心。我见过一个案例:程序用system("df -h"),结果攻击者修改了PATH环境变量,把df指向了自己的恶意程序。然后以root权限执行了。
正确的做法是:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main() {
// 1. 先降权
if (setuid(getuid()) == -1) {
perror("setuid");
return 1;
}
// 2. 清理环境变量
clearenv();
setenv("PATH", "/usr/bin:/bin", 1);
// 3. 使用绝对路径执行
execl("/usr/bin/df", "df", "-h", NULL);
// 如果execl返回,说明出错了
perror("execl");
return 1;
}
这里做了三件事:
- 降权:先把自己变成普通用户,再执行外部程序
- 清环境:防止攻击者通过环境变量注入恶意代码
- 绝对路径:避免PATH劫持
20.6 知识体系图
下面这张图总结了setuid/setgid安全的核心逻辑:
20.7 避坑指南
最后,分享几个我踩过的坑:
- 别用system():它启动一个shell,shell会继承环境变量和权限。用exec系列函数替代。
- 降权要彻底:setuid()只降有效用户ID,别忘了检查setgid()和补充组。我曾经只降了UID,没降GID,结果程序还能写一个只有root组才能写的目录。
- capability不是万能的:有些老系统不支持。而且capability只在文件系统支持的情况下才生效(比如ext4、xfs)。
- setuid脚本是噩梦:很多系统会忽略shell脚本的setuid位。如果你非要写,用C封装一下。
嗯,setuid/setgid这个话题,说深了可以讲一整天。但核心就一句话:能不用root就不用,用了赶紧扔。你想想看,权限这东西,给出去容易,收回来可就难了。
公众号:蓝海资料掘金营,微信deep3321