pam authentication in python without root privileges

14,653

Solution 1

short: use a proper Python PAM implementation, setup PAM properly.

long: In a sane PAM setup, you do not need root privileges. In the end this is one of the things PAM provides, privilege separation.

pam_unix has a way to check the password for you. Seems the PAM implementation of web2py (note, it's from some contrib subdirectory...) is not doing the right thing. Maybe your PAM setup is not correct, which is hard to tell without further information; this also depends heavily on operating system and flavour/distribution.

There are multiple PAM bindings for Python out there (unfortunately nothing in the standard library), use these instead. And for configuration, there are tons of tutorials, find the right one for your system.

old/wrong, don't do this: You do not need to be root, you only need to be able to read /etc/shadow. This file has usually group shadow with read only access. So you simply need to add the user that is running the PAM check in the shadow group.

groupadd <user> shadow should do the trick.

Solution 2

At the end I ended up using pexpect and trying to su - username. It's a bit slow, but it works pretty good. The below example isn't polished but you'll get the idea.

Cheers,

Jay

#!/usr/bin/python
import pexpect
def pam(username, password):
        '''Accepts username and password and tried to use PAM for authentication'''
        try:
                child = pexpect.spawn('/bin/su - %s'%(username))
                child.expect('Password:')
                child.sendline(password)
                result=child.expect(['su: Authentication failure',username])
                child.close()
        except Exception as err:
                child.close()
                print ("Error authenticating. Reason: "%(err))
                return False
        if result == 0:
                print ("Authentication failed for user %s."%(username))
                return False
        else:
                print ("Authentication succeeded for user %s."%(username))
                return True

if __name__ == '__main__':
        print pam(username='default',password='chandgeme')

Solution 3

I think the pam module is your best choice, but you don't have to embed it into your program directly. You could write a simple service which binds to a port on localhost, or listens on a UNIX domain socket, and fills PAM requests for other processes on the same host. Then have your web2py application connect to it for user/password validation.

For example:

import asyncore
import pam
import socket

class Client(asyncore.dispatcher_with_send):

    def __init__(self, sock):
        asyncore.dispatcher_with_send.__init__(self, sock)
        self._buf = ''

    def handle_read(self):
        data = self._buf + self.recv(1024)
        if not data:
            self.close()
            return
        reqs, data = data.rsplit('\r\n', 1)
        self._buf = data
        for req in reqs.split('\r\n'):
            try:
                user, passwd = req.split()
            except:
                self.send('bad\r\n')
            else:
                if pam.authenticate(user, passwd):
                    self.send('ok\r\n')
                else:
                    self.send('fail\r\n')

    def handle_close(self):
        self.close()


class Service(asyncore.dispatcher_with_send):

    def __init__(self, addr):
        asyncore.dispatcher_with_send.__init__(self)
        self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
        self.set_reuse_addr()
        self.bind(addr)
        self.listen(1)

    def handle_accept(self):
        conn, _ = self.accept()
        Client(conn)

def main():
    addr = ('localhost', 8317)
    Service(addr)
    try:
        asyncore.loop()
    except KeyboardInterrupt:
        pass

if __name__ == '__main__':
    main()

Usage:

% telnet localhost 8317
bob abc123
ok
larry badpass
fail
incomplete
bad

Solution 4

Maybe python-pam can work for you.

Solution 5

Not if you use they usual system (unix style) login credentials. At some point the PAM library must read the shadow file which is only readable by root. However, if you use a PAM profile that authenticates with an alternate method, such as LDAP or a database, then it can work without needing root.

This is one reason I developed my own framework that runs different parts of the URL path space under different user credentials. The login part (only) can run as root to authenticate with PAM (system), other path subtree handlers run as different users.

I'm using the PyPAM module for this.

Share:
14,653
jay_t
Author by

jay_t

Updated on June 15, 2022

Comments

  • jay_t
    jay_t almost 2 years

    I'm looking for a way to let my python program handle authentication through pam. I'm using http://code.google.com/p/web2py/source/browse/gluon/contrib/pam.py for this, which works out great as long as my python program runs as root which is not ideal to my opinion.

    How can I make use of pam for username/password validation without requiring root privs?

  • jay_t
    jay_t about 13 years
    Thanks, although I don't like this particular approach it made me start thinking. I have found a solution now. See answer.
  • EminezArtus
    EminezArtus about 8 years
    @FoxWilson Could you give us an example of how you think it would best be sanitized?
  • tew
    tew about 8 years
    @EminezArtus in the line which spawns the /bin/su process, the "username" argument (which is presumably user-specified) isn't sanitized. Imagine what would happen if a user passed in a username "root; rm -rf /" -- all files that the application user has access to would be deleted. pexpect's spawn method can accept command-line arguments: a better solution might be pexpect.spawn('/bin/su', ['-', username]). It looks like pexpect does not process shell metacharacters like ";", so this proposed attack probably wouldn't work, but better safe than sorry.
  • tew
    tew almost 8 years
    @EminezArtus I was thinking about this problem a little more, and one of the more fun things you could do with this is passing the username -c "/bin/rm -rf /home/user/" user.
  • cardamom
    cardamom over 5 years
    Thanks, this worked. Had to use sudo usermod -a -G shadow <user> instead, for some reason Debian did not like groupadd today. The other important point to note is that this only takes effect once you log out and log back in again.
  • PyTis
    PyTis about 5 years
    @cardamom, +1, Thanks! Very helpful!
  • Pacerier
    Pacerier almost 5 years
    Link down _____
  • HunnyBear
    HunnyBear over 4 years
    This is strongly discouraged for many applications, e.g. if you are using it for an apache process, you shouldn't give apache the abiility to read /etc/shadow because of the danger if an attacker were to get the contents of that file
  • trapicki
    trapicki over 4 years
    @HunnyBear: I was wrong. Reading the question again, and about pam, especially the pam_unix man page linux.die.net/man/8/pam_unix, I noticed that in a sane application 'pam' will take the credentials and answer correctly without special priviledges and without disclosing information. Seems like the referenced web2py implementation is just doing a poor job.
  • HunnyBear
    HunnyBear about 4 years
    yeah, I feel like the way to work around this for a python library might be something janky like requiring the ability to sudo pam, or something