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

Attachment: signature.asc
Description: Digital signature

Reply via email to