Skip to content

Commit

Permalink
Merge remote-tracking branch 'upstream/main' into sniffer-without-cha…
Browse files Browse the repository at this point in the history
…meleon
  • Loading branch information
jstucke committed Mar 22, 2024
2 parents 63a9507 + 3e64e3a commit 953d2f1
Show file tree
Hide file tree
Showing 7 changed files with 106 additions and 66 deletions.
2 changes: 0 additions & 2 deletions .github/workflows/pre-commit.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ name: pre-commit

on:
pull_request:
push:
branches: [main]

jobs:
lint:
Expand Down
3 changes: 0 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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!
Expand Down
4 changes: 0 additions & 4 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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 <https://securityonline.info/honeypots-16-honeypots-in-a-single-pypi-package/>`_

Notes
=====
- Almost all servers and emulators are stripped-down - You can adjust that as needed
Expand Down
6 changes: 3 additions & 3 deletions honeypots/base_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)))
Expand Down
2 changes: 1 addition & 1 deletion honeypots/ntp_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
{
Expand Down
153 changes: 101 additions & 52 deletions honeypots/ssh_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand All @@ -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 "
Expand All @@ -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):
Expand All @@ -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):
Expand Down Expand Up @@ -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:
Expand All @@ -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",
Expand All @@ -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()
Expand All @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" },
]
Expand Down

0 comments on commit 953d2f1

Please sign in to comment.