diff --git a/.github/dependabot.yml b/.github/dependabot.yml index e470ff9f1e..44f3aeadc9 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -3,13 +3,22 @@ # Please see the documentation for all configuration options: # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates +--- version: 2 updates: - - package-ecosystem: "pip" # See documentation for possible values - directory: "/" # Location of package manifests + - package-ecosystem: "pip" + directory: "/" schedule: interval: "weekly" - - package-ecosystem: "github-actions" # See documentation for possible values - directory: "/" # Location of package manifests + - package-ecosystem: "pip" + directory: "/docs" + schedule: + interval: "weekly" + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + - package-ecosystem: "docker" + directory: "/docker" schedule: interval: "weekly" diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index e6c646bb12..6274767206 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -39,28 +39,27 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 # Add support for more platforms with QEMU (optional) # https://github.com/docker/setup-qemu-action - name: Set up QEMU - uses: docker/setup-qemu-action@v2 + uses: docker/setup-qemu-action@v3 - name: Login to Docker Hub - if: github.event_name != 'pull_request' - uses: docker/login-action@v2 + if: ${{ (github.event_name != 'pull_request') && (github.repository == 'cowrie/cowrie') }} + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 + uses: docker/setup-buildx-action@v3 with: platforms: linux/amd64,linux/arm64 - # $(DOCKER) buildx build --platform ${PLATFORM} -t ${IMAGE}:${TAG} --build-arg BUILD_DATE=${BUILD_DATE} -f docker/Dockerfile --push . - name: Build - uses: docker/build-push-action@v4 + uses: docker/build-push-action@v5 with: push: false load: true @@ -72,11 +71,13 @@ jobs: docker run -d --rm cowrie:test - name: Build and push - if: github.event_name != 'pull_request' - uses: docker/build-push-action@v4 + if: ${{ (github.event_name != 'pull_request') && (github.repository == 'cowrie/cowrie') }} + uses: docker/build-push-action@v5 with: file: docker/Dockerfile platforms: linux/amd64,linux/arm64 push: true load: false + sbom: true + provenance: true tags: cowrie/cowrie:latest diff --git a/.github/workflows/tox.yml b/.github/workflows/tox.yml index 8b97c44649..3d3084f1d1 100644 --- a/.github/workflows/tox.yml +++ b/.github/workflows/tox.yml @@ -20,11 +20,11 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.8", "3.9", "3.10", "3.11", "pypy-3.8", "pypy-3.9"] + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "pypy-3.9", "pypy-3.10"] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install dependencies diff --git a/.readthedocs.yml b/.readthedocs.yml index eeac796425..5d21f76444 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -15,9 +15,13 @@ formats: sphinx: configuration: docs/conf.py +build: + os: ubuntu-22.04 + tools: + python: "3.11" + # Optionally Python version and requirements required to build your docs python: - version: 3.8 install: - requirements: docs/requirements.txt - method: pip diff --git a/INSTALL.rst b/INSTALL.rst index 9636b86070..304721870e 100644 --- a/INSTALL.rst +++ b/INSTALL.rst @@ -198,20 +198,12 @@ Running using Supervisord (OPTIONAL) On Debian, put the below in /etc/supervisor/conf.d/cowrie.conf:: [program:cowrie] - command=/home/cowrie/cowrie/bin/cowrie start + command=/home/cowrie/cowrie/bin/cowrie start -n directory=/home/cowrie/cowrie/ user=cowrie autorestart=true redirect_stderr=true -Update the bin/cowrie script, change:: - - DAEMONIZE="" - -to:: - - DAEMONIZE="-n" - Configure Additional Output Plugins (OPTIONAL) ********************************************** diff --git a/Makefile b/Makefile index f8b355fc9c..8396d870d1 100644 --- a/Makefile +++ b/Makefile @@ -84,17 +84,23 @@ TAG=$(shell git rev-parse --short=8 HEAD) docker-build: docker/Dockerfile ## Build Docker image -$(DOCKER) buildx create --name cowrie-builder $(DOCKER) buildx use cowrie-builder - $(DOCKER) buildx build --platform ${PLATFORM} -t ${IMAGE}:${TAG} -t ${IMAGE}:latest --build-arg BUILD_DATE=${BUILD_DATE} -f docker/Dockerfile --push . + $(DOCKER) buildx build --sbom=true --provenance=true --platform ${PLATFORM} -t ${IMAGE}:${TAG} -t ${IMAGE}:latest --build-arg BUILD_DATE=${BUILD_DATE} -f docker/Dockerfile . -.PHONY: docker-run -docker-run: docker-start ## Run Docker container +.PHONY: docker-load +docker-load: docker-build ## Load Docker image + -$(DOCKER) buildx create --name cowrie-builder + $(DOCKER) buildx use cowrie-builder + $(DOCKER) buildx build --sbom=true --provenance=true --load -t ${IMAGE}:${TAG} -t ${IMAGE}:latest --build-arg BUILD_DATE=${BUILD_DATE} -f docker/Dockerfile . -.PHONY: docker-push -docker-push: docker-build ## Push Docker image to Docker Hub +.PHONY: docker-build ## Push Docker image +docker-push: ## Push Docker image to Docker Hub + -$(DOCKER) buildx create --name cowrie-builder @echo "Pushing image to GitHub Docker Registry...\n" - $(DOCKER) push $(IMAGE):$(TAG) - $(DOCKER) tag $(IMAGE):$(TAG) $(IMAGE):latest - $(DOCKER) push $(IMAGE):latest + $(DOCKER) buildx use cowrie-builder + $(DOCKER) buildx build --sbom=true --provenance=true --platform ${PLATFORM} -t ${IMAGE}:${TAG} -t ${IMAGE}:latest --build-arg BUILD_DATE=${BUILD_DATE} -f docker/Dockerfile --push . + +.PHONY: docker-run +docker-run: docker-start ## Run Docker container .PHONY: docker-start docker-start: docker-create-volumes ## Start Docker container diff --git a/bin/asciinema b/bin/asciinema index 76c00bbe76..c6ddb98df7 100755 --- a/bin/asciinema +++ b/bin/asciinema @@ -1,131 +1,12 @@ #!/usr/bin/env python -import getopt -import json -import os -import struct import sys +from os import path -OP_OPEN, OP_CLOSE, OP_WRITE, OP_EXEC = 1, 2, 3, 4 -TYPE_INPUT, TYPE_OUTPUT, TYPE_INTERACT = 1, 2, 3 - -COLOR_INTERACT = "\033[36m" -COLOR_INPUT = "\033[33m" -COLOR_RESET = "\033[0m" - - -def playlog(fd, settings): - thelog = {} - thelog["version"] = 1 - thelog["width"] = 80 - thelog["height"] = 24 - thelog["duration"] = 0.0 - thelog["command"] = "/bin/bash" - thelog["title"] = "Cowrie Recording" - theenv = {} - theenv["TERM"] = "xterm256-color" - theenv["SHELL"] = "/bin/bash" - thelog["env"] = theenv - stdout = [] - thelog["stdout"] = stdout - - ssize = struct.calcsize(" ..." - % os.path.basename(sys.argv[0]) - ) - - if verbose: - print( - " -c colorify the output based on what streams are being received" - ) - print(" -h display this help") - print(" -o write to the specified output file") +cowriepath = path.dirname(sys.argv[0]) + "/../src" +sys.path.append(cowriepath) +from cowrie.scripts import asciinema # noqa: E402 if __name__ == "__main__": - settings = {"colorify": 0, "output": ""} - - try: - optlist, args = getopt.getopt(sys.argv[1:], "hco:") - except getopt.GetoptError as error: - sys.stderr.write(f"{sys.argv[0]}: {error}\n") - help() - sys.exit(1) - - for o, a in optlist: - if o == "-h": - help() - if o == "-c": - settings["colorify"] = True - if o == "-o": - settings["output"] = a - - if len(args) < 1: - help() - sys.exit(2) - - for logfile in args: - try: - logfd = open(logfile, "rb") - playlog(logfd, settings) - except OSError as e: - sys.stderr.write(f"{sys.argv[0]}: {e}\n") + asciinema.run() diff --git a/bin/createdynamicprocess b/bin/createdynamicprocess new file mode 100755 index 0000000000..ba4ce4a81b --- /dev/null +++ b/bin/createdynamicprocess @@ -0,0 +1,12 @@ +#!/usr/bin/env python + +import sys +from os import path + +cowriepath = path.dirname(sys.argv[0]) + "/../src" +sys.path.append(cowriepath) + +from cowrie.scripts import createdynamicprocess # noqa: E402 + +if __name__ == "__main__": + createdynamicprocess.run() diff --git a/bin/createdynamicprocess.py b/bin/createdynamicprocess.py deleted file mode 100755 index e1640977e6..0000000000 --- a/bin/createdynamicprocess.py +++ /dev/null @@ -1,56 +0,0 @@ -#!/usr/bin/env python - -import datetime -import json -import random - -import psutil - -command: dict = {} -command["command"] = {} -command["command"]["ps"] = [] - -randomStates = ["Ss", "S<", "D<", "Ss+"] -for proc in psutil.process_iter(): - try: - info = proc.as_dict( - attrs=[ - "pid", - "name", - "cmdline", - "username", - "cpu_percent", - "memory_percent", - "memory_info", - "create_time", - "terminal", - "status", - "cpu_times", - ] - ) - except psutil.NoSuchProcess: - pass - else: - object = {} - object["USER"] = info["username"] - object["PID"] = info["pid"] - if info["cmdline"]: - object["COMMAND"] = "/".join(info["cmdline"]) - else: - object["COMMAND"] = "[ " + info["name"] + " ]" - object["CPU"] = info["cpu_percent"] - object["MEM"] = info["memory_percent"] - object["RSS"] = info["memory_info"].rss - object["VSZ"] = info["memory_info"].vms - object["START"] = datetime.datetime.fromtimestamp(info["create_time"]).strftime( - "%b%d" - ) - if info["terminal"]: - object["TTY"] = str(info["terminal"]).replace("/dev/", "") - else: - object["TTY"] = "?" - object["STAT"] = random.choice(randomStates) - object["TIME"] = info["cpu_times"].user - command["command"]["ps"].append(object) - -print(json.dumps(command, indent=4, sort_keys=True)) diff --git a/bin/createfs b/bin/createfs index 55f2748d2c..cdad92a47b 100755 --- a/bin/createfs +++ b/bin/createfs @@ -1,194 +1,13 @@ #!/usr/bin/env python -############################################################### -# This program creates a cowrie file system pickle file. -# -# This is meant to build a brand new filesystem. -# To edit the file structure, please use 'bin/fsctl' -# -############################################################## - -import fnmatch -import getopt -import os -import pickle import sys -from stat import ( - S_ISBLK, - S_ISCHR, - S_ISDIR, - S_ISFIFO, - S_ISLNK, - S_ISREG, - S_ISSOCK, - ST_MODE, -) - -( - A_NAME, - A_TYPE, - A_UID, - A_GID, - A_SIZE, - A_MODE, - A_CTIME, - A_CONTENTS, - A_TARGET, - A_REALFILE, -) = range(0, 10) -T_LINK, T_DIR, T_FILE, T_BLK, T_CHR, T_SOCK, T_FIFO = range(0, 7) -PROC = False -VERBOSE = False - -blacklist_files = [ - "/root/fs.pickle", - "/root/createfs", - "*cowrie*", - "*kippo*", -] - - -def logit(ftxt): - if VERBOSE: - sys.stderr.write(ftxt) - - -def checkblacklist(ftxt): - for value in blacklist_files: - if fnmatch.fnmatch(ftxt, value): - return True - return False - - -def recurse(localroot, root, tree, maxdepth=100): - if maxdepth == 0: - return - - localpath = os.path.join(localroot, root[1:]) - - logit(" %s\n" % (localpath)) - - if not os.access(localpath, os.R_OK): - logit(" Cannot access %s\n" % localpath) - return - - for name in os.listdir(localpath): - fspath = os.path.join(root, name) - if checkblacklist(fspath): - continue +from os import path - path = os.path.join(localpath, name) +cowriepath = path.dirname(sys.argv[0]) + "/../src" +sys.path.append(cowriepath) - try: - if os.path.islink(path): - s = os.lstat(path) - else: - s = os.stat(path) - except OSError: - continue - - entry = [ - name, - T_FILE, - s.st_uid, - s.st_gid, - s.st_size, - s.st_mode, - int(s.st_mtime), - [], - None, - None, - ] - - if S_ISLNK(s[ST_MODE]): - if not os.access(path, os.R_OK): - logit(" Cannot access link: %s\n" % path) - continue - realpath = os.path.realpath(path) - if not realpath.startswith(localroot): - logit( - ' Link "%s" has real path "%s" outside local root "%s"\n' - % (path, realpath, localroot) - ) - continue - else: - entry[A_TYPE] = T_LINK - entry[A_TARGET] = realpath[len(localroot) :] - elif S_ISDIR(s[ST_MODE]): - entry[A_TYPE] = T_DIR - if (PROC or not localpath.startswith("/proc/")) and maxdepth > 0: - recurse(localroot, fspath, entry[A_CONTENTS], maxdepth - 1) - elif S_ISREG(s[ST_MODE]): - entry[A_TYPE] = T_FILE - elif S_ISBLK(s[ST_MODE]): - entry[A_TYPE] = T_BLK - elif S_ISCHR(s[ST_MODE]): - entry[A_TYPE] = T_CHR - elif S_ISSOCK(s[ST_MODE]): - entry[A_TYPE] = T_SOCK - elif S_ISFIFO(s[ST_MODE]): - entry[A_TYPE] = T_FIFO - else: - sys.stderr.write("We should handle %s" % path) - sys.exit(1) - - tree.append(entry) - - -def help(brief=False): - print( - "Usage: %s [-h] [-v] [-p] [-l dir] [-d maxdepth] [-o file]\n" - % os.path.basename(sys.argv[0]) - ) - - if not brief: - print(" -v verbose") - print(" -p include /proc") - print( - " -l local root directory (default is current working directory)" - ) - print(" -d maximum depth (default is full depth)") - print(" -o write output to file instead of stdout") - print(" -h display this help\n") - - sys.exit(1) +from cowrie.scripts import createfs # noqa: E402 if __name__ == "__main__": - maxdepth = 100 - localroot = os.getcwd() - output = "" - - try: - optlist, args = getopt.getopt(sys.argv[1:], "hvpl:d:o:", ["help"]) - except getopt.GetoptError as error: - sys.stderr.write("Error: %s\n" % error) - help() - - for o, a in optlist: - if o == "-v": - VERBOSE = True - elif o == "-p": - PROC = True - elif o == "-l": - localroot = a - elif o == "-d": - maxdepth = int(a) - elif o == "-o": - output = a - elif o in ["-h", "--help"]: - help() - - if output and os.path.isfile(output): - sys.stderr.write("File: %s exists!\n" % output) - sys.exit(1) - - logit("Processing:\n") - - tree = ["/", T_DIR, 0, 0, 0, 0, 0, [], ""] - recurse(localroot, "/", tree[A_CONTENTS], maxdepth) - - if output: - pickle.dump(tree, open(output, "wb")) - else: - print(pickle.dumps(tree)) + createfs.run() diff --git a/bin/fsctl b/bin/fsctl index 2cfed09666..0d5cadfbfc 100755 --- a/bin/fsctl +++ b/bin/fsctl @@ -1,774 +1,12 @@ #!/usr/bin/env python -################################################################ -# This is a command line interpreter used to edit -# cowrie file system pickle files. -# -# It is intended to mimic a basic bash shell and supports -# relative file references. -# -# Do not use to build a complete file system. Use: -# /opt/cowrie/bin/createfs -# -# Instead it should be used to edit existing file systems -# such as the default: /opt/cowrie/data/fs.pickle. -# -# Donovan Hubbard -# Douglas Hubbard -# March 2013 -################################################################ - -import cmd -import copy -import os -import pickle import sys -import time -from stat import ( - S_IRGRP, - S_IROTH, - S_IRUSR, - S_IWGRP, - S_IWOTH, - S_IWUSR, - S_IXGRP, - S_IXOTH, - S_IXUSR, -) - -( - A_NAME, - A_TYPE, - A_UID, - A_GID, - A_SIZE, - A_MODE, - A_CTIME, - A_CONTENTS, - A_TARGET, - A_REALFILE, -) = list(range(0, 10)) -T_LINK, T_DIR, T_FILE, T_BLK, T_CHR, T_SOCK, T_FIFO = list(range(0, 7)) - - -def getpath(fs, path): - cwd = fs - for part in path.split("/"): - if not len(part): - continue - ok = False - for c in cwd[A_CONTENTS]: - if c[A_NAME] == part: - cwd = c - ok = True - break - if not ok: - raise Exception("File not found") - return cwd - - -def exists(fs, path): - try: - getpath(fs, path) - return True - except Exception as e: - if str(e) == "File not found": - return False - else: - raise Exception(e) - - -def is_directory(fs, path): - """ - Returns whether or not the file at 'path' is a directory - - :param fs: - :param path: - :return: - """ - file = getpath(fs, path) - if file[A_TYPE] == T_DIR: - return True - else: - return False - - -def resolve_reference(pwd, relativeReference): - """ - Used to resolve a current working directory and a relative reference into an absolute file reference. - """ - - tempPath = os.path.join(pwd, relativeReference) - absoluteReference = os.path.normpath(tempPath) - - return absoluteReference - - -class fseditCmd(cmd.Cmd): - def __init__(self, pickle_file_path): - cmd.Cmd.__init__(self) - - if not os.path.isfile(pickle_file_path): - print("File %s does not exist." % pickle_file_path) - sys.exit(1) - - try: - pickle_file = open(pickle_file_path, "rb") - except OSError as e: - print(f"Unable to open file {pickle_file_path}: {repr(e)}") - sys.exit(1) - - try: - self.fs = pickle.load(pickle_file, encoding="utf-8") - except Exception: - print( - ( - "Unable to load file '%s'. " - + "Are you sure it is a valid pickle file?" - ) - % (pickle_file_path,) - ) - sys.exit(1) - - self.pickle_file_path = pickle_file_path - - # get the name of the file so we can display it as the prompt - path_parts = pickle_file_path.split("/") - self.fs_name = path_parts[-1] - - self.update_pwd("/") - - self.intro = ( - "\nKippo/Cowrie file system interactive editor\n" - + "Donovan Hubbard, Douglas Hubbard, March 2013\n" - + "Type 'help' for help\n" - ) - - def save_pickle(self): - """ - saves the current file system to the pickle - :return: - """ - try: - pickle.dump(self.fs, open(self.pickle_file_path, "wb")) - except Exception as e: - print( - ( - "Unable to save pickle file '%s'. " - + "Are you sure you have write access?" - ) - % (self.pickle_file_path,) - ) - print(str(e)) - sys.exit(1) - - def do_exit(self, args): - """ - Exits the file system editor - """ - return True - - def do_EOF(self, args): - """ - The escape character ctrl+d exits the session - """ - # exiting from the do_EOF method does not create a newline automatically - # so we add it manually - print() - return True - - def do_ls(self, args): - """ - Prints the contents of a directory, use ls -l to list in long format - Prints the current directory if no arguments are specified - """ - - longls = False - - if args.startswith("-l"): - longls = True - args = args[3:] - - if not len(args): - path = self.pwd - else: - path = resolve_reference(self.pwd, args) - - if exists(self.fs, path) is False: - print(f"ls: cannot access {path}: No such file or directory") - return - - if is_directory(self.fs, path) is False: - print(f"ls: {path} is not a directory") - return - - cwd = getpath(self.fs, path) - files = cwd[A_CONTENTS] - files.sort() - - largest = 0 - if len(files): - largest = max([x[A_SIZE] for x in files]) - - for file in files: - if not longls: - if file[A_TYPE] == T_DIR: - print(file[A_NAME] + "/") - else: - print(file[A_NAME]) - continue - - perms = ["-"] * 10 - - if file[A_MODE] & S_IRUSR: - perms[1] = "r" - if file[A_MODE] & S_IWUSR: - perms[2] = "w" - if file[A_MODE] & S_IXUSR: - perms[3] = "x" - - if file[A_MODE] & S_IRGRP: - perms[4] = "r" - if file[A_MODE] & S_IWGRP: - perms[5] = "w" - if file[A_MODE] & S_IXGRP: - perms[6] = "x" - - if file[A_MODE] & S_IROTH: - perms[7] = "r" - if file[A_MODE] & S_IWOTH: - perms[8] = "w" - if file[A_MODE] & S_IXOTH: - perms[9] = "x" - - linktarget = "" - - if file[A_TYPE] == T_DIR: - perms[0] = "d" - elif file[A_TYPE] == T_LINK: - perms[0] = "l" - linktarget = f" -> {file[A_TARGET]}" - - perms = "".join(perms) - ctime = time.localtime(file[A_CTIME]) - uid = file[A_UID] - gid = file[A_GID] - - if uid == 0: - uid = "root" - else: - uid = str(uid).rjust(4) - - if gid == 0: - gid = "root" - else: - gid = str(gid).rjust(4) - - print( - "%s 1 %s %s %s %s %s%s" - % ( - perms, - uid, - gid, - str(file[A_SIZE]).rjust(len(str(largest))), - time.strftime("%Y-%m-%d %H:%M", ctime), - file[A_NAME], - linktarget, - ) - ) - - def update_pwd(self, directory): - self.pwd = directory - self.prompt = self.fs_name + ":" + self.pwd + "$ " - - def do_cd(self, args): - """ - Changes the current directory.\nUsage: cd - """ - - # count the number of arguments - # 1 or more arguments: changes the directory to the first arg - # and ignores the rest - # 0 arguments: changes to '/' - arguments = args.split() - - if not len(arguments): - self.update_pwd("/") - else: - relative_dir = arguments[0] - target_dir = resolve_reference(self.pwd, relative_dir) - - if exists(self.fs, target_dir) is False: - print("cd: %s: No such file or directory" % target_dir) - elif is_directory(self.fs, target_dir): - self.update_pwd(target_dir) - else: - print("cd: %s: Not a directory" % target_dir) - - def do_pwd(self, args): - """ - Prints the current working directory - - :param args: - :return: - """ - print(self.pwd) - - def do_mkdir(self, args): - """ - Add a new directory in the target directory. - Handles relative or absolute file paths. \n - Usage: mkdir ... - """ - - arg_list = args.split() - if len(arg_list) < 1: - print("usage: mkdir ...") - else: - for arg in arg_list: - self.mkfile(arg.split(), T_DIR) - - def do_touch(self, args): - """ - Add a new file in the target directory. - Handles relative or absolute file paths. \n - Usage: touch [] - """ - - arg_list = args.split() - - if len(arg_list) < 1: - print("Usage: touch ()") - else: - self.mkfile(arg_list, T_FILE) - - def mkfile(self, args, file_type): - """ - args must be a list of arguments - """ - cwd = self.fs - path = resolve_reference(self.pwd, args[0]) - pathList = path.split("/") - parentdir = "/".join(pathList[:-1]) - fileName = pathList[len(pathList) - 1] - - if not exists(self.fs, parentdir): - print(("Parent directory %s doesn't exist!") % (parentdir,)) - self.mkfile(parentdir.split(), T_DIR) - - if exists(self.fs, path): - print(f"Error: {path} already exists!") - return - - cwd = getpath(self.fs, parentdir) - - # get uid, gid, mode from parent - uid = cwd[A_UID] - gid = cwd[A_GID] - mode = cwd[A_MODE] - - # Modify file_mode when it is a file - if file_type == T_FILE: - file_file_mode = int("0o100000", 8) - permits = mode & (2**9 - 1) - mode = file_file_mode + permits - - # create default file/directory size if none is specified - if len(args) == 1: - size = 4096 - else: - size = int(args[1]) - - # set the last update time stamp to now - ctime = time.time() - - cwd[A_CONTENTS].append( - [fileName, file_type, uid, gid, size, mode, ctime, [], None, None] - ) - - self.save_pickle() - - print("Added '%s'" % path) - - def do_rm(self, arguments): - """ - Remove an object from the file system. - Will not remove a directory unless the -r switch is invoked.\n - Usage: rm [-r] - """ - - args = arguments.split() - - if len(args) < 1 or len(args) > 2: - print("Usage: rm [-r] ") - return - - if len(args) == 2 and args[0] != "-r": - print("Usage: rm [-r] ") - return - - if len(args) == 1: - target_path = resolve_reference(self.pwd, args[0]) - else: - target_path = resolve_reference(self.pwd, args[1]) - - if exists(self.fs, target_path) is False: - print(f"File '{target_path}' doesn't exist") - return - - if target_path == "/": - print("rm: cannot delete root directory '/'") - return - - target_object = getpath(self.fs, target_path) - - if target_object[A_TYPE] == T_DIR and args[0] != "-r": - print(f"rm: cannot remove '{target_path}': Is a directory") - return - - parent_path = "/".join(target_path.split("/")[:-1]) - parent_object = getpath(self.fs, parent_path) +from os import path - parent_object[A_CONTENTS].remove(target_object) - - self.save_pickle() - - print("Deleted %s" % target_path) - - def do_rmdir(self, arguments): - """ - Remove a file object. Like the unix command, - this can only delete empty directories. - Use rm -r to recursively delete full directories.\n - Usage: rmdir - """ - args = arguments.split() - - if len(args) != 1: - print("Usage: rmdir ") - return - - target_path = resolve_reference(self.pwd, args[0]) - - if exists(self.fs, target_path) is False: - print(f"File '{target_path}' doesn't exist") - return - - target_object = getpath(self.fs, target_path) - - if target_object[A_TYPE] != T_DIR: - print(f"rmdir: failed to remove '{target_path}': Not a directory") - return - - # The unix rmdir command does not delete directories if they are not - # empty - if len(target_object[A_CONTENTS]) != 0: - print(f"rmdir: failed to remove '{target_path}': Directory not empty") - return - - parent_path = "/".join(target_path.split("/")[:-1]) - parent_object = getpath(self.fs, parent_path) - - parent_object[A_CONTENTS].remove(target_object) - - self.save_pickle() - - if self.pwd == target_path: - self.do_cd("..") - - print("Deleted %s" % target_path) - - def do_mv(self, arguments): - """ - Moves a file/directory from one directory to another.\n - Usage: mv - """ - args = arguments.split() - if len(args) != 2: - print("Usage: mv ") - return - src = resolve_reference(self.pwd, args[0]) - dst = resolve_reference(self.pwd, args[1]) - - if src == "/": - print("mv: cannot move the root directory '/'") - return - - src = src.strip("/") - dst = dst.strip("/") - - if not exists(self.fs, src): - print("Source file '%s' does not exist!" % src) - return - - # Get the parent directory of the source file - # srcparent = '/'.join(src.split('/')[:-1]) - srcparent = "/".join(src.split("/")[:-1]) - - # Get the object for source - srcl = getpath(self.fs, src) - - # Get the object for the source's parent - srcparentl = getpath(self.fs, srcparent) - - # if the specified filepath is a directory, maintain the current name - if exists(self.fs, dst) and is_directory(self.fs, dst): - dstparent = dst - dstname = srcl[A_NAME] - else: - dstparent = "/".join(dst.split("/")[:-1]) - dstname = dst.split("/")[-1] - - if exists(self.fs, dstparent + "/" + dstname): - print("A file already exists at " + dst + "!") - return - - if not exists(self.fs, dstparent): - print("Destination directory '%s' doesn't exist!" % dst) - return - - if src == self.pwd: - self.do_cd("..") - - dstparentl = getpath(self.fs, dstparent) - copy = srcl[:] - copy[A_NAME] = dstname - dstparentl[A_CONTENTS].append(copy) - srcparentl[A_CONTENTS].remove(srcl) - - self.save_pickle() - - print(f"File moved from /{src} to /{dst}") - - def do_cp(self, arguments): - """ - Copies a file/directory from one directory to another.\n - Usage: cp - """ - args = arguments.split() - if len(args) != 2: - print("Usage: cp ") - return - - # src, dst = args[0], args[1] - - src = resolve_reference(self.pwd, args[0]) - dst = resolve_reference(self.pwd, args[1]) - - src = src.strip("/") - dst = dst.strip("/") - - if not exists(self.fs, src): - print(f"Source file '{src}' does not exist!") - return - - # Get the parent directory of the source file - # srcparent = "/".join(src.split("/")[:-1]) - - # Get the object for source - srcl = getpath(self.fs, src) - - # Get the object for the source's parent - # srcparentl = getpath(self.fs, srcparent) - - # if the specified filepath is a directory, maintain the current name - if exists(self.fs, dst) and is_directory(self.fs, dst): - dstparent = dst - dstname = srcl[A_NAME] - else: - dstparent = "/".join(dst.split("/")[:-1]) - dstname = dst.split("/")[-1] - - if exists(self.fs, dstparent + "/" + dstname): - print(f"A file already exists at {dstparent}/{dstname}!") - return - - if not exists(self.fs, dstparent): - print(f"Destination directory {dstparent} doesn't exist!") - return - - dstparentl = getpath(self.fs, dstparent) - coppy = copy.deepcopy(srcl) - coppy[A_NAME] = dstname - dstparentl[A_CONTENTS].append(coppy) - - self.save_pickle() - - print(f"File copied from /{src} to /{dstparent}/{dstname}") - - def do_chown(self, args): - """ - Change file ownership - """ - arg_list = args.split() - - if len(arg_list) != 2: - print("Incorrect number of arguments.\nUsage: chown ") - return - - uid = arg_list[0] - target_path = resolve_reference(self.pwd, arg_list[1]) - - if not exists(self.fs, target_path): - print("File '%s' doesn't exist." % target_path) - return - - target_object = getpath(self.fs, target_path) - olduid = target_object[A_UID] - target_object[A_UID] = int(uid) - print("former UID: " + str(olduid) + ". New UID: " + str(uid)) - self.save_pickle() - - def do_chgrp(self, args): - """ - Change file ownership - """ - arg_list = args.split() - - if len(arg_list) != 2: - print("Incorrect number of arguments.\nUsage: chgrp ") - return - - gid = arg_list[0] - target_path = resolve_reference(self.pwd, arg_list[1]) - - if not exists(self.fs, target_path): - print("File '%s' doesn't exist." % target_path) - return - - target_object = getpath(self.fs, target_path) - oldgid = target_object[A_GID] - target_object[A_GID] = int(gid) - print("former GID: " + str(oldgid) + ". New GID: " + str(gid)) - self.save_pickle() - - def do_chmod(self, args): - """ - Change file permissions - only modes between 000 and 777 are implemented - """ - - arg_list = args.split() - - if len(arg_list) != 2: - print("Incorrect number of arguments.\nUsage: chmod ") - return - - mode = arg_list[0] - target_path = resolve_reference(self.pwd, arg_list[1]) - - if not exists(self.fs, target_path): - print("File '%s' doesn't exist." % target_path) - return - - target_object = getpath(self.fs, target_path) - oldmode = target_object[A_MODE] - - if target_object[A_TYPE] == T_LINK: - print(target_path + " is a link, nothing changed.") - return - - try: - num = int(mode, 8) - except Exception: - print("Incorrect mode: " + mode) - return - - if num < 0 or num > 511: - print("Incorrect mode: " + mode) - return - - target_object[A_MODE] = (oldmode & 0o7777000) | (num & 0o777) - self.save_pickle() - - def do_file(self, args): - """ - Identifies file types.\nUsage: file - """ - arg_list = args.split() - - if len(arg_list) != 1: - print("Incorrect number of arguments.\nUsage: file ") - return - - target_path = resolve_reference(self.pwd, arg_list[0]) - - if not exists(self.fs, target_path): - print("File '%s' doesn't exist." % target_path) - return - - target_object = getpath(self.fs, target_path) - - file_type = target_object[A_TYPE] - - if file_type == T_FILE: - msg = "normal file object" - elif file_type == T_DIR: - msg = "directory" - elif file_type == T_LINK: - msg = "link" - elif file_type == T_BLK: - msg = "block file" - elif file_type == T_CHR: - msg = "character special" - elif file_type == T_SOCK: - msg = "socket" - elif file_type == T_FIFO: - msg = "named pipe" - else: - msg = "unrecognized file" - - print(target_path + " is a " + msg) - - def do_clear(self, args): - """ - Clears the screen - """ - os.system("clear") - - def emptyline(self): - """ - By default the cmd object will repeat the last command - if a blank line is entered. Since this is different than - bash behavior, overriding this method will stop it. - """ - pass - - def help_help(self): - print("Type help to get more information.") - - def help_about(self): - print( - "Kippo/Cowrie stores information about its file systems in a " - + "series of nested lists. Once the lists are made, they are " - + "stored in a pickle file on the hard drive. Every time cowrie " - + "gets a new client, it reads from the pickle file and loads " - + "the fake file system into memory. By default this file " - + "is /opt/cowrie/data/fs.pickle. Originally the script " - + "/opt/cowrie/bin/createfs was used to copy the file system " - + "of the existing computer. However, it quite difficult to " - + "edit the pickle file by hand.\n\nThis script strives to be " - + "a bash-like interface that allows users to modify " - + "existing fs pickle files. It supports many of the " - + "common bash commands and even handles relative file " - + "paths. Keep in mind that you need to restart the " - + "cowrie process in order for the new file system to be " - + "reloaded into memory.\n\nDonovan Hubbard, Douglas Hubbard, " - + "March 2013\nVersion 1.0" - ) +cowriepath = path.dirname(sys.argv[0]) + "/../src" +sys.path.append(cowriepath) +from cowrie.scripts import fsctl # noqa: E402 if __name__ == "__main__": - if len(sys.argv) != 2: - print( - "Usage: %s " - % os.path.basename( - sys.argv[0], - ) - ) - sys.exit(1) - - pickle_file_name = sys.argv[1].strip() - print(pickle_file_name) - - fseditCmd(pickle_file_name).cmdloop() + fsctl.run() diff --git a/bin/playlog b/bin/playlog index d8eeeaad6c..39bdec1de6 100755 --- a/bin/playlog +++ b/bin/playlog @@ -1,139 +1,12 @@ #!/usr/bin/env python -# -# Copyright (C) 2003-2011 Upi Tamminen -# -import getopt -import os -import struct import sys -import time +from os import path -OP_OPEN, OP_CLOSE, OP_WRITE, OP_EXEC = 1, 2, 3, 4 -TYPE_INPUT, TYPE_OUTPUT, TYPE_INTERACT = 1, 2, 3 - - -def playlog(fd, settings): - ssize = struct.calcsize(" settings["maxdelay"]: - sleeptime = settings["maxdelay"] - if settings["maxdelay"] > 0: - time.sleep(sleeptime) - prevtime = curtime - if settings["colorify"] and color: - stdout.write(color) - stdout.write(data) - if settings["colorify"] and color: - stdout.write(b"\033[0m") - color = None - sys.stdout.flush() - elif str(tty) == str(currtty) and op == OP_CLOSE: - break - - -def help(brief=0): - print( - "Usage: %s [-bfhi] [-m secs] [-w file] ...\n" - % os.path.basename(sys.argv[0]) - ) - - if not brief: - print(" -f keep trying to read the log until it's closed") - print( - " -m maximum delay in seconds, to avoid" - + " boredom or fast-forward\n" - + " to the end. (default is 3.0)" - ) - print(" -i show the input stream instead of output") - print(" -b show both input and output streams") - print( - " -c colorify the output stream based on what streams are being received" - ) - print(" -h display this help\n") - - sys.exit(1) +cowriepath = path.dirname(sys.argv[0]) + "/../src" +sys.path.append(cowriepath) +from cowrie.scripts import playlog # noqa: E402 if __name__ == "__main__": - - settings = { - "tail": 0, - "maxdelay": 3.0, - "input_only": 0, - "both_dirs": 0, - "colorify": 0, - } - - try: - optlist, args = getopt.getopt(sys.argv[1:], "fhibcm:w:", ["help"]) - except getopt.GetoptError as error: - print("Error: %s\n" % error) - help() - - options = [x[0] for x in optlist] - if "-b" in options and "-i" in options: - print("Error: -i and -b cannot be used together. Please select only one flag") - sys.exit(1) - - for o, a in optlist: - if o == "-f": - settings["tail"] = 1 - elif o == "-m": - settings["maxdelay"] = float(a) # takes decimals - elif o == "-i": - settings["input_only"] = 1 - elif o == "-b": - settings["both_dirs"] = 1 - elif o in ["-h", "--help"]: - help() - elif o == "-c": - settings["colorify"] = 1 - - if len(args) < 1: - help() - - try: - for logfile in args: - logfd = open(logfile, "rb") - playlog(logfd, settings) - except OSError: - print("\n\n[!] Couldn't open log file (%s)!" % logfile) - sys.exit(2) + playlog.run() diff --git a/docker/Dockerfile b/docker/Dockerfile index dbd606bc0c..c94a913692 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -5,7 +5,7 @@ ARG ARCH= ARG BUILD_DATE ARG TAG -FROM ${ARCH}debian:bullseye-slim as builder +FROM ${ARCH}debian:bookworm-slim as builder WORKDIR / @@ -26,8 +26,8 @@ RUN groupadd -r ${COWRIE_GROUP} && \ # Set up Debian prereqs RUN export DEBIAN_FRONTEND=noninteractive; \ - apt-get update && \ - apt-get install -y \ + apt-get -q update && \ + apt-get -q install -y \ -o APT::Install-Suggests=false \ -o APT::Install-Recommends=false \ python3-pip \ @@ -39,7 +39,6 @@ RUN export DEBIAN_FRONTEND=noninteractive; \ python3 \ rustc \ cargo \ - git \ build-essential \ python3-virtualenv \ libsnappy-dev && \ @@ -62,8 +61,8 @@ RUN python3 -m venv cowrie-env && \ COPY --chown=${COWRIE_USER}:${COWRIE_GROUP} . ${COWRIE_HOME}/cowrie-git -FROM gcr.io/distroless/python3-debian11 AS runtime -#FROM gcr.io/distroless/python3-debian11:debug AS runtime +FROM gcr.io/distroless/python3-debian12 AS runtime +#FROM gcr.io/distroless/python3-debian12:debug AS runtime LABEL org.opencontainers.image.created=${BUILD_DATE} LABEL org.opencontainers.image.authors="Michel Oosterhof " @@ -78,7 +77,7 @@ LABEL org.opencontainers.image.ref.name=${TAG} LABEL org.opencontainers.image.title="Cowrie SSH/Telnet Honeypot" LABEL org.opencontainers.image.description="Cowrie SSH/Telnet Honeypot" #LABEL org.opencontainers.image.base.digest="7beb0248fd81" -LABEL org.opencontainers.image.base.name="gcr.io/distroless/python3-debian11" +LABEL org.opencontainers.image.base.name="gcr.io/distroless/python3-debian12" ENV COWRIE_GROUP=cowrie \ COWRIE_USER=cowrie \ @@ -105,7 +104,7 @@ COPY --from=builder --chown=0:0 /etc/group /etc/group COPY --from=builder --chown=${COWRIE_USER}:${COWRIE_GROUP} ${COWRIE_HOME} ${COWRIE_HOME} -RUN python3 -m compileall ${COWRIE_HOME} /usr/lib/python3.9 +RUN [ "python3", "-m", "compileall", "${COWRIE_HOME}", "/usr/lib/python3.11" ] VOLUME [ "/cowrie/cowrie-git/var", "/cowrie/cowrie-git/etc" ] diff --git a/docs/conf.py b/docs/conf.py index 3ae9aeab43..121c7176d6 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -17,7 +17,7 @@ # -- Project information ----------------------------------------------------- project = "cowrie" -copyright = "2014-2022, Michel Oosterhof" +copyright = "2014-2023, Michel Oosterhof" author = "Michel Oosterhof" # The version info for the project you're documenting, acts as replacement for diff --git a/docs/elk/logstash-cowrie.conf b/docs/elk/logstash-cowrie.conf index b1e09ca2a3..c4ea955251 100644 --- a/docs/elk/logstash-cowrie.conf +++ b/docs/elk/logstash-cowrie.conf @@ -17,6 +17,7 @@ filter { if [type] == "cowrie" { json { source => message + target => honeypot } date { diff --git a/docs/requirements.txt b/docs/requirements.txt index d844ef3618..270ab5108b 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,4 +1,6 @@ -readthedocs-sphinx-search==0.3.1 -Sphinx==5.1.1 -sphinx-copybutton==0.5.0 -sphinx_rtd_theme==1.0.0 +readthedocs-sphinx-search==0.3.2 +setuptools==69.2.0 +Sphinx==7.2.6 +sphinx-copybutton==0.5.2 +sphinx_rtd_theme==2.0.0 +twisted==24.3.0 diff --git a/etc/cowrie.cfg.dist b/etc/cowrie.cfg.dist index c609d192a6..3f46f67192 100644 --- a/etc/cowrie.cfg.dist +++ b/etc/cowrie.cfg.dist @@ -209,6 +209,11 @@ config_files_path = ${honeypot:share_path}/pool_configs network_config = default_network.xml nw_filter_config = default_filter.xml +# libvirt URI, common settings are qemu:///system or qemu:///session +libvirt_uri = qemu:///system +# Use this syntax to directly connect to the UNIX socket +# libvirt_uri = qemu+unix:///session?socket=/home/cowrie/.cache/libvirt/libvirt-sock + # ===================================== # Guest details (for a generic x86-64 guest, like Ubuntu) # @@ -1061,3 +1066,22 @@ api_key = abcdef1234567890fedcba0987654321 ddsource = cowrie ddtags = env:dev service = honeypot + +# Oracle Cloud custom logs output module +# sends JSON directly to Oracle Cloud custom logs +# mandatory field: authtype, log_ocid +# optional fields (to be set if user_principals is selected as authtype): user_ocid, fingerprint, tenancy_ocid, region, keyfile +# For more information on Oracle Cloud custom logs: https://docs.oracle.com/en-us/iaas/Content/Logging/Concepts/custom_logs.htm +# For more information on Oracle Cloud user principal authentication method: https://docs.oracle.com/en-us/iaas/Content/API/Concepts/apisigningkey.htm#five +# For more information on Oracle Cloud instance principal authentication method: https://blogs.oracle.com/developers/post/accessing-the-oracle-cloud-infrastructure-api-using-instance-principals +[output_oraclecloud] +enabled = false +# authtype must be set either to user_principals or to instance_principals +authtype = instance_principals +# following parameters must be set in case user_principals is used. keyfile is the absolute path to your API pem key file. +#log_ocid = ocid1.log.oc1.eu-stockholm-1.xxx +#user_ocid = ocid1.user.oc1..xxx +#fingerprint = 77:9c:4xxxxx +#tenancy_ocid = ocid1.tenancy.oc1..xxx +#region = eu-stockholm-1 +#keyfile = /home/xx/key.pem diff --git a/pyproject.toml b/pyproject.toml index 4c9eb4a5b4..214ae01510 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,11 +43,10 @@ homepage = "https://www.cowrie.org/" repository = "https://github.com/cowrie/cowrie" [project.scripts] -fsctl = "bin/fsctl" -asciinema = "bin/asciinema:main" -cowrie = "bin/cowrie:main" -creatfs = "bin/createfs:main" -playlog = "bin/playlog:main" +fsctl = "cowrie.scripts.fsctl:run" +asciinema = "cowrie.scripts.asciinema:run" +creatfs = "cowrie.scripts.createfs:run" +playlog = "cowrie.scripts.playlog:run" [project.optional-dependencies] csirtg = ["csirtgsdk==1.1.5"] @@ -91,7 +90,7 @@ disallow_subclassing_any = true warn_unused_ignores = true # Getting these passing should be easy -strict_concatenate = true +# strict_concatenate = true # These are too strict for us at the moment @@ -105,23 +104,45 @@ disallow_untyped_calls = false [tool.pylint."MESSAGES CONTROL"] -disable = ["R0901", "R0902", "R0903", "R0904", "R0912", "R0913", "R0914", "R0915", "C0103", "C0114", "C0115", "C0116", "C0301"] +disable = ["R0901", "R0902", "R0903", "R0904", "R0912", "R0913", "R0914", "R0915", "C0103", "C0114", "C0115", "C0116", "C0301", "W0201"] +ignored-classes = ["twisted.internet.reactor"] [tool.pyright] - +include = ["src"] +typeCheckingMode = "strict" +reportArgumentType = "none" +reportAssignmentType = "none" +reportAttributeAccessIssue = "none" +reportCallIssue = "information" +reportGeneralTypeIssues = "information" +reportIncompatibleMethodOverride = "information" +reportIncompatibleVariableOverride = "none" +reportMissingImports = "none" +reportMissingModuleSource = "none" +reportOperatorIssue = "information" +reportOptionalCall = "information" +reportOptionalMemberAccess = "none" +reportOptionalOperand = "information" +reportPossiblyUnboundVariable = "information" +reportUnsupportedDunderAll = "information" [tool.ruff] line-length = 88 # Enable Pyflakes `E` and `F` codes by default. -select = ["E", "F", "UP", "YTT", "B", "T20", "Q", "RUF"] -ignore = ["E501", "UP007", "B019", "RUF001"] +lint.select = ["E", "F", "UP", "YTT", "B", "T20", "Q", "RUF"] +lint.ignore = ["E501", "UP007", "B019", "RUF001", "UP038"] # Assume Python 3.10. target-version = "py310" +# Ignore `T201` (print) in all scripts +[tool.ruff.lint.per-file-ignores] +"src/cowrie/scripts/*" = ["T201"] + + [tool.setuptools] package-dir = {"" = "src"} diff --git a/requirements-dev.txt b/requirements-dev.txt index c34894fa82..0ff4ed3e90 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,25 +1,24 @@ -Sphinx==6.2.1 -build==0.10.0 -coverage==7.2.7 +build==1.1.1 +coverage==7.4.4 mypy-extensions==1.0.0; platform_python_implementation=='CPython' and python_version>'3.8' -mypy-zope==1.0.0; platform_python_implementation=='CPython' and python_version>'3.8' -mypy==1.4.1; platform_python_implementation=='CPython' and python_version>'3.8' -pathspec==0.11.1 -pipdeptree==2.9.0 -pre-commit==3.3.2 -pylint==2.17.4 -pyre-check==0.9.18; python_version>'3.8' -pyright==1.1.313 -pytype==2023.6.2; platform_python_implementation=='CPython' and python_version<'3.11' -pyupgrade==3.10.1 +mypy-zope==1.0.4; platform_python_implementation=='CPython' and python_version>'3.8' +mypy==1.9.0; platform_python_implementation=='CPython' and python_version>'3.8' +pathspec==0.12.1 +pipdeptree==2.13.2 +pre-commit==3.5.0 +pylint==3.1.0 +pyre-check==0.9.19; python_version>'3.8' +pyright==1.1.353 +pytype==2024.1.24; platform_python_implementation=='CPython' and python_version<'3.11' +pyupgrade==3.15.0 pyyaml==6.0.1 -readthedocs-sphinx-search==0.3.1 -ruff==0.0.272 -setuptools==67.8.0 +readthedocs-sphinx-search==0.3.2 +ruff==0.2.2 +setuptools==69.2.0 sphinx-copybutton==0.5.2 -sphinx_rtd_theme==1.2.2 -tox==4.6.0 -types-python-dateutil==2.8.19.13; python_version>'3.8' -types-redis==4.6.0.3; python_version>'3.8' -types-requests==2.31.0.1; python_version>'3.8' -yamllint==1.32.0 +sphinx_rtd_theme==2.0.0 +tox==4.14.1 +types-python-dateutil==2.8.19.20240106; python_version>'3.8' +types-redis==4.6.0.20240311; python_version>'3.8' +types-requests==2.31.0.20240311; python_version>'3.8' +yamllint==1.35.1 diff --git a/requirements-output.txt b/requirements-output.txt index 2b4032cc69..9c4ad80ff9 100644 --- a/requirements-output.txt +++ b/requirements-output.txt @@ -6,22 +6,22 @@ geoip2 requests==2.31.0 # elasticsearch -elasticsearch==8.9.0 +elasticsearch==8.12.1 # hpfeeds -hpfeeds3==0.9.10 +hpfeeds==3.1.0 # mysql -mysql-connector-python==8.1.0 +mysql-connector-python==8.3.0 # mongodb -pymongo==4.4.1 +pymongo==4.6.2 # rethinkdblog -rethinkdb==2.4.9 +rethinkdb==2.4.10.post1 # s3 -botocore==1.29.151 +botocore==1.34.54 # slack slackclient==2.9.4 @@ -33,11 +33,13 @@ influxdb==5.3.1 wokkel==18.0.0 # misp -pymisp==2.4.174 +pymisp==2.4.187 # redis -redis==4.5.5 +redis==5.0.1 +# Oracle Cloud +oci==2.122.0 # nsq -gnsq==1.0.2 \ No newline at end of file +gnsq==1.0.2 diff --git a/requirements.txt b/requirements.txt index 7b0f27a579..9926210178 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,13 +1,13 @@ appdirs==1.4.4 -attrs==23.1.0 -bcrypt==4.0.1 -configparser==6.0.0 -cryptography==41.0.6 -packaging==23.1 +attrs==23.2.0 +bcrypt==4.1.2 +configparser==6.0.1 +cryptography==42.0.5 +packaging==24.0 pyasn1_modules==0.3.0 -pyparsing==3.1.0 +pyparsing==3.1.1 python-dateutil==2.8.2 -service_identity==23.1.0 +service_identity==24.1.0 tftpy==0.8.2 -treq==22.2.0 +treq==23.11.0 twisted==23.10.0 diff --git a/setup.py b/setup.py index 8634a434bd..5ad5c41ce2 100755 --- a/setup.py +++ b/setup.py @@ -2,12 +2,6 @@ from setuptools import setup -try: - import twisted -except ImportError: - raise SystemExit("twisted not found. Make sure you " - "have installed the Twisted core package.") - setup( packages=["cowrie", "twisted"], include_package_data=True, diff --git a/src/backend_pool/libvirt/backend_service.py b/src/backend_pool/libvirt/backend_service.py index ba6c8221d1..075d2b7122 100644 --- a/src/backend_pool/libvirt/backend_service.py +++ b/src/backend_pool/libvirt/backend_service.py @@ -1,3 +1,7 @@ +""" +backend service +""" + # Copyright (c) 2019 Guilherme Borges # See the COPYRIGHT file for more information @@ -7,15 +11,15 @@ import random import sys import uuid +from collections.abc import Callable from twisted.python import log +from cowrie.core.config import CowrieConfig + import backend_pool.libvirt.guest_handler import backend_pool.libvirt.network_handler import backend_pool.util -from cowrie.core.config import CowrieConfig - -LIBVIRT_URI = "qemu:///system" class LibvirtError(Exception): @@ -23,17 +27,22 @@ class LibvirtError(Exception): class LibvirtBackendService: - def __init__(self): - # lazy import to avoid exception if not using the backend_pool and libvirt not installed (#1185) + def __init__(self) -> None: + # lazy import to avoid exception if not using the backend_pool + # and libvirt not installed (#1185) import libvirt + libvirt_uri: str = CowrieConfig.get( + "backend_pool", "libvirt_uri", fallback="qemu:///system" + ) + # open connection to libvirt - self.conn = libvirt.open(LIBVIRT_URI) + self.conn = libvirt.open(libvirt_uri) if self.conn is None: log.msg( - eventid="cowrie.backend_pool.qemu", - format="Failed to open connection to %(uri)s", - uri=LIBVIRT_URI, + eventid="cowrie.backend_pool.libvirtd", + format="Failed to open connection to libvirtd at %(uri)s", + uri=libvirt_uri, ) raise LibvirtError() @@ -48,12 +57,15 @@ def __init__(self): self.network_table = backend_pool.util.generate_network_table(seed) log.msg( - eventid="cowrie.backend_pool.qemu", format="Connection to QEMU established" + eventid="cowrie.backend_pool.libvirtd", + format="Connection to libvirtd established at %(uri)s", + uri=libvirt_uri, ) - def start_backend(self): + def start_backend(self) -> None: """ - Initialises QEMU/libvirt environment needed to run guests. Namely starts networks and network filters. + Initialises QEMU/libvirt environment needed to run guests. + Namely starts networks and network filters. """ # create a network filter self.filter = backend_pool.libvirt.network_handler.create_filter(self.conn) @@ -66,24 +78,25 @@ def start_backend(self): # service is ready to be used (create guests and use them) self.ready = True - def stop_backend(self): + def stop_backend(self) -> None: log.msg( - eventid="cowrie.backend_pool.qemu", format="Doing QEMU clean shutdown..." + eventid="cowrie.backend_pool.libvirtd", + format="Doing libvirtd clean shutdown...", ) self.ready = False self.destroy_all_cowrie() - def shutdown_backend(self): + def shutdown_backend(self) -> None: self.conn.close() # close libvirt connection log.msg( - eventid="cowrie.backend_pool.qemu", - format="Connection to QEMU closed successfully", + eventid="cowrie.backend_pool.libvirtd", + format="Connection to libvirtd closed successfully", ) - def get_mac_ip(self, ip_tester): + def get_mac_ip(self, ip_tester: Callable[[str], bool]) -> tuple[str, str]: """ Get a MAC and IP that are not being used by any guest. """ @@ -115,7 +128,9 @@ def create_guest(self, ip_tester): self.conn, guest_mac, guest_unique_id ) if dom is None: - log.msg(eventid="cowrie.backend_pool.qemu", format="Failed to create guest") + log.msg( + eventid="cowrie.backend_pool.libvirtd", format="Failed to create guest" + ) return None return dom, snapshot, guest_ip @@ -125,7 +140,7 @@ def destroy_guest(self, domain, snapshot): return try: - # destroy the domain in qemu + # destroy the domain in QEMU domain.destroy() # we want to remove the snapshot if either: @@ -145,16 +160,17 @@ def destroy_guest(self, domain, snapshot): os.remove(snapshot) # destroy its disk snapshot except Exception as error: log.err( - eventid="cowrie.backend_pool.qemu", + eventid="cowrie.backend_pool.libvirtd", format="Error destroying guest: %(error)s", error=error, ) - def __destroy_all_guests(self): + def __destroy_all_guests(self) -> None: domains = self.conn.listDomainsID() if not domains: log.msg( - eventid="cowrie.backend_pool.qemu", format="Could not get domain list" + eventid="cowrie.backend_pool.libvirtd", + format="Could not get domain list", ) for domain_id in domains: @@ -165,11 +181,12 @@ def __destroy_all_guests(self): except KeyboardInterrupt: pass - def __destroy_all_networks(self): + def __destroy_all_networks(self) -> None: networks = self.conn.listNetworks() if not networks: log.msg( - eventid="cowrie.backend_pool.qemu", format="Could not get network list" + eventid="cowrie.backend_pool.libvirtd", + format="Could not get network list", ) for network in networks: @@ -177,11 +194,11 @@ def __destroy_all_networks(self): n = self.conn.networkLookupByName(network) n.destroy() - def __destroy_all_network_filters(self): + def __destroy_all_network_filters(self) -> None: network_filters = self.conn.listNWFilters() if not network_filters: log.msg( - eventid="cowrie.backend_pool.qemu", + eventid="cowrie.backend_pool.libvirtd", format="Could not get network filters list", ) @@ -190,7 +207,7 @@ def __destroy_all_network_filters(self): n = self.conn.nwfilterLookupByName(nw_filter) n.undefine() - def destroy_all_cowrie(self): + def destroy_all_cowrie(self) -> None: self.__destroy_all_guests() self.__destroy_all_networks() self.__destroy_all_network_filters() diff --git a/src/backend_pool/libvirt/guest_handler.py b/src/backend_pool/libvirt/guest_handler.py index f60e4a8587..8169d8a8ab 100644 --- a/src/backend_pool/libvirt/guest_handler.py +++ b/src/backend_pool/libvirt/guest_handler.py @@ -7,9 +7,10 @@ from twisted.python import log +from cowrie.core.config import CowrieConfig + import backend_pool.libvirt.snapshot_handler import backend_pool.util -from cowrie.core.config import CowrieConfig class QemuGuestError(Exception): diff --git a/src/backend_pool/libvirt/network_handler.py b/src/backend_pool/libvirt/network_handler.py index 30bc77fcd5..941205842b 100644 --- a/src/backend_pool/libvirt/network_handler.py +++ b/src/backend_pool/libvirt/network_handler.py @@ -6,9 +6,10 @@ from twisted.python import log -import backend_pool.util from cowrie.core.config import CowrieConfig +import backend_pool.util + def create_filter(connection): # lazy import to avoid exception if not using the backend_pool and libvirt not installed (#1185) diff --git a/src/backend_pool/libvirt/snapshot_handler.py b/src/backend_pool/libvirt/snapshot_handler.py index 54a08269a7..be74740e8a 100644 --- a/src/backend_pool/libvirt/snapshot_handler.py +++ b/src/backend_pool/libvirt/snapshot_handler.py @@ -8,7 +8,7 @@ import subprocess -def create_disk_snapshot(source_img, destination_img): +def create_disk_snapshot(source_img: str, destination_img: str) -> bool: try: shutil.chown(source_img, getpass.getuser()) except PermissionError: diff --git a/src/backend_pool/nat.py b/src/backend_pool/nat.py index c019cd9160..2ac8cb01e6 100644 --- a/src/backend_pool/nat.py +++ b/src/backend_pool/nat.py @@ -1,23 +1,31 @@ from __future__ import annotations from threading import Lock +from typing import Any +from twisted.internet.interfaces import IAddress from twisted.internet import protocol +from twisted.internet.protocol import connectionDone from twisted.internet import reactor +from twisted.python import failure class ClientProtocol(protocol.Protocol): + server_protocol: ServerProtocol + def dataReceived(self, data: bytes) -> None: - self.server_protocol.transport.write(data) # type: ignore + assert self.server_protocol.transport is not None + self.server_protocol.transport.write(data) - def connectionLost(self, reason): + def connectionLost(self, reason: failure.Failure = connectionDone) -> None: + assert self.server_protocol.transport is not None self.server_protocol.transport.loseConnection() class ClientFactory(protocol.ClientFactory): - def __init__(self, server_protocol): + def __init__(self, server_protocol: ServerProtocol) -> None: self.server_protocol = server_protocol - def buildProtocol(self, addr): + def buildProtocol(self, addr: IAddress) -> ClientProtocol: client_protocol = ClientProtocol() client_protocol.server_protocol = self.server_protocol self.server_protocol.client_protocol = client_protocol @@ -25,31 +33,32 @@ def buildProtocol(self, addr): class ServerProtocol(protocol.Protocol): - def __init__(self, dst_ip, dst_port): - self.dst_ip = dst_ip - self.dst_port = dst_port - self.client_protocol = None - self.buffer = [] + def __init__(self, dst_ip: str, dst_port: int): + self.dst_ip: str = dst_ip + self.dst_port: int = dst_port + self.client_protocol: ClientProtocol + self.buffer: list[bytes] = [] def connectionMade(self): reactor.connectTCP(self.dst_ip, self.dst_port, ClientFactory(self)) - def dataReceived(self, data): + def dataReceived(self, data: bytes) -> None: self.buffer.append(data) self.sendData() - def sendData(self): + def sendData(self) -> None: if not self.client_protocol: - reactor.callLater(0.5, self.sendData) + reactor.callLater(0.5, self.sendData) # type: ignore[attr-defined] return + assert self.client_protocol.transport is not None for packet in self.buffer: self.client_protocol.transport.write(packet) self.buffer = [] - def connectionLost(self, reason): - if self.client_protocol: - self.client_protocol.transport.loseConnection() + def connectionLost(self, reason: failure.Failure = connectionDone) -> None: + assert self.client_protocol.transport is not None + self.client_protocol.transport.loseConnection() class ServerFactory(protocol.Factory): @@ -57,27 +66,30 @@ def __init__(self, dst_ip: str, dst_port: int) -> None: self.dst_ip: str = dst_ip self.dst_port: int = dst_port - def buildProtocol(self, addr): + def buildProtocol(self, addr: IAddress) -> ServerProtocol: return ServerProtocol(self.dst_ip, self.dst_port) class NATService: """ - This service provides a NAT-like service when the backend pool is located in a remote machine. - Guests are bound to a local IP (e.g., 192.168.150.0/24), and so not accessible from a remote Cowrie. - This class provides TCP proxies that associate accessible IPs in the backend pool's machine to the internal - IPs used by guests, like a NAT. + This service provides a NAT-like service when the backend pool + is located in a remote machine. Guests are bound to a local + IP (e.g., 192.168.150.0/24), and so not accessible from a remote + Cowrie. This class provides TCP proxies that associate accessible + IPs in the backend pool's machine to the internal IPs used by + guests, like a NAT. """ def __init__(self): - self.bindings = {} + self.bindings: dict[int, Any] = {} self.lock = ( Lock() ) # we need to be thread-safe just in case, this is accessed from multiple clients - def request_binding(self, guest_id, dst_ip, ssh_port, telnet_port): - self.lock.acquire() - try: + def request_binding( + self, guest_id: int, dst_ip: str, ssh_port: int, telnet_port: int + ) -> tuple[int, int]: + with self.lock: # see if binding is already created if guest_id in self.bindings: # increase connected @@ -88,21 +100,18 @@ def request_binding(self, guest_id, dst_ip, ssh_port, telnet_port): self.bindings[guest_id][2]._realPortNumber, ) else: - nat_ssh = reactor.listenTCP( + nat_ssh = reactor.listenTCP( # type: ignore[attr-defined] 0, ServerFactory(dst_ip, ssh_port), interface="0.0.0.0" ) - nat_telnet = reactor.listenTCP( + nat_telnet = reactor.listenTCP( # type: ignore[attr-defined] 0, ServerFactory(dst_ip, telnet_port), interface="0.0.0.0" ) self.bindings[guest_id] = [1, nat_ssh, nat_telnet] return nat_ssh._realPortNumber, nat_telnet._realPortNumber - finally: - self.lock.release() - def free_binding(self, guest_id): - self.lock.acquire() - try: + def free_binding(self, guest_id: int) -> None: + with self.lock: self.bindings[guest_id][0] -= 1 # stop listening if no one is connected @@ -110,15 +119,9 @@ def free_binding(self, guest_id): self.bindings[guest_id][1].stopListening() self.bindings[guest_id][2].stopListening() del self.bindings[guest_id] - finally: - self.lock.release() def free_all(self): - self.lock.acquire() - try: - + with self.lock: for guest_id in self.bindings: self.bindings[guest_id][1].stopListening() self.bindings[guest_id][2].stopListening() - finally: - self.lock.release() diff --git a/src/backend_pool/pool_server.py b/src/backend_pool/pool_server.py index 02d7201d76..6652520879 100644 --- a/src/backend_pool/pool_server.py +++ b/src/backend_pool/pool_server.py @@ -1,3 +1,10 @@ +""" +The main interface of the backend pool is exposed as a TCP server +in _pool_server.py_. The protocol is a very simple wire protocol, +always composed of an op-code, a status code (for responses), and +any needed data thereafter. +""" + # Copyright (c) 2019 Guilherme Borges # See the COPYRIGHT file for more information @@ -5,15 +12,27 @@ import struct +from twisted.internet.address import IPv4Address, IPv6Address +from twisted.internet.interfaces import IAddress from twisted.internet.protocol import Factory, Protocol from twisted.python import log +from cowrie.core.config import CowrieConfig + from backend_pool.nat import NATService from backend_pool.pool_service import NoAvailableVMs, PoolService -from cowrie.core.config import CowrieConfig + +RES_OP_I = b"i" +RES_OP_R = b"r" +RES_OP_F = b"f" +RES_OP_U = b"u" class PoolServer(Protocol): + """ + Main PoolServer + """ + def __init__(self, factory: PoolServerFactory) -> None: self.factory: PoolServerFactory = factory self.local_pool: bool = ( @@ -25,7 +44,6 @@ def __init__(self, factory: PoolServerFactory) -> None: self.use_nat: bool = CowrieConfig.getboolean( "backend_pool", "use_nat", fallback=True ) - if self.use_nat: self.nat_public_ip: str = CowrieConfig.get("backend_pool", "nat_public_ip") @@ -35,7 +53,7 @@ def dataReceived(self, data: bytes) -> None: ] # yes, this needs to be done to extract the op code correctly response: bytes = b"" - if res_op == b"i": + if res_op == RES_OP_I: recv = struct.unpack("!II?", data[1:]) # set the pool service thread configs @@ -48,9 +66,9 @@ def dataReceived(self, data: bytes) -> None: # respond with ok self.factory.initialised = True - response = struct.pack("!cI", b"i", 0) + response = struct.pack("!cI", RES_OP_I, 0) - elif res_op == b"r": + elif res_op == RES_OP_R: # receives: attacker ip (used to serve same VM to same attacker) # sends: status code, guest_id, guest_ip, guest's ssh and telnet port @@ -92,12 +110,10 @@ def dataReceived(self, data: bytes) -> None: guest_id, guest_ip, ssh_port, telnet_port ) - fmt = "!cIIH{}sHHH{}s".format( - len(self.nat_public_ip), len(guest_snapshot) - ) + fmt = f"!cIIH{len(self.nat_public_ip)}sHHH{len(guest_snapshot)}s" response = struct.pack( fmt, - b"r", + RES_OP_R, 0, guest_id, len(self.nat_public_ip), @@ -111,7 +127,7 @@ def dataReceived(self, data: bytes) -> None: fmt = f"!cIIH{len(guest_ip)}sHHH{len(guest_snapshot)}s" response = struct.pack( fmt, - b"r", + RES_OP_R, 0, guest_id, len(guest_ip), @@ -126,9 +142,9 @@ def dataReceived(self, data: bytes) -> None: eventid="cowrie.backend_pool.server", format="No VM available, returning error code", ) - response = struct.pack("!cI", b"r", 1) + response = struct.pack("!cI", RES_OP_R, 1) - elif res_op == b"f": + elif res_op == RES_OP_F: # receives: guest_id recv = struct.unpack("!I", data[1:]) guest_id = recv[0] @@ -146,7 +162,7 @@ def dataReceived(self, data: bytes) -> None: # free the vm self.factory.pool_service.free_vm(guest_id) - elif res_op == b"u": + elif res_op == RES_OP_U: # receives: guest_id recv = struct.unpack("!I", data[1:]) guest_id = recv[0] @@ -169,6 +185,10 @@ def dataReceived(self, data: bytes) -> None: class PoolServerFactory(Factory): + """ + Factory for PoolServer + """ + def __init__(self) -> None: self.initialised: bool = False @@ -191,7 +211,8 @@ def stopFactory(self) -> None: if self.pool_service: self.pool_service.shutdown_pool() - def buildProtocol(self, addr): + def buildProtocol(self, addr: IAddress) -> PoolServer: + assert isinstance(addr, (IPv4Address, IPv6Address)) log.msg( eventid="cowrie.backend_pool.server", format="Received connection from %(host)s:%(port)s", diff --git a/src/backend_pool/pool_service.py b/src/backend_pool/pool_service.py index 0878f95ad4..0f46a7d3d7 100644 --- a/src/backend_pool/pool_service.py +++ b/src/backend_pool/pool_service.py @@ -1,22 +1,74 @@ +""" +The server interfaces exposes a producer-consumer infinite loop +that runs on _pool_service.py_. + +The **producer** is an infinite loop started by the server, and +runs every 5 seconds. It creates VMs up to the configured limit, +checks which VMs become available (by testing if they accept SSH +and/or Telnet connections), and destroys VMs that are no longer +needed. + +**Consumer** methods are called by server request, and basically +involve requesting and freeing VMs. All operations on shared data +in the producer-consumer are guarded by a lock, since there may be +concurrent requests. The lock protects the _guests_ list, which +contains references for each VM backend (in our case libvirt/QEMU +instances). """ + # Copyright (c) 2019 Guilherme Borges # See the COPYRIGHT file for more information + from __future__ import annotations +from dataclasses import dataclass import os import time from threading import Lock +from typing import Optional from twisted.internet import reactor from twisted.internet import threads from twisted.python import log +from cowrie.core.config import CowrieConfig + import backend_pool.libvirt.backend_service import backend_pool.util -from cowrie.core.config import CowrieConfig + + +POOL_STATE_CREATED = "created" +POOL_STATE_AVAILABLE = "available" +POOL_STATE_USING = "using" +POOL_STATE_USED = "used" +POOL_STATE_UNAVAILABLE = "unavailable" +POOL_STATE_DESTROYED = "destroyed" + + +@dataclass +class Guest: + """Class for keeping track of QEMU guests.""" + + id: int + client_ips: list[str] + connected: int + state: str + prev_state: str | None + start_timestamp: float + freed_timestamp: float + domain: str + guest_ip: str + name: str + snapshot: str class NoAvailableVMs(Exception): - pass + """ + no VM's available + """ + + +# pass +# class PoolService: @@ -31,8 +83,10 @@ class PoolService: unavailable: marked for destruction after timeout destroyed: deleted by qemu, can be removed from list - A lock is required to manipulate VMs in states [available, using, used], since these are the ones that can be - accessed by several consumers and the producer. All other states are accessed only by the single producer. + A lock is required to manipulate VMs in states [available, + using, used], since these are the ones that can be accessed by + several consumers and the producer. All other states are accessed + only by the single producer. """ def __init__(self, nat_service): @@ -47,7 +101,8 @@ def __init__(self, nat_service): self.loop_sleep_time: int = 5 self.loop_next_call = None - # default configs; custom values will come from the client when they connect to the pool + # default configs; custom values will come from the client + # when they connect to the pool self.max_vm: int = 2 self.vm_unused_timeout: int = 600 self.share_guests: bool = True @@ -80,7 +135,7 @@ def __init__(self, nat_service): self.any_vm_up: bool = False # TODO fix for no VM available - def start_pool(self): + def start_pool(self) -> None: # cleanup older qemu objects self.qemu.destroy_all_cowrie() @@ -101,10 +156,11 @@ def start_pool(self): "backend_pool", "recycle_period", fallback=-1 ) if recycle_period > 0: - reactor.callLater(recycle_period, self.restart_pool) + reactor.callLater(recycle_period, self.restart_pool) # type: ignore[attr-defined] - def stop_pool(self): - # lazy import to avoid exception if not using the backend_pool and libvirt not installed (#1185) + def stop_pool(self) -> None: + # lazy import to avoid exception if not using the backend_pool + # and libvirt not installed (#1185) import libvirt log.msg(eventid="cowrie.backend_pool.service", format="Trying pool clean stop") @@ -132,8 +188,9 @@ def stop_pool(self): except libvirt.libvirtError: print("Not connected to QEMU") # noqa: T201 - def shutdown_pool(self): - # lazy import to avoid exception if not using the backend_pool and libvirt not installed (#1185) + def shutdown_pool(self) -> None: + # lazy import to avoid exception if not using the backend_pool + # and libvirt not installed (#1185) import libvirt self.stop_pool() @@ -143,7 +200,7 @@ def shutdown_pool(self): except libvirt.libvirtError: print("Not connected to QEMU") # noqa: T201 - def restart_pool(self): + def restart_pool(self) -> None: log.msg( eventid="cowrie.backend_pool.service", format="Refreshing pool, terminating current instances and rebooting", @@ -151,7 +208,9 @@ def restart_pool(self): self.stop_pool() self.start_pool() - def set_configs(self, max_vm, vm_unused_timeout, share_guests): + def set_configs( + self, max_vm: int, vm_unused_timeout: int, share_guests: bool + ) -> None: """ Custom configurations sent from the client are set on the pool here. """ @@ -159,23 +218,25 @@ def set_configs(self, max_vm, vm_unused_timeout, share_guests): self.vm_unused_timeout = vm_unused_timeout self.share_guests = share_guests - def get_guest_states(self, states): + def get_guest_states(self, states: list[str]) -> list[Guest]: return [g for g in self.guests if g["state"] in states] - def existing_pool_size(self): - return len([g for g in self.guests if g["state"] != "destroyed"]) + def existing_pool_size(self) -> int: + return len([g for g in self.guests if g["state"] != POOL_STATE_DESTROYED]) - def is_ip_free(self, ip): + def is_ip_free(self, ip: str) -> bool: for guest in self.guests: if guest["guest_ip"] == ip: return False return True - def has_connectivity(self, ip): + def has_connectivity(self, ip: str) -> bool: """ - This method checks if a guest has either SSH or Telnet connectivity, to know whether it is ready for connections - and healthy. It takes into account whether those services are enabled, and if SSH is enabled and available, then - no Telnet check needs to be done. + This method checks if a guest has either SSH or Telnet + connectivity, to know whether it is ready for connections + and healthy. It takes into account whether those services + are enabled, and if SSH is enabled and available, then no + Telnet check needs to be done. """ # check SSH connectivity, if enabled in configs, if disabled then we need to check telnet has_ssh = ( @@ -198,57 +259,54 @@ def __producer_mark_timed_out(self, guest_timeout: int) -> None: """ Checks timed-out VMs and acquires lock to safely mark for deletion """ - self.guest_lock.acquire() - try: + with self.guest_lock: # only mark VMs not in use - used_guests = self.get_guest_states(["used"]) + used_guests = self.get_guest_states([POOL_STATE_USED]) for guest in used_guests: timed_out = ( - guest["freed_timestamp"] + guest_timeout < backend_pool.util.now() + guest.freed_timestamp + guest_timeout < backend_pool.util.now() ) # only mark guests without clients - # (and guest['connected'] == 0) sometimes did not work correctly as some VMs are not signaled as freed + # (and guest['connected'] == 0) sometimes did not + # work correctly as some VMs are not signaled as freed if timed_out: log.msg( eventid="cowrie.backend_pool.service", format="Guest %(guest_id)s (%(guest_ip)s) marked for deletion (timed-out)", - guest_id=guest["id"], - guest_ip=guest["guest_ip"], + guest_id=guest.id, + guest_ip=guest.guest_ip, ) - guest["state"] = "unavailable" - finally: - self.guest_lock.release() + guest.state = POOL_STATE_UNAVAILABLE - def __producer_check_health(self): + def __producer_check_health(self) -> None: """ Checks all usable guests, and whether they should have connectivity. If they don't, then mark them for deletion. """ - self.guest_lock.acquire() - try: - usable_guests = self.get_guest_states(["available", "using", "used"]) + with self.guest_lock: + usable_guests = self.get_guest_states( + [POOL_STATE_AVAILABLE, POOL_STATE_USING, POOL_STATE_USED] + ) for guest in usable_guests: - if not self.has_connectivity(guest["guest_ip"]): + if not self.has_connectivity(guest.guest_ip): log.msg( eventid="cowrie.backend_pool.service", format="Guest %(guest_id)s @ %(guest_ip)s has no connectivity... Destroying", - guest_id=guest["id"], - guest_ip=guest["guest_ip"], + guest_id=guest.id, + guest_ip=guest.guest_ip, ) - guest["state"] = "unavailable" - finally: - self.guest_lock.release() + guest.state = POOL_STATE_UNAVAILABLE - def __producer_destroy_timed_out(self): + def __producer_destroy_timed_out(self) -> None: """ Loops over 'unavailable' guests, and invokes qemu to destroy the corresponding domain """ - unavailable_guests = self.get_guest_states(["unavailable"]) + unavailable_guests = self.get_guest_states([POOL_STATE_UNAVAILABLE]) for guest in unavailable_guests: try: - self.qemu.destroy_guest(guest["domain"], guest["snapshot"]) - guest["state"] = "destroyed" + self.qemu.destroy_guest(guest.domain, guest.snapshot) + guest.state = POOL_STATE_DESTROYED except Exception as error: log.err( eventid="cowrie.backend_pool.service", @@ -256,37 +314,37 @@ def __producer_destroy_timed_out(self): error=error, ) - def __producer_remove_destroyed(self): + def __producer_remove_destroyed(self) -> None: """ Removes guests marked as destroyed (so no qemu domain existing) and simply removes their object from the list """ - destroyed_guests = self.get_guest_states(["destroyed"]) + destroyed_guests = self.get_guest_states([POOL_STATE_DESTROYED]) for guest in destroyed_guests: self.guests.remove(guest) - def __producer_mark_available(self): + def __producer_mark_available(self) -> None: """ Checks recently-booted guests ('created' state), and whether they are accepting SSH or Telnet connections, which indicates they are ready to be used ('available' state). No lock needed since the 'created' state is only accessed by the single-threaded producer """ - created_guests = self.get_guest_states(["created"]) + created_guests = self.get_guest_states([POOL_STATE_CREATED]) for guest in created_guests: - if self.has_connectivity(guest["guest_ip"]): + if self.has_connectivity(guest.guest_ip): self.any_vm_up = True # TODO fix for no VM available - guest["state"] = "available" - boot_time = int(time.time() - guest["start_timestamp"]) + guest.state = POOL_STATE_AVAILABLE + boot_time = int(time.time() - guest.start_timestamp) log.msg( eventid="cowrie.backend_pool.service", format="Guest %(guest_id)s ready for connections @ %(guest_ip)s! (boot %(boot_time)ss)", - guest_id=guest["id"], - guest_ip=guest["guest_ip"], + guest_id=guest.id, + guest_ip=guest.guest_ip, boot_time=boot_time, ) - def __producer_create_guests(self): + def __producer_create_guests(self) -> None: """ Creates guests until the pool has the allotted amount """ @@ -299,7 +357,7 @@ def __producer_create_guests(self): self.guests.append( { "id": self.guest_id, - "state": "created", + "state": POOL_STATE_CREATED, "prev_state": None, # used in case a guest is requested and freed immediately, to revert the state "start_timestamp": time.time(), "guest_ip": guest_ip, @@ -317,7 +375,7 @@ def __producer_create_guests(self): if self.guest_id == 252: self.guest_id = 0 - def producer_loop(self): + def producer_loop(self) -> None: # delete old VMs, but do not let pool size be 0 if self.existing_pool_size() > 1: # mark timed-out VMs for destruction @@ -339,52 +397,37 @@ def producer_loop(self): self.__producer_mark_available() # sleep until next iteration - self.loop_next_call = reactor.callLater( + self.loop_next_call = reactor.callLater( # type: ignore[attr-defined] self.loop_sleep_time, self.producer_loop ) # Consumers - def __consumers_get_guest_ip(self, src_ip): - self.guest_lock.acquire() - try: + def __consumers_get_guest_ip(self, src_ip: str) -> Optional[Guest]: + with self.guest_lock: # if ip is the same, doesn't matter if being used or not - usable_guests = self.get_guest_states(["used", "using"]) + usable_guests = self.get_guest_states([POOL_STATE_USED, POOL_STATE_USING]) for guest in usable_guests: - if src_ip in guest["client_ips"]: + if src_ip in guest.client_ips: return guest - finally: - self.guest_lock.release() - return None - def __consumers_get_available_guest(self): - self.guest_lock.acquire() - try: - available_guests = self.get_guest_states(["available"]) + def __consumers_get_available_guest(self) -> Optional[Guest]: + with self.guest_lock: + available_guests = self.get_guest_states([POOL_STATE_AVAILABLE]) for guest in available_guests: return guest - finally: - self.guest_lock.release() - return None - def __consumers_get_any_guest(self): - self.guest_lock.acquire() - try: - # try to get a VM with few clients - least_conn = None - - usable_guests = self.get_guest_states(["using", "used"]) - for guest in usable_guests: - if not least_conn or guest["connected"] < least_conn["connected"]: - least_conn = guest - - return least_conn - finally: - self.guest_lock.release() + def __consumers_get_any_guest(self) -> Optional[Guest]: + """ + try to get a VM with few clients + """ + with self.guest_lock: + usable_guests = self.get_guest_states([POOL_STATE_USING, POOL_STATE_USED]) + return min(usable_guests, key=lambda guest: guest.connected) # Consumer methods to be called concurrently - def request_vm(self, src_ip): + def request_vm(self, src_ip: str) -> tuple[int, str, str]: # first check if there is one for the ip guest = self.__consumers_get_guest_ip(src_ip) @@ -404,38 +447,32 @@ def request_vm(self, src_ip): self.stop_pool() raise NoAvailableVMs() - guest["prev_state"] = guest["state"] - guest["state"] = "using" - guest["connected"] += 1 - guest["client_ips"].add(src_ip) + guest.prev_state = guest.state + guest.state = POOL_STATE_USING + guest.connected += 1 + guest.client_ips.append(src_ip) - return guest["id"], guest["guest_ip"], guest["snapshot"] + return guest.id, guest.guest_ip, guest.snapshot - def free_vm(self, guest_id): - self.guest_lock.acquire() - try: + def free_vm(self, guest_id: int) -> None: + with self.guest_lock: for guest in self.guests: - if guest["id"] == guest_id: - guest["freed_timestamp"] = backend_pool.util.now() - guest["connected"] -= 1 + if guest.id == guest_id: + guest.freed_timestamp = backend_pool.util.now() + guest.connected -= 1 - if guest["connected"] == 0: - guest["state"] = "used" + if guest.connected == 0: + guest.state = POOL_STATE_USED return - finally: - self.guest_lock.release() - def reuse_vm(self, guest_id): - self.guest_lock.acquire() - try: + def reuse_vm(self, guest_id: int) -> None: + with self.guest_lock: for guest in self.guests: - if guest["id"] == guest_id: - guest["connected"] -= 1 + if guest.id == guest_id: + guest.connected -= 1 - if guest["connected"] == 0: + if guest.connected == 0: # revert machine state to previous - guest["state"] = guest["prev_state"] - guest["prev_state"] = None + guest.state = guest.prev_state + guest.prev_state = None return - finally: - self.guest_lock.release() diff --git a/src/backend_pool/ssh_exec.py b/src/backend_pool/ssh_exec.py index e60f58468a..4e33bd27a7 100644 --- a/src/backend_pool/ssh_exec.py +++ b/src/backend_pool/ssh_exec.py @@ -3,6 +3,7 @@ from twisted.conch.ssh import channel, common, connection, transport, userauth from twisted.internet import defer, protocol from twisted.internet import reactor +from twisted.internet.interfaces import IAddress class PasswordAuth(userauth.SSHUserAuthClient): @@ -25,16 +26,17 @@ def __init__(self, command, done_deferred, callback, *args, **kwargs): self.data = b"" - def channelOpen(self, data): + def channelOpen(self, specificData: bytes) -> None: + assert self.conn is not None self.conn.sendRequest(self, "exec", common.NS(self.command), wantReply=True) def dataReceived(self, data: bytes) -> None: self.data += data - def extReceived(self, dataType, data): + def extReceived(self, dataType: int, data: bytes) -> None: self.data += data - def closeReceived(self): + def closeReceived(self) -> None: self.conn.transport.loseConnection() self.done_deferred.callback(self.data) @@ -50,7 +52,7 @@ def __init__(self, cmd, done_deferred, callback): self.done_deferred = done_deferred self.callback = callback - def serviceStarted(self): + def serviceStarted(self) -> None: self.openChannel( CommandChannel(self.command, self.done_deferred, self.callback, conn=self) ) @@ -64,10 +66,10 @@ def __init__(self, username, password, command, done_deferred, callback): self.done_deferred = done_deferred self.callback = callback - def verifyHostKey(self, pub_key, fingerprint): + def verifyHostKey(self, hostKey, fingerprint): return defer.succeed(True) - def connectionSecure(self): + def connectionSecure(self) -> None: self.requestService( PasswordAuth( self.username, @@ -85,7 +87,7 @@ def __init__(self, username, password, command, done_deferred, callback): self.done_deferred = done_deferred self.callback = callback - def buildProtocol(self, addr): + def buildProtocol(self, addr: IAddress) -> ClientCommandTransport: return ClientCommandTransport( self.username, self.password, diff --git a/src/backend_pool/telnet_exec.py b/src/backend_pool/telnet_exec.py index 3511279665..1a06f421e1 100644 --- a/src/backend_pool/telnet_exec.py +++ b/src/backend_pool/telnet_exec.py @@ -2,11 +2,11 @@ from __future__ import annotations import re -from typing import Optional from twisted.conch.telnet import StatefulTelnetProtocol, TelnetTransport from twisted.internet import defer from twisted.internet import reactor +from twisted.internet.interfaces import IAddress from twisted.internet.protocol import ClientFactory from twisted.python import log @@ -16,6 +16,12 @@ class TelnetConnectionError(Exception): class TelnetClient(StatefulTelnetProtocol): + """ + A telnet client + """ + + factory: TelnetFactory + def __init__(self): # output from server self.response: bytes = b"" @@ -23,7 +29,7 @@ def __init__(self): # callLater instance to wait until we have stop getting output for some time self.done_callback = None - self.command: Optional[bytes] = None + self.command: bytes | None = None def connectionMade(self): """ @@ -68,7 +74,7 @@ def lineReceived(self, line: bytes) -> None: # start countdown to command done (when reached, consider the output was completely received and close) if not self.done_callback: - self.done_callback = reactor.callLater(0.5, self.close) # type: ignore + self.done_callback = reactor.callLater(0.5, self.close) # type: ignore[attr-defined] else: self.done_callback.reset(0.5) @@ -106,7 +112,7 @@ def __init__(self, username, password, prompt, command, done_deferred, callback) self.done_deferred = done_deferred self.callback = callback - def buildProtocol(self, addr): + def buildProtocol(self, addr: IAddress) -> TelnetTransport: transport = TelnetTransport(TelnetClient) transport.factory = self return transport diff --git a/src/backend_pool/util.py b/src/backend_pool/util.py index 247ccba540..5e44e822f4 100644 --- a/src/backend_pool/util.py +++ b/src/backend_pool/util.py @@ -1,9 +1,12 @@ +""" +general utility functions +""" + # Copyright (c) 2019 Guilherme Borges # See the COPYRIGHT file for more information from __future__ import annotations -from typing import Optional import os import random @@ -12,13 +15,11 @@ def ping(guest_ip: str) -> int: - # could use `capture_output=True` instead of `stdout` and `stderr` args in Python 3.7 out = subprocess.run(["ping", "-c 1", guest_ip], capture_output=True) return out.returncode == 0 -def nmap_port(guest_ip: str, port: int) -> int: - # could use `capture_output=True` instead of `stdout` and `stderr` args in Python 3.7 +def nmap_port(guest_ip: str, port: int) -> bool: out = subprocess.run( ["nmap", guest_ip, "-PN", "-p", str(port)], capture_output=True, @@ -35,9 +36,10 @@ def to_byte(n: int) -> str: return hex(n)[2:].zfill(2) -def generate_network_table(seed: Optional[int] = None) -> dict[str, str]: +def generate_network_table(seed: int | None = None) -> dict[str, str]: """ - Generates a table associating MAC and IP addressed to be distributed by our virtual network adapter via DHCP. + Generates a table associating MAC and IP addressed to be + distributed by our virtual network adapter via DHCP. """ # we use the seed in case we want to generate the same table twice @@ -71,10 +73,9 @@ def now() -> float: def to_absolute_path(path: str) -> str: """ - Converts a relative path to absolute, useful when converting cowrie configs (relative) to qemu paths - (which must be absolute) + Converts a relative path to absolute, useful when converting + cowrie configs (relative) to qemu paths (which must be absolute) """ if not os.path.isabs(path): return os.path.join(os.getcwd(), path) - else: - return path + return path diff --git a/src/cowrie/commands/adduser.py b/src/cowrie/commands/adduser.py index f8c4c1798e..7fdc551b7e 100644 --- a/src/cowrie/commands/adduser.py +++ b/src/cowrie/commands/adduser.py @@ -4,7 +4,6 @@ from __future__ import annotations import random -from typing import Optional from twisted.internet import reactor @@ -17,7 +16,7 @@ class Command_adduser(HoneyPotCommand): item: int - output: list[tuple[int, str]] = [ + output: tuple[tuple[int, str], ...] = ( (O_O, "Adding user `%(username)s' ...\n"), (O_O, "Adding new group `%(username)s' (1001) ...\n"), ( @@ -47,8 +46,8 @@ class Command_adduser(HoneyPotCommand): (O_O, "Deleting group `%(username)s' (1001) ...\n"), (O_O, "Deleting home directory `/home/%(username)s' ...\n"), (O_Q, "Try again? [Y/n] "), - ] - username: Optional[str] = None + ) + username: str | None = None def start(self) -> None: self.item = 0 diff --git a/src/cowrie/commands/apt.py b/src/cowrie/commands/apt.py index cf0b76a0bc..d913b3ea8e 100644 --- a/src/cowrie/commands/apt.py +++ b/src/cowrie/commands/apt.py @@ -6,7 +6,7 @@ import random import re -from typing import Any, Optional +from typing import Any from collections.abc import Callable from twisted.internet import defer, reactor @@ -34,7 +34,7 @@ class Command_aptget(HoneyPotCommand): Any installed packages, places a 'Segfault' at /usr/bin/PACKAGE.''' """ - packages: dict[str, dict[str, Any]] = {} + packages: dict[str, dict[str, Any]] def start(self) -> None: if len(self.args) == 0: @@ -47,8 +47,9 @@ def start(self) -> None: self.do_moo() else: self.do_locked() + self.packages = {} - def sleep(self, time: float, time2: Optional[float] = None) -> defer.Deferred: + def sleep(self, time: float, time2: float | None = None) -> defer.Deferred: d: defer.Deferred = defer.Deferred() if time2: time = random.randint(int(time * 100), int(time2 * 100.0)) / 100.0 diff --git a/src/cowrie/commands/awk.py b/src/cowrie/commands/awk.py index 745de5444f..a9bd8478e7 100644 --- a/src/cowrie/commands/awk.py +++ b/src/cowrie/commands/awk.py @@ -12,7 +12,6 @@ import getopt import re -from typing import Optional from re import Match from twisted.python import log @@ -29,16 +28,14 @@ class Command_awk(HoneyPotCommand): """ # code is an array of dictionaries contain the regexes to match and the code to execute - code: list[dict[str, str]] = [] + code: list[dict[str, str]] def start(self) -> None: try: optlist, args = getopt.gnu_getopt(self.args, "Fvf", ["version"]) except getopt.GetoptError as err: self.errorWrite( - "awk: invalid option -- '{}'\nTry 'awk --help' for more information.\n".format( - err.opt - ) + f"awk: invalid option -- '{err.opt}'\nTry 'awk --help' for more information.\n" ) self.exit() return @@ -110,7 +107,7 @@ def awk_print(self, words: str) -> None: self.write(words) self.write("\n") - def output(self, inb: Optional[bytes]) -> None: + def output(self, inb: bytes | None) -> None: """ This is the awk output. """ @@ -130,7 +127,6 @@ def repl(m: Match) -> str: return "" for inputline in inputlines: - # split by whitespace and add full line in $0 as awk does. # TODO: change here to use custom field separator words = inputline.split() diff --git a/src/cowrie/commands/base.py b/src/cowrie/commands/base.py index 6f7f8d5634..0513f1ade0 100644 --- a/src/cowrie/commands/base.py +++ b/src/cowrie/commands/base.py @@ -11,7 +11,6 @@ import random import re import time -from typing import Optional from collections.abc import Callable from twisted.internet import error, reactor @@ -133,7 +132,6 @@ def call(self) -> None: class Command_echo(HoneyPotCommand): def call(self) -> None: - newline = True escape_decode = False @@ -847,7 +845,7 @@ def start(self) -> None: self.write("Enter new UNIX password: ") self.protocol.password_input = True self.callbacks = [self.ask_again, self.finish] - self.passwd: Optional[str] = None + self.passwd: str | None = None def ask_again(self, line: str) -> None: self.passwd = line @@ -1009,7 +1007,6 @@ def handle_CTRL_C(self) -> None: class Command_sh(HoneyPotCommand): def call(self) -> None: if self.args and self.args[0].strip() == "-c": - line = " ".join(self.args[1:]) # it might be sh -c 'echo "sometext"', so don't use line.strip('\'\"') diff --git a/src/cowrie/commands/cat.py b/src/cowrie/commands/cat.py index c6b9e834cd..e9aefb816e 100644 --- a/src/cowrie/commands/cat.py +++ b/src/cowrie/commands/cat.py @@ -9,7 +9,6 @@ import getopt -from typing import Optional from twisted.python import log @@ -72,7 +71,7 @@ def start(self) -> None: self.output(self.input_data) self.exit() - def output(self, inb: Optional[bytes]) -> None: + def output(self, inb: bytes | None) -> None: """ This is the cat output, with optional line numbering """ diff --git a/src/cowrie/commands/curl.py b/src/cowrie/commands/curl.py index cc6bee90f8..2b12e25d66 100644 --- a/src/cowrie/commands/curl.py +++ b/src/cowrie/commands/curl.py @@ -6,7 +6,6 @@ import getopt import ipaddress import os -from typing import Optional from twisted.internet import error from twisted.python import compat, log @@ -182,7 +181,7 @@ class Command_curl(HoneyPotCommand): """ limit_size: int = CowrieConfig.getint("honeypot", "download_limit_size", fallback=0) - outfile: Optional[str] = None # outfile is the file saved inside the honeypot + outfile: str | None = None # outfile is the file saved inside the honeypot artifact: Artifact # artifact is the file saved for forensics in the real file system currentlength: int = 0 # partial size during download totallength: int = 0 # total length diff --git a/src/cowrie/commands/dd.py b/src/cowrie/commands/dd.py index fdcb7b5e9b..90f19d91a1 100644 --- a/src/cowrie/commands/dd.py +++ b/src/cowrie/commands/dd.py @@ -22,12 +22,14 @@ class Command_dd(HoneyPotCommand): dd command """ - ddargs: dict[str, str] = {} + ddargs: dict[str, str] def start(self) -> None: if not self.args or self.args[0] == ">": return + self.ddargs = {} + for arg in self.args: if arg.find("=") == -1: self.write(f"unknown operand: {arg}") diff --git a/src/cowrie/commands/ethtool.py b/src/cowrie/commands/ethtool.py index 134ce96964..da151a1a21 100644 --- a/src/cowrie/commands/ethtool.py +++ b/src/cowrie/commands/ethtool.py @@ -30,7 +30,6 @@ def do_ethtool_help(self) -> None: ) def do_ethtool_lo(self) -> None: - self.write( """Settings for lo: Link detected: yes\n""" diff --git a/src/cowrie/commands/finger.py b/src/cowrie/commands/finger.py index 40810cb208..18686309f8 100644 --- a/src/cowrie/commands/finger.py +++ b/src/cowrie/commands/finger.py @@ -1,128 +1,129 @@ -from __future__ import annotations -from cowrie.shell.command import HoneyPotCommand -import datetime -import getopt - -commands = {} - -FINGER_HELP = """Usage:""" - - -class Command_finger(HoneyPotCommand): - def call(self): - time = datetime.datetime.utcnow() - user_data = [] - # Get all user data and convert to string - all_users_byte = self.fs.file_contents("/etc/passwd") - all_users = all_users_byte.decode("utf-8") - # Convert all new lines to : character - all_users = all_users.replace("\n", ":") - # Convert into list by splitting string - all_users_list = all_users.split(":") - # Loop over the data in sets of 7 - for i in range(0, len(all_users_list), 7): - x = i - # Ensure any added list contains data and is not a blank space by > - if len(all_users_list[x : x + 7]) != 1: - # Take the next 7 elements and put them a list, then add to 2d> - user_data.append(all_users_list[x : x + 7]) - # THIS CODE IS FOR DEBUGGING self.write(str(user_data)) - - # If finger called without args - if len(self.args) == 0: - self.write("Login\tName\tTty Idle\tLogin Time Office Office Phone\n") - for i in range(len(user_data)): - if len(str(user_data[i][0])) > 6: - if len(str(user_data[i][4])) > 6: - self.write( - "{}+ {}+ *:{}\t\t{} (:{})\n".format( - str(user_data[i][0])[:6], - str(user_data[i][4])[:6], - str(user_data[i][2]), - str(time.strftime("%b %d %H:%M")), - str(user_data[i][3]), - ) - ) - else: - self.write( - "{}+ {}\t*:{}\t\t{} (:{})\n".format( - str(user_data[i][0])[:6], - str(user_data[i][4]), - str(user_data[i][2]), - str(time.strftime("%b %d %H:%M")), - str(user_data[i][3]), - ) - ) - else: - if len(str(user_data[i][4])) > 6: - self.write( - "{}\t{}+ *:{}\t\t{} (:{})\n".format( - str(user_data[i][0]), - str(user_data[i][4])[:6], - str(user_data[i][2]), - str(time.strftime("%b %d %H:%M")), - str(user_data[i][3]), - ) - ) - else: - self.write( - "{}\t{}\t*:{}\t\t{} (:{})\n".format( - str(user_data[i][0]), - str(user_data[i][4]), - str(user_data[i][2]), - str(time.strftime("%b %d %H:%M")), - str(user_data[i][3]), - ) - ) - # self.write(f"name: %20." + str(user_data[i][0]) + "\n") > - # time = datetime.datetime.utcnow() - # self.write("{}\n".format(time.strftime("%a %b %d %H:%M:%S UTC %Y"> - return - - if len(self.args): - try: - opts, args = getopt.gnu_getopt(self.args, "") - except getopt.GetoptError as err: - self.errorWrite( - f"""finger: invalid option -- '{err.opt}' -usage: finger [-lmps] [login ...]\n""" - ) - return - # If args given not any predefined, assume is username - if len(args) > 0: - - for i in range(len(user_data)): - # Run if check to check if user is real - if args[0] == user_data[i][0]: - # Display user data - self.write( - """Login: """ - + str(user_data[i][0]) - + """ Name: """ - + str(user_data[i][4]) - + """ -Directory: """ - + str(user_data[i][5]) - + """ Shell: """ - + str(user_data[i][6]) - + """ -On since """ - + str(time.strftime("%a %b %d %H:%M")) - + """ (UTC) on :0 from :0 (messages off) -No mail. -No Plan. -""" - ) - return - # If user is NOT real inform user - self.write(f"finger: {args[0]}: no such user\n") - - # IF TIME ALLOWS: Seperate into multiple functions - # IF TIME ALLOWS: Make my comments more concise and remove debuggi> - return - # Base.py has some helpful stuff - return - - -commands["bin/finger"] = Command_finger -commands["finger"] = Command_finger +from __future__ import annotations + +import datetime +import getopt + +from cowrie.shell.command import HoneyPotCommand + +commands = {} + +FINGER_HELP = """Usage:""" + + +class Command_finger(HoneyPotCommand): + def call(self): + time = datetime.datetime.utcnow() + user_data = [] + # Get all user data and convert to string + all_users_byte = self.fs.file_contents("/etc/passwd") + all_users = all_users_byte.decode("utf-8") + # Convert all new lines to : character + all_users = all_users.replace("\n", ":") + # Convert into list by splitting string + all_users_list = all_users.split(":") + # Loop over the data in sets of 7 + for i in range(0, len(all_users_list), 7): + x = i + # Ensure any added list contains data and is not a blank space by > + if len(all_users_list[x : x + 7]) != 1: + # Take the next 7 elements and put them a list, then add to 2d> + user_data.append(all_users_list[x : x + 7]) + # THIS CODE IS FOR DEBUGGING self.write(str(user_data)) + + # If finger called without args + if len(self.args) == 0: + self.write("Login\tName\tTty Idle\tLogin Time Office Office Phone\n") + for i in range(len(user_data)): + if len(str(user_data[i][0])) > 6: + if len(str(user_data[i][4])) > 6: + self.write( + "{}+ {}+ *:{}\t\t{} (:{})\n".format( + str(user_data[i][0])[:6], + str(user_data[i][4])[:6], + str(user_data[i][2]), + str(time.strftime("%b %d %H:%M")), + str(user_data[i][3]), + ) + ) + else: + self.write( + "{}+ {}\t*:{}\t\t{} (:{})\n".format( + str(user_data[i][0])[:6], + str(user_data[i][4]), + str(user_data[i][2]), + str(time.strftime("%b %d %H:%M")), + str(user_data[i][3]), + ) + ) + else: + if len(str(user_data[i][4])) > 6: + self.write( + "{}\t{}+ *:{}\t\t{} (:{})\n".format( + str(user_data[i][0]), + str(user_data[i][4])[:6], + str(user_data[i][2]), + str(time.strftime("%b %d %H:%M")), + str(user_data[i][3]), + ) + ) + else: + self.write( + "{}\t{}\t*:{}\t\t{} (:{})\n".format( + str(user_data[i][0]), + str(user_data[i][4]), + str(user_data[i][2]), + str(time.strftime("%b %d %H:%M")), + str(user_data[i][3]), + ) + ) + # self.write(f"name: %20." + str(user_data[i][0]) + "\n") > + # time = datetime.datetime.utcnow() + # self.write("{}\n".format(time.strftime("%a %b %d %H:%M:%S UTC %Y"> + return + + try: + opts, args = getopt.gnu_getopt(self.args, "") + except getopt.GetoptError as err: + self.errorWrite( + f"""finger: invalid option -- '{err.opt}' +usage: finger [-lmps] [login ...]\n""" + ) + return + + # If args given not any predefined, assume is username + if len(args) > 0: + for i in range(len(user_data)): + # Run if check to check if user is real + if args[0] == user_data[i][0]: + # Display user data + self.write( + """Login: """ + + str(user_data[i][0]) + + """ Name: """ + + str(user_data[i][4]) + + """ +Directory: """ + + str(user_data[i][5]) + + """ Shell: """ + + str(user_data[i][6]) + + """ +On since """ + + str(time.strftime("%a %b %d %H:%M")) + + """ (UTC) on :0 from :0 (messages off) +No mail. +No Plan. +""" + ) + return + # If user is NOT real inform user + self.write(f"finger: {args[0]}: no such user\n") + + # IF TIME ALLOWS: Seperate into multiple functions + # IF TIME ALLOWS: Make my comments more concise and remove debuggi> + return + # Base.py has some helpful stuff + return + + +commands["bin/finger"] = Command_finger +commands["finger"] = Command_finger diff --git a/src/cowrie/commands/fs.py b/src/cowrie/commands/fs.py index 26d2989877..e7f341b5bd 100644 --- a/src/cowrie/commands/fs.py +++ b/src/cowrie/commands/fs.py @@ -258,7 +258,7 @@ def call(self) -> None: newpath = self.fs.resolve_path(pname, self.protocol.cwd) inode = self.fs.getfile(newpath) except Exception: - pass + inode = None if pname == "-": self.errorWrite("bash: cd: OLDPWD not set\n") return @@ -371,9 +371,7 @@ def call(self) -> None: if i[fs.A_NAME] == basename: if i[fs.A_TYPE] == fs.T_DIR and not recursive: self.errorWrite( - "rm: cannot remove `{}': Is a directory\n".format( - i[fs.A_NAME] - ) + f"rm: cannot remove `{i[fs.A_NAME]}': Is a directory\n" ) else: dir.remove(i) @@ -456,7 +454,7 @@ def resolv(pname: str) -> str: dir = self.fs.get_path(os.path.dirname(resolv(dest))) outfile = os.path.basename(dest.rstrip("/")) if outfile in [x[fs.A_NAME] for x in dir]: - dir.remove([x for x in dir if x[fs.A_NAME] == outfile][0]) + dir.remove(next(x for x in dir if x[fs.A_NAME] == outfile)) s[fs.A_NAME] = outfile dir.append(s) @@ -480,7 +478,7 @@ def call(self) -> None: optlist, args = getopt.gnu_getopt(self.args, "-bfiStTuv") except getopt.GetoptError: self.errorWrite("Unrecognized option\n") - self.exit() + return def resolv(pname: str) -> str: rsv: str = self.fs.resolve_path(pname, self.protocol.cwd) @@ -552,7 +550,7 @@ def call(self) -> None: return try: self.fs.mkdir(pname, 0, 0, 4096, 16877) - except (fs.FileNotFound): + except fs.FileNotFound: self.errorWrite( f"mkdir: cannot create directory `{f}': No such file or directory\n" ) diff --git a/src/cowrie/commands/ftpget.py b/src/cowrie/commands/ftpget.py index 3a94746729..239767c3aa 100644 --- a/src/cowrie/commands/ftpget.py +++ b/src/cowrie/commands/ftpget.py @@ -6,7 +6,6 @@ import getopt import os import socket -from typing import Optional, Union from twisted.python import log @@ -27,7 +26,7 @@ def connect( host: str = "", port: int = 0, timeout: float = -999.0, - source_address: Optional[tuple[str, int]] = None, + source_address: tuple[str, int] | None = None, ) -> str: if host != "": self.host = host @@ -46,7 +45,7 @@ def connect( return self.welcome def ntransfercmd( - self, cmd: str, rest: Union[int, str, None] = None + self, cmd: str, rest: int | str | None = None ) -> tuple[socket.socket, int]: size = 0 if self.passiveserver: @@ -166,9 +165,7 @@ def start(self) -> None: path = os.path.dirname(fakeoutfile) if not path or not self.fs.exists(path) or not self.fs.isdir(path): self.write( - "ftpget: can't open '{}': No such file or directory".format( - self.local_file - ) + f"ftpget: can't open '{self.local_file}': No such file or directory" ) self.exit() return @@ -250,9 +247,7 @@ def ftp_download(self) -> bool: ftp.connect(host=self.host, port=self.port, timeout=30) except Exception as e: log.msg( - "FTP connect failed: host={}, port={}, err={}".format( - self.host, self.port, str(e) - ) + f"FTP connect failed: host={self.host}, port={self.port}, err={e!s}" ) self.write("ftpget: can't connect to remote host: Connection refused\n") return False @@ -280,7 +275,7 @@ def ftp_download(self) -> bool: self.write(f"ftpget: unexpected server response to USER: {e!s}\n") try: ftp.quit() - except socket.timeout: + except TimeoutError: pass return False @@ -299,7 +294,7 @@ def ftp_download(self) -> bool: self.write(f"ftpget: unexpected server response to USER: {e!s}\n") try: ftp.quit() - except socket.timeout: + except TimeoutError: pass return False @@ -310,7 +305,7 @@ def ftp_download(self) -> bool: try: ftp.quit() - except socket.timeout: + except TimeoutError: pass return True diff --git a/src/cowrie/commands/gcc.py b/src/cowrie/commands/gcc.py index bf99cbef0b..cc3611459c 100644 --- a/src/cowrie/commands/gcc.py +++ b/src/cowrie/commands/gcc.py @@ -166,12 +166,10 @@ def version(self, short: bool) -> None: version_short = ".".join([str(v) for v in Command_gcc.APP_VERSION[:2]]) if short: - data = """{} (Debian {}-8) {} + data = f"""{Command_gcc.APP_NAME} (Debian {version}-8) {version} Copyright (C) 2010 Free Software Foundation, Inc. This is free software; see the source for copying conditions. There is NO -warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.""".format( - Command_gcc.APP_NAME, version, version - ) +warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.""" else: data = """Using built-in specs. COLLECT_GCC=gcc diff --git a/src/cowrie/commands/groups.py b/src/cowrie/commands/groups.py index 4ad52ead59..2c3cbd9e55 100644 --- a/src/cowrie/commands/groups.py +++ b/src/cowrie/commands/groups.py @@ -51,6 +51,7 @@ def call(self): else: content = self.fs.file_contents("/etc/group") self.output(content, "") + def output(self, file_content, username): groups_string = bytes("", encoding="utf-8") if not username: @@ -77,7 +78,6 @@ def output(self, file_content, username): self.writeBytes(groups_string + b"\n") def check_valid_user(self, username): - usr_byte = bytes(username, encoding="utf-8") users = self.fs.file_contents("/etc/shadow") lines = users.split(b"\n") diff --git a/src/cowrie/commands/iptables.py b/src/cowrie/commands/iptables.py index 3008fddd5b..da2ce85392 100644 --- a/src/cowrie/commands/iptables.py +++ b/src/cowrie/commands/iptables.py @@ -4,7 +4,7 @@ import optparse -from typing import Any, Optional +from typing import Any from cowrie.shell.command import HoneyPotCommand @@ -17,7 +17,7 @@ def __init__(self, msg: str) -> None: class OptionParsingExit(Exception): - def __init__(self, status: int, msg: Optional[str]) -> None: + def __init__(self, status: int, msg: str | None) -> None: self.msg = msg self.status = status @@ -26,7 +26,7 @@ class ModifiedOptionParser(optparse.OptionParser): def error(self, msg: str) -> None: raise OptionParsingError(msg) - def exit(self, status: int = 0, msg: Optional[str] = None) -> None: + def exit(self, status: int = 0, msg: str | None = None) -> None: raise OptionParsingExit(status, msg) @@ -283,7 +283,7 @@ def show_help(self) -> None: """ self.write( - """{} {}' + f"""{Command_iptables.APP_NAME} {Command_iptables.APP_VERSION}' Usage: iptables -[AD] chain rule-specification [options] iptables -I chain [rulenum] rule-specification [options] @@ -345,9 +345,7 @@ def show_help(self) -> None: [!] --fragment -f match second or further fragments only --modprobe= try to insert modules using this command --set-counters PKTS BYTES set the counter during insert/append -[!] --version -V print package version.\n""".format( - Command_iptables.APP_NAME, Command_iptables.APP_VERSION - ) +[!] --version -V print package version.\n""" ) self.exit() diff --git a/src/cowrie/commands/perl.py b/src/cowrie/commands/perl.py index 981ed1132f..06ee29cdce 100644 --- a/src/cowrie/commands/perl.py +++ b/src/cowrie/commands/perl.py @@ -82,6 +82,7 @@ def start(self) -> None: "Unrecognized switch: -" + err.opt + " (-h will show valid options).\n" ) self.exit() + return # Parse options for o, _a in opts: @@ -101,9 +102,7 @@ def start(self) -> None: self.exit() else: self.write( - 'Can\'t open perl script "{}": No such file or directory\n'.format( - value - ) + f'Can\'t open perl script "{value}": No such file or directory\n' ) self.exit() diff --git a/src/cowrie/commands/python.py b/src/cowrie/commands/python.py index f157ea9b2c..36c3297ab6 100644 --- a/src/cowrie/commands/python.py +++ b/src/cowrie/commands/python.py @@ -110,7 +110,6 @@ def start(self) -> None: if self.fs.exists(sourcefile) or value == "-": self.exit() else: - self.write( "python: can't open file '%s': [Errno 2] No such file or directory\n" % (value) diff --git a/src/cowrie/commands/scp.py b/src/cowrie/commands/scp.py index a9a6a75266..506fb4e41b 100644 --- a/src/cowrie/commands/scp.py +++ b/src/cowrie/commands/scp.py @@ -161,11 +161,9 @@ def parse_scp_data(self, data: bytes) -> bytes: pos += 1 if re.match(rb"^C0[\d]{3} [\d]+ [^\s]+$", header): - r = re.search(rb"C(0[\d]{3}) ([\d]+) ([^\s]+)", header) if r and r.group(1) and r.group(2) and r.group(3): - dend = pos + int(r.group(2)) if dend > len(data): diff --git a/src/cowrie/commands/ssh.py b/src/cowrie/commands/ssh.py index 799039b9af..592b798168 100644 --- a/src/cowrie/commands/ssh.py +++ b/src/cowrie/commands/ssh.py @@ -53,6 +53,8 @@ def start(self) -> None: except getopt.GetoptError: self.write("Unrecognized option\n") self.exit() + return + for opt in optlist: if opt[0] == "-V": self.write( @@ -82,10 +84,8 @@ def start(self) -> None: self.ip = host else: self.write( - "ssh: Could not resolve hostname {}: \ - Name or service not known\n".format( - host - ) + f"ssh: Could not resolve hostname {host}: \ + Name or service not known\n" ) self.exit() else: @@ -98,10 +98,8 @@ def start(self) -> None: self.user = user self.write( - "The authenticity of host '{} ({})' \ - can't be established.\n".format( - self.host, self.ip - ) + f"The authenticity of host '{self.host} ({self.ip})' \ + can't be established.\n" ) self.write( "RSA key fingerprint is \ @@ -112,10 +110,8 @@ def start(self) -> None: def yesno(self, line: str) -> None: self.write( - "Warning: Permanently added '{}' (RSA) to the \ - list of known hosts.\n".format( - self.host - ) + f"Warning: Permanently added '{self.host}' (RSA) to the \ + list of known hosts.\n" ) self.write(f"{self.user}@{self.host}'s password: ") self.protocol.password_input = True @@ -136,10 +132,8 @@ def finish(self, line: str) -> None: self.protocol.cwd = "/" self.protocol.password_input = False self.write( - "Linux {} 2.6.26-2-686 #1 SMP Wed Nov 4 20:45:37 \ - UTC 2009 i686\n".format( - self.protocol.hostname - ) + f"Linux {self.protocol.hostname} 2.6.26-2-686 #1 SMP Wed Nov 4 20:45:37 \ + UTC 2009 i686\n" ) self.write(f"Last login: {time.ctime(time.time() - 123123)} from 192.168.9.4\n") self.exit() diff --git a/src/cowrie/commands/tee.py b/src/cowrie/commands/tee.py index fc220dc7ba..d841c9d815 100644 --- a/src/cowrie/commands/tee.py +++ b/src/cowrie/commands/tee.py @@ -9,7 +9,6 @@ import getopt import os -from typing import Optional from twisted.python import log @@ -25,7 +24,7 @@ class Command_tee(HoneyPotCommand): """ append = False - teeFiles: list[str] = [] + teeFiles: list[str] writtenBytes = 0 ignoreInterupts = False @@ -41,6 +40,8 @@ def start(self) -> None: self.exit() return + self.teeFiles = [] + for o, _a in optlist: if o in ("--help"): self.help() @@ -81,7 +82,7 @@ def write_to_file(self, data: bytes) -> None: for outf in self.teeFiles: self.fs.update_size(outf, self.writtenBytes) - def output(self, inb: Optional[bytes]) -> None: + def output(self, inb: bytes | None) -> None: """ This is the tee output, if no file supplied """ diff --git a/src/cowrie/commands/uniq.py b/src/cowrie/commands/uniq.py index b42ca2b631..31e9a355b0 100644 --- a/src/cowrie/commands/uniq.py +++ b/src/cowrie/commands/uniq.py @@ -51,7 +51,6 @@ class Command_uniq(HoneyPotCommand): - last_line = None def start(self) -> None: diff --git a/src/cowrie/commands/uptime.py b/src/cowrie/commands/uptime.py index 7bf6ca16a5..da772cee69 100644 --- a/src/cowrie/commands/uptime.py +++ b/src/cowrie/commands/uptime.py @@ -14,7 +14,9 @@ class Command_uptime(HoneyPotCommand): def call(self) -> None: self.write( - "{} up {}, 1 user, load average: 0.00, 0.00, 0.00\n".format(time.strftime("%H:%M:%S"), utils.uptime(self.protocol.uptime())) + "{} up {}, 1 user, load average: 0.00, 0.00, 0.00\n".format( + time.strftime("%H:%M:%S"), utils.uptime(self.protocol.uptime()) + ) ) diff --git a/src/cowrie/commands/wget.py b/src/cowrie/commands/wget.py index 6a4ebc4049..406708a798 100644 --- a/src/cowrie/commands/wget.py +++ b/src/cowrie/commands/wget.py @@ -7,7 +7,7 @@ import ipaddress import os import time -from typing import Any, Optional +from typing import Any from twisted.internet import error from twisted.python import compat, log @@ -64,7 +64,7 @@ class Command_wget(HoneyPotCommand): limit_size: int = CowrieConfig.getint("honeypot", "download_limit_size", fallback=0) quiet: bool = False - outfile: Optional[str] = None # outfile is the file saved inside the honeypot + outfile: str | None = None # outfile is the file saved inside the honeypot artifact: Artifact # artifact is the file saved for forensics in the real file system currentlength: int = 0 # partial size during download totallength: int = 0 # total length @@ -139,9 +139,7 @@ def start(self) -> None: path = os.path.dirname(self.outfile) # type: ignore if not path or not self.fs.exists(path) or not self.fs.isdir(path): self.errorWrite( - "wget: {}: Cannot open: No such file or directory\n".format( - self.outfile - ) + f"wget: {self.outfile}: Cannot open: No such file or directory\n" ) self.exit() return diff --git a/src/cowrie/commands/yum.py b/src/cowrie/commands/yum.py index 2a1aa844ed..9842483aa7 100644 --- a/src/cowrie/commands/yum.py +++ b/src/cowrie/commands/yum.py @@ -10,7 +10,7 @@ import hashlib import random import re -from typing import Any, Optional +from typing import Any from collections.abc import Callable from twisted.internet import defer @@ -41,7 +41,7 @@ class Command_yum(HoneyPotCommand): Any installed packages, places a 'Segfault' at /usr/bin/PACKAGE.''' """ - packages: dict[str, dict[str, Any]] = {} + packages: dict[str, dict[str, Any]] def start(self) -> None: if len(self.args) == 0: @@ -52,8 +52,9 @@ def start(self) -> None: self.do_install() else: self.do_locked() + self.packages = {} - def sleep(self, time: float, time2: Optional[float] = None) -> defer.Deferred: + def sleep(self, time: float, time2: float | None = None) -> defer.Deferred: d: defer.Deferred = defer.Deferred() if time2: time = random.randint(int(time * 100), int(time2 * 100)) / 100.0 diff --git a/src/cowrie/core/artifact.py b/src/cowrie/core/artifact.py index 61880f51d3..10ee9f4e5b 100644 --- a/src/cowrie/core/artifact.py +++ b/src/cowrie/core/artifact.py @@ -26,7 +26,7 @@ import os import tempfile from types import TracebackType -from typing import Any, Optional +from typing import Any from twisted.python import log @@ -34,13 +34,14 @@ class Artifact: - artifactDir: str = CowrieConfig.get("honeypot", "download_path") def __init__(self, label: str) -> None: self.label: str = label - self.fp = tempfile.NamedTemporaryFile(dir=self.artifactDir, delete=False) # pylint: disable=R1732 + self.fp = tempfile.NamedTemporaryFile( # pylint: disable=R1732 + dir=self.artifactDir, delete=False + ) self.tempFilename = self.fp.name self.closed: bool = False @@ -52,9 +53,9 @@ def __enter__(self) -> Any: def __exit__( self, - etype: Optional[type[BaseException]], - einst: Optional[BaseException], - etrace: Optional[TracebackType], + etype: type[BaseException] | None, + einst: BaseException | None, + etrace: TracebackType | None, ) -> bool: self.close() return True @@ -65,10 +66,13 @@ def write(self, data: bytes) -> None: def fileno(self) -> Any: return self.fp.fileno() - def close(self, keepEmpty: bool = False) -> Optional[tuple[str, str]]: + def close(self, keepEmpty: bool = False) -> tuple[str, str] | None: size: int = self.fp.tell() if size == 0 and not keepEmpty: - os.remove(self.fp.name) + try: + os.remove(self.fp.name) + except FileNotFoundError: + pass return None self.fp.seek(0) diff --git a/src/cowrie/core/auth.py b/src/cowrie/core/auth.py index 3ea83fd047..c4e6ce2c25 100644 --- a/src/cowrie/core/auth.py +++ b/src/cowrie/core/auth.py @@ -12,7 +12,7 @@ from collections import OrderedDict from os import path from random import randint -from typing import Any, Union +from typing import Any from re import Pattern from twisted.python import log @@ -36,7 +36,7 @@ class UserDB: def __init__(self) -> None: self.userdb: dict[ - tuple[Union[Pattern[bytes], bytes], Union[Pattern[bytes], bytes]], bool + tuple[Pattern[bytes] | bytes, Pattern[bytes] | bytes], bool ] = OrderedDict() self.load() @@ -70,8 +70,8 @@ def checklogin( self, thelogin: bytes, thepasswd: bytes, src_ip: str = "0.0.0.0" ) -> bool: for credentials, policy in self.userdb.items(): - login: Union[bytes, Pattern[bytes]] - passwd: Union[bytes, Pattern[bytes]] + login: bytes | Pattern[bytes] + passwd: bytes | Pattern[bytes] login, passwd = credentials if self.match_rule(login, thelogin): @@ -80,14 +80,12 @@ def checklogin( return False - def match_rule( - self, rule: Union[bytes, Pattern[bytes]], data: bytes - ) -> Union[bool, bytes]: + def match_rule(self, rule: bytes | Pattern[bytes], data: bytes) -> bool | bytes: if isinstance(rule, bytes): return rule in [b"*", data] return bool(rule.search(data)) - def re_or_bytes(self, rule: bytes) -> Union[Pattern[bytes], bytes]: + def re_or_bytes(self, rule: bytes) -> Pattern[bytes] | bytes: """ Convert a /.../ type rule to a regex, otherwise return the string as-is diff --git a/src/cowrie/core/checkers.py b/src/cowrie/core/checkers.py index 89ad11ee2e..005f6d54ba 100644 --- a/src/cowrie/core/checkers.py +++ b/src/cowrie/core/checkers.py @@ -87,7 +87,7 @@ def checkPamUser(self, username, pamConversion, ip): return r.addCallback(self.cbCheckPamUser, username, ip) def cbCheckPamUser(self, responses, username, ip): - for (response, _) in responses: + for response, _ in responses: if self.checkUserPass(username, response, ip): return defer.succeed(username) return defer.fail(UnauthorizedLogin()) diff --git a/src/cowrie/core/config.py b/src/cowrie/core/config.py index 65857ae972..974f051250 100644 --- a/src/cowrie/core/config.py +++ b/src/cowrie/core/config.py @@ -11,8 +11,6 @@ from os import environ from os.path import abspath, dirname, exists, join -from typing import Union - def to_environ_key(key: str) -> str: return key.upper() @@ -36,7 +34,7 @@ def get(self, section: str, option: str, *, raw: bool = False, **kwargs) -> str: return super().get(section, option, raw=raw, **kwargs) -def readConfigFile(cfgfile: Union[list[str], str]) -> configparser.ConfigParser: +def readConfigFile(cfgfile: list[str] | str) -> configparser.ConfigParser: """ Read config files and return ConfigParser object diff --git a/src/cowrie/core/credentials.py b/src/cowrie/core/credentials.py index 88d75d5d5e..50821eb4d1 100644 --- a/src/cowrie/core/credentials.py +++ b/src/cowrie/core/credentials.py @@ -39,7 +39,7 @@ class IUsername(ICredentials): """ Encapsulate username only - @type username: C{str} + @type username: C{bytes} @ivar username: The username associated with these credentials. """ @@ -48,10 +48,10 @@ class IUsernamePasswordIP(IUsernamePassword): """ I encapsulate a username, a plaintext password and a source IP - @type username: C{str} + @type username: C{bytes} @ivar username: The username associated with these credentials. - @type password: C{str} + @type password: C{bytes} @ivar password: The password associated with these credentials. @type ip: C{str} @@ -71,16 +71,16 @@ class PluggableAuthenticationModulesIP: Twisted removed IPAM in 15, adding in Cowrie now """ - def __init__(self, username: str, pamConversion: Callable, ip: str) -> None: - self.username: str = username + def __init__(self, username: bytes, pamConversion: Callable, ip: str) -> None: + self.username: bytes = username self.pamConversion: Callable = pamConversion self.ip: str = ip @implementer(IUsername) class Username: - def __init__(self, username: str): - self.username: str = username + def __init__(self, username: bytes): + self.username: bytes = username @implementer(IUsernamePasswordIP) @@ -89,10 +89,10 @@ class UsernamePasswordIP: This credential interface also provides an IP address """ - def __init__(self, username: str, password: str, ip: str) -> None: - self.username: str = username - self.password: str = password + def __init__(self, username: bytes, password: bytes, ip: str) -> None: + self.username: bytes = username + self.password: bytes = password self.ip: str = ip - def checkPassword(self, password: str) -> bool: + def checkPassword(self, password: bytes) -> bool: return self.password == password diff --git a/src/cowrie/core/output.py b/src/cowrie/core/output.py index 9f88f649eb..d35830c36f 100644 --- a/src/cowrie/core/output.py +++ b/src/cowrie/core/output.py @@ -158,7 +158,6 @@ def emit(self, event: dict) -> None: - 'message' or 'format' """ sessionno: str - ev: dict # Ignore stdout and stderr in output plugins if "printed" in event: @@ -193,7 +192,7 @@ def emit(self, event: dict) -> None: if "format" in ev and ("message" not in ev or ev["message"] == ()): try: - ev["message"] = ev["format"] % ev + ev["message"] = ev["format"] % ev # type: ignore del ev["format"] except Exception: pass @@ -225,6 +224,9 @@ def emit(self, event: dict) -> None: sessionno = f"S{sshmatch.groups()[0]}" if sessionno == "0": return + else: + print(f"Can't determine sessionno: {ev!r}") # noqa: T201 + return if sessionno in self.ips: ev["src_ip"] = self.ips[sessionno] diff --git a/src/cowrie/insults/insults.py b/src/cowrie/insults/insults.py index 17fc9478e6..f51ab8ad28 100644 --- a/src/cowrie/insults/insults.py +++ b/src/cowrie/insults/insults.py @@ -170,7 +170,6 @@ def connectionLost(self, reason): if self.redirFiles: for rp in self.redirFiles: - rf = rp[0] if rp[1]: diff --git a/src/cowrie/output/abuseipdb.py b/src/cowrie/output/abuseipdb.py index f651b29aea..c02e8db3c0 100644 --- a/src/cowrie/output/abuseipdb.py +++ b/src/cowrie/output/abuseipdb.py @@ -133,17 +133,17 @@ def start(self): def stop(self): self.logbook.cleanup_and_dump_state(mode=1) - def write(self, ev): + def write(self, event): if self.logbook.sleeping: return - if ev["eventid"].rsplit(".", 1)[0] == "cowrie.login": + if event["eventid"].rsplit(".", 1)[0] == "cowrie.login": # If tolerance_attempts was set to 1 or 0, we don't need to # keep logs so our handling of the event is different than if > 1 if self.tolerance_attempts <= 1: - self.intolerant_observer(ev["src_ip"], time(), ev["username"]) + self.intolerant_observer(event["src_ip"], time(), event["username"]) else: - self.tolerant_observer(ev["src_ip"], time()) + self.tolerant_observer(event["src_ip"], time()) def intolerant_observer(self, ip, t, uname): # Checks if already reported; if yes, checks if we can rereport yet. @@ -358,7 +358,7 @@ def report_ip_single(self, ip, t, uname): "ip": ip, "categories": "18,22", "comment": "Cowrie Honeypot: Unauthorised SSH/Telnet login attempt " - 'with user "{}" at {}'.format(uname, t), + f'with user "{uname}" at {t}', } self.http_request(params) diff --git a/src/cowrie/output/crashreporter.py b/src/cowrie/output/crashreporter.py index 0973a14c9e..4a319e5e15 100644 --- a/src/cowrie/output/crashreporter.py +++ b/src/cowrie/output/crashreporter.py @@ -49,7 +49,7 @@ def stop(self): """ pass - def write(self, entry): + def write(self, event): """ events are done in emit() not in write() """ diff --git a/src/cowrie/output/csirtg.py b/src/cowrie/output/csirtg.py index ae9c0dd34a..edc8d1c1fd 100644 --- a/src/cowrie/output/csirtg.py +++ b/src/cowrie/output/csirtg.py @@ -39,12 +39,12 @@ def start(self): def stop(self): pass - def write(self, e): + def write(self, event): """ Only pass on connection events """ - if e["eventid"] == "cowrie.session.connect": - self.submitIp(e) + if event["eventid"] == "cowrie.session.connect": + self.submitIp(event) def submitIp(self, e): peerIP = e["src_ip"] diff --git a/src/cowrie/output/cuckoo.py b/src/cowrie/output/cuckoo.py index 9297f53396..ba33f2b63a 100644 --- a/src/cowrie/output/cuckoo.py +++ b/src/cowrie/output/cuckoo.py @@ -48,6 +48,7 @@ class Output(cowrie.core.output.Output): """ cuckoo output """ + api_user: str api_passwd: str url_base: bytes @@ -68,32 +69,32 @@ def stop(self): """ pass - def write(self, entry): - if entry["eventid"] == "cowrie.session.file_download": + def write(self, event): + if event["eventid"] == "cowrie.session.file_download": log.msg("Sending file to Cuckoo") - p = urlparse(entry["url"]).path + p = urlparse(event["url"]).path if p == "": - fileName = entry["shasum"] + fileName = event["shasum"] else: b = os.path.basename(p) if b == "": - fileName = entry["shasum"] + fileName = event["shasum"] else: fileName = b if ( self.cuckoo_force - or self.cuckoo_check_if_dup(os.path.basename(entry["outfile"])) is False + or self.cuckoo_check_if_dup(os.path.basename(event["outfile"])) is False ): - self.postfile(entry["outfile"], fileName) + self.postfile(event["outfile"], fileName) - elif entry["eventid"] == "cowrie.session.file_upload": + elif event["eventid"] == "cowrie.session.file_upload": if ( self.cuckoo_force - or self.cuckoo_check_if_dup(os.path.basename(entry["outfile"])) is False + or self.cuckoo_check_if_dup(os.path.basename(event["outfile"])) is False ): log.msg("Sending file to Cuckoo") - self.postfile(entry["outfile"], entry["filename"]) + self.postfile(event["outfile"], event["filename"]) def cuckoo_check_if_dup(self, sha256: str) -> bool: """ diff --git a/src/cowrie/output/datadog.py b/src/cowrie/output/datadog.py index 82de67d13e..a4ce8012bd 100644 --- a/src/cowrie/output/datadog.py +++ b/src/cowrie/output/datadog.py @@ -9,7 +9,6 @@ from io import BytesIO from twisted.internet import reactor -from twisted.internet.ssl import ClientContextFactory from twisted.python import log from twisted.web import client, http_headers from twisted.web.client import FileBodyProducer @@ -33,24 +32,25 @@ def start(self) -> None: self.service = CowrieConfig.get( "output_datadog", "service", fallback="honeypot" ) - self.hostname = CowrieConfig.get("output_datadog", "hostname", fallback=platform.node()) - contextFactory = WebClientContextFactory() - self.agent = client.Agent(reactor, contextFactory) + self.hostname = CowrieConfig.get( + "output_datadog", "hostname", fallback=platform.node() + ) + self.agent = client.Agent(reactor) def stop(self) -> None: pass - def write(self, logentry): - for i in list(logentry.keys()): + def write(self, event): + for i in list(event.keys()): # Remove twisted 15 legacy keys if i.startswith("log_"): - del logentry[i] + del event[i] message = [ { "ddsource": self.ddsource, "ddtags": self.ddtags, "hostname": self.hostname, - "message": json.dumps(logentry), + "message": json.dumps(event), "service": self.service, } ] @@ -65,8 +65,3 @@ def postentry(self, entry): headers = http_headers.Headers(base_headers) body = FileBodyProducer(BytesIO(json.dumps(entry).encode("utf8"))) self.agent.request(b"POST", self.url, headers, body) - - -class WebClientContextFactory(ClientContextFactory): - def getContext(self, hostname, port): - return ClientContextFactory.getContext(self) diff --git a/src/cowrie/output/discord.py b/src/cowrie/output/discord.py index fb8832516e..ec580f3a9e 100644 --- a/src/cowrie/output/discord.py +++ b/src/cowrie/output/discord.py @@ -8,7 +8,6 @@ from io import BytesIO from twisted.internet import reactor -from twisted.internet.ssl import ClientContextFactory from twisted.web import client, http_headers from twisted.web.client import FileBodyProducer @@ -19,21 +18,20 @@ class Output(cowrie.core.output.Output): def start(self) -> None: self.url = CowrieConfig.get("output_discord", "url").encode("utf8") - contextFactory = WebClientContextFactory() - self.agent = client.Agent(reactor, contextFactory) + self.agent = client.Agent(reactor) def stop(self) -> None: pass - def write(self, logentry): - webhook_message = "__New logentry__\n" + def write(self, event): + webhook_message = "__New event__\n" - for i in list(logentry.keys()): + for i in list(event.keys()): # Remove twisted 15 legacy keys if i.startswith("log_"): - del logentry[i] + del event[i] else: - webhook_message += f"{i}: `{logentry[i]}`\n" + webhook_message += f"{i}: `{event[i]}`\n" self.postentry({"content": webhook_message}) @@ -46,8 +44,3 @@ def postentry(self, entry): body = FileBodyProducer(BytesIO(json.dumps(entry).encode("utf8"))) self.agent.request(b"POST", self.url, headers, body) - - -class WebClientContextFactory(ClientContextFactory): - def getContext(self, hostname, port): - return ClientContextFactory.getContext(self) diff --git a/src/cowrie/output/dshield.py b/src/cowrie/output/dshield.py index b0777d391c..ee7d911f2e 100644 --- a/src/cowrie/output/dshield.py +++ b/src/cowrie/output/dshield.py @@ -26,6 +26,7 @@ class Output(cowrie.core.output.Output): """ dshield output """ + debug: bool = False userid: str batch_size: int @@ -41,20 +42,20 @@ def start(self): def stop(self): pass - def write(self, entry): + def write(self, event): if ( - entry["eventid"] == "cowrie.login.success" - or entry["eventid"] == "cowrie.login.failed" + event["eventid"] == "cowrie.login.success" + or event["eventid"] == "cowrie.login.failed" ): - date = dateutil.parser.parse(entry["timestamp"]) + date = dateutil.parser.parse(event["timestamp"]) self.batch.append( { "date": str(date.date()), "time": date.time().strftime("%H:%M:%S"), "timezone": time.strftime("%z"), - "source_ip": entry["src_ip"], - "user": entry["username"], - "password": entry["password"], + "source_ip": event["src_ip"], + "user": event["username"], + "password": event["password"], } ) diff --git a/src/cowrie/output/elasticsearch.py b/src/cowrie/output/elasticsearch.py index 2b00e38d18..77c76643a6 100644 --- a/src/cowrie/output/elasticsearch.py +++ b/src/cowrie/output/elasticsearch.py @@ -114,12 +114,12 @@ def check_geoip_pipeline(self): def stop(self): pass - def write(self, logentry): - for i in list(logentry.keys()): + def write(self, event): + for i in list(event.keys()): # remove twisted 15 legacy keys if i.startswith("log_"): - del logentry[i] + del event[i] self.es.index( - index=self.index, doc_type=self.type, body=logentry, pipeline=self.pipeline + index=self.index, doc_type=self.type, body=event, pipeline=self.pipeline ) diff --git a/src/cowrie/output/graylog.py b/src/cowrie/output/graylog.py index dfbc48e2c0..5f4841d221 100644 --- a/src/cowrie/output/graylog.py +++ b/src/cowrie/output/graylog.py @@ -4,14 +4,16 @@ from __future__ import annotations +from io import BytesIO import json import time -from io import BytesIO -from twisted.internet import reactor -from twisted.internet.ssl import ClientContextFactory +from zope.interface import implementer + +from twisted.internet import reactor, ssl from twisted.web import client, http_headers from twisted.web.client import FileBodyProducer +from twisted.web.iweb import IPolicyForHTTPS import cowrie.core.output from cowrie.core.config import CowrieConfig @@ -20,23 +22,23 @@ class Output(cowrie.core.output.Output): def start(self) -> None: self.url = CowrieConfig.get("output_graylog", "url").encode("utf8") - contextFactory = WebClientContextFactory() + contextFactory = WhitelistContextFactory() self.agent = client.Agent(reactor, contextFactory) def stop(self) -> None: pass - def write(self, logentry): - for i in list(logentry.keys()): + def write(self, event): + for i in list(event.keys()): # Remove twisted 15 legacy keys if i.startswith("log_"): - del logentry[i] + del event[i] gelf_message = { "version": "1.1", - "host": logentry["sensor"], + "host": event["sensor"], "timestamp": time.time(), - "short_message": json.dumps(logentry), + "short_message": json.dumps(event), "level": 1, } @@ -53,6 +55,7 @@ def postentry(self, entry): self.agent.request(b"POST", self.url, headers, body) -class WebClientContextFactory(ClientContextFactory): - def getContext(self, hostname, port): - return ClientContextFactory.getContext(self) +@implementer(IPolicyForHTTPS) +class WhitelistContextFactory: + def creatorForNetloc(self, hostname, port): + return ssl.CertificateOptions(verify=False) diff --git a/src/cowrie/output/greynoise.py b/src/cowrie/output/greynoise.py index a7f1b35d47..714adbc7e2 100644 --- a/src/cowrie/output/greynoise.py +++ b/src/cowrie/output/greynoise.py @@ -35,12 +35,12 @@ def stop(self): """ pass - def write(self, entry): - if entry["eventid"] == "cowrie.session.connect": - self.scanip(entry) + def write(self, event): + if event["eventid"] == "cowrie.session.connect": + self.scanip(event) @defer.inlineCallbacks - def scanip(self, entry): + def scanip(self, event): """ Scan IP against GreyNoise API """ @@ -49,19 +49,19 @@ def message(query): if query["noise"]: log.msg( eventid="cowrie.greynoise.result", - session=entry["session"], + session=event["session"], format=f"GreyNoise: {query['ip']} has been observed scanning the Internet. GreyNoise " f"classification is {query['classification']} and the believed owner is {query['name']}", ) if query["riot"]: log.msg( eventid="cowrie.greynoise.result", - session=entry["session"], + session=event["session"], format=f"GreyNoise: {query['ip']} belongs to a benign service or provider. " f"The owner is {query['name']}.", ) - gn_url = f"{GNAPI_URL}{entry['src_ip']}".encode() + gn_url = f"{GNAPI_URL}{event['src_ip']}".encode() headers = {"User-Agent": [COWRIE_USER_AGENT], "key": self.apiKey} try: @@ -91,4 +91,4 @@ def message(query): if j["message"] == "Success": message(j) else: - log.msg("GreyNoise: no results for for IP {}".format(entry["src_ip"])) + log.msg("GreyNoise: no results for for IP {}".format(event["src_ip"])) diff --git a/src/cowrie/output/hpfeeds3.py b/src/cowrie/output/hpfeeds3.py index e1f1335137..63ebacc82c 100644 --- a/src/cowrie/output/hpfeeds3.py +++ b/src/cowrie/output/hpfeeds3.py @@ -53,17 +53,17 @@ def start(self): def stop(self): self.client.stopService() - def write(self, entry): - session = entry["session"] - if entry["eventid"] == "cowrie.session.connect": + def write(self, event): + session = event["session"] + if event["eventid"] == "cowrie.session.connect": self.meta[session] = { "session": session, - "startTime": entry["timestamp"], + "startTime": event["timestamp"], "endTime": "", - "peerIP": entry["src_ip"], - "peerPort": entry["src_port"], - "hostIP": entry["dst_ip"], - "hostPort": entry["dst_port"], + "peerIP": event["src_ip"], + "peerPort": event["src_port"], + "hostIP": event["dst_ip"], + "hostPort": event["dst_port"], "loggedin": None, "credentials": [], "commands": [], @@ -72,47 +72,47 @@ def write(self, entry): "version": None, "ttylog": None, "hashes": set(), - "protocol": entry["protocol"], + "protocol": event["protocol"], } - elif entry["eventid"] == "cowrie.login.success": - u, p = entry["username"], entry["password"] + elif event["eventid"] == "cowrie.login.success": + u, p = event["username"], event["password"] self.meta[session]["loggedin"] = (u, p) - elif entry["eventid"] == "cowrie.login.failed": - u, p = entry["username"], entry["password"] + elif event["eventid"] == "cowrie.login.failed": + u, p = event["username"], event["password"] self.meta[session]["credentials"].append((u, p)) - elif entry["eventid"] == "cowrie.command.input": - c = entry["input"] + elif event["eventid"] == "cowrie.command.input": + c = event["input"] self.meta[session]["commands"].append(c) - elif entry["eventid"] == "cowrie.command.failed": - uc = entry["input"] + elif event["eventid"] == "cowrie.command.failed": + uc = event["input"] self.meta[session]["unknownCommands"].append(uc) - elif entry["eventid"] == "cowrie.session.file_download": - if "url" in entry: - url = entry["url"] + elif event["eventid"] == "cowrie.session.file_download": + if "url" in event: + url = event["url"] self.meta[session]["urls"].append(url) - self.meta[session]["hashes"].add(entry["shasum"]) + self.meta[session]["hashes"].add(event["shasum"]) - elif entry["eventid"] == "cowrie.session.file_upload": - self.meta[session]["hashes"].add(entry["shasum"]) + elif event["eventid"] == "cowrie.session.file_upload": + self.meta[session]["hashes"].add(event["shasum"]) - elif entry["eventid"] == "cowrie.client.version": - v = entry["version"] + elif event["eventid"] == "cowrie.client.version": + v = event["version"] self.meta[session]["version"] = v - elif entry["eventid"] == "cowrie.log.closed": - # entry["ttylog"] - with open(entry["ttylog"], "rb") as ttylog: + elif event["eventid"] == "cowrie.log.closed": + # event["ttylog"] + with open(event["ttylog"], "rb") as ttylog: self.meta[session]["ttylog"] = ttylog.read().hex() - elif entry["eventid"] == "cowrie.session.closed": + elif event["eventid"] == "cowrie.session.closed": meta = self.meta.pop(session, None) if meta: log.msg("publishing metadata to hpfeeds", logLevel=logging.DEBUG) - meta["endTime"] = entry["timestamp"] + meta["endTime"] = event["timestamp"] meta["hashes"] = list(meta["hashes"]) self.client.publish(self.channel, json.dumps(meta).encode("utf-8")) diff --git a/src/cowrie/output/influx.py b/src/cowrie/output/influx.py index d11f837e7f..ab633f5331 100644 --- a/src/cowrie/output/influx.py +++ b/src/cowrie/output/influx.py @@ -54,10 +54,8 @@ def start(self): match = re.search(r"^\d+[dhmw]{1}$", retention_policy_duration) if not match: log.msg( - ( - "output_influx: invalid retention policy." - "Using default '{}'.." - ).format(retention_policy_duration) + "output_influx: invalid retention policy." + f"Using default '{retention_policy_duration}'.." ) retention_policy_duration = retention_policy_duration_default else: @@ -102,18 +100,18 @@ def start(self): def stop(self): pass - def write(self, entry): + def write(self, event): if self.client is None: log.msg("output_influx: client object is not instantiated") return # event id - eventid = entry["eventid"] + eventid = event["eventid"] # measurement init m = { "measurement": eventid.replace(".", "_"), - "tags": {"session": entry["session"], "src_ip": entry["src_ip"]}, + "tags": {"session": event["session"], "src_ip": event["src_ip"]}, "fields": {"sensor": self.sensor}, } @@ -121,87 +119,87 @@ def write(self, entry): if eventid in ["cowrie.command.failed", "cowrie.command.input"]: m["fields"].update( { - "input": entry["input"], + "input": event["input"], } ) elif eventid == "cowrie.session.connect": m["fields"].update( { - "protocol": entry["protocol"], - "src_port": entry["src_port"], - "dst_port": entry["dst_port"], - "dst_ip": entry["dst_ip"], + "protocol": event["protocol"], + "src_port": event["src_port"], + "dst_port": event["dst_port"], + "dst_ip": event["dst_ip"], } ) elif eventid in ["cowrie.login.success", "cowrie.login.failed"]: m["fields"].update( { - "username": entry["username"], - "password": entry["password"], + "username": event["username"], + "password": event["password"], } ) elif eventid == "cowrie.session.file_download": m["fields"].update( { - "shasum": entry.get("shasum"), - "url": entry.get("url"), - "outfile": entry.get("outfile"), + "shasum": event.get("shasum"), + "url": event.get("url"), + "outfile": event.get("outfile"), } ) elif eventid == "cowrie.session.file_download.failed": - m["fields"].update({"url": entry.get("url")}) + m["fields"].update({"url": event.get("url")}) elif eventid == "cowrie.session.file_upload": m["fields"].update( { - "shasum": entry.get("shasum"), - "outfile": entry.get("outfile"), + "shasum": event.get("shasum"), + "outfile": event.get("outfile"), } ) elif eventid == "cowrie.session.closed": - m["fields"].update({"duration": entry["duration"]}) + m["fields"].update({"duration": event["duration"]}) elif eventid == "cowrie.client.version": m["fields"].update( { - "version": ",".join(entry["version"]), + "version": ",".join(event["version"]), } ) elif eventid == "cowrie.client.kex": m["fields"].update( { - "maccs": ",".join(entry["macCS"]), - "kexalgs": ",".join(entry["kexAlgs"]), - "keyalgs": ",".join(entry["keyAlgs"]), - "compcs": ",".join(entry["compCS"]), - "enccs": ",".join(entry["encCS"]), + "maccs": ",".join(event["macCS"]), + "kexalgs": ",".join(event["kexAlgs"]), + "keyalgs": ",".join(event["keyAlgs"]), + "compcs": ",".join(event["compCS"]), + "enccs": ",".join(event["encCS"]), } ) elif eventid == "cowrie.client.size": m["fields"].update( { - "height": entry["height"], - "width": entry["width"], + "height": event["height"], + "width": event["width"], } ) elif eventid == "cowrie.client.var": m["fields"].update( { - "name": entry["name"], - "value": entry["value"], + "name": event["name"], + "value": event["value"], } ) elif eventid == "cowrie.client.fingerprint": - m["fields"].update({"fingerprint": entry["fingerprint"]}) + m["fields"].update({"fingerprint": event["fingerprint"]}) # cowrie.direct-tcpip.data, cowrie.direct-tcpip.request # cowrie.log.closed @@ -215,6 +213,6 @@ def write(self, entry): if not result: log.msg( - "output_influx: error when writing '{}' measurement" - "in the db.".format(eventid) + f"output_influx: error when writing '{eventid}' measurement" + "in the db." ) diff --git a/src/cowrie/output/jsonlog.py b/src/cowrie/output/jsonlog.py index 46bd48c3f8..c93115747d 100644 --- a/src/cowrie/output/jsonlog.py +++ b/src/cowrie/output/jsonlog.py @@ -57,16 +57,16 @@ def start(self): def stop(self): self.outfile.flush() - def write(self, logentry): + def write(self, event): if self.epoch_timestamp: - logentry["epoch"] = int(logentry["time"] * 1000000 / 1000) - for i in list(logentry.keys()): + event["epoch"] = int(event["time"] * 1000000 / 1000) + for i in list(event.keys()): # Remove twisted 15 legacy keys if i.startswith("log_") or i == "time" or i == "system": - del logentry[i] + del event[i] try: - json.dump(logentry, self.outfile, separators=(",", ":")) + json.dump(event, self.outfile, separators=(",", ":")) self.outfile.write("\n") self.outfile.flush() except TypeError: - log.err("jsonlog: Can't serialize: '" + repr(logentry) + "'") + log.err("jsonlog: Can't serialize: '" + repr(event) + "'") diff --git a/src/cowrie/output/localsyslog.py b/src/cowrie/output/localsyslog.py index 0f367d11d9..26c31902b5 100644 --- a/src/cowrie/output/localsyslog.py +++ b/src/cowrie/output/localsyslog.py @@ -53,19 +53,19 @@ def start(self): def stop(self): pass - def write(self, logentry): - if "isError" not in logentry: - logentry["isError"] = False + def write(self, event): + if "isError" not in event: + event["isError"] = False if self.format == "cef": self.syslog.emit( { - "message": [cowrie.core.cef.formatCef(logentry)], + "message": [cowrie.core.cef.formatCef(event)], "isError": False, "system": "cowrie", } ) else: # message appears with additional spaces if message key is defined - logentry["message"] = [logentry["message"]] - self.syslog.emit(logentry) + event["message"] = [event["message"]] + self.syslog.emit(event) diff --git a/src/cowrie/output/malshare.py b/src/cowrie/output/malshare.py index 347d51e14c..c61ae27fbb 100644 --- a/src/cowrie/output/malshare.py +++ b/src/cowrie/output/malshare.py @@ -65,22 +65,22 @@ def stop(self): """ pass - def write(self, entry): - if entry["eventid"] == "cowrie.session.file_download": - p = urlparse(entry["url"]).path + def write(self, event): + if event["eventid"] == "cowrie.session.file_download": + p = urlparse(event["url"]).path if p == "": - fileName = entry["shasum"] + fileName = event["shasum"] else: b = os.path.basename(p) if b == "": - fileName = entry["shasum"] + fileName = event["shasum"] else: fileName = b - self.postfile(entry["outfile"], fileName) + self.postfile(event["outfile"], fileName) - elif entry["eventid"] == "cowrie.session.file_upload": - self.postfile(entry["outfile"], entry["filename"]) + elif event["eventid"] == "cowrie.session.file_upload": + self.postfile(event["outfile"], event["filename"]) def postfile(self, artifact, fileName): """ diff --git a/src/cowrie/output/misp.py b/src/cowrie/output/misp.py index 812fb9f75f..221d8a4821 100644 --- a/src/cowrie/output/misp.py +++ b/src/cowrie/output/misp.py @@ -63,22 +63,22 @@ def stop(self): """ pass - def write(self, entry): + def write(self, event): """ Push file download to MISP """ - if entry["eventid"] == "cowrie.session.file_download": - file_sha_attrib = self.find_attribute("sha256", entry["shasum"]) + if event["eventid"] == "cowrie.session.file_download": + file_sha_attrib = self.find_attribute("sha256", event["shasum"]) if file_sha_attrib: # file is known, add sighting! if self.debug: log.msg("File known, add sighting") - self.add_sighting(entry, file_sha_attrib) + self.add_sighting(event, file_sha_attrib) else: # file is unknown, new event with upload if self.debug: log.msg("File unknwon, add new event") - self.create_new_event(entry) + self.create_new_event(event) @ignore_warnings def find_attribute(self, attribute_type, searchterm): @@ -95,17 +95,17 @@ def find_attribute(self, attribute_type, searchterm): return None @ignore_warnings - def create_new_event(self, entry): + def create_new_event(self, event): attribute = MISPAttribute() attribute.type = "malware-sample" - attribute.value = entry["shasum"] - attribute.data = Path(entry["outfile"]) - attribute.comment = "File uploaded to Cowrie ({})".format(entry["sensor"]) + attribute.value = event["shasum"] + attribute.data = Path(event["outfile"]) + attribute.comment = "File uploaded to Cowrie ({})".format(event["sensor"]) attribute.expand = "binary" - if "url" in entry: + if "url" in event: attributeURL = MISPAttribute() attributeURL.type = "url" - attributeURL.value = entry["url"] + attributeURL.value = event["url"] attributeURL.to_ids = True else: attributeURL = MISPAttribute() @@ -113,12 +113,12 @@ def create_new_event(self, entry): attributeURL.value = "External upload" attributeIP = MISPAttribute() attributeIP.type = "ip-src" - attributeIP.value = entry["src_ip"] + attributeIP.value = event["src_ip"] attributeDT = MISPAttribute() attributeDT.type = "datetime" - attributeDT.value = entry["timestamp"] + attributeDT.value = event["timestamp"] event = MISPEvent() - event.info = "File uploaded to Cowrie ({})".format(entry["sensor"]) + event.info = "File uploaded to Cowrie ({})".format(event["sensor"]) event.add_tag("tlp:white") event.attributes = [attribute, attributeURL, attributeIP, attributeDT] event.run_expansions() @@ -129,7 +129,7 @@ def create_new_event(self, entry): log.msg(f"Event creation result: \n{result}") @ignore_warnings - def add_sighting(self, entry, attribute): + def add_sighting(self, event, attribute): sighting = MISPSighting() - sighting.source = "{} (Cowrie)".format(entry["sensor"]) + sighting.source = "{} (Cowrie)".format(event["sensor"]) self.misp_api.add_sighting(sighting, attribute) diff --git a/src/cowrie/output/mongodb.py b/src/cowrie/output/mongodb.py index 5a3a0d1c06..cbb4578c09 100644 --- a/src/cowrie/output/mongodb.py +++ b/src/cowrie/output/mongodb.py @@ -52,80 +52,80 @@ def start(self): def stop(self): self.mongo_client.close() - def write(self, entry): - for i in list(entry.keys()): + def write(self, event): + for i in list(event.keys()): # Remove twisted 15 legacy keys if i.startswith("log_"): - del entry[i] + del event[i] - eventid = entry["eventid"] + eventid = event["eventid"] if eventid == "cowrie.session.connect": # Check if sensor exists, else add it. doc = self.col_sensors.find_one({"sensor": self.sensor}) if not doc: - self.insert_one(self.col_sensors, entry) + self.insert_one(self.col_sensors, event) # Prep extra elements just to make django happy later on - entry["starttime"] = entry["timestamp"] - entry["endtime"] = None - entry["sshversion"] = None - entry["termsize"] = None + event["starttime"] = event["timestamp"] + event["endtime"] = None + event["sshversion"] = None + event["termsize"] = None log.msg("Session Created") - self.insert_one(self.col_sessions, entry) + self.insert_one(self.col_sessions, event) elif eventid in ["cowrie.login.success", "cowrie.login.failed"]: - self.insert_one(self.col_auth, entry) + self.insert_one(self.col_auth, event) elif eventid in ["cowrie.command.input", "cowrie.command.failed"]: - self.insert_one(self.col_input, entry) + self.insert_one(self.col_input, event) elif eventid == "cowrie.session.file_download": # ToDo add a config section and offer to store the file in the db - useful for central logging # we will add an option to set max size, if its 16mb or less we can store as normal, # If over 16 either fail or we just use gridfs both are simple enough. - self.insert_one(self.col_downloads, entry) + self.insert_one(self.col_downloads, event) elif eventid == "cowrie.client.version": - doc = self.col_sessions.find_one({"session": entry["session"]}) + doc = self.col_sessions.find_one({"session": event["session"]}) if doc: - doc["sshversion"] = entry["version"] - self.update_one(self.col_sessions, entry["session"], doc) + doc["sshversion"] = event["version"] + self.update_one(self.col_sessions, event["session"], doc) else: pass elif eventid == "cowrie.client.size": - doc = self.col_sessions.find_one({"session": entry["session"]}) + doc = self.col_sessions.find_one({"session": event["session"]}) if doc: - doc["termsize"] = "{}x{}".format(entry["width"], entry["height"]) - self.update_one(self.col_sessions, entry["session"], doc) + doc["termsize"] = "{}x{}".format(event["width"], event["height"]) + self.update_one(self.col_sessions, event["session"], doc) else: pass elif eventid == "cowrie.session.closed": - doc = self.col_sessions.find_one({"session": entry["session"]}) + doc = self.col_sessions.find_one({"session": event["session"]}) if doc: - doc["endtime"] = entry["timestamp"] - self.update_one(self.col_sessions, entry["session"], doc) + doc["endtime"] = event["timestamp"] + self.update_one(self.col_sessions, event["session"], doc) else: pass elif eventid == "cowrie.log.closed": # ToDo Compress to opimise the space and if your sending to remote db - with open(entry["ttylog"]) as ttylog: - entry["ttylogpath"] = entry["ttylog"] - entry["ttylog"] = ttylog.read().encode().hex() - self.insert_one(self.col_ttylog, entry) + with open(event["ttylog"]) as ttylog: + event["ttylogpath"] = event["ttylog"] + event["ttylog"] = ttylog.read().encode().hex() + self.insert_one(self.col_ttylog, event) elif eventid == "cowrie.client.fingerprint": - self.insert_one(self.col_keyfingerprints, entry) + self.insert_one(self.col_keyfingerprints, event) elif eventid == "cowrie.direct-tcpip.request": - self.insert_one(self.col_ipforwards, entry) + self.insert_one(self.col_ipforwards, event) elif eventid == "cowrie.direct-tcpip.data": - self.insert_one(self.col_ipforwardsdata, entry) + self.insert_one(self.col_ipforwardsdata, event) # Catch any other event types else: - self.insert_one(self.col_event, entry) + self.insert_one(self.col_event, event) diff --git a/src/cowrie/output/mysql.py b/src/cowrie/output/mysql.py index 9984b91c0a..36349dd821 100644 --- a/src/cowrie/output/mysql.py +++ b/src/cowrie/output/mysql.py @@ -107,8 +107,8 @@ def simpleQuery(self, sql, args): d.addErrback(self.sqlerror) @defer.inlineCallbacks - def write(self, entry): - if entry["eventid"] == "cowrie.session.connect": + def write(self, event): + if event["eventid"] == "cowrie.session.connect": if self.debug: log.msg( f"output_mysql: SELECT `id` FROM `sensors` WHERE `ip` = '{self.sensor}'" @@ -133,99 +133,99 @@ def write(self, entry): self.simpleQuery( "INSERT INTO `sessions` (`id`, `starttime`, `sensor`, `ip`) " "VALUES (%s, FROM_UNIXTIME(%s), %s, %s)", - (entry["session"], entry["time"], sensorid, entry["src_ip"]), + (event["session"], event["time"], sensorid, event["src_ip"]), ) - elif entry["eventid"] == "cowrie.login.success": + elif event["eventid"] == "cowrie.login.success": self.simpleQuery( "INSERT INTO `auth` (`session`, `success`, `username`, `password`, `timestamp`) " "VALUES (%s, %s, %s, %s, FROM_UNIXTIME(%s))", ( - entry["session"], + event["session"], 1, - entry["username"], - entry["password"], - entry["time"], + event["username"], + event["password"], + event["time"], ), ) - elif entry["eventid"] == "cowrie.login.failed": + elif event["eventid"] == "cowrie.login.failed": self.simpleQuery( "INSERT INTO `auth` (`session`, `success`, `username`, `password`, `timestamp`) " "VALUES (%s, %s, %s, %s, FROM_UNIXTIME(%s))", ( - entry["session"], + event["session"], 0, - entry["username"], - entry["password"], - entry["time"], + event["username"], + event["password"], + event["time"], ), ) - elif entry["eventid"] == "cowrie.session.params": + elif event["eventid"] == "cowrie.session.params": self.simpleQuery( "INSERT INTO `params` (`session`, `arch`) VALUES (%s, %s)", - (entry["session"], entry["arch"]), + (event["session"], event["arch"]), ) - elif entry["eventid"] == "cowrie.command.input": + elif event["eventid"] == "cowrie.command.input": self.simpleQuery( "INSERT INTO `input` (`session`, `timestamp`, `success`, `input`) " "VALUES (%s, FROM_UNIXTIME(%s), %s , %s)", - (entry["session"], entry["time"], 1, entry["input"]), + (event["session"], event["time"], 1, event["input"]), ) - elif entry["eventid"] == "cowrie.command.failed": + elif event["eventid"] == "cowrie.command.failed": self.simpleQuery( "INSERT INTO `input` (`session`, `timestamp`, `success`, `input`) " "VALUES (%s, FROM_UNIXTIME(%s), %s , %s)", - (entry["session"], entry["time"], 0, entry["input"]), + (event["session"], event["time"], 0, event["input"]), ) - elif entry["eventid"] == "cowrie.session.file_download": + elif event["eventid"] == "cowrie.session.file_download": self.simpleQuery( "INSERT INTO `downloads` (`session`, `timestamp`, `url`, `outfile`, `shasum`) " "VALUES (%s, FROM_UNIXTIME(%s), %s, %s, %s)", ( - entry["session"], - entry["time"], - entry.get("url", ""), - entry["outfile"], - entry["shasum"], + event["session"], + event["time"], + event.get("url", ""), + event["outfile"], + event["shasum"], ), ) - elif entry["eventid"] == "cowrie.session.file_download.failed": + elif event["eventid"] == "cowrie.session.file_download.failed": self.simpleQuery( "INSERT INTO `downloads` (`session`, `timestamp`, `url`, `outfile`, `shasum`) " "VALUES (%s, FROM_UNIXTIME(%s), %s, %s, %s)", - (entry["session"], entry["time"], entry.get("url", ""), "NULL", "NULL"), + (event["session"], event["time"], event.get("url", ""), "NULL", "NULL"), ) - elif entry["eventid"] == "cowrie.session.file_upload": + elif event["eventid"] == "cowrie.session.file_upload": self.simpleQuery( "INSERT INTO `downloads` (`session`, `timestamp`, `url`, `outfile`, `shasum`) " "VALUES (%s, FROM_UNIXTIME(%s), %s, %s, %s)", ( - entry["session"], - entry["time"], + event["session"], + event["time"], "", - entry["outfile"], - entry["shasum"], + event["outfile"], + event["shasum"], ), ) - elif entry["eventid"] == "cowrie.session.input": + elif event["eventid"] == "cowrie.session.input": self.simpleQuery( "INSERT INTO `input` (`session`, `timestamp`, `realm`, `input`) " "VALUES (%s, FROM_UNIXTIME(%s), %s , %s)", - (entry["session"], entry["time"], entry["realm"], entry["input"]), + (event["session"], event["time"], event["realm"], event["input"]), ) - elif entry["eventid"] == "cowrie.client.version": + elif event["eventid"] == "cowrie.client.version": r = yield self.db.runQuery( "SELECT `id` FROM `clients` WHERE `version` = %s", - (entry["version"],), + (event["version"],), ) if r: @@ -233,60 +233,60 @@ def write(self, entry): else: yield self.db.runQuery( "INSERT INTO `clients` (`version`) VALUES (%s)", - (entry["version"],), + (event["version"],), ) r = yield self.db.runQuery("SELECT LAST_INSERT_ID()") id = int(r[0][0]) self.simpleQuery( "UPDATE `sessions` SET `client` = %s WHERE `id` = %s", - (id, entry["session"]), + (id, event["session"]), ) - elif entry["eventid"] == "cowrie.client.size": + elif event["eventid"] == "cowrie.client.size": self.simpleQuery( "UPDATE `sessions` SET `termsize` = %s WHERE `id` = %s", - ("{}x{}".format(entry["width"], entry["height"]), entry["session"]), + ("{}x{}".format(event["width"], event["height"]), event["session"]), ) - elif entry["eventid"] == "cowrie.session.closed": + elif event["eventid"] == "cowrie.session.closed": self.simpleQuery( "UPDATE `sessions` " "SET `endtime` = FROM_UNIXTIME(%s) " "WHERE `id` = %s", - (entry["time"], entry["session"]), + (event["time"], event["session"]), ) - elif entry["eventid"] == "cowrie.log.closed": + elif event["eventid"] == "cowrie.log.closed": self.simpleQuery( "INSERT INTO `ttylog` (`session`, `ttylog`, `size`) " "VALUES (%s, %s, %s)", - (entry["session"], entry["ttylog"], entry["size"]), + (event["session"], event["ttylog"], event["size"]), ) - elif entry["eventid"] == "cowrie.client.fingerprint": + elif event["eventid"] == "cowrie.client.fingerprint": self.simpleQuery( "INSERT INTO `keyfingerprints` (`session`, `username`, `fingerprint`) " "VALUES (%s, %s, %s)", - (entry["session"], entry["username"], entry["fingerprint"]), + (event["session"], event["username"], event["fingerprint"]), ) - elif entry["eventid"] == "cowrie.direct-tcpip.request": + elif event["eventid"] == "cowrie.direct-tcpip.request": self.simpleQuery( "INSERT INTO `ipforwards` (`session`, `timestamp`, `dst_ip`, `dst_port`) " "VALUES (%s, FROM_UNIXTIME(%s), %s, %s)", - (entry["session"], entry["time"], entry["dst_ip"], entry["dst_port"]), + (event["session"], event["time"], event["dst_ip"], event["dst_port"]), ) - elif entry["eventid"] == "cowrie.direct-tcpip.data": + elif event["eventid"] == "cowrie.direct-tcpip.data": self.simpleQuery( "INSERT INTO `ipforwardsdata` (`session`, `timestamp`, `dst_ip`, `dst_port`, `data`) " "VALUES (%s, FROM_UNIXTIME(%s), %s, %s, %s)", ( - entry["session"], - entry["time"], - entry["dst_ip"], - entry["dst_port"], - entry["data"], + event["session"], + event["time"], + event["dst_ip"], + event["dst_port"], + event["data"], ), ) diff --git a/src/cowrie/output/oraclecloud.py b/src/cowrie/output/oraclecloud.py new file mode 100644 index 0000000000..b6863f9d22 --- /dev/null +++ b/src/cowrie/output/oraclecloud.py @@ -0,0 +1,115 @@ +from __future__ import annotations + +import datetime +import json +import secrets +import string + +import oci + +from twisted.python import log + +import cowrie.core.output +from cowrie.core.config import CowrieConfig + + +class Output(cowrie.core.output.Output): + """ + Oracle Cloud output + """ + + def generate_random_log_id(self): + charset = string.ascii_letters + string.digits + random_log_id = "".join(secrets.choice(charset) for _ in range(32)) + return f"cowrielog-{random_log_id}" + + def sendLogs(self, event): + log_id = self.generate_random_log_id() + # Initialize service client with default config file + current_time = datetime.datetime.utcnow() + self.log_ocid = CowrieConfig.get("output_oraclecloud", "log_ocid") + self.hostname = CowrieConfig.get("honeypot", "hostname") + + try: + # Send the request to service, some parameters are not required, see API + # doc for more info + self.loggingingestion_client.put_logs( + log_id=self.log_ocid, + put_logs_details=oci.loggingingestion.models.PutLogsDetails( + specversion="1.0", + log_entry_batches=[ + oci.loggingingestion.models.LogEntryBatch( + entries=[ + oci.loggingingestion.models.LogEntry( + data=event, + id=log_id, + time=current_time.strftime("%Y-%m-%dT%H:%M:%S.%fZ"), + ) + ], + source=self.hostname, + type="cowrie", + ) + ], + ), + timestamp_opc_agent_processing=current_time.strftime( + "%Y-%m-%dT%H:%M:%S.%fZ" + ), + ) + except oci.exceptions.ServiceError as ex: + log.err( + f"Oracle Cloud plugin Error: {ex.message}\n" + + f"Oracle Cloud plugin Status Code: {ex.status}\n" + ) + except Exception as ex: + log.err(f"Oracle Cloud plugin Error: {ex}") + raise + + def start(self): + """ + Initialize Oracle Cloud LoggingClient with user or instance principal authentication + """ + authtype = CowrieConfig.get("output_oraclecloud", "authtype") + + if authtype == "instance_principals": + signer = oci.auth.signers.InstancePrincipalsSecurityTokenSigner() + + # In the base case, configuration does not need to be provided as the region and tenancy are obtained from the InstancePrincipalsSecurityTokenSigner + # identity_client = oci.identity.IdentityClient(config={}, signer=signer) + self.loggingingestion_client = oci.loggingingestion.LoggingClient( + config={}, signer=signer + ) + + elif authtype == "user_principals": + tenancy_ocid = CowrieConfig.get("output_oraclecloud", "tenancy_ocid") + user_ocid = CowrieConfig.get("output_oraclecloud", "user_ocid") + region = CowrieConfig.get("output_oraclecloud", "region") + fingerprint = CowrieConfig.get("output_oraclecloud", "fingerprint") + keyfile = CowrieConfig.get("output_oraclecloud", "keyfile") + + config_with_key_content = { + "user": user_ocid, + "key_file": keyfile, + "fingerprint": fingerprint, + "tenancy": tenancy_ocid, + "region": region, + } + oci.config.validate_config(config_with_key_content) + self.loggingingestion_client = oci.loggingingestion.LoggingClient( + config_with_key_content + ) + else: + raise ValueError("Invalid authentication type") + + def stop(self): + pass + + def write(self, event): + """ + Push to Oracle Cloud put_logs + """ + # Add the entry to redis + for i in list(event.keys()): + # Remove twisted 15 legacy keys + if i.startswith("log_"): + del event[i] + self.sendLogs(json.dumps(event)) diff --git a/src/cowrie/output/redis.py b/src/cowrie/output/redis.py index 07dd4deb67..e042747ddb 100644 --- a/src/cowrie/output/redis.py +++ b/src/cowrie/output/redis.py @@ -50,13 +50,13 @@ def start(self): def stop(self): pass - def write(self, logentry): + def write(self, event): """ Push to redis """ # Add the entry to redis - for i in list(logentry.keys()): + for i in list(event.keys()): # Remove twisted 15 legacy keys if i.startswith("log_"): - del logentry[i] - self.send_method(self.redis, self.keyname, json.dumps(logentry)) + del event[i] + self.send_method(self.redis, self.keyname, json.dumps(event)) diff --git a/src/cowrie/output/rethinkdblog.py b/src/cowrie/output/rethinkdblog.py index dff908ce92..fe5d18487d 100644 --- a/src/cowrie/output/rethinkdblog.py +++ b/src/cowrie/output/rethinkdblog.py @@ -16,7 +16,6 @@ def iso8601_to_timestamp(value): class Output(cowrie.core.output.Output): - # noinspection PyAttributeOutsideInit def start(self): self.host = CowrieConfig.get(RETHINK_DB_SEGMENT, "host") @@ -36,13 +35,13 @@ def start(self): def stop(self): self.connection.close() - def write(self, logentry): - for i in list(logentry.keys()): + def write(self, event): + for i in list(event.keys()): # remove twisted 15 legacy keys if i.startswith("log_"): - del logentry[i] + del event[i] - if "timestamp" in logentry: - logentry["timestamp"] = iso8601_to_timestamp(logentry["timestamp"]) + if "timestamp" in event: + event["timestamp"] = iso8601_to_timestamp(event["timestamp"]) - r.table(self.table).insert(logentry).run(self.connection) + r.table(self.table).insert(event).run(self.connection) diff --git a/src/cowrie/output/reversedns.py b/src/cowrie/output/reversedns.py index 12468c3d68..714b20209e 100644 --- a/src/cowrie/output/reversedns.py +++ b/src/cowrie/output/reversedns.py @@ -16,7 +16,7 @@ class Output(cowrie.core.output.Output): Output plugin used for reverse DNS lookup """ - timeout: list[int] = [3] + timeout: list[int] def start(self): """ @@ -30,9 +30,9 @@ def stop(self): """ pass - def write(self, entry): + def write(self, event): """ - Process log entry + Process log event """ def processConnect(result): @@ -44,10 +44,10 @@ def processConnect(result): payload = result[0][0].payload log.msg( eventid="cowrie.reversedns.connect", - session=entry["session"], + session=event["session"], format="reversedns: PTR record for IP %(src_ip)s is %(ptr)s" " ttl=%(ttl)i", - src_ip=entry["src_ip"], + src_ip=event["src_ip"], ptr=str(payload.name), ttl=payload.ttl, ) @@ -61,10 +61,10 @@ def processForward(result): payload = result[0][0].payload log.msg( eventid="cowrie.reversedns.forward", - session=entry["session"], + session=event["session"], format="reversedns: PTR record for IP %(dst_ip)s is %(ptr)s" " ttl=%(ttl)i", - dst_ip=entry["dst_ip"], + dst_ip=event["dst_ip"], ptr=str(payload.name), ttl=payload.ttl, ) @@ -79,13 +79,13 @@ def cbError(failure): log.msg("reversedns: Error in DNS lookup") failure.printTraceback() - if entry["eventid"] == "cowrie.session.connect": - d = self.reversedns(entry["src_ip"]) + if event["eventid"] == "cowrie.session.connect": + d = self.reversedns(event["src_ip"]) if d is not None: d.addCallback(processConnect) d.addErrback(cbError) - elif entry["eventid"] == "cowrie.direct-tcpip.request": - d = self.reversedns(entry["dst_ip"]) + elif event["eventid"] == "cowrie.direct-tcpip.request": + d = self.reversedns(event["dst_ip"]) if d is not None: d.addCallback(processForward) d.addErrback(cbError) diff --git a/src/cowrie/output/s3.py b/src/cowrie/output/s3.py index e4ff171e31..f03820215e 100644 --- a/src/cowrie/output/s3.py +++ b/src/cowrie/output/s3.py @@ -51,12 +51,12 @@ def start(self) -> None: def stop(self) -> None: pass - def write(self, entry: dict[str, Any]) -> None: - if entry["eventid"] == "cowrie.session.file_download": - self.upload(entry["shasum"], entry["outfile"]) + def write(self, event: dict[str, Any]) -> None: + if event["eventid"] == "cowrie.session.file_download": + self.upload(event["shasum"], event["outfile"]) - elif entry["eventid"] == "cowrie.session.file_upload": - self.upload(entry["shasum"], entry["outfile"]) + elif event["eventid"] == "cowrie.session.file_upload": + self.upload(event["shasum"], event["outfile"]) @defer.inlineCallbacks def _object_exists_remote(self, shasum): diff --git a/src/cowrie/output/slack.py b/src/cowrie/output/slack.py index 6eb82b0b84..b61b2d37ac 100644 --- a/src/cowrie/output/slack.py +++ b/src/cowrie/output/slack.py @@ -49,17 +49,17 @@ def start(self): def stop(self): pass - def write(self, logentry): - for i in list(logentry.keys()): + def write(self, event): + for i in list(event.keys()): # Remove twisted 15 legacy keys if i.startswith("log_"): - del logentry[i] + del event[i] self.sc = WebClient(self.slack_token) self.sc.chat_postMessage( channel=self.slack_channel, text="{} {}".format( time.strftime("%Y-%m-%d %H:%M:%S"), - json.dumps(logentry, indent=4, sort_keys=True), + json.dumps(event, indent=4, sort_keys=True), ), ) diff --git a/src/cowrie/output/socketlog.py b/src/cowrie/output/socketlog.py index 921174b97f..f10fb5b007 100644 --- a/src/cowrie/output/socketlog.py +++ b/src/cowrie/output/socketlog.py @@ -24,13 +24,13 @@ def start(self): def stop(self): self.sock.close() - def write(self, logentry): - for i in list(logentry.keys()): + def write(self, event): + for i in list(event.keys()): # Remove twisted 15 legacy keys if i.startswith("log_"): - del logentry[i] + del event[i] - message = json.dumps(logentry) + "\n" + message = json.dumps(event) + "\n" try: self.sock.sendall(message.encode()) diff --git a/src/cowrie/output/splunk.py b/src/cowrie/output/splunk.py index a31fe2144d..7b17313192 100644 --- a/src/cowrie/output/splunk.py +++ b/src/cowrie/output/splunk.py @@ -12,11 +12,13 @@ from io import BytesIO from typing import Any -from twisted.internet import reactor -from twisted.internet.ssl import ClientContextFactory +from zope.interface import implementer + +from twisted.internet import reactor, ssl from twisted.python import log from twisted.web import client, http_headers from twisted.web.client import FileBodyProducer +from twisted.web.iweb import IPolicyForHTTPS import cowrie.core.output from cowrie.core.config import CowrieConfig @@ -34,22 +36,23 @@ class Output(cowrie.core.output.Output): def start(self) -> None: self.token = CowrieConfig.get("output_splunk", "token") self.url = CowrieConfig.get("output_splunk", "url").encode("utf8") - self.index = CowrieConfig.get("output_splunk", "index", fallback=None) - self.source = CowrieConfig.get("output_splunk", "source", fallback=None) - self.sourcetype = CowrieConfig.get("output_splunk", "sourcetype", fallback=None) + self.index = CowrieConfig.get("output_splunk", "index", fallback="main") + self.source = CowrieConfig.get("output_splunk", "source", fallback="cowrie") + self.sourcetype = CowrieConfig.get( + "output_splunk", "sourcetype", fallback="cowrie" + ) self.host = CowrieConfig.get("output_splunk", "host", fallback=None) - contextFactory = WebClientContextFactory() - # contextFactory.method = TLSv1_METHOD + contextFactory = WhitelistContextFactory() self.agent = client.Agent(reactor, contextFactory) def stop(self) -> None: pass - def write(self, logentry): - for i in list(logentry.keys()): + def write(self, event): + for i in list(event.keys()): # Remove twisted 15 legacy keys if i.startswith("log_"): - del logentry[i] + del event[i] splunkentry = {} if self.index: @@ -61,8 +64,8 @@ def write(self, logentry): if self.host: splunkentry["host"] = self.host else: - splunkentry["host"] = logentry["sensor"] - splunkentry["event"] = logentry + splunkentry["host"] = event["sensor"] + splunkentry["event"] = event self.postentry(splunkentry) def postentry(self, entry): @@ -111,6 +114,7 @@ def processResult(result): return d -class WebClientContextFactory(ClientContextFactory): - def getContext(self, hostname, port): - return ClientContextFactory.getContext(self) +@implementer(IPolicyForHTTPS) +class WhitelistContextFactory: + def creatorForNetloc(self, hostname, port): + return ssl.CertificateOptions(verify=False) diff --git a/src/cowrie/output/sqlite.py b/src/cowrie/output/sqlite.py index 0b92dfa77e..43927118fe 100644 --- a/src/cowrie/output/sqlite.py +++ b/src/cowrie/output/sqlite.py @@ -51,8 +51,8 @@ def simpleQuery(self, sql, args): d.addErrback(self.sqlerror) @defer.inlineCallbacks - def write(self, entry): - if entry["eventid"] == "cowrie.session.connect": + def write(self, event): + if event["eventid"] == "cowrie.session.connect": r = yield self.db.runQuery( "SELECT `id` FROM `sensors` " "WHERE `ip` = ?", (self.sensor,) ) @@ -69,78 +69,78 @@ def write(self, entry): self.simpleQuery( "INSERT INTO `sessions` (`id`, `starttime`, `sensor`, `ip`) " "VALUES (?, ?, ?, ?)", - (entry["session"], entry["timestamp"], sensorid, entry["src_ip"]), + (event["session"], event["timestamp"], sensorid, event["src_ip"]), ) - elif entry["eventid"] == "cowrie.login.success": + elif event["eventid"] == "cowrie.login.success": self.simpleQuery( "INSERT INTO `auth` (`session`, `success`, `username`, `password`, `timestamp`) " "VALUES (?, ?, ?, ?, ?)", ( - entry["session"], + event["session"], 1, - entry["username"], - entry["password"], - entry["timestamp"], + event["username"], + event["password"], + event["timestamp"], ), ) - elif entry["eventid"] == "cowrie.login.failed": + elif event["eventid"] == "cowrie.login.failed": self.simpleQuery( "INSERT INTO `auth` (`session`, `success`, `username`, `password`, `timestamp`) " "VALUES (?, ?, ?, ?, ?)", ( - entry["session"], + event["session"], 0, - entry["username"], - entry["password"], - entry["timestamp"], + event["username"], + event["password"], + event["timestamp"], ), ) - elif entry["eventid"] == "cowrie.command.input": + elif event["eventid"] == "cowrie.command.input": self.simpleQuery( "INSERT INTO `input` (`session`, `timestamp`, `success`, `input`) " "VALUES (?, ?, ?, ?)", - (entry["session"], entry["timestamp"], 1, entry["input"]), + (event["session"], event["timestamp"], 1, event["input"]), ) - elif entry["eventid"] == "cowrie.command.failed": + elif event["eventid"] == "cowrie.command.failed": self.simpleQuery( "INSERT INTO `input` (`session`, `timestamp`, `success`, `input`) " "VALUES (?, ?, ?, ?)", - (entry["session"], entry["timestamp"], 0, entry["input"]), + (event["session"], event["timestamp"], 0, event["input"]), ) - elif entry["eventid"] == "cowrie.session.params": + elif event["eventid"] == "cowrie.session.params": self.simpleQuery( "INSERT INTO `params` (`session`, `arch`) " "VALUES (?, ?)", - (entry["session"], entry["arch"]), + (event["session"], event["arch"]), ) - elif entry["eventid"] == "cowrie.session.file_download": + elif event["eventid"] == "cowrie.session.file_download": self.simpleQuery( "INSERT INTO `downloads` (`session`, `timestamp`, `url`, `outfile`, `shasum`) " "VALUES (?, ?, ?, ?, ?)", ( - entry["session"], - entry["timestamp"], - entry["url"], - entry["outfile"], - entry["shasum"], + event["session"], + event["timestamp"], + event["url"], + event["outfile"], + event["shasum"], ), ) - elif entry["eventid"] == "cowrie.session.file_download.failed": + elif event["eventid"] == "cowrie.session.file_download.failed": self.simpleQuery( "INSERT INTO `downloads` (`session`, `timestamp`, `url`, `outfile`, `shasum`) " "VALUES (?, ?, ?, ?, ?)", - (entry["session"], entry["timestamp"], entry["url"], "NULL", "NULL"), + (event["session"], event["timestamp"], event["url"], "NULL", "NULL"), ) - elif entry["eventid"] == "cowrie.client.version": + elif event["eventid"] == "cowrie.client.version": r = yield self.db.runQuery( - "SELECT `id` FROM `clients` " "WHERE `version` = ?", (entry["version"],) + "SELECT `id` FROM `clients` " "WHERE `version` = ?", (event["version"],) ) if r and r[0][0]: @@ -148,63 +148,63 @@ def write(self, entry): else: yield self.db.runQuery( "INSERT INTO `clients` (`version`) " "VALUES (?)", - (entry["version"],), + (event["version"],), ) r = yield self.db.runQuery("SELECT LAST_INSERT_ROWID()") id = int(r[0][0]) self.simpleQuery( "UPDATE `sessions` " "SET `client` = ? " "WHERE `id` = ?", - (id, entry["session"]), + (id, event["session"]), ) - elif entry["eventid"] == "cowrie.client.size": + elif event["eventid"] == "cowrie.client.size": self.simpleQuery( "UPDATE `sessions` " "SET `termsize` = ? " "WHERE `id` = ?", - ("{}x{}".format(entry["width"], entry["height"]), entry["session"]), + ("{}x{}".format(event["width"], event["height"]), event["session"]), ) - elif entry["eventid"] == "cowrie.session.closed": + elif event["eventid"] == "cowrie.session.closed": self.simpleQuery( "UPDATE `sessions` " "SET `endtime` = ? " "WHERE `id` = ?", - (entry["timestamp"], entry["session"]), + (event["timestamp"], event["session"]), ) - elif entry["eventid"] == "cowrie.log.closed": + elif event["eventid"] == "cowrie.log.closed": self.simpleQuery( "INSERT INTO `ttylog` (`session`, `ttylog`, `size`) " "VALUES (?, ?, ?)", - (entry["session"], entry["ttylog"], entry["size"]), + (event["session"], event["ttylog"], event["size"]), ) - elif entry["eventid"] == "cowrie.client.fingerprint": + elif event["eventid"] == "cowrie.client.fingerprint": self.simpleQuery( "INSERT INTO `keyfingerprints` (`session`, `username`, `fingerprint`) " "VALUES (?, ?, ?)", - (entry["session"], entry["username"], entry["fingerprint"]), + (event["session"], event["username"], event["fingerprint"]), ) - elif entry["eventid"] == "cowrie.direct-tcpip.request": + elif event["eventid"] == "cowrie.direct-tcpip.request": self.simpleQuery( "INSERT INTO `ipforwards` (`session`, `timestamp`, `dst_ip`, `dst_port`) " "VALUES (?, ?, ?, ?)", ( - entry["session"], - entry["timestamp"], - entry["dst_ip"], - entry["dst_port"], + event["session"], + event["timestamp"], + event["dst_ip"], + event["dst_port"], ), ) - elif entry["eventid"] == "cowrie.direct-tcpip.data": + elif event["eventid"] == "cowrie.direct-tcpip.data": self.simpleQuery( "INSERT INTO `ipforwardsdata` (`session`, `timestamp`, `dst_ip`, `dst_port`, `data`) " "VALUES (?, ?, ?, ?, ?)", ( - entry["session"], - entry["timestamp"], - entry["dst_ip"], - entry["dst_port"], - entry["data"], + event["session"], + event["timestamp"], + event["dst_ip"], + event["dst_port"], + event["data"], ), ) diff --git a/src/cowrie/output/telegram.py b/src/cowrie/output/telegram.py index a72345fbf2..267017069c 100644 --- a/src/cowrie/output/telegram.py +++ b/src/cowrie/output/telegram.py @@ -18,35 +18,35 @@ def start(self): def stop(self): pass - def write(self, logentry): - for i in list(logentry.keys()): + def write(self, event): + for i in list(event.keys()): # remove twisted 15 legacy keys if i.startswith("log_"): - del logentry[i] + del event[i] logon_type = "" # Prepare logon type - if "HoneyPotSSHTransport" in (logentry["system"].split(","))[0]: + if "HoneyPotSSHTransport" in (event["system"].split(","))[0]: logon_type = "SSH" - elif "CowrieTelnetTransport" in (logentry["system"].split(","))[0]: + elif "CowrieTelnetTransport" in (event["system"].split(","))[0]: logon_type = "Telnet" # Prepare base message - msgtxt = "[Cowrie " + logentry["sensor"] + "]" - msgtxt += "\nEvent: " + logentry["eventid"] + msgtxt = "[Cowrie " + event["sensor"] + "]" + msgtxt += "\nEvent: " + event["eventid"] msgtxt += "\nLogon type: " + logon_type - msgtxt += "\nSource: " + logentry["src_ip"] + "" - msgtxt += "\nSession: " + logentry["session"] + "" + msgtxt += "\nSource: " + event["src_ip"] + "" + msgtxt += "\nSession: " + event["session"] + "" - if logentry["eventid"] == "cowrie.login.success": - msgtxt += "\nUsername: " + logentry["username"] + "" - msgtxt += "\nPassword: " + logentry["password"] + "" + if event["eventid"] == "cowrie.login.success": + msgtxt += "\nUsername: " + event["username"] + "" + msgtxt += "\nPassword: " + event["password"] + "" self.send_message(msgtxt) - elif logentry["eventid"] in ["cowrie.command.failed", "cowrie.command.input"]: - msgtxt += "\nCommand:
" + logentry["input"] + "
" + elif event["eventid"] in ["cowrie.command.failed", "cowrie.command.input"]: + msgtxt += "\nCommand:
" + event["input"] + "
" self.send_message(msgtxt) - elif logentry["eventid"] == "cowrie.session.file_download": - msgtxt += "\nUrl: " + logentry.get("url", "") + elif event["eventid"] == "cowrie.session.file_download": + msgtxt += "\nUrl: " + event.get("url", "") self.send_message(msgtxt) def send_message(self, message): diff --git a/src/cowrie/output/textlog.py b/src/cowrie/output/textlog.py index c3a4756495..1ce5c77f6e 100644 --- a/src/cowrie/output/textlog.py +++ b/src/cowrie/output/textlog.py @@ -48,12 +48,12 @@ def start(self): def stop(self): pass - def write(self, logentry): + def write(self, event): if self.format == "cef": - self.outfile.write("{} ".format(logentry["timestamp"])) - self.outfile.write(f"{cowrie.core.cef.formatCef(logentry)}\n") + self.outfile.write("{} ".format(event["timestamp"])) + self.outfile.write(f"{cowrie.core.cef.formatCef(event)}\n") else: - self.outfile.write("{} ".format(logentry["timestamp"])) - self.outfile.write("{} ".format(logentry["session"])) - self.outfile.write("{}\n".format(logentry["message"])) + self.outfile.write("{} ".format(event["timestamp"])) + self.outfile.write("{} ".format(event["session"])) + self.outfile.write("{}\n".format(event["message"])) self.outfile.flush() diff --git a/src/cowrie/output/threatjammer.py b/src/cowrie/output/threatjammer.py index 108f94b4f0..784bf38bbb 100644 --- a/src/cowrie/output/threatjammer.py +++ b/src/cowrie/output/threatjammer.py @@ -182,9 +182,9 @@ def stop(self): format="ThreatJammer.com output plugin successfully terminated. Bye!", ) - def write(self, ev): - if ev["eventid"].rsplit(".", 1)[0] in self.track_events: - source_ip: str = ev["src_ip"] + def write(self, event): + if event["eventid"].rsplit(".", 1)[0] in self.track_events: + source_ip: str = event["src_ip"] self.ip_set.add(source_ip) if self.last_report == -1: diff --git a/src/cowrie/output/virustotal.py b/src/cowrie/output/virustotal.py index 6bc061c479..416e71ebac 100644 --- a/src/cowrie/output/virustotal.py +++ b/src/cowrie/output/virustotal.py @@ -42,7 +42,6 @@ from twisted.internet import defer from twisted.internet import reactor -from twisted.internet.ssl import ClientContextFactory from twisted.python import log from twisted.web import client, http_headers from twisted.web.iweb import IBodyProducer @@ -67,14 +66,14 @@ class Output(cowrie.core.output.Output): agent: Any scan_url: bool scan_file: bool - url_cache: dict[ - str, datetime.datetime - ] = {} # url and last time succesfully submitted + url_cache: dict[str, datetime.datetime] # url and last time succesfully submitted def start(self) -> None: """ Start output plugin """ + self.url_cache = {} + self.apiKey = CowrieConfig.get("output_virustotal", "api_key") self.debug = CowrieConfig.getboolean( "output_virustotal", "debug", fallback=False @@ -94,26 +93,26 @@ def start(self) -> None: self.commenttext = CowrieConfig.get( "output_virustotal", "commenttext", fallback=COMMENT ) - self.agent = client.Agent(reactor, WebClientContextFactory()) + self.agent = client.Agent(reactor) def stop(self) -> None: """ Stop output plugin """ - def write(self, entry: dict[str, Any]) -> None: - if entry["eventid"] == "cowrie.session.file_download": - if self.scan_url and "url" in entry: + def write(self, event: dict[str, Any]) -> None: + if event["eventid"] == "cowrie.session.file_download": + if self.scan_url and "url" in event: log.msg("Checking url scan report at VT") - self.scanurl(entry) - if self._is_new_shasum(entry["shasum"]) and self.scan_file: + self.scanurl(event) + if self._is_new_shasum(event["shasum"]) and self.scan_file: log.msg("Checking file scan report at VT") - self.scanfile(entry) + self.scanfile(event) - elif entry["eventid"] == "cowrie.session.file_upload": - if self._is_new_shasum(entry["shasum"]) and self.scan_file: + elif event["eventid"] == "cowrie.session.file_upload": + if self._is_new_shasum(event["shasum"]) and self.scan_file: log.msg("Checking file scan report at VT") - self.scanfile(entry) + self.scanfile(event) def _is_new_shasum(self, shasum): # Get the downloaded file's modification time @@ -134,14 +133,14 @@ def _is_new_shasum(self, shasum): return False return True - def scanfile(self, entry): + def scanfile(self, event): """ Check file scan report for a hash Argument is full event so we can access full file later on """ vtUrl = f"{VTAPI_URL}file/report".encode() headers = http_headers.Headers({"User-Agent": [COWRIE_USER_AGENT]}) - fields = {"apikey": self.apiKey, "resource": entry["shasum"], "allinfo": 1} + fields = {"apikey": self.apiKey, "resource": event["shasum"], "allinfo": 1} body = StringProducer(urlencode(fields).encode("utf-8")) d = self.agent.request(b"POST", vtUrl, headers, body) @@ -185,22 +184,22 @@ def processResult(result): log.msg( eventid="cowrie.virustotal.scanfile", format="VT: New file %(sha256)s", - session=entry["session"], + session=event["session"], sha256=j["resource"], is_new="true", ) try: - b = os.path.basename(urlparse(entry["url"]).path) + b = os.path.basename(urlparse(event["url"]).path) if b == "": - fileName = entry["shasum"] + fileName = event["shasum"] else: fileName = b except KeyError: - fileName = entry["shasum"] + fileName = event["shasum"] if self.upload is True: - return self.postfile(entry["outfile"], fileName) + return self.postfile(event["outfile"], fileName) else: return elif j["response_code"] == 1: @@ -216,7 +215,7 @@ def processResult(result): eventid="cowrie.virustotal.scanfile", format="VT: Binary file with sha256 %(sha256)s was found malicious " "by %(positives)s out of %(total)s feeds (scanned on %(scan_date)s)", - session=entry["session"], + session=event["session"], positives=j["positives"], total=j["total"], scan_date=j["scan_date"], @@ -293,14 +292,14 @@ def processResult(result): d.addErrback(cbError) return d - def scanurl(self, entry): + def scanurl(self, event): """ Check url scan report for a hash """ - if entry["url"] in self.url_cache: + if event["url"] in self.url_cache: log.msg( "output_virustotal: url {} was already successfully submitted".format( - entry["url"] + event["url"] ) ) return @@ -309,7 +308,7 @@ def scanurl(self, entry): headers = http_headers.Headers({"User-Agent": [COWRIE_USER_AGENT]}) fields = { "apikey": self.apiKey, - "resource": entry["url"], + "resource": event["url"], "scan": 1, "allinfo": 1, } @@ -354,14 +353,14 @@ def processResult(result): log.msg("VT: {}".format(j["verbose_msg"])) # we got a status=200 assume it was successfully submitted - self.url_cache[entry["url"]] = datetime.datetime.now() + self.url_cache[event["url"]] = datetime.datetime.now() if j["response_code"] == 0: log.msg( eventid="cowrie.virustotal.scanurl", format="VT: New URL %(url)s", - session=entry["session"], - url=entry["url"], + session=event["session"], + url=event["url"], is_new="true", ) return d @@ -382,7 +381,7 @@ def processResult(result): eventid="cowrie.virustotal.scanurl", format="VT: URL %(url)s was found malicious by " "%(positives)s out of %(total)s feeds (scanned on %(scan_date)s)", - session=entry["session"], + session=event["session"], positives=j["positives"], total=j["total"], scan_date=j["scan_date"], @@ -448,11 +447,6 @@ def processResult(result): return d -class WebClientContextFactory(ClientContextFactory): - def getContext(self, hostname, port): - return ClientContextFactory.getContext(self) - - @implementer(IBodyProducer) class StringProducer: def __init__(self, body): @@ -481,12 +475,12 @@ def encode_multipart_formdata(fields, files): """ BOUNDARY = b"----------ThIs_Is_tHe_bouNdaRY_$" L = [] - for (key, value) in fields: + for key, value in fields: L.append(b"--" + BOUNDARY) L.append(b'Content-Disposition: form-data; name="%s"' % key.encode()) L.append(b"") L.append(value.encode()) - for (key, filename, value) in files: + for key, filename, value in files: L.append(b"--" + BOUNDARY) L.append( b'Content-Disposition: form-data; name="%s"; filename="%s"' diff --git a/src/cowrie/output/xmpp.py b/src/cowrie/output/xmpp.py index a9b258b2da..68aaa7d78c 100644 --- a/src/cowrie/output/xmpp.py +++ b/src/cowrie/output/xmpp.py @@ -82,14 +82,14 @@ def run(self, application, jidstr, password, muc, server): self.anonymous = True self.xmppclient.startService() - def write(self, logentry): - for i in list(logentry.keys()): + def write(self, event): + for i in list(event.keys()): # Remove twisted 15 legacy keys if i.startswith("log_"): - del logentry[i] + del event[i] elif i == "time": - del logentry[i] - msgJson = json.dumps(logentry, indent=5) + del event[i] + msgJson = json.dumps(event, indent=5) self.muc.groupChat(self.muc.jrooms, msgJson) diff --git a/src/cowrie/python/logfile.py b/src/cowrie/python/logfile.py index 49112c2bc7..27d1080143 100644 --- a/src/cowrie/python/logfile.py +++ b/src/cowrie/python/logfile.py @@ -18,22 +18,20 @@ class CowrieDailyLogFile(logfile.DailyLogFile): Overload original Twisted with improved date formatting """ - def suffix(self, tupledate): + def suffix(self, tupledate: float | tuple[int, int, int]) -> str: """ Return the suffix given a (year, month, day) tuple or unixtime """ - try: - return "{:02d}-{:02d}-{:02d}".format( - tupledate[0], tupledate[1], tupledate[2] - ) - except Exception: - # try taking a float unixtime + if isinstance(tupledate, tuple): + return f"{tupledate[0]:02d}-{tupledate[1]:02d}-{tupledate[2]:02d}" + if isinstance(tupledate, float): return "_".join(map(str, self.toDate(tupledate))) + raise AttributeError("wrong type") def logger(): directory = CowrieConfig.get("honeypot", "log_path", fallback="var/log/cowrie") - logfile = CowrieDailyLogFile("cowrie.log", directory) + cowrielog = CowrieDailyLogFile("cowrie.log", directory) # use Z for UTC (Zulu) time, it's shorter. if "TZ" in environ and environ["TZ"] == "UTC": @@ -41,4 +39,4 @@ def logger(): else: timeFormat = "%Y-%m-%dT%H:%M:%S.%f%z" - return textFileLogObserver(logfile, timeFormat=timeFormat) + return textFileLogObserver(cowrielog, timeFormat=timeFormat) diff --git a/src/cowrie/scripts/__init__.py b/src/cowrie/scripts/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/cowrie/scripts/asciinema.py b/src/cowrie/scripts/asciinema.py new file mode 100755 index 0000000000..8dfa2dd12b --- /dev/null +++ b/src/cowrie/scripts/asciinema.py @@ -0,0 +1,135 @@ +#!/usr/bin/env python + +import getopt +import json +import os +import struct +import sys + +OP_OPEN, OP_CLOSE, OP_WRITE, OP_EXEC = 1, 2, 3, 4 +TYPE_INPUT, TYPE_OUTPUT, TYPE_INTERACT = 1, 2, 3 + +COLOR_INTERACT = "\033[36m" +COLOR_INPUT = "\033[33m" +COLOR_RESET = "\033[0m" + + +def playlog(fd, settings): + thelog = {} + thelog["version"] = 1 + thelog["width"] = 80 + thelog["height"] = 24 + thelog["duration"] = 0.0 + thelog["command"] = "/bin/bash" + thelog["title"] = "Cowrie Recording" + theenv = {} + theenv["TERM"] = "xterm256-color" + theenv["SHELL"] = "/bin/bash" + thelog["env"] = theenv + stdout = [] + thelog["stdout"] = stdout + + ssize = struct.calcsize(" ..." + % os.path.basename(sys.argv[0]) + ) + + if verbose: + print( + " -c colorify the output based on what streams are being received" + ) + print(" -h display this help") + print(" -o write to the specified output file") + + +def run(): + settings = {"colorify": 0, "output": ""} + + try: + optlist, args = getopt.getopt(sys.argv[1:], "hco:") + except getopt.GetoptError as error: + sys.stderr.write(f"{sys.argv[0]}: {error}\n") + help() + sys.exit(1) + + for o, a in optlist: + if o == "-h": + help() + if o == "-c": + settings["colorify"] = True + if o == "-o": + settings["output"] = a + + if len(args) < 1: + help() + sys.exit(2) + + for logfile in args: + try: + logfd = open(logfile, "rb") + playlog(logfd, settings) + except OSError as e: + sys.stderr.write(f"{sys.argv[0]}: {e}\n") + + +if __name__ == "__main__": + run() diff --git a/src/cowrie/scripts/createdynamicprocess.py b/src/cowrie/scripts/createdynamicprocess.py new file mode 100755 index 0000000000..e80b56d214 --- /dev/null +++ b/src/cowrie/scripts/createdynamicprocess.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python + +import datetime +import json +import random + +import psutil + + +def run(): + command: dict = {} + command["command"] = {} + command["command"]["ps"] = [] + + randomStates = ["Ss", "S<", "D<", "Ss+"] + for proc in psutil.process_iter(): + try: + info = proc.as_dict( + attrs=[ + "pid", + "name", + "cmdline", + "username", + "cpu_percent", + "memory_percent", + "memory_info", + "create_time", + "terminal", + "status", + "cpu_times", + ] + ) + except psutil.NoSuchProcess: + pass + else: + object = {} + object["USER"] = info["username"] + object["PID"] = info["pid"] + if info["cmdline"]: + object["COMMAND"] = "/".join(info["cmdline"]) + else: + object["COMMAND"] = "[ " + info["name"] + " ]" + object["CPU"] = info["cpu_percent"] + object["MEM"] = info["memory_percent"] + object["RSS"] = info["memory_info"].rss + object["VSZ"] = info["memory_info"].vms + object["START"] = datetime.datetime.fromtimestamp( + info["create_time"] + ).strftime("%b%d") + if info["terminal"]: + object["TTY"] = str(info["terminal"]).replace("/dev/", "") + else: + object["TTY"] = "?" + object["STAT"] = random.choice(randomStates) + object["TIME"] = info["cpu_times"].user + command["command"]["ps"].append(object) + + print(json.dumps(command, indent=4, sort_keys=True)) + + +if __name__ == "__main__": + run() diff --git a/src/cowrie/scripts/createfs.py b/src/cowrie/scripts/createfs.py new file mode 100755 index 0000000000..e8f648e690 --- /dev/null +++ b/src/cowrie/scripts/createfs.py @@ -0,0 +1,200 @@ +#!/usr/bin/env python + +############################################################### +# This program creates a cowrie file system pickle file. +# +# This is meant to build a brand new filesystem. +# To edit the file structure, please use 'bin/fsctl' +# +############################################################## + +import fnmatch +import getopt +import os +import pickle +import sys +from stat import ( + S_ISBLK, + S_ISCHR, + S_ISDIR, + S_ISFIFO, + S_ISLNK, + S_ISREG, + S_ISSOCK, + ST_MODE, +) + +( + A_NAME, + A_TYPE, + A_UID, + A_GID, + A_SIZE, + A_MODE, + A_CTIME, + A_CONTENTS, + A_TARGET, + A_REALFILE, +) = range(0, 10) +T_LINK, T_DIR, T_FILE, T_BLK, T_CHR, T_SOCK, T_FIFO = range(0, 7) +PROC = False +VERBOSE = False + +blacklist_files = [ + "/root/fs.pickle", + "/root/createfs", + "*cowrie*", + "*kippo*", +] + + +def logit(ftxt): + if VERBOSE: + sys.stderr.write(ftxt) + + +def checkblacklist(ftxt): + for value in blacklist_files: + if fnmatch.fnmatch(ftxt, value): + return True + return False + + +def recurse(localroot, root, tree, maxdepth=100): + if maxdepth == 0: + return + + localpath = os.path.join(localroot, root[1:]) + + logit(" %s\n" % (localpath)) + + if not os.access(localpath, os.R_OK): + logit(" Cannot access %s\n" % localpath) + return + + for name in os.listdir(localpath): + fspath = os.path.join(root, name) + if checkblacklist(fspath): + continue + + path = os.path.join(localpath, name) + + try: + if os.path.islink(path): + s = os.lstat(path) + else: + s = os.stat(path) + except OSError: + continue + + entry = [ + name, + T_FILE, + s.st_uid, + s.st_gid, + s.st_size, + s.st_mode, + int(s.st_mtime), + [], + None, + None, + ] + + if S_ISLNK(s[ST_MODE]): + if not os.access(path, os.R_OK): + logit(" Cannot access link: %s\n" % path) + continue + realpath = os.path.realpath(path) + if not realpath.startswith(localroot): + logit( + ' Link "{}" has real path "{}" outside local root "{}"\n'.format( + path, realpath, localroot + ) + ) + continue + else: + entry[A_TYPE] = T_LINK + entry[A_TARGET] = realpath[len(localroot) :] + elif S_ISDIR(s[ST_MODE]): + entry[A_TYPE] = T_DIR + if (PROC or not localpath.startswith("/proc/")) and maxdepth > 0: + recurse(localroot, fspath, entry[A_CONTENTS], maxdepth - 1) + elif S_ISREG(s[ST_MODE]): + entry[A_TYPE] = T_FILE + elif S_ISBLK(s[ST_MODE]): + entry[A_TYPE] = T_BLK + elif S_ISCHR(s[ST_MODE]): + entry[A_TYPE] = T_CHR + elif S_ISSOCK(s[ST_MODE]): + entry[A_TYPE] = T_SOCK + elif S_ISFIFO(s[ST_MODE]): + entry[A_TYPE] = T_FIFO + else: + sys.stderr.write("We should handle %s" % path) + sys.exit(1) + + tree.append(entry) + + +def help(brief=False): + print( + "Usage: %s [-h] [-v] [-p] [-l dir] [-d maxdepth] [-o file]\n" + % os.path.basename(sys.argv[0]) + ) + + if not brief: + print(" -v verbose") + print(" -p include /proc") + print( + " -l local root directory (default is current working directory)" + ) + print(" -d maximum depth (default is full depth)") + print(" -o write output to file instead of stdout") + print(" -h display this help\n") + + sys.exit(1) + + +def run(): + maxdepth = 100 + localroot = os.getcwd() + output = "" + + try: + optlist, args = getopt.getopt(sys.argv[1:], "hvpl:d:o:", ["help"]) + except getopt.GetoptError as error: + sys.stderr.write("Error: %s\n" % error) + help() + return + + for o, a in optlist: + if o == "-v": + pass + elif o == "-p": + pass + elif o == "-l": + localroot = a + elif o == "-d": + maxdepth = int(a) + elif o == "-o": + output = a + elif o in ["-h", "--help"]: + help() + + if output and os.path.isfile(output): + sys.stderr.write("File: %s exists!\n" % output) + sys.exit(1) + + logit("Processing:\n") + + tree = ["/", T_DIR, 0, 0, 0, 0, 0, [], ""] + recurse(localroot, "/", tree[A_CONTENTS], maxdepth) + + if output: + pickle.dump(tree, open(output, "wb")) + else: + print(pickle.dumps(tree)) + + +if __name__ == "__main__": + run() diff --git a/src/cowrie/scripts/fsctl.py b/src/cowrie/scripts/fsctl.py new file mode 100755 index 0000000000..38ed61e5d3 --- /dev/null +++ b/src/cowrie/scripts/fsctl.py @@ -0,0 +1,779 @@ +#!/usr/bin/env python + +################################################################ +# This is a command line interpreter used to edit +# cowrie file system pickle files. +# +# It is intended to mimic a basic bash shell and supports +# relative file references. +# +# Do not use to build a complete file system. Use: +# /opt/cowrie/bin/createfs +# +# Instead it should be used to edit existing file systems +# such as the default: /opt/cowrie/data/fs.pickle. +# +# Donovan Hubbard +# Douglas Hubbard +# March 2013 +################################################################ + +import cmd +import copy +import os +import pickle +import sys +import time +from stat import ( + S_IRGRP, + S_IROTH, + S_IRUSR, + S_IWGRP, + S_IWOTH, + S_IWUSR, + S_IXGRP, + S_IXOTH, + S_IXUSR, +) + +from cowrie.shell.fs import FileNotFound + +( + A_NAME, + A_TYPE, + A_UID, + A_GID, + A_SIZE, + A_MODE, + A_CTIME, + A_CONTENTS, + A_TARGET, + A_REALFILE, +) = list(range(0, 10)) +T_LINK, T_DIR, T_FILE, T_BLK, T_CHR, T_SOCK, T_FIFO = list(range(0, 7)) + + +def getpath(fs, path): + cwd = fs + for part in path.split("/"): + if not len(part): + continue + ok = False + for c in cwd[A_CONTENTS]: + if c[A_NAME] == part: + cwd = c + ok = True + break + if not ok: + raise FileNotFound + return cwd + + +def exists(fs, path): + try: + getpath(fs, path) + return True + except FileNotFound: + return False + + +def is_directory(fs, path): + """ + Returns whether or not the file at 'path' is a directory + + :param fs: + :param path: + :return: + """ + file = getpath(fs, path) + if file[A_TYPE] == T_DIR: + return True + else: + return False + + +def resolve_reference(pwd, relativeReference): + """ + Used to resolve a current working directory and a relative reference into an absolute file reference. + """ + + tempPath = os.path.join(pwd, relativeReference) + absoluteReference = os.path.normpath(tempPath) + + return absoluteReference + + +class fseditCmd(cmd.Cmd): + def __init__(self, pickle_file_path): + cmd.Cmd.__init__(self) + + if not os.path.isfile(pickle_file_path): + print("File %s does not exist." % pickle_file_path) + sys.exit(1) + + try: + pickle_file = open(pickle_file_path, "rb") + except OSError as e: + print(f"Unable to open file {pickle_file_path}: {e!r}") + sys.exit(1) + + try: + self.fs = pickle.load(pickle_file, encoding="utf-8") + except Exception: + print( + ( + "Unable to load file '%s'. " + + "Are you sure it is a valid pickle file?" + ) + % (pickle_file_path,) + ) + sys.exit(1) + + self.pickle_file_path = pickle_file_path + + # get the name of the file so we can display it as the prompt + path_parts = pickle_file_path.split("/") + self.fs_name = path_parts[-1] + + self.update_pwd("/") + + self.intro = ( + "\nKippo/Cowrie file system interactive editor\n" + + "Donovan Hubbard, Douglas Hubbard, March 2013\n" + + "Type 'help' for help\n" + ) + + def save_pickle(self): + """ + saves the current file system to the pickle + :return: + """ + try: + pickle.dump(self.fs, open(self.pickle_file_path, "wb")) + except Exception as e: + print( + ( + "Unable to save pickle file '%s'. " + + "Are you sure you have write access?" + ) + % (self.pickle_file_path,) + ) + print(str(e)) + sys.exit(1) + + def do_exit(self, args): + """ + Exits the file system editor + """ + return True + + def do_EOF(self, args): + """ + The escape character ctrl+d exits the session + """ + # exiting from the do_EOF method does not create a newline automatically + # so we add it manually + print() + return True + + def do_ls(self, args): + """ + Prints the contents of a directory, use ls -l to list in long format + Prints the current directory if no arguments are specified + """ + + longls = False + + if args.startswith("-l"): + longls = True + args = args[3:] + + if not len(args): + path = self.pwd + else: + path = resolve_reference(self.pwd, args) + + if exists(self.fs, path) is False: + print(f"ls: cannot access {path}: No such file or directory") + return + + if is_directory(self.fs, path) is False: + print(f"ls: {path} is not a directory") + return + + cwd = getpath(self.fs, path) + files = cwd[A_CONTENTS] + files.sort() + + largest = 0 + if len(files): + largest = max([x[A_SIZE] for x in files]) + + for file in files: + if not longls: + if file[A_TYPE] == T_DIR: + print(file[A_NAME] + "/") + else: + print(file[A_NAME]) + continue + + perms = ["-"] * 10 + + if file[A_MODE] & S_IRUSR: + perms[1] = "r" + if file[A_MODE] & S_IWUSR: + perms[2] = "w" + if file[A_MODE] & S_IXUSR: + perms[3] = "x" + + if file[A_MODE] & S_IRGRP: + perms[4] = "r" + if file[A_MODE] & S_IWGRP: + perms[5] = "w" + if file[A_MODE] & S_IXGRP: + perms[6] = "x" + + if file[A_MODE] & S_IROTH: + perms[7] = "r" + if file[A_MODE] & S_IWOTH: + perms[8] = "w" + if file[A_MODE] & S_IXOTH: + perms[9] = "x" + + linktarget = "" + + if file[A_TYPE] == T_DIR: + perms[0] = "d" + elif file[A_TYPE] == T_LINK: + perms[0] = "l" + linktarget = f" -> {file[A_TARGET]}" + + perms = "".join(perms) + ctime = time.localtime(file[A_CTIME]) + uid = file[A_UID] + gid = file[A_GID] + + if uid == 0: + uid = "root" + else: + uid = str(uid).rjust(4) + + if gid == 0: + gid = "root" + else: + gid = str(gid).rjust(4) + + print( + "{} 1 {} {} {} {} {}{}".format( + perms, + uid, + gid, + str(file[A_SIZE]).rjust(len(str(largest))), + time.strftime("%Y-%m-%d %H:%M", ctime), + file[A_NAME], + linktarget, + ) + ) + + def update_pwd(self, directory): + self.pwd = directory + self.prompt = self.fs_name + ":" + self.pwd + "$ " + + def do_cd(self, args): + """ + Changes the current directory.\nUsage: cd + """ + + # count the number of arguments + # 1 or more arguments: changes the directory to the first arg + # and ignores the rest + # 0 arguments: changes to '/' + arguments = args.split() + + if not len(arguments): + self.update_pwd("/") + else: + relative_dir = arguments[0] + target_dir = resolve_reference(self.pwd, relative_dir) + + if exists(self.fs, target_dir) is False: + print("cd: %s: No such file or directory" % target_dir) + elif is_directory(self.fs, target_dir): + self.update_pwd(target_dir) + else: + print("cd: %s: Not a directory" % target_dir) + + def do_pwd(self, args): + """ + Prints the current working directory + + :param args: + :return: + """ + print(self.pwd) + + def do_mkdir(self, args): + """ + Add a new directory in the target directory. + Handles relative or absolute file paths. \n + Usage: mkdir ... + """ + + arg_list = args.split() + if len(arg_list) < 1: + print("usage: mkdir ...") + else: + for arg in arg_list: + self.mkfile(arg.split(), T_DIR) + + def do_touch(self, args): + """ + Add a new file in the target directory. + Handles relative or absolute file paths. \n + Usage: touch [] + """ + + arg_list = args.split() + + if len(arg_list) < 1: + print("Usage: touch ()") + else: + self.mkfile(arg_list, T_FILE) + + def mkfile(self, args, file_type): + """ + args must be a list of arguments + """ + cwd = self.fs + path = resolve_reference(self.pwd, args[0]) + pathList = path.split("/") + parentdir = "/".join(pathList[:-1]) + fileName = pathList[len(pathList) - 1] + + if not exists(self.fs, parentdir): + print(f"Parent directory {parentdir} doesn't exist!") + self.mkfile(parentdir.split(), T_DIR) + + if exists(self.fs, path): + print(f"Error: {path} already exists!") + return + + cwd = getpath(self.fs, parentdir) + + # get uid, gid, mode from parent + uid = cwd[A_UID] + gid = cwd[A_GID] + mode = cwd[A_MODE] + + # Modify file_mode when it is a file + if file_type == T_FILE: + file_file_mode = int("0o100000", 8) + permits = mode & (2**9 - 1) + mode = file_file_mode + permits + + # create default file/directory size if none is specified + if len(args) == 1: + size = 4096 + else: + size = int(args[1]) + + # set the last update time stamp to now + ctime = time.time() + + cwd[A_CONTENTS].append( + [fileName, file_type, uid, gid, size, mode, ctime, [], None, None] + ) + + self.save_pickle() + + print("Added '%s'" % path) + + def do_rm(self, arguments): + """ + Remove an object from the file system. + Will not remove a directory unless the -r switch is invoked.\n + Usage: rm [-r] + """ + + args = arguments.split() + + if len(args) < 1 or len(args) > 2: + print("Usage: rm [-r] ") + return + + if len(args) == 2 and args[0] != "-r": + print("Usage: rm [-r] ") + return + + if len(args) == 1: + target_path = resolve_reference(self.pwd, args[0]) + else: + target_path = resolve_reference(self.pwd, args[1]) + + if exists(self.fs, target_path) is False: + print(f"File '{target_path}' doesn't exist") + return + + if target_path == "/": + print("rm: cannot delete root directory '/'") + return + + target_object = getpath(self.fs, target_path) + + if target_object[A_TYPE] == T_DIR and args[0] != "-r": + print(f"rm: cannot remove '{target_path}': Is a directory") + return + + parent_path = "/".join(target_path.split("/")[:-1]) + parent_object = getpath(self.fs, parent_path) + + parent_object[A_CONTENTS].remove(target_object) + + self.save_pickle() + + print("Deleted %s" % target_path) + + def do_rmdir(self, arguments): + """ + Remove a file object. Like the unix command, + this can only delete empty directories. + Use rm -r to recursively delete full directories.\n + Usage: rmdir + """ + args = arguments.split() + + if len(args) != 1: + print("Usage: rmdir ") + return + + target_path = resolve_reference(self.pwd, args[0]) + + if exists(self.fs, target_path) is False: + print(f"File '{target_path}' doesn't exist") + return + + target_object = getpath(self.fs, target_path) + + if target_object[A_TYPE] != T_DIR: + print(f"rmdir: failed to remove '{target_path}': Not a directory") + return + + # The unix rmdir command does not delete directories if they are not + # empty + if len(target_object[A_CONTENTS]) != 0: + print(f"rmdir: failed to remove '{target_path}': Directory not empty") + return + + parent_path = "/".join(target_path.split("/")[:-1]) + parent_object = getpath(self.fs, parent_path) + + parent_object[A_CONTENTS].remove(target_object) + + self.save_pickle() + + if self.pwd == target_path: + self.do_cd("..") + + print("Deleted %s" % target_path) + + def do_mv(self, arguments): + """ + Moves a file/directory from one directory to another.\n + Usage: mv + """ + args = arguments.split() + if len(args) != 2: + print("Usage: mv ") + return + src = resolve_reference(self.pwd, args[0]) + dst = resolve_reference(self.pwd, args[1]) + + if src == "/": + print("mv: cannot move the root directory '/'") + return + + src = src.strip("/") + dst = dst.strip("/") + + if not exists(self.fs, src): + print("Source file '%s' does not exist!" % src) + return + + # Get the parent directory of the source file + # srcparent = '/'.join(src.split('/')[:-1]) + srcparent = "/".join(src.split("/")[:-1]) + + # Get the object for source + srcl = getpath(self.fs, src) + + # Get the object for the source's parent + srcparentl = getpath(self.fs, srcparent) + + # if the specified filepath is a directory, maintain the current name + if exists(self.fs, dst) and is_directory(self.fs, dst): + dstparent = dst + dstname = srcl[A_NAME] + else: + dstparent = "/".join(dst.split("/")[:-1]) + dstname = dst.split("/")[-1] + + if exists(self.fs, dstparent + "/" + dstname): + print("A file already exists at " + dst + "!") + return + + if not exists(self.fs, dstparent): + print("Destination directory '%s' doesn't exist!" % dst) + return + + if src == self.pwd: + self.do_cd("..") + + dstparentl = getpath(self.fs, dstparent) + copy = srcl[:] + copy[A_NAME] = dstname + dstparentl[A_CONTENTS].append(copy) + srcparentl[A_CONTENTS].remove(srcl) + + self.save_pickle() + + print(f"File moved from /{src} to /{dst}") + + def do_cp(self, arguments): + """ + Copies a file/directory from one directory to another.\n + Usage: cp + """ + args = arguments.split() + if len(args) != 2: + print("Usage: cp ") + return + + # src, dst = args[0], args[1] + + src = resolve_reference(self.pwd, args[0]) + dst = resolve_reference(self.pwd, args[1]) + + src = src.strip("/") + dst = dst.strip("/") + + if not exists(self.fs, src): + print(f"Source file '{src}' does not exist!") + return + + # Get the parent directory of the source file + # srcparent = "/".join(src.split("/")[:-1]) + + # Get the object for source + srcl = getpath(self.fs, src) + + # Get the object for the source's parent + # srcparentl = getpath(self.fs, srcparent) + + # if the specified filepath is a directory, maintain the current name + if exists(self.fs, dst) and is_directory(self.fs, dst): + dstparent = dst + dstname = srcl[A_NAME] + else: + dstparent = "/".join(dst.split("/")[:-1]) + dstname = dst.split("/")[-1] + + if exists(self.fs, dstparent + "/" + dstname): + print(f"A file already exists at {dstparent}/{dstname}!") + return + + if not exists(self.fs, dstparent): + print(f"Destination directory {dstparent} doesn't exist!") + return + + dstparentl = getpath(self.fs, dstparent) + coppy = copy.deepcopy(srcl) + coppy[A_NAME] = dstname + dstparentl[A_CONTENTS].append(coppy) + + self.save_pickle() + + print(f"File copied from /{src} to /{dstparent}/{dstname}") + + def do_chown(self, args): + """ + Change file ownership + """ + arg_list = args.split() + + if len(arg_list) != 2: + print("Incorrect number of arguments.\nUsage: chown ") + return + + uid = arg_list[0] + target_path = resolve_reference(self.pwd, arg_list[1]) + + if not exists(self.fs, target_path): + print("File '%s' doesn't exist." % target_path) + return + + target_object = getpath(self.fs, target_path) + olduid = target_object[A_UID] + target_object[A_UID] = int(uid) + print("former UID: " + str(olduid) + ". New UID: " + str(uid)) + self.save_pickle() + + def do_chgrp(self, args): + """ + Change file ownership + """ + arg_list = args.split() + + if len(arg_list) != 2: + print("Incorrect number of arguments.\nUsage: chgrp ") + return + + gid = arg_list[0] + target_path = resolve_reference(self.pwd, arg_list[1]) + + if not exists(self.fs, target_path): + print("File '%s' doesn't exist." % target_path) + return + + target_object = getpath(self.fs, target_path) + oldgid = target_object[A_GID] + target_object[A_GID] = int(gid) + print("former GID: " + str(oldgid) + ". New GID: " + str(gid)) + self.save_pickle() + + def do_chmod(self, args): + """ + Change file permissions + only modes between 000 and 777 are implemented + """ + + arg_list = args.split() + + if len(arg_list) != 2: + print("Incorrect number of arguments.\nUsage: chmod ") + return + + mode = arg_list[0] + target_path = resolve_reference(self.pwd, arg_list[1]) + + if not exists(self.fs, target_path): + print("File '%s' doesn't exist." % target_path) + return + + target_object = getpath(self.fs, target_path) + oldmode = target_object[A_MODE] + + if target_object[A_TYPE] == T_LINK: + print(target_path + " is a link, nothing changed.") + return + + try: + num = int(mode, 8) + except Exception: + print("Incorrect mode: " + mode) + return + + if num < 0 or num > 511: + print("Incorrect mode: " + mode) + return + + target_object[A_MODE] = (oldmode & 0o7777000) | (num & 0o777) + self.save_pickle() + + def do_file(self, args): + """ + Identifies file types.\nUsage: file + """ + arg_list = args.split() + + if len(arg_list) != 1: + print("Incorrect number of arguments.\nUsage: file ") + return + + target_path = resolve_reference(self.pwd, arg_list[0]) + + if not exists(self.fs, target_path): + print("File '%s' doesn't exist." % target_path) + return + + target_object = getpath(self.fs, target_path) + + file_type = target_object[A_TYPE] + + if file_type == T_FILE: + msg = "normal file object" + elif file_type == T_DIR: + msg = "directory" + elif file_type == T_LINK: + msg = "link" + elif file_type == T_BLK: + msg = "block file" + elif file_type == T_CHR: + msg = "character special" + elif file_type == T_SOCK: + msg = "socket" + elif file_type == T_FIFO: + msg = "named pipe" + else: + msg = "unrecognized file" + + print(target_path + " is a " + msg) + + def do_clear(self, args): + """ + Clears the screen + """ + os.system("clear") + + def emptyline(self) -> bool: + """ + By default the cmd object will repeat the last command + if a blank line is entered. Since this is different than + bash behavior, overriding this method will stop it. + """ + return False + + def help_help(self): + print("Type help to get more information.") + + def help_about(self): + print( + "Kippo/Cowrie stores information about its file systems in a " + + "series of nested lists. Once the lists are made, they are " + + "stored in a pickle file on the hard drive. Every time cowrie " + + "gets a new client, it reads from the pickle file and loads " + + "the fake file system into memory. By default this file " + + "is /opt/cowrie/data/fs.pickle. Originally the script " + + "/opt/cowrie/bin/createfs was used to copy the file system " + + "of the existing computer. However, it quite difficult to " + + "edit the pickle file by hand.\n\nThis script strives to be " + + "a bash-like interface that allows users to modify " + + "existing fs pickle files. It supports many of the " + + "common bash commands and even handles relative file " + + "paths. Keep in mind that you need to restart the " + + "cowrie process in order for the new file system to be " + + "reloaded into memory.\n\nDonovan Hubbard, Douglas Hubbard, " + + "March 2013\nVersion 1.0" + ) + + +def run(): + if len(sys.argv) < 2 or len(sys.argv) > 3: + print( + "Usage: %s [command]" + % os.path.basename( + sys.argv[0], + ) + ) + sys.exit(1) + + pickle_file_name = sys.argv[1].strip() + print(pickle_file_name) + + if len(sys.argv) == 3: + fseditCmd(pickle_file_name).onecmd(sys.argv[2]) + else: + fseditCmd(pickle_file_name).cmdloop() + + +if __name__ == "__main__": + run() diff --git a/src/cowrie/scripts/playlog.py b/src/cowrie/scripts/playlog.py new file mode 100755 index 0000000000..4f23f9070e --- /dev/null +++ b/src/cowrie/scripts/playlog.py @@ -0,0 +1,142 @@ +#!/usr/bin/env python +# +# Copyright (C) 2003-2011 Upi Tamminen +# + +import getopt +import os +import struct +import sys +import time + +OP_OPEN, OP_CLOSE, OP_WRITE, OP_EXEC = 1, 2, 3, 4 +TYPE_INPUT, TYPE_OUTPUT, TYPE_INTERACT = 1, 2, 3 + + +def playlog(fd, settings): + ssize = struct.calcsize(" settings["maxdelay"]: + sleeptime = settings["maxdelay"] + if settings["maxdelay"] > 0: + time.sleep(sleeptime) + prevtime = curtime + if settings["colorify"] and color: + stdout.write(color) + stdout.write(data) + if settings["colorify"] and color: + stdout.write(b"\033[0m") + color = None + sys.stdout.flush() + elif str(tty) == str(currtty) and op == OP_CLOSE: + break + + +def help(brief=0): + print( + "Usage: %s [-bfhi] [-m secs] [-w file] ...\n" + % os.path.basename(sys.argv[0]) + ) + + if not brief: + print(" -f keep trying to read the log until it's closed") + print( + " -m maximum delay in seconds, to avoid" + + " boredom or fast-forward\n" + + " to the end. (default is 3.0)" + ) + print(" -i show the input stream instead of output") + print(" -b show both input and output streams") + print( + " -c colorify the output stream based on what streams are being received" + ) + print(" -h display this help\n") + + sys.exit(1) + + +def run(): + settings = { + "tail": 0, + "maxdelay": 3.0, + "input_only": 0, + "both_dirs": 0, + "colorify": 0, + } + + try: + optlist, args = getopt.getopt(sys.argv[1:], "fhibcm:w:", ["help"]) + except getopt.GetoptError as error: + print("Error: %s\n" % error) + help() + return + + options = [x[0] for x in optlist] + if "-b" in options and "-i" in options: + print("Error: -i and -b cannot be used together. Please select only one flag") + sys.exit(1) + + for o, a in optlist: + if o == "-f": + settings["tail"] = 1 + elif o == "-m": + settings["maxdelay"] = float(a) # takes decimals + elif o == "-i": + settings["input_only"] = 1 + elif o == "-b": + settings["both_dirs"] = 1 + elif o in ["-h", "--help"]: + help() + elif o == "-c": + settings["colorify"] = 1 + + if len(args) < 1: + help() + + for logfile in args: + try: + with open(logfile, "rb") as f: + playlog(f, settings) + except OSError: + print(f"\n[!] Couldn't open log file {logfile}!") + + +if __name__ == "__main__": + run() diff --git a/src/cowrie/shell/command.py b/src/cowrie/shell/command.py index 80773f5b9b..e8bd65bac3 100644 --- a/src/cowrie/shell/command.py +++ b/src/cowrie/shell/command.py @@ -12,7 +12,6 @@ import shlex import stat import time -from typing import Optional from collections.abc import Callable from twisted.internet import error @@ -35,9 +34,9 @@ def __init__(self, protocol, *args): self.environ = self.protocol.cmdstack[0].environ self.fs = self.protocol.fs self.data: bytes = b"" # output data - self.input_data: Optional[ + self.input_data: None | ( bytes - ] = None # used to store STDIN data passed via PIPE + ) = None # used to store STDIN data passed via PIPE self.writefn: Callable[[bytes], None] = self.protocol.pp.outReceived self.errorWritefn: Callable[[bytes], None] = self.protocol.pp.errReceived # MS-DOS style redirect handling, inside the command diff --git a/src/cowrie/shell/filetransfer.py b/src/cowrie/shell/filetransfer.py index 63c87eb0b9..00ca6aaaf0 100644 --- a/src/cowrie/shell/filetransfer.py +++ b/src/cowrie/shell/filetransfer.py @@ -37,7 +37,6 @@ class CowrieSFTPFile: """ contents: bytes - bytesReceived: int = 0 bytesReceivedLimit: int = CowrieConfig.getint( "honeypot", "download_limit_size", fallback=0 ) @@ -45,6 +44,7 @@ class CowrieSFTPFile: def __init__(self, sftpserver, filename, flags, attrs): self.sftpserver = sftpserver self.filename = filename + self.bytesReceived: int = 0 openFlags = 0 if flags & FXF_READ == FXF_READ and flags & FXF_WRITE == 0: diff --git a/src/cowrie/shell/fs.py b/src/cowrie/shell/fs.py index 15c179d77e..3aaa164a2d 100644 --- a/src/cowrie/shell/fs.py +++ b/src/cowrie/shell/fs.py @@ -15,7 +15,7 @@ import sys import stat import time -from typing import Any, Optional +from typing import Any from twisted.python import log @@ -104,7 +104,6 @@ class PermissionDenied(Exception): class HoneyPotFilesystem: def __init__(self, arch: str, home: str) -> None: - self.fs: list[Any] try: @@ -143,9 +142,7 @@ def init_honeyfs(self, honeyfs_path: str) -> None: realfile_path: str = os.path.join(path, filename) virtual_path: str = "/" + os.path.relpath(realfile_path, honeyfs_path) - f: Optional[list[Any]] = self.getfile( - virtual_path, follow_symlinks=False - ) + f: list[Any] | None = self.getfile(virtual_path, follow_symlinks=False) if f and f[A_TYPE] == T_FILE: self.update_realfile(f, realfile_path) @@ -242,7 +239,7 @@ def exists(self, path: str) -> bool: Return True if path refers to an existing path. Returns False for broken symbolic links. """ - f: Optional[list[Any]] = self.getfile(path, follow_symlinks=True) + f: list[Any] | None = self.getfile(path, follow_symlinks=True) if f is not None: return True return False @@ -252,13 +249,12 @@ def lexists(self, path: str) -> bool: Return True if path refers to an existing path. Returns True for broken symbolic links. """ - f: Optional[list[Any]] = self.getfile(path, follow_symlinks=False) + f: list[Any] | None = self.getfile(path, follow_symlinks=False) if f is not None: return True return False def update_realfile(self, f: Any, realfile: str) -> None: - if ( not f[A_REALFILE] and os.path.exists(realfile) @@ -268,7 +264,7 @@ def update_realfile(self, f: Any, realfile: str) -> None: ): f[A_REALFILE] = realfile - def getfile(self, path: str, follow_symlinks: bool = True) -> Optional[list[Any]]: + def getfile(self, path: str, follow_symlinks: bool = True) -> list[Any] | None: """ This returns the Cowrie file system object for a path """ @@ -276,7 +272,7 @@ def getfile(self, path: str, follow_symlinks: bool = True) -> Optional[list[Any] return self.fs pieces: list[str] = path.strip("/").split("/") cwd: str = "" - p: Optional[list[Any]] = self.fs + p: list[Any] | None = self.fs for piece in pieces: if not isinstance(p, list): return None @@ -341,7 +337,7 @@ def mkfile( gid: int, size: int, mode: int, - ctime: Optional[float] = None, + ctime: float | None = None, ) -> bool: if self.newcount > 10000: return False @@ -355,7 +351,7 @@ def mkfile( _dir = self.get_path(_path) outfile: str = os.path.basename(path) if outfile in [x[A_NAME] for x in _dir]: - _dir.remove([x for x in _dir if x[A_NAME] == outfile][0]) + _dir.remove(next(x for x in _dir if x[A_NAME] == outfile)) _dir.append([outfile, T_FILE, uid, gid, size, mode, ctime, [], None, None]) self.newcount += 1 return True @@ -367,7 +363,7 @@ def mkdir( gid: int, size: int, mode: int, - ctime: Optional[float] = None, + ctime: float | None = None, ) -> None: if self.newcount > 10000: raise OSError(errno.EDQUOT, os.strerror(errno.EDQUOT), path) @@ -390,7 +386,7 @@ def isfile(self, path: str) -> bool: links, so both islink() and isfile() can be true for the same path. """ try: - f: Optional[list[Any]] = self.getfile(path) + f: list[Any] | None = self.getfile(path) except Exception: return False if f is None: @@ -406,7 +402,7 @@ def islink(self, path: str) -> bool: runtime. """ try: - f: Optional[list[Any]] = self.getfile(path) + f: list[Any] | None = self.getfile(path) except Exception: return False if f is None: @@ -434,7 +430,7 @@ def isdir(self, path: str) -> bool: # Below additions for SFTP support, try to keep functions here similar to os.* - def open(self, filename: str, openFlags: int, mode: int) -> Optional[int]: + def open(self, filename: str, openFlags: int, mode: int) -> int | None: """ #log.msg("fs.open %s" % filename) @@ -515,7 +511,7 @@ def mkdir2(self, path: str) -> None: """ FIXME mkdir() name conflicts with existing mkdir """ - directory: Optional[list[Any]] = self.getfile(path) + directory: list[Any] | None = self.getfile(path) if directory: raise OSError(errno.EEXIST, os.strerror(errno.EEXIST), path) self.mkdir(path, 0, 0, 4096, 16877) @@ -539,19 +535,19 @@ def rmdir(self, path: str) -> bool: return False def utime(self, path: str, _atime: float, mtime: float) -> None: - p: Optional[list[Any]] = self.getfile(path) + p: list[Any] | None = self.getfile(path) if not p: raise OSError(errno.ENOENT, os.strerror(errno.ENOENT)) p[A_CTIME] = mtime def chmod(self, path: str, perm: int) -> None: - p: Optional[list[Any]] = self.getfile(path) + p: list[Any] | None = self.getfile(path) if not p: raise OSError(errno.ENOENT, os.strerror(errno.ENOENT)) p[A_MODE] = stat.S_IFMT(p[A_MODE]) | perm def chown(self, path: str, uid: int, gid: int) -> None: - p: Optional[list[Any]] = self.getfile(path) + p: list[Any] | None = self.getfile(path) if not p: raise OSError(errno.ENOENT, os.strerror(errno.ENOENT)) if uid != -1: @@ -560,13 +556,13 @@ def chown(self, path: str, uid: int, gid: int) -> None: p[A_GID] = gid def remove(self, path: str) -> None: - p: Optional[list[Any]] = self.getfile(path, follow_symlinks=False) + p: list[Any] | None = self.getfile(path, follow_symlinks=False) if not p: raise OSError(errno.ENOENT, os.strerror(errno.ENOENT)) self.get_path(os.path.dirname(path)).remove(p) def readlink(self, path: str) -> str: - p: Optional[list[Any]] = self.getfile(path, follow_symlinks=False) + p: list[Any] | None = self.getfile(path, follow_symlinks=False) if not p: raise OSError(errno.ENOENT, os.strerror(errno.ENOENT)) if not p[A_MODE] & stat.S_IFLNK: @@ -577,7 +573,7 @@ def symlink(self, targetPath: str, linkPath: str) -> None: raise NotImplementedError def rename(self, oldpath: str, newpath: str) -> None: - old: Optional[list[Any]] = self.getfile(oldpath) + old: list[Any] | None = self.getfile(oldpath) if not old: raise OSError(errno.ENOENT, os.strerror(errno.ENOENT)) new = self.getfile(newpath) @@ -596,7 +592,7 @@ def lstat(self, path: str) -> _statobj: return self.stat(path, follow_symlinks=False) def stat(self, path: str, follow_symlinks: bool = True) -> _statobj: - p: Optional[list[Any]] + p: list[Any] | None if path == "/": p = [] p[A_TYPE] = T_DIR @@ -628,7 +624,7 @@ def realpath(self, path: str) -> str: return path def update_size(self, filename: str, size: int) -> None: - f: Optional[list[Any]] = self.getfile(filename) + f: list[Any] | None = self.getfile(filename) if not f: return if f[A_TYPE] != T_FILE: diff --git a/src/cowrie/shell/honeypot.py b/src/cowrie/shell/honeypot.py index e246d69a3a..710d999498 100644 --- a/src/cowrie/shell/honeypot.py +++ b/src/cowrie/shell/honeypot.py @@ -8,7 +8,7 @@ import os import re import shlex -from typing import Any, Optional +from typing import Any from twisted.internet import error from twisted.python import failure, log @@ -30,7 +30,7 @@ def __init__( if hasattr(protocol.user, "windowSize"): self.environ["COLUMNS"] = str(protocol.user.windowSize[1]) self.environ["LINES"] = str(protocol.user.windowSize[0]) - self.lexer: Optional[shlex.shlex] = None + self.lexer: shlex.shlex | None = None self.showPrompt() def lineReceived(self, line: str) -> None: @@ -46,7 +46,7 @@ def lineReceived(self, line: str) -> None: tokkie: str | None = self.lexer.get_token() # log.msg("tok: %s" % (repr(tok))) - if tokkie is None: # self.lexer.eof put None for mypy + if tokkie is None: # self.lexer.eof put None for mypy if tokens: self.cmdpending.append(tokens) break @@ -118,6 +118,10 @@ def lineReceived(self, line: str) -> None: self.showPrompt() def do_command_substitution(self, start_tok: str) -> str: + """ + this performs command substitution, like replace $(ls) `ls` + """ + result = "" if start_tok[0] == "(": # start parsing the (...) expression cmd_expr = start_tok @@ -134,6 +138,10 @@ def do_command_substitution(self, start_tok: str) -> str: result = start_tok[:backtick_pos] cmd_expr = start_tok[backtick_pos:] pos = 1 + else: + log.msg(f"failed command substitution: {start_tok}") + return start_tok + opening_count = 1 closing_count = 0 @@ -167,7 +175,7 @@ def do_command_substitution(self, start_tok: str) -> str: if opening_count > closing_count and pos == len(cmd_expr) - 1: if self.lexer: tokkie = self.lexer.get_token() - if tokkie is None: # self.lexer.eof put None for mypy + if tokkie is None: # self.lexer.eof put None for mypy break else: cmd_expr = cmd_expr + " " + tokkie @@ -291,7 +299,6 @@ def parse_file_arguments(arguments: str) -> list[str]: lastpp = None for index, cmd in reversed(list(enumerate(cmd_array))): - cmdclass = self.protocol.getCommand( cmd["command"], environ["PATH"].split(":") ) @@ -403,24 +410,24 @@ def handle_TAB(self) -> None: clue = "" else: clue = line.split()[-1].decode("utf8") + # clue now contains the string to complete or is empty. # line contains the buffer as bytes - - try: - basedir = os.path.dirname(clue) - except Exception: - pass + basedir = os.path.dirname(clue) if basedir and basedir[-1] != "/": basedir += "/" - files = [] - tmppath = basedir if not basedir: tmppath = self.protocol.cwd + else: + tmppath = basedir + try: r = self.protocol.fs.resolve_path(tmppath, self.protocol.cwd) except Exception: return + + files = [] for x in self.protocol.fs.get_path(r): if clue == "": files.append(x) @@ -498,7 +505,6 @@ def __init__( self.redirect = redirect # dont send to terminal if enabled def connectionMade(self) -> None: - self.input_data = b"" def outReceived(self, data: bytes) -> None: @@ -535,10 +541,10 @@ def errReceived(self, data: bytes) -> None: self.protocol.terminal.write(data) self.err_data = self.err_data + data - def inConnectionLost(self): + def inConnectionLost(self) -> None: pass - def outConnectionLost(self): + def outConnectionLost(self) -> None: """ Called from HoneyPotBaseProtocol.call_command() to run a next command in the chain """ @@ -549,11 +555,11 @@ def outConnectionLost(self): npcmdargs = self.next_command.cmdargs self.protocol.call_command(self.next_command, npcmd, *npcmdargs) - def errConnectionLost(self): + def errConnectionLost(self) -> None: pass - def processExited(self, reason): + def processExited(self, reason: failure.Failure) -> None: log.msg(f"processExited for {self.cmd}, status {reason.value.exitCode}") - def processEnded(self, reason): + def processEnded(self, reason: failure.Failure) -> None: log.msg(f"processEnded for {self.cmd}, status {reason.value.exitCode}") diff --git a/src/cowrie/shell/protocol.py b/src/cowrie/shell/protocol.py index f620541d45..541f60a94b 100644 --- a/src/cowrie/shell/protocol.py +++ b/src/cowrie/shell/protocol.py @@ -9,6 +9,7 @@ import sys import time import traceback +from typing import ClassVar from twisted.conch import recvline from twisted.conch.insults import insults @@ -26,7 +27,7 @@ class HoneyPotBaseProtocol(insults.TerminalProtocol, TimeoutMixin): Base protocol for interactive and non-interactive use """ - commands = {} + commands: ClassVar = {} for c in cowrie.commands.__all__: try: module = __import__( @@ -289,6 +290,7 @@ def timeoutConnection(self) -> None: """ this logs out when connection times out """ + assert self.terminal is not None self.terminal.write(b"timed out waiting for input: auto-logout\n") HoneyPotBaseProtocol.timeoutConnection(self) @@ -318,14 +320,15 @@ def characterReceived(self, ch, moreCharactersComing): self.lineBuffer[self.lineBufferIndex : self.lineBufferIndex + 1] = [ch] self.lineBufferIndex += 1 if not self.password_input: + assert self.terminal is not None self.terminal.write(ch) - def handle_RETURN(self): + def handle_RETURN(self) -> None: if len(self.cmdstack) == 1: if self.lineBuffer: self.historyLines.append(b"".join(self.lineBuffer)) self.historyPosition = len(self.historyLines) - return recvline.RecvLine.handle_RETURN(self) + recvline.RecvLine.handle_RETURN(self) def handle_CTRL_C(self) -> None: if self.cmdstack: @@ -340,6 +343,7 @@ def handle_TAB(self) -> None: self.cmdstack[-1].handle_TAB() def handle_CTRL_K(self) -> None: + assert self.terminal is not None self.terminal.eraseToLineEnd() self.lineBuffer = self.lineBuffer[0 : self.lineBufferIndex] @@ -348,11 +352,13 @@ def handle_CTRL_L(self) -> None: Handle a 'form feed' byte - generally used to request a screen refresh/redraw. """ + assert self.terminal is not None self.terminal.eraseDisplay() self.terminal.cursorHome() self.drawInputLine() def handle_CTRL_U(self) -> None: + assert self.terminal is not None for _ in range(self.lineBufferIndex): self.terminal.cursorBackward() self.terminal.deleteCharacter() diff --git a/src/cowrie/shell/pwd.py b/src/cowrie/shell/pwd.py index a7dc7b9709..07eb8f4460 100644 --- a/src/cowrie/shell/pwd.py +++ b/src/cowrie/shell/pwd.py @@ -30,7 +30,7 @@ from __future__ import annotations from binascii import crc32 from random import randint, seed -from typing import Any, Union +from typing import Any from twisted.python import log @@ -45,7 +45,7 @@ class Passwd: """ passwd_file = "{}/etc/passwd".format(CowrieConfig.get("honeypot", "contents_path")) - passwd: list[dict[str, Any]] = [] + passwd: list[dict[str, Any]] def __init__(self) -> None: self.load() @@ -82,7 +82,7 @@ def load(self) -> None: pw_shell, ) = line.split(":") - e: dict[str, Union[str, int]] = {} + e: dict[str, str | int] = {} e["pw_name"] = pw_name e["pw_passwd"] = pw_passwd e["pw_gecos"] = pw_gecos @@ -180,7 +180,7 @@ def load(self) -> None: (gr_name, _, gr_gid, gr_mem) = line.split(":") - e: dict[str, Union[str, int]] = {} + e: dict[str, str | int] = {} e["gr_name"] = gr_name try: e["gr_gid"] = int(gr_gid) diff --git a/src/cowrie/shell/server.py b/src/cowrie/shell/server.py index c3120139f6..862e61aa04 100644 --- a/src/cowrie/shell/server.py +++ b/src/cowrie/shell/server.py @@ -50,11 +50,10 @@ class CowrieServer: multiple Cowrie connections """ - fs = None - process = None - hostname: str = CowrieConfig.get("honeypot", "hostname") - def __init__(self, realm: IRealm) -> None: + self.fs = None + self.process = None + self.hostname: str = CowrieConfig.get("honeypot", "hostname") try: arches = [ arch.strip() for arch in CowrieConfig.get("shell", "arch").split(",") diff --git a/src/cowrie/ssh/factory.py b/src/cowrie/ssh/factory.py index 6399343bfb..fa50189228 100644 --- a/src/cowrie/ssh/factory.py +++ b/src/cowrie/ssh/factory.py @@ -9,7 +9,6 @@ from configparser import NoOptionError import time -from typing import Optional from twisted.conch.openssh_compat import primes from twisted.conch.ssh import factory, keys, transport @@ -31,11 +30,11 @@ class CowrieSSHFactory(factory.SSHFactory): They listen directly to the TCP port """ - starttime: Optional[float] = None - privateKeys: dict[bytes, bytes] = {} - publicKeys: dict[bytes, bytes] = {} + starttime: float | None = None + privateKeys: dict[bytes, bytes] + publicKeys: dict[bytes, bytes] primes = None - portal: Optional[tp.Portal] = None # gets set by plugin + portal: tp.Portal | None = None # gets set by plugin ourVersionString: bytes = CowrieConfig.get( "ssh", "version", fallback="SSH-2.0-OpenSSH_6.0p1 Debian-4+deb7u2" ).encode("ascii") @@ -43,6 +42,8 @@ class CowrieSSHFactory(factory.SSHFactory): def __init__(self, backend, pool_handler): self.pool_handler = pool_handler self.backend: str = backend + self.privateKeys = {} + self.publicKeys = {} self.services = { b"ssh-userauth": ProxySSHAuthServer if self.backend == "proxy" diff --git a/src/cowrie/ssh/forwarding.py b/src/cowrie/ssh/forwarding.py index a312efa623..c3acabb928 100644 --- a/src/cowrie/ssh/forwarding.py +++ b/src/cowrie/ssh/forwarding.py @@ -171,6 +171,7 @@ def write(self, data: bytes) -> None: except ValueError: log.err("Failed to parse TCP tunnel response code") self._close("Connection refused") + return if res_code != 200: log.err(f"Unexpected response code: {res_code}") self._close("Connection refused") diff --git a/src/cowrie/ssh/session.py b/src/cowrie/ssh/session.py index f29b308bd8..b795c40aca 100644 --- a/src/cowrie/ssh/session.py +++ b/src/cowrie/ssh/session.py @@ -8,6 +8,8 @@ from __future__ import annotations +from typing import Literal + from twisted.conch.ssh import session from twisted.conch.ssh.common import getNS from twisted.python import log @@ -21,7 +23,7 @@ class HoneyPotSSHSession(session.SSHSession): def __init__(self, *args, **kw): session.SSHSession.__init__(self, *args, **kw) - def request_env(self, data: bytes) -> int: + def request_env(self, data: bytes) -> Literal[0, 1]: name, rest = getNS(data) value, rest = getNS(rest) if rest: diff --git a/src/cowrie/ssh/transport.py b/src/cowrie/ssh/transport.py index d20c638c93..c71df583d1 100644 --- a/src/cowrie/ssh/transport.py +++ b/src/cowrie/ssh/transport.py @@ -121,7 +121,7 @@ def dataReceived(self, data: bytes) -> None: ), format="Remote SSH version: %(version)s", ) - m = re.match(rb"SSH-(\d+.\d+)-(.*)", self.otherVersionString) + m = re.match(rb"SSH-(\d+\.\d+)-(.*)", self.otherVersionString) if m is None: log.msg( f"Bad protocol version identification: {self.otherVersionString!r}" diff --git a/src/cowrie/ssh/userauth.py b/src/cowrie/ssh/userauth.py index 47033f4d57..93601258a1 100644 --- a/src/cowrie/ssh/userauth.py +++ b/src/cowrie/ssh/userauth.py @@ -29,7 +29,7 @@ class HoneyPotSSHUserAuthServer(userauth.SSHUserAuthServer): """ bannerSent: bool = False - user: str + user: bytes _pamDeferred: defer.Deferred | None def serviceStarted(self) -> None: @@ -142,7 +142,7 @@ def _pamConv(self, items: list[tuple[Any, int]]) -> defer.Deferred: elif kind in (3, 4): return defer.fail(error.ConchError("cannot handle PAM 3 or 4 messages")) else: - return defer.fail(error.ConchError(f"bad PAM auth kind {kind}" )) + return defer.fail(error.ConchError(f"bad PAM auth kind {kind}")) packet = NS(b"") + NS(b"") + NS(b"") packet += struct.pack(">L", len(resp)) for prompt, echo in resp: diff --git a/src/cowrie/ssh_proxy/client_transport.py b/src/cowrie/ssh_proxy/client_transport.py index 6c8eaa981f..cd0381093c 100644 --- a/src/cowrie/ssh_proxy/client_transport.py +++ b/src/cowrie/ssh_proxy/client_transport.py @@ -29,7 +29,6 @@ def get_string(data: bytes) -> tuple[int, bytes]: class BackendSSHFactory(protocol.ClientFactory): - server: Any def buildProtocol(self, addr): @@ -58,7 +57,7 @@ def connectionMade(self): self.factory.server.sshParse.set_client(self) transport.SSHClientTransport.connectionMade(self) - def verifyHostKey(self, pub_key, fingerprint): + def verifyHostKey(self, hostKey, fingerprint): return defer.succeed(True) def connectionSecure(self): @@ -143,11 +142,11 @@ def timeoutConnection(self): self.transport.loseConnection() self.factory.server.transport.loseConnection() - def dispatchMessage(self, message_num, payload): - if message_num in [6, 52]: + def dispatchMessage(self, messageNum, payload): + if messageNum in [6, 52]: return # TODO consume these in authenticateBackend - if message_num == 98: + if messageNum == 98: # looking for RFC 4254 - 6.10. Returning Exit Status pointer = 4 # ignore recipient_channel leng, message = get_string(payload[pointer:]) @@ -158,9 +157,9 @@ def dispatchMessage(self, message_num, payload): log.msg(f"exitCode: {exit_status}") if transport.SSHClientTransport.isEncrypted(self, "both"): - self.packet_buffer(message_num, payload) + self.packet_buffer(messageNum, payload) else: - transport.SSHClientTransport.dispatchMessage(self, message_num, payload) + transport.SSHClientTransport.dispatchMessage(self, messageNum, payload) def packet_buffer(self, message_num: int, payload: bytes) -> None: """ diff --git a/src/cowrie/ssh_proxy/protocols/port_forward.py b/src/cowrie/ssh_proxy/protocols/port_forward.py index 81f35f44b8..51e2dff1f4 100644 --- a/src/cowrie/ssh_proxy/protocols/port_forward.py +++ b/src/cowrie/ssh_proxy/protocols/port_forward.py @@ -38,5 +38,5 @@ class PortForward(base_protocol.BaseProtocol): def __init__(self, uuid, chan_name, ssh): super().__init__(uuid, chan_name, ssh) - def parse_packet(self, parent: str, payload: bytes) -> None: + def parse_packet(self, parent: str, data: bytes) -> None: pass diff --git a/src/cowrie/ssh_proxy/protocols/sftp.py b/src/cowrie/ssh_proxy/protocols/sftp.py index b4ebda2588..9caf972778 100644 --- a/src/cowrie/ssh_proxy/protocols/sftp.py +++ b/src/cowrie/ssh_proxy/protocols/sftp.py @@ -33,6 +33,43 @@ from cowrie.ssh_proxy.protocols import base_protocol +PACKETLAYOUT = { + 1: "SSH_FXP_INIT", + # ['uint32', 'version'], [['string', 'extension_name'], ['string', 'extension_data']]] + 2: "SSH_FXP_VERSION", + # [['uint32', 'version'], [['string', 'extension_name'], ['string', 'extension_data']]] + 3: "SSH_FXP_OPEN", + # [['uint32', 'id'], ['string', 'filename'], ['uint32', 'pflags'], ['ATTRS', 'attrs']] + 4: "SSH_FXP_CLOSE", # [['uint32', 'id'], ['string', 'handle']] + 5: "SSH_FXP_READ", # [['uint32', 'id'], ['string', 'handle'], ['uint64', 'offset'], ['uint32', 'len']] + 6: "SSH_FXP_WRITE", + # [['uint32', 'id'], ['string', 'handle'], ['uint64', 'offset'], ['string', 'data']] + 7: "SSH_FXP_LSTAT", # [['uint32', 'id'], ['string', 'path']] + 8: "SSH_FXP_FSTAT", # [['uint32', 'id'], ['string', 'handle']] + 9: "SSH_FXP_SETSTAT", # [['uint32', 'id'], ['string', 'path'], ['ATTRS', 'attrs']] + 10: "SSH_FXP_FSETSTAT", # [['uint32', 'id'], ['string', 'handle'], ['ATTRS', 'attrs']] + 11: "SSH_FXP_OPENDIR", # [['uint32', 'id'], ['string', 'path']] + 12: "SSH_FXP_READDIR", # [['uint32', 'id'], ['string', 'handle']] + 13: "SSH_FXP_REMOVE", # [['uint32', 'id'], ['string', 'filename']] + 14: "SSH_FXP_MKDIR", # [['uint32', 'id'], ['string', 'path'], ['ATTRS', 'attrs']] + 15: "SSH_FXP_RMDIR", # [['uint32', 'id'], ['string', 'path']] + 16: "SSH_FXP_REALPATH", # [['uint32', 'id'], ['string', 'path']] + 17: "SSH_FXP_STAT", # [['uint32', 'id'], ['string', 'path']] + 18: "SSH_FXP_RENAME", # [['uint32', 'id'], ['string', 'oldpath'], ['string', 'newpath']] + 19: "SSH_FXP_READLINK", # [['uint32', 'id'], ['string', 'path']] + 20: "SSH_FXP_SYMLINK", # [['uint32', 'id'], ['string', 'linkpath'], ['string', 'targetpath']] + 101: "SSH_FXP_STATUS", + # [['uint32', 'id'], ['uint32', 'error_code'], ['string', 'error_message'], ['string', 'language']] + 102: "SSH_FXP_HANDLE", # [['uint32', 'id'], ['string', 'handle']] + 103: "SSH_FXP_DATA", # [['uint32', 'id'], ['string', 'data']] + 104: "SSH_FXP_NAME", + # [['uint32', 'id'], ['uint32', 'count'], [['string', 'filename'], ['string', 'longname'], ['ATTRS', 'attrs']]] + 105: "SSH_FXP_ATTRS", # [['uint32', 'id'], ['ATTRS', 'attrs']] + 200: "SSH_FXP_EXTENDED", # [] + 201: "SSH_FXP_EXTENDED_REPLY", # [] +} + + class SFTP(base_protocol.BaseProtocol): prevID: int = 0 ID: int = 0 @@ -43,42 +80,6 @@ class SFTP(base_protocol.BaseProtocol): payloadOffset: int = 0 theFile: bytes = b"" - packetLayout = { - 1: "SSH_FXP_INIT", - # ['uint32', 'version'], [['string', 'extension_name'], ['string', 'extension_data']]] - 2: "SSH_FXP_VERSION", - # [['uint32', 'version'], [['string', 'extension_name'], ['string', 'extension_data']]] - 3: "SSH_FXP_OPEN", - # [['uint32', 'id'], ['string', 'filename'], ['uint32', 'pflags'], ['ATTRS', 'attrs']] - 4: "SSH_FXP_CLOSE", # [['uint32', 'id'], ['string', 'handle']] - 5: "SSH_FXP_READ", # [['uint32', 'id'], ['string', 'handle'], ['uint64', 'offset'], ['uint32', 'len']] - 6: "SSH_FXP_WRITE", - # [['uint32', 'id'], ['string', 'handle'], ['uint64', 'offset'], ['string', 'data']] - 7: "SSH_FXP_LSTAT", # [['uint32', 'id'], ['string', 'path']] - 8: "SSH_FXP_FSTAT", # [['uint32', 'id'], ['string', 'handle']] - 9: "SSH_FXP_SETSTAT", # [['uint32', 'id'], ['string', 'path'], ['ATTRS', 'attrs']] - 10: "SSH_FXP_FSETSTAT", # [['uint32', 'id'], ['string', 'handle'], ['ATTRS', 'attrs']] - 11: "SSH_FXP_OPENDIR", # [['uint32', 'id'], ['string', 'path']] - 12: "SSH_FXP_READDIR", # [['uint32', 'id'], ['string', 'handle']] - 13: "SSH_FXP_REMOVE", # [['uint32', 'id'], ['string', 'filename']] - 14: "SSH_FXP_MKDIR", # [['uint32', 'id'], ['string', 'path'], ['ATTRS', 'attrs']] - 15: "SSH_FXP_RMDIR", # [['uint32', 'id'], ['string', 'path']] - 16: "SSH_FXP_REALPATH", # [['uint32', 'id'], ['string', 'path']] - 17: "SSH_FXP_STAT", # [['uint32', 'id'], ['string', 'path']] - 18: "SSH_FXP_RENAME", # [['uint32', 'id'], ['string', 'oldpath'], ['string', 'newpath']] - 19: "SSH_FXP_READLINK", # [['uint32', 'id'], ['string', 'path']] - 20: "SSH_FXP_SYMLINK", # [['uint32', 'id'], ['string', 'linkpath'], ['string', 'targetpath']] - 101: "SSH_FXP_STATUS", - # [['uint32', 'id'], ['uint32', 'error_code'], ['string', 'error_message'], ['string', 'language']] - 102: "SSH_FXP_HANDLE", # [['uint32', 'id'], ['string', 'handle']] - 103: "SSH_FXP_DATA", # [['uint32', 'id'], ['string', 'data']] - 104: "SSH_FXP_NAME", - # [['uint32', 'id'], ['uint32', 'count'], [['string', 'filename'], ['string', 'longname'], ['ATTRS', 'attrs']]] - 105: "SSH_FXP_ATTRS", # [['uint32', 'id'], ['ATTRS', 'attrs']] - 200: "SSH_FXP_EXTENDED", # [] - 201: "SSH_FXP_EXTENDED_REPLY", # [] - } - def __init__(self, uuid, chan_name, ssh): super().__init__(uuid, chan_name, ssh) @@ -88,7 +89,7 @@ def __init__(self, uuid, chan_name, ssh): self.parent: str self.offset: int = 0 - def parse_packet(self, parent: str, payload: bytes) -> None: + def parse_packet(self, parent: str, data: bytes) -> None: self.parent = parent if parent == "[SERVER]": @@ -99,28 +100,28 @@ def parse_packet(self, parent: str, payload: bytes) -> None: raise Exception if self.parentPacket.packetSize == 0: - self.parentPacket.packetSize = int(payload[:4].hex(), 16) - len(payload[4:]) - payload = payload[4:] - self.parentPacket.data = payload - payload = b"" + self.parentPacket.packetSize = int(data[:4].hex(), 16) - len(data[4:]) + data = data[4:] + self.parentPacket.data = data + data = b"" else: - if len(payload) > self.parentPacket.packetSize: + if len(data) > self.parentPacket.packetSize: self.parentPacket.data = ( - self.parentPacket.data + payload[: self.parentPacket.packetSize] + self.parentPacket.data + data[: self.parentPacket.packetSize] ) - payload = payload[self.parentPacket.packetSize :] + data = data[self.parentPacket.packetSize :] self.parentPacket.packetSize = 0 else: - self.parentPacket.packetSize -= len(payload) - self.parentPacket.data = self.parentPacket.data + payload - payload = b"" + self.parentPacket.packetSize -= len(data) + self.parentPacket.data = self.parentPacket.data + data + data = b"" if self.parentPacket.packetSize == 0: self.handle_packet(parent) - if len(payload) != 0: - self.parse_packet(parent, payload) + if len(data) != 0: + self.parse_packet(parent, data) def handle_packet(self, parent: str) -> None: self.packetSize: int = self.parentPacket.packetSize @@ -128,7 +129,7 @@ def handle_packet(self, parent: str) -> None: self.command: bytes sftp_num: int = self.extract_int(1) - packet: str = self.packetLayout[sftp_num] + packet: str = PACKETLAYOUT[sftp_num] self.prevID: int = self.ID self.ID: int = self.extract_int(4) diff --git a/src/cowrie/ssh_proxy/protocols/ssh.py b/src/cowrie/ssh_proxy/protocols/ssh.py index 28062e491d..c12d6d2581 100644 --- a/src/cowrie/ssh_proxy/protocols/ssh.py +++ b/src/cowrie/ssh_proxy/protocols/ssh.py @@ -44,46 +44,46 @@ ) from cowrie.ssh_proxy.util import int_to_hex, string_to_hex +PACKETLAYOUT = { + 1: "SSH_MSG_DISCONNECT", # ['uint32', 'reason_code'], ['string', 'reason'], ['string', 'language_tag'] + 2: "SSH_MSG_IGNORE", # ['string', 'data'] + 3: "SSH_MSG_UNIMPLEMENTED", # ['uint32', 'seq_no'] + 4: "SSH_MSG_DEBUG", # ['boolean', 'always_display'] + 5: "SSH_MSG_SERVICE_REQUEST", # ['string', 'service_name'] + 6: "SSH_MSG_SERVICE_ACCEPT", # ['string', 'service_name'] + 20: "SSH_MSG_KEXINIT", # ['string', 'service_name'] + 21: "SSH_MSG_NEWKEYS", + 50: "SSH_MSG_USERAUTH_REQUEST", # ['string', 'username'], ['string', 'service_name'], ['string', 'method_name'] + 51: "SSH_MSG_USERAUTH_FAILURE", # ['name-list', 'authentications'], ['boolean', 'partial_success'] + 52: "SSH_MSG_USERAUTH_SUCCESS", # + 53: "SSH_MSG_USERAUTH_BANNER", # ['string', 'message'], ['string', 'language_tag'] + 60: "SSH_MSG_USERAUTH_INFO_REQUEST", # ['string', 'name'], ['string', 'instruction'], + # ['string', 'language_tag'], ['uint32', 'num-prompts'], + # ['string', 'prompt[x]'], ['boolean', 'echo[x]'] + 61: "SSH_MSG_USERAUTH_INFO_RESPONSE", # ['uint32', 'num-responses'], ['string', 'response[x]'] + 80: "SSH_MSG_GLOBAL_REQUEST", # ['string', 'request_name'], ['boolean', 'want_reply'] #tcpip-forward + 81: "SSH_MSG_REQUEST_SUCCESS", + 82: "SSH_MSG_REQUEST_FAILURE", + 90: "SSH_MSG_CHANNEL_OPEN", # ['string', 'channel_type'], ['uint32', 'sender_channel'], + # ['uint32', 'initial_window_size'], ['uint32', 'maximum_packet_size'], + 91: "SSH_MSG_CHANNEL_OPEN_CONFIRMATION", # ['uint32', 'recipient_channel'], ['uint32', 'sender_channel'], + # ['uint32', 'initial_window_size'], ['uint32', 'maximum_packet_size'] + 92: "SSH_MSG_CHANNEL_OPEN_FAILURE", # ['uint32', 'recipient_channel'], ['uint32', 'reason_code'], + # ['string', 'reason'], ['string', 'language_tag'] + 93: "SSH_MSG_CHANNEL_WINDOW_ADJUST", # ['uint32', 'recipient_channel'], ['uint32', 'additional_bytes'] + 94: "SSH_MSG_CHANNEL_DATA", # ['uint32', 'recipient_channel'], ['string', 'data'] + 95: "SSH_MSG_CHANNEL_EXTENDED_DATA", # ['uint32', 'recipient_channel'], + # ['uint32', 'data_type_code'], ['string', 'data'] + 96: "SSH_MSG_CHANNEL_EOF", # ['uint32', 'recipient_channel'] + 97: "SSH_MSG_CHANNEL_CLOSE", # ['uint32', 'recipient_channel'] + 98: "SSH_MSG_CHANNEL_REQUEST", # ['uint32', 'recipient_channel'], ['string', 'request_type'], + # ['boolean', 'want_reply'] + 99: "SSH_MSG_CHANNEL_SUCCESS", + 100: "SSH_MSG_CHANNEL_FAILURE", +} -class SSH(base_protocol.BaseProtocol): - packetLayout = { - 1: "SSH_MSG_DISCONNECT", # ['uint32', 'reason_code'], ['string', 'reason'], ['string', 'language_tag'] - 2: "SSH_MSG_IGNORE", # ['string', 'data'] - 3: "SSH_MSG_UNIMPLEMENTED", # ['uint32', 'seq_no'] - 4: "SSH_MSG_DEBUG", # ['boolean', 'always_display'] - 5: "SSH_MSG_SERVICE_REQUEST", # ['string', 'service_name'] - 6: "SSH_MSG_SERVICE_ACCEPT", # ['string', 'service_name'] - 20: "SSH_MSG_KEXINIT", # ['string', 'service_name'] - 21: "SSH_MSG_NEWKEYS", - 50: "SSH_MSG_USERAUTH_REQUEST", # ['string', 'username'], ['string', 'service_name'], ['string', 'method_name'] - 51: "SSH_MSG_USERAUTH_FAILURE", # ['name-list', 'authentications'], ['boolean', 'partial_success'] - 52: "SSH_MSG_USERAUTH_SUCCESS", # - 53: "SSH_MSG_USERAUTH_BANNER", # ['string', 'message'], ['string', 'language_tag'] - 60: "SSH_MSG_USERAUTH_INFO_REQUEST", # ['string', 'name'], ['string', 'instruction'], - # ['string', 'language_tag'], ['uint32', 'num-prompts'], - # ['string', 'prompt[x]'], ['boolean', 'echo[x]'] - 61: "SSH_MSG_USERAUTH_INFO_RESPONSE", # ['uint32', 'num-responses'], ['string', 'response[x]'] - 80: "SSH_MSG_GLOBAL_REQUEST", # ['string', 'request_name'], ['boolean', 'want_reply'] #tcpip-forward - 81: "SSH_MSG_REQUEST_SUCCESS", - 82: "SSH_MSG_REQUEST_FAILURE", - 90: "SSH_MSG_CHANNEL_OPEN", # ['string', 'channel_type'], ['uint32', 'sender_channel'], - # ['uint32', 'initial_window_size'], ['uint32', 'maximum_packet_size'], - 91: "SSH_MSG_CHANNEL_OPEN_CONFIRMATION", # ['uint32', 'recipient_channel'], ['uint32', 'sender_channel'], - # ['uint32', 'initial_window_size'], ['uint32', 'maximum_packet_size'] - 92: "SSH_MSG_CHANNEL_OPEN_FAILURE", # ['uint32', 'recipient_channel'], ['uint32', 'reason_code'], - # ['string', 'reason'], ['string', 'language_tag'] - 93: "SSH_MSG_CHANNEL_WINDOW_ADJUST", # ['uint32', 'recipient_channel'], ['uint32', 'additional_bytes'] - 94: "SSH_MSG_CHANNEL_DATA", # ['uint32', 'recipient_channel'], ['string', 'data'] - 95: "SSH_MSG_CHANNEL_EXTENDED_DATA", # ['uint32', 'recipient_channel'], - # ['uint32', 'data_type_code'], ['string', 'data'] - 96: "SSH_MSG_CHANNEL_EOF", # ['uint32', 'recipient_channel'] - 97: "SSH_MSG_CHANNEL_CLOSE", # ['uint32', 'recipient_channel'] - 98: "SSH_MSG_CHANNEL_REQUEST", # ['uint32', 'recipient_channel'], ['string', 'request_type'], - # ['boolean', 'want_reply'] - 99: "SSH_MSG_CHANNEL_SUCCESS", - 100: "SSH_MSG_CHANNEL_FAILURE", - } +class SSH(base_protocol.BaseProtocol): def __init__(self, server): super().__init__() @@ -106,8 +106,8 @@ def parse_num_packet(self, parent: str, message_num: int, payload: bytes) -> Non self.packetSize = len(payload) self.sendOn = True - if message_num in self.packetLayout: - packet = self.packetLayout[message_num] + if message_num in PACKETLAYOUT: + packet = PACKETLAYOUT[message_num] else: packet = f"UNKNOWN_{message_num}" @@ -364,7 +364,7 @@ def parse_num_packet(self, parent: str, message_num: int, payload: bytes) -> Non self.server.sendPacket(message_num, payload) def send_back(self, parent, message_num, payload): - packet = self.packetLayout[message_num] + packet = PACKETLAYOUT[message_num] if parent == "[SERVER]": direction = "PROXY -> FRONTEND" diff --git a/src/cowrie/ssh_proxy/protocols/term.py b/src/cowrie/ssh_proxy/protocols/term.py index 1cc36810b9..e8573e0f86 100644 --- a/src/cowrie/ssh_proxy/protocols/term.py +++ b/src/cowrie/ssh_proxy/protocols/term.py @@ -89,8 +89,8 @@ def channel_closed(self) -> None: duration=time.time() - self.startTime, ) - def parse_packet(self, parent: str, payload: bytes) -> None: - self.data: bytes = payload + def parse_packet(self, parent: str, data: bytes) -> None: + self.data: bytes = data if parent == "[SERVER]": while len(self.data) > 0: @@ -164,13 +164,13 @@ def parse_packet(self, parent: str, payload: bytes) -> None: self.data = self.data[1:] if self.ttylogEnabled: - self.ttylogSize += len(payload) + self.ttylogSize += len(data) ttylog.ttylog_write( self.ttylogFile, - len(payload), + len(data), ttylog.TYPE_OUTPUT, time.time(), - payload, + data, ) elif parent == "[CLIENT]": @@ -216,11 +216,11 @@ def parse_packet(self, parent: str, payload: bytes) -> None: self.upArrow = False if self.ttylogEnabled: - self.ttylogSize += len(payload) + self.ttylogSize += len(data) ttylog.ttylog_write( self.ttylogFile, - len(payload), + len(data), ttylog.TYPE_INPUT, time.time(), - payload, + data, ) diff --git a/src/cowrie/ssh_proxy/server_transport.py b/src/cowrie/ssh_proxy/server_transport.py index d4955df876..01bbd890f5 100644 --- a/src/cowrie/ssh_proxy/server_transport.py +++ b/src/cowrie/ssh_proxy/server_transport.py @@ -239,27 +239,27 @@ def dataReceived(self, data: bytes) -> None: self.sendKexInit() packet = self.getPacket() while packet: - message_num = ord(packet[0:1]) - self.dispatchMessage(message_num, packet[1:]) + messageNum = ord(packet[0:1]) + self.dispatchMessage(messageNum, packet[1:]) packet = self.getPacket() - def dispatchMessage(self, message_num, payload): + def dispatchMessage(self, messageNum, payload): # overriden dispatchMessage sets services, we do that here too then # we're particularly interested in userauth, since Twisted does most of that for us - if message_num == 5: + if messageNum == 5: self.ssh_SERVICE_REQUEST(payload) - elif 50 <= message_num <= 79: # userauth numbers + elif 50 <= messageNum <= 79: # userauth numbers self.frontendAuthenticated = False transport.SSHServerTransport.dispatchMessage( - self, message_num, payload + self, messageNum, payload ) # let userauth deal with it # TODO delay userauth until backend is connected? elif transport.SSHServerTransport.isEncrypted(self, "both"): - self.packet_buffer(message_num, payload) + self.packet_buffer(messageNum, payload) else: - transport.SSHServerTransport.dispatchMessage(self, message_num, payload) + transport.SSHServerTransport.dispatchMessage(self, messageNum, payload) def sendPacket(self, messageType, payload): """ @@ -306,9 +306,7 @@ def ssh_KEXINIT(self, packet): cencCS = ",".join([alg.decode("utf-8") for alg in encCS]) cmacCS = ",".join([alg.decode("utf-8") for alg in macCS]) ccompCS = ",".join([alg.decode("utf-8") for alg in compCS]) - hasshAlgorithms = "{kex};{enc};{mac};{cmp}".format( - kex=ckexAlgs, enc=cencCS, mac=cmacCS, cmp=ccompCS - ) + hasshAlgorithms = f"{ckexAlgs};{cencCS};{cmacCS};{ccompCS}" hassh = md5(hasshAlgorithms.encode("utf-8")).hexdigest() log.msg( @@ -417,7 +415,7 @@ def receiveError(self, reasonCode: str, description: str) -> None: """ log.msg(f"Got remote error, code {reasonCode} reason: {description}") - def packet_buffer(self, message_num: int, payload: bytes) -> None: + def packet_buffer(self, messageNum: int, payload: bytes) -> None: """ We have to wait until we have a connection to the backend is ready. Meanwhile, we hold packets from client to server in here. @@ -425,9 +423,9 @@ def packet_buffer(self, message_num: int, payload: bytes) -> None: if not self.backendConnected: # wait till backend connects to send packets to them log.msg("Connection to backend not ready, buffering packet from frontend") - self.delayedPackets.append([message_num, payload]) + self.delayedPackets.append([messageNum, payload]) else: if len(self.delayedPackets) > 0: - self.delayedPackets.append([message_num, payload]) + self.delayedPackets.append([messageNum, payload]) else: - self.sshParse.parse_num_packet("[SERVER]", message_num, payload) + self.sshParse.parse_num_packet("[SERVER]", messageNum, payload) diff --git a/src/cowrie/telnet/factory.py b/src/cowrie/telnet/factory.py index b2ccca4b48..f586acde06 100644 --- a/src/cowrie/telnet/factory.py +++ b/src/cowrie/telnet/factory.py @@ -27,11 +27,11 @@ class HoneyPotTelnetFactory(protocol.ServerFactory): """ tac: IPlugin - portal: tp.Portal | None = None # gets set by Twisted plugin banner: bytes starttime: float def __init__(self, backend, pool_handler): + self.portal: tp.Portal | None = None # gets set by Twisted plugin self.backend: str = backend self.pool_handler = pool_handler super().__init__() @@ -73,12 +73,3 @@ def stopFactory(self) -> None: Stop output plugins """ protocol.ServerFactory.stopFactory(self) - - def buildProtocol(self, addr): - """ - Overidden so we can keep a reference to running protocols (which is used for testing) - """ - p = self.protocol() - p.factory = self - - return p diff --git a/src/cowrie/telnet/session.py b/src/cowrie/telnet/session.py index b1113a27a9..96e5a5d4e7 100644 --- a/src/cowrie/telnet/session.py +++ b/src/cowrie/telnet/session.py @@ -23,11 +23,11 @@ class HoneyPotTelnetSession(TelnetBootstrapProtocol): id = 0 # telnet can only have 1 simultaneous session, unlike SSH - windowSize = [40, 80] - # to be populated by HoneyPotTelnetAuthProtocol after auth - transportId = None def __init__(self, username, server): + # to be populated by HoneyPotTelnetAuthProtocol after auth + self.transportId = None + self.windowSize = [40, 80] self.username = username.decode() self.server = server diff --git a/src/cowrie/telnet/transport.py b/src/cowrie/telnet/transport.py index 36c90bad28..de5efd6a11 100644 --- a/src/cowrie/telnet/transport.py +++ b/src/cowrie/telnet/transport.py @@ -22,6 +22,7 @@ class CowrieTelnetTransport(TelnetTransport, TimeoutMixin): """ CowrieTelnetTransport """ + def connectionMade(self): self.transportId: str = uuid.uuid4().hex[:12] sessionno = self.transport.sessionno diff --git a/src/cowrie/telnet/userauth.py b/src/cowrie/telnet/userauth.py index 60e2a8760c..c8ce9abe8a 100644 --- a/src/cowrie/telnet/userauth.py +++ b/src/cowrie/telnet/userauth.py @@ -31,7 +31,7 @@ class HoneyPotTelnetAuthProtocol(AuthenticatingTelnetProtocol): loginPrompt = b"login: " passwordPrompt = b"Password: " - windowSize = [40, 80] + windowSize: list[int] def connectionMade(self): # self.transport.negotiationMap[NAWS] = self.telnet_NAWS @@ -41,6 +41,7 @@ def connectionMade(self): # I need to doubly escape here since my underlying # CowrieTelnetTransport hack would remove it and leave just \n + self.windowSize = [40, 80] self.transport.write(self.factory.banner.replace(b"\n", b"\r\r\n")) self.transport.write(self.loginPrompt) @@ -128,22 +129,22 @@ def telnet_NAWS(self, data): else: log.msg("Wrong number of NAWS bytes") - def enableLocal(self, opt): - if opt == ECHO: + def enableLocal(self, option: bytes) -> bool: # type: ignore + if option == ECHO: return True # TODO: check if twisted now supports SGA (see git commit c58056b0) - elif opt == SGA: + elif option == SGA: return False else: return False - def enableRemote(self, opt): + def enableRemote(self, option: bytes) -> bool: # type: ignore # TODO: check if twisted now supports LINEMODE (see git commit c58056b0) - if opt == LINEMODE: + if option == LINEMODE: return False - elif opt == NAWS: + elif option == NAWS: return True - elif opt == SGA: + elif option == SGA: return True else: return False diff --git a/src/cowrie/test/fake_transport.py b/src/cowrie/test/fake_transport.py index 2b968813e8..f02b3c5c08 100644 --- a/src/cowrie/test/fake_transport.py +++ b/src/cowrie/test/fake_transport.py @@ -3,11 +3,15 @@ from __future__ import annotations +from typing import ClassVar + from collections.abc import Callable from twisted.conch.insults import insults from twisted.test import proto_helpers +BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE, N_COLORS = list(range(9)) + class Container: """This class is placeholder for creating a fake interface. @@ -23,7 +27,7 @@ class Container: sessionno = 1 starttime = 0 session: Container | None - sessions: dict[int, str] = {} + sessions: ClassVar[dict[int, str]] = {} conn: Container | None transport: Container | None factory: Container | None @@ -44,11 +48,10 @@ class FakeTransport(proto_helpers.StringTransport): # Thanks to TerminalBuffer (some code was taken from twisted Terminal Buffer) - redirFiles: set[list[str]] = set() + redirFiles: ClassVar[set[list[str]]] = set() width = 80 height = 24 void = object() - BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE, N_COLORS = list(range(9)) for keyID in ( "UP_ARROW", @@ -79,7 +82,7 @@ class FakeTransport(proto_helpers.StringTransport): TAB = "\x09" BACKSPACE = "\x08" - modes: dict[str, Callable] = {} + modes: ClassVar[dict[str, Callable]] = {} # '\x01': self.handle_HOME, # CTRL-A # '\x02': self.handle_LEFT, # CTRL-B @@ -100,17 +103,6 @@ def setModes(self, modes): self.modes[m] = True aborting = False - transport = Container() - transport.session = Container() - transport.session.conn = Container() - transport.session.conn.transport = Container() - transport.session.conn.transport.transport = Container() - transport.session.conn.transport.transport.sessionno = 1 - transport.session.conn.transport.factory = Container() - transport.session.conn.transport.factory.sessions = {} - transport.session.conn.transport.factory.starttime = 0 - factory = Container() - session: dict[str, str] = {} def abortConnection(self): self.aborting = True @@ -150,8 +142,8 @@ def reset(self): "underline": False, "blink": False, "reverseVideo": False, - "foreground": self.WHITE, - "background": self.BLACK, + "foreground": WHITE, + "background": BLACK, } self.charsets = { insults.G0: insults.CS_US, @@ -159,6 +151,20 @@ def reset(self): insults.G2: insults.CS_ALTERNATE, insults.G3: insults.CS_ALTERNATE_SPECIAL, } + + def clear(self): + proto_helpers.StringTransport.clear(self) + self.transport = Container() + self.transport.session = Container() + self.transport.session.conn = Container() + self.transport.session.conn.transport = Container() + self.transport.session.conn.transport.transport = Container() + self.transport.session.conn.transport.transport.sessionno = 1 + self.transport.session.conn.transport.factory = Container() + self.transport.session.conn.transport.factory.sessions = {} + self.transport.session.conn.transport.factory.starttime = 0 + self.factory = Container() + self.session: dict[str, str] = {} self.eraseDisplay() def eraseDisplay(self): diff --git a/src/twisted/plugins/cowrie_plugin.py b/src/twisted/plugins/cowrie_plugin.py index 41a66e0dcd..51dca27768 100644 --- a/src/twisted/plugins/cowrie_plugin.py +++ b/src/twisted/plugins/cowrie_plugin.py @@ -68,8 +68,8 @@ class Options(usage.Options): """ # The '-c' parameters is currently ignored - optParameters: list[str] = [] - optFlags: list[list[str]] = [["help", "h", "Display this help and exit."]] + optParameters: ClassVar[list[str]] = [] + optFlags: ClassVar[list[list[str]]] = [["help", "h", "Display this help and exit."]] @provider(ILogObserver) @@ -90,12 +90,14 @@ class CowrieServiceMaker: tapname: ClassVar[str] = "cowrie" description: ClassVar[str] = "She sells sea shells by the sea shore." options = Options - output_plugins: list[Callable] = [] + output_plugins: list[Callable] topService: service.Service def __init__(self) -> None: self.pool_handler = None + self.output_plugins = [] + # ssh is enabled by default self.enableSSH: bool = CowrieConfig.getboolean("ssh", "enabled", fallback=True) diff --git a/tox.ini b/tox.ini index e5a61be7b3..2b73ed0aa6 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] skipsdist = True -envlist = lint,docs,typing,py38,py39,py310,py311,pypy38,pypy39 +envlist = lint,docs,typing,py38,py39,py310,py311,py312,pypy39, pypy310 deps = -r{toxinidir}/requirements.txt skip_missing_interpreters = True @@ -10,8 +10,9 @@ python = 3.9: py39 3.10: py310, lint, docs, typing 3.11: py311 - pypy-3.8: pypy38 + 3.12: py312 pypy-3.9: pypy39 + pypy-3.10: pypy310 [testenv] setenv = @@ -33,6 +34,7 @@ allowlist_externals = commands = ruff {toxinidir}/src - yamllint {toxinidir} + - pyright - pylint {toxinidir}/src basepython = python3.10