This time I have been thinking about the problem of how to limit the access permissions of login users in Linux system.

Plan a

Use rbash to restrict user access. In Ubuntu, use it directly

bash -r
Copy the code

Centos 7 does not support direct use of rbash, you can set up soft links

ln -s /bin/bash /bin/rbash

useradd -s /bin/rbash testuser
Copy the code

In this case, the shell environment of the newly created user testuser is the Rbash environment. Rbash mainly imposes the following restrictions on users

  • Use the CD command to change the directory
  • Set or unset environment variables (SHELL, PATH, ENV, or BASH_ENV)
  • Specifies the file name that contains the argument ‘/’
  • Specifies the file name that contains the argument ‘-‘
  • Using redirects the output ‘>’, ‘> >’ to ‘> |’, ‘< >’ > ‘&’, ‘& >’
  • Replace the current shell command with another command using the exec built-in command
  • Use the -f or -d options to add and remove built-in commands
  • Use the enable option to enable invalid built-in commands
  • Make the -p option for the built-in commands
  • useset +rorset +o\To turn off restricted mode

When we set the user’s shell environment to Rbash, the user is affected by the above restrictions. What if you needed to go a step further and limit the commands that users can access? This can also be solved.

All commands executed by the shell are obtained by searching the $PATH environment variable. Rbash does not allow commands with paths, that is, /bin/echo is not allowed to execute commands. Therefore, user access to commands is restricted. We can modify the user’s PATH environment variable, for example, set PATH to /home/testuser/usercmd, and create a soft link to the command that the user is allowed to access. Like the ls command

export PATH=/home/testuser/usercmd

ln -s /bin/ls /home/testuser/usercmd/ls
Copy the code

At this point, the testuser user can use the ls command.

If there is such a scenario, it is for a line of operations staff, their login user rights must be limited and access, including path only allows for the use of the limited order, these can be addressed by the above methods, however, if you allow them to restart the machine, or modify the network adapter configuration information? These operations can be performed only by super user permission, and cannot be performed directly through the above method.

Faced with this scenario, we can consider plan two

Scheme 2

The environment used by the author is centos7.9, and the built-in shell version of the system is 4.2. When I was thinking about this, I thought that if you had to restrict users’ permissions so severely, you could have implemented a shell like a switch, or tailored a shell using BusyBox. This is a viable approach, but in general it is expensive to implement a shell specifically, and busyBox tailoring is cumbersome. Based on this, the author wondered if it was possible to modify a shell directly based on the BASH4.2 source code.

That is, every time the shell executes, a check is performed on the command we have executed. If it is a command we allow, the execution continues. If it is a command that is not accessible, the result is returned directly. These allowed commands are read in the configuration file. Only the root permission can modify the configuration. In this way, the user can be configured to directly limit the execution permission and access to the command.

After verification, the author thinks it is ok. Now let’s refine this idea.

First of all, we need to solve the following problems

  1. Modify the BASH4.2 source code to read the configuration in a way that restricts the commands the user can execute
  2. How do I enable a user to execute commands that can only be executed by a superuser

Comb through the bash-4.2 source code

Take a look at the flow of shell code execution through a simple code flowchart, as shown below

When bash reads commands, shell scripts are parsed through the YACC syntax parser and a uniform abstract syntax tree structure is generated, as shown below

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;
Copy the code

In this structure, type represents the type of the command, common SIMPLE commands are of type SIMPLE, WHILE and IF represent WHILE loop statements and IF conditional statements, and CONNECTION type, Indicates that the shell script is multiple shell operations connected by pipe symbols. The value structure represents the specific content of a command.

For our scenario, a limited command line is open to the user, and the type is SIMPLE or CONNECTION. This is a simple command or shell script with several simple commands connected by pipe symbols.

Next, let’s look at the structure of 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;
Copy the code

The connection structure contains two COMMAND Pointers to shell commands to the left and right of the pipe symbol. This is a recursive relationship. The first pointer points to a shell COMMAND to the left of the pipe symbol, usually a SIMPLE COMMAND. Second points to the shell command to the right, and second can also pipe multiple shell commands linked by symbols, of type CONNECTION, and so on.

Simple_com has a simpler structure

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;
Copy the code

Line indicates the start line of the command, words indicates the command details, and redirects indicates the redirection information.

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;
Copy the code

Words is a single-linked list structure, such as the command ls -l, which bash interprets as ls –color= auto-l, as shown below

After parsing shell scripts, execute commands in three steps.

  • Find_func()Find the command from PATH, if present, execute; If not, go to the next step
  • Find if the command is builtin builtin, if so, execute; If not, go to the next step
  • callsearch_for_commandFind command, that is, the execution mode of the command is/bin/echoThis way, and then in/binFind commands in this path and execute them if they exist; If no, the command does not exist and an error is reported.

We need to restrict the commands that users can access. Before executing a COMMAND, check whether the COMMAND complies with our standards. That is, execute the COMMAND execution in the execute_command_internal() function.

Add code

In execute_command. C, before executing the execute_in_subshell function, we check and add the following code

if (running_startup_files == 0) {
    if (check_command_permission_for_smbash(command) == - 1) {
      internal_error(_("command is not allowed"));
      returnEXECUTION_FAILURE; }}Copy the code

If running_startup_files is 0, the shell command executed is not from the initializing. Bashrc startup script. Only shell commands that are initialized successfully need to be checked. If the check command is an open command, you can continue to run the command. Otherwise, a message indicating that Command is not allowed is displayed.

static int check_command_permission_for_smbash(COMMAND *cmd) {
  if (cmd->type == cm_simple) {
    return check_simple_command_permission_for_smbash(cmd);
  }
  if (cmd->type == cm_connection) {
    if (check_command_permission_for_smbash(cmd->value.Connection->first) == - 1) {
      return - 1;
    } else {
      returncheck_command_permission_for_smbash(cmd->value.Connection->second); }}return - 1;
}
Copy the code

As mentioned earlier, we only need to consider two types of commands: CONNECTION and SIMPLE. If SIMPLE is the type, it is processed directly; if CONNECTION is the type, it means that the command is multiple commands connected by pipe symbols. In other words, Each command needs to be checked recursively.

static int check_simple_command_permission_for_smbash(COMMAND *cmd) {
  if(cmd->type ! = cm_simple) {return - 1;
  }

  int res = 0;

  char *cmdstr = cmd->value.Simple->words->word->word;
  if (NULL == cmdstr || strlen(cmdstr) == 0) { 
    return 0;
  }

  // whatever cmdstr is, exit must be supported, printf will be executed each time
  if (strcmp(cmdstr, "exit") = =0 || strcmp(cmdstr, "printf") = =0) return 0;

  if (strstr(cmdstr, "/") != NULL) {
    res = find_path_from_conf(limit_paths, cmdstr);
  } else {
    res = find_cmd_from_conf(limit_commands, cmdstr);
  }

  if (res == - 1) {
    internal_error(_("%s "), cmdstr);
  }

  return res;
}
Copy the code

For SIMPLE commands of type SIMPLE, check the first node in the words list. The exit command is an exit command, which is supported by default. The shell has an environment variable set that is executed every time a command is called. If it is not set, a command that prints messages is executed by default. Printf is used, so this command must be supported by default.

The user enters a command that returns success if it is the one we released, searches for the executable’s path if it is the one we released, and returns -1 if it is not.

This completes the inspection of the command.

Load commands that allow access through configuration

It would be hard to fit the changing scenario if the commands that allow access were written in code, so we set the commands that allow access to be configurable in /etc/smbash.conf, as shown below

sm_limit_cmd = arpdel, arpset, resumeifconfig, show_DB_log, set_ipconfig, set_ipconfig_file
sm_limit_path = /usr/local/sbin, /usr/local/bin
Copy the code

Sm_limit_cmd indicates the set of commands that users are allowed to access, and sm_limit_PATH indicates the path prefix that allows users to execute commands with paths. In the common.h header file, add these functions

#define LIMIT_SMBASH_CONF "/etc/smbash.conf"
#define SM_LIMIT_PATH "sm_limit_path"
#define SM_LIMIT_CMD "sm_limit_cmd"

extern int load_conf_file(const char*);
extern int load_from_conf(char* * *,char *);
extern int find_cmd_from_conf(char* *,char *);
extern int find_path_from_conf(char* *,char *);
extern void free_limits(char* * *);
extern void trim_string(char *, char);
Copy the code

Let’s take a look at each of these functions. The code is relatively simple

/* if smbash.conf exists, load limits of commands and paths from it, which contains the shell commands and paths only allowed by user */
int load_conf_file(const char* conf) {
  if(access(conf, F_OK) ! =0) {
    fprintf(stderr."access %s failed\n", conf);
    return - 1;
  }

  FILE *fconf = fopen(conf, "r");
  char buf[2048] = {0};
  while (fgets(buf, sizeof(buf), fconf) ! =NULL) {
    trim_string(buf, ' ');
    trim_string(buf, '\r');
    trim_string(buf, '\n');

    if (strncmp(buf, SM_LIMIT_CMD, strlen(SM_LIMIT_CMD)) == 0) {
      load_from_conf(&limit_commands, buf);
    } else if (strncmp(buf, SM_LIMIT_PATH, strlen(SM_LIMIT_PATH)) == 0) {
      load_from_conf(&limit_paths, buf);
    } else 
      continue;
  }

  fclose(fconf);
  return 0;
}
Copy the code

The load_conf_file function loads a limited set of commands and an accessible executable path from the configuration file in two global variables

extern char **limit_commands = (char* *)NULL;
extern char **limit_paths = (char* *)NULL;
Copy the code

If it is a command, save it in limit_commands, if it is a path, save it in limit_paths.

int load_from_conf(char *** limits, char * buf) {
  if (NULL! = *limits) {fprintf(stderr."load %s duplicated\n", LIMIT_SMBASH_CONF);
    return - 1;
  }

  // del last comma
  if (buf[strlen(buf) - 1] = =', ') {
    buf[strlen(buf) - 1] = '\ 0';
  }

  char *pstart = strstr(buf, "=");
  if (NULL == pstart) {
    fprintf(stderr."configuration failed\n");
    return - 1;
  }

  pstart ++; // skip '='
  int cnt = 0;
  char *str = pstart;
  while ((str = strstr(str, ",")) != NULL) {
    cnt ++;
    str ++;
  }

  // last elements in limits is NULL as terminate
  *limits = (char* *)malloc (sizeof(char *) * (cnt + 1 + 1));
  if (NULL == limits) {
    fprintf(stderr."malloc failed\n");
    return - 1;
  }
  
  char *token = NULL;
  cnt = 0;
  while ((token = (char *)strtok(pstart, ",")) != NULL) {
    pstart = NULL;

    (*limits)[cnt] = (char *) malloc (sizeof(char) * (strlen(token) + 1));
    if (NULL == (*limits)[cnt]) {
      fprintf(stderr."malloc failed\n");
      return - 1;
    }

    memcpy((*limits)[cnt], token, strlen(token));
    (*limits)[cnt][strlen(token)] = '\ 0';
    cnt ++;
  }
  (*limits)[cnt] = NULL;

  return 0;
} 

void free_limits(char *** plimits) {
  int idx = 0;
  char ** limits = *plimits;
  if (NULL! = limits) {for(; limits[idx] ! =NULL; idx ++) {
      free(limits[idx]);
    }

    free(limits);
    limits = NULL; }}void trim_string(char * str, char delim) {
  if (NULL == str) { return; }
  char *p = (char *)malloc(sizeof(char) * (strlen(str) + 1));
  if (NULL == p) {
    fprintf(stderr."malloc failed\n");
    return;
  }

  char *strp = str;
  char *savep = p;
  int idx = 0;
  while(*strp ! ='\ 0') {
    if(*strp ! = delim) { *p++ = *strp++; idx ++; }else 
      strp ++;
  }
  savep[idx] = '\ 0';

  memcpy(str, savep, strlen(savep));
  str[strlen(savep)] = '\ 0';

  free(savep);
}
Copy the code

After writing the code in the configuration folder, you can directly look up the two global variables in the array. The following shows the implementation of the two lookup functions

int find_cmd_from_conf(char ** limits, char * str) {
  if (NULL == limits || NULL == str) { 
    return - 1;
  }
  int idx = 0;
  for(; limits[idx] ! =NULL; idx ++) {
    if (strcmp(str, limits[idx]) == 0) {
      returnidx; }}return - 1;
}

int find_path_from_conf(char ** limits, char * path) {
  if (NULL == limits || NULL == path) return - 1;

  int idx = 0;
  for(; limits[idx] ! =NULL; idx ++) {
    if (strncmp(limits[idx], path, strlen(limits[idx])) == 0) {
      returnidx; }}return - 1;
}
Copy the code

Load the configuration file, which can be loaded as soon as the shell is initialized, so that you can use the global variable information in memory directly when checking for commands that meet requirements.

After the code is added, compile to smbash, copy the smbash binary to the /bin directory, and specify the shell environment when you create the user

useradd -s /bin/smbash -m testuser
Copy the code

In this case, when you log in to the system using testuser, the testuser user can only access the commands specified in the /etc/smbash.conf file

testuser> pwd
-smbash: pwd 
-smbash: command is not allowed
Copy the code

Well, we’ve solved the first problem by changing bash’s behavior by adding code to Bash 4.2 to limit the commands that users can access.

Next, solve the second problem

How do I enable testuser to perform operations that require super privileges correctly

As we already know, testUser’s shell environment is the newly created Smbash shell, which limits the commands that users can use, especially for super-privileged commands such as su or sudo. But for operations personnel, sometimes, such as modifying network card configuration, or restarting the computer, these operations require superuser privileges, so how to do this.

First, if the testuser user needs to run a command with superuser privileges, the sudo command needs to be used, and the testuesr user must be added to the /etc/sudoers file without entering a password, as shown below

testuser ALL=(ALL) NOPASSWD:ALL
Copy the code

This gives TestUser superuser privileges to use sudo. However, the sudo command is not available to user testuser. The following error occurs when user testuser runs the sudo command

-smbash: sudo 
-smbash: command is not allowed
Copy the code

But we can think of another way. Through C program, call system library function way to achieve this goal. Execl (“/bin/sh”, “sh”, “-c”, command, (char *) 0); execl(“/bin/sh”, “sh”, “-c”, (char *) 0) In other words, the system library function actually calls /bin/sh to execute the script, bypassing testuser’s current smbash environment, and sudo permission can be executed directly at /bin/sh without a password. This is where the sudo superuser privileges we set for testUser earlier come in handy.

This C program is very simple

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

char *default_cmd[] = {
 "shutdown"."reboot".""
};

char *default_cmd_args[] = {
  "sudo /usr/sbin/shutdown"."sudo /usr/bin/reboot"
};

int find_default_cmd(const char* cmd);
Copy the code

In the application, the commands that need to use sudo permissions are fixed directly, because such commands are limited and cannot be changed at any time. After all, superuser permissions are very dangerous for operation and maintenance.

#include "limit-exe.h"

int find_default_cmd(const char* cmd) {
  if (NULL == cmd) return - 1;

  int idx = 0;
  for (; strlen(default_cmd[idx]) > 0; idx ++) {
    if (strcmp(cmd, default_cmd[idx]) == 0) {
      returnidx; }}return - 1;
}

int main(int argc, char *argv[]) {
  if (argc < 2) return 0;

  int res = find_default_cmd(argv[1]);
  char cmdstr[256] = {0};
  int idx = 2;  // skip argv[1]
  if (- 1! = res) {strcat(cmdstr, default_cmd_args[res]);

    while (idx < argc) {
      strcat(cmdstr, "");
      strcat(cmdstr, argv[idx++]);
    }

    system(cmdstr);
  }

  return 0;
}
Copy the code

Compile the limit_exe executable and install it in /usr/local/bin/so that testuser can run reboot or shutdown from the program. The last step, of course, is to set the alias alias for both commands in the testuser.bashrc file

alias shutdown="/usr/local/bin/limit_exe shutdown"
alias reboot="/usr/local/bin/limit_exe reboot"
Copy the code

Set the permission on the.bashrc file to read-only

chattr -i /home/testuser/.bashrc
Copy the code

That’s the goal.

conclusion

This paper uses two schemes to solve the problem of restricting Linux users’ access to commands. The first scheme uses rbash, but it has many limitations, especially when encountering the operation and maintenance scenarios mentioned in the article. Scheme 2 limits the commands that users can access by modifying bash 4.2 source code. At the same time, it uses system library functions to bypass current smbash and call Bourne shell to execute sudo access commands. This allows you to execute superuser privileges even in smbash limited environments.