PAM Authentication for a Legacy Application
Asked Answered
R

5

10

I have a legacy app that receives a username/password request asynchronously over the wire. Since I already have the username and password stored as variables, what would be the best way to authenticate with PAM on Linux (Debian 6)?

I've tried writing my own conversation function, but I'm not sure of the best way of getting the password into it. I've considered storing it in appdata and referencing that from the pam_conv struct, but there's almost no documentation on how to do that.

Is there a simpler way to authenticate users without the overkill of a conversation function? I'm unable to use pam_set_data successfully either, and I'm not sure that's even appropriate.

Here's what I'm doing:

user = guiMessage->username;
pass = guiMessage->password;

pam_handle_t* pamh = NULL;
int           pam_ret;
struct pam_conv conv = {
  my_conv,
  NULL
};

pam_start("nxs_login", user, &conv, &pamh);
pam_ret = pam_authenticate(pamh, 0);

if (pam_ret == PAM_SUCCESS)
  permissions = 0xff;

pam_end(pamh, pam_ret);

And initial attempts at the conversation function resulted in (password is hard-coded for testing):

int 
my_conv(int num_msg, const struct pam_message **msg, struct pam_response **resp, void *data)
{
  struct pam_response *aresp;

  if (num_msg <= 0 || num_msg > PAM_MAX_NUM_MSG)
    return (PAM_CONV_ERR);
  if ((aresp = (pam_response*)calloc(num_msg, sizeof *aresp)) == NULL)
    return (PAM_BUF_ERR);
  aresp[0].resp_retcode = 0;
  aresp[0].resp = strdup("mypassword");

  *resp = aresp;
  return (PAM_SUCCESS);
}

Any help would be appreciated. Thank you!

Revoice answered 6/5, 2011 at 15:49 Comment(0)
O
16

This is what I ended up doing. See the comment marked with three asterisks.

#include <stdlib.h>
#include <iostream>
#include <fstream>
#include <security/pam_appl.h>
#include <unistd.h>

// To build this:
// g++ test.cpp -lpam -o test

// if pam header files missing try:
// sudo apt install libpam0g-dev

struct pam_response *reply;

//function used to get user input
int function_conversation(int num_msg, const struct pam_message **msg, struct pam_response **resp, void *appdata_ptr)
{
  *resp = reply;
  return PAM_SUCCESS;
}

int main(int argc, char** argv)
{
  if(argc != 2) {
      fprintf(stderr, "Usage: check_user <username>\n");
      exit(1);
  }
  const char *username;
  username = argv[1];

  const struct pam_conv local_conversation = { function_conversation, NULL };
  pam_handle_t *local_auth_handle = NULL; // this gets set by pam_start

  int retval;

  // local_auth_handle gets set based on the service
  retval = pam_start("common-auth", username, &local_conversation, &local_auth_handle);

  if (retval != PAM_SUCCESS)
  {
    std::cout << "pam_start returned " << retval << std::endl;
    exit(retval);
  }

  reply = (struct pam_response *)malloc(sizeof(struct pam_response));

  // *** Get the password by any method, or maybe it was passed into this function.
  reply[0].resp = getpass("Password: ");
  reply[0].resp_retcode = 0;

  retval = pam_authenticate(local_auth_handle, 0);

  if (retval != PAM_SUCCESS)
  {
    if (retval == PAM_AUTH_ERR)
    {
      std::cout << "Authentication failure." << std::endl;
    }
    else
    {
      std::cout << "pam_authenticate returned " << retval << std::endl;
    }
    exit(retval);
  }

  std::cout << "Authenticated." << std::endl;

  retval = pam_end(local_auth_handle, retval);

  if (retval != PAM_SUCCESS)
  {
    std::cout << "pam_end returned " << retval << std::endl;
    exit(retval);
  }

  return retval;
}
Orthotropic answered 11/5, 2011 at 20:16 Comment(6)
Thanks! And here I was trying to find ways to communicate into the conversation function instead of just bypassing and working with the response outside of it.Revoice
This isn't working for 'root' (just 'root', all other users are auth-ed fine). Is that a bug?Willams
I don't know. I didn't try root. You're probably doing something wrong if you need to use the root password on a regular basis.Orthotropic
You could and maybe even should use the appdata. It is an arbitrary pointer that you pass to PAM and then PAM hands it back to you in callbacks to conversation function. This way you can pass any arbitrary data. It could be pointer to the getpass function, or to the string that you pass to getpass or to a password itself. Whatever. Main point is that is allows you to avoid variables that have to be global to be shared between your function and conversation function.Pollock
This solution will cause an out-of-bounds heap read if any PAM module tries to handle multiple messages in one conversation function call! This is rare, but may happen. In this case *resp is expected to be an array of num_msg pam_response structs and not to be a pointer to one struct.Tasty
This solution has two issues. It works because pam_response struct is dynamically allocated and also the char * returned by getpass() is dynamically allocated. Actually the pam_response and also its member reply MUST be dynamically allocated in the conversation function because after it was called the struct and its member will be freeed. You can simply prove this by adding a free(reply) call before return retval: You will get a double free.Rafter
E
2

The way standard information (such as a password) is passed for PAM is by using variables set in the pam handle with pam_set_item (see the man page for pam_set_item).

You can set anything your application will need to use later into the pam_stack. If you want to put the password into the pam_stack you should be able to do that immediately after calling pam_start() by setting the PAM_AUTHTOK variable into the stack similar to the pseudo code below:

pam_handle_t* handle = NULL;
pam_start("common-auth", username, NULL, &handle);
pam_set_item( handle, PAM_AUTHTOK, password);

This will make the password available on the stack to any module that cares to use it, but you generally have to tell the module to use it by setting the standard use_first_pass, or try_first_pass options in the pam_configuration for the service (in this case /etc/pam.d/common-auth).

The standard pam_unix module does support try_first_pass, so it wouldn't hurt to add that into your pam configuration on your system (at the end of the line for pam_unix).

After you do this any call to pam_authenticate() that are invoked from the common-auth service should just pick the password up and go with it.

One small note about the difference between use_first_pass and try_first_pass: They both tell the module (in this case pam_unix) to try the password on the pam_stack, but they differ in behavior when their is no password/AUTHTOK available. In the missing case use_first_pass fails, and try_first_pass allows the module to prompt for a password.

Expressivity answered 28/8, 2012 at 17:17 Comment(4)
pubs.opengroup.org/onlinepubs/8329799/… mentions that PAM_AUTHTOK is "only available to PAM modules and not to applications". So you should not be able to do that which you described. Have you checked whether it works? Maybe it does but is not portable?Pollock
Then again pam_set_item documentation (pubs.opengroup.org/onlinepubs/8329799/…) doesn't seem to repeat that condition while it does offer PAM_AUTHOK.Pollock
I think this wasn't tested. pam_start doesn't like NULL very much.Cyb
This is not portable because on many systems there is no config for common-auth in /etc/pam.d.Rafter
M
2

Fantius' solution worked for me, even as root.

I originally opted for John's solution, as it was cleaner and made use of PAM variables without the conversation function (really, there isn't a need for it here), but it did not, and will not, work. As Adam Badura alluded to in both posts, PAM has some internal checks to prevent direct setting of PAM_AUTHTOK.

John's solution will result in behaviour similar to what is mentioned here, where any password value will be allowed to login (even if you declare, but do not define, the pam_conv variable).

I would also recommend users be aware of the placement of the malloc, as it will likely differ in your application (remember, the code above is more of a test/template, than anything else).

Much answered 13/2, 2018 at 2:23 Comment(1)
thanks for reminding me why i'm a casual on these sorts of sites ;) my apologies!Much
R
1
struct pam_conv {
    int (*conv)(int num_msg, const struct pam_message **msg,
                struct pam_response **resp, void *appdata_ptr);
    void *appdata_ptr;
};

The second field(appdata_ptr) of the struct pam_conv is passed to the conversation function, therefore we can use it as our password pointer.

     static int convCallback(int num_msg, const struct pam_message** msg,
                             struct pam_response** resp, void* appdata_ptr)
     {
            struct pam_response* aresp;
        
            if (num_msg <= 0 || num_msg > PAM_MAX_NUM_MSG)
                return (PAM_CONV_ERR);
            if ((aresp = (pam_response*)calloc(num_msg, sizeof * aresp)) == NULL)
                return (PAM_BUF_ERR);
            aresp[0].resp_retcode = 0;
            aresp[0].resp = strdup((char*)appdata_ptr);
                                                    
            *resp = aresp;
            
            return (PAM_SUCCESS);
    }

    int main()
    {
        ....
        pam_handle_t* pamH = 0;
        char *password = strdup("foopassword");
        struct pam_conv conversation = {convCallback, password};
        int retvalPam = pam_start("check_user", "foousername", &conversation, &pamH);
        
        //Call pam_authenticate(pamH, 0)
        //Call pam_end(pamH, 0);
        ...
        ...
        free(password);
    }
Rieth answered 20/10, 2020 at 4:13 Comment(1)
This also has issues if num_msg is greater than 1.Rafter
R
0

Fantius' solution has issues. I just gave it a try and used a static string instead of calling the evil getpass() and got a segmentation fault. Then I checked the code and saw that there is no free() for the reply variable. But with free'ing the variable I got a double free detected.

The reason for this is that the conversation function usually gets the password entered from "somewhere" and requires dynamic allocations for

  1. the pam_response and
  2. its struct member resp

Because of this the PAM lib will take care of releasing memory which was dynamically allocated by the conversation function. The only exception is when an error appeared in the conversation function. In this case it must take care of the memory by itself. You can easily check this in the source code of PAM's own misc_conv() function which is somewhat a generic conversation function.

This means that Fantius' function_conversation() function is much too simple by not taking care of allocations and memory releases. In addition it can also cause issues in cases where multiple messages are used. It also uses a global variable instead of using the appdata_ptr.

This is my conversation function:

int conversation(int num_msg, const struct pam_message **msg, struct pam_response **resp, void *appdata_ptr) {
  if (num_msg < 1) return PAM_CONV_ERR;
  *resp = calloc(num_msg, sizeof(struct pam_response));
  if (*resp == 0) return PAM_SYSTEM_ERR;
  for (int c=0; c<num_msg; ++c) {
    struct pam_response *reply = &(*resp[c]);
    reply->resp = strdup(appdata_ptr);
    reply->resp_retcode = 0;
  }
  return PAM_SUCCESS;
}

This is also very simpel by using strdup(). Do not use it in production code without handling strdup() failures.

And this is the related function to check the password of an user:

int check_password(const char * const pam_config, const char * const username, const char * const ro_password) {
  pam_handle_t *pamh;
  struct passwd *pw;
  int result, ret_val;

  ret_val = 1;
  if ((pw = getpwnam(username)) == NULL) return 2;
  /** set up response and conversation structure containing password as appdata_ptr */
  const struct pam_conv conv = { conversation, (void *) ro_password };
  if ((result = pam_start(pam_config, pw->pw_name, &conv, &pamh)) != PAM_SUCCESS) goto out;
  if ((result = pam_authenticate(pamh, 0)) != PAM_SUCCESS) goto out;
  if ((result = pam_acct_mgmt(pamh, 0)) != PAM_SUCCESS) goto out;
  ret_val = 0;
out:
  if ((result = pam_end(pamh, result)) != PAM_SUCCESS) ret_val = 5;
  return ret_val;
}

The pam_config argument is a string defining the PAM service to be used. A PAM service as configured in /etc/pam.d directory. Reasonable values are e.g. "login", "sshd", "ppp" or "remote".

Using the function is very simple:

check_password(pam_config, username, password);

It will return 0 on success. "Success" means: User exists, the password matches user's password and the user is allowed to login (e.g. password is NOT expired).

This approach via PAM lib does also work well if user data is not stored locally but in an AD or LDAP service.

Rafter answered 29/4 at 17:23 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.