diff --git a/README.md b/README.md index f0ce5b6..9484c18 100644 --- a/README.md +++ b/README.md @@ -403,9 +403,6 @@ qsshserver.kill_server() - Lib: Sockets - Logs: ip, port -## Open Shell -[![Open in Cloud Shell](https://img.shields.io/static/v1?label=%3E_&message=Open%20in%20Cloud%20Shell&color=3267d6&style=flat-square)](https://ssh.cloud.google.com/cloudshell/editor?cloudshell_git_repo=https://github.com/qeeqbox/honeypots&tutorial=README.md) [![Open in repl.it Shell](https://img.shields.io/static/v1?label=%3E_&message=Open%20in%20repl.it%20Shell&color=606c74&style=flat-square)](https://repl.it/github/qeeqbox/honeypots) - ## acknowledgment - By using this framework, you are accepting the license terms of all these packages: `pipenv twisted psutil psycopg2-binary dnspython requests impacket paramiko redis mysql-connector pycryptodome vncdotool service_identity requests[socks] pygments http.server` - Let me know if I missed a reference or resource! diff --git a/README.rst b/README.rst index 9ff29e4..5cbc03e 100644 --- a/README.rst +++ b/README.rst @@ -369,10 +369,6 @@ acknowledgement - By using this framework, you are accepting the license terms of all these packages: `pipenv twisted psutil psycopg2-binary dnspython requests impacket paramiko redis mysql-connector pycryptodome vncdotool service_identity requests[socks] pygments http.server` - Let me know if I missed a reference or resource! -Some Articles -============= -- `securityonline `_ - Notes ===== - Almost all servers and emulators are stripped-down - You can adjust that as needed diff --git a/honeypots/base_server.py b/honeypots/base_server.py index c751283..5ce54b1 100644 --- a/honeypots/base_server.py +++ b/honeypots/base_server.py @@ -28,13 +28,13 @@ class BaseServer(ABC): def __init__(self, **kwargs): self.auto_disabled = False self.process = None - self.uuid = f"honeypotslogger_{__class__.__name__}_{str(uuid4())[:8]}" + self.uuid = f"honeypotslogger_{self.__class__.__name__}_{str(uuid4())[:8]}" self.config = kwargs.get("config", "") if self.config: - self.logs = setup_logger(__class__.__name__, self.uuid, self.config) + self.logs = setup_logger(self.__class__.__name__, self.uuid, self.config) set_local_vars(self, self.config) else: - self.logs = setup_logger(__class__.__name__, self.uuid, None) + self.logs = setup_logger(self.__class__.__name__, self.uuid, None) self.ip = kwargs.get("ip", None) or (hasattr(self, "ip") and self.ip) or "0.0.0.0" self.port = ( (kwargs.get("port", None) and int(kwargs.get("port", None))) diff --git a/honeypots/dhcp_server.py b/honeypots/dhcp_server.py index 05e5c20..d247286 100644 --- a/honeypots/dhcp_server.py +++ b/honeypots/dhcp_server.py @@ -10,50 +10,20 @@ // ------------------------------------------------------------- """ -from os import getenv from socket import inet_aton import struct -from uuid import uuid4 from twisted.internet import reactor from twisted.internet.protocol import DatagramProtocol from honeypots.base_server import BaseServer -from honeypots.helper import ( - server_arguments, - setup_logger, - set_local_vars, - check_bytes, -) +from honeypots.helper import check_bytes, server_arguments class QDHCPServer(BaseServer): NAME = "dhcp_server" DEFAULT_PORT = 67 - def __init__(self, **kwargs): - self.auto_disabled = None - self.process = None - self.uuid = "honeypotslogger" + "_" + __class__.__name__ + "_" + str(uuid4())[:8] - self.config = kwargs.get("config", "") - if self.config: - self.logs = setup_logger(__class__.__name__, self.uuid, self.config) - set_local_vars(self, self.config) - else: - self.logs = setup_logger(__class__.__name__, self.uuid, None) - self.ip = kwargs.get("ip", None) or (hasattr(self, "ip") and self.ip) or "0.0.0.0" - self.port = ( - (kwargs.get("port", None) and int(kwargs.get("port", None))) - or (hasattr(self, "port") and self.port) - or 67 - ) - self.options = ( - kwargs.get("options", "") - or (hasattr(self, "options") and self.options) - or getenv("HONEYPOTS_OPTIONS", "") - or "" - ) - def server_main(self): _q_s = self diff --git a/honeypots/ntp_server.py b/honeypots/ntp_server.py index 9299af0..7f3d8d3 100644 --- a/honeypots/ntp_server.py +++ b/honeypots/ntp_server.py @@ -72,7 +72,7 @@ def datagramReceived(self, data, addr): # noqa: N802 self.transport.write(response, addr) status = "success" except (struct.error, TypeError, IndexError): - status = "error" + status = "failed" _q_s.log( { diff --git a/honeypots/ssh_server.py b/honeypots/ssh_server.py index a530ab9..d685001 100644 --- a/honeypots/ssh_server.py +++ b/honeypots/ssh_server.py @@ -9,6 +9,8 @@ // contributors list qeeqbox/honeypots/graphs/contributors // ------------------------------------------------------------- """ +from __future__ import annotations + import logging from _thread import start_new_thread from binascii import hexlify @@ -20,6 +22,7 @@ from socket import socket, AF_INET, SOCK_STREAM, SOL_SOCKET, SO_REUSEADDR from threading import Event from time import time +from typing import TYPE_CHECKING from paramiko import ( RSAKey, @@ -40,9 +43,17 @@ check_bytes, ) +if TYPE_CHECKING: + from paramiko.channel import Channel + + # deactivate logging output of paramiko logging.getLogger("paramiko").setLevel(logging.CRITICAL) +CTRL_C = b"\x03" +CTRL_D = b"\x04" +ANSI_SEQUENCE = b"\x1b" +DEL = b"\x7f" COMMANDS = { "ls": ( "bin boot cdrom dev etc home lib lib32 libx32 lib64 lost+found media mnt opt proc root " @@ -66,6 +77,7 @@ "Linux n1-v26 5.4.0-26-generic #26-Ubuntu SMP %TIME x86_64 x86_64 x86_64 GNU/Linux" ), } +ANSI_REGEX = re.compile(rb"(?:\x1B[@-_]|[\x80-\x9F])[0-?]*[ -/]*[@-~]") class QSSHServer(BaseServer): @@ -77,17 +89,15 @@ def __init__(self, **kwargs): self.mocking_server = choice( ["OpenSSH 7.5", "OpenSSH 7.3", "Serv-U SSH Server 15.1.1.108", "OpenSSH 6.4"] ) - self.ansi = re.compile(r"(?:\x1B[@-_]|[\x80-\x9F])[0-?]*[ -/]*[@-~]") - def generate_pub_pri_keys(self): - with suppress(Exception): - key = RSAKey.generate(2048) - string_io = StringIO() - key.write_private_key(string_io) - return key.get_base64(), string_io.getvalue() - return None, None + @staticmethod + def generate_pub_pri_keys() -> str: + key = RSAKey.generate(2048) + string_io = StringIO() + key.write_private_key(string_io) + return string_io.getvalue() - def server_main(self): # noqa: C901,PLR0915 + def server_main(self): # noqa: C901 _q_s = self class SSHHandle(ServerInterface): @@ -146,7 +156,6 @@ def check_channel_pty_request(self, *_, **__): return True def handle_connection(client, priv): - t = Transport(client) try: ip, port = client.getpeername() except OSError as err: @@ -159,42 +168,32 @@ def handle_connection(client, priv): "src_port": port, } ) - t.local_version = "SSH-2.0-" + _q_s.mocking_server - t.add_server_key(RSAKey(file_obj=StringIO(priv))) - ssh_handle = SSHHandle(ip, port) - try: - t.start_server(server=ssh_handle) - except (SSHException, EOFError, ConnectionResetError) as err: - _q_s.logger.warning(f"Server error: {err}") - return - conn = t.accept(30) - if "interactive" in _q_s.options and conn is not None: - _handle_interactive_session(conn, ip, port) - with suppress(TimeoutError): - ssh_handle.event.wait(2) - with suppress(Exception): - conn.close() - with suppress(Exception): - t.close() - def _handle_interactive_session(conn, ip, port): + with Transport(client) as session: + session.local_version = f"SSH-2.0-{_q_s.mocking_server}" + session.add_server_key(RSAKey(file_obj=StringIO(priv))) + ssh_handle = SSHHandle(ip, port) + try: + session.start_server(server=ssh_handle) + except (SSHException, EOFError, ConnectionResetError) as err: + _q_s.logger.debug(f"Server error: {err}", exc_info=True) + return + + with session.accept(30) as conn: + if "interactive" in _q_s.options and conn is not None: + _handle_interactive_session(conn, ip, port) + with suppress(TimeoutError): + ssh_handle.event.wait(2) + + def _handle_interactive_session(conn: Channel, ip: str, port: int): conn.send(b"Welcome to Ubuntu 20.04 LTS (GNU/Linux 5.4.0-26-generic x86_64)\r\n\r\n") timeout = time() + 300 while time() < timeout: try: conn.send(b"$ ") - line = "" - while not line.endswith("\x0d") and not line.endswith("\x0a"): - # timeout if the user does not send anything for 10 seconds - conn.settimeout(10) - recv = conn.recv(1).decode() - if not recv: - raise EOFError - if _q_s.ansi.match(recv) is None and recv != "\x7f": - line += recv + line = _receive_line(conn) except (TimeoutError, EOFError): break - line = line.strip() _q_s.log( { "action": "interactive", @@ -203,26 +202,15 @@ def _handle_interactive_session(conn, ip, port): "data": {"command": line}, } ) - if line in COMMANDS: - response = COMMANDS.get(line) - if "%TIME" in response: - response = response.replace( - "%TIME", datetime.now().strftime("%a %b %d %H:%M:%S UTC %Y") - ) - conn.send(f"{response}\r\n".encode()) - elif line.startswith("cd "): - _, target, *_ = line.split(" ") - conn.send(f"sh: 1: cd: can't cd to {target}\r\n".encode()) - elif line == "exit": + if line == "exit": break - else: - conn.send(f"{line}: command not found\r\n".encode()) + _respond(conn, line) sock = socket(AF_INET, SOCK_STREAM) sock.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1) sock.bind((self.ip, self.port)) sock.listen(1) - _, private_key = self.generate_pub_pri_keys() + private_key = self.generate_pub_pri_keys() while True: with suppress(Exception): client, _ = sock.accept() @@ -242,6 +230,67 @@ def test_server(self, ip=None, port=None, username=None, password=None): ssh.connect(_ip, port=_port, username=_username, password=_password) +def _receive_line(conn: Channel) -> str: + line = b"" + while not any(line.endswith(char) for char in [b"\r", b"\n", CTRL_C]): + # timeout if the user does not send anything for 10 seconds + conn.settimeout(10) + # a button press may equate to multiple bytes (e.g. non-ascii chars, + # ANSI sequences, etc.), so we receive more than one byte here + recv = conn.recv(1024) + if not recv or recv == CTRL_D: # capture ctrl+D + conn.send(b"^D\r\n") + raise EOFError + if recv == CTRL_C: + conn.send(b"^C\r\n") + elif recv == b"\r": + # ssh only sends "\r" on enter press so we also need to send "\n" back + conn.send(b"\n") + elif ANSI_SEQUENCE in recv: + recv = ANSI_REGEX.sub(b"", recv) + if DEL in recv: + recv.replace(DEL, b"") + if recv: + line += recv + conn.send(recv) + return line.strip().decode(errors="replace") + + +def _respond(conn: Channel, line: str): + if line == "" or line.endswith(CTRL_C.decode()): + return + if line in COMMANDS: + response = COMMANDS.get(line) + if "%TIME" in response: + response = response.replace( + "%TIME", datetime.now().strftime("%a %b %d %H:%M:%S UTC %Y") + ) + conn.send(f"{response}\r\n".encode()) + elif line.startswith("cd "): + target = _parse_args(line) + if not target: + conn.send(b"\r\n") + else: + if target.startswith("~"): + target = target.replace("~", "/root") + conn.send(f"sh: 1: cd: can't cd to {target}\r\n".encode()) + elif line.startswith("ls "): + target = _parse_args(line) + if not target: + conn.send(f"{COMMANDS['ls']}\r\n".encode()) + else: + conn.send(f"ls: cannot open directory '{target}': Permission denied\r\n".encode()) + else: + conn.send(f"{line}: command not found\r\n".encode()) + + +def _parse_args(line: str) -> str | None: + args = [i for i in line.split(" ")[1:] if i and not i.startswith("-")] + if args: + return args[0] + return None + + if __name__ == "__main__": parsed = server_arguments() if parsed.docker or parsed.aws or parsed.custom: diff --git a/pyproject.toml b/pyproject.toml index ba5ce36..136a61c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta" [project] name = "honeypots" -version = "0.64" +version = "0.65" authors = [ { name = "QeeqBox", email = "gigaqeeq@gmail.com" }, ]