Package: src:libpam-krb5 Followup-For: Bug #399002 Hello,
The attacked patch adds a setuid-wrapper to allow verification of the keytab for non-root PAM programs. The new verify_creds_setuid_helper function forks our new suid helper binary against which it does a standard kerberos service authentication by getting a service ticket from the KDC and sending a AP_REQ and verifying the resulting AP_REP it receives. In case the PAM process is called as root or a custom keytab was provided as PAM module option the old code is used. The helper doesn't drop privileges at the moment because it complicates the code and we're not sure how non-Linux systems behave regarding dropped privileges and ptrace/core dumps. To prevent possible attacks we've chosen to keep the setuid privileges during the run of the helper. As the Kerberos library must support untrusted input anyway, this should cause no issues. However there's one potential problem with the setuid helper. The Kerberos library uses init-functions which initialize for example gettext on load. The setuid helper can only clean the environment after these init-functions are run so an attacker could inject environment variables. We don't know if this could be an issue. In case you think this is problematic we could easily split the wrapper into two binaries, where the first (setuid) is not linked to Kerberos and the second (not-setuid) performs the validation. Regards Simon -- + privacy is necessary + using gnupg http://gnupg.org + public key id: 0x1972F726F0D556E7
From 01139eb31b3f3f6c41b425e492d5146499b4e0e2 Mon Sep 17 00:00:00 2001 Message-Id: <01139eb31b3f3f6c41b425e492d5146499b4e0e2.1465566262.git.si...@ruderich.org> From: Simon Ruderich <[email protected]> Date: Fri, 10 Jun 2016 14:48:02 +0200 Subject: [PATCH] Add setuid helper to allow TGT verification by non-root processes To prevent KDB spoofing the Kerberos option verify_ap_req_nofail = true can be used to verify that the ticket originates from the same KDC which provided the host keytab. However if a PAM program is not running as root, it can't read the keytab and thus can't perform this validation. Add a setuid helper which performs the authentication when running not as root. The helper performs a manual AP_REQ/AP_REP exchange to authenticate the KDC against the host keytab. verify_ap_req_nofail is respected and behaves as before. The helper supports both setuid root and setgid to a separate group (with access to /etc/krb5.keytab). Per default setuid is used which should require no changes on most systems. --- .gitignore | 1 + Makefile.am | 8 ++- auth.c | 207 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++- krb5_vfycrd.c | 126 +++++++++++++++++++++++++++++++++++ 4 files changed, 340 insertions(+), 2 deletions(-) create mode 100644 krb5_vfycrd.c diff --git a/.gitignore b/.gitignore index c282488..5aea49f 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ /config.log /config.status /configure +/krb5_vfycrd /libtool /m4/libtool.m4 /m4/ltoptions.m4 diff --git a/Makefile.am b/Makefile.am index 0cdf2ff..a6f02de 100644 --- a/Makefile.am +++ b/Makefile.am @@ -18,7 +18,7 @@ EXTRA_DIST = .gitignore LICENSE autogen pam_krb5.map pam_krb5.pod \ tests/tap/libtap.sh # Everything we build needs the Kerbeors headers and library flags. -AM_CPPFLAGS = $(KRB5_CPPFLAGS) +AM_CPPFLAGS = $(KRB5_CPPFLAGS) -DKRB5_VFYCRD_PATH=\"$(sbindir)/krb5_vfycrd\" AM_LDFLAGS = $(KRB5_LDFLAGS) noinst_LTLIBRARIES = pam-util/libpamutil.la portable/libportable.la @@ -30,6 +30,12 @@ pam_util_libpamutil_la_SOURCES = pam-util/args.c pam-util/args.h \ pam-util/logging.c pam-util/logging.h pam-util/options.c \ pam-util/options.h pam-util/vector.c pam-util/vector.h +sbin_PROGRAMS = krb5_vfycrd +krb5_vfycrd_LDADD = $(KRB5_LIBS) + +install-exec-hook: + chmod 4755 $(DESTDIR)$(sbindir)/krb5_vfycrd + if HAVE_LD_VERSION_SCRIPT VERSION_LDFLAGS = -Wl,--version-script=${srcdir}/pam_krb5.map else diff --git a/auth.c b/auth.c index 6d35bd0..b60e63c 100644 --- a/auth.c +++ b/auth.c @@ -27,6 +27,10 @@ #endif #include <pwd.h> #include <sys/stat.h> +#include <sys/types.h> +#include <sys/wait.h> +#include <sys/time.h> +#include <sys/resource.h> #include <internal.h> #include <pam-util/args.h> @@ -632,7 +636,7 @@ password_auth_attempt(struct pam_args *args, const char *service, * Returns a Kerberos status code (0 for success). */ static krb5_error_code -verify_creds(struct pam_args *args, krb5_creds *creds) +verify_creds_normal(struct pam_args *args, krb5_creds *creds) { krb5_verify_init_creds_opt opts; krb5_keytab keytab = NULL; @@ -678,6 +682,207 @@ verify_creds(struct pam_args *args, krb5_creds *creds) return retval; } +static krb5_error_code +verify_creds_setuid_helper(struct pam_args *args, krb5_creds *creds) +{ + pid_t pid; + int fd_stdin[2]; + int fd_stdout[2]; + struct sigaction temp_sa, saved_sa; + int retval = KRB5KRB_ERR_GENERIC; /* need to return KRB5 error codes */ + + if (pipe(fd_stdin)) { + return retval; + } + if (pipe(fd_stdout)) { + close(fd_stdin[0]); + close(fd_stdin[1]); + return retval; + } + + /* Install a temporary signal handler for SIGCHLD to prevent the + * application from receiving unexpected signals. + * + * TODO: noreap pam module option like pam_unix.so? */ + memset(&temp_sa, 0, sizeof(temp_sa)); + temp_sa.sa_handler = SIG_DFL; + sigemptyset(&temp_sa.sa_mask); + sigaction(SIGCHLD, &temp_sa, &saved_sa); + + pid = fork(); + if (pid > 0) { + /* parent */ + uint32_t length_princ, length_req, length_rep; /* see krb5_vfycrd.c */ + + FILE *chld_stdin, *chld_stdout; + char buf[4096]; + krb5_data packet_req, packet_rep; + krb5_context ctx = args->config->ctx->context; + krb5_principal server_princ = NULL; + krb5_auth_context auth_ctx = NULL; + krb5_ccache ccache = NULL; + krb5_creds creds_server_in, *creds_server = NULL; + krb5_ap_rep_enc_part *reply; + + packet_req.data = NULL; + packet_rep.data = NULL; + + close(fd_stdin[0]); + close(fd_stdout[1]); + + chld_stdin = fdopen(fd_stdin[1], "w"); + chld_stdout = fdopen(fd_stdout[0], "r"); + if (!chld_stdin || !chld_stdout) + goto cleanup; + + /* Read principal name from server. */ + if (fread(&length_princ, sizeof(length_princ), 1, chld_stdout) != 1) + goto cleanup; + if (length_princ >= sizeof(buf)) /* space for '\0' */ + goto cleanup; + if (fread(buf, length_princ, 1, chld_stdout) != 1) + goto cleanup; + buf[length_princ] = 0; + if ((retval = krb5_parse_name(ctx, buf, &server_princ))) + goto cleanup; + + /* Use a temporary credential cache so we don't pollute the user's + * cache. */ + if ((retval = krb5_cc_new_unique(ctx, "MEMORY", NULL, &ccache))) + goto cleanup; + if ((retval = krb5_cc_initialize(ctx, ccache, creds->client))) + goto cleanup; + if ((retval = krb5_cc_store_cred(ctx, ccache, creds))) + goto cleanup; + + /* Get server ticket from KDC. */ + memset(&creds_server_in, 0, sizeof(creds_server_in)); + creds_server_in.client = creds->client; + creds_server_in.server = server_princ; + if ((retval = krb5_timeofday(ctx, &creds_server_in.times.endtime))) + goto cleanup; + creds_server_in.times.endtime += 5*60; + if ((retval = krb5_get_credentials(ctx, 0, ccache, + &creds_server_in, &creds_server))) + goto cleanup; + + if ((retval = krb5_mk_req_extended(ctx, &auth_ctx, 0, NULL, + creds_server, &packet_req))) + goto cleanup; + + /* Send AP_REQ to server. */ + retval = KRB5KRB_ERR_GENERIC; + length_req = packet_req.length; + if (fwrite(&length_req, sizeof(length_req), 1, chld_stdin) != 1) + goto cleanup; + if (fwrite(packet_req.data, length_req, 1, chld_stdin) != 1) + goto cleanup; + if (fflush(chld_stdin)) + goto cleanup; + + /* Receive AP_REP from server. */ + if (fread(&length_rep, sizeof(length_rep), 1, chld_stdout) != 1) + goto cleanup; + if (length_rep > sizeof(buf)) + goto cleanup; + if (fread(buf, length_rep, 1, chld_stdout) != 1) + goto cleanup; + + /* Verify AP_REP. */ + packet_rep.length = length_rep; + packet_rep.data = (krb5_pointer)buf; + if ((retval = krb5_rd_rep(ctx, auth_ctx, &packet_rep, &reply))) + goto cleanup; + krb5_free_ap_rep_enc_part(ctx, reply); /* must be passed */ + + retval = 0; /* success */ + +cleanup: + if (server_princ) + krb5_free_principal(ctx, server_princ); + if (ccache) + krb5_cc_destroy(ctx, ccache); + if (packet_req.data) + krb5_free_data_contents(ctx, &packet_req); + + if (retval) + putil_err_krb5(args, retval, + "setuid-helper credential verification failed"); + + if (chld_stdin) + fclose(chld_stdin); + if (chld_stdout) + fclose(chld_stdout); + + int status; + while (waitpid(pid, &status, 0) < 0 && errno == EINTR) + continue; + + if (!(WIFEXITED(status) && WEXITSTATUS(status) == 0)) { + if (WIFEXITED(status)) { + putil_err(args, "failed to run setuid helper, exit code: %d\n", WEXITSTATUS(status)); + } else if (WIFSIGNALED(status)) { + putil_err(args, "failed to run setuid helper, killed by signal: %d\n", WTERMSIG(status)); + } else { + putil_err(args, "failed to run setuid helper, unknown status: %d\n", status); + } + retval = KRB5KRB_ERR_GENERIC; + } + + } else if (pid == 0) { + /* child */ + struct rlimit limit; + + char *argv[] = { KRB5_VFYCRD_PATH, NULL }; + char *envp[] = { NULL }; + + close(fd_stdin[1]); + close(fd_stdout[0]); + + if (dup2(fd_stdin[0], STDIN_FILENO) == -1) + _exit(1); + if (dup2(fd_stdout[1], STDOUT_FILENO) == -1) + _exit(2); + + if (getrlimit(RLIMIT_NOFILE, &limit) == 0) { + int i; + for (i = 0; i < (int)limit.rlim_max; i++) { + if (i != STDIN_FILENO + && i != STDOUT_FILENO + && i != STDERR_FILENO) + close(i); + } + } + + execve(argv[0], argv, envp); + _exit(3); + } else { + /* fork failed */ + retval = KRB5KRB_ERR_GENERIC; + + close(fd_stdin[0]); + close(fd_stdin[1]); + close(fd_stdout[0]); + close(fd_stdout[1]); + } + + sigaction(SIGCHLD, &saved_sa, NULL); + + return retval; +} + +static krb5_error_code +verify_creds(struct pam_args *args, krb5_creds *creds) +{ + /* Don't use the setuid-helper if a keytab was specified or when running + * as root. We assume the program has read-access to the keytab in those + * cases. */ + if (args->config->keytab || geteuid() == 0) + return verify_creds_normal(args, creds); + else + return verify_creds_setuid_helper(args, creds); +} + /* * Give the user a nicer error message when we've attempted PKINIT without diff --git a/krb5_vfycrd.c b/krb5_vfycrd.c new file mode 100644 index 0000000..aae99c7 --- /dev/null +++ b/krb5_vfycrd.c @@ -0,0 +1,126 @@ +/* + * Setuid helper for non-root processes to verify tickets against the local + * host keytab to prevent KDC spoofing attacks. + * + * Copyright 2016 Julian Brost <[email protected]> + * Copyright 2016 Simon Ruderich <[email protected]> + * + * See LICENSE for licensing terms. + */ + +#include <config.h> +#include <portable/system.h> +#include <portable/krb5.h> +#include <profile.h> + + +extern char **environ; + +int main() { + size_t length_princ_size_t; + /* Fixed size to allow communication between 32 and 64 bit programs and + * this helper on the same system. */ + uint32_t length_princ, length_req, length_rep; + + char *princ_name; + char buf[4096]; + krb5_context ctx; + profile_t profile; + int verify_ap_req_nofail = 0; + krb5_keytab keytab; + krb5_kt_cursor cursor; + krb5_keytab_entry entry; + krb5_principal princ = NULL; + krb5_data packet; + krb5_auth_context auth_ctx = NULL; + + /* We can't trust the environment. Ensure Kerberos doesn't use it. */ + environ = NULL; + + if (krb5_init_secure_context(&ctx)) + /* Use differing exit codes to help debugging. The codes are not + * stable and maybe change between releases. */ + exit(10); + + /* Read "verify_ap_req_nofail" option from /etc/krb5.conf. */ + if (krb5_get_profile(ctx, &profile)) + exit(11); + profile_get_boolean(profile, "libdefaults", "verify_ap_req_nofail", NULL, + 0 /* default value */, &verify_ap_req_nofail); + profile_release(profile); + profile = NULL; + + /* Get first principal of default keytab to verify against. */ + if (krb5_kt_default(ctx, &keytab)) + exit(20); + if (krb5_kt_start_seq_get(ctx, keytab, &cursor)) { + /* We failed to open the keytab. Abort if verify_ap_req_nofail is + * true, otherwise ignore the missing (or unreadable) keytab to + * replicate the behavior of krb5_verify_init_creds(). */ + if (verify_ap_req_nofail) + exit(21); + else + exit(0); /* success */ + } + if (krb5_kt_next_entry(ctx, keytab, &entry, &cursor)) + exit(22); + if (krb5_copy_principal(ctx, entry.principal, &princ)) + exit(23); + if (krb5_free_keytab_entry_contents(ctx, &entry)) + exit(24); + if (krb5_kt_end_seq_get(ctx, keytab, &cursor)) + exit(25); + if (krb5_kt_close(ctx, keytab)) + exit(26); + keytab = NULL; + + + /* Send (first) server principal name to client. */ + if (krb5_unparse_name(ctx, princ, &princ_name)) + exit(30); + length_princ_size_t = strlen(princ_name); + if (length_princ_size_t >= sizeof(buf)) /* principal must (later) fit into buffer */ + exit(31); + length_princ = (uint32_t)length_princ_size_t; + if (fwrite(&length_princ, sizeof(length_princ), 1, stdout) != 1) + exit(32); + if (fwrite(princ_name, length_princ, 1, stdout) != 1) + exit(33); + if (fflush(stdout)) + exit(34); + krb5_free_unparsed_name(ctx, princ_name); + + /* Receive AP_REQ from client. */ + if (fread(&length_req, sizeof(length_req), 1, stdin) != 1) + exit(40); + if (length_req > sizeof(buf)) + exit(41); + if (fread(buf, length_req, 1, stdin) != 1) + exit(42); + + /* Verify AP_REQ. This checks the ticket against the keytab and thus + * prevents KDC spoofing. */ + packet.length = length_req; + packet.data = (krb5_pointer)buf; + if (krb5_rd_req(ctx, &auth_ctx, &packet, princ, + NULL /* default keytab */, NULL, NULL)) + exit(50); + + /* Send AP_REP to client. */ + if (krb5_mk_rep(ctx, auth_ctx, &packet)) + exit(60); + length_rep = packet.length; + if (fwrite(&length_rep, sizeof(length_rep), 1, stdout) != 1) + exit(61); + if (fwrite(packet.data, length_rep, 1, stdout) != 1) + exit(62); + if (fflush(stdout)) + exit(63); + krb5_free_data_contents(ctx, &packet); + + krb5_free_principal(ctx, princ); + krb5_free_context(ctx); + + /* Verification successful. */ + exit(0); +} -- 2.1.4
signature.asc
Description: Digital signature

