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: