Commit message: pam_userdb: fix password hash comparison

Starting with commit Linux-PAM-0-77-28-g0b3e583 that introduced hashed
passwords support in pam_userdb, hashes are compared case-insensitively.
This bug leads to accepting hashes for completely different passwords in
addition to those that should be accepted.

Additionally, commit Linux-PAM-1_1_6-13-ge2a8187 that added support for
modern password hashes with different lengths and settings, did not
update the hash comparison accordingly, which leads to accepting
computed hashes longer than stored hashes when the latter is a prefix
of the former.

* modules/pam_userdb/pam_userdb.c (user_lookup): Reject the computed
hash whose length differs from the stored hash length.
Compare computed and stored hashes case-sensitively.
Fixes CVE-2013-7041.

Bug-Debian: http://bugs.debian.org/731368
CVE: None
Vulnerability description: None

Project: linux-pam
File: pam_userdb.c

obtain_authtok(pam_handle_t *pamh)
{
    char *resp;
    const void *item;
    int retval;

    retval = pam_prompt(pamh, PAM_PROMPT_ECHO_OFF, &resp, _("Password: "));

    if (retval != PAM_SUCCESS)
	return retval;

    if (resp == NULL)
	return PAM_CONV_ERR;

    /* set the auth token */
    retval = pam_set_item(pamh, PAM_AUTHTOK, resp);

    /* clean it up */
    _pam_overwrite(resp);
    _pam_drop(resp);

    if ( (retval != PAM_SUCCESS) ||
	 (retval = pam_get_item(pamh, PAM_AUTHTOK, &item))
	 != PAM_SUCCESS ) {
	return retval;
    }

    return retval;
}

Project: linux-pam
File: pam_userdb.c

_pam_parse (pam_handle_t *pamh, int argc, const char **argv,
	    const char **database, const char **cryptmode)
{
  int ctrl;

  *database = NULL;
  *cryptmode = NULL;

  /* step through arguments */
  for (ctrl = 0; argc-- > 0; ++argv)
    {
      /* generic options */

      if (!strcmp(*argv,"debug"))
	ctrl |= PAM_DEBUG_ARG;
      else if (!strcasecmp(*argv, "icase"))
	ctrl |= PAM_ICASE_ARG;
      else if (!strcasecmp(*argv, "dump"))
	ctrl |= PAM_DUMP_ARG;
      else if (!strcasecmp(*argv, "unknown_ok"))
	ctrl |= PAM_UNKNOWN_OK_ARG;
      else if (!strcasecmp(*argv, "key_only"))
	ctrl |= PAM_KEY_ONLY_ARG;
      else if (!strcasecmp(*argv, "use_first_pass"))
	ctrl |= PAM_USE_FPASS_ARG;
      else if (!strcasecmp(*argv, "try_first_pass"))
	ctrl |= PAM_TRY_FPASS_ARG;
      else if (!strncasecmp(*argv,"db=", 3))
	{
	  *database = (*argv) + 3;
	  if (**database == '\0') {
	    *database = NULL;
	    pam_syslog(pamh, LOG_ERR,
		       "db= specification missing argument - ignored");
	  }
	}
      else if (!strncasecmp(*argv,"crypt=", 6))
	{
	  *cryptmode = (*argv) + 6;
	  if (**cryptmode == '\0')
	    pam_syslog(pamh, LOG_ERR,
		       "crypt= specification missing argument - ignored");
	}
      else
	{
	  pam_syslog(pamh, LOG_ERR, "unknown option: %s", *argv);
	}
    }

  return ctrl;
}

Project: linux-pam
File: pam_userdb.c

user_lookup (pam_handle_t *pamh, const char *database, const char *cryptmode,
	     const char *user, const char *pass, int ctrl)
{
    DBM *dbm;
    datum key, data;

    /* Open the DB file. */
    dbm = dbm_open(database, O_RDONLY, 0644);
    if (dbm == NULL) {
	pam_syslog(pamh, LOG_ERR,
		   "user_lookup: could not open database `%s': %m", database);
	return -2;
    }

    /* dump out the database contents for debugging */
    if (ctrl & PAM_DUMP_ARG) {
	pam_syslog(pamh, LOG_INFO, "Database dump:");
	for (key = dbm_firstkey(dbm);  key.dptr != NULL;
	     key = dbm_nextkey(dbm)) {
	    data = dbm_fetch(dbm, key);
	    pam_syslog(pamh, LOG_INFO,
		       "key[len=%d] = `%s', data[len=%d] = `%s'",
		       key.dsize, key.dptr, data.dsize, data.dptr);
	}
    }

    /* do some more init work */
    memset(&key, 0, sizeof(key));
    memset(&data, 0, sizeof(data));
    if (ctrl & PAM_KEY_ONLY_ARG) {
	if (asprintf(&key.dptr, "%s-%s", user, pass) < 0)
	    key.dptr = NULL;
	else
	    key.dsize = strlen(key.dptr);
    } else {
        key.dptr = x_strdup(user);
        key.dsize = strlen(user);
    }

    if (key.dptr) {
	data = dbm_fetch(dbm, key);
	memset(key.dptr, 0, key.dsize);
	free(key.dptr);
    }

    if (ctrl & PAM_DEBUG_ARG) {
	pam_syslog(pamh, LOG_INFO,
		   "password in database is [%p]`%.*s', len is %d",
		   data.dptr, data.dsize, (char *) data.dptr, data.dsize);
    }

    if (data.dptr != NULL) {
	int compare = 0;

	if (ctrl & PAM_KEY_ONLY_ARG)
	  {
	    dbm_close (dbm);
	    return 0; /* found it, data contents don't matter */
	}

	if (cryptmode && strncasecmp(cryptmode, "crypt", 5) == 0) {

	  /* crypt(3) password storage */

	  char *cryptpw;

	  if (data.dsize < 13) {
	    compare = -2;
	  } else if (ctrl & PAM_ICASE_ARG) {
	    compare = -2;
	  } else {
	    cryptpw = crypt (pass, data.dptr);

	    if (cryptpw && strlen(cryptpw) == (size_t)data.dsize) {
	      compare = memcmp(data.dptr, cryptpw, data.dsize);
	    } else {
	      compare = -2;
	      if (ctrl & PAM_DEBUG_ARG) {
		if (cryptpw)
		  pam_syslog(pamh, LOG_INFO, "lengths of computed and stored hashes differ");
		else
		  pam_syslog(pamh, LOG_INFO, "crypt() returned NULL");
	      }
	    };

	  };

	} else {

	  /* Unknown password encryption method -
	   * default to plaintext password storage
	   */

	  if (strlen(pass) != (size_t)data.dsize) {
	    compare = 1; /* wrong password len -> wrong password */
	  } else if (ctrl & PAM_ICASE_ARG) {
	    compare = strncasecmp(data.dptr, pass, data.dsize);
	  } else {
	    compare = strncmp(data.dptr, pass, data.dsize);
	  }

	  if (cryptmode && strncasecmp(cryptmode, "none", 4)
		&& (ctrl & PAM_DEBUG_ARG)) {
	    pam_syslog(pamh, LOG_INFO, "invalid value for crypt parameter: %s",
		       cryptmode);
	    pam_syslog(pamh, LOG_INFO, "defaulting to plaintext password mode");
	  }

	}

	dbm_close(dbm);
	if (compare == 0)
	    return 0; /* match */
	else
	    return -1; /* wrong */
    } else {
        int saw_user = 0;

	if (ctrl & PAM_DEBUG_ARG) {
	    pam_syslog(pamh, LOG_INFO, "error returned by dbm_fetch: %m");
	}

	/* probably we should check dbm_error() here */

        if ((ctrl & PAM_KEY_ONLY_ARG) == 0) {
	    dbm_close(dbm);
            return 1; /* not key_only, so no entry => no entry for the user */
        }

        /* now handle the key_only case */
        for (key = dbm_firstkey(dbm);
             key.dptr != NULL;
             key = dbm_nextkey(dbm)) {
            int compare;
            /* first compare the user portion (case sensitive) */
            compare = strncmp(key.dptr, user, strlen(user));
            if (compare == 0) {
                /* assume failure */
                compare = -1;
                /* if we have the divider where we expect it to be... */
                if (key.dptr[strlen(user)] == '-') {
		    saw_user = 1;
		    if ((size_t)key.dsize == strlen(user) + 1 + strlen(pass)) {
		        if (ctrl & PAM_ICASE_ARG) {
			    /* compare the password portion (case insensitive)*/
                            compare = strncasecmp(key.dptr + strlen(user) + 1,
                                                  pass,
                                                  strlen(pass));
		        } else {
                            /* compare the password portion (case sensitive) */
                            compare = strncmp(key.dptr + strlen(user) + 1,
                                              pass,
                                              strlen(pass));
		        }
		    }
                }
                if (compare == 0) {
                    dbm_close(dbm);
                    return 0; /* match */
                }
            }
        }
        dbm_close(dbm);
	if (saw_user)
	    return -1; /* saw the user, but password mismatch */
	else
	    return 1; /* not found */
    }

    /* NOT REACHED */
    return -2;
}

Project: linux-pam
File: pam_userdb.c

pam_sm_acct_mgmt(pam_handle_t *pamh, int flags UNUSED,
		 int argc, const char **argv)
{
    const char *username;
    const char *database = NULL;
    const char *cryptmode = NULL;
    int retval = PAM_AUTH_ERR, ctrl;

    /* parse arguments */
    ctrl = _pam_parse(pamh, argc, argv, &database, &cryptmode);

    /* Get the username */
    retval = pam_get_user(pamh, &username, NULL);
    if ((retval != PAM_SUCCESS) || (!username)) {
        pam_syslog(pamh, LOG_ERR,"can not get the username");
        return PAM_SERVICE_ERR;
    }

    /* Now use the username to look up password in the database file */
    retval = user_lookup(pamh, database, cryptmode, username, "", ctrl);
    switch (retval) {
        case -2:
	    /* some sort of system error. The log was already printed */
	    return PAM_SERVICE_ERR;
	case -1:
	    /* incorrect password, but we don't care */
	    /* FALL THROUGH */
	case 0:
	    /* authentication succeeded. dumbest password ever. */
	    return PAM_SUCCESS;
	case 1:
	    /* the user does not exist in the database */
	    return PAM_USER_UNKNOWN;
        default:
	    /* we don't know anything about this return value */
	    pam_syslog(pamh, LOG_ERR,
		       "internal module error (retval = %d, user = `%s'",
		       retval, username);
            return PAM_SERVICE_ERR;
    }

    return PAM_SUCCESS;
}

Project: linux-pam
File: pam_userdb.c

pam_sm_authenticate(pam_handle_t *pamh, int flags UNUSED,
		    int argc, const char **argv)
{
     const char *username;
     const void *password;
     const char *database = NULL;
     const char *cryptmode = NULL;
     int retval = PAM_AUTH_ERR, ctrl;

     /* parse arguments */
     ctrl = _pam_parse(pamh, argc, argv, &database, &cryptmode);
     if (database == NULL) {
        pam_syslog(pamh, LOG_ERR, "can not get the database name");
        return PAM_SERVICE_ERR;
     }

     /* Get the username */
     retval = pam_get_user(pamh, &username, NULL);
     if ((retval != PAM_SUCCESS) || (!username)) {
        pam_syslog(pamh, LOG_ERR, "can not get the username");
        return PAM_SERVICE_ERR;
     }

     if ((ctrl & PAM_USE_FPASS_ARG) == 0 && (ctrl & PAM_TRY_FPASS_ARG) == 0) {
        /* Converse to obtain a password */
        retval = obtain_authtok(pamh);
        if (retval != PAM_SUCCESS) {
	    pam_syslog(pamh, LOG_ERR, "can not obtain password from user");
	    return retval;
        }
     }

     /* Check if we got a password */
     retval = pam_get_item(pamh, PAM_AUTHTOK, &password);
     if (retval != PAM_SUCCESS || password == NULL) {
        if ((ctrl & PAM_TRY_FPASS_ARG) != 0) {
	    /* Converse to obtain a password */
	    retval = obtain_authtok(pamh);
	    if (retval != PAM_SUCCESS) {
	        pam_syslog(pamh, LOG_ERR, "can not obtain password from user");
		return retval;
	    }
	    retval = pam_get_item(pamh, PAM_AUTHTOK, &password);
	}
	if (retval != PAM_SUCCESS || password == NULL) {
	    pam_syslog(pamh, LOG_ERR, "can not recover user password");
	    return PAM_AUTHTOK_RECOVERY_ERR;
	}
     }

     if (ctrl & PAM_DEBUG_ARG)
	 pam_syslog(pamh, LOG_INFO, "Verify user `%s' with a password",
		    username);

     /* Now use the username to look up password in the database file */
     retval = user_lookup(pamh, database, cryptmode, username, password, ctrl);
     switch (retval) {
	 case -2:
	     /* some sort of system error. The log was already printed */
	     return PAM_SERVICE_ERR;
	 case -1:
	     /* incorrect password */
	     pam_syslog(pamh, LOG_WARNING,
			"user `%s' denied access (incorrect password)",
			username);
	     return PAM_AUTH_ERR;
	 case 1:
	     /* the user does not exist in the database */
	     if (ctrl & PAM_DEBUG_ARG)
		 pam_syslog(pamh, LOG_NOTICE,
			    "user `%s' not found in the database", username);
	     return PAM_USER_UNKNOWN;
	 case 0:
	     /* Otherwise, the authentication looked good */
	     pam_syslog(pamh, LOG_NOTICE, "user '%s' granted access", username);
	     return PAM_SUCCESS;
	 default:
	     /* we don't know anything about this return value */
	     pam_syslog(pamh, LOG_ERR,
		      "internal module error (retval = %d, user = `%s'",
		      retval, username);
	     return PAM_SERVICE_ERR;
     }

     /* should not be reached */
     return PAM_IGNORE;
}

Project: linux-pam
File: pam_userdb.c

pam_sm_setcred(pam_handle_t *pamh UNUSED, int flags UNUSED,
	       int argc UNUSED, const char **argv UNUSED)
{
    return PAM_SUCCESS;
}