mirror of
https://github.com/paradoxxxzero/butterfly.git
synced 2026-05-26 07:08:08 +00:00
terminal: support PAM authentication
Fix #129 Actually, we are reinventing the wheel... But after all, it is not possible to change the profile name of `su`, so we just pull in the PAM bindings for Python and use it for PAM authentication. A new option `--pam_profile` has been added for users to specify their preferred PAM profile. Note that Butterfly should be started as ROOT or it will not be possible to authenticate via PAM.
This commit is contained in:
14
README.md
14
README.md
@@ -45,6 +45,20 @@ To get an overview of butterfly features.
|
||||
$ butterfly.server.py --host=myhost --port=57575
|
||||
```
|
||||
|
||||
Or with login prompt
|
||||
|
||||
```bash
|
||||
$ butterfly.server.py --host=myhost --port=57575 --login
|
||||
```
|
||||
|
||||
Or with PAM authentication (ROOT required)
|
||||
|
||||
```bash
|
||||
# butterfly.server.py --host=myhost --port=57575 --login --pam_profile=sshd
|
||||
```
|
||||
|
||||
You can change `sshd` to your preferred PAM profile.
|
||||
|
||||
The first time it will ask you to generate the certificates (see: [here](http://paradoxxxzero.github.io/2014/03/21/butterfly-with-ssl-auth.html))
|
||||
|
||||
|
||||
|
||||
@@ -52,6 +52,8 @@ tornado.options.define("unsecure", default=False,
|
||||
help="Don't use ssl not recommended")
|
||||
tornado.options.define("login", default=False,
|
||||
help="Use login screen at start")
|
||||
tornado.options.define("pam_profile", default="", type=str,
|
||||
help="When --login=True provided and running as ROOT, use PAM with the specified PAM profile for authentication and then execute the user's default shell. Will override --shell.")
|
||||
tornado.options.define("force_unicode_width",
|
||||
default=False,
|
||||
help="Force all unicode characters to the same width."
|
||||
|
||||
171
butterfly/pam.py
Normal file
171
butterfly/pam.py
Normal file
@@ -0,0 +1,171 @@
|
||||
# (c) 2007 Chris AtLee <chris@atlee.ca>
|
||||
# Licensed under the MIT license:
|
||||
# http://www.opensource.org/licenses/mit-license.php
|
||||
#
|
||||
# Original author: Chris AtLee
|
||||
#
|
||||
# Modified by David Ford, 2011-12-6
|
||||
# added py3 support and encoding
|
||||
# added pam_end
|
||||
# added pam_setcred to reset credentials after seeing Leon Walker's remarks
|
||||
# added byref as well
|
||||
# use readline to prestuff the getuser input
|
||||
# Modified by Peter Cai, 2017-02-10
|
||||
# interactive login for Butterfly
|
||||
|
||||
'''
|
||||
PAM module for python
|
||||
Provides an authenticate function that will allow the caller to authenticate
|
||||
a user against the Pluggable Authentication Modules (PAM) on the system.
|
||||
Implemented using ctypes, so no compilation is necessary.
|
||||
'''
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
||||
from ctypes import CDLL, POINTER, Structure, CFUNCTYPE, cast, byref, sizeof
|
||||
from ctypes import c_void_p, c_size_t, c_char_p, c_char, c_int
|
||||
from ctypes import memmove
|
||||
from ctypes.util import find_library
|
||||
|
||||
class PamHandle(Structure):
|
||||
"""wrapper class for pam_handle_t pointer"""
|
||||
_fields_ = [ ("handle", c_void_p) ]
|
||||
|
||||
def __init__(self):
|
||||
Structure.__init__(self)
|
||||
self.handle = 0
|
||||
|
||||
class PamMessage(Structure):
|
||||
"""wrapper class for pam_message structure"""
|
||||
_fields_ = [ ("msg_style", c_int), ("msg", c_char_p) ]
|
||||
|
||||
def __repr__(self):
|
||||
return "<PamMessage %i '%s'>" % (self.msg_style, self.msg)
|
||||
|
||||
class PamResponse(Structure):
|
||||
"""wrapper class for pam_response structure"""
|
||||
_fields_ = [ ("resp", c_char_p), ("resp_retcode", c_int) ]
|
||||
|
||||
def __repr__(self):
|
||||
return "<PamResponse %i '%s'>" % (self.resp_retcode, self.resp)
|
||||
|
||||
conv_func = CFUNCTYPE(c_int, c_int, POINTER(POINTER(PamMessage)), POINTER(POINTER(PamResponse)), c_void_p)
|
||||
|
||||
class PamConv(Structure):
|
||||
"""wrapper class for pam_conv structure"""
|
||||
_fields_ = [ ("conv", conv_func), ("appdata_ptr", c_void_p) ]
|
||||
|
||||
# Various constants
|
||||
PAM_PROMPT_ECHO_OFF = 1
|
||||
PAM_PROMPT_ECHO_ON = 2
|
||||
PAM_ERROR_MSG = 3
|
||||
PAM_TEXT_INFO = 4
|
||||
PAM_REINITIALIZE_CRED = 8
|
||||
|
||||
libc = CDLL(find_library("c"))
|
||||
libpam = CDLL(find_library("pam"))
|
||||
libpam_misc = CDLL(find_library("pam_misc"))
|
||||
|
||||
calloc = libc.calloc
|
||||
calloc.restype = c_void_p
|
||||
calloc.argtypes = [c_size_t, c_size_t]
|
||||
|
||||
pam_end = libpam.pam_end
|
||||
pam_end.restype = c_int
|
||||
pam_end.argtypes = [PamHandle, c_int]
|
||||
|
||||
pam_start = libpam.pam_start
|
||||
pam_start.restype = c_int
|
||||
pam_start.argtypes = [c_char_p, c_char_p, POINTER(PamConv), POINTER(PamHandle)]
|
||||
|
||||
pam_setcred = libpam.pam_setcred
|
||||
pam_setcred.restype = c_int
|
||||
pam_setcred.argtypes = [PamHandle, c_int]
|
||||
|
||||
pam_strerror = libpam.pam_strerror
|
||||
pam_strerror.restype = c_char_p
|
||||
pam_strerror.argtypes = [PamHandle, c_int]
|
||||
|
||||
pam_authenticate = libpam.pam_authenticate
|
||||
pam_authenticate.restype = c_int
|
||||
pam_authenticate.argtypes = [PamHandle, c_int]
|
||||
|
||||
misc_conv = libpam_misc.misc_conv
|
||||
|
||||
class PAM():
|
||||
code = 0
|
||||
reason = None
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def authenticate(self, username, service='login', encoding='utf-8', resetcreds=True):
|
||||
"""PAM authentication through standard input for the given service.
|
||||
Returns True for success, or False for failure.
|
||||
self.code (integer) and self.reason (string) are always stored and may
|
||||
be referenced for the reason why authentication failed. 0/'Success' will
|
||||
be stored for success.
|
||||
Python3 expects bytes() for ctypes inputs. This function will make
|
||||
necessary conversions using the supplied encoding.
|
||||
Inputs:
|
||||
username: username to authenticate
|
||||
service: PAM service to authenticate against, defaults to 'login'
|
||||
Returns:
|
||||
success: True
|
||||
failure: False
|
||||
"""
|
||||
|
||||
# python3 ctypes prefers bytes
|
||||
if sys.version_info >= (3,):
|
||||
if isinstance(username, str): username = username.encode(encoding)
|
||||
if isinstance(service, str): service = service.encode(encoding)
|
||||
else:
|
||||
if isinstance(username, unicode):
|
||||
username = username.encode(encoding)
|
||||
if isinstance(service, unicode):
|
||||
service = service.encode(encoding)
|
||||
|
||||
if b'\x00' in username or b'\x00' in service:
|
||||
self.code = 4 # PAM_SYSTEM_ERR in Linux-PAM
|
||||
self.reason = 'strings may not contain NUL'
|
||||
return False
|
||||
|
||||
handle = PamHandle()
|
||||
conv = PamConv(conv_func(misc_conv), 0)
|
||||
retval = pam_start(service, username, byref(conv), byref(handle))
|
||||
|
||||
if retval != 0:
|
||||
# This is not an authentication error, something has gone wrong starting up PAM
|
||||
self.code = retval
|
||||
self.reason = "pam_start() failed"
|
||||
return False
|
||||
|
||||
retval = pam_authenticate(handle, 0)
|
||||
auth_success = retval == 0
|
||||
|
||||
if auth_success and resetcreds:
|
||||
retval = pam_setcred(handle, PAM_REINITIALIZE_CRED);
|
||||
|
||||
# store information to inform the caller why we failed
|
||||
self.code = retval
|
||||
self.reason = pam_strerror(handle, retval)
|
||||
if sys.version_info >= (3,):
|
||||
self.reason = self.reason.decode(encoding)
|
||||
|
||||
pam_end(handle, retval)
|
||||
|
||||
return auth_success
|
||||
|
||||
def login_prompt(username, profile, env):
|
||||
pam = PAM()
|
||||
|
||||
success = pam.authenticate(username, profile)
|
||||
print('{} {}'.format(pam.code, pam.reason))
|
||||
|
||||
if success:
|
||||
su = '/usr/bin/su'
|
||||
if not os.path.exists(su):
|
||||
su = '/bin/su'
|
||||
os.execvpe(su, [su, username], env)
|
||||
return success
|
||||
@@ -31,7 +31,7 @@ import tornado.process
|
||||
import tornado.web
|
||||
import tornado.websocket
|
||||
from logging import getLogger
|
||||
from butterfly import utils, __version__
|
||||
from butterfly import pam, utils, __version__
|
||||
|
||||
log = getLogger('butterfly')
|
||||
ioloop = tornado.ioloop.IOLoop.instance()
|
||||
@@ -223,7 +223,7 @@ class Terminal(object):
|
||||
# Unsecure connection with su
|
||||
if server.root:
|
||||
if self.socket.local:
|
||||
if self.callee != self.caller:
|
||||
if self.callee != self.caller and tornado.options.options.pam_profile == "":
|
||||
# Force password prompt by dropping rights
|
||||
# to the daemon user
|
||||
os.setuid(daemon.uid)
|
||||
@@ -234,6 +234,7 @@ class Terminal(object):
|
||||
sys.exit(1)
|
||||
os.setuid(daemon.uid)
|
||||
|
||||
if (not server.root) or tornado.options.options.pam_profile == "":
|
||||
if os.path.exists('/usr/bin/su'):
|
||||
args = ['/usr/bin/su']
|
||||
else:
|
||||
@@ -245,6 +246,8 @@ class Terminal(object):
|
||||
args.append(tornado.options.options.shell)
|
||||
args.append(self.callee.name)
|
||||
os.execvpe(args[0], args, env)
|
||||
else:
|
||||
pam.login_prompt(self.callee.name, tornado.options.options.pam_profile, env)
|
||||
|
||||
def communicate(self):
|
||||
fcntl.fcntl(self.fd, fcntl.F_SETFL, os.O_NONBLOCK)
|
||||
|
||||
Reference in New Issue
Block a user