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 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 * 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 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 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