#!/usr/bin/env python3 """ FTP 协议抓取工具 — 模拟 FTP Server,打印客户端每一条命令和交互细节 用法:python ftp_capture.py [--port 21] 需要管理员权限(Linux 无需,Windows 需要管理员权限绑定 21 端口) """ import socket import threading import sys import os import time import struct import re HOST = "0.0.0.0" PORT = 21 DATA_DIR = "ftp_capture_data" def log(fmt, *args): ts = time.strftime("%H:%M:%S", time.localtime()) msg = fmt % args if args else fmt print(f"[{ts}] {msg}") def recv_line(sock): data = b"" while True: ch = sock.recv(1) if not ch: return None data += ch if data.endswith(b"\r\n"): return data def send_line(sock, line): sock.sendall((line + "\r\n").encode()) log(">>> %s", line) class FtpHandler(threading.Thread): def __init__(self, ctrl_sock, addr): super().__init__(daemon=True) self.ctrl = ctrl_sock self.addr = addr self.cwd = "/" self.data_listener = None self.data_port = 0 self.pasv_socket = None def run(self): log("=== New connection from %s:%d ===", *self.addr) send_line(self.ctrl, "220 Service ready for new user.") while True: raw = recv_line(self.ctrl) if raw is None: log("=== Connection closed ===") break line = raw.decode(errors="replace").rstrip() log("<<< %s", line) parts = line.split() cmd = parts[0].upper() arg = parts[1] if len(parts) > 1 else None try: self.handle(cmd, arg) except Exception as e: log("!!! ERROR: %s", e) send_line(self.ctrl, "550 Internal error.") self.cleanup() def handle(self, cmd, arg): handler = getattr(self, f"cmd_{cmd}", None) if handler: handler(arg) else: send_line(self.ctrl, f"502 Command not implemented.") def cmd_USER(self, arg): send_line(self.ctrl, "230 User logged in, proceed.") def cmd_PASS(self, arg): send_line(self.ctrl, "230 User logged in, proceed.") def cmd_FEAT(self, arg): send_line(self.ctrl, "211-System status, or system help reply.") send_line(self.ctrl, " EPSV") send_line(self.ctrl, " PASV") send_line(self.ctrl, " SIZE") send_line(self.ctrl, " NLST") send_line(self.ctrl, "211 System status, or system help reply.") def cmd_PWD(self, arg): send_line(self.ctrl, f'257 "{self.cwd}"') def cmd_CWD(self, arg): if arg == "." or arg == "/": self.cwd = "/" send_line(self.ctrl, "250 Requested file action okay, completed.") else: send_line(self.ctrl, "550 Requested action not taken.") def cmd_TYPE(self, arg): send_line(self.ctrl, "200 Command okay.") def cmd_SYST(self, arg): send_line(self.ctrl, "215 UNIX Type: L8") def cmd_NOOP(self, arg): send_line(self.ctrl, "200 Command okay.") def cmd_QUIT(self, arg): send_line(self.ctrl, "221 Service closing control connection.") self.ctrl.close() def cmd_SIZE(self, arg): send_line(self.ctrl, "550 File unavailable.") def cmd_PASV(self, arg): """创建 PASV 数据监听""" self.cleanup_data() pasv = socket.socket(socket.AF_INET, socket.SOCK_STREAM) pasv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) pasv.bind(("0.0.0.0", 0)) pasv.listen(1) self.pasv_socket = pasv port = pasv.getsockname()[1] p1, p2 = port >> 8, port & 0xFF # 使用 127.0.0.1 本地环回避免防火墙问题 send_line(self.ctrl, f"227 Entering Passive Mode (127,0,0,1,{p1},{p2}).") log(" PASV listener on port %d (127.0.0.1:port)", port) def cmd_EPSV(self, arg): self.cleanup_data() pasv = socket.socket(socket.AF_INET, socket.SOCK_STREAM) pasv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) pasv.bind(("0.0.0.0", 0)) pasv.listen(1) self.pasv_socket = pasv port = pasv.getsockname()[1] send_line(self.ctrl, f"229 Entering Extended Passive Mode (|||{port}|)") log(" EPSV listener on port %d", port) def cmd_LIST(self, arg): send_line(self.ctrl, "150 File status okay; about to open data connection.") conn = self.accept_data() if conn: log(" LIST: sending directory listing (empty)") conn.sendall(b"-rw-rw-rw- 1 owner group 123 Jan 01 1970 test.txt\r\n") conn.close() log(" LIST: data closed") send_line(self.ctrl, "226 Closing data connection.") def cmd_NLST(self, arg): self.cmd_LIST(arg) def cmd_STOR(self, arg): """接收文件上传""" size = 0 send_line(self.ctrl, "150 File status okay; about to open data connection.") conn = self.accept_data() if conn: log(" STOR: receiving file '%s'...", arg) data = b"" while True: try: chunk = conn.recv(4096) if not chunk: break data += chunk size += len(chunk) except socket.timeout: break conn.close() log(" STOR: received %d bytes, data closed", size) else: log(" STOR: data connection failed!") send_line(self.ctrl, f"226 Closing data connection.") def cmd_RETR(self, arg): send_line(self.ctrl, "550 File unavailable.") def cmd_DELE(self, arg): send_line(self.ctrl, "250 Requested file action okay, completed.") def accept_data(self): """接受数据连接(5秒超时)""" if self.pasv_socket is None: log(" ERROR: no PASV listener!") return None try: self.pasv_socket.settimeout(5) conn, addr = self.pasv_socket.accept() log(" Data connection from %s:%d", *addr) return conn except socket.timeout: log(" Data accept TIMEOUT (5s)") return None except Exception as e: log(" Data accept ERROR: %s", e) return None def cleanup_data(self): if self.pasv_socket: try: self.pasv_socket.close() except: pass self.pasv_socket = None def cleanup(self): self.cleanup_data() try: self.ctrl.close() except: pass def main(): import argparse parser = argparse.ArgumentParser(description="FTP Protocol Capture") parser.add_argument("--port", type=int, default=PORT, help=f"FTP port (default {PORT}, may need admin)") args = parser.parse_args() # Windows 下低端口需要管理员权限 if sys.platform == "win32" and args.port < 1024: import ctypes if not ctypes.windll.shell32.IsUserAnAdmin(): log("WARNING: Port %d may need admin rights on Windows", args.port) log(" Run as Administrator or use --port 2121") os.makedirs(DATA_DIR, exist_ok=True) server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) server.bind((HOST, args.port)) server.listen(5) log("FTP capture server on port %d", args.port) log("Connect with MobaXterm/FileZilla and try operations") log("=" * 50) try: while True: conn, addr = server.accept() FtpHandler(conn, addr).start() except KeyboardInterrupt: log("Stopped") finally: server.close() if __name__ == "__main__": main()