Package: src:libpam-krb5 Followup-For: Bug #399002 Hello,
Revised patch attached which adds support for Heimdal (the Debian package with our patch builds fine now) and fixes backwards compatibility with verify_ap_req_nofail = false (the old patch always rejected missing KDC validation even if verify_ap_req_nofail was set to false). Regards Simon -- + privacy is necessary + using gnupg http://gnupg.org + public key id: 0x1972F726F0D556E7
From 38dc32de02d7f5e09af4679d437e459eec441a82 Mon Sep 17 00:00:00 2001 Message-Id: <38dc32de02d7f5e09af4679d437e459eec441a82.1465574311.git.si...@ruderich.org> From: Simon Ruderich <si...@ruderich.org> Date: Fri, 10 Jun 2016 17:16:43 +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 | 214 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++- krb5_vfycrd.c | 150 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 371 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..ed8c68c 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,214 @@ 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 (length_princ == 0) { + /* Special case: helper could not read keytab and + * verify_ap_req_nofail is false, skip KDC authenticity + * validation. */ + retval = 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..8efc978 --- /dev/null +++ b/krb5_vfycrd.c @@ -0,0 +1,150 @@ +/* + * Setuid helper for non-root processes to verify tickets against the local + * host keytab to prevent KDC spoofing attacks. + * + * Copyright 2016 Julian Brost <jul...@0x4a42.net> + * Copyright 2016 Simon Ruderich <si...@ruderich.org> + * + * See LICENSE for licensing terms. + */ + +#include <config.h> +#include <portable/system.h> +#include <portable/krb5.h> +#ifdef HAVE_KRB5_MIT +# include <profile.h> +#endif + + +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; +#ifdef HAVE_KRB5_MIT + profile_t profile; +#endif + 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. */ +#ifdef HAVE_KRB5_MIT + 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; +#else /* Heimdal */ + verify_ap_req_nofail = krb5_config_get_bool_default(ctx, NULL, + 0 /* default value */, + "libdefaults", "verify_ap_req_nofail", NULL); +#endif + + /* 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 { + /* Empty princial name indicates success to caller. */ + length_princ = 0; + if (fwrite(&length_princ, sizeof(length_princ), 1, stdout) != 1) + exit(22); + exit(0); /* success */ + } + } + if (krb5_kt_next_entry(ctx, keytab, &entry, &cursor)) + exit(23); + if (krb5_copy_principal(ctx, entry.principal, &princ)) + exit(24); +/* krb5_kt_free_entry() is deprecated in MIT, but its replacement + * krb5_free_keytab_entry_contents() is not available in Heimdal. */ +#ifndef HAVE_KRB5_MIT +# define krb5_free_keytab_entry_contents krb5_kt_free_entry +#endif + if (krb5_free_keytab_entry_contents(ctx, &entry)) + exit(25); + if (krb5_kt_end_seq_get(ctx, keytab, &cursor)) + exit(26); + if (krb5_kt_close(ctx, keytab)) + exit(27); + 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); + /* Ensure principal is not an empty string as length 0 indicates success + * if keytab can't be read and verify_ap_req_nofail is false (see above). */ + if (length_princ_size_t == 0) + exit(31); + if (length_princ_size_t >= sizeof(buf)) /* principal must (later) fit into buffer */ + exit(32); + length_princ = (uint32_t)length_princ_size_t; + if (fwrite(&length_princ, sizeof(length_princ), 1, stdout) != 1) + exit(33); + if (fwrite(princ_name, length_princ, 1, stdout) != 1) + exit(34); + if (fflush(stdout)) + exit(35); + 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