/* * scado: setuid in capability sauce. * Copyright (C) 2016 Davide Berardi, University of Bologna * * This file is part of cado. * * Cado is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; If not, see . * */ #define _GNU_SOURCE #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define EDIT_PAM_MAXTRIES 3 #define SUCCESS 0 /* Generic error */ #define ERROR_EXIT -1 /* Authentication error */ #define ERROR_AUTH -2 const char *tmpdir; #define OPTSTRING "hu:UDle" struct option long_options[] = { {"help", no_argument, NULL, 'h'}, {"update", required_argument, NULL, 'u'}, {"update-all", no_argument, NULL, 'U'}, {"delete", no_argument, NULL, 'D'}, {"list", no_argument, NULL, 'l'}, {"edit", no_argument, NULL, 'e'}, }; enum scado_option {none, update, delete, list, edit, toomany}; void usage_n_exit(char *progname, char *message) { fprintf(stderr, "%s - script cado, setuid in capability sauce\n", progname); fprintf(stderr, "usage: %s OPTIONS\n", progname); fprintf(stderr, "Options:\n"); fprintf(stderr, " -h --help display help message and exit\n"); fprintf(stderr, " -u --update update the checksum of command\n"); fprintf(stderr, " -U --update-all update the checksum of all commands\n"); fprintf(stderr, " -D --delete delete the scado file\n"); fprintf(stderr, " -l --list print the scado file\n"); fprintf(stderr, " -e --edit edit the scado file\n"); if (message) fprintf(stderr, "\n%s\n",message); exit(ERROR_EXIT); } /* watchdog for garbage collection. the temporary file for scado editing is removed in case of abend */ static int editor_garbage_collect(char *path) { int checkpipe[2]; pid_t childpid; if (pipe(checkpipe)) return -1; /* Start garbage collector * Double fork to avoid zombie processes and to remove the temporary file when it is not needed any more */ if(!(childpid = fork())) { /* Child */ if(!fork()) { /* Grandchild */ if (close(checkpipe[1]) == 0 && setsid() > 0) { char c; if (read(checkpipe[0], &c, 1) == 0) unlink(path); } exit(0); } else exit(0); } waitpid(childpid, NULL, 0); /* (grand) parent */ close(checkpipe[0]); return(checkpipe[1]); } static void editor_garbage_collect_do_not_unlink(int fd) { char c = 'K'; // keep it, any other non-null char would fit. int n = write(fd, &c, 1); (void) n; } /* command line selectable functions */ int scado_none_or_toomany(char *progname, char *username, char *program_path) { usage_n_exit(progname, "select exactly one option among: -u -U -D -l -e"); return 0; } int scado_delete(char *progname, char *username, char *program_path) { char scado_file[PATH_MAX]; if (get_scado_file(username, scado_file) < 0) return ERROR_EXIT; /* Get the authorization. */ if (pam_check(username) != PAM_SUCCESS) { return ERROR_AUTH; } return unlink(scado_file); } int scado_update(char *progname, char *username, char *program_path) { char tmp_file[PATH_MAX]; char scado_file[PATH_MAX]; if (get_scado_file(username, scado_file) < 0) return ERROR_EXIT; /* Get the authorization. */ if (pam_check(username) != PAM_SUCCESS) { return ERROR_AUTH; } if (snprintf(tmp_file, PATH_MAX, "%s/%s", tmpdir, TMP_TEMPLATE) < 0) return ERROR_EXIT; if (copytemp(scado_file, tmp_file) < 0) return ERROR_EXIT; scado_copy_update(tmp_file, scado_file, program_path); if (unlink(tmp_file) < 0) return ERROR_EXIT; return SUCCESS; } int scado_list(char *progname, char *username, char *program_path) { char scado_file[PATH_MAX]; int fd; int outfl = fcntl(STDOUT_FILENO, F_GETFL, 0); if (outfl & O_APPEND) fcntl(STDOUT_FILENO, F_SETFL, outfl & ~O_APPEND); if (get_scado_file(username, scado_file) < 0) return ERROR_EXIT; if ((fd = open(scado_file, O_RDONLY)) < 0) return fd; copyfile(fd, STDOUT_FILENO); close(fd); return SUCCESS; } int scado_edit(char *progname, char *username, char *program_path) { char tmp_file[PATH_MAX]; char scado_file[PATH_MAX]; char *editor; char *args; int status = 0; pid_t pid, xpid; char digest_before[DIGESTSTRLEN + 1]; char digest_after[DIGESTSTRLEN + 1]; int garbage_collect_fd; /* Ignore signals. */ (void) signal(SIGHUP, SIG_IGN); (void) signal(SIGINT, SIG_IGN); (void) signal(SIGQUIT, SIG_IGN); if (get_scado_file(username, scado_file) < 0) return ERROR_EXIT; if (snprintf(tmp_file, PATH_MAX, "%s/%s", tmpdir, TMP_TEMPLATE) < 0) return ERROR_EXIT; if (copytemp_digest(scado_file, tmp_file, digest_before) < 0) { *digest_before = 0; if (errno == ENOENT) { int tmpfd=mkstemp(tmp_file); if (tmpfd < 0) return ERROR_EXIT; if (write(tmpfd, DEFAULT_MESSAGE, sizeof(DEFAULT_MESSAGE)-1) < 0) return ERROR_EXIT; close(tmpfd); } else return ERROR_EXIT; } garbage_collect_fd = editor_garbage_collect(tmp_file); /* Get the editor from the configuration. */ if (((editor = getenv("VISUAL")) == NULL || *editor == '\0') && ((editor = getenv("EDITOR")) == NULL || *editor == '\0')) { editor = EDITOR; } switch (pid = fork()) { case -1: perror("fork"); exit(ERROR_EXIT); case 0: /* XXX secure? */ if (setgid(getgid()) < 0) { perror("setgid(getgid())"); exit(ERROR_EXIT); } if (setuid(getuid()) < 0) { perror("setuid(getuid())"); exit(ERROR_EXIT); } if (asprintf(&args, "%s %s", editor, tmp_file) < 0) { exit(ERROR_EXIT); } execsp(args); perror(editor); exit(ERROR_EXIT); default: /* parent */ break; } /* parent */ for (;;) { xpid = waitpid(pid, &status, 0); if (xpid == -1) { if(errno != EINTR) { fprintf(stderr, "wait pid: error waiting for PID %ld (%s): %s \n", (long) xpid, editor, strerror(errno)); return ERROR_EXIT; } } else if (WIFEXITED(status) && WEXITSTATUS(status)) { fprintf(stderr,"wait pid: exit status"); return ERROR_EXIT; } else if(WIFSIGNALED(status)) { fprintf(stderr, "%s killed: signal %d (%score dumped)\n", editor, WTERMSIG(status), WCOREDUMP(status) ? "": "no "); } else { break; } } /* Restore signals. */ (void) signal(SIGHUP, SIG_DFL); (void) signal(SIGINT, SIG_DFL); (void) signal(SIGQUIT, SIG_DFL); if(compute_digest(tmp_file, digest_after) < 0) return ERROR_EXIT; if (strcmp(digest_before, digest_after) != 0) { int count; /* Get the authorization. */ for (count = 0; count < EDIT_PAM_MAXTRIES; count++) { if (pam_check(username) == PAM_SUCCESS) { break; } if (count < EDIT_PAM_MAXTRIES - 1) fprintf(stderr, "PAM authorization failed\n"); else { editor_garbage_collect_do_not_unlink(garbage_collect_fd); fprintf(stderr, "A copy of the edited scado file has been saved as %s\n", tmp_file); return ERROR_AUTH; } } scado_copy_update(tmp_file, scado_file, NULL); } close(garbage_collect_fd); return SUCCESS; } typedef int (* option_function) (char *progname, char *username, char *program_path); /* array to select the requested option */ option_function option_map[] = { scado_none_or_toomany, scado_update, scado_delete, scado_list, scado_edit, scado_none_or_toomany}; /* update the chosen option. If one was already chosen, switch to "toomany" */ static inline enum scado_option set_option(enum scado_option option, enum scado_option newvalue) { return option == none ? newvalue : toomany; } int main(int argc, char **argv) { struct passwd *pw; char *progname = basename(argv[0]); char *username; char *program_path = ""; /* "" means ALL */ enum scado_option option=none; int rval = 1; if ((pw = getpwuid(getuid())) == NULL) { fprintf(stderr, "Your UID isn't in the passwd file.\n"); exit(ERROR_EXIT); } username = pw->pw_name; if ((tmpdir = getenv("TMPDIR")) == NULL || *tmpdir== '\0') { tmpdir = TMPDIR; } while(1) { int c = getopt_long(argc, argv, OPTSTRING, long_options, NULL); if (c < 0) break; switch(c) { case 'h': usage_n_exit(progname, NULL); break; case 'u': program_path = optarg; case 'U': option=set_option(option,update); break; case 'D': option=set_option(option,delete); break; case 'l': option=set_option(option,list); break; case 'e': option=set_option(option,edit); break; default: usage_n_exit(progname, NULL); } } if (optind != argc) /* unknown trailing arguments */ usage_n_exit(progname, "unknown trailing arguments"); rval = option_map[option](progname, username, program_path); if (rval == ERROR_AUTH) { fprintf(stderr, "%s: Authorization failure.\n", progname); } else if (rval) { fprintf(stderr, "%s returned: %d, %s\n", progname, rval, strerror(errno)); } return rval; }