diff --git a/opencanary/config.py b/opencanary/config.py index 81c0b77..f628b51 100644 --- a/opencanary/config.py +++ b/opencanary/config.py @@ -1,4 +1,3 @@ -from six import iteritems import os import sys import json @@ -6,6 +5,7 @@ import string import subprocess import shutil +import re from os.path import expanduser from pkg_resources import resource_filename from pathlib import Path @@ -43,6 +43,11 @@ def detectIPTables(): return False +SERVICE_REGEXES = { + "ssh.version": r"(SSH-(2.0|1.5|1.99|1.0)-([!-,\-./0-~]+(:?$|\s))(?:[ -~]*)){1,253}$", +} + + class Config: def __init__(self, configfile=SETTINGS): self.__config = None @@ -93,53 +98,20 @@ def getVal(self, key, default=None): return default raise e - def setValues(self, params): # noqa: C901 + def checkValues(self): # noqa: C901 """Set all the valid values in params and return a list of errors for invalid""" - - # silently ensure that node_id and mac are not modified via web - sacred = ["device.node_id", "device.mac"] - for k in sacred: - if k in params: - del params[k] - - # if dhcp is enabled, ignore the static ip settings - if params.get("device.dhcp.enabled", False): - static = [ - "device.ip_address", - "device.netmask", - "device.gw", - "device.dns1", - "device.dns2", - ] - for k in static: - if k in params: - del params[k] - - # for each section, if disabled, delete ignore section's settings - disabled_modules = tuple( - filter( - lambda m: not params.get("%s.enabled" % m, False), - ["ftp", "ssh", "smb", "http"], - ) - ) - for k in params.keys(): - if not k.endswith("enabled") and k.startswith(disabled_modules): - del params[k] - continue - + params = self.__config # test options indpenedently for validity errors = [] - for key, value in iteritems(params): + for key, value in params.items(): try: - self.valid(key, value) + self.is_valid(key, value) except ConfigException as e: errors.append(e) # Test that no ports overlap - ports = {k: v for k, v in iteritems(self.__config) if k.endswith(".port")} - newports = {k: v for k, v in iteritems(params) if k.endswith(".port")} - ports.update(newports) - ports = [(port, setting) for setting, port in iteritems(ports)] + ports = {k: int(v) for k, v in params.items() if k.endswith(".port")} + ports = [(port, setting) for setting, port in ports.items()] ports.sort() for port, settings in itertools.groupby(ports, lambda x: x[0]): @@ -150,29 +122,9 @@ def setValues(self, params): # noqa: C901 for (port, setting) in settings: errors.append(ConfigException(setting, errmsg)) - # Delete invalid settings for which an error is reported - for err in errors: - if err.key in params: - del params[err.key] - - # Update current settings - self.__config.update(params) return errors - def setVal(self, key, val): - """Set value only if valid otherwise throw exception""" - errs = self.setValues({key: val}) - - # successful update - if not errs: - return - - # raise first error reported on the update key - for e in errs: - if e.key == key: - raise e - - def valid(self, key, val): # noqa: C901 + def is_valid(self, key, val): # noqa: C901 """ Test an the validity of an individual setting Raise config error message on failure. @@ -186,30 +138,19 @@ def valid(self, key, val): # noqa: C901 ) if key.endswith(".port"): - if (not isinstance(val, int)) or val < 1 or val > 65535: - raise ConfigException(key, "Invalid port number (%s)" % val) - + if not isinstance(val, int): + raise ConfigException( + key, "Invalid port number (%s). Must be an integer." % val + ) + if val < 1 or val > 65535: + raise ConfigException( + key, "Invalid port number (%s). Must be between 1 and 65535." % val + ) # Max length of SSH version string is 255 chars including trailing CR and LF # https://tools.ietf.org/html/rfc4253 if key == "ssh.version" and len(val) > 253: raise ConfigException(key, "SSH version string too long (%s..)" % val[:5]) - if key == "smb.filelist": - extensions = ["PDF", "DOC", "DOCX"] - for f in val: - if "name" not in f: - raise ConfigException(key, "No filename specified for %s" % f) - if "type" not in f: - raise ConfigException(key, "No filetype specified for %s" % f) - if not f["name"]: - raise ConfigException(key, "Filename cannot be empty") - if not f["type"]: - raise ConfigException(key, "File type cannot be empty") - if f["type"] not in extensions: - raise ConfigException( - key, "Extension %s is not supported" % f["type"] - ) - if key == "device.name": allowed_chars = string.ascii_letters + string.digits + "+-#_" @@ -235,23 +176,11 @@ def valid(self, key, val): # noqa: C901 "Please use only characters, digits, spaces and any of the following: + - # _", ) - return True + if key in SERVICE_REGEXES.keys(): + if not re.match(SERVICE_REGEXES[key], val): + raise ConfigException(key, f"{val} is not valid.") - def saveSettings(self): - """Backup config file to older version and save to new file""" - try: - cfg = self.__configfile - if os.path.isfile(cfg): - os.rename(cfg, cfg + ".bak") - - with open(cfg, "w") as f: - json.dump( - self.__config, f, sort_keys=True, indent=4, separators=(",", ": ") - ) - - except Exception as e: - print("[-] Failed to save config file %s" % e) - raise ConfigException("config", "%s" % e) + return True def __repr__(self): return self.__config.__repr__() @@ -287,3 +216,8 @@ def __repr__(self): config = Config() +errors = config.checkValues() +if errors: + for error in errors: + print(error) + sys.exit(1) diff --git a/opencanary/modules/ssh.py b/opencanary/modules/ssh.py index 71c8353..dfbcc41 100644 --- a/opencanary/modules/ssh.py +++ b/opencanary/modules/ssh.py @@ -50,11 +50,14 @@ def serviceStarted(self): self.bannerSent = False def sendBanner(self): - if self.bannerSent: + banner = self.transport.factory.preauth_banner + + if self.bannerSent or not banner: return - data = "" - data = "\r\n".join(data.splitlines() + [""]) - self.transport.sendPacket(userauth.MSG_USERAUTH_BANNER, NS(data) + NS("en")) + + # data = "" + # data = "\r\n".join(data.splitlines() + [""]) + self.transport.sendPacket(userauth.MSG_USERAUTH_BANNER, NS(banner) + NS("en")) self.bannerSent = True def auth_password(self, packet): @@ -121,8 +124,6 @@ def auth_publickey(self, packet): c = credentials.SSHPrivateKey(None, None, None, None, None) - # self.log(key=key) - return self.portal.login(c, None, conchinterfaces.IConchUser).addErrback( self._ebPassword ) @@ -132,55 +133,6 @@ def ssh_USERAUTH_REQUEST(self, packet): return userauth.SSHUserAuthServer.ssh_USERAUTH_REQUEST(self, packet) -# As implemented by Kojoney -class HoneyPotSSHFactory(factory.SSHFactory): - services = { - b"ssh-userauth": HoneyPotSSHUserAuthServer, - b"ssh-connection": connection.SSHConnection, - } - - # Special delivery to the loggers to avoid scope problems - def logDispatch(self, sessionid, msg): - data = {} - data["logdata"] = msg - self.logger.log(data) - # for dblog in self.dbloggers: - # dblog.logDispatch(sessionid, msg) - - def __init__(self, logger=None, version=None): - # protocol^Wwhatever instances are kept here for the interact feature - self.sessions = {} - self.logger = logger - self.version = version - - def buildProtocol(self, addr): - # FIXME: try to mimic something real 100% - t = HoneyPotTransport() - _modulis = "/etc/ssh/moduli", "/private/etc/moduli" - - if self.version: - t.ourVersionString = self.version - else: - t.ourVersionString = "empty" - - t.supportedPublicKeys = self.privateKeys.keys() - for _moduli in _modulis: - try: - self.primes = primes.parseModuliFile(_moduli) - break - except IOError: - pass - - if not self.primes: - ske = t.supportedKeyExchanges[:] - if "diffie-hellman-group-exchange-sha1" in ske: - ske.remove("diffie-hellman-group-exchange-sha1") - t.supportedKeyExchanges = ske - - t.factory = self - return t - - @implementer(portal.IRealm) class HoneyPotRealm: def __init__(self): @@ -272,6 +224,61 @@ def sendDisconnect(self, reason, desc): self.transport.loseConnection() +class HoneyPotSSHFactory(factory.SSHFactory): + + protocol = HoneyPotTransport + services = { + b"ssh-userauth": HoneyPotSSHUserAuthServer, + b"ssh-connection": connection.SSHConnection, + } + + # Special delivery to the loggers to avoid scope problems + def logDispatch(self, sessionid, msg): + data = {} + data["logdata"] = msg + self.logger.log(data) + + def __init__(self, logger=None, version=None, path=SSH_PATH, preauth_banner=None): + # protocol^Wwhatever instances are kept here for the interact feature + self.sessions = {} + self.logger = logger + self.version = version + self.protocol.ourVersionString = version + self.preauth_banner = preauth_banner + rsa_pubKeyString, rsa_privKeyString = getRSAKeys(path) + dsa_pubKeyString, dsa_privKeyString = getDSAKeys(path) + self.publicKeys = { + b"ssh-rsa": keys.Key.fromString(data=rsa_pubKeyString), + b"ssh-dss": keys.Key.fromString(data=dsa_pubKeyString), + b"rsa-sha2-512": keys.Key.fromString(data=rsa_pubKeyString), + b"rsa-sha2-256": keys.Key.fromString(data=rsa_pubKeyString), + } + self.privateKeys = { + b"ssh-rsa": keys.Key.fromString(data=rsa_privKeyString), + b"ssh-dss": keys.Key.fromString(data=dsa_privKeyString), + b"rsa-sha2-512": keys.Key.fromString(data=rsa_privKeyString), + b"rsa-sha2-256": keys.Key.fromString(data=rsa_privKeyString), + } + + def getPrimes(self): + """ + Called when the factory is started to get Diffie-Hellman generators and + primes to use. Returns a dictionary mapping number of bits to lists + of tuple of (generator, prime). + + @rtype: L{dict} + """ + _modulis = ["/etc/ssh/moduli", "/private/etc/moduli"] + _primes = None + for _moduli in _modulis: + try: + _primes = primes.parseModuliFile(_moduli) + break + except IOError: + pass + return _primes + + class HoneyPotSSHSession(session.SSHSession): def request_env(self, data): # print('request_env: %s' % (repr(data))) @@ -305,14 +312,14 @@ def windowChanged(self, windowSize): self.windowSize = windowSize -def getRSAKeys(): +def getRSAKeys(path): """ Checks for existing RSA Keys, if there are none, generates a 2048 bit RSA key pair, saves them to a temporary location and returns the keys formatted as OpenSSH keys. """ - public_key = os.path.join(SSH_PATH, "id_rsa.pub") - private_key = os.path.join(SSH_PATH, "id_rsa") + public_key = os.path.join(path, "id_rsa.pub") + private_key = os.path.join(path, "id_rsa") if not (os.path.exists(public_key) and os.path.exists(private_key)): ssh_key = rsa.generate_private_key( @@ -340,14 +347,14 @@ def getRSAKeys(): return public_key_string, private_key_string -def getDSAKeys(): +def getDSAKeys(path): """ Checks for existing DSA Keys, if there are none, generates a 2048 bit DSA key pair, saves them to a temporary location and returns the keys formatted as OpenSSH keys. """ - public_key = os.path.join(SSH_PATH, "id_dsa.pub") - private_key = os.path.join(SSH_PATH, "id_dsa") + public_key = os.path.join(path, "id_dsa.pub") + private_key = os.path.join(path, "id_dsa") if not (os.path.exists(public_key) and os.path.exists(private_key)): ssh_key = dsa.generate_private_key(key_size=1024, backend=default_backend()) @@ -407,27 +414,30 @@ def __init__(self, config=None, logger=None): self.version = config.getVal( "ssh.version", default="SSH-2.0-OpenSSH_5.1p1 Debian-5" ).encode("utf8") + + self.preauth_banner = ( + config.getVal("ssh.preauth_banner", default="") + .replace("\\r", "\r") + .replace("\\n", "\n") + .encode("utf8") + ) + + # need to ensure there's a trailing CRLF + if self.preauth_banner and not self.preauth_banner.endswith("\r\n"): + self.preauth_banner += "\r\n" + + self.ssh_keys_path = config.getVal("ssh.key_path", default=SSH_PATH) self.listen_addr = config.getVal("device.listen_addr", default="") def getService(self): - factory = HoneyPotSSHFactory(version=self.version, logger=self.logger) + factory = HoneyPotSSHFactory( + version=self.version, + logger=self.logger, + path=self.ssh_keys_path, + preauth_banner=self.preauth_banner, + ) factory.canaryservice = self factory.portal = portal.Portal(HoneyPotRealm()) - - rsa_pubKeyString, rsa_privKeyString = getRSAKeys() - dsa_pubKeyString, dsa_privKeyString = getDSAKeys() factory.portal.registerChecker(HoneypotPasswordChecker(logger=factory.logger)) factory.portal.registerChecker(CanaryPublicKeyChecker(logger=factory.logger)) - factory.publicKeys = { - b"ssh-rsa": keys.Key.fromString(data=rsa_pubKeyString), - b"ssh-dss": keys.Key.fromString(data=dsa_pubKeyString), - b"rsa-sha2-512": keys.Key.fromString(data=rsa_pubKeyString), - b"rsa-sha2-256": keys.Key.fromString(data=rsa_pubKeyString), - } - factory.privateKeys = { - b"ssh-rsa": keys.Key.fromString(data=rsa_privKeyString), - b"ssh-dss": keys.Key.fromString(data=dsa_privKeyString), - b"rsa-sha2-512": keys.Key.fromString(data=rsa_privKeyString), - b"rsa-sha2-256": keys.Key.fromString(data=rsa_privKeyString), - } return internet.TCPServer(self.port, factory, interface=self.listen_addr)