From 5150ddb6e7903b6664c7c6590b39c008aa0f1f4b Mon Sep 17 00:00:00 2001 From: Giga <52905881+giga-a@users.noreply.github.com> Date: Sun, 28 Jan 2024 22:41:28 -0800 Subject: [PATCH 1/9] Update pyproject.toml --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 8e7dbf7..7fe0deb 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" }, ] From 2512c13e02bf67ec797b16c57222b5407b7e576c Mon Sep 17 00:00:00 2001 From: Giga <52905881+giga-a@users.noreply.github.com> Date: Sun, 28 Jan 2024 22:41:42 -0800 Subject: [PATCH 2/9] Update README.rst --- README.rst | 4 ---- 1 file changed, 4 deletions(-) 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 From 425e461d93a0d1e33327d7fac2c9fb00fec2a760 Mon Sep 17 00:00:00 2001 From: Giga <52905881+giga-a@users.noreply.github.com> Date: Sun, 28 Jan 2024 22:41:59 -0800 Subject: [PATCH 3/9] Update README.md --- README.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/README.md b/README.md index 3da7fe6..c265ea3 100644 --- a/README.md +++ b/README.md @@ -401,9 +401,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! From 757b4b0ebba8eaf2daf459299f10f44a0a51a984 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Stucke?= Date: Mon, 29 Jan 2024 09:55:53 +0100 Subject: [PATCH 4/9] ntp server: status naming consistency fix --- honeypots/ntp_server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/honeypots/ntp_server.py b/honeypots/ntp_server.py index 600e3f4..d6894d1 100644 --- a/honeypots/ntp_server.py +++ b/honeypots/ntp_server.py @@ -107,7 +107,7 @@ def datagramReceived(self, data, addr): self.transport.write(response, addr) status = "success" except (struct.error, TypeError, IndexError): - status = "error" + status = "failed" _q_s.logs.info( { From 783439958287d8a62403561e5b4a686f9bbb9f24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Stucke?= Date: Thu, 15 Feb 2024 14:20:53 +0100 Subject: [PATCH 5/9] made sniffer usable without chameleon mode --- honeypots/__main__.py | 39 +++++++++++++++++++++------------------ honeypots/helper.py | 2 +- honeypots/qbsniffer.py | 18 +++++++++++------- 3 files changed, 33 insertions(+), 26 deletions(-) diff --git a/honeypots/__main__.py b/honeypots/__main__.py index b873504..d07ed2c 100755 --- a/honeypots/__main__.py +++ b/honeypots/__main__.py @@ -12,7 +12,6 @@ from pathlib import Path from signal import alarm, SIGALRM, SIGINT, signal, SIGTERM, SIGTSTP from subprocess import Popen -from sys import stdout from time import sleep from typing import Any from uuid import uuid4 @@ -155,7 +154,7 @@ class HoneypotsManager: def __init__(self, options: Namespace, server_args: dict[str, str | int]): self.options = options self.server_args = server_args - self.config_data = self._load_config() if self.options.config else None + self.config_data = self._load_config() if self.options.config else {} self.auto = options.auto if geteuid() != 0 else False self.honeypots: list[tuple[Any, str, bool]] = [] @@ -171,9 +170,11 @@ def main(self): print(service) elif self.options.kill: clean_all() - elif self.options.chameleon and self.config_data is not None: + elif self.options.chameleon and self.config_data: self._start_chameleon_mode() elif self.options.setup: + if self.options.sniffer: + self._set_up_sniffer() self._set_up_honeypots() def _load_config(self): @@ -288,19 +289,6 @@ def _start_chameleon_mode(self): # noqa: C901,PLR0912 logger.error("logging must be configured with db_sqlite or db_postgres") sys.exit(1) - sniffer_filter = self.config_data.get("sniffer_filter") - sniffer_interface = self.config_data.get("sniffer_interface") - if not (sniffer_filter and sniffer_interface): - return - - if not self.options.test and self.options.sniffer: - _check_interfaces(sniffer_interface) - if self.options.iptables: - _fix_ip_tables() - logger.info("[x] Wait for 10 seconds...") - stdout.flush() - sleep(2) - if self.options.config != "": logger.warning( "[x] Config.json file overrides --ip, --port, --username and --password" @@ -330,7 +318,7 @@ def _start_chameleon_mode(self): # noqa: C901,PLR0912 sys.exit(1) if self.options.sniffer: - self._start_sniffer(sniffer_filter, sniffer_interface) + self._set_up_sniffer() if not self.options.test: logger.info("[x] Everything looks good!") @@ -347,6 +335,20 @@ def _setup_logging(self) -> logging.Logger: drop = True return setup_logger("main", uuid, self.options.config, drop) + def _set_up_sniffer(self): + sniffer_filter = self.config_data.get("sniffer_filter") + sniffer_interface = self.config_data.get("sniffer_interface") + if not sniffer_interface: + logger.error('If sniffer is enabled, "sniffer_interface" must be set in the config') + sys.exit(1) + if not self.options.test and self.options.sniffer: + _check_interfaces(sniffer_interface) + if self.options.iptables: + _fix_ip_tables() + logger.info("[x] Wait for iptables update...") + sleep(2) + self._start_sniffer(sniffer_filter, sniffer_interface) + def _start_sniffer(self, sniffer_filter, sniffer_interface): logger.info("[x] Starting sniffer") sniffer = QBSniffer( @@ -355,7 +357,8 @@ def _start_sniffer(self, sniffer_filter, sniffer_interface): config=self.options.config, ) sniffer.run_sniffer(process=True) - self.honeypots.append((sniffer, "sniffer", True)) + sleep(0.1) + self.honeypots.append((sniffer, "sniffer", sniffer.process.is_alive())) def _stats_loop(self, logs): while True: diff --git a/honeypots/helper.py b/honeypots/helper.py index cea01fd..82320b2 100644 --- a/honeypots/helper.py +++ b/honeypots/helper.py @@ -130,7 +130,7 @@ def _parse_record(record: LogRecord, custom_filter: dict, type_: str) -> LogReco return record -def setup_logger(name: str, temp_name: str, config: str, drop: bool = False): +def setup_logger(name: str, temp_name: str, config: str | None, drop: bool = False): logs = "terminal" logs_location = "" config_data = {} diff --git a/honeypots/qbsniffer.py b/honeypots/qbsniffer.py index 14ad1cb..d3c0c61 100644 --- a/honeypots/qbsniffer.py +++ b/honeypots/qbsniffer.py @@ -23,7 +23,7 @@ from scapy.layers.inet import IP, TCP from scapy.sendrecv import send, sniff -from honeypots.helper import server_arguments, setup_logger +from honeypots.helper import server_arguments, set_up_error_logging, setup_logger TCP_SYN_FLAG = 0b10 @@ -86,11 +86,12 @@ def __init__(self, filter_=None, interface=None, config=""): self.logs = setup_logger(__class__.__name__, self.uuid, config) else: self.logs = setup_logger(__class__.__name__, self.uuid, None) + self.logger = set_up_error_logging() - def find_icmp(self, x1, x2): - for _ in self.ICMP_codes: - if x1 == _[0] and x2 == _[1]: - return _[2] + def find_icmp(self, type_, code): + for icmp_type, icmp_code, msg_type in self.ICMP_codes: + if type_ == icmp_type and code == icmp_code: + return msg_type return "None" @staticmethod @@ -104,7 +105,10 @@ def get_layers(packet: Packet) -> Iterable[str]: pass def scapy_sniffer_main(self): - sniff(filter=self.filter, iface=self.interface, prn=self.capture_logic) + try: + sniff(filter=self.filter, iface=self.interface, prn=self.capture_logic) + except PermissionError as error: + self.logger.error(f"Could not start sniffer: {error}") def _get_payloads(self, layers: list[str], packet: Packet): hex_payloads, raw_payloads, _fields = {}, {}, {} @@ -237,7 +241,7 @@ def run_sniffer(self, process=None): else: self.scapy_sniffer_main() - def kill_sniffer(self): + def kill_server(self): self.process.terminate() self.process.join() From 7e6cc418ec5a98c8477b26952560d19a764109e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Stucke?= Date: Thu, 15 Feb 2024 14:35:46 +0100 Subject: [PATCH 6/9] rename sniffer module and add base class to enable configuration through the config file --- honeypots/__init__.py | 4 +- honeypots/__main__.py | 6 +- honeypots/base_server.py | 3 + honeypots/{qbsniffer.py => sniffer.py} | 133 ++++++++++++------------- 4 files changed, 71 insertions(+), 75 deletions(-) rename honeypots/{qbsniffer.py => sniffer.py} (68%) diff --git a/honeypots/__init__.py b/honeypots/__init__.py index be50362..a9c1572 100644 --- a/honeypots/__init__.py +++ b/honeypots/__init__.py @@ -28,7 +28,7 @@ from .pjl_server import QPJLServer from .pop3_server import QPOP3Server from .postgres_server import QPostgresServer -from .qbsniffer import QBSniffer +from .sniffer import QSniffer from .rdp_server import QRDPServer from .redis_server import QRedisServer from .sip_server import QSIPServer @@ -41,7 +41,7 @@ from .vnc_server import QVNCServer __all__ = [ - "QBSniffer", + "QSniffer", "QDHCPServer", "QDNSServer", "QElasticServer", diff --git a/honeypots/__main__.py b/honeypots/__main__.py index d07ed2c..7586be4 100755 --- a/honeypots/__main__.py +++ b/honeypots/__main__.py @@ -20,7 +20,7 @@ from psutil import net_io_counters, Process from honeypots import ( - QBSniffer, + QSniffer, QDHCPServer, QDNSServer, QElasticServer, @@ -351,14 +351,14 @@ def _set_up_sniffer(self): def _start_sniffer(self, sniffer_filter, sniffer_interface): logger.info("[x] Starting sniffer") - sniffer = QBSniffer( + sniffer = QSniffer( filter_=sniffer_filter, interface=sniffer_interface, config=self.options.config, ) sniffer.run_sniffer(process=True) sleep(0.1) - self.honeypots.append((sniffer, "sniffer", sniffer.process.is_alive())) + self.honeypots.append((sniffer, "sniffer", sniffer.server_is_alive())) def _stats_loop(self, logs): while True: diff --git a/honeypots/base_server.py b/honeypots/base_server.py index c751283..6b594c4 100644 --- a/honeypots/base_server.py +++ b/honeypots/base_server.py @@ -88,6 +88,9 @@ def kill_server(self): except TimeoutError: self._server_process.kill() + def server_is_alive(self) -> bool: + return self._server_process and self._server_process.is_alive() + @abstractmethod def server_main(self): pass diff --git a/honeypots/qbsniffer.py b/honeypots/sniffer.py similarity index 68% rename from honeypots/qbsniffer.py rename to honeypots/sniffer.py index d3c0c61..b7deb6a 100644 --- a/honeypots/qbsniffer.py +++ b/honeypots/sniffer.py @@ -12,84 +12,81 @@ from __future__ import annotations +import re from binascii import hexlify from multiprocessing import Process -import re from sys import stdout from typing import Iterable, TYPE_CHECKING -from uuid import uuid4 from netifaces import AF_INET, AF_LINK, ifaddresses from scapy.layers.inet import IP, TCP from scapy.sendrecv import send, sniff -from honeypots.helper import server_arguments, set_up_error_logging, setup_logger - +from honeypots.base_server import BaseServer +from honeypots.helper import server_arguments + +ICMP_CODES = [ + (0, 0, "Echo/Ping reply"), + (3, 0, "Destination network unreachable"), + (3, 1, "Destination host unreachable"), + (3, 2, "Destination protocol unreachable"), + (3, 3, "Destination port unreachable"), + (3, 4, "Fragmentation required"), + (3, 5, "Source route failed"), + (3, 6, "Destination network unknown"), + (3, 7, "Destination host unknown"), + (3, 8, "Source host isolated"), + (3, 9, "Network administratively prohibited"), + (3, 10, "Host administratively prohibited"), + (3, 11, "Network unreachable for TOS"), + (3, 12, "Host unreachable for TOS"), + (3, 13, "Communication administratively prohibited"), + (3, 14, "Host Precedence Violation"), + (3, 15, "Precedence cutoff in effect"), + (4, 0, "Source quench"), + (5, 0, "Redirect Datagram for the Network"), + (5, 1, "Redirect Datagram for the Host"), + (5, 2, "Redirect Datagram for the TOS & network"), + (5, 3, "Redirect Datagram for the TOS & host"), + (8, 0, "Echo/Ping Request"), + (9, 0, "Router advertisement"), + (10, 0, "Router discovery/selection/solicitation"), + (11, 0, "TTL expired in transit"), + (11, 1, "Fragment reassembly time exceeded"), + (12, 0, "Pointer indicates the error"), + (12, 1, "Missing a required option"), + (12, 2, "Bad length"), + (13, 0, "Timestamp"), + (14, 0, "Timestamp Reply"), + (15, 0, "Information Request"), + (16, 0, "Information Reply"), + (17, 0, "Address Mask Request"), + (18, 0, "Address Mask Reply"), + (30, 0, "Information Request"), +] TCP_SYN_FLAG = 0b10 if TYPE_CHECKING: from scapy.packet import Packet -class QBSniffer: - def __init__(self, filter_=None, interface=None, config=""): +class QSniffer(BaseServer): + NAME = "sniffer" + + def __init__(self, filter_=None, interface=None, config="", **kwargs): + super().__init__(config=config, **kwargs) self.current_ip = ifaddresses(interface)[AF_INET][0]["addr"].encode("utf-8") self.current_mac = ifaddresses(interface)[AF_LINK][0]["addr"].encode("utf-8") self.filter = filter_ self.interface = interface self.method = "TCPUDP" - self.ICMP_codes = [ - (0, 0, "Echo/Ping reply"), - (3, 0, "Destination network unreachable"), - (3, 1, "Destination host unreachable"), - (3, 2, "Destination protocol unreachable"), - (3, 3, "Destination port unreachable"), - (3, 4, "Fragmentation required"), - (3, 5, "Source route failed"), - (3, 6, "Destination network unknown"), - (3, 7, "Destination host unknown"), - (3, 8, "Source host isolated"), - (3, 9, "Network administratively prohibited"), - (3, 10, "Host administratively prohibited"), - (3, 11, "Network unreachable for TOS"), - (3, 12, "Host unreachable for TOS"), - (3, 13, "Communication administratively prohibited"), - (3, 14, "Host Precedence Violation"), - (3, 15, "Precedence cutoff in effect"), - (4, 0, "Source quench"), - (5, 0, "Redirect Datagram for the Network"), - (5, 1, "Redirect Datagram for the Host"), - (5, 2, "Redirect Datagram for the TOS & network"), - (5, 3, "Redirect Datagram for the TOS & host"), - (8, 0, "Echo/Ping Request"), - (9, 0, "Router advertisement"), - (10, 0, "Router discovery/selection/solicitation"), - (11, 0, "TTL expired in transit"), - (11, 1, "Fragment reassembly time exceeded"), - (12, 0, "Pointer indicates the error"), - (12, 1, "Missing a required option"), - (12, 2, "Bad length"), - (13, 0, "Timestamp"), - (14, 0, "Timestamp Reply"), - (15, 0, "Information Request"), - (16, 0, "Information Reply"), - (17, 0, "Address Mask Request"), - (18, 0, "Address Mask Reply"), - (30, 0, "Information Request"), - ] self.allowed_ports = [] self.allowed_ips = [] self.common = re.compile(rb"pass|user|login") - self.uuid = f"honeypotslogger_{__class__.__name__}_{str(uuid4())[:8]}" - self.config = config - if config: - self.logs = setup_logger(__class__.__name__, self.uuid, config) - else: - self.logs = setup_logger(__class__.__name__, self.uuid, None) - self.logger = set_up_error_logging() - def find_icmp(self, type_, code): - for icmp_type, icmp_code, msg_type in self.ICMP_codes: + @staticmethod + def find_icmp(type_, code): + for icmp_type, icmp_code, msg_type in ICMP_CODES: if type_ == icmp_type and code == icmp_code: return msg_type return "None" @@ -104,7 +101,7 @@ def get_layers(packet: Packet) -> Iterable[str]: except AttributeError: pass - def scapy_sniffer_main(self): + def server_main(self): try: sniff(filter=self.filter, iface=self.interface, prn=self.capture_logic) except PermissionError as error: @@ -119,7 +116,7 @@ def _get_payloads(self, layers: list[str], packet: Packet): raw_payloads[layer] = _fields[layer]["load"] hex_payloads[layer] = hexlify(_fields[layer]["load"]) if re.search(self.common, raw_payloads[layer]): - self._log( + self.log( { "action": "creds_check", "payload": raw_payloads[layer], @@ -136,7 +133,7 @@ def capture_logic(self, packet: Packet): try: if self.method == "ALL": try: - self._log( + self.log( { "action": "all", "layers": _layers, @@ -159,7 +156,7 @@ def capture_logic(self, packet: Packet): and packet.haslayer("ICMP") and packet["IP"].src != self.current_ip ): - self._log( + self.log( { "action": "icmp", "dest_ip": packet["IP"].src, @@ -184,7 +181,7 @@ def capture_logic(self, packet: Packet): stdout.flush() def _handle_tcp_scan(self, packet: Packet, hex_payloads: dict, raw_payloads: dict): - self._log( + self.log( { "action": "tcpscan", "dest_ip": packet["IP"].src, @@ -208,7 +205,7 @@ def _log_tcp_udp(self, packet: Packet, hex_payloads: dict, raw_payloads: dict): for layer in ["TCP", "UDP"]: if packet.haslayer(layer): try: - self._log( + self.log( { "action": f"{layer.lower()}payload", "dest_ip": packet["IP"].src, @@ -222,9 +219,9 @@ def _log_tcp_udp(self, packet: Packet, hex_payloads: dict, raw_payloads: dict): except Exception as error: self._log_error(error, 3) - def _log(self, log_data: dict): + def log(self, log_data: dict): log_data.update({"ip": self.current_ip, "mac": self.current_mac}) - self.logs.info(["sniffer", log_data]) + self.logs.info([self.NAME, log_data]) def _log_error(self, error: Exception, _id: int): self.logs.error( @@ -236,20 +233,16 @@ def _log_error(self, error: Exception, _id: int): def run_sniffer(self, process=None): if process: - self.process = Process(name="QSniffer_", target=self.scapy_sniffer_main) - self.process.start() + self._server_process = Process(name="QSniffer_", target=self.server_main) + self._server_process.start() else: - self.scapy_sniffer_main() - - def kill_server(self): - self.process.terminate() - self.process.join() + self.server_main() if __name__ == "__main__": parsed = server_arguments() if parsed.docker or parsed.aws or parsed.custom: - qsniffer = QBSniffer( + qsniffer = QSniffer( filter_=parsed.filter, interface=parsed.interface, config=parsed.config ) qsniffer.run_sniffer() From 0e6f1a213e87c3ff21487344e267578d121bdb32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Stucke?= Date: Thu, 22 Feb 2024 16:21:09 +0100 Subject: [PATCH 7/9] ssh: bug fixes + refactoring --- honeypots/ssh_server.py | 153 ++++++++++++++++++++++++++-------------- 1 file changed, 101 insertions(+), 52 deletions(-) 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: From fc8841297ab3d0af9d9f18cf8c390ad1617c4ba5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Stucke?= Date: Mon, 4 Mar 2024 14:26:26 +0100 Subject: [PATCH 8/9] base server config bug fix --- honeypots/base_server.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/honeypots/base_server.py b/honeypots/base_server.py index 1faea65..7888edd 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 = None 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))) From ab9965aff9cb84a1d480e57dd8c3417f4164257a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Stucke?= Date: Thu, 21 Mar 2024 16:00:03 +0100 Subject: [PATCH 9/9] only run the pre-commit action for PRs and not on push --- .github/workflows/pre-commit.yaml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/pre-commit.yaml b/.github/workflows/pre-commit.yaml index 505623a..9a0a630 100644 --- a/.github/workflows/pre-commit.yaml +++ b/.github/workflows/pre-commit.yaml @@ -2,8 +2,6 @@ name: pre-commit on: pull_request: - push: - branches: [main] jobs: lint: