Merge pull request #30 from honigbij/auth-checklogin-ip

IP address checking during authorization
This commit is contained in:
Michel Oosterhof 2015-03-20 10:41:24 +04:00
commit d64b749801
3 changed files with 221 additions and 25 deletions

View File

@ -69,6 +69,24 @@ filesystem_file = fs.pickle
# (default: data_path)
data_path = data
# Class that implements the checklogin() method.
#
# Class must be defined in kippo/core/auth.py
# Default is the 'UserDB' class which uses the password database.
#
# Alternatively the 'AuthRandom' class can be used, which will let
# a user login after a random number of attempts.
# It will also cache username/password combinations that allow login.
#
auth_class = UserDB
# When AuthRandom is used also set the
# auth_class_parameters: <min try>, <max try>, <maxcache>
# for example: 2, 5, 10 = allows access after randint(2,5) attempts
# and cache 10 combinations.
#
#auth_class = AuthRandom
#auth_class_parameters = 2, 5, 10
# Directory for creating simple commands that only output text.
#
# The command must be placed under this directory with the proper path, such

View File

@ -2,13 +2,18 @@
# See the COPYRIGHT file for more information
import string
import json
from os import path
from sys import modules
from random import randint
from zope.interface import implementer
import twisted
from twisted.cred.checkers import ICredentialsChecker
from twisted.cred.credentials import IUsernamePassword, ISSHPrivateKey, IPluggableAuthenticationModules
from twisted.cred.credentials import IUsernamePassword, ISSHPrivateKey, \
IPluggableAuthenticationModules, ICredentials
from twisted.cred.error import UnauthorizedLogin, UnhandledCredentials
from twisted.internet import defer
@ -28,8 +33,7 @@ class UserDB(object):
def load(self):
'''load the user db'''
userdb_file = '%s/userdb.txt' % \
(config().get('honeypot', 'data_path'),)
userdb_file = '%s/userdb.txt' % (config().get('honeypot', 'data_path'),)
f = open(userdb_file, 'r')
while True:
@ -41,7 +45,7 @@ class UserDB(object):
if not line:
continue
if line.startswith( '#' ):
if line.startswith('#'):
continue
(login, uid_str, passwd) = line.split(':', 2)
@ -59,8 +63,7 @@ class UserDB(object):
def save(self):
'''save the user db'''
userdb_file = '%s/userdb.txt' % \
(config().get('honeypot', 'data_path'),)
userdb_file = '%s/userdb.txt' % (config().get('honeypot', 'data_path'),)
# Note: this is subject to races between kippo instances, but hey ...
f = open(userdb_file, 'w')
@ -68,14 +71,14 @@ class UserDB(object):
f.write('%s:%d:%s\n' % (login, uid, passwd))
f.close()
def checklogin(self, thelogin, thepasswd):
def checklogin(self, thelogin, thepasswd, src_ip = '0.0.0.0'):
'''check entered username/password against database'''
'''note that it allows multiple passwords for a single username'''
'''it also knows wildcard '*' for any password'''
'''prepend password with ! to explicitly deny it. Denials must come before wildcards'''
for (login, uid, passwd) in self.userdb:
# explicitly fail on !password
if login == thelogin and passwd == '!'+thepasswd:
if login == thelogin and passwd == '!' + thepasswd:
return False
if login == thelogin and passwd in (thepasswd, '*'):
return True
@ -114,6 +117,121 @@ class UserDB(object):
self.userdb.append((login, uid, passwd))
self.save()
class AuthRandom(object):
"""
Alternative class that defines the checklogin() method.
Users will be authenticated after a random number of attempts.
"""
def __init__(self, parameters):
# Default values
self.mintry, self.maxtry, self.maxcache = 2, 5, 10
parlist = parameters.split(',')
if len(parlist) == 3:
self.mintry = int(parlist[0])
self.maxtry = int(parlist[1])
self.maxcache = int(parlist[2])
if self.maxtry < self.mintry:
self.maxtry = self.mintry + 1
log.msg('maxtry < mintry, adjusting maxtry to: %d' % self.maxtry)
self.uservar = {}
self.loadvars()
def loadvars(self):
# Load user vars from json file
uservar_file = '%s/uservar.json' % (config().get('honeypot', 'data_path'))
if path.isfile(uservar_file):
with open(uservar_file, 'rb') as fp:
try:
self.uservar = json.load(fp)
except:
self.uservar = {}
def savevars(self):
# Save the user vars to json file
uservar_file = '%s/uservar.json' % (config().get('honeypot', 'data_path'))
data = self.uservar
# Note: this is subject to races between kippo logins
with open(uservar_file, 'wb') as fp:
json.dump(data, fp)
def checklogin(self, thelogin, thepasswd, src_ip):
'''Every new source IP will have to try a random number of times between'''
''''mintry' and 'maxtry' before succeeding to login.'''
'''All username/password combinations must be different.'''
'''The successful login combination is stored with the IP address.'''
'''Successful username/passwords pairs are also cached for 'maxcache' times.'''
'''This is to allow access for returns from different IP addresses.'''
'''Variables are saved in 'uservar.json' in the data directory.'''
auth = False
userpass = thelogin + ':' + thepasswd
if not 'cache' in self.uservar:
self.uservar['cache'] = []
cache = self.uservar['cache']
# Check if it is the first visit from src_ip
if src_ip not in self.uservar:
self.uservar[src_ip] = {}
ipinfo = self.uservar[src_ip]
ipinfo['try'] = 0
if userpass in cache:
log.msg('first time for %s, found cached: %s' % (src_ip, userpass))
ipinfo['max'] = 1
ipinfo['user'] = thelogin
ipinfo['pw'] = thepasswd
auth = True
self.savevars()
return auth
else:
ipinfo['max'] = randint(self.mintry, self.maxtry)
log.msg('first time for %s, need: %d' % (src_ip, ipinfo['max']))
ipinfo = self.uservar[src_ip]
# Fill in missing variables
if not 'max' in ipinfo:
ipinfo['max'] = randint(self.mintry, self.maxtry)
if not 'try' in ipinfo:
ipinfo['try'] = 0
if not 'tried' in ipinfo:
ipinfo['tried'] = []
# Don't count repeated username/password combinations
if userpass in ipinfo['tried']:
log.msg('already tried this combination')
self.savevars()
return auth
ipinfo['try'] += 1
attempts = ipinfo['try']
need = ipinfo['max']
log.msg('login attempt: %d' % attempts)
# Check if enough login attempts are tried
if attempts < need:
self.uservar[src_ip]['tried'].append(userpass)
elif attempts == need:
ipinfo['user'] = thelogin
ipinfo['pw'] = thepasswd
cache.append(userpass)
if len(cache) > self.maxcache:
cache.pop(0)
auth = True
# Returning after successful login
elif attempts > need:
if not 'user' in ipinfo or not 'pw' in ipinfo:
log.msg('return, but username or password not set!!!')
ipinfo['tried'].append(userpass)
ipinfo['try'] = 1
else:
log.msg('login return, expect: [%s/%s]' % (ipinfo['user'], ipinfo['pw']))
if thelogin == ipinfo['user'] and thepasswd == ipinfo['pw']:
auth = True
self.savevars()
return auth
@implementer(ICredentialsChecker)
class HoneypotPublicKeyChecker:
"""
@ -125,8 +243,27 @@ class HoneypotPublicKeyChecker:
def requestAvatarId(self, credentials):
_pubKey = keys.Key.fromString(credentials.blob)
log.msg(format='public key attempt for user %(username)s with fingerprint %(fingerprint)s',
username=credentials.username, fingerprint=_pubKey.fingerprint())
return failure.Failure(error.ConchError("Incorrect signature"))
username=credentials.username,
fingerprint=_pubKey.fingerprint())
return failure.Failure(error.ConchError('Incorrect signature'))
# This credential interface also provides an IP address
@implementer(IUsernamePassword)
class UsernamePasswordIP:
def __init__(self, username, password, ip):
self.username = username
self.password = password
self.ip = ip
# This credential interface also provides an IP address
@implementer(IPluggableAuthenticationModules)
class PluggableAuthenticationModulesIP:
def __init__(self, username, pamConversion, ip):
self.username = username
self.pamConversion = pamConversion
self.ip = ip
@implementer(ICredentialsChecker)
class HoneypotPasswordChecker:
@ -138,37 +275,59 @@ class HoneypotPasswordChecker:
def requestAvatarId(self, credentials):
if hasattr(credentials, 'password'):
if self.checkUserPass(credentials.username, credentials.password):
if self.checkUserPass(credentials.username, credentials.password,
credentials.ip):
return defer.succeed(credentials.username)
else:
return defer.fail(UnauthorizedLogin())
elif hasattr(credentials, 'pamConversion'):
return self.checkPamUser(credentials.username,
credentials.pamConversion)
credentials.pamConversion, credentials.ip)
return defer.fail(UnhandledCredentials())
def checkPamUser(self, username, pamConversion):
def checkPamUser(self, username, pamConversion, ip):
r = pamConversion((('Password:', 1),))
return r.addCallback(self.cbCheckPamUser, username)
return r.addCallback(self.cbCheckPamUser, username, ip)
def cbCheckPamUser(self, responses, username):
for response, zero in responses:
if self.checkUserPass(username, response):
def cbCheckPamUser(self, responses, username, ip):
for (response, zero) in responses:
if self.checkUserPass(username, response, ip):
return defer.succeed(username)
return defer.fail(UnauthorizedLogin())
def checkUserPass(self, theusername, thepassword):
if UserDB().checklogin(theusername, thepassword):
#log.msg( 'login attempt [%s/%s] succeeded' % (theusername, thepassword) )
log.msg( eventid='KIPP0002',
def checkUserPass(self, theusername, thepassword, ip):
# UserDB is the default auth_class
authname = UserDB
parameters = None
# Is the auth_class defined in the config file?
if config().has_option('honeypot', 'auth_class'):
authclass = config().get('honeypot', 'auth_class')
# Check if authclass exists in this module
if hasattr(modules[__name__], authclass):
authname = getattr(modules[__name__], authclass)
# Are there auth_class parameters?
if config().has_option('honeypot', 'auth_class_parameters'):
parameters = config().get('honeypot', 'auth_class_parameters')
else:
log.msg('auth_class: %s not found in %s' % (authclass, __name__))
if parameters:
theauth = authname(parameters)
else:
theauth = authname()
if theauth.checklogin(theusername, thepassword, ip):
log.msg(eventid='KIPP0002',
format='login attempt [%(username)s/%(password)s] succeeded',
username=theusername, password=thepassword )
username=theusername, password=thepassword)
return True
else:
#log.msg( 'login attempt [%s/%s] failed' % (theusername, thepassword) )
log.msg( eventid='KIPP0003',
log.msg(eventid='KIPP0003',
format='login attempt [%(username)s/%(password)s] failed',
username=theusername, password=thepassword )
username=theusername, password=thepassword)
return False
# vim: set sw=4 et:

View File

@ -56,6 +56,25 @@ class HoneyPotSSHUserAuthServer(userauth.SSHUserAuthServer):
self.sendBanner()
return userauth.SSHUserAuthServer.ssh_USERAUTH_REQUEST(self, packet)
# Overridden to pass src_ip to auth.UsernamePasswordIP
def auth_password(self, packet):
password = getNS(packet[1:])[0]
src_ip = self.transport.transport.getPeer().host
c = auth.UsernamePasswordIP(self.user, password, src_ip)
return self.portal.login(c, None, conchinterfaces.IConchUser).addErrback(
self._ebPassword)
# Overridden to pass src_ip to auth.PluggableAuthenticationModulesIP
def auth_keyboard_interactive(self, packet):
if self._pamDeferred is not None:
self.transport.sendDisconnect(
transport.DISCONNECT_PROTOCOL_ERROR,
"only one keyboard interactive attempt at a time")
return defer.fail(error.IgnoreAuthentication())
src_ip = self.transport.transport.getPeer().host
c = auth.PluggableAuthenticationModulesIP(self.user, self._pamConv, src_ip)
return self.portal.login(c, None, conchinterfaces.IConchUser)
# As implemented by Kojoney
class HoneyPotSSHFactory(factory.SSHFactory):
services = {