/* * hlogin(1) * * Copyleft (C) 2022 ~keith * * This file is part of hlogin. * * hlogin is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License as published by the * Free Software Foundation, version 3. * * hlogin is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * for more details. * * You should have received a copy of the GNU General Public License * along with hlogin. If not, see . */ #define __USE_GNU #define _GNU_SOURCE #include "config.h" #include #include #include #include #include "gettext.h" #define VAR_(x) #x #define VAR(x) VAR_(x) #pragma message "LOCALEDIR=" VAR(LOCALEDIR) #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "ui.h" int pam_conv_handler(int msgc, const struct pam_message *msgv[], struct pam_response **resp, void *appdata_ptr) { if (msgc <= 0) return PAM_CONV_ERR; struct pam_response *result = calloc(msgc, sizeof(struct pam_response)); if (!result) return PAM_CONV_ERR; char *info_concat = NULL; size_t info_len; for (int i = 0; i < msgc; i++) { int r; char *text; switch (msgv[i]->msg_style) { case PAM_PROMPT_ECHO_OFF: case PAM_PROMPT_ECHO_ON: if (info_concat) { ui_setup_message(info_concat); ui_message_run(); free(info_concat); info_concat = NULL; } ui_setup_dialog((char *) msgv[i]->msg, msgv[i]->msg_style == PAM_PROMPT_ECHO_OFF); r = ui_run(); text = ui_get_text(); if (r != 0) { _pam_overwrite(text); free(text); text = NULL; } result[i].resp_retcode = 0; result[i].resp = text; break; case PAM_ERROR_MSG: case PAM_TEXT_INFO: if (!info_concat) { info_len = strlen((char *) msgv[i]->msg); info_concat = realloc(info_concat, sizeof(char) * (info_len + 1)); strcpy(info_concat, (char *) msgv[i]->msg); info_concat[info_len] = '\0'; } else { info_concat[info_len] = '\n'; size_t pos = info_len + 1; info_len = pos + strlen((char *) msgv[i]->msg); info_concat = realloc(info_concat, sizeof(char) * (info_len + 1)); strcpy(info_concat + pos, (char *) msgv[i]->msg); info_concat[info_len] = '\0'; } result[i].resp_retcode = 0; result[i].resp = NULL; break; default: if (info_concat) { ui_setup_message(info_concat); ui_message_run(); free(info_concat); info_concat = NULL; } goto FAILURE; } } if (info_concat) { ui_setup_message(info_concat); ui_message_run(); free(info_concat); info_concat = NULL; } *resp = result; return PAM_SUCCESS; FAILURE: if (result) { for (int i = 0; i < msgc; i++) { if (result[i].resp) { _pam_overwrite(result[i].resp); free(result[i].resp); } } free(result); result = NULL; } return PAM_CONV_ERR; } /** number of seconds to hang after a login failure or error */ unsigned int fail_delay = 3; /** maximum number of login attempts before a failure */ unsigned int max_tries = 5; char *origin = "test_origin"; char *tty_path = NULL; char *tty_name = NULL; char *tty_id = NULL; char *remote_host = NULL; pid_t login_pid; void log_utmp(char *login_name) { struct utmpx ut; memset(&ut, 0, sizeof(ut)); struct utmpx *ent; setutxent(); while ((ent = getutxent())) if (ent->ut_pid == login_pid && ent->ut_type >= INIT_PROCESS && ent->ut_type <= DEAD_PROCESS) break; // try by tty_name if (ent == NULL && tty_name) { setutxent(); ut.ut_type = LOGIN_PROCESS; strncpy(ut.ut_line, tty_name, sizeof(ut.ut_line)); ent = getutxline(&ut); } // try by tty_id if (ent == NULL && tty_id) { setutxent(); ut.ut_type = DEAD_PROCESS; strncpy(ut.ut_id, tty_id, sizeof(ut.ut_id)); ent = getutxid(&ut); } // copy or create if (ent) memcpy(&ut, ent, sizeof(ut)); else memset(&ut, 0, sizeof(ut)); // update utmp entry strncpy(ut.ut_user, login_name, sizeof(ut.ut_user)); ut.ut_type = USER_PROCESS; ut.ut_pid = login_pid; if (tty_id && ut.ut_id[0] == 0) strncpy(ut.ut_id, tty_id, sizeof(ut.ut_id)); if (tty_name) strncpy(ut.ut_line, tty_name, sizeof(ut.ut_line)); if (remote_host) strncpy(ut.ut_host, remote_host, sizeof(ut.ut_host)); // TODO copy remote_addr // set time struct timeval tv; gettimeofday(&tv, NULL); ut.ut_tv.tv_sec = tv.tv_sec; ut.ut_tv.tv_usec = tv.tv_usec; pututxline(&ut); endutxent(); updwtmpx(_PATH_WTMP, &ut); } const char *CANCEL_SENTINEL = "\0CANCEL\0"; /** try_login - prompt for login name & password and attempt to log in * returns login_name on success or NULL on failure */ char *try_login(pam_handle_t *pam, unsigned int try_count) { int result; // prompt for username ui_setup_dialog(_("Login name:"), false); result = ui_run(); char *login_name = ui_get_text(); if (result != 0) return CANCEL_SENTINEL; result = pam_set_item(pam, PAM_USER, login_name); if (result != PAM_SUCCESS) { syslog(LOG_NOTICE, _("Couldn't set PAM_USER: %s"), pam_strerror(pam, result)); ui_setup_message(_("Failed to begin authentication.")); ui_message_run(); return NULL; } result = pam_authenticate(pam, 0); if (result != PAM_SUCCESS) { // log to syslog if (result == PAM_MAXTRIES || try_count >= max_tries) syslog(LOG_NOTICE, _("TOO MANY LOGIN TRIES (%u) FROM '%s' FOR '%s', %s"), try_count, origin, login_name, pam_strerror(pam, result)); else syslog(LOG_NOTICE, _("FAILED LOGIN (%u) FROM '%s' FOR '%s', %s"), try_count, origin, login_name, pam_strerror(pam, result)); // display error message ui_setup_message(_("Authentication failed.")); ui_message_run(); return NULL; } return login_name; } int get_pam_login_name(pam_handle_t *pam, char **login_name) { const void *item = (const void *) *login_name; int result = pam_get_item(pam, PAM_USER, &item); *login_name = (char *) item; return result; } void init_env(bool no_destroy, pam_handle_t *pam, struct passwd *pwd) { char *buf = getenv("TERM"); if (buf) buf = strdup(buf); if (!no_destroy) environ = calloc(1, sizeof(char *)); setenv("HOME", pwd->pw_dir, 0); setenv("USER", pwd->pw_name, 1); setenv("LOGNAME", pwd->pw_name, 1); setenv("SHELL", pwd->pw_shell, 1); setenv("TERM", buf ? buf : "dumb", 1); free(buf); // TODO use ENV_PATH and ENV_SUPATH from logindefs setenv("PATH", _PATH_DEFPATH, 0); // apparently mailx is a bitch without this asprintf(&buf, "%s/%s", _PATH_MAILDIR, pwd->pw_name); setenv("MAIL", buf, 0); free(buf); char **pam_env = pam_getenvlist(pam); for (int i = 0; pam_env && pam_env[i]; i++) putenv(pam_env[i]); } static pid_t child_pid; static volatile sig_atomic_t got_sig = 0; static void handle_sig(int signal) { if (child_pid) kill(-child_pid, signal); else got_sig = 1; if (signal == SIGTERM) kill(-child_pid, SIGHUP); } int main(int argc, char* argv[]) { int result; setlocale(LC_ALL, ""); bindtextdomain(PACKAGE, LOCALEDIR); textdomain(PACKAGE); openlog("hlogin", LOG_ODELAY, LOG_AUTHPRIV); login_pid = getpid(); if (isatty(STDIN_FILENO)) tty_path = ttyname(STDIN_FILENO); else if (isatty(STDOUT_FILENO)) tty_path = ttyname(STDOUT_FILENO); if (tty_path) { if (strncmp(tty_path, "/dev/", 5) == 0) tty_name = tty_path + 5; else tty_name = tty_path; for (char *x = tty_name; x && *x; x++) { if ((*x) >= '0' && (*x) <= '9') { tty_id = x; break; } } } // TODO remote login stuff if (remote_host) origin = remote_host; else if (tty_name) origin = tty_name; else syslog(LOG_NOTICE, _("Couldn't determine origin: neither remote host nor TTY")); pam_handle_t *pam = NULL; struct pam_conv conv = { .conv = pam_conv_handler, .appdata_ptr = NULL }; result = pam_start(remote_host ? "remote" : "login", NULL, &conv, &pam); if (result != PAM_SUCCESS) { syslog(LOG_ERR, _("Couldn't initialize PAM: %s"), pam_strerror(pam, result)); fprintf(stderr, _("pam_start error: %s. Abort.\n"), pam_strerror(pam, result)); sleep(fail_delay); return EXIT_FAILURE; } result = pam_set_item(pam, PAM_RHOST, remote_host); if (result != PAM_SUCCESS) { syslog(LOG_ERR, _("Couldn't set PAM_RHOST: %s"), pam_strerror(pam, result)); fprintf(stderr, _("pam_set_item error: %s. Abort.\n"), pam_strerror(pam, result)); sleep(fail_delay); return EXIT_FAILURE; } if (tty_path) { result = pam_set_item(pam, PAM_TTY, tty_path); if (result != PAM_SUCCESS) { syslog(LOG_ERR, _("Couldn't set PAM_TTY: %s"), pam_strerror(pam, result)); fprintf(stderr, _("pam_set_item error: %s. Abort.\n"), pam_strerror(pam, result)); sleep(fail_delay); return EXIT_FAILURE; } } ui_init(); char *login_name; for (unsigned int try = 1; try <= max_tries; try++) { login_name = try_login(pam, try); if (login_name) break; } if (login_name == CANCEL_SENTINEL) { syslog(LOG_NOTICE, _("Login from '%s' aborted"), origin); ui_end(); sleep(fail_delay); return EXIT_SUCCESS; } if (!login_name) { char *msg; // TRANSLATORS: default value is 5 asprintf(&msg, _("Exceeded maximum login attempts (%u).\n"), max_tries); ui_setup_message(msg); ui_message_run(); sleep(fail_delay); ui_end(); return EXIT_FAILURE; } // Is user account valid? result = pam_acct_mgmt(pam, 0); if (result == PAM_NEW_AUTHTOK_REQD) result = pam_chauthtok(pam, PAM_CHANGE_EXPIRED_AUTHTOK); if (result != PAM_SUCCESS) { syslog(LOG_ERR, _("User validation failed for '%s': %s"), login_name, pam_strerror(pam, result)); switch (result) { /* case PAM_ACCT_EXPIRED: asprintf(&msg, _("The user account '%s' has expired and cannot log in."), login_name); break; */ case PAM_AUTHTOK_ERR: case PAM_AUTHTOK_RECOVERY_ERR: case PAM_AUTHTOK_LOCK_BUSY: case PAM_AUTHTOK_DISABLE_AGING: // asprintf(&msg, _("Failed to update login credentials for '%s'."), login_name); ui_setup_message(_("Failed to update login credentials.")); break; default: // asprintf(&msg, _("Cannot log in as '%s'. Is the user account valid?"), login_name); ui_setup_message(_("Cannot log in.")); } ui_message_run(); sleep(fail_delay); ui_end(); return EXIT_FAILURE; } // ensure we have the correct login_name result = get_pam_login_name(pam, &login_name); if (result != PAM_SUCCESS) { syslog(LOG_ERR, _("Couldn't get PAM_USER: %s"), pam_strerror(pam, result)); ui_setup_message(_("Cannot log in. The user account data is missing or invalid.")); ui_message_run(); sleep(fail_delay); ui_end(); return EXIT_FAILURE; } if ((!login_name) || login_name[0] == '\0') { syslog(LOG_ERR, _("NULL user name. Abort.")); ui_setup_message(_("Cannot log in. The user account data is missing or invalid.")); ui_message_run(); sleep(fail_delay); ui_end(); return EXIT_FAILURE; } struct passwd *pwd = getpwnam(login_name); if (!pwd) { syslog(LOG_ERR, _("No passwd entry for '%s'. Abort."), login_name); ui_setup_message(_("Cannot log in. The user account data is missing or invalid.")); ui_message_run(); sleep(fail_delay); ui_end(); return EXIT_FAILURE; } result = (pwd->pw_uid != 0) ? initgroups(login_name, pwd->pw_gid) : setgroups(0, NULL); if (result < 0) { syslog(LOG_ERR, _("groups initialization failed: %m")); ui_setup_message(_("Cannot log in. Unable to establish a session.")); ui_message_run(); sleep(fail_delay); ui_end(); return EXIT_FAILURE; } // begin PAM session result = pam_setcred(pam, PAM_ESTABLISH_CRED); if (result != PAM_SUCCESS) { syslog(LOG_ERR, _("Couldn't init PAM session: %s"), pam_strerror(pam, result)); ui_setup_message(_("Cannot log in. Unable to establish a session.")); ui_message_run(); sleep(fail_delay); ui_end(); return EXIT_FAILURE; } result = pam_open_session(pam, 0); if (result != PAM_SUCCESS) { pam_setcred(pam, PAM_DELETE_CRED); syslog(LOG_ERR, _("Couldn't init PAM session: %s"), pam_strerror(pam, result)); ui_setup_message(_("Cannot log in. Unable to establish a session.")); ui_message_run(); sleep(fail_delay); ui_end(); return EXIT_FAILURE; } result = pam_setcred(pam, PAM_REINITIALIZE_CRED); if (result != PAM_SUCCESS) { pam_close_session(pam, 0); syslog(LOG_ERR, _("Couldn't init PAM session: %s"), pam_strerror(pam, result)); ui_setup_message(_("Cannot log in. Unable to establish a session.")); ui_message_run(); sleep(fail_delay); ui_end(); return EXIT_FAILURE; } endpwent(); ui_end(); log_utmp(login_name); // TODO log audit and lastlog syslog(LOG_INFO, _("LOGIN FROM '%s' BY '%s'"), origin, login_name); // chown TTY if (tty_path) { // TODO set group as well if (chown(tty_path, pwd->pw_uid, -1) != 0) syslog(LOG_ERR, _("failed to chown TTY '%s': %m"), tty_path); } if (setgid(pwd->pw_gid) < 0 && pwd->pw_gid) { syslog(LOG_ALERT, _("setgid() failed: %m")); fprintf(stderr, _("error applying user GID: %m. Abort.\n")); sleep(fail_delay); return EXIT_FAILURE; } if ((!pwd->pw_shell) || pwd->pw_shell[0] == '\0') pwd->pw_shell = _PATH_BSHELL; init_env(false, pam, pwd); // Get ready to fork! // signal handlers - i think this is in case something comes in while we're forking? struct sigaction sa, prev_hup, prev_term; memset(&sa, 0, sizeof(sa)); signal(SIGALRM, SIG_DFL); signal(SIGQUIT, SIG_DFL); signal(SIGTSTP, SIG_IGN); sa.sa_handler = SIG_IGN; sigaction(SIGINT, &sa, NULL); sigaction(SIGHUP, &sa, &prev_hup); if (tty_path) ioctl(0, TIOCNOTTY, NULL); sa.sa_handler = handle_sig; sigaction(SIGHUP, &sa, NULL); sigaction(SIGTERM, &sa, &prev_term); closelog(); child_pid = fork(); if (child_pid < 0) { fprintf(stderr, _("error forking: %m. Abort.\n")); openlog("hlogin", LOG_ODELAY, LOG_AUTHPRIV); syslog(LOG_ALERT, _("fork() failed: %m")); pam_setcred(pam, PAM_DELETE_CRED); pam_end(pam, pam_close_session(pam, 0)); sleep(fail_delay); return EXIT_FAILURE; } if (child_pid) { // we're in the parent close(STDIN_FILENO); close(STDOUT_FILENO); close(STDERR_FILENO); sa.sa_handler = SIG_IGN; sigaction(SIGQUIT, &sa, NULL); sigaction(SIGINT, &sa, NULL); while (wait(NULL) == -1 && errno == EINTR); openlog("hlogin", LOG_ODELAY, LOG_AUTHPRIV); pam_setcred(pam, PAM_DELETE_CRED); pam_end(pam, pam_close_session(pam, 0)); return EXIT_SUCCESS; } // child // reset signal handlers sigaction(SIGHUP, &prev_hup, NULL); sigaction(SIGTERM, &prev_term, NULL); if (got_sig) return EXIT_FAILURE; setsid(); // reopen tty if (tty_path) { int fd = open(tty_path, O_RDWR | O_NONBLOCK); if (fd == -1) { syslog(LOG_ERR, _("can't reopen tty: %m")); return EXIT_FAILURE; } if (!isatty(fd)) { close(fd); syslog(LOG_ERR, _("%s is not a tty"), tty_path); return EXIT_FAILURE; } fcntl(fd, F_SETFL, fcntl(fd, F_GETFL) & ~O_NONBLOCK); // set tty as stdin, stdout, and stderr for (int i = 0; i < fd; i++) close(i); for (int i = 0; i < 3; i++) if (fd != i) dup2(fd, i); if (fd >= 3) close(fd); } openlog("hlogin", LOG_ODELAY, LOG_AUTHPRIV); if (ioctl(0, TIOCSCTTY, 1) != 0) syslog(LOG_ERR, _("TIOCSCTTY failed: %m")); signal(SIGINT, SIG_DFL); // continue becoming the user if (setuid(pwd->pw_uid) < 0 && pwd->pw_uid) { syslog(LOG_ALERT, _("setuid() failed: %m")); fprintf(stderr, _("error applying user UID: %m. Abort.\n")); sleep(fail_delay); return EXIT_FAILURE; } if (chdir(pwd->pw_dir) < 0) { syslog(LOG_ALERT, _("failed to chdir to home (%s): %m"), pwd->pw_dir); fprintf(stderr, _("error changing to home directory: %m. Abort.\n")); sleep(fail_delay); return EXIT_FAILURE; } pam_end(pam, PAM_SUCCESS | PAM_DATA_SILENT); // execute login shell char **shell_argv = calloc(2, sizeof(char *)); char *shell_base = strrchr(pwd->pw_shell, '/'); if (shell_base) shell_base++; else shell_base = pwd->pw_shell; shell_argv[0] = malloc(sizeof(char) * (strlen(shell_base) + 2)); shell_argv[0][0] = '-'; strcpy(shell_argv[0] + 1, shell_base); execvp(pwd->pw_shell, shell_argv); return EXIT_SUCCESS; }