1

I wrote a code in C using getspnam() & putspent(). I have two users user1 and user2, I changed the password using my code for user1 and then user2 in the order.

After I changed the password for user2, the user1 password reset back the oldest one.

Should I flush or reset shadow file anywhere?

#include <errno.h>
#include <crypt.h>
#include <shadow.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>

void print_usage() {
    printf("Usage: change_password username old_password new_password\n");
}

int main(int argc, const char *argv[]) {
    if (argc < 4) {
        print_usage();
        return 1;
    }

    if (setuid(0)) {
        perror("setuid");
        return 1;
    }

    FILE* fps;
    if (!(fps = fopen("/etc/shadow", "r+"))) {
        perror("Error opening shadow");
        return (1);
    }


    // Get shadow password.
    struct spwd *spw = getspnam(argv[1]);
    if (!spw) {
        if (errno == EACCES) puts("Permission denied.");
        else if (!errno) puts("No such user.");
        else puts(strerror(errno));
        return 1;
    }

    char *buffer = argv[2];

    char *hashed = crypt(buffer, spw->sp_pwdp);
    //    printf("%s\n%s\n", spw->sp_pwdp, hashed);
    if (!strcmp(spw->sp_pwdp, hashed)) {
        puts("Password matched.");
    } else {
        puts("Password DID NOT match.");
        return -1;
    }

    char *newpwd = crypt(argv[3], spw->sp_pwdp);

    spw->sp_pwdp = newpwd;

    strcpy(spw->sp_pwdp, newpwd);
    putspent(spw, fps);
    fclose(fps);
    return 0;
}
Dominique
  • 16,450
  • 15
  • 56
  • 112
  • 1
    [The manual page](http://man7.org/linux/man-pages/man3/getspnam.3.html) says "The `putspent()` function writes ... a text line in the shadow password file format to `stream`". It doesn't say anything about *replacing* an existing entry, just that it writes "a text line". Have you checked the file for duplicate entries? – Some programmer dude Nov 29 '18 at 06:35
  • There is no extra entry. I chances the user1 entry in shadow first time. After i change the password for user2 it reverts the user1 entry in shadow file. After I change password for user1 again, it reverts user2 password. – Varghees Samraj Nov 29 '18 at 06:38
  • 1
    Then my guess is that `putspent` function just writes at the current position of the file really. You might need to find the specific position of the user you want to modify, and seek to that position. Or rewrite the whole file. – Some programmer dude Nov 29 '18 at 06:41
  • Note the both the currently-posted answers have privilege escalation holes that allow the caller direct and full access to `root`. Writing secure code is hard - **use the tools the OS provides**. – Andrew Henle Nov 30 '18 at 09:01

2 Answers2

3

If you want to change the password for some user from a script or a program, use the chpasswd utility (specifically, /usr/sbin/chpasswd).

If we assume you have written a secure privileged utility or application that can update user passwords, then you could use something like

int change_password(const char *username, const char *password)
{
    FILE *cmd;
    int   status;

    if (!username || !*username)
        return errno = EINVAL; /* NULL or empty username */
    if (!password || !*password)
        return errno = EINVAL; /* NULL or empty password */

    if (strlen(username) != strcspn(username, "\t\n\r:"))
        return errno = EINVAL; /* Username contains definitely invalid characters. */
    if (strlen(password) != strcspn(password, "\t\n\r"))
        return errno = EINVAL; /* Password contains definitely invalid characters. */

    /* Ensure that whatever sh variant is used,
       the path we supply will be used as-is. */
    setenv("IFS", "", 1);

    /* Use the default C locale, just in case. */
    setenv("LANG", "C", 1);
    setenv("LC_ALL", "C", 1);

    errno = ENOMEM;
    cmd = popen("/usr/sbin/chpasswd >/dev/null 2>/dev/null", "w");
    if (!cmd)
        return errno;

    fprintf(cmd, "%s:%s\n", username, password);
    if (fflush(cmd) || ferror(cmd)) {
        const int saved_errno = errno;
        pclose(cmd);
        return errno;
    }

    status = pclose(cmd);
    if (!WIFEXITED(status))
        return errno = ECHILD; /* chpasswd died unexpectedly. */
    if (WEXITSTATUS(status))
        return errno = EACCES; /* chpasswd failed to change the password. */

    /* Success. */
    return 0;
}

(but this is untested, because I personally would use the underlying POSIX <unistd.h> I/O (fork(), exec*(), etc.) instead, for maximum control. See e.g. this example of how I handle a non-privileged operation, opening a file or URL in the user's preferred application. With privileged data, I'm much more paranoid. In particular, I'd check the ownership and mode of /, /usr/, /usr/sbin/, and /usr/sbin/chpasswd first, in that order, using lstat() and stat(), to ensure my application/utility is not being hoodwinked into executing a fake chpasswd.)

The password is supplied to the function in clear text. PAM will handle encrypting it (per /etc/pam.d/chpasswd), and the relevant system configuration used (per /etc/login.defs).

All the standard security warnings about privileged operations apply. You do not want to pass usernames and passwords as command-line parameters, because they are visible in the process list (see ps axfu output, for example). Environment variables are similarly accessible, and passed on to all child processes by default, so they're out as well. Descriptors obtained via pipe() or socketpair() are safe, unless you leak the descriptors to child processes. Having open FILE streams to child processes when starting another, often leaks the descriptor between the parent and the first child to the latter child.

You must take all possible precautions to stop your application/utility being used to subvert user accounts. You either learn to do it when you first start thinking about writing code or scripts to manage user information, or you add to the horrible mound of insecure code exploited by nefarious actors trying to exploit innocent users, and will never learn to do it properly. (No, you will not learn it later. Nobody who says they'll add checks and security later on, actually does so. We humans just don't work that way. Security and robustness is either baked in from the get go, or slapped haphazardly later on like frosting: it changes nothing, even if it looks good.)

The setenv() commands ensure chpasswd is run in the default C locale. Andrew Henle pointed out that some sh implementations could split the command path if IFS environment variable was set to a suitable value, so we clear it to empty, just in case. The trailing >/dev/null 2>/dev/null redirects its standard output and standard error to /dev/null (nowhere), in case it might print an error message with sensitive information in it. (It will reliably exit with a nonzero exit status, if any errors do occur; and that's what we rely on, above.)

Nominal Animal
  • 38,216
  • 5
  • 59
  • 86
  • You need to add `IFS=` to your environment if you're going to run `/usr/bin/chpasswd` from a setuid-root process. – Andrew Henle Nov 30 '18 at 08:59
  • @AndrewHenle: Okay, but why? The shell is not involved at all in parsing the lines, and I don't see [the sources](https://github.com/shadow-maint/shadow/blob/master/src/chpasswd.c) using `IFS` internally. – Nominal Animal Nov 30 '18 at 19:21
  • The shell is involved. Per [the POSIX documentation for `popen()`](http://pubs.opengroup.org/onlinepubs/9699919799/functions/popen.html): "The environment of the executed command shall be as if a child process were created within the `popen()` call using the `fork()` function, and the child invoked the `sh` utility using the call: `execl(shell path, "sh", "-c", command, (char *)0);`" A nefarious user could set `IFS=/` and run the `usr` command as located by the user's `PATH`. And it would run as `root`. See https://en.wikipedia.org/wiki/Internal_field_separator Some shells do ignore `IFS`. – Andrew Henle Nov 30 '18 at 19:35
  • @AndrewHenle: Ah, now I understand: not because the username/password might be mangled, but because the path to the binary *might* be affected in some implementations of `sh`. (`dash` and `bash`, the two shells used for `sh` on most Linux systems, are not affected.) Personally, I always use `fork()` and `exec*()` for such, for full control; I do not really trust having an unknown binary (who-knows-which implementation of `sh`) in the between. – Nominal Animal Nov 30 '18 at 19:53
1

Use passwd command to set the password instead. Open passwd using popen in write mode and send password to modify the shadow file.

#include <errno.h>
#include <crypt.h>
#include <shadow.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>

void print_usage() {

}

int change_password(char *username, char *password)
{
    char cmd[32];
    sprintf(cmd, " passwd %s 2>/dev/null", username);
    FILE *fp= popen(cmd, "w");
    fprintf(fp, "%s\n", password);
    fprintf(fp, "%s\n", password);
    pclose(fp);
    return 0;
}

int main(int argc, const char *argv[]) {
    if (argc < 4) {
        print_usage();
        return 1;
    }

    if (setuid(0)) {
        perror("setuid");
        return 1;
    }

    FILE* fps;
    if (!(fps = fopen("/etc/shadow", "r+"))) {
        perror("Error opening shadow");
        return (1);
    }


    // Get shadow password.
    struct spwd *spw = getspnam(argv[1]);
    if (!spw) {
        if (errno == EACCES) puts("Permission denied.");
        else if (!errno) puts("No such user.");
        else puts(strerror(errno));
        return 1;
    }

    char *buffer = argv[2];

    char *hashed = crypt(buffer, spw->sp_pwdp);
    //    printf("%s\n%s\n", spw->sp_pwdp, hashed);
    if (!strcmp(spw->sp_pwdp, hashed)) {
        puts("Password matched.");
    } else {
        puts("Password DID NOT match.");
        return -1;
    }
    change_password(argv[1], argv[3]);
    fclose(fps);
    return 0;
}
CrazyProgrammer
  • 544
  • 8
  • 29
  • *Open `passwd` using `popen`...* No. The `passwd` utility will almost certainly use the controlling terminal instead of `stdin` to read the password from. See https://github.com/shadow-maint/shadow/blob/master/src/passwd.c. Also [man `getpass()`](http://man7.org/linux/man-pages/man3/getpass.3.html): "The `getpass()` function opens `/dev/tty` ..." – Andrew Henle Nov 29 '18 at 13:19
  • @NominalAnimal Note that the posted code is insecure. Assuming it's installed as setuid-root, it allows the calling user complete access to the `root` account. – Andrew Henle Nov 30 '18 at 08:56
  • @AndrewHenle: Agreed; I didn't even read the code, and posted that approach as a separate answer, with what I think are appropriate warnings and adminitions, instead. (If an user runs `ln -s /bin/bash /tmp/passwd && PATH=/tmp:$PATH /path/to/this-setuid-program`, they get a full root shell.) – Nominal Animal Nov 30 '18 at 19:28
  • 1
    I suggest fixing a potential resource leak: fps. Suggested fix: adding _fclose(fps);_ before the last _return 1;_ and _return -1;_. – Heng Li Oct 08 '19 at 17:10