这段时间思考了这么一个问题,如何限制 linux 系统中登录用户的访问权限,限制用户能够使用的命令。在某些场景下,要求能够限制用户使用的命令,同时还能够执行像shutdown这种超级用户才能使用的命令。
方案一
以 rbash 的方式来限制用户的访问权限,在 ubuntu 系统中,直接使用
bash -r
就可以进入 rbash,在 centos 7 系统中,不支持直接使用,可以建立软链接的方式
ln -s /bin/bash /bin/rbash
useradd -s /bin/rbash testuser
这样,新创建的用户 testuser 的 shell 环境就是 rbash 环境。rbash 主要对用户做了如下限制
- 使用命令cd更改目录
- 设置或者取消环境变量的设置(SHELL, PATH, ENV, or BASH_ENV)
- 指定包含参数’/'的文件名
- 指定包含参数’ - '的文件名
- 使用重定向输出’>’, ‘>>’, ‘> |’, ‘<>’ ‘>&’,’&>’
- 使用 exec 内置命令用其他命令代替当前shell命令
- 使用 -f 或者 -d 选项添加和删除内置命令
- 使用enable选项启动失效的内置命令
- 对内置命令制定 -p 选项
- 使用
set +r
或者set +o\
来关闭限制模式
当我们将用户的 shell 环境设置成 rbash 时,用户就会受到以上限制的影响。假如需要更加深入一步,还要限制用户能够访问的命令呢。这个也可以解决。
shell 上执行的命令,都是通过在 $PATH
环境变量中进行查找获取到的,因为 rbash 的缘故,不允许命令中带有路径,也就是不允许 /bin/echo
这种方式执行命令,所以限制用户的访问命令,我们可以修改用户的 PATH 环境变量,比如将 PATH 设置在 /home/testuser/usercmd
中,允许用户访问什么样的命令,就在/home/testuser/usercmd
中创建一个指向该命令的软链接即可,比如 ls 命令
export PATH=/home/testuser/usercmd
ln -s /bin/ls /home/testuser/usercmd/ls
这样,testuser 用户就能够使用 ls 命令了。
假如有这么一个场景,就是对于一线运维人员来说,他们登录的用户权限肯定是受限的,包括路径访问权限,只允许使用有限的命令,这些都可以通过上面的方法来解决,但是,如果允许他们能够重启机器,或者修改网卡配置信息呢,这些可都是需要超级用户权限才能执行的操作,直接通过上面的方式是不能直接执行的。
面对这种场景,我们可以考虑方案二
方案二
笔者使用的环境是 centos7.9,系统内置的 shell 版本是 4.2 版本。笔者在思考这个问题的时候想过,如果非要这么严格的限制用户的权限,那干脆类似交换机那样,专门实现一个shell,或者使用 busybox 裁剪一个shell 好了。这确实是一种可行的方法,但是一般来说,专门实现一个 shell 代价比较高,busybox 裁剪的话,也是很麻烦的。基于此,笔者想到,是不是可以直接在 bash4.2 源代码的基础上,改造一个 shell 出来呢。
也就是说,在每次执行 shell 的时候,都会对我们执行的命令进行一个检查,如果是我们允许的命令,那就继续执行,如果是不允许访问的命令,直接返回结果。而这些允许的命令都是在配置文件中读取的,配置的修改只允许 root 权限才能进行。这样,是不是通过可配置的方式,就直接限定了用户的执行权限和可访问的命令了呢。
经过验证,笔者认为是可以的。下面咱们来慢慢完善这个思路。
首先,我们需要解决以下这几个问题
- 修改 bash4.2 源代码,实现读取配置的方式来限制用户能够执行的命令
- 如何让用户可以执行某些超级用户权限才能执行的命令
梳理 bash-4.2 源代码
通过一个简单的代码流程图,来看一下shell代码执行的流程,如下所示
bash 在读取命令的时候,是通过 yacc 语法解析器来解析 shell 脚本的,同时会生成一个统一的抽象语法树的结构,如下所示
typedef struct command {
enum command_type type; /* FOR CASE WHILE IF CONNECTION or SIMPLE. */
int flags; /* Flags controlling execution environment. */
int line; /* line number the command starts on */
REDIRECT *redirects; /* Special redirects for FOR CASE, etc. */
union {
struct for_com *For;
struct case_com *Case;
struct while_com *While;
struct if_com *If;
struct connection *Connection;
struct simple_com *Simple;
struct function_def *Function_def;
struct group_com *Group;
#if defined (SELECT_COMMAND)
struct select_com *Select;
#endif
#if defined (DPAREN_ARITHMETIC)
struct arith_com *Arith;
#endif
#if defined (COND_COMMAND)
struct cond_com *Cond;
#endif
#if defined (ARITH_FOR_COMMAND)
struct arith_for_com *ArithFor;
#endif
struct subshell_com *Subshell;
struct coproc_com *Coproc;
} value;
} COMMAND;
该结构中 type 表示命令的类型,常用的简单命令,类型为 SIMPLE, WHILE 和 IF 表示 while 循环语句和 if 条件语句,而 CONNECTION 类型,表示该 shell 脚本是通过管道符号连接起来的多个 shell 操作。value 结构,表示的是命令的具体内容。
针对我们的场景,开放给用户有限的命令行,type 类型就是 SIMPLE 或者 CONNECTION。也就是简单命令或者通过管道符号连接的起来的若干个简单命令的 shell 脚本。
接着我们看下 connection 的结构
typedef struct connection {
int ignore; /* Unused; simplifies make_command (). */
COMMAND *first; /* Pointer to the first command. */
COMMAND *second; /* Pointer to the second command. */
int connector; /* What separates this command from others. */
} CONNECTION;
connection 结构,包含两个 COMMAND 指针,分别指向管道符号左右两边的shell命令,这是一个递归的关系,first 指针指向管道符号左边的 shell 命令,通常就是一个 SIMPLE 类型的命令,second 指向的是右边的shell命令,而 second 同样可以管道符号连接起来的多个 shell 命令,类型为 CONNECTION,以此类推。
simple_com
的结构就比较简单
typedef struct simple_com {
int flags; /* See description of CMD flags. */
int line; /* line number the command starts on */
WORD_LIST *words; /* The program name, the arguments,
variable assignments, etc. */
REDIRECT *redirects; /* Redirections to perform. */
} SIMPLE_COM;
line 表示命令起始行,words 就表示命令的具体内容,而 redirects 表示重定向信息。
typedef struct word_desc {
char *word; /* Zero terminated string. */
int flags; /* Flags associated with this word. */
} WORD_DESC;
/* A linked list of words. */
typedef struct word_list {
struct word_list *next;
WORD_DESC *word;
} WORD_LIST;
words 是一个单链表结构,比如 ls -l
这条命令,bash 会将其解释成 ls --color=auto -l
,words 结构的组成如下所示
解析完 shell 脚本之后,在执行命令时,分为三个步骤进行。
Find_func()
从 PATH 中查找命令,如果存在,执行;如果不存在,进入下一步- 查找命令是否是 builtin 内建命令,如果存在,执行;如果不存在,进入下一步
- 调用
search_for_command
查找命令,即认为命令的执行方式是/bin/echo
这种方式,然后在/bin
这个路径中查找命令,如果存在就执行;如果不存在,说明命令不存在,报错。
shell 执行的流程大概就是上述这个流程,我们需要限制用户能够访问的命令,那么在命令执行之前,检查命令是否符合我们的标准即可,也就是在 execute_command_internal()
函数中,执行 COMMAND 执行进行检查。
添加代码
在 execute_command.c
中,在执行 execute_in_subshell
函数之前,我们进行检查,添加如下代码
if (running_startup_files == 0) {
if (check_command_permission_for_smbash(command)