5

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 in my opinion.

How can I make use of PAM for username/password validation without requiring root privileges?

5 Answers 5

8

Note: may be outdated, see comments by Donovan Baarda!

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.

Sign up to request clarification or add additional context in comments.

7 Comments

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.
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
@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.
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
I think the behaviour of pam_unix has maybe changed over time. I definitely had a program working without root or shadow group permissions in the past and noticed that now it doesn't. Some of the comments I've seen suggest this is by design, and the fact that it used to work may have been considered a security hole. Possible reasons why it used to work are; 1. My program starts as root and setuid() drops root, but also did getpwnam() before dropping them. Perhaps pam_unix used to keep file handles to shadow open? 2. Old nss-caches ran as root, bypassing shadow protection.
|
4

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

1 Comment

Thanks, although I don't like this particular approach it made me start thinking. I have found a solution now. See answer.
4

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')

3 Comments

@FoxWilson Could you give us an example of how you think it would best be sanitized?
@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.
@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.
3

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.

1 Comment

Link down _____
3

Maybe python-pam can work for you.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.