FTP可以上传文件

This commit is contained in:
2026-07-22 01:46:29 +08:00
parent 038a2542af
commit c646df5c2a
76 changed files with 851 additions and 172 deletions

256
test/ftp_capture.py Normal file
View File

@@ -0,0 +1,256 @@
#!/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()