1052 字
5 分钟
Linux SUID与shell脚本问题
2025-06-24
2025-07-14

1. uid与SUID#

1.1. uid#

Linux使用uid标识用户,每个用户有一个唯一的uidrootuid为0,其他可登录用户的uid一般从1000开始,使用id指令可以查看自己的uid

实际上一个进程运行中主要有3个uid,分别是real uidsaved uideffective uidreal uid用于表示进程的实际调用者,saved uid用于在切换用户时保存uideffective uid则是真正进行访问控制的uid,所有权限都是基于effective uid的,这才是实际有用的uid

默认情况下三个uid相同,uid0(root)的用户可以随意更改三个uid,而普通用户则只能把effective uid更改为另外两个uid之一

1.2. SUID#

SetUID,SUIDlinux中的特殊权限位,用在所有者的执行位,表示为s,如果所有者没有执行权限则表示为S,数字表示为4000,即对应第12个权限位

带有SUID位的二进制可执行文件,在执行时euid会被替换为所有者的euid,即以所有者的身份执行

如以下代码

#include<stdlib.h>
#include<unistd.h>
#include<sys/wait.h>

int main()
{
        pid_t pid;
        pid = fork();
        if(pid==-1) exit(-1);
        if(pid)
                waitpid(pid,NULL,0);
        else
                execlp("id","id",NULL);
        return 0;
}

编译之后将拥有者改为root,直接运行,得到

uid=1000(ztsubaki) gid=1000(ztsubaki) groups=1000(ztsubaki),4(adm),20(dialout),24(cdrom),25(floppy),27(sudo),29(audio),30(dip),44(video),46(plugdev),100(users),107(netdev)

当我们为其增加SUID

 sudo chmod u+s ./a.out

其表示为

-rwsr-xr-x 1 root     root       16048 Jul 14 20:31 a.out

再次运行程序,输出为

uid=1000(ztsubaki) gid=1000(ztsubaki) euid=0(root) groups=1000(ztsubaki),4(adm),20(dialout),24(cdrom),25(floppy),27(sudo),29(audio),30(dip),44(video),46(plugdev),100(users),107(netdev)

可见后者新增了euid=0(root),表示euidreal uid不同,为root

对于SUID的作用,最常见的还是给予运行者有限的root权限,如passwd命令允许普通用户修改自己的密码,这就必须更改保存密码用的shadow文件,改文件肯定不可能开放所有人的读写权限,实际上该文件权限为-rw-r-----,此时就必须使用SUIDroot的身份运行passwd才能修改密码,同时passwd命令对应的可执行程序普通用户无法轻易更改,也确保了无法修改别人的密码

可以查看passwd的权限位

ls -l /usr/bin/passwd
-rwsr-xr-x 1 root root 64152 May 30  2024 /usr/bin/passwd

类似地,对于gid,也有egidSGIDSGID位于第11位,即2000,记为写在组执行权限上的sS,作用与SUID基本一致

2. SUID与脚本#

上文提到,SUID只能作用于二进制可执行文件,无法作用于脚本,这是因为脚本的解释器与脚本本身分离,不受脚本权限的保护,若解释器权限较弱,通过替换解释器就可以执行任意别的内容,如果此时SUID生效就比较危险,故linux不允许脚本使用SUID,为脚本设置的SUID将会失效

如果在知道风险之后依然有使用脚本的需求,我们可能会很自然想到在二进制可执行程序中调用脚本,比如下面的程序

#include<stdlib.h>
#include<unistd.h>
#include<sys/wait.h>

int main()
{
        pid_t pid;
        pid = fork();
        if(pid==-1) exit(-1);
        if(pid)
                waitpid(pid,NULL,0);
        else
                execlp("sh","sh","./t.sh",NULL);
        return 0;
}

脚本如下

id

输出如下

uid=1000(ztsubaki) gid=1000(ztsubaki) groups=1000(ztsubaki),4(adm),20(dialout),24(cdrom),25(floppy),27(sudo),29(audio),30(dip),44(video),46(plugdev),100(users),107(netdev)

并没有出现预期结果

经过分析,我们可以在shman文件中找到一句话

-p priviliged    Do not attempt to reset effective uid if it does not match uid. This is not set by default to help avoid incorrect usage by setuid root programs via system(3) or popen(3).

可见,sh实际上默认会在real uideffective uid不同时替换掉euid,因此,我们在系统调用中加入-p参数,即

                execlp("sh","sh","-p","./t.sh",NULL);

这样就可以得到预期结果

CAUTION

无论如何请务必谨慎使用该方法

最后,我们一直通过forkexec产生子进程测试,如果直接使用system

使用如下代码

#include<stdlib.h>
#include<unistd.h>
#include<sys/wait.h>

int main()
{
        pid_t pid;
        system("id");
        pid = fork();
        if(pid==-1) exit(-1);
        if(pid)
                waitpid(pid,NULL,0);
        else
                execlp("id","id",NULL);
        return 0;
}

更改所有者,设置SUID后运行得到

uid=1000(ztsubaki) gid=1000(ztsubaki) groups=1000(ztsubaki),4(adm),20(dialout),24(cdrom),25(floppy),27(sudo),29(audio),30(dip),44(video),46(plugdev),100(users),107(netdev)
uid=1000(ztsubaki) gid=1000(ztsubaki) euid=0(root) groups=1000(ztsubaki),4(adm),20(dialout),24(cdrom),25(floppy),27(sudo),29(audio),30(dip),44(video),46(plugdev),100(users),107(netdev)

发现system调用中SUID还是没有生效,通过strace可以发现

[pid  6020] execve("/bin/sh", ["sh", "-c", "--", "id"], 0x7ffcfa8f9238 /* 34 vars */ <unfinished ...>

system本质上是调用了sh -c来执行命令行,在这个过程中就会替换掉euid,这就导致SUID不会正常生效了

Linux SUID与shell脚本问题
https://www.ztsubaki.top/posts/26/26/
作者
ZTsubaki
发布于
2025-06-24
许可协议
CC BY-SA 4.0