1 /*
2 * Ticket creation routines for pam-krb5.
3 *
4 * pam_setcred and pam_open_session need to do similar but not identical work
5 * to create the user's ticket cache. The shared code is abstracted here into
6 * the pamk5_setcred function.
7 *
8 * Copyright 2005-2009, 2014, 2017, 2020 Russ Allbery <eagle@eyrie.org>
9 * Copyright 2011
10 * The Board of Trustees of the Leland Stanford Junior University
11 * Copyright 2005 Andres Salomon <dilinger@debian.org>
12 * Copyright 1999-2000 Frank Cusack <fcusack@fcusack.com>
13 *
14 * SPDX-License-Identifier: BSD-3-clause or GPL-1+
15 */
16
17 #include <config.h>
18 #include <portable/krb5.h>
19 #include <portable/pam.h>
20 #include <portable/system.h>
21
22 #include <assert.h>
23 #include <errno.h>
24 #include <pwd.h>
25
26 #include <module/internal.h>
27 #include <pam-util/args.h>
28 #include <pam-util/logging.h>
29
30
31 /*
32 * Given a cache name and an existing cache, initialize a new cache, store the
33 * credentials from the existing cache in it, and return a pointer to the new
34 * cache in the cache argument. Returns either PAM_SUCCESS or
35 * PAM_SERVICE_ERR.
36 */
37 static int
cache_init_from_cache(struct pam_args * args,const char * ccname,krb5_ccache old,krb5_ccache * cache)38 cache_init_from_cache(struct pam_args *args, const char *ccname,
39 krb5_ccache old, krb5_ccache *cache)
40 {
41 struct context *ctx;
42 krb5_creds creds;
43 krb5_cc_cursor cursor;
44 int pamret;
45 krb5_error_code status;
46
47 *cache = NULL;
48 memset(&creds, 0, sizeof(creds));
49 if (args == NULL || args->config == NULL || args->config->ctx == NULL
50 || args->config->ctx->context == NULL)
51 return PAM_SERVICE_ERR;
52 if (old == NULL)
53 return PAM_SERVICE_ERR;
54 ctx = args->config->ctx;
55 status = krb5_cc_start_seq_get(ctx->context, old, &cursor);
56 if (status != 0) {
57 putil_err_krb5(args, status, "cannot open new credentials");
58 return PAM_SERVICE_ERR;
59 }
60 status = krb5_cc_next_cred(ctx->context, old, &cursor, &creds);
61 if (status != 0) {
62 putil_err_krb5(args, status, "cannot read new credentials");
63 pamret = PAM_SERVICE_ERR;
64 goto done;
65 }
66 pamret = pamk5_cache_init(args, ccname, &creds, cache);
67 if (pamret != PAM_SUCCESS) {
68 krb5_free_cred_contents(ctx->context, &creds);
69 pamret = PAM_SERVICE_ERR;
70 goto done;
71 }
72 krb5_free_cred_contents(ctx->context, &creds);
73
74 /*
75 * There probably won't be any additional credentials, but check for them
76 * and copy them just in case.
77 */
78 while (krb5_cc_next_cred(ctx->context, old, &cursor, &creds) == 0) {
79 status = krb5_cc_store_cred(ctx->context, *cache, &creds);
80 krb5_free_cred_contents(ctx->context, &creds);
81 if (status != 0) {
82 putil_err_krb5(args, status,
83 "cannot store additional credentials"
84 " in %s",
85 ccname);
86 pamret = PAM_SERVICE_ERR;
87 goto done;
88 }
89 }
90 pamret = PAM_SUCCESS;
91
92 done:
93 krb5_cc_end_seq_get(ctx->context, ctx->cache, &cursor);
94 if (pamret != PAM_SUCCESS && *cache != NULL) {
95 krb5_cc_destroy(ctx->context, *cache);
96 *cache = NULL;
97 }
98 return pamret;
99 }
100
101
102 /*
103 * Determine the name of a new ticket cache. Handles ccache and ccache_dir
104 * PAM options and returns newly allocated memory.
105 *
106 * The ccache option, if set, contains a string with possible %u and %p
107 * escapes. The former is replaced by the UID and the latter is replaced by
108 * the PID (a suitable unique string).
109 */
110 static char *
build_ccache_name(struct pam_args * args,uid_t uid)111 build_ccache_name(struct pam_args *args, uid_t uid)
112 {
113 char *cache_name = NULL;
114 int retval;
115
116 if (args->config->ccache == NULL) {
117 retval = asprintf(&cache_name, "%s/krb5cc_%d_XXXXXX",
118 args->config->ccache_dir, (int) uid);
119 if (retval < 0) {
120 putil_crit(args, "malloc failure: %s", strerror(errno));
121 return NULL;
122 }
123 } else {
124 size_t len = 0, delta;
125 char *p, *q;
126
127 for (p = args->config->ccache; *p != '\0'; p++) {
128 if (p[0] == '%' && p[1] == 'u') {
129 len += snprintf(NULL, 0, "%ld", (long) uid);
130 p++;
131 } else if (p[0] == '%' && p[1] == 'p') {
132 len += snprintf(NULL, 0, "%ld", (long) getpid());
133 p++;
134 } else {
135 len++;
136 }
137 }
138 len++;
139 cache_name = malloc(len);
140 if (cache_name == NULL) {
141 putil_crit(args, "malloc failure: %s", strerror(errno));
142 return NULL;
143 }
144 for (p = args->config->ccache, q = cache_name; *p != '\0'; p++) {
145 if (p[0] == '%' && p[1] == 'u') {
146 delta = snprintf(q, len, "%ld", (long) uid);
147 q += delta;
148 len -= delta;
149 p++;
150 } else if (p[0] == '%' && p[1] == 'p') {
151 delta = snprintf(q, len, "%ld", (long) getpid());
152 q += delta;
153 len -= delta;
154 p++;
155 } else {
156 *q = *p;
157 q++;
158 len--;
159 }
160 }
161 *q = '\0';
162 }
163 return cache_name;
164 }
165
166
167 /*
168 * Create a new context for a session if we've lost the context created during
169 * authentication (such as when running under OpenSSH). Return PAM_IGNORE if
170 * we're ignoring this user or if apparently our pam_authenticate never
171 * succeeded.
172 */
173 static int
create_session_context(struct pam_args * args)174 create_session_context(struct pam_args *args)
175 {
176 struct context *ctx = NULL;
177 PAM_CONST char *user;
178 const char *tmpname;
179 int status, pamret;
180
181 /* If we're going to ignore the user anyway, don't even bother. */
182 if (args->config->ignore_root || args->config->minimum_uid > 0) {
183 pamret = pam_get_user(args->pamh, &user, NULL);
184 if (pamret == PAM_SUCCESS && pamk5_should_ignore(args, user)) {
185 pamret = PAM_IGNORE;
186 goto fail;
187 }
188 }
189
190 /*
191 * Create the context and locate the temporary ticket cache. Load the
192 * ticket cache back into the context and flush out the other data that
193 * would have been set if we'd kept our original context.
194 */
195 pamret = pamk5_context_new(args);
196 if (pamret != PAM_SUCCESS) {
197 putil_crit_pam(args, pamret, "creating session context failed");
198 goto fail;
199 }
200 ctx = args->config->ctx;
201 tmpname = pamk5_get_krb5ccname(args, "PAM_KRB5CCNAME");
202 if (tmpname == NULL) {
203 putil_debug(args, "unable to get PAM_KRB5CCNAME, assuming"
204 " non-Kerberos login");
205 pamret = PAM_IGNORE;
206 goto fail;
207 }
208 putil_debug(args, "found initial ticket cache at %s", tmpname);
209 status = krb5_cc_resolve(ctx->context, tmpname, &ctx->cache);
210 if (status != 0) {
211 putil_err_krb5(args, status, "cannot resolve cache %s", tmpname);
212 pamret = PAM_SERVICE_ERR;
213 goto fail;
214 }
215 status = krb5_cc_get_principal(ctx->context, ctx->cache, &ctx->princ);
216 if (status != 0) {
217 putil_err_krb5(args, status, "cannot retrieve principal");
218 pamret = PAM_SERVICE_ERR;
219 goto fail;
220 }
221
222 /*
223 * We've rebuilt the context. Push it back into the PAM state for any
224 * further calls to session or account management, which OpenSSH does keep
225 * the context for.
226 */
227 pamret = pam_set_data(args->pamh, "pam_krb5", ctx, pamk5_context_destroy);
228 if (pamret != PAM_SUCCESS) {
229 putil_err_pam(args, pamret, "cannot set context data");
230 goto fail;
231 }
232 return PAM_SUCCESS;
233
234 fail:
235 pamk5_context_free(args);
236 return pamret;
237 }
238
239
240 /*
241 * Sets user credentials by creating the permanent ticket cache and setting
242 * the proper ownership. This function may be called by either pam_sm_setcred
243 * or pam_sm_open_session. The refresh flag should be set to true if we
244 * should reinitialize an existing ticket cache instead of creating a new one.
245 */
246 int
pamk5_setcred(struct pam_args * args,bool refresh)247 pamk5_setcred(struct pam_args *args, bool refresh)
248 {
249 struct context *ctx = NULL;
250 krb5_ccache cache = NULL;
251 char *cache_name = NULL;
252 bool set_context = false;
253 int status = 0;
254 int pamret;
255 struct passwd *pw = NULL;
256 uid_t uid;
257 gid_t gid;
258
259 /* If configured not to create a cache, we have nothing to do. */
260 if (args->config->no_ccache) {
261 pamret = PAM_SUCCESS;
262 goto done;
263 }
264
265 /*
266 * If we weren't able to obtain a context, we were probably run by OpenSSH
267 * with its weird PAM handling, so we're going to cobble up a new context
268 * for ourselves.
269 */
270 pamret = pamk5_context_fetch(args);
271 if (pamret != PAM_SUCCESS) {
272 putil_debug(args, "no context found, creating one");
273 pamret = create_session_context(args);
274 if (pamret != PAM_SUCCESS || args->config->ctx == NULL)
275 goto done;
276 set_context = true;
277 }
278 ctx = args->config->ctx;
279
280 /*
281 * Some programs (xdm, for instance) appear to call setcred over and over
282 * again, so avoid doing useless work.
283 */
284 if (ctx->initialized) {
285 pamret = PAM_SUCCESS;
286 goto done;
287 }
288
289 /*
290 * Get the uid. The user is not required to be a local account for
291 * pam_authenticate, but for either pam_setcred (other than DELETE) or for
292 * pam_open_session, the user must be a local account.
293 */
294 pw = pam_modutil_getpwnam(args->pamh, ctx->name);
295 if (pw == NULL) {
296 putil_err(args, "getpwnam failed for %s", ctx->name);
297 pamret = PAM_USER_UNKNOWN;
298 goto done;
299 }
300 uid = pw->pw_uid;
301 gid = pw->pw_gid;
302
303 /* Get the cache name. If reinitializing, this is our existing cache. */
304 if (refresh) {
305 const char *name, *k5name;
306
307 /*
308 * Solaris su calls pam_setcred as root with PAM_REINITIALIZE_CREDS,
309 * preserving the user-supplied environment. An xlock program may
310 * also do this if it's setuid root and doesn't drop credentials
311 * before calling pam_setcred.
312 *
313 * There isn't any safe way of reinitializing the exiting ticket cache
314 * for the user if we're setuid without calling setreuid(). Calling
315 * setreuid() is possible, but if the calling application is threaded,
316 * it will change credentials for the whole application, with possibly
317 * bizarre and unintended (and insecure) results. Trying to verify
318 * ownership of the existing ticket cache before using it fails under
319 * various race conditions (for example, having one of the elements of
320 * the path be a symlink and changing the target of that symlink
321 * between our check and the call to krb5_cc_resolve). Without
322 * calling setreuid(), we run the risk of replacing a file owned by
323 * another user with a credential cache.
324 *
325 * We could fail with an error in the setuid case, which would be
326 * maximally safe, but it would prevent use of the module for
327 * authentication with programs such as Solaris su. Failure to
328 * reinitialize the cache is normally not a serious problem, just a
329 * missing feature. We therefore log an error and exit with
330 * PAM_SUCCESS for the setuid case.
331 *
332 * We do not use issetugid here since it always returns true if setuid
333 * was was involved anywhere in the process of running the binary.
334 * This would prevent a setuid screensaver that drops permissions from
335 * refreshing a credential cache. The issetugid behavior is safer,
336 * since the environment should ideally not be trusted even if the
337 * binary completely changed users away from the original user, but in
338 * that case the binary needs to take some responsibility for either
339 * sanitizing the environment or being certain that the calling user
340 * is permitted to act as the target user.
341 */
342 if (getuid() != geteuid() || getgid() != getegid()) {
343 putil_err(args, "credential reinitialization in a setuid context"
344 " ignored");
345 pamret = PAM_SUCCESS;
346 goto done;
347 }
348 name = pamk5_get_krb5ccname(args, "KRB5CCNAME");
349 if (name == NULL)
350 name = krb5_cc_default_name(ctx->context);
351 if (name == NULL) {
352 putil_err(args, "unable to get ticket cache name");
353 pamret = PAM_SERVICE_ERR;
354 goto done;
355 }
356 if (strncmp(name, "FILE:", strlen("FILE:")) == 0)
357 name += strlen("FILE:");
358
359 /*
360 * If the cache we have in the context and the cache we're
361 * reinitializing are the same cache, don't do anything; otherwise,
362 * we'll end up destroying the cache. This should never happen; this
363 * case triggering is a sign of a bug, probably in the calling
364 * application.
365 */
366 if (ctx->cache != NULL) {
367 k5name = krb5_cc_get_name(ctx->context, ctx->cache);
368 if (k5name != NULL) {
369 if (strncmp(k5name, "FILE:", strlen("FILE:")) == 0)
370 k5name += strlen("FILE:");
371 if (strcmp(name, k5name) == 0) {
372 pamret = PAM_SUCCESS;
373 goto done;
374 }
375 }
376 }
377
378 cache_name = strdup(name);
379 if (cache_name == NULL) {
380 putil_crit(args, "malloc failure: %s", strerror(errno));
381 pamret = PAM_BUF_ERR;
382 goto done;
383 }
384 putil_debug(args, "refreshing ticket cache %s", cache_name);
385
386 /*
387 * If we're refreshing the cache, we didn't really create it and the
388 * user's open session created by login is probably still managing
389 * it. Thus, don't remove it when PAM is shut down.
390 */
391 ctx->dont_destroy_cache = 1;
392 } else {
393 char *cache_name_tmp;
394 size_t len;
395
396 cache_name = build_ccache_name(args, uid);
397 if (cache_name == NULL) {
398 pamret = PAM_BUF_ERR;
399 goto done;
400 }
401 len = strlen(cache_name);
402 if (len > 6 && strncmp("XXXXXX", cache_name + len - 6, 6) == 0) {
403 if (strncmp(cache_name, "FILE:", strlen("FILE:")) == 0)
404 cache_name_tmp = cache_name + strlen("FILE:");
405 else
406 cache_name_tmp = cache_name;
407 pamret = pamk5_cache_mkstemp(args, cache_name_tmp);
408 if (pamret != PAM_SUCCESS)
409 goto done;
410 }
411 putil_debug(args, "initializing ticket cache %s", cache_name);
412 }
413
414 /*
415 * Initialize the new ticket cache and point the environment at it. Only
416 * chown the cache if the cache is of type FILE or has no type (making the
417 * assumption that the default cache type is FILE; otherwise, due to the
418 * type prefix, we'd end up with an invalid path.
419 */
420 pamret = cache_init_from_cache(args, cache_name, ctx->cache, &cache);
421 if (pamret != PAM_SUCCESS)
422 goto done;
423 if (strncmp(cache_name, "FILE:", strlen("FILE:")) == 0)
424 status = chown(cache_name + strlen("FILE:"), uid, gid);
425 else if (strchr(cache_name, ':') == NULL)
426 status = chown(cache_name, uid, gid);
427 if (status == -1) {
428 putil_crit(args, "chown of ticket cache failed: %s", strerror(errno));
429 pamret = PAM_SERVICE_ERR;
430 goto done;
431 }
432 pamret = pamk5_set_krb5ccname(args, cache_name, "KRB5CCNAME");
433 if (pamret != PAM_SUCCESS) {
434 putil_crit(args, "setting KRB5CCNAME failed: %s", strerror(errno));
435 goto done;
436 }
437
438 /*
439 * If we had a temporary ticket cache, delete the environment variable so
440 * that we won't get confused and think we still have a temporary ticket
441 * cache when called again.
442 *
443 * FreeBSD PAM, at least as of 7.2, doesn't support deleting environment
444 * variables using the syntax supported by Solaris and Linux. Work
445 * around that by setting the variable to an empty value if deleting it
446 * fails.
447 */
448 if (pam_getenv(args->pamh, "PAM_KRB5CCNAME") != NULL) {
449 pamret = pam_putenv(args->pamh, "PAM_KRB5CCNAME");
450 if (pamret != PAM_SUCCESS)
451 pamret = pam_putenv(args->pamh, "PAM_KRB5CCNAME=");
452 if (pamret != PAM_SUCCESS)
453 goto done;
454 }
455
456 /* Destroy the temporary cache and put the new cache in the context. */
457 krb5_cc_destroy(ctx->context, ctx->cache);
458 ctx->cache = cache;
459 cache = NULL;
460 ctx->initialized = 1;
461 if (args->config->retain_after_close)
462 ctx->dont_destroy_cache = 1;
463
464 done:
465 if (ctx != NULL && cache != NULL)
466 krb5_cc_destroy(ctx->context, cache);
467 free(cache_name);
468
469 /* If we stored our Kerberos context in PAM data, don't free it. */
470 if (set_context)
471 args->ctx = NULL;
472
473 return pamret;
474 }
475