导语
蜜罐成败在于"骗真"与"安全"。这篇是实操清单,直接映射到你现有实现的每个 handler。
目录
- HTTPHandler 实战细节
- FTPHandler 实战细节
- TelnetHandler 实战细节
- SSHHandler(asyncssh)实战细节
- 伪装通用策略与反识别建议
1. HTTPHandler 实战细节(基于你代码)
现状要点:
- 手工解析 request head,随机 nginx 版本,动态
.php文件,返回 admin/phpmyadmin 模板,记录attack_logger.log_*。
缺陷与改进:
-
POST body 未读 :需根据
Content-Length读取并脱敏后记录。pythonif 'content-length' in headers: body_len = int(headers['content-length']) body = await reader.read(body_len) -
增加 keep-alive 限制支持:支持短期 keep-alive 增加真实度。
-
提高头部多样性 :偶尔返回 Apache/IIS、不同
Date格式、chunked encoding。 -
模板外置 :用
templates/存放 HTML,便于快速替换和本地化。
日志样例:
json
{"session_id":"...","protocol":"http","inputs":["GET /phpmyadmin HTTP/1.1"],"events":[{"type":"auth_attempt","username":"php","password":"<captured>"}]}
2. FTPHandler 实战细节
现状要点:
- 实现 USER/PASS/SYST/PWD/CWD/LIST/RETR/STOR/QUIT,记录 auth 与 file_access。
改进:
- 实现 PASV/PORT:伪造 PASV 返回并监听随机端口以记录是否有数据连接尝试。
- 概率性失败/延迟:不要总是返回 230。部分返回 530/421 增加真实感。
- 限制并发与速率:每 IP 每秒尝试次数限制,避免日志污染。
日志样例:
json
{"session_id":"...","protocol":"ftp","username":"admin","authenticated":true,"events":[{"type":"file_access","file_path":"/etc/passwd","access_type":"read"}]}
3. TelnetHandler 实战细节
现状要点:
- Cisco-like CLI,支持
show version、show interfaces、ping、traceroute、reload,记录输入并保存原始 bytes。
改进:
- 命令 registry:将命令->处理函数映射化,便于扩展与单元测试。
- 实现 Telnet options negotiation(IAC):高级侦测器会检查 IAC 交互;做最小支持避免被识别为简单实现。
- 状态化 prompt:将 hostname、uptime 等做为可配置模板,增强仿真。
日志样例:
json
{"session_id":"...","protocol":"telnet","login":"admin","inputs":[{"ts":"...","input":"show running-config"}],"raw_base64":"..."}
4. SSHHandler(asyncssh)实战细节
现状要点:
HoneySSHServer实现validate_password/validate_public_key,HoneySSHSession处理 shell/exec,fake_exec_response提供假输出。
改进:
- session meta 完善:包含 client_version、host_key_fingerprint。
- 丰富 fake 输出 :
/etc/passwd、uname -a、whoami返回更真实内容。 - SFTP/ SCP 风险 :禁用或高度受限(当前实现
allow_scp=False是正确选择)。 - Host key 管理:支持配置 host_key_path 或生成临时 host key(当前有此逻辑,建议日志记录 fingerprint)。
日志样例:
json
{"event":"auth_attempt","time":"...","method":"password","username":"root","remote_ip":"x.x.x.x"}
5. 伪装通用策略与反识别建议
- 概率性行为:不要让所有请求都产生相同成功/失败模式。
- 随机延迟:根据 body 大小模拟延迟,避免"瞬秒"响应。
- 头部与模板多样化:Server header、Date 格式、ETag 随机化。
- 限制暴露能力:所有可能导致真实资源写入/执行的命令统一返回安全拒绝(如 550 或 403),并记录尝试。
代码
python
#!/usr/bin/env python3
"""
FTP协议处理器
"""
import asyncio
import base64
import uuid
from datetime import datetime, timezone
from .base_handler import ProtocolHandler
from .logger import op_logger
from multihoneypot.attacklogger import attack_logger, AttackType, ProtocolType
class FTPHandler(ProtocolHandler):
"""
FTP协议处理器,模拟FTP控制通道并记录客户端交互
"""
def __init__(self, host, port, log_file):
"""
初始化FTP处理器
Args:
host (str): 监听主机地址
port (int): 监听端口
log_file (str): 日志文件路径
"""
super().__init__("ftp", host, port, log_file)
async def start(self):
"""
启动FTP服务器
"""
# 创建异步服务器实例来处理客户端连接
self._server = await asyncio.start_server(self._handle_client, host=self.host, port=self.port)
addrs = ", ".join(str(sock.getsockname()) for sock in self._server.sockets)
op_logger.info("FTP监听于 %s", addrs)
# 启动服务器任务并返回任务对象
return asyncio.create_task(self._server.serve_forever())
async def _send_line(self, writer: asyncio.StreamWriter, line: str):
"""
向客户端发送一行FTP响应
Args:
writer (asyncio.StreamWriter): 用于向连接写入数据的流写入器
line (str): 要发送的响应行
"""
writer.write((line + "\r\n").encode())
await writer.drain()
async def _handle_client(self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter):
"""
处理FTP客户端连接
Args:
reader (asyncio.StreamReader): 用于从连接中读取数据的流读取器
writer (asyncio.StreamWriter): 用于向连接写入数据的流写入器
"""
# 初始化会话相关信息
peer = writer.get_extra_info("peername") or ("unknown", 0)
session_id = str(uuid.uuid4())
start_ts = datetime.now(timezone.utc)
raw_buf = bytearray()
inputs = []
username = None
authenticated = False
op_logger.info("FTP新连接 %s 会话=%s", peer, session_id)
try:
# 发送FTP欢迎消息
await self._send_line(writer, "220 Microsoft FTP Service")
while True:
# 读取客户端输入行
line = await reader.readline()
if not line:
break
raw_buf.extend(line)
try:
text = line.decode(errors="ignore").rstrip("\r\n")
except Exception:
text = "<无法解码>"
inputs.append({"ts": datetime.now(timezone.utc).isoformat(), "input": text})
op_logger.debug("FTP %s: %s", session_id, text)
# 解析FTP命令和参数
parts = text.split(" ", 1)
cmd = parts[0].upper()
arg = parts[1] if len(parts) > 1 else ""
# 处理FTP命令
if cmd == "USER":
username = arg
await self._send_line(writer, "331 Password required for " + username)
elif cmd == "PASS":
# 记录认证尝试到攻击日志
attack_logger.log_auth_attempt(
source_ip=peer[0],
destination_port=self.port,
protocol=ProtocolType.FTP,
username=username,
password=arg
)
await self._send_line(writer, "230 User " + username + " logged in")
authenticated = True
elif cmd == "SYST":
await self._send_line(writer, "215 UNIX Type: L8")
elif cmd == "PWD":
await self._send_line(writer, '257 "/" is current directory')
elif cmd == "CWD":
await self._send_line(writer, "250 CWD command successful")
elif cmd == "LIST":
# 无数据连接:发送预设的成功消息
await self._send_line(writer, "150 Opening ASCII mode data connection for file list")
await self._send_line(writer, "226 Transfer complete")
elif cmd in ("RETR", "STOR"):
# 记录文件访问到攻击日志
attack_logger.log_file_access(
source_ip=peer[0],
destination_port=self.port,
protocol=ProtocolType.FTP,
file_path=arg,
access_type="read" if cmd == "RETR" else "write"
)
# 出于安全原因拒绝实际传输
await self._send_line(writer, "550 Action not taken (simulated)")
elif cmd == "QUIT":
await self._send_line(writer, "221 Goodbye.")
break
else:
await self._send_line(writer, "502 Command not implemented")
except asyncio.CancelledError:
op_logger.info("FTP会话已取消 %s", session_id)
except Exception as e:
op_logger.exception("FTP处理器错误 %s: %s", session_id, e)
finally:
# 构建并保存会话日志条目
entry = {
"session_id": session_id,
"start_time": start_ts.isoformat(),
"end_time": datetime.now(timezone.utc).isoformat(),
"duration_seconds": (datetime.now(timezone.utc) - start_ts).total_seconds(),
"remote_ip": peer[0],
"remote_port": peer[1],
"username": username,
"authenticated": authenticated,
"inputs": inputs,
"raw_base64": base64.b64encode(bytes(raw_buf)).decode("ascii"),
}
await self.persist(entry)
try:
writer.close()
await writer.wait_closed()
except Exception:
pass
op_logger.info("FTP关闭会话 %s 来自 %s", session_id, peer)
python
#!/usr/bin/env python3
"""
HTTP协议处理器
"""
import asyncio
import base64
import random
import uuid
from datetime import datetime, timezone
from typing import Optional
from .base_handler import ProtocolHandler
from .logger import op_logger
from multihoneypot.attacklogger import attack_logger, AttackType, ProtocolType
class HTTPHandler(ProtocolHandler):
"""
HTTP协议处理器,模拟简单的HTTP服务端
"""
# 多个响应页面内容,增加随机性
RESPONSE_BODIES = [
b"<!DOCTYPE html>\n<html>\n<head>\n<title>Welcome to nginx!</title>\n<style>\n body {\n width: 35em;\n margin: 0 auto;\n font-family: Tahoma, Verdana, Arial, sans-serif;\n }\n</style>\n</head>\n<body>\n<h1>Welcome to nginx!</h1>\n<p>If you see this page, the nginx web server is successfully installed and\nworking. Further configuration is required.</p>\n\n<p>For online documentation and support please refer to\n<a href="http://nginx.org/">nginx.org</a>.<br/>\nCommercial support is available at\n<a href="http://nginx.com/">nginx.com</a>.</p>\n\n<p><em>Thank you for using nginx.</em></p>\n</body>\n</html>\n",
b"<!DOCTYPE html>\n<html>\n<head>\n<title>Test Page for nginx</title>\n<style>\n body {\n width: 35em;\n margin: 0 auto;\n font-family: Tahoma, Verdana, Arial, sans-serif;\n }\n</style>\n</head>\n<body>\n<h1>Welcome to nginx!</h1>\n<p>This is a test page used to test the correct operation of the nginx.</p>\n</body>\n</html>\n",
b"<!DOCTYPE html>\n<html>\n<head>\n<title>Default Web Page</title>\n<style>\n body {\n width: 35em;\n margin: 0 auto;\n font-family: Tahoma, Verdana, Arial, sans-serif;\n }\n</style>\n</head>\n<body>\n<h1>Default Web Page</h1>\n<p>This is the default web page for this server.</p>\n<p>The web server software is running but no content has been added, yet.</p>\n</body>\n</html>\n"
]
# 真实的nginx版本号
NGINX_VERSIONS = [
"nginx/1.18.0",
"nginx/1.20.1",
"nginx/1.21.6",
"nginx/1.22.1",
"nginx/1.23.3",
"nginx/1.24.0"
]
# 登录页面模板
LOGIN_PAGE = b"""<!DOCTYPE html>
<html>
<head>
<title>Login</title>
<style>
body { font-family: Arial, sans-serif; background-color: #f5f5f5; }
.login-container { width: 300px; margin: 100px auto; padding: 20px; background-color: white; border-radius: 5px; box-shadow: 0 0 10px rgba(0,0,0,0.1); }
h2 { text-align: center; color: #333; }
input { width: 100%; padding: 10px; margin: 10px 0; border: 1px solid #ddd; border-radius: 3px; box-sizing: border-box; }
button { width: 100%; padding: 10px; background-color: #007cba; color: white; border: none; border-radius: 3px; cursor: pointer; }
button:hover { background-color: #005a87; }
</style>
</head>
<body>
<div class="login-container">
<h2>Admin Login</h2>
<form method="POST">
<input type="text" name="username" placeholder="Username" required>
<input type="password" name="password" placeholder="Password" required>
<button type="submit">Login</button>
</form>
</div>
</body>
</html>"""
# 登录API的JSON响应
LOGIN_JSON_RESPONSES = [
b'{"success": false, "message": "Invalid credentials"}',
b'{"success": false, "message": "Authentication failed"}',
b'{"error": {"code": "INVALID_CREDENTIALS", "message": "The provided credentials are incorrect"}}',
b'{"error": "Unauthorized", "message": "Login failed. Please check your username and password."}'
]
# phpMyAdmin页面模板
PHPMYADMIN_PAGE = b"""<!DOCTYPE html>
<html lang="en">
<head>
<title>phpMyAdmin</title>
<meta charset="utf-8">
<style>
body { font-family: sans-serif; background-color: #f5f5f5; margin: 0; padding: 0; }
.container { max-width: 500px; margin: 100px auto; background: white; padding: 30px; border-radius: 5px; box-shadow: 0 0 10px rgba(0,0,0,0.1); }
h1 { text-align: center; color: #666; }
.form-group { margin-bottom: 20px; }
label { display: block; margin-bottom: 5px; }
input { width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 3px; box-sizing: border-box; }
button { width: 100%; padding: 10px; background-color: #ffd600; color: black; border: none; border-radius: 3px; cursor: pointer; font-weight: bold; }
button:hover { background-color: #e6c000; }
</style>
</head>
<body>
<div class="container">
<h1>Welcome to phpMyAdmin</h1>
<form method="post" action="index.php">
<div class="form-group">
<label for="server">Server:</label>
<input type="text" id="server" name="server" value="localhost" required>
</div>
<div class="form-group">
<label for="username">Username:</label>
<input type="text" id="username" name="username" required>
</div>
<div class="form-group">
<label for="password">Password:</label>
<input type="password" id="password" name="password" required>
</div>
<button type="submit">Go</button>
</form>
</div>
</body>
</html>"""
# Admin面板页面模板
ADMIN_PAGE = b"""<!DOCTYPE html>
<html>
<head>
<title>Admin Panel</title>
<style>
body { font-family: Arial, sans-serif; margin: 0; padding: 0; background-color: #f1f1f1; }
.header { background-color: #333; color: white; padding: 15px; }
.login-form { width: 300px; margin: 100px auto; background: white; padding: 30px; border-radius: 5px; box-shadow: 0 0 10px rgba(0,0,0,0.1); }
.form-group { margin-bottom: 20px; }
label { display: block; margin-bottom: 5px; }
input { width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 3px; box-sizing: border-box; }
button { width: 100%; padding: 10px; background-color: #4CAF50; color: white; border: none; border-radius: 3px; cursor: pointer; }
button:hover { background-color: #45a049; }
</style>
</head>
<body>
<div class="header">
<h2>Admin Panel</h2>
</div>
<div class="login-form">
<h3>Login</h3>
<form method="POST">
<div class="form-group">
<label>Username:</label>
<input type="text" name="username" required>
</div>
<div class="form-group">
<label>Password:</label>
<input type="password" name="password" required>
</div>
<button type="submit">Login</button>
</form>
</div>
</body>
</html>"""
# API数据示例
API_DATA = [
b'{"users": [{"id": 1, "name": "John Doe", "email": "john@example.com"}, {"id": 2, "name": "Jane Smith", "email": "jane@example.com"}], "total": 2}',
b'{"products": [{"id": 1, "name": "Product 1", "price": 29.99}, {"id": 2, "name": "Product 2", "price": 39.99}], "total": 2}',
b'{"orders": [{"id": 1001, "customer": "John Doe", "amount": 99.99, "status": "shipped"}, {"id": 1002, "customer": "Jane Smith", "amount": 149.99, "status": "pending"}], "total": 2}'
]
# 静态资源内容
STATIC_CONTENTS = {
"/favicon.ico": (b"\x00\x00\x01\x00\x01\x00\x00\x00\x00\x00\x01\x00\x00\x00", "image/x-icon"),
"/robots.txt": (b"User-agent: *\nDisallow: /admin/\nDisallow: /private/\n", "text/plain"),
"/sitemap.xml": (
b"<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n<url>\n<loc>http://localhost/</loc>\n</url>\n</urlset>",
"application/xml"),
}
def __init__(self, host, port, log_file):
"""
初始化HTTP处理器
Args:
host (str): 监听主机地址
port (int): 监听端口
log_file (str): 日志文件路径
"""
super().__init__("http", host, port, log_file)
# 生成带hash的动态路径
self.dynamic_paths = self._generate_dynamic_paths()
def _generate_dynamic_paths(self):
"""生成动态路径"""
paths = {}
for i in range(10):
path = f"/{uuid.uuid4().hex}.php"
content = b"<?php\n// This is a simulated PHP file\nphpinfo();\n?>"
paths[path] = (content, "text/plain")
return paths
async def start(self):
"""
启动HTTP服务器
"""
self._server = await asyncio.start_server(self._handle, host=self.host, port=self.port)
addrs = ", ".join(str(sock.getsockname()) for sock in self._server.sockets)
op_logger.info("HTTP监听于 %s", addrs)
return asyncio.create_task(self._server.serve_forever())
def _generate_session_id(self):
"""生成随机会话ID"""
return ''.join(random.choices('0123456789abcdef', k=32))
def _generate_etag(self):
"""生成ETag"""
return '"' + ''.join(random.choices('0123456789abcdef', k=16)) + '"'
async def _simulate_network_delay(self, writer: asyncio.StreamWriter, content_length: int):
"""
模拟网络延迟
Args:
writer (asyncio.StreamWriter): 流写入器
content_length (int): 内容长度
"""
# 根据内容长度模拟网络延迟
delay = min(content_length / 10000, 1.0) # 最大1秒延迟
if delay > 0.01:
await asyncio.sleep(delay * 0.1)
async def _handle(self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter):
"""
处理HTTP客户端请求
Args:
reader (asyncio.StreamReader): 用于从连接中读取数据的流读取器
writer (asyncio.StreamWriter): 用于向连接写入数据的流写入器
"""
peer = writer.get_extra_info("peername") or ("unknown", 0)
session_id = str(uuid.uuid4())
start_ts = datetime.now(timezone.utc)
try:
raw = await reader.read(64 * 1024) # 读取最多64KB的头部和内容
text = raw.decode(errors="ignore")
# 解析请求
request_lines = text.split('\n')
if not request_lines:
writer.close()
return
request_line = request_lines[0].strip()
headers = {}
for line in request_lines[1:]:
if ':' in line:
key, value = line.split(':', 1)
headers[key.strip().lower()] = value.strip()
parts = request_line.split()
if len(parts) < 3:
writer.close()
return
method, path, version = parts[0], parts[1], parts[2]
# 记录HTTP请求到攻击日志
attack_logger.log_command_execution(
source_ip=peer[0],
destination_port=self.port,
protocol=ProtocolType.HTTP,
command=f"{method} {path} {version}",
command_type="http_request"
)
# 检查是否为可疑路径
if any(pattern in path.lower() for pattern in ['manager', 'python', '.env', 'flask', 'django']):
body = b'<!DOCTYPE html>\n<html>\n<head>\n<title>404 Not Found</title>\n</head>\n<body>\n<center><h1>404 Not Found</h1></center>\n<hr><center>nginx</center>\n</body>\n</html>\n'
server_header = random.choice(self.NGINX_VERSIONS)
resp = b"HTTP/1.1 404 Not Found\r\nServer: %s\r\nDate: %s\r\nContent-Type: text/html\r\nContent-Length: %d\r\nConnection: close\r\n\r\n%s" % (
server_header.encode(),
datetime.now().strftime('%a, %d %b %Y %H:%M:%S GMT').encode(),
len(body),
body
)
writer.write(resp)
await writer.drain()
return
# 处理phpMyAdmin页面请求
if path.startswith("/phpmyadmin"):
if path == "/phpmyadmin" or path == "/phpmyadmin/":
if method == "GET":
body = self.PHPMYADMIN_PAGE
content_type = "text/html"
elif method == "POST":
# 记录登录尝试
post_data = f"<PHPMyAdmin login attempt with {headers.get('content-length', 'unknown')} bytes of data>"
op_logger.info("phpMyAdmin login attempt from %s: %s", peer[0], post_data)
# 记录认证尝试到攻击日志
attack_logger.log_auth_attempt(
source_ip=peer[0],
destination_port=self.port,
protocol=ProtocolType.HTTP,
username="phpmyadmin_user",
password="<captured>"
)
# 返回登录失败页面
body = b"""<!DOCTYPE html>
<html>
<head>
<title>phpMyAdmin</title>
<style>
body { font-family: sans-serif; background-color: #f5f5f5; }
.error { width: 500px; margin: 100px auto; background: white; padding: 30px; border-radius: 5px; box-shadow: 0 0 10px rgba(0,0,0,0.1); border-left: 5px solid #f44336; }
.error h2 { color: #f44336; }
.back-link { display: block; text-align: center; margin-top: 20px; color: #2196F3; text-decoration: none; }
</style>
</head>
<body>
<div class="error">
<h2>Login failed</h2>
<p>Invalid credentials. Please try again.</p>
<a href="index.php" class="back-link">Back to login</a>
</div>
</body>
</html>"""
content_type = "text/html"
else:
body = b'{"error": "Method not allowed"}'
content_type = "application/json"
server_header = random.choice(self.NGINX_VERSIONS)
resp = b"HTTP/1.1 200 OK\r\nServer: %s\r\nDate: %s\r\nContent-Type: %s\r\nContent-Length: %d\r\nConnection: close\r\n\r\n%s" % (
server_header.encode(),
datetime.now().strftime('%a, %d %b %Y %H:%M:%S GMT').encode(),
content_type.encode(),
len(body),
body
)
await self._simulate_network_delay(writer, len(body))
writer.write(resp)
await writer.drain()
return
else:
# phpMyAdmin子路径返回404
body = b'<!DOCTYPE html>\n<html>\n<head>\n<title>404 Not Found</title>\n</head>\n<body>\n<center><h1>404 Not Found</h1></center>\n<hr><center>nginx</center>\n</body>\n</html>\n'
server_header = random.choice(self.NGINX_VERSIONS)
resp = b"HTTP/1.1 404 Not Found\r\nServer: %s\r\nDate: %s\r\nContent-Type: text/html\r\nContent-Length: %d\r\nConnection: close\r\n\r\n%s" % (
server_header.encode(),
datetime.now().strftime('%a, %d %b %Y %H:%M:%S GMT').encode(),
len(body),
body
)
writer.write(resp)
await writer.drain()
return
# 处理Admin面板请求
if path.startswith("/admin"):
if path == "/admin" or path == "/admin/":
if method == "GET":
body = self.ADMIN_PAGE
content_type = "text/html"
elif method == "POST":
# 记录登录尝试
post_data = f"<Admin panel login attempt with {headers.get('content-length', 'unknown')} bytes of data>"
op_logger.info("Admin panel login attempt from %s: %s", peer[0], post_data)
# 记录认证尝试到攻击日志
attack_logger.log_auth_attempt(
source_ip=peer[0],
destination_port=self.port,
protocol=ProtocolType.HTTP,
username="admin_user",
password="<captured>"
)
# 返回登录失败页面
body = b"""<!DOCTYPE html>
<html>
<head>
<title>Admin Panel</title>
<style>
body { font-family: Arial, sans-serif; margin: 0; padding: 0; background-color: #f1f1f1; }
.header { background-color: #333; color: white; padding: 15px; }
.error { width: 300px; margin: 100px auto; background: white; padding: 30px; border-radius: 5px; box-shadow: 0 0 10px rgba(0,0,0,0.1); border-left: 5px solid #f44336; }
.error h3 { color: #f44336; margin-top: 0; }
.back-link { display: block; text-align: center; margin-top: 20px; color: #2196F3; text-decoration: none; }
</style>
</head>
<body>
<div class="header">
<h2>Admin Panel</h2>
</div>
<div class="error">
<h3>Login Failed</h3>
<p>Invalid username or password.</p>
<a href="/" class="back-link">Back to login</a>
</div>
</body>
</html>"""
content_type = "text/html"
else:
body = b'{"error": "Method not allowed"}'
content_type = "application/json"
server_header = random.choice(self.NGINX_VERSIONS)
resp = b"HTTP/1.1 200 OK\r\nServer: %s\r\nDate: %s\r\nContent-Type: %s\r\nContent-Length: %d\r\nConnection: close\r\n\r\n%s" % (
server_header.encode(),
datetime.now().strftime('%a, %d %b %Y %H:%M:%S GMT').encode(),
content_type.encode(),
len(body),
body
)
await self._simulate_network_delay(writer, len(body))
writer.write(resp)
await writer.drain()
return
else:
# Admin子路径返回404
body = b'<!DOCTYPE html>\n<html>\n<head>\n<title>404 Not Found</title>\n</head>\n<body>\n<center><h1>404 Not Found</h1></center>\n<hr><center>nginx</center>\n</body>\n</html>\n'
server_header = random.choice(self.NGINX_VERSIONS)
resp = b"HTTP/1.1 404 Not Found\r\nServer: %s\r\nDate: %s\r\nContent-Type: text/html\r\nContent-Length: %d\r\nConnection: close\r\n\r\n%s" % (
server_header.encode(),
datetime.now().strftime('%a, %d %b %Y %H:%M:%S GMT').encode(),
len(body),
body
)
writer.write(resp)
await writer.drain()
return
# 处理API请求
if path.startswith("/api/"):
if method == "GET":
body = random.choice(self.API_DATA)
content_type = "application/json"
else:
body = b'{"error": "Method not allowed"}'
content_type = "application/json"
server_header = random.choice(self.NGINX_VERSIONS)
resp = b"HTTP/1.1 200 OK\r\nServer: %s\r\nDate: %s\r\nContent-Type: %s\r\nContent-Length: %d\r\nConnection: close\r\n\r\n%s" % (
server_header.encode(),
datetime.now().strftime('%a, %d %b %Y %H:%M:%S GMT').encode(),
content_type.encode(),
len(body),
body
)
await self._simulate_network_delay(writer, len(body))
writer.write(resp)
await writer.drain()
return
# 处理登录页面请求
if path == "/login" and method == "GET":
# 检查Accept头以决定返回HTML还是JSON
accept_header = headers.get('accept', '')
if 'application/json' in accept_header and 'text/html' not in accept_header:
# 返回JSON格式的登录页面信息
body = b'{"login_url": "/login", "method": "POST", "fields": ["username", "password"]}'
content_type = "application/json"
else:
# 返回HTML登录页面
body = self.LOGIN_PAGE
content_type = "text/html"
server_header = random.choice(self.NGINX_VERSIONS)
resp = b"HTTP/1.1 200 OK\r\nServer: %s\r\nDate: %s\r\nContent-Type: %s\r\nContent-Length: %d\r\nConnection: close\r\n\r\n%s" % (
server_header.encode(),
datetime.now().strftime('%a, %d %b %Y %H:%M:%S GMT').encode(),
content_type.encode(),
len(body),
body
)
await self._simulate_network_delay(writer, len(body))
writer.write(resp)
await writer.drain()
return
# 处理登录表单提交
if path == "/login" and method == "POST":
# 检查Content-Type以决定如何处理请求
content_type_header = headers.get('content-type', '').lower()
# 记录提交的登录凭据
post_data = b""
content_length = 0
if "content-length" in headers:
try:
content_length = int(headers["content-length"])
except ValueError:
pass
# 简化处理,实际应该读取body内容
post_data = f"<{content_length} bytes of POST data>".encode()
# 记录认证尝试到攻击日志
attack_logger.log_auth_attempt(
source_ip=peer[0],
destination_port=self.port,
protocol=ProtocolType.HTTP,
username="web_user",
password="<captured>"
)
# 根据请求的Accept头决定返回什么类型的数据
accept_header = headers.get('accept', '')
if 'application/json' in accept_header and 'text/html' not in accept_header:
# 返回JSON格式的错误响应
body = random.choice(self.LOGIN_JSON_RESPONSES)
content_type = "application/json"
status_line = "HTTP/1.1 401 Unauthorized"
else:
# 返回HTML格式的错误响应
body = b'<!DOCTYPE html>\n<html>\n<head>\n<title>Login Failed</title>\n</head>\n<body>\n<center><h1>Login Failed</h1></center>\n<hr><center>nginx</center>\n</body>\n</html>\n'
content_type = "text/html"
status_line = "HTTP/1.1 401 Unauthorized"
server_header = random.choice(self.NGINX_VERSIONS)
resp = b"%s\r\nServer: %s\r\nDate: %s\r\nContent-Type: %s\r\nContent-Length: %d\r\nConnection: close\r\n\r\n%s" % (
status_line.encode(),
server_header.encode(),
datetime.now().strftime('%a, %d %b %Y %H:%M:%S GMT').encode(),
content_type.encode(),
len(body),
body
)
writer.write(resp)
await writer.drain()
# 记录登录尝试
op_logger.info("Login attempt from %s: %s", peer[0], post_data.decode(errors="ignore"))
return
# 处理静态资源请求
all_static_contents = {**self.STATIC_CONTENTS, **self.dynamic_paths}
if path in all_static_contents:
content, content_type = all_static_contents[path]
# 记录文件访问到攻击日志
attack_logger.log_file_access(
source_ip=peer[0],
destination_port=self.port,
protocol=ProtocolType.HTTP,
file_path=path,
access_type="read"
)
server_header = random.choice(self.NGINX_VERSIONS)
resp = b"HTTP/1.1 200 OK\r\nServer: %s\r\nDate: %s\r\nContent-Type: %s\r\nContent-Length: %d\r\nETag: %s\r\nCache-Control: max-age=3600\r\nConnection: close\r\n\r\n%s" % (
server_header.encode(),
datetime.now().strftime('%a, %d %b %Y %H:%M:%S GMT').encode(),
content_type.encode(),
len(content),
self._generate_etag().encode(),
content
)
await self._simulate_network_delay(writer, len(content))
writer.write(resp)
await writer.drain()
return
# 处理/favicon.ico特殊请求
if path == "/favicon.ico":
content = b"\x00\x00\x01\x00\x01\x00\x00\x00\x00\x00\x01\x00\x00\x00"
# 记录文件访问到攻击日志
attack_logger.log_file_access(
source_ip=peer[0],
destination_port=self.port,
protocol=ProtocolType.HTTP,
file_path=path,
access_type="read"
)
server_header = random.choice(self.NGINX_VERSIONS)
resp = b"HTTP/1.1 200 OK\r\nServer: %s\r\nDate: %s\r\nContent-Type: image/x-icon\r\nContent-Length: %d\r\nETag: %s\r\nCache-Control: max-age=86400\r\nConnection: close\r\n\r\n%s" % (
server_header.encode(),
datetime.now().strftime('%a, %d %b %Y %H:%M:%S GMT').encode(),
len(content),
self._generate_etag().encode(),
content
)
await self._simulate_network_delay(writer, len(content))
writer.write(resp)
await writer.drain()
return
# 检查是否需要返回304
if 'if-none-match' in headers:
server_header = random.choice(self.NGINX_VERSIONS)
resp = b"HTTP/1.1 304 Not Modified\r\nServer: %s\r\nDate: %s\r\nETag: %s\r\nConnection: close\r\n\r\n" % (
server_header.encode(),
datetime.now().strftime('%a, %d %b %Y %H:%M:%S GMT').encode(),
self._generate_etag().encode()
)
writer.write(resp)
await writer.drain()
return
# 生成Cookie
has_cookie = 'cookie' in headers
session_cookie = ""
if not has_cookie:
session_cookie = "Set-Cookie: sessionid=%s; Path=/; HttpOnly\r\n" % self._generate_session_id()
# 随机选择响应页面
body = random.choice(self.RESPONSE_BODIES)
# 随机选择nginx版本
server_header = random.choice(self.NGINX_VERSIONS)
# 构建响应
resp_headers = [
"HTTP/1.1 200 OK",
"Server: %s" % server_header,
"Date: %s" % datetime.now().strftime('%a, %d %b %Y %H:%M:%S GMT'),
"Content-Type: text/html",
"Content-Length: %d" % len(body),
"ETag: %s" % self._generate_etag(),
"Cache-Control: no-cache",
"Connection: close"
]
if not has_cookie:
resp_headers.append(session_cookie.rstrip())
resp = ("\r\n".join(resp_headers) + "\r\n\r\n").encode() + body
await self._simulate_network_delay(writer, len(body))
writer.write(resp)
await writer.drain()
entry = {
"session_id": session_id,
"start_time": start_ts.isoformat(),
"end_time": datetime.now(timezone.utc).isoformat(),
"duration_seconds": (datetime.now(timezone.utc) - start_ts).total_seconds(),
"remote_ip": peer[0],
"remote_port": peer[1],
"inputs": [text],
"raw_base64": base64.b64encode(raw).decode("ascii"),
}
await self.persist(entry)
except Exception as e:
op_logger.exception("HTTP处理器错误 %s: %s", session_id, e)
finally:
try:
writer.close()
await writer.wait_closed()
except Exception:
pass
op_logger.info("HTTP关闭会话 %s 来自 %s", session_id, peer)
python
#!/usr/bin/env python3
"""
HTTPS协议处理器
"""
import asyncio
import base64
import random
import ssl
import uuid
from datetime import datetime, timezone
from typing import Optional
from .http_handler import HTTPHandler
from .logger import op_logger
class HTTPSHandler(HTTPHandler):
"""
HTTPS协议处理器,通过SSL包装HTTP处理器实现HTTPS服务
"""
def __init__(self, host, port, log_file, certfile: str, keyfile: str):
"""
初始化HTTPS处理器
Args:
host (str): 监听主机地址
port (int): 监听端口
log_file (str): 日志文件路径
certfile (str): SSL证书文件路径
keyfile (str): SSL密钥文件路径
"""
super().__init__(host, port, log_file)
self.name = "https"
self.certfile = certfile
self.keyfile = keyfile
# 生成带hash的动态路径
self.dynamic_paths = self._generate_dynamic_paths()
async def start(self):
"""
启动HTTPS服务器
"""
try:
# 创建SSL上下文
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
ctx.options |= ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1 # 禁用旧版本
ctx.load_cert_chain(certfile=self.certfile, keyfile=self.keyfile)
# 添加更强的SSL配置
ctx.set_ciphers('ECDHE+AESGCM:ECDHE+CHACHA20:DHE+AESGCM:DHE+CHACHA20:!aNULL:!MD5:!DSS')
self._server = await asyncio.start_server(self._handle, host=self.host, port=self.port, ssl=ctx)
addrs = ", ".join(str(sock.getsockname()) for sock in self._server.sockets)
op_logger.info("HTTPS监听于 %s", addrs)
asyncio.create_task(self._server.serve_forever())
except FileNotFoundError:
op_logger.error("找不到SSL证书文件: certfile=%s keyfile=%s", self.certfile, self.keyfile)
op_logger.error("请使用--generate-cert参数自动生成证书,或手动提供证书文件")
except Exception as e:
op_logger.exception("HTTPS服务器启动失败: %s", e)
python
#!/usr/bin/env python3
"""
HTTPS协议处理器
"""
import asyncio
import base64
import random
import ssl
import uuid
from datetime import datetime, timezone
from typing import Optional
from .http_handler import HTTPHandler
from .logger import op_logger
class HTTPSHandler(HTTPHandler):
"""
HTTPS协议处理器,通过SSL包装HTTP处理器实现HTTPS服务
"""
def __init__(self, host, port, log_file, certfile: str, keyfile: str):
"""
初始化HTTPS处理器
Args:
host (str): 监听主机地址
port (int): 监听端口
log_file (str): 日志文件路径
certfile (str): SSL证书文件路径
keyfile (str): SSL密钥文件路径
"""
super().__init__(host, port, log_file)
self.name = "https"
self.certfile = certfile
self.keyfile = keyfile
# 生成带hash的动态路径
self.dynamic_paths = self._generate_dynamic_paths()
async def start(self):
"""
启动HTTPS服务器
"""
try:
# 创建SSL上下文
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
ctx.options |= ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1 # 禁用旧版本
ctx.load_cert_chain(certfile=self.certfile, keyfile=self.keyfile)
# 添加更强的SSL配置
ctx.set_ciphers('ECDHE+AESGCM:ECDHE+CHACHA20:DHE+AESGCM:DHE+CHACHA20:!aNULL:!MD5:!DSS')
self._server = await asyncio.start_server(self._handle, host=self.host, port=self.port, ssl=ctx)
addrs = ", ".join(str(sock.getsockname()) for sock in self._server.sockets)
op_logger.info("HTTPS监听于 %s", addrs)
asyncio.create_task(self._server.serve_forever())
except FileNotFoundError:
op_logger.error("找不到SSL证书文件: certfile=%s keyfile=%s", self.certfile, self.keyfile)
op_logger.error("请使用--generate-cert参数自动生成证书,或手动提供证书文件")
except Exception as e:
op_logger.exception("HTTPS服务器启动失败: %s", e)
python
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
增强版 SSH 蜜罐处理器(基于 asyncssh)
与现有 ProtocolHandler 集成:继承自 ProtocolHandler,并调用 self.persist(entry) 保存事件。
功能:
- 完整 SSH 握手(KEX、host key)
- 认证点位(password / publickey):记录凭证尝试
- 交互 shell(PTY)与 exec:记录命令与响应
- 会话元数据(session_id, client_version, remote ip/port, start/end/duration)
- 可配置:是否接受密码、公钥;欢迎词、prompt、伪文件内容等
注意:不要在生产网络直接暴露未隔离的蜜罐实例,务必放在隔离网络。
"""
from __future__ import annotations
import asyncio
import asyncssh
import base64
import uuid
import logging
from datetime import datetime, timezone
from typing import Dict, Any, Optional
from .base_handler import ProtocolHandler
from .config import get_ssh_config
from .logger import op_logger
from multihoneypot.attacklogger import attack_logger, AttackType, ProtocolType
def now_iso() -> str:
return datetime.now(timezone.utc).isoformat()
class HoneySSHServer(asyncssh.SSHServer):
"""
asyncssh 的 server-side 回调:处理连接级事件与认证回调委托给 handler。
"""
def __init__(self, handler: "SSHHandler", peername):
self.handler = handler
self.peername = peername # (ip, port)
def connection_made(self, conn):
# conn: asyncssh.SSHServerConnection
client_version = getattr(conn, "client_version", None)
op_logger.info("connection_made peer=%s client_version=%s", self.peername, client_version)
def connection_lost(self, exc):
op_logger.info("connection_lost peer=%s exc=%s", self.peername, exc)
def begin_auth(self, username: str) -> bool:
# True 表示需要认证(将触发 validate_password / validate_public_key)
return True
async def validate_password(self, username: str, password: str) -> bool:
# 记录认证尝试到攻击日志
attack_logger.log_auth_attempt(
source_ip=self.peername[0],
destination_port=self.handler.port,
protocol=ProtocolType.SSH,
username=username,
password=password
)
# 记录尝试(避免在日志中直接打印明文密码,请注意权限)
await self.handler._record_auth_attempt({
"method": "password",
"username": username,
"password": password, # 如果合规/敏感,后续可替换为 hash
"remote_ip": self.peername[0],
"remote_port": self.peername[1],
"time": now_iso(),
})
# 根据配置决定是否允许(蜜罐策略)
# return bool(self.handler.config.get("accept_password", False))
allowed: Dict[str, str] = self.handler.config.get("allowed_credentials", {})
if username in allowed and allowed[username] == password:
return True
return False
async def validate_public_key(self, username: str, key: asyncssh.PKey) -> bool:
"""
验证SSH公钥认证请求
Args:
username (str): 尝试认证的用户名
key (asyncssh.PKey): 客户端提供的公钥对象
Returns:
bool: 如果接受公钥认证则返回True,否则返回False
"""
fp = key.get_fingerprint()
# 记录认证尝试到攻击日志
attack_logger.log_auth_attempt(
source_ip=self.peername[0],
destination_port=self.handler.port,
protocol=ProtocolType.SSH,
username=username,
pubkey_fingerprint=fp
)
await self.handler._record_auth_attempt({
"method": "publickey",
"username": username,
"pubkey_fingerprint": fp,
"pubkey_type": getattr(key, "get_type", lambda: "unknown")(),
"remote_ip": self.peername[0],
"remote_port": self.peername[1],
"time": now_iso(),
})
return bool(self.handler.config.get("accept_publickey", False))
class HoneySSHSession(asyncssh.SSHServerSession):
"""
处理单个 session(shell/exec/pty)
"""
def __init__(self, handler: "SSHHandler", session_meta: Dict[str, Any]):
self.handler = handler
self.session_meta = session_meta
self._chan = None
self._buffer = ""
self.pty_info = None
self._closed = False
def connection_made(self, chan):
self._chan = chan
op_logger.debug("session connection_made session_id=%s", self.session_meta.get("session_id"))
def connection_lost(self, exc):
op_logger.debug("session connection_lost session_id=%s exc=%s", self.session_meta.get("session_id"), exc)
def session_started(self):
# callback when session established
pass
def pty_requested(self, term, width, height, pxwidth, pxheight, modes):
self.pty_info = {"term": term, "width": width, "height": height}
return True
def shell_requested(self):
# 启动交互 shell
asyncio.create_task(self._run_shell())
return True
def exec_requested(self, command):
# 记录命令执行到攻击日志
if self.session_meta.get("remote_ip"):
attack_logger.log_command_execution(
source_ip=self.session_meta["remote_ip"],
destination_port=self.session_meta.get("destination_port", 22),
protocol=ProtocolType.SSH,
command=command,
command_type="exec"
)
# 记录 exec 命令并立即返回伪结果后退出
asyncio.create_task(self.handler._record_command(self.session_meta, command, is_exec=True))
# 使用 handler 的 fake_exec_response 生成结果
out = self.handler.fake_exec_response(command)
try:
# write 可能在不同线程上下文,使用 chan.write
if self._chan:
self._chan.write(out + "\n")
self._chan.exit(0)
except Exception:
op_logger.exception("exec response error")
return True
async def _run_shell(self):
"""
读取 channel 数据(byte),按行解析命令并响应。
同时记录原始字节(base64)与解析后的命令事件。
"""
prompt = self.handler.shell_prompt()
welcome = self.handler.welcome_message()
try:
if self._chan is None:
return
# 写欢迎词和 prompt
if welcome:
self._chan.write(welcome + "\r\n")
if prompt:
self._chan.write(prompt)
# 读取循环
while True:
data = await self._chan.read(1024)
if data is None:
break
# data 是 bytes
try:
decoded = data.decode(errors="ignore")
except Exception:
decoded = str(data)
# 记录原始交互片段
await self.handler._record_interaction(self.session_meta, data)
# 拼接缓冲并按换行拆分以得到完整命令
self._buffer += decoded
if "\n" in self._buffer or "\r" in self._buffer:
# 取第一条命令(保守)
line, _, rest = self._buffer.partition("\n")
line = line.rstrip("\r")
self._buffer = rest
cmd = line.strip()
# 记录命令执行到攻击日志
if self.session_meta.get("remote_ip") and cmd:
attack_logger.log_command_execution(
source_ip=self.session_meta["remote_ip"],
destination_port=self.session_meta.get("destination_port", 22),
protocol=ProtocolType.SSH,
command=cmd,
command_type="shell"
)
# 记录命令事件
await self.handler._record_command(self.session_meta, cmd, is_exec=False)
# 特殊命令处理
if cmd in ("exit", "logout", "quit"):
self._chan.write("logout\r\n")
self._chan.exit(0)
break
# 生成伪输出并写回
resp = self.handler.fake_exec_response(cmd)
if resp:
self._chan.write(resp + "\r\n")
# 再写 prompt
if prompt:
self._chan.write(prompt)
else:
# 未到换行,继续读取
pass
except Exception as e:
op_logger.exception("shell loop exception session=%s: %s", self.session_meta.get("session_id"), e)
finally:
self._closed = True
class SSHHandler(ProtocolHandler):
"""
可直接替换你现有的 SSH Handler:
- 保持与 ProtocolHandler.persist(entry) 的兼容(调用 await self.persist(entry))
- 配置源自 get_ssh_config(config)
"""
def __init__(self, host: str, port: int, log_file: str, config=None):
super().__init__("ssh", host, port, log_file)
self.config: Dict[str, Any] = {}
if config:
self.config.update(get_ssh_config(config))
# 默认值
self.config.setdefault("banner", "SSH-2.0-OpenSSH_7.9")
self.config.setdefault("accept_password", False)
self.config.setdefault("accept_publickey", False)
self.config.setdefault("shell_prompt", "[root@server ~]$ ")
self.config.setdefault("welcome_message", "Welcome to Ubuntu 18.04.6 LTS (GNU/Linux 4.15.0-xyz)")
self.config.setdefault("host_key_path", None) # 若配置了 path 则读取私钥文件
self.config.setdefault("allowed_credentials", {"root": "123456", "admin": "adminpwd"})
self._server = None
self._active_sessions = {} # session_id -> meta
def shell_prompt(self) -> str:
return str(self.config.get("shell_prompt", ""))
def welcome_message(self) -> str:
return str(self.config.get("welcome_message", ""))
async def _record_auth_attempt(self, info: Dict[str, Any]):
# 标准化保存
entry = {
"event": "auth_attempt",
"time": now_iso(),
**info
}
try:
await self.persist(entry)
except Exception:
op_logger.exception("persist auth attempt failed")
async def _record_command(self, session_meta: Dict[str, Any], command: str, is_exec: bool = False):
entry = {
"event": "command",
"time": now_iso(),
"session_id": session_meta.get("session_id"),
"username": session_meta.get("username"),
"command": command,
"is_exec": bool(is_exec),
"remote_ip": session_meta.get("remote_ip"),
"remote_port": session_meta.get("remote_port"),
}
try:
await self.persist(entry)
except Exception:
op_logger.exception("persist command failed")
async def _record_interaction(self, session_meta: Dict[str, Any], raw_bytes: bytes):
entry = {
"event": "interaction",
"time": now_iso(),
"session_id": session_meta.get("session_id"),
"username": session_meta.get("username"),
"raw_base64": base64.b64encode(raw_bytes).decode("ascii"),
"remote_ip": session_meta.get("remote_ip"),
"remote_port": session_meta.get("remote_port"),
}
try:
await self.persist(entry)
except Exception:
op_logger.exception("persist interaction failed")
def fake_exec_response(self, cmd: str) -> str:
# 伪造命令输出(可扩展成模板或脚本)
cmd = (cmd or "").strip()
if cmd in ("ls", "ls -la"):
return "total 8\ndrwxr-xr-x 2 root root 4096 Sep 20 12:00 .\n-rw-r--r-- 1 root root 23 Sep 20 12:00 README"
if cmd.startswith("cat "):
return "This is a fake file content"
if cmd in ("whoami",):
return "root"
if cmd in ("pwd",):
return "/root"
if cmd.startswith("uname") or cmd.startswith("uname -a"):
return "Linux server 4.15.0-xyz #1 SMP Thu Sep 10 12:00:00 UTC 2020 x86_64 GNU/Linux"
if cmd.startswith("wget ") or cmd.startswith("curl "):
return "Downloading... saved to /tmp/fakefile"
if cmd == "":
return ""
return f"bash: {cmd}: command not found"
async def start(self):
"""
启动 asyncssh 服务。会生成临时 host key(除非配置 host_key_path)。
"""
# 读取配置 host key 或生成临时 host key
host_key_path = self.config.get("host_key_path")
server_host_keys = None
if host_key_path:
try:
server_host_keys = [host_key_path]
except Exception:
op_logger.exception("failed to use host_key_path %s", host_key_path)
server_host_keys = None
if not server_host_keys:
# 生成临时 host key(RSA)
try:
# asyncssh.generate_private_key 返回 PKey,create_server 接受 PKey 对象
temp_key = asyncssh.generate_private_key("ssh-rsa")
server_host_keys = [temp_key]
op_logger.info("generated ephemeral host key")
except Exception:
op_logger.exception("failed to generate ephemeral host key")
server_host_keys = None # let asyncssh decide (rare)
# 启动 asyncssh server
op_logger.info("starting asyncssh server on %s:%s", self.host, self.port)
try:
server = await asyncssh.create_server(
lambda: HoneySSHServer(self, None),
host=self.host,
port=self.port,
server_host_keys=server_host_keys,
# 下面参数可以根据需要调整
allow_scp=False,
# 如果要支持 SFTP,你需要传入 sftp_factory
)
self._server = server
op_logger.info("asyncssh server started listen=%s:%s", self.host, self.port)
# asyncssh.create_server 返回一个 Server 对象;保持运行由外部 event loop 管理
except Exception as e:
op_logger.exception("failed to start asyncssh server: %s", e)
raise
async def stop(self):
if self._server:
try:
self._server.close()
await self._server.wait_closed()
op_logger.info("ssh server closed")
except Exception:
op_logger.exception("error while stopping ssh server")