/* * Copyright (C) 2008 Red Hat, Inc. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General * Public License along with this library; if not, write to the * Free Software Foundation, Inc., 59 Temple Place, Suite 330, * Boston, MA 02111-1307, USA. * * Author: David Zeuthen */ #ifdef HAVE_CONFIG_H # include "config.h" #endif #define _GNU_SOURCE #include #include #include #include #include #include #include #include #include #ifdef POLKIT_AUTHFW_PAM #include #endif /* POLKIT_AUTHFW_PAM */ #include #include #include #define POLKIT_AGENT_I_KNOW_API_IS_SUBJECT_TO_CHANGE #include static gchar *original_user_name = NULL; static gchar *original_cwd = NULL; static gchar *command_line = NULL; static struct passwd *pw; #ifndef HAVE_CLEARENV extern char **environ; static int clearenv (void) { if (environ != NULL) environ[0] = NULL; return 0; } #endif static void usage (int argc, char *argv[]) { g_printerr ("pkexec --version |\n" " --help |\n" " --disable-internal-agent |\n" " [--user username] PROGRAM [ARGUMENTS...]\n" "\n" "See the pkexec manual page for more details.\n"); } /* ---------------------------------------------------------------------------------------------------- */ static void log_message (gint level, gboolean print_to_stderr, const gchar *format, ...) { static gboolean is_log_open = FALSE; va_list var_args; gchar *s; const gchar *tty; if (!is_log_open) { openlog ("pkexec", LOG_PID, LOG_AUTHPRIV); /* security/authorization messages (private) */ is_log_open = TRUE; } va_start (var_args, format); s = g_strdup_vprintf (format, var_args); va_end (var_args); tty = ttyname (0); if (tty == NULL) tty = "unknown"; /* first complain to syslog */ syslog (level, "%s: %s [USER=%s] [TTY=%s] [CWD=%s] [COMMAND=%s]", original_user_name, s, pw->pw_name, tty, original_cwd, command_line); /* and then on stderr */ if (print_to_stderr) g_printerr ("%s\n", s); g_free (s); } /* ---------------------------------------------------------------------------------------------------- */ #ifdef POLKIT_AUTHFW_PAM static int pam_conversation_function (int n, const struct pam_message **msg, struct pam_response **resp, void *data) { g_assert_not_reached (); return PAM_CONV_ERR; } static gboolean open_session (const gchar *user_to_auth) { gboolean ret; gint rc; pam_handle_t *pam_h; struct pam_conv conversation; ret = FALSE; pam_h = NULL; conversation.conv = pam_conversation_function; conversation.appdata_ptr = NULL; /* start the pam stack */ rc = pam_start ("polkit-1", user_to_auth, &conversation, &pam_h); if (rc != PAM_SUCCESS) { g_printerr ("pam_start() failed: %s\n", pam_strerror (pam_h, rc)); goto out; } /* open a session */ rc = pam_open_session (pam_h, 0); /* flags */ if (rc != PAM_SUCCESS) { g_printerr ("pam_open_session() failed: %s\n", pam_strerror (pam_h, rc)); goto out; } ret = TRUE; out: if (pam_h != NULL) pam_end (pam_h, rc); return ret; } #endif /* POLKIT_AUTHFW_PAM */ /* ---------------------------------------------------------------------------------------------------- */ typedef gboolean (*FdCallback) (gint fd, gpointer user_data); static gboolean set_close_on_exec (gint fd, gpointer user_data) { gint fd_bottom; fd_bottom = GPOINTER_TO_INT (user_data); if (fd >= fd_bottom) { if (fcntl (fd, F_SETFD, FD_CLOEXEC) != 0 && errno != EBADF) { return FALSE; } } return TRUE; } static gboolean fdwalk (FdCallback callback, gpointer user_data) { gint fd; gint max_fd; g_return_val_if_fail (callback != NULL, FALSE); max_fd = sysconf (_SC_OPEN_MAX); for (fd = 0; fd < max_fd; fd++) { if (!callback (fd, user_data)) return FALSE; } return TRUE; } /* ---------------------------------------------------------------------------------------------------- */ static gchar * find_action_for_path (PolkitAuthority *authority, const gchar *path) { GList *l; GList *actions; gchar *action_id; GError *error; actions = NULL; action_id = NULL; error = NULL; actions = polkit_authority_enumerate_actions_sync (authority, NULL, &error); if (actions == NULL) { g_warning ("Error enumerating actions: %s", error->message); g_error_free (error); goto out; } for (l = actions; l != NULL; l = l->next) { PolkitActionDescription *action_desc = POLKIT_ACTION_DESCRIPTION (l->data); const gchar *path_for_action; path_for_action = polkit_action_description_get_annotation (action_desc, "org.freedesktop.policykit.exec.path"); if (path_for_action == NULL) continue; if (g_strcmp0 (path_for_action, path) == 0) { action_id = g_strdup (polkit_action_description_get_action_id (action_desc)); goto out; } } out: g_list_foreach (actions, (GFunc) g_object_unref, NULL); g_list_free (actions); /* Fall back to org.freedesktop.policykit.exec */ if (action_id == NULL) action_id = g_strdup ("org.freedesktop.policykit.exec"); return action_id; } /* ---------------------------------------------------------------------------------------------------- */ static gboolean is_valid_shell (const gchar *shell) { gboolean ret; gchar *contents; gchar **shells; GError *error; guint n; ret = FALSE; contents = NULL; shells = NULL; error = NULL; if (!g_file_get_contents ("/etc/shells", &contents, NULL, /* gsize *length */ &error)) { g_printerr ("Error getting contents of /etc/shells: %s\n", error->message); g_error_free (error); goto out; } shells = g_strsplit (contents, "\n", 0); for (n = 0; shells != NULL && shells[n] != NULL; n++) { if (g_strcmp0 (shell, shells[n]) == 0) { ret = TRUE; goto out; } } out: g_free (contents); g_strfreev (shells); return ret; } static gboolean validate_environment_variable (const gchar *key, const gchar *value) { gboolean ret; /* Generally we bail if any environment variable value contains * * - '/' characters * - '%' characters * - '..' substrings */ g_return_val_if_fail (key != NULL, FALSE); g_return_val_if_fail (value != NULL, FALSE); ret = FALSE; /* special case $SHELL */ if (g_strcmp0 (key, "SHELL") == 0) { /* check if it's in /etc/shells */ if (!is_valid_shell (value)) { log_message (LOG_CRIT, TRUE, "The value for the SHELL variable was not found the /etc/shells file"); g_printerr ("\n" "This incident has been reported.\n"); goto out; } } else if (strstr (value, "/") != NULL || strstr (value, "%") != NULL || strstr (value, "..") != NULL) { log_message (LOG_CRIT, TRUE, "The value for environment variable %s contains suscipious content", key); g_printerr ("\n" "This incident has been reported.\n"); goto out; } ret = TRUE; out: return ret; } /* ---------------------------------------------------------------------------------------------------- */ int main (int argc, char *argv[]) { guint n; guint ret; gint rc; gboolean opt_show_help; gboolean opt_show_version; gboolean opt_disable_internal_agent; PolkitAuthority *authority; PolkitAuthorizationResult *result; PolkitSubject *subject; PolkitDetails *details; GError *error; gchar *action_id; gchar **exec_argv; gchar *path; struct passwd pwstruct; gchar pwbuf[8192]; gchar *s; const gchar *environment_variables_to_save[] = { "SHELL", "LANG", "LINGUAS", "LANGUAGE", "LC_COLLATE", "LC_CTYPE", "LC_MESSAGES", "LC_MONETARY", "LC_NUMERIC", "LC_TIME", "LC_ALL", "TERM", "COLORTERM", /* For now, avoiding pretend that running X11 apps as another user in the same session * will ever work... See * * https://bugs.freedesktop.org/show_bug.cgi?id=17970#c26 * * and surrounding comments for a lot of discussion about this. */ #if 0 "DESKTOP_STARTUP_ID", "DISPLAY", "XAUTHORITY", "DBUS_SESSION_BUS_ADDRESS", "ORBIT_SOCKETDIR", #endif NULL }; GPtrArray *saved_env; gchar *opt_user; pid_t pid_of_caller; uid_t uid_of_caller; gpointer local_agent_handle; ret = 127; authority = NULL; subject = NULL; details = NULL; result = NULL; action_id = NULL; saved_env = NULL; path = NULL; command_line = NULL; opt_user = NULL; local_agent_handle = NULL; /* check for correct invocation */ if (geteuid () != 0) { g_printerr ("pkexec must be setuid root\n"); goto out; } original_user_name = g_strdup (g_get_user_name ()); if (original_user_name == NULL) { g_printerr ("Error getting user name.\n"); goto out; } original_cwd = g_strdup (get_current_dir_name ()); if (original_cwd == NULL) { g_printerr ("Error getting cwd.\n"); goto out; } /* First process options and find the command-line to invoke. Avoid using fancy library routines * that depend on environtment variables since we haven't cleared the environment just yet. */ opt_show_help = FALSE; opt_show_version = FALSE; opt_disable_internal_agent = FALSE; for (n = 1; n < (guint) argc; n++) { if (strcmp (argv[n], "--help") == 0) { opt_show_help = TRUE; } else if (strcmp (argv[n], "--version") == 0) { opt_show_version = TRUE; } else if (strcmp (argv[n], "--user") == 0 || strcmp (argv[n], "-u") == 0) { n++; if (n >= (guint) argc) { usage (argc, argv); goto out; } opt_user = g_strdup (argv[n]); } else if (strcmp (argv[n], "--disable-internal-agent") == 0) { opt_disable_internal_agent = TRUE; } else { break; } } if (opt_show_help) { usage (argc, argv); ret = 0; goto out; } else if (opt_show_version) { g_print ("pkexec version %s\n", PACKAGE_VERSION); ret = 0; goto out; } if (opt_user == NULL) opt_user = g_strdup ("root"); /* Now figure out the command-line to run - argv is guaranteed to be NULL-terminated, see * * http://lkml.indiana.edu/hypermail/linux/kernel/0409.2/0287.html * * but do check this is the case. * * We also try to locate the program in the path if a non-absolute path is given. */ g_assert (argv[argc] == NULL); path = g_strdup (argv[n]); if (path == NULL) { usage (argc, argv); goto out; } if (path[0] != '/') { /* g_find_program_in_path() is not suspectible to attacks via the environment */ s = g_find_program_in_path (path); if (s == NULL) { g_printerr ("Cannot run program %s: %s\n", path, strerror (ENOENT)); goto out; } g_free (path); argv[n] = path = s; } if (access (path, F_OK) != 0) { g_printerr ("Error accessing %s: %s\n", path, g_strerror (errno)); goto out; } command_line = g_strjoinv (" ", argv + n); exec_argv = argv + n; /* Look up information about the user we care about - yes, the return * value of this function is a bit funky */ rc = getpwnam_r (opt_user, &pwstruct, pwbuf, sizeof pwbuf, &pw); if (rc == 0 && pw == NULL) { g_printerr ("User `%s' does not exist.\n", opt_user); goto out; } else if (pw == NULL) { g_printerr ("Error getting information for user `%s': %s\n", opt_user, g_strerror (rc)); goto out; } /* now save the environment variables we care about */ saved_env = g_ptr_array_new (); for (n = 0; environment_variables_to_save[n] != NULL; n++) { const gchar *key = environment_variables_to_save[n]; const gchar *value; value = g_getenv (key); if (value == NULL) continue; /* To qualify for the paranoia goldstar - we validate the value of each * environment variable passed through - this is to attempt to avoid * exploits in (potentially broken) programs launched via pkexec(1). */ if (!validate_environment_variable (key, value)) goto out; g_ptr_array_add (saved_env, g_strdup (key)); g_ptr_array_add (saved_env, g_strdup (value)); } /* Nuke the environment to get a well-known and sanitized environment to avoid attacks * via e.g. the DBUS_SYSTEM_BUS_ADDRESS environment variable and similar. */ if (clearenv () != 0) { g_printerr ("Error clearing environment: %s\n", g_strerror (errno)); goto out; } /* Initialize the GLib type system - this is needed to interact with the * PolicyKit daemon */ g_type_init (); /* now check if the program that invoked us is authorized */ pid_of_caller = getppid (); if (pid_of_caller == 1) { /* getppid() can return 1 if the parent died (meaning that we are reaped * by /sbin/init); get process group leader instead - for example, this * happens when launching via gnome-panel (alt+f2, then 'pkexec gedit'). */ pid_of_caller = getpgrp (); } subject = polkit_unix_process_new (pid_of_caller); if (subject == NULL) { g_printerr ("No such process for pid %d: %s\n", (gint) pid_of_caller, error->message); g_error_free (error); goto out; } /* paranoia: check that the uid of pid_of_caller matches getuid() */ error = NULL; uid_of_caller = polkit_unix_process_get_owner (POLKIT_UNIX_PROCESS (subject), &error); if (error != NULL) { g_printerr ("Error determing pid of caller (pid %d): %s\n", (gint) pid_of_caller, error->message); g_error_free (error); goto out; } if (uid_of_caller != getuid ()) { g_printerr ("User of caller (%d) does not match our uid (%d)\n", uid_of_caller, getuid ()); goto out; } error = NULL; authority = polkit_authority_get_sync (NULL /* GCancellable* */, &error); if (authority == NULL) { g_printerr ("Error getting authority: %s\n", error->message); g_error_free (error); goto out; } details = polkit_details_new (); polkit_details_insert (details, "command-line", command_line); s = g_strdup_printf ("%s (%s)", pw->pw_gecos, pw->pw_name); polkit_details_insert (details, "user", s); g_free (s); s = g_strdup_printf ("%d", (gint) pw->pw_uid); polkit_details_insert (details, "uid", s); g_free (s); polkit_details_insert (details, "program", path); action_id = find_action_for_path (authority, path); g_assert (action_id != NULL); try_again: error = NULL; result = polkit_authority_check_authorization_sync (authority, subject, action_id, details, POLKIT_CHECK_AUTHORIZATION_FLAGS_ALLOW_USER_INTERACTION, NULL, &error); if (result == NULL) { g_printerr ("Error checking for authorization %s: %s\n", action_id, error->message); goto out; } if (polkit_authorization_result_get_is_authorized (result)) { /* do nothing */ } else if (polkit_authorization_result_get_is_challenge (result)) { if (local_agent_handle == NULL && !opt_disable_internal_agent) { PolkitAgentListener *listener; error = NULL; /* this will fail if we can't find a controlling terminal */ listener = polkit_agent_text_listener_new (NULL, &error); if (listener == NULL) { g_printerr ("Error creating textual authentication agent: %s\n", error->message); g_error_free (error); goto out; } local_agent_handle = polkit_agent_listener_register (listener, POLKIT_AGENT_REGISTER_FLAGS_RUN_IN_THREAD, subject, NULL, /* object_path */ NULL, /* GCancellable */ &error); g_object_unref (listener); if (local_agent_handle == NULL) { g_printerr ("Error registering local authentication agent: %s\n", error->message); g_error_free (error); goto out; } g_object_unref (result); result = NULL; goto try_again; } else { g_printerr ("Error executing command as another user: No authentication agent found.\n"); goto out; } } else { log_message (LOG_WARNING, TRUE, "Error executing command as another user: Not authorized"); g_printerr ("\n" "This incident has been reported.\n"); goto out; } /* Set PATH to a safe list */ g_ptr_array_add (saved_env, g_strdup ("PATH")); if (pw->pw_uid != 0) s = g_strdup_printf ("/usr/bin:/bin:/usr/sbin:/sbin:%s/bin", pw->pw_dir); else s = g_strdup_printf ("/usr/sbin:/usr/bin:/sbin:/bin:%s/bin", pw->pw_dir); g_ptr_array_add (saved_env, s); g_ptr_array_add (saved_env, g_strdup ("LOGNAME")); g_ptr_array_add (saved_env, g_strdup (pw->pw_name)); g_ptr_array_add (saved_env, g_strdup ("USER")); g_ptr_array_add (saved_env, g_strdup (pw->pw_name)); g_ptr_array_add (saved_env, g_strdup ("HOME")); g_ptr_array_add (saved_env, g_strdup (pw->pw_dir)); s = g_strdup_printf ("%d", getuid ()); g_ptr_array_add (saved_env, g_strdup ("PKEXEC_UID")); g_ptr_array_add (saved_env, s); /* set the environment */ for (n = 0; n < saved_env->len - 1; n += 2) { const gchar *key = saved_env->pdata[n]; const gchar *value = saved_env->pdata[n + 1]; if (!g_setenv (key, value, TRUE)) { g_printerr ("Error setting environment variable %s to '%s': %s\n", key, value, g_strerror (errno)); goto out; } } /* set close_on_exec on all file descriptors except stdin, stdout, stderr */ if (!fdwalk (set_close_on_exec, GINT_TO_POINTER (3))) { g_printerr ("Error setting close-on-exec for file desriptors\n"); goto out; } /* if not changing to uid 0, become uid 0 before changing to the user */ if (pw->pw_uid != 0) { setreuid (0, 0); if ((geteuid () != 0) || (getuid () != 0)) { g_printerr ("Error becoming uid 0: %s\n", g_strerror (errno)); goto out; } } /* open session - with PAM enabled, this runs the open_session() part of the PAM * stack - this includes applying limits via pam_limits.so but also other things * requested via the current PAM configuration. * * NOTE NOTE NOTE: pam_limits.so doesn't seem to clear existing limits - e.g. * * $ ulimit -t * unlimited * * $ su - * Password: * # ulimit -t * unlimited * # logout * * $ ulimit -t 1000 * $ ulimit -t * 1000 * $ su - * Password: * # ulimit -t * 1000 * * TODO: The question here is whether we should clear the limits before applying them? * As evident above, neither su(1) (and, for that matter, nor sudo(8)) does this. */ #ifdef POLKIT_AUTHFW_PAM if (!open_session (pw->pw_name)) { goto out; } #endif /* POLKIT_AUTHFW_PAM */ /* become the user */ if (setgroups (0, NULL) != 0) { g_printerr ("Error setting groups: %s\n", g_strerror (errno)); goto out; } if (initgroups (pw->pw_name, pw->pw_gid) != 0) { g_printerr ("Error initializing groups for %s: %s\n", pw->pw_name, g_strerror (errno)); goto out; } setregid (pw->pw_gid, pw->pw_gid); setreuid (pw->pw_uid, pw->pw_uid); if ((geteuid () != pw->pw_uid) || (getuid () != pw->pw_uid) || (getegid () != pw->pw_gid) || (getgid () != pw->pw_gid)) { g_printerr ("Error becoming real+effective uid %d and gid %d: %s\n", pw->pw_uid, pw->pw_gid, g_strerror (errno)); goto out; } /* change to home directory */ if (chdir (pw->pw_dir) != 0) { g_printerr ("Error changing to home directory %s: %s\n", pw->pw_dir, g_strerror (errno)); goto out; } /* Log the fact that we're executing a command */ log_message (LOG_NOTICE, FALSE, "Executing command"); /* exec the program */ if (execv (path, exec_argv) != 0) { g_printerr ("Error executing %s: %s\n", path, g_strerror (errno)); goto out; } /* if exec doesn't fail, it never returns... */ g_assert_not_reached (); out: /* if applicable, nuke the local authentication agent */ if (local_agent_handle != NULL) polkit_agent_listener_unregister (local_agent_handle); if (result != NULL) g_object_unref (result); g_free (action_id); if (details != NULL) g_object_unref (details); if (subject != NULL) g_object_unref (subject); if (authority != NULL) g_object_unref (authority); if (saved_env != NULL) { g_ptr_array_foreach (saved_env, (GFunc) g_free, NULL); g_ptr_array_free (saved_env, TRUE); } g_free (path); g_free (command_line); g_free (opt_user); g_free (original_user_name); g_free (original_cwd); return ret; }