Skip to content

Commit

Permalink
Merge pull request #54 from fkie-cad/main
Browse files Browse the repository at this point in the history
configurable proxy template
  • Loading branch information
giga-a authored Feb 1, 2024
2 parents 478ae5f + bb7ed9c commit 403c06f
Show file tree
Hide file tree
Showing 35 changed files with 315 additions and 324 deletions.
30 changes: 16 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -253,19 +253,21 @@ qsshserver.kill_server()
- Port: 21/tcp
- Lib: Twisted.ftp
- Logs: ip, port, username and password (default)
- Options: Capture all threat actor commands and data (avalible)
- Options: Capture all threat actor commands and data (available)
- QHTTPProxyServer
- Server: HTTP Proxy
- Port: 8080/tcp
- Lib: Twisted (low level emulation)
- Logs: ip, port and data
- Options: Capture all threat actor commands and data (avalible)
- Options: Capture all threat actor commands and data (available)
- Returns a dummy template by default
- A custom template can be provided by setting `"template"` for this server in `config.json` (should be an absolute path)
- QHTTPServer
- Server: HTTP
- Port: 80/tcp
- Lib: Twisted.http
- Logs: ip, port, username and password
- Options: Capture all threat actor commands and data (avalible)
- Options: Capture all threat actor commands and data (available)
- QHTTPSServer
- Server: HTTPS
- Port: 443/tcp
Expand All @@ -276,7 +278,7 @@ qsshserver.kill_server()
- Port: 143/tcp
- Lib: Twisted.imap
- Logs: ip, port, username and password (default)
- Options: Capture all threat actor commands and data (avalible)
- Options: Capture all threat actor commands and data (available)
- QMysqlServer
- Emulator: Mysql
- Port: 3306/tcp
Expand All @@ -287,7 +289,7 @@ qsshserver.kill_server()
- Port: 110/tcp
- Lib: Twisted.pop3
- Logs: ip, port, username and password (default)
- Options: Capture all threat actor commands and data (avalible)
- Options: Capture all threat actor commands and data (available)
- QPostgresServer
- Emulator: Postgres
- Port: 5432/tcp
Expand All @@ -308,7 +310,7 @@ qsshserver.kill_server()
- Port: 25/tcp
- Lib: smtpd
- Logs: ip, port, username and password (default)
- Options: Capture all threat actor commands and data (avalible)
- Options: Capture all threat actor commands and data (available)
- QSOCKS5Server
- Server: SOCK5
- Port: 1080/tcp
Expand All @@ -319,7 +321,7 @@ qsshserver.kill_server()
- Port: 22/tcp
- Lib: paramiko
- Logs: ip, port, username and password
- Options: Capture all threat actor commands and data (avalible)
- Options: Capture all threat actor commands and data (available)
- QTelnetServer
- Server: Telnet
- Port: 23/tcp
Expand Down Expand Up @@ -359,7 +361,7 @@ qsshserver.kill_server()
- Emulator: Oracle
- Port: 1521/tcp
- Lib: Twisted (low level emulation)
- Logs: ip, port and connet data
- Logs: ip, port and connect data
- QSNMPServer
- Emulator: SNMP
- Port: 161/udp
Expand All @@ -370,31 +372,31 @@ qsshserver.kill_server()
- Port: 5060/udp
- Lib: Twisted.sip
- Logs: ip, port and data
- Options: Capture all threat actor commands and data (avalible)
- Options: Capture all threat actor commands and data (available)
- QIRCServer
- Emulator: IRC
- Port: 6667/tcp
- Lib: Twisted.irc
- Logs: ip, port, username and password
- Options: Capture all threat actor commands and data (avalible)
- Options: Capture all threat actor commands and data (available)
- QPJLServer
- Emulator: PJL
- Port: 9100/tcp
- Lib: Twisted
- Logs: ip, port
- Options: Capture all threat actor commands and data (avalible)
- Options: Capture all threat actor commands and data (available)
- QIPPServer
- Emulator: IPP
- Port: 631/tcp
- Lib: Twisted
- Logs: ip, por
- Options: Capture all threat actor commands and data (avalible)
- Logs: ip, port
- Options: Capture all threat actor commands and data (available)
- QRDPServer
- Emulator: RDP
- Port: 3389/tcp
- Lib: Sockets
- Logs: ip, port, username and password
- Options: Capture all threat actor commands and data (avalible)
- Options: Capture all threat actor commands and data (available)
- QDHCPServer
- Emulator: DHCP
- Port: 67/udp
Expand Down
13 changes: 9 additions & 4 deletions honeypots/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from argparse import ArgumentParser, SUPPRESS, Namespace
from atexit import register
from functools import wraps
from json import loads
from json import JSONDecodeError, loads
from os import geteuid
from pathlib import Path
from signal import alarm, SIGALRM, SIGINT, signal, SIGTERM, SIGTSTP
Expand Down Expand Up @@ -182,9 +182,14 @@ def _load_config(self):
logger.error(f'Config file "{config_path}" not found')
sys.exit(1)
try:
return loads(config_path.read_text())
except Exception as error:
logger.exception(f"Unable to load or parse config.json file: {error}")
config_data = loads(config_path.read_text())
logger.info(f"Successfully loaded config file {config_path}")
return config_data
except FileNotFoundError:
logger.error(f"Unable to load config file: File {config_path} not found")
sys.exit(1)
except JSONDecodeError as error:
logger.error(f"Unable to parse config file as JSON: {error}")
sys.exit(1)

def _set_up_honeypots(self): # noqa: C901
Expand Down
124 changes: 64 additions & 60 deletions honeypots/http_proxy_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,23 @@
// contributors list qeeqbox/honeypots/graphs/contributors
// -------------------------------------------------------------
"""
from __future__ import annotations

from pathlib import Path
from shlex import split

from dns.resolver import query as dsnquery
from dns.resolver import resolve as dsnquery
from twisted.internet import reactor
from twisted.internet.protocol import Protocol, Factory
from subprocess import Popen
from email.parser import BytesParser
from os import path, getenv
from os import getenv
from honeypots.helper import (
close_port_wrapper,
get_free_port,
kill_server_wrapper,
server_arguments,
set_up_error_logging,
setup_logger,
set_local_vars,
check_if_server_is_running,
Expand All @@ -34,11 +38,14 @@


class QHTTPProxyServer:
NAME = "http_proxy_server"

def __init__(self, **kwargs):
self.auto_disabled = None
self.process = None
self.uuid = "honeypotslogger" + "_" + __class__.__name__ + "_" + str(uuid4())[:8]
self.config = kwargs.get("config", "")
self.template: str | None = None
if self.config:
self.logs = setup_logger(__class__.__name__, self.uuid, self.config)
set_local_vars(self, self.config)
Expand All @@ -56,6 +63,20 @@ def __init__(self, **kwargs):
or getenv("HONEYPOTS_OPTIONS", "")
or ""
)
self.logger = set_up_error_logging()
self.template_contents: str | None = self._load_template()

def _load_template(self) -> str | None:
if self.template:
try:
template_contents = Path(self.template).read_text(errors="replace")
self.logger.debug(
f"[{self.NAME}]: Successfully loaded custom template {self.template}"
)
return template_contents
except FileNotFoundError:
self.logger.error(f"[{self.NAME}]: Template file {self.template} not found")
return None

def http_proxy_server_main(self):
_q_s = self
Expand All @@ -72,7 +93,7 @@ def resolve_domain(self, request_string):
host = headers["host"].split(":")
_q_s.logs.info(
{
"server": "http_proxy_server",
"server": _q_s.NAME,
"action": "query",
"src_ip": self.transport.getPeer().host,
"src_port": self.transport.getPeer().port,
Expand All @@ -81,14 +102,13 @@ def resolve_domain(self, request_string):
"data": host[0],
}
)
# return '127.0.0.1'
return dsnquery(host[0], "A")[0].address
return None

def dataReceived(self, data):
def dataReceived(self, data): # noqa: N802
_q_s.logs.info(
{
"server": "http_proxy_server",
"server": _q_s.NAME,
"action": "connection",
"src_ip": self.transport.getPeer().host,
"src_port": self.transport.getPeer().port,
Expand All @@ -98,7 +118,7 @@ def dataReceived(self, data):
)
ip = self.resolve_domain(data)
if ip:
self.write(_create_dummy_response(DUMMY_TEMPLATE))
self.write(_create_dummy_response(_q_s.template_contents or DUMMY_TEMPLATE))
else:
self.transport.loseConnection()

Expand All @@ -110,74 +130,58 @@ def dataReceived(self, data):
def write(self, data):
self.transport.write(data)

def write(self, data):
self.transport.write(data)

factory = Factory()
factory.protocol = CustomProtocolParent
reactor.listenTCP(port=self.port, factory=factory, interface=self.ip)
reactor.run()

def run_server(self, process=False, auto=False):
def run_server(self, process=False, auto=False) -> bool | None:
status = "error"
run = False
if process:
if auto and not self.auto_disabled:
port = get_free_port()
if port > 0:
self.port = port
run = True
elif self.close_port() and self.kill_server():
run = True
if not process:
self.http_proxy_server_main()
return None

if run:
self.process = Popen(
[
"python3",
path.realpath(__file__),
"--custom",
"--ip",
str(self.ip),
"--port",
str(self.port),
"--options",
str(self.options),
"--config",
str(self.config),
"--uuid",
str(self.uuid),
]
if auto and not self.auto_disabled:
port = get_free_port()
if port > 0:
self.port = port
run = True
elif self.close_port() and self.kill_server():
run = True

if run:
self.process = Popen(
split(
f"python3 {Path(__file__)} --custom --ip {self.ip} --port {self.port} "
f"--options '{self.options}' --config '{self.config}' --uuid {self.uuid}"
)
if self.process.poll() is None and check_if_server_is_running(self.uuid):
status = "success"

self.logs.info(
{
"server": "http_proxy_server",
"action": "process",
"status": status,
"src_ip": self.ip,
"src_port": self.port,
"dest_ip": self.ip,
"dest_port": self.port,
}
)
if self.process.poll() is None and check_if_server_is_running(self.uuid):
status = "success"

self.logs.info(
{
"server": self.NAME,
"action": "process",
"status": status,
"src_ip": self.ip,
"src_port": self.port,
"dest_ip": self.ip,
"dest_port": self.port,
}
)

if status == "success":
return True
else:
self.kill_server()
return False
else:
self.http_proxy_server_main()
if status == "success":
return True
self.kill_server()
return False

def close_port(self):
ret = close_port_wrapper("http_proxy_server", self.ip, self.port, self.logs)
return ret
return close_port_wrapper(self.NAME, self.ip, self.port, self.logs)

def kill_server(self):
ret = kill_server_wrapper("http_proxy_server", self.uuid, self.process)
return ret
return kill_server_wrapper(self.NAME, self.uuid, self.process)

def test_server(self, ip=None, port=None, domain=None):
with suppress(Exception):
Expand Down
4 changes: 4 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -144,3 +144,7 @@ exclude = [
]
line-length = 99
target-version = "py38"

[tool.ruff.lint.per-file-ignores]
# don't check for "magic value" in tests
"tests/*" = ["PLR2004"]
10 changes: 10 additions & 0 deletions tests/data/test_template.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Test Template Title</title>
</head>
<body>
<p>This is a template for testing!</p>
</body>
</html>
9 changes: 2 additions & 7 deletions tests/test_dhcp_server.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
from __future__ import annotations

from time import sleep

import pytest

from honeypots import QDHCPServer
Expand All @@ -10,6 +8,7 @@
EXPECTED_KEYS,
IP,
load_logs_from_file,
wait_for_server,
)

PORT = "50067"
Expand All @@ -21,13 +20,9 @@
indirect=True,
)
def test_dhcp_server(server_logs):
sleep(1) # give the server some time to start

with connect_to(IP, PORT, udp=True) as connection:
with wait_for_server(PORT), connect_to(IP, PORT, udp=True) as connection:
connection.send(b"\x03" * 240)

sleep(1) # give the server process some time to write logs

logs = load_logs_from_file(server_logs)

assert len(logs) == 1
Expand Down
Loading

0 comments on commit 403c06f

Please sign in to comment.