【Linux】内网穿透 FTP 终极复现手册2026 版–cpolar
- 前言
- 一、 核心背景与原理
- 二、 服务端:ftp_server.py (Kali 运行)
-
- 如果复现过程中遇到 2个域名不一样
- 三、访问FTP服务器
- 四、 (非必须)客户端:ftp_client.py (Windows 运行)
- 五、 快速复现流程总结
前言
因为有个靶场所需需要搭建一个 FTP 服务器,又因为比较懒不高兴开 VPS 服务器去搭建 FTP ,用内网穿透工具 cpolar 搭建了2条 tcp 隧道,复现了 ftp 但是花了很长时间;为避免各位踩坑特此整理了一个笔记。 安全性问题最重要说三遍: 操作过程中请注意,FTP 主目录下不要放敏感文件!!! 操作过程中请注意,FTP 主目录下不要放敏感文件!!! 操作过程中请注意,FTP 主目录下不要放敏感文件!!!
一、 核心背景与原理
本方案 cpolar 需要搭建两条 tcp 隧道1,在双重 NAT(内网环境)下,FTP 协议的** “双通道” **特性会导致连接失败。
- 控制通道 (Port A): 负责指令:tcp://3.tcp.cpolar.top:11935 -> tcp://127.0.0.1:2121
- 数据通道 (Port B): 负责文件内容。tcp://3.tcp.cpolar.top:11667 -> tcp://127.0.0.1:2120
穿透痛点:
解决方案: 通过 Python 脚本 “暴力劫持” FTP 的 PASV(被动模式)响应,强制将客户端引导至公网隧道出口。
IP漂移以及解决方法
流量的一生:
二、 服务端:ftp_server.py (Kali 运行)
功能: 重写 PASV 逻辑,实现 “协议层端口重定向”。
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# 运行环境: Windows / Linux / macOS (跨平台通用)
# 依赖安装: pip install pyftpdlib
# cpolar 两条 tcp 隧道一条映射 2121 端口,一条映射 2120 端口
# 复现环境时 cpolar 映射公网 IP 是 8.148.71.245 有可能随着时间变动会变动,需要调整代码
# 另外也可能 两条隧道公网 IP 不一样,
from pyftpdlib.authorizers import DummyAuthorizer # 权限管理器
from pyftpdlib.handlers import FTPHandler # 协议处理器
from pyftpdlib.servers import FTPServer # 监听服务器
import logging # 日志模块
# 开启 DEBUG 日志,实时查看 FTP 握手过程
logging.basicConfig(level=logging.DEBUG)
class CpolarFTPHandler(FTPHandler):
"""
【核心黑科技】自定义处理器,拦截并重写被动模式响应
"""
def ftp_PASV(self, line):
# 1. 暴力劫持响应内容:
# 强制告诉客户端:请去连接公网地址 8.148.71.245 的 11667 端口
# 端口计算公式:45 * 256 + 147 = 11667
self.respond('227 Entering Passive Mode (8,148,71,245,45,147).')
# 2. 启动本地监听:
# 虽然告诉客户端连外网,但本地服务器必须在 2120 端口等着 cpolar 转回来的流量
try:
self.handle_pasv() # pyftpdlib 2.x 内部监听启动方法
except AttributeError:
super().ftp_PASV(line) # 兼容性兜底
def main():
# 实例化权限管理器
authorizer = DummyAuthorizer()
# 设置匿名用户路径与全权限 (elradfmw)
authorizer.add_anonymous("/home/kali/Desktop/tmp/tmp", perm="elradfmw") # 假设 ftp 主目录在这里
handler = CpolarFTPHandler
handler.authorizer = authorizer
# 【关键设置】禁用 IP 来源检查
# 因为 cpolar 的控制流和数据流 IP 可能被识别为不一致,必须开启此项
handler.passive_promiscuous = True
# 设置被动模式本地监听端口范围 (对应隧道 B 的本地端)
handler.passive_ports = range(2120, 2121)
# 启动服务器,监听本地 2121 (对应隧道 A 的本地端)
server = FTPServer(('0.0.0.0', 2121), handler)
print("—————————————")
print("💎 穿透服务端已就绪 (Python 战神版)")
print("期待流量路径:外网 11667 -> 隧道 -> 本地 2120")
print("—————————————")
server.serve_forever()
if __name__ == "__main__":
main()
如果复现过程中遇到 2个域名不一样
上面代码请修改为下面的核心逻辑(因复现中又没遇到,故未做调试)
import socket
# 假设你的两个隧道域名不一样
CONTROL_DOMAIN = "3.tcp.cpolar.top" # 控制通道域名
DATA_DOMAIN = "4.tcp.vip.cpolar.cn" # 数据通道域名(假设不同)
def get_ip(domain):
"""自动获取域名的当前公网 IP"""
return socket.gethostbyname(domain)
class CpolarFTPHandler(FTPHandler):
def ftp_PASV(self, line):
# 自动获取数据通道当前的真实公网 IP
data_ip = get_ip(DATA_DOMAIN)
# 将 IP 格式化为 FTP 协议需要的逗号分隔形式
ip_parts = data_ip.replace('.', ',')
# 动态生成响应:使用数据通道的 IP 和 11667 端口 (45*256 + 147)
# 注意:这里的 P1, P2 需要根据 cpolar 隧道 B 实际分配的外网端口实时计算。
pasv_resp = f'227 Entering Passive Mode ({ip_parts},45,147).'
self.respond(pasv_resp)
self.handle_pasv()
三、访问FTP服务器
windows 直接访问 随便打开一个文件夹,输入 ftp://内网穿透真实ip:内网穿透控制通道端口 案例中是 ftp://8.148.71.245:11935/ 
下图为 FTP 跟目录中的文件,可见和访问结果一模一样。 
但是 cmd 访问会出错 
使用 FileZilla 可以完美解决
FileZilla 可以读取的原因系:编辑 -> 设置 -> 连接 -> FTP -> 被动模式,已经勾选了使用服务器的外部 IP 地址来代替。 
四、 (非必须)客户端:ftp_client.py (Windows 运行)
功能: 强制开启被动模式,避开 Windows 原生 FTP 工具的限制。
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# 运行环境: Windows / Linux / macOS (跨平台通用)
# 依赖说明: 仅使用 Python 标准库,无需额外安装依赖
# — 配置区:需与 cpolar 公网信息一致 —
import ftplib
import os
FTP_HOST = "8.148.71.245" # cpolar 公网 IP/域名
FTP_PORT = 11935 # cpolar 控制端映射的外网端口
FTP_USER = "anonymous" # 匿名用户
FTP_PASS = "" # 密码留空
def main():
try:
print(f"正在建立隧道连接: {FTP_HOST}:{FTP_PORT}…")
ftp = ftplib.FTP()
# 1. 建立控制通道连接
ftp.connect(FTP_HOST, FTP_PORT)
ftp.login(FTP_USER, FTP_PASS)
# 2. 【核心步骤】强制开启被动模式
# 解决 Windows CMD 无法列目录的根本手段:让客户端主动连服务器的数据端口
ftp.set_pasv(True)
print("✅ 登录成功!数据通道已就绪。")
while True:
# 获取用户指令
cmd = input("\\n[远程控制] ls(列表), get(下载), quit(退出): ").strip().split()
if not cmd: continue
action = cmd[0].lower()
if action == 'ls':
print("— 目录列表 —")
ftp.retrlines('LIST') # 接收数据流并打印
elif action == 'get' and len(cmd) > 1:
filename = cmd[1]
print(f"正在通过穿透隧道提取: {filename}…")
with open(filename, 'wb') as f:
# 获取文件二进制流
ftp.retrbinary(f'RETR {filename}', f.write)
print(f"✨ 下载成功!保存在: {os.path.abspath(filename)}")
elif action == 'quit':
ftp.quit()
print("👋 连接已关闭。")
break
except Exception as e:
print(f"\\n⚠️ 连接中断: {e}")
if __name__ == "__main__":
main()

五、 快速复现流程总结
1. 隧道配置 (cpolar)
- 隧道 A (控制):TCP, 本地端口 2121, 获取外网端口(如 11935)。
- 隧道 B (数据):TCP, 本地端口 2120, 获取外网随机端口(如 11667)。
2. 参数计算如果外网随机端口变更(假设变为
N
N
N):
- 计算端口高位:
P
1
=
N
/
/
256
P1 = N // 256
P1=N//256 - 计算端口低位:
P
2
=
N
%
256
P2 = N \\% 256
P2=N%256 - 修改服务端脚本:self.respond(‘227 … (IP,P1,P2).’)
3. 为什么不再使用 vsftpd?
传统软件很难在不修改源码的情况下实现 “伪造 227 响应” 。使用 Python pyftpdlib 可以精准控制协议握手的每一个字节,是内网穿透环境下的最优解。
cpolar 搭建隧道方法详见 cpolar 官网文档 ↩︎




