mirror of https://github.com/cowrie/cowrie.git
Merge pull request #30 from honigbij/auth-checklogin-ip
IP address checking during authorization
This commit is contained in:
commit
d64b749801
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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 = {
|
||||
|
|
Loading…
Reference in New Issue