【Linux】内网穿透 FTP 终极复现手册2026 版--cpolar
- 前言
- [一、 核心背景与原理](#一、 核心背景与原理)
- [二、 服务端:ftp_server.py (Kali 运行)](#二、 服务端:ftp_server.py (Kali 运行))
-
- [如果复现过程中遇到 2个域名不一样](#如果复现过程中遇到 2个域名不一样)
- 三、访问FTP服务器
- [四、 (非必须)客户端:ftp_client.py (Windows 运行)](#四、 (非必须)客户端:ftp_client.py (Windows 运行))
- [五、 快速复现流程总结](#五、 快速复现流程总结)
前言
因为有个靶场所需需要搭建一个 FTP 服务器,又因为比较懒不高兴开 VPS 服务器去搭建 FTP ,用内网穿透工具 cpolar 搭建了2条 tcp 隧道,复现了 ftp 但是花了很长时间;为避免各位踩坑特此整理了一个笔记。
安全性问题最重要说三遍:
操作过程中请注意,FTP 主目录下不要放敏感文件!!!
操作过程中请注意,FTP 主目录下不要放敏感文件!!!
操作过程中请注意,FTP 主目录下不要放敏感文件!!!
一、 核心背景与原理
本方案 cpolar 需要搭建两条 tcp 隧道[1](#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
穿透痛点:
- 端口不一致:
cpolar随机分配的外网端口(如11667)与本地监听端口(如2120)无法对应。 - IP 伪装失效: 服务端默认会返回内网
IP,导致公网客户端连接失败。
解决方案: 通过 Python 脚本 "暴力劫持" FTP 的 PASV(被动模式)响应,强制将客户端引导至公网隧道出口。
IP漂移以及解决方法
- 复现过程中
cpolar映射到过2个域名:3.tcp.cpolar.top和3.tcp.vip.cpolar.cn; - 因域名不一致导致公网
IP也不一致,可自行ping命令得到真实IP; - 随着时间的推移,
IP也会变动,每次复现需要确定真实IP。
流量的一生:
- 户连
隧道A:11935→ 转发至本地2121(登录成功)。 - 用户发
ls指令 → 服务端"撒谎"说:请连隧道B:11667拿数据。 - 用户连
隧道B:11667→ 转发至本地2120(获取目录成功)。
二、 服务端:ftp_server.py (Kali 运行)
功能: 重写 PASV 逻辑,实现 "协议层端口重定向"。
python
#!/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个域名不一样
上面代码请修改为下面的核心逻辑(因复现中又没遇到,故未做调试)
python
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 工具的限制。
python
#!/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 可以精准控制协议握手的每一个字节,是内网穿透环境下的最优解。