FTP可以上传文件
This commit is contained in:
256
test/ftp_capture.py
Normal file
256
test/ftp_capture.py
Normal 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()
|
||||
Reference in New Issue
Block a user