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

Attachment: signature.asc
Description: Digital signature

Reply via email to