SUMMARY: poppassd

From: Stuart Kendrick (sbk@fred.fhcrc.org)
Date: Thu Apr 30 1998 - 09:52:16 CDT


Attached is a copy of poppassd.c which works under Solaris 2.6 running
NIS+ (prompts from /bin/passwd change under these conditions).

Compile with:

gcc -c -g poppassd.c
gcc -o poppassd -g poppassd.o

/*
 * poppassd.c
 *
 * A Eudora and NUPOP change password server for Solaris.
 *
 * Graziano Sommariva
 * Ansaldo Sistemi Informatici Genova Italy.
 * sommariva@ansaldo.it
 *
 * Based on earlier versions by
 * John Norstad
 * j-norstad@nwu.edu
 *
 * Doesn't actually change any passwords itself. It simply listens for
 * incoming requests, gathers the required information (user name, old
 * password, new password) and executes /bin/passwd, talking to it over
 * a pseudo-terminal pair. The advantage of this is that we don't need
 * to have any knowledge of either the password file format (which may
 * include dbx files that need to be rebuilt) or of any file locking
 * protocol /bin/passwd and cohorts may use (and which isn't documented).
 *
 * The current version has been tested under Solaris 2.3 and used by
 * Eudora 2.0.3 .
 *
 * Note that unencrypted passwords are transmitted over the network. If
 * this bothers you, think hard about whether you want to implement the
 * password changing feature. On the other hand, it's no worse than what
 * happens when you run /bin/passwd while connected via telnet or rlogin.
 * Well, maybe it is, since the use of a dedicated port makes it slightly
 * easier for a network snooper to snarf passwords off the wire.
 *
 * NOTE: In addition to the security issue outlined in the above paragraph,
 * you should be aware that this program is going to be run as root by
 * ordinary users and it mucks around with the password file. This should
 * set alarms off in your head. I think I've devised a pretty foolproof
 * way to ensure that security is maintained, but I'm no security expert and
 * you would be a fool to install this without first reading the code and
 * ensuring yourself that what I consider safe is good enough for you. If
 * something goes wrong, it's your fault, not mine.
 *
 *
 * I made changes on the retriving of the crypted password and about
 * the assigning of the pseudo-ttys.
 *
 * Should be owned by root, and executable only by root. It can be started
 * with an entry in /etc/inetd.conf such as the following:
 *
 * poppassd stream tcp nowait root /usr/local/bin/poppassd poppassd
 *
 * and in /etc/services:
 *
 * poppassd 106/tcp
 *
 * Logs to the local2 facility. Should have an entry in /etc/syslog.conf
 * like the following:
 *
 * local2.err /var/adm/poppassd-log
 *
 * 4/26/95 -
 * Added AUTH_UID. This feature disables the ability of users below
 * the specified UID from being able to change their passwords via
 * this program.
 *
 * Changed #include <strings.h> to <string.h> and moved varargs.h
 * above the syslog.h include.
 *
 * I also added a timeout during input. [mark@qualcomm.com]
 *
 * 2/29/96 -
 * Added string for Solaris2.5 passwd program. [mark@qualcomm.com]
 *
 * 11/18/97
 * Added strings for Solaris2.6/NIS+ passwd program. [js@cctechnol.com]
 *
 * 4/30/97
 * Added more strings to support /bin/passwd under Solaris 2.6/NIS+.
 * [sbk@fred.fhcrc.org]
 *
 *
 */
 
/* Steve Dorner's description of the simple protocol:
 *
 * The server's responses should be like an FTP server's responses;
 * 1xx for in progress, 2xx for success, 3xx for more information
 * needed, 4xx for temporary failure, and 5xx for permanent failure.
 * Putting it all together, here's a sample conversation:
 *
 * S: 200 hello\r\n
 * E: user yourloginname\r\n
 * S: 300 please send your password now\r\n
 * E: pass yourcurrentpassword\r\n
 * S: 200 My, that was tasty\r\n
 * E: newpass yournewpassword\r\n
 * S: 200 Happy to oblige\r\n
 * E: quit\r\n
 * S: 200 Bye-bye\r\n
 * S: <closes connection>
 * E: <closes connection>
 */
 
#define VERSION "1.2b for Solaris 2.6 w/NIS+"

#define SUCCESS 1
#define FAILURE 0
#define BUFSIZE 512

#include <sys/conf.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <unistd.h>
#include <fcntl.h>
#include <varargs.h>
#include <syslog.h>
#include <stdlib.h>
#include <stdio.h>
#include <ctype.h>
#include <string.h>
#include <errno.h>
#include <pwd.h>
#include <shadow.h>
#include <string.h>
#include <termios.h>
#include <stropts.h>
#include <dirent.h>
#include <signal.h>

/* Prompt strings expected from the "passwd" command. If you want
 * to port this program to yet another flavor of UNIX, you may need to add
 * more prompt strings here.
 *
 * Each prompt is defined as an array of pointers to alternate
 * strings, terminated by an empty string. In the strings, '*'
 * matches any sequence of 0 or more characters. Pattern matching
 * is case-insensitive.
 */

        /* UIDs below this one are not authorized to change their password */
#define AUTH_UID 99
#define LOG_FACILITY LOG_LOCAL1

static char *P1[] =
   {"Old password:",
    "Old password: ",
    "Enter login password: ",
    "Enter login(NIS) password: ",
    "Enter login(NIS+) password: ",
    "Changing password for *.\nOld password:",
    "Changing password for * on *.\nOld password:",
    "Changing NIS password for * on *.\nOld password:",
    "Changing password for *\n*'s Old password:",
    "passwd: Changing password for a85011\nOld password: ",
    "passwd: Changing password for *\nEnter login(NIS+) password:",
    "passwd: Changing password for *\nEnter login(NIS+) password: ",
    "passwd: Changing password for *\nEnter login password:",
    "passwd: Changing password for *\nEnter login password: ",
    ""};

static char *P2[] =
   {"\nNew password:",
    "\nNew password: ",
    "\n*'s New password:",
    ""};

static char *P3[] =
   {"\nRe-enter new password:",
    "\nRe-enter new password: ",
    "\nRe-enter new password:",
    "\nRetype new password:",
    "\nEnter the new password again:",
    "\n*Re-enter *'s new password:",
    "\nVerify:",
    ""};
    
static char *P4[] =
   {"\n",
    "NIS(YP) passwd/attributes changed on *\n",
    "NIS entry changed on *\n",
    "NIS+ password information changed for *\nNIS+ credential information changed for *",
    "NIS+ password information changed for *\nNIS+ credential information changed for *\n",
    "passwd (SYSTEM): passwd successfully changed for *",
    "passwd (SYSTEM): passwd successfully changed for *\n",
     ""};

abortme()
{
      WriteToClient ("500 Timeout. Input idle for too long.");
      exit(1);
}

main (argc, argv)
int argc;
char *argv[];
{
     char line[BUFSIZE];
     char user[BUFSIZE];
     char oldpass[BUFSIZE];
     char newpass[BUFSIZE];
     char emess[BUFSIZE];
     char *slavedev;
     struct passwd *pw, *getpwnam();
     struct spwd *sp, *getspnam();
     int c, master;
     pid_t pid, wpid;
     int wstat;
     
     (void)signal(SIGALRM, (void *)abortme);

     *user = *oldpass = *newpass = 0;
     
     openlog ("poppassd", LOG_PID, LOG_FACILITY);
     
     WriteToClient ("200 poppassd v%s hello, who are you?", VERSION);
     ReadFromClient (line);
     sscanf (line, "user %s", user) ;
     if (strlen (user) == 0)
     {
          WriteToClient ("500 Username required.");
          exit(1);
     }

     WriteToClient ("200 your password please.");
     ReadFromClient (line);
     sscanf (line, "pass %s", oldpass) ;
     if (strlen (oldpass) == 0)
     {
          WriteToClient ("500 Password required.");
          exit(1);
     }
     
     if ( ((pw = getpwnam (user)) == NULL) || ((sp = getspnam (user)) == NULL) )
     {
          WriteToClient ("500 Unknown user, %s.", user);
          exit(1);
     }

     if (pw->pw_uid <= AUTH_UID)
     {
          syslog (LOG_ERR, "Authorization error, attempt to change password of %s", user);
          WriteToClient ("500 Old password is incorrect.");
          exit(1);
     }

     if (chkPass (user, oldpass, sp->sp_pwdp) == FAILURE)
     {
          WriteToClient ("500 Old password is incorrect.");
          exit(1);
     }

     WriteToClient ("200 your new password please.");
     ReadFromClient (line);
     sscanf (line, "newpass %s", newpass);
     
     /* new pass required */
     if (strlen (newpass) == 0)
     {
          WriteToClient ("500 New password required.");
          exit(1);
     }
     /* get pts to talk to password program */
     if ((master = findpts (&slavedev)) < 0)
     {
          syslog (LOG_ERR, "can't find pts for master");
          WriteToClient("500 Server busy - try again later.");
          exit (1);
     }

     /* fork child process to talk to password program */
     if ((pid = fork()) < 0) /* Error, can't fork */
     {
          syslog (LOG_ERR, "can't fork for passwd: %m");
          WriteToClient ("500 Server error (can't fork passwd), get help!");
          exit (1);
     }

     if (pid) /* Parent */
     {
          sleep (1); /* Make sure child is ready. Is this really needed? */
          if (talktochild (master, user, oldpass, newpass, emess) == FAILURE)
          {
               syslog (LOG_ERR, "failed attempt by %s", user);
               if (*emess == '\0') {
                  WriteToClient ("500 Unable to change password." );
               } else {
                  WriteToClient ("500 %s", emess);
               }
               exit(1);
          }

          if ((wpid = waitpid (pid, &wstat, 0)) < 0)
          {
               syslog (LOG_ERR, "wait for /bin/passwd child failed: %m");
               WriteToClient ("500 Server error (wait failed), get help!");
               exit (1);
          }

          if (pid != wpid)
          {
               syslog (LOG_ERR, "wrong child (/bin/passwd waited for!");
               WriteToClient ("500 Server error (wrong child), get help!");
               exit (1);
          }

          if (WIFEXITED (wstat) == 0)
          {
               syslog (LOG_ERR, "child (/bin/passwd) killed?");
               WriteToClient ("500 Server error (funny wstat), get help!");
               exit (1);
          }

          if (WEXITSTATUS (wstat) != 0)
          {
               syslog (LOG_ERR, "child (/bin/passwd) exited abnormally");
               WriteToClient ("500 Server error (abnormal exit), get help!");
               exit (1);
          }

          syslog (LOG_ERR, "password changed for %s", user);
          WriteToClient ("200 Password changed, thank-you.");

          ReadFromClient (line);
          if (strncmp(line, "quit", 4) != 0) {
                  WriteToClient("500 Quit required.");
                exit (1);
          }
          
          WriteToClient("200 Bye.");
          exit (0);
     }
     else /* Child */
     {
          dochild (master, slavedev, user, pw);
     }
}

/*
 * dochild
 *
 * Do child stuff - set up slave pts and execl /bin/passwd.
 *
 * Code adapted from "Advanced Programming in the UNIX Environment"
 * by W. Richard Stevens.
 *
 */

dochild (master, slavedev, user, pw)
int master;
char *slavedev, *user;
struct passwd *pw;

{
   int slave;
   struct termios stermios;

   /* Start new session - gets rid of controlling terminal. */
   
   if (setsid() < 0) {
      syslog(LOG_ERR, "setsid failed: %m");
      return(0);
   }

  /* Open slave pty and acquire as new controlling terminal. */
    
   if ((slave = open(slavedev, O_RDWR)) < 0) {
      syslog(LOG_ERR, "can't open slave pty %s: %m");
      return(0);
   }
   ioctl(slave, I_PUSH, "ptem");
   ioctl(slave, I_PUSH, "ldterm");

   /* Close master. */

   close(master);

   /* Make slave stdin/out/err of child. */

   if (dup2(slave, STDIN_FILENO) != STDIN_FILENO) {
      syslog(LOG_ERR, "dup2 error to stdin: %m");
      return(0);
   }
   if (dup2(slave, STDOUT_FILENO) != STDOUT_FILENO) {
      syslog(LOG_ERR, "dup2 error to stdout: %m");
      return(0);
   }
   if (dup2(slave, STDERR_FILENO) != STDERR_FILENO) {
      syslog(LOG_ERR, "dup2 error to stderr: %m");
      return(0);
   }

   if (slave > 2) close(slave);

   /* Set proper terminal attributes - no echo, canonical input processing,
      no map NL to CR/NL on output. */

   if (tcgetattr(0, &stermios) < 0) {
      syslog(LOG_ERR, "tcgetattr error: %m");
      return(0);
   }
   stermios.c_lflag &= ~(ECHO | ECHOE | ECHOK | ECHONL);
   stermios.c_lflag |= ICANON;
   stermios.c_oflag &= ~(ONLCR);
   if (tcsetattr(0, TCSANOW, &stermios) < 0) {
      syslog(LOG_ERR, "tcsetattr error: %m");
      return(0);
   }

  /*
   * Become the user trying who's password is being changed. We're
   * about to exec /bin/passwd with is setuid root anyway, but this
   * way it looks to the child completely like it's being run by
   * the normal user, which makes it do its own password verification
   * before doing any thing. In theory, we've already verified the
   * password, but this extra level of checking doesn't hurt. Besides,
   * the way I do it here, if somebody manages to change somebody
   * else's password, you can complain to your vendor about security
   * holes, not to me!
   */

   setuid (pw->pw_uid);
   setgid (pw->pw_gid);

   /* Fork /bin/passwd. */

   if (execl("/bin/passwd", "passwd", user, (char*)0) < 0) {
      syslog(LOG_ERR, "can't exec /bin/passwd: %m");
      return(0);
   }
}

/*
 * findpts()
 *
 * Finds the first available pseudo-terminal master/slave pair. The master
 * side is opened and a fd returned as the function value. A pointer to the
 * name of the slave side (i.e. "/dev/pts/N" N [1 N-1]) is returned
 * in the argument, which should be a char**.
 * The name itself is stored in a static buffer.
 *
 * A negative value is returned on any sort of error.
 *
 * Modified by Norstad to remove assumptions about number of pts's allocated
 * on this UNIX box.
 */
findpts (slave)
char **slave;
{
   int master;
   extern char *ptsname();

   if ((master = open("/dev/ptmx", O_RDWR)) >= 1) {
      grantpt(master);
      unlockpt(master);
      *slave = ptsname(master);
      return(master);
   }
   return (-1);
}

/*
 * writestring()
 *
 * Write a string in a single write() system call.
 */
writestring (fd, s)
char *s;
{
     int l;

     l = strlen (s);
     write (fd, s, l);
}

/*
 * talktochild()
 *
 * Handles the conversation between the parent and child (password program)
 * processes.
 *
 * Returns SUCCESS is the conversation is completed without any problems,
 * FAILURE if any errors are encountered (in which case, it can be assumed
 * that the password wasn't changed).
 */
talktochild (master, user, oldpass, newpass, emess)
int master;
char *user, *oldpass, *newpass, *emess;
{
     char buf[BUFSIZE];
     char pswd[BUFSIZE+1];
     int m, n;

     *emess = 0;

     if (!expect(master, P1, buf))
     {
             syslog (LOG_DEBUG, "fail userid");
             return FAILURE;
     }
     

     sprintf(pswd, "%s\n", oldpass);
     writestring(master, pswd);
     if (!expect(master, P2, buf))
     {
             syslog (LOG_DEBUG, "fail oldpass");
             return FAILURE;
     }
     

     sprintf(pswd, "%s\n", newpass);
     writestring(master, pswd);

     if (!expect(master, P3, buf)) {
             syslog (LOG_DEBUG, "fail newpass");
             getemess(master, P2, buf);
             strcpy(emess, buf);
             return FAILURE;
     }

     writestring(master, pswd);

     if (!expect(master, P4, buf))
     {
             syslog (LOG_DEBUG, "fail 2nd newpass");
             return FAILURE;
     }

     return SUCCESS;
}

/*
 * match ()
 *
 * Matches a string against a pattern. Wild-card characters '*' in
 * the pattern match any sequence of 0 or more characters in the string.
 * The match is case-insensitive.
 *
 * Entry: str = string.
 * pat = pattern.
 *
 * Exit: function result =
 * 0 if no match.
 * 1 if the string matches some initial segment of
 * the pattern.
 * 2 if the string matches the full pattern.
 */
match (str, pat)
char *str;
char *pat;
{
   int result;
   
   for (; *str && *pat && *pat != '*'; str++, pat++)
      if (tolower(*str) != tolower(*pat)) return 0;
   if (*str == 0) return *pat == 0 ? 2 : 1;
   if (*pat == 0) return 0;
   for (; *str; str++) if ((result = match(str, pat+1)) != 0) return result;
   return 0;
}

/*
 * expect ()
 *
 * Reads 'passwd' command output and compares it to expected output.
 *
 * Entry: master = fid of master pts.
 * expected = pointer to array of pointers to alternate expected
 * strings, terminated by an empty string.
 * buf = pointer to buffer.
 *
 * Exit: function result = SUCCESS if output matched, FAILURE if not.
 * buf = the text read from the slave.
 *
 * Text is read from the slave and accumulated in buf. As long as
 * the text accumulated so far is an initial segment of at least
 * one of the expected strings, the function continues the read.
 * As soon as one of full expected strings has been read, the
 * function returns SUCCESS. As soon as the text accumulated so far
 * is not an initial segment of or exact match for at least one of
 * the expected strings, the function returns FAILURE.
 */
expect (master, expected, buf)
int master;
char **expected;
char *buf;
{
     int n, m;
     char **s;
     int initialSegment;
     int result;
     char c;
     n = 0;
     buf[0] = 0;
     while (1) {
             if (n >= BUFSIZE-1) {
           syslog(LOG_ERR, "buffer overflow on read from child");
           return FAILURE;
        }
             m = read(master, buf+n, BUFSIZE-1-n);
        if (m < 0) {
           syslog(LOG_ERR, "read error from child: %m");
           return FAILURE;
        }
        n += m;
        buf[n] = 0;

        initialSegment = 0;
        for (s = expected; **s != 0; s++) {
           result = match(buf, *s);
           if (result == 2) return SUCCESS;
           initialSegment = initialSegment || result == 1;
        }
        if (!initialSegment) return FAILURE;
     }
}

/*
 * getemess()
 *
 * This function accumulates a 'passwd' command error message issued
 * after the first copy of the password has been sent.
 *
 * Entry: master = fid of master pts.
 * expected = pointer to array of pointers to alternate expected
 * strings for first password prompt, terminated by an
 * empty string.
 * buf = pointer to buffer containing text read so far.
 *
 * Exit: buf = the error message read from the slave.
 *
 * Text is read from the slave and accumulated in buf until the text
 * at the end of the buffer is an exact match for one of the expected
 * prompt strings. The expected prompt string is removed from the buffer,
 * returning just the error message text. Newlines in the error message
 * text are replaced by spaces.
 */
getemess (master, expected, buf)
int master;
char **expected;
char *buf;
{
   int n, m;
   char **s;
   char *p, *q;

   n = strlen(buf);
   while (1) {
      for (s = expected; **s != 0; s++) {
         for (p = buf; *p; p++) {
            if (match(p, *s) == 2) {
               *p = 0;
               for (q = buf; *q; q++) if (*q == '\n') *q = ' ';
               return;
            }
         }
      }
      if (n >= BUFSIZE-1) {
         syslog(LOG_ERR, "buffer overflow on read from child");
         return;
      }
      m = read(master, buf+n, BUFSIZE+1-n);
      if (m < 0) {
         syslog(LOG_ERR, "read error from child: %m");
         return;
      }
      n += m;
      buf[n] = 0;
   }
}

WriteToClient (fmt, va_alist)
char *fmt;
va_dcl
{
        va_list ap;
        
        va_start (ap);
        vfprintf (stdout, fmt, ap);
        fputs ("\r\n", stdout );
        fflush (stdout);
        va_end (ap);
}

ReadFromClient (line)
char *line;
{
        char *sp;
        int i;

        strcpy (line, "");
        alarm(60);
        fgets (line, BUFSIZE, stdin);
        alarm(0);
        if ((sp = strchr(line, '\n')) != NULL) *sp = '\0';
        if ((sp = strchr(line, '\r')) != NULL) *sp = '\0';
        
        /* convert initial keyword on line to lower case. */
        
        for (sp = line; isalpha(*sp); sp++) *sp = tolower(*sp);
}

char *crypt(char *, char *);

int chkPass (user, pass, sp)
char *user;
char *pass;
char *sp;
{

     /* Compare the supplied password with the password file entry */
     if (strcmp (crypt (pass, sp), sp) != 0)
          return (FAILURE);
     else
          return (SUCCESS);
}



This archive was generated by hypermail 2.1.2 : Fri Sep 28 2001 - 23:12:39 CDT